153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
/**
|
||
* 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<T = unknown> {
|
||
code: number
|
||
msg: string
|
||
data?: T
|
||
}
|
||
|
||
/** 解析 bx / 运输模块响应体:兼容直出 JSON 与 { code, data } 信封 */
|
||
export function pickBxData<T>(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<T> & Record<string, unknown>
|
||
if ('data' in obj && obj.data !== undefined) {
|
||
return obj.data as T
|
||
}
|
||
return res as T
|
||
}
|
||
|
||
/** 解析列表类响应,保证始终返回数组 */
|
||
export function pickBxList<T>(res: unknown): T[] {
|
||
const payload = pickBxData<T[] | { list?: T[]; rows?: T[] }>(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<T = unknown>(config: AxiosRequestConfig) {
|
||
return bxAxios.request<any, BxApiResponse<T>>(config)
|
||
}
|
||
|
||
const bxRequest = {
|
||
get<T = unknown>(url: string, config?: AxiosRequestConfig) {
|
||
return request<T>({ ...config, url, method: 'GET' })
|
||
},
|
||
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
||
return request<T>({ ...config, url, method: 'POST', data })
|
||
},
|
||
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
||
return request<T>({ ...config, url, method: 'PUT', data })
|
||
},
|
||
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
||
return request<T>({ ...config, url, method: 'PATCH', data })
|
||
},
|
||
delete<T = unknown>(url: string, config?: AxiosRequestConfig) {
|
||
return request<T>({ ...config, url, method: 'DELETE' })
|
||
}
|
||
}
|
||
|
||
export default bxRequest
|