102 lines
3.2 KiB
TypeScript
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
|