luoqian a68bb6c4b3 feat: 新增文件库功能,支持局域网文件浏览与媒体播放
后端:
- 新增 ManagedLibraryRoot / ManagedFileRecord 数据模型及 SQLite 迁移
- 新增文件库服务、端点服务及定时扫描后台任务
- 新增 REST API: drives、directories、roots CRUD、files 分页搜索、文本预览
- 新增文件流端点支持视频/音频流式传输
- 数据库切换为 SQLite,Kestrel 绑定 0.0.0.0 支持局域网访问

前端:
- 管理端:磁盘浏览、目录选择、根目录添加/启用/删除/扫描
- 客户端:根目录选择、文件搜索/筛选/分页、音视频播放、文本预览
- 全新响应式 UI(桌面+移动端),CSS 变量设计系统
- HTTP 客户端支持 Vite 开发代理与生产同源自动切换
- 移除 HTTPS 强制重定向以提升移动端视频流兼容性
2026-05-21 16:45:56 +08:00

95 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from 'axios'
import { isWebView2 } from './env'
// WebView2 自定义协议前缀
const WEBVIEW2_BASE = 'app://api/'
// Vite 开发页走 5206 APIAPI 托管前端时使用同源地址。
const isViteDevServer = window.location.port === '51552'
const HTTP_ORIGIN = isViteDevServer
? `${window.location.protocol}//${window.location.hostname || 'localhost'}:5206`
: window.location.origin
const HTTP_BASE = `${HTTP_ORIGIN}/api/`
export const apiOrigin = (): string => HTTP_ORIGIN
export const apiUrl = (path: string): string => {
if (/^https?:\/\//i.test(path)) return path
const normalized = path.startsWith('/') ? path : `/${path}`
return `${isWebView2() ? '' : HTTP_ORIGIN}${normalized}`
}
// ─── axios 实例 ────────────────────────────────────────────────────────────────
const http = axios.create({
headers: { 'Content-Type': 'application/json' },
})
// 请求拦截器:仅在浏览器环境下注入鉴权 Token
// WebView2 本地运行,不需要鉴权
http.interceptors.request.use((config) => {
if (!isWebView2()) {
const token = localStorage.getItem('authToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
}
return config
})
// 响应拦截器:统一解包 C# 返回的 { success, data/error } 结构
// C# BuildSuccessResponseBody 固定格式:{ "success": true, "data": ... }
// 错误格式:{ "success": false, "error": "..." }
// WebView2 桥接和 HTTP 两个环境返回结构相同,拦截器可统一处理
http.interceptors.response.use(
(response) => {
const payload = response.data as { success: boolean; data?: unknown; error?: string; message?: string }
if (payload?.success === false) {
return Promise.reject(new Error(payload.error ?? payload.message ?? '请求失败'))
}
return (payload?.data ?? payload) as never
},
(error) => {
const msg: string =
error.response?.data?.error ??
error.response?.data?.message ??
error.message ??
'网络错误'
return Promise.reject(new Error(msg))
},
)
// ─── 统一请求方法 ──────────────────────────────────────────────────────────────
interface RequestOptions {
method?: string
headers?: Record<string, string>
body?: unknown
}
export async function request<T = unknown>(endpoint: string, options: RequestOptions = {}): Promise<T> {
const url = (isWebView2() ? WEBVIEW2_BASE : HTTP_BASE) + endpoint
// WebView2直接走桥接 fetch桥接脚本已完整覆盖 window.fetch
if (isWebView2()) {
const res = await fetch(url, {
method: options.method ?? 'GET',
headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
})
const payload = await res.json() as { success: boolean; data?: T; error?: string; message?: string }
if (payload?.success === false) {
throw new Error(payload.error ?? payload.message ?? '请求失败')
}
return (payload?.data ?? payload) as T
}
// 普通浏览器:走 axios拦截器处理鉴权和响应解包
return http.request<T>({
url,
method: options.method ?? 'GET',
headers: options.headers,
data: options.body,
}) as Promise<T>
}