2026-07-10 16:45:06 +08:00
|
|
|
|
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
|
2026-08-03 17:24:45 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 用户操作分布:哪些模块路径算"业务操作"(用于看板统计)
|
|
|
|
|
|
* 请求命中这些路径时,会自动调用 record_visit 记录一次(同一模块 60 秒内去重)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const VISIT_TRACKED_PREFIXES: Array<[string, string]> = [
|
|
|
|
|
|
['/fine-tune', 'fine-tune'],
|
|
|
|
|
|
['/model-eval', 'model-eval'],
|
|
|
|
|
|
['/model-inference', 'model-inference'],
|
2026-08-04 17:02:57 +08:00
|
|
|
|
['/model-compare', 'model-inference'],
|
2026-08-03 17:24:45 +08:00
|
|
|
|
['/data-process', 'data-process'],
|
|
|
|
|
|
['/data-convert', 'data-convert'],
|
|
|
|
|
|
['/model-manage', 'model-manage'],
|
|
|
|
|
|
['/dataset-manage', 'dataset'],
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
function trackVisit(url: string | undefined) {
|
|
|
|
|
|
if (!url) return
|
|
|
|
|
|
for (const [prefix, module] of VISIT_TRACKED_PREFIXES) {
|
|
|
|
|
|
if (url.includes(prefix)) {
|
|
|
|
|
|
const key = `visit:${module}`
|
|
|
|
|
|
const last = Number(sessionStorage.getItem(key) || 0)
|
|
|
|
|
|
if (Date.now() - last < 60000) return // 60 秒内去重
|
|
|
|
|
|
sessionStorage.setItem(key, String(Date.now()))
|
|
|
|
|
|
// fire-and-forget 调用后端记录接口
|
|
|
|
|
|
import('./modules/audit-visit').then(({ recordModuleVisit }) => {
|
|
|
|
|
|
recordModuleVisit(module, url).catch(() => { /* ignore */ })
|
|
|
|
|
|
}).catch(() => { /* ignore */ })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 后端统一响应格式
|
|
|
|
|
|
* code === 0 表示成功,data 为业务数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ApiResult<T = any> {
|
|
|
|
|
|
code: number
|
|
|
|
|
|
message?: string
|
|
|
|
|
|
data: T
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const service: AxiosInstance = axios.create({
|
2026-07-21 10:09:36 +08:00
|
|
|
|
// Use a relative path; Vite proxies /modelTF to http://localhost:17861 in local development.
|
|
|
|
|
|
baseURL: '/modelTF',
|
2026-08-03 16:20:21 +08:00
|
|
|
|
timeout: 120000,
|
2026-07-10 16:45:06 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-03 09:34:08 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 从 localStorage 取当前用户 token(登录时后端返回 platform-token-{user_id})。
|
|
|
|
|
|
* 后端鉴权中间件依赖此 header 解析当前用户身份。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getAuthToken(): string | null {
|
|
|
|
|
|
const USER_STORAGE_KEY = 'currentUser'
|
|
|
|
|
|
const raw = localStorage.getItem(USER_STORAGE_KEY)
|
|
|
|
|
|
if (raw) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const user = JSON.parse(raw)
|
|
|
|
|
|
// 后端 login 返回的 token 格式为 platform-token-{user.id}
|
|
|
|
|
|
if (user?.id) return `platform-token-${user.id}`
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* ignore */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 兼容改造前 admin 会话
|
|
|
|
|
|
if (localStorage.getItem('username') === 'admin') return 'platform-token-admin'
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 09:49:48 +08:00
|
|
|
|
function getActiveTenantId(): string | null {
|
|
|
|
|
|
return localStorage.getItem('activeTenantId')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Headers shared by Axios and raw fetch requests such as SSE streaming. */
|
|
|
|
|
|
export function getAuthHeaders(): Record<string, string> {
|
|
|
|
|
|
const headers: Record<string, string> = {}
|
|
|
|
|
|
const token = getAuthToken()
|
|
|
|
|
|
if (token) headers.Authorization = `Bearer ${token}`
|
|
|
|
|
|
const tenantId = getActiveTenantId()
|
|
|
|
|
|
if (tenantId) headers['X-Tenant-ID'] = tenantId
|
|
|
|
|
|
return headers
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 09:34:08 +08:00
|
|
|
|
// 请求拦截器:注入 Authorization header
|
2026-07-10 16:45:06 +08:00
|
|
|
|
service.interceptors.request.use(
|
2026-08-03 09:34:08 +08:00
|
|
|
|
(config) => {
|
2026-08-21 09:49:48 +08:00
|
|
|
|
const authHeaders = getAuthHeaders()
|
|
|
|
|
|
if (Object.keys(authHeaders).length) {
|
2026-08-03 09:34:08 +08:00
|
|
|
|
config.headers = config.headers || {}
|
2026-08-21 09:49:48 +08:00
|
|
|
|
Object.assign(config.headers, authHeaders)
|
2026-08-03 09:34:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
return config
|
|
|
|
|
|
},
|
2026-07-10 16:45:06 +08:00
|
|
|
|
(error) => Promise.reject(error),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 响应拦截器:统一解包 { code, data, message }
|
|
|
|
|
|
service.interceptors.response.use(
|
|
|
|
|
|
(response) => {
|
|
|
|
|
|
const res = response.data as ApiResult
|
|
|
|
|
|
// 二进制流等非 JSON 响应直接返回
|
|
|
|
|
|
if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') {
|
|
|
|
|
|
return response
|
|
|
|
|
|
}
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
return res.data
|
|
|
|
|
|
}
|
|
|
|
|
|
// 业务错误
|
|
|
|
|
|
const message = res.message || '请求失败'
|
|
|
|
|
|
ElMessage.error(message)
|
|
|
|
|
|
return Promise.reject(new Error(message))
|
|
|
|
|
|
},
|
|
|
|
|
|
(error) => {
|
2026-07-21 11:06:34 +08:00
|
|
|
|
const detail = error.response?.data?.detail
|
|
|
|
|
|
const message = detail?.message || error.response?.data?.message || detail || error.message || '网络异常'
|
2026-07-10 16:45:06 +08:00
|
|
|
|
ElMessage.error(message)
|
|
|
|
|
|
return Promise.reject(error)
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
/** GET 请求,返回已解包的 data */
|
|
|
|
|
|
export function get<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> {
|
|
|
|
|
|
return service.get(url, { params, ...config }) as unknown as Promise<T>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** POST 请求 */
|
|
|
|
|
|
export function post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
|
|
|
|
|
return service.post(url, data, config) as unknown as Promise<T>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** PUT 请求 */
|
|
|
|
|
|
export function put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
|
|
|
|
|
return service.put(url, data, config) as unknown as Promise<T>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** DELETE 请求 */
|
|
|
|
|
|
export function del<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> {
|
|
|
|
|
|
return service.delete(url, { params, ...config }) as unknown as Promise<T>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default service
|