后端: - 新增 ManagedLibraryRoot / ManagedFileRecord 数据模型及 SQLite 迁移 - 新增文件库服务、端点服务及定时扫描后台任务 - 新增 REST API: drives、directories、roots CRUD、files 分页搜索、文本预览 - 新增文件流端点支持视频/音频流式传输 - 数据库切换为 SQLite,Kestrel 绑定 0.0.0.0 支持局域网访问 前端: - 管理端:磁盘浏览、目录选择、根目录添加/启用/删除/扫描 - 客户端:根目录选择、文件搜索/筛选/分页、音视频播放、文本预览 - 全新响应式 UI(桌面+移动端),CSS 变量设计系统 - HTTP 客户端支持 Vite 开发代理与生产同源自动切换 - 移除 HTTPS 强制重定向以提升移动端视频流兼容性
95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
import axios from 'axios'
|
||
import { isWebView2 } from './env'
|
||
|
||
// WebView2 自定义协议前缀
|
||
const WEBVIEW2_BASE = 'app://api/'
|
||
|
||
// Vite 开发页走 5206 API;API 托管前端时使用同源地址。
|
||
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>
|
||
}
|