Files
shanxia2.0/bx-mobile/src/api/bxClient.ts
T
2026-08-09 19:42:32 +08:00

102 lines
3.2 KiB
TypeScript

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<boolean> | null = null
async function tryRefresh(): Promise<boolean> {
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