2026-05-21 15:52:36 +08:00
|
|
|
|
import axios from 'axios'
|
|
|
|
|
|
import { isWebView2 } from './env'
|
|
|
|
|
|
|
|
|
|
|
|
// WebView2 自定义协议前缀
|
|
|
|
|
|
const WEBVIEW2_BASE = 'app://api/'
|
|
|
|
|
|
|
2026-05-21 16:45:56 +08:00
|
|
|
|
// 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}`
|
|
|
|
|
|
}
|
2026-05-21 15:52:36 +08:00
|
|
|
|
|
|
|
|
|
|
// ─── 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) => {
|
2026-05-21 16:45:56 +08:00
|
|
|
|
const payload = response.data as { success: boolean; data?: unknown; error?: string; message?: string }
|
2026-05-21 15:52:36 +08:00
|
|
|
|
if (payload?.success === false) {
|
2026-05-21 16:45:56 +08:00
|
|
|
|
return Promise.reject(new Error(payload.error ?? payload.message ?? '请求失败'))
|
2026-05-21 15:52:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
|
})
|
2026-05-21 16:45:56 +08:00
|
|
|
|
const payload = await res.json() as { success: boolean; data?: T; error?: string; message?: string }
|
2026-05-21 15:52:36 +08:00
|
|
|
|
if (payload?.success === false) {
|
2026-05-21 16:45:56 +08:00
|
|
|
|
throw new Error(payload.error ?? payload.message ?? '请求失败')
|
2026-05-21 15:52:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
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>
|
|
|
|
|
|
}
|