/** * bx 报销模块 HTTP 客户端 * 协议:code === 0、msg、data.rows(与门户 request code===200 分离) */ import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/store/modules/user' export interface BxApiResponse { code: number msg: string data?: T } /** 解析 bx / 运输模块响应体:兼容直出 JSON 与 { code, data } 信封 */ export function pickBxData(res: unknown): T { if (res == null) return res as T if (Array.isArray(res)) return res as T if (typeof res !== 'object') return res as T const obj = res as BxApiResponse & Record if ('data' in obj && obj.data !== undefined) { return obj.data as T } return res as T } /** 解析列表类响应,保证始终返回数组 */ export function pickBxList(res: unknown): T[] { const payload = pickBxData(res) if (Array.isArray(payload)) return payload if (payload && typeof payload === 'object') { const record = payload as { list?: T[]; rows?: T[] } if (Array.isArray(record.list)) return record.list if (Array.isArray(record.rows)) return record.rows } return [] } const bxAxios = axios.create({ baseURL: '/api', timeout: 60000, headers: { 'Content-Type': 'application/json' } }) /** FastAPI @router.get("/") 集合根须带尾斜杠,否则 307 重定向到绝对 URL 会丢 Authorization */ const BX_COLLECTION_ROOT = /^\/(transport\/(order|plan|freight)|reimburses|advances|payments|workflows|expense-items)$/ function normalizeBxUrl(url?: string): string | undefined { if (!url || url.endsWith('/')) return url return BX_COLLECTION_ROOT.test(url) ? `${url}/` : url } /** 从 localStorage 兜底读取 accessToken(兼容 Pinia persist 的版本化 key) */ function readTokenFallback(): string { if (typeof localStorage === 'undefined') return '' try { // 尝试 Pinia persist 的版本化 key(如 sys-v3.0.2-user) for (const k of Object.keys(localStorage)) { if (!k.startsWith('sys-') || !k.includes('-user')) continue const raw = localStorage.getItem(k) if (!raw) continue const parsed = JSON.parse(raw) as { accessToken?: unknown } const token = parsed?.accessToken if (typeof token === 'string' && token.trim()) return token.trim() } } catch { // ignore } return '' } bxAxios.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { if (config.url) { config.url = normalizeBxUrl(config.url) } const store = useUserStore() let token = store.accessToken as unknown as string if (!token) { token = readTokenFallback() } if (token) { if (typeof config.headers.set === 'function') { config.headers.set('Authorization', `Bearer ${token}`) } else { config.headers.Authorization = `Bearer ${token}` } } return config }) bxAxios.interceptors.response.use( async (res) => { if (res.config.responseType === 'blob') { const contentType = String(res.headers['content-type'] || '') if (contentType.includes('application/json')) { const text = await (res.data as Blob).text() try { const json = JSON.parse(text) as BxApiResponse if (json.code !== 0) { ElMessage.error(json.msg || '请求失败') return Promise.reject(new Error(json.msg)) } } catch { /* 非 JSON blob 直出 */ } } return res.data } const body = res.data as BxApiResponse if (body?.code !== undefined && body.code !== 0) { ElMessage.error(body.msg || '请求失败') return Promise.reject(new Error(body.msg || '请求失败')) } return body }, async (err) => { if (err.response?.status === 401) { const userStore = useUserStore() await userStore.logOut() } else { const detail = err.response?.data?.detail || err.response?.data?.msg ElMessage.error(detail || err.message || '网络异常') } return Promise.reject(err) } ) function request(config: AxiosRequestConfig) { return bxAxios.request>(config) } const bxRequest = { get(url: string, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'GET' }) }, post(url: string, data?: unknown, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'POST', data }) }, put(url: string, data?: unknown, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'PUT', data }) }, patch(url: string, data?: unknown, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'PATCH', data }) }, delete(url: string, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'DELETE' }) } } export default bxRequest