import axios, { type InternalAxiosRequestConfig } from 'axios' import { showToast } from 'vant' import { portalAuthApi } from './portalAuth' export class BizError extends Error { code?: number constructor(message: string, code?: number) { super(message) this.code = code this.name = 'BizError' } } /** * FastAPI 以 @router.get("/") + prefix 注册的集合根须带尾斜杠,否则 307 会丢 Authorization。 * 发票夹路由是无尾斜杠的 /invoices、/invoice-ledgers、/invoice-wallets,切勿追加 `/`。 * 与 web-admin/src/utils/http/bx.ts 保持一致。 */ const BX_COLLECTION_ROOT = /^\/(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 } const bxClient = axios.create({ baseURL: import.meta.env.VITE_API_URL || '/api', timeout: 0, headers: { 'Content-Type': 'application/json' }, }) let refreshing: Promise | null = null async function tryRefresh(): Promise { const rt = localStorage.getItem('refreshToken') if (!rt) return false try { const res: any = await portalAuthApi.refreshToken(rt) const token = res?.data?.token const refreshToken = res?.data?.refreshToken if (!token) return false localStorage.setItem('accessToken', token) if (refreshToken) localStorage.setItem('refreshToken', refreshToken) return true } catch { return false } } bxClient.interceptors.request.use((config: InternalAxiosRequestConfig) => { if (config.url) config.url = normalizeBxUrl(config.url) if (config.data instanceof FormData) { delete (config.headers as any)['Content-Type'] } const token = localStorage.getItem('accessToken') if (token) config.headers.Authorization = `Bearer ${token}` return config }) bxClient.interceptors.response.use( (res) => { if (res.config.responseType === 'blob') return res.data const body = res.data if (body?.code !== undefined && body.code !== 0) { showToast(body.msg || '请求失败') return Promise.reject(new BizError(body.msg || '请求失败', body.code)) } return body }, async (err) => { const original = err.config if (err.response?.status === 401 && original && !(original as any)._retry) { ;(original as any)._retry = true if (!refreshing) { refreshing = tryRefresh().finally(() => { refreshing = null }) } const ok = await refreshing if (ok) { original.headers = original.headers || {} original.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}` return bxClient.request(original) } localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') showToast('登录已过期,请重新登录') if (window.location.pathname !== '/login') { window.location.href = '/login' } } else if (err.response?.status !== 401) { const msg = err.response?.data?.msg || err.response?.data?.detail || err.message || '网络异常' showToast(msg) } return Promise.reject(err) }, ) export default bxClient