181 lines
5.7 KiB
TypeScript
181 lines
5.7 KiB
TypeScript
/**
|
||
* 地磅(db)模块 HTTP 客户端
|
||
* 协议:直接 JSON 响应(与原 simple-dibang-xiaoshou 一致,非门户 code/data 信封)
|
||
*/
|
||
import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useUserStore } from '@/store/modules/user'
|
||
import { fetchRefreshToken } from '@/api/auth'
|
||
|
||
type RetryableConfig = InternalAxiosRequestConfig & {
|
||
_retry?: boolean
|
||
/** 为 false 时不弹出全局错误提示(由调用方自行处理) */
|
||
showErrorMessage?: boolean
|
||
}
|
||
|
||
/** 地磅集合根路径(后端 @router.get("/") 注册,须带尾斜杠,否则 307 重定向会丢 Authorization) */
|
||
const DB_COLLECTION_ROOT = /^\/(plans|inventory|weighing)$/
|
||
|
||
function normalizeDbUrl(url?: string): string | undefined {
|
||
if (!url || url.endsWith('/')) return url
|
||
return DB_COLLECTION_ROOT.test(url) ? `${url}/` : url
|
||
}
|
||
|
||
const dbAxios = axios.create({
|
||
baseURL: '/api',
|
||
timeout: 60000,
|
||
headers: { 'Content-Type': 'application/json' }
|
||
})
|
||
|
||
let isRefreshing = false
|
||
let pendingRequests: Array<{
|
||
resolve: (token: string) => void
|
||
reject: (error: unknown) => void
|
||
}> = []
|
||
|
||
function subscribeTokenRefresh(
|
||
resolve: (token: string) => void,
|
||
reject: (error: unknown) => void
|
||
) {
|
||
pendingRequests.push({ resolve, reject })
|
||
}
|
||
|
||
function onRefreshed(newToken: string) {
|
||
pendingRequests.forEach(({ resolve }) => resolve(newToken))
|
||
pendingRequests = []
|
||
}
|
||
|
||
function onRefreshFailed(error: unknown) {
|
||
pendingRequests.forEach(({ reject }) => reject(error))
|
||
pendingRequests = []
|
||
}
|
||
|
||
/** 从 localStorage 兜底读取 accessToken(兼容 Pinia persist 的版本化 key) */
|
||
function readTokenFallback(): string {
|
||
if (typeof localStorage === 'undefined') return ''
|
||
try {
|
||
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 ''
|
||
}
|
||
|
||
dbAxios.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
||
if (config.url) {
|
||
config.url = normalizeDbUrl(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
|
||
})
|
||
|
||
dbAxios.interceptors.response.use(
|
||
(res) => res.data,
|
||
async (err) => {
|
||
const status = err.response?.status
|
||
const config = err.config as RetryableConfig | undefined
|
||
|
||
if (status === 401 && config && !config._retry) {
|
||
const userStore = useUserStore()
|
||
const storedRefreshToken = userStore.refreshToken
|
||
|
||
if (storedRefreshToken) {
|
||
if (isRefreshing) {
|
||
return new Promise((resolve, reject) => {
|
||
subscribeTokenRefresh(
|
||
(newToken: string) => {
|
||
if (typeof config.headers.set === 'function') {
|
||
config.headers.set('Authorization', `Bearer ${newToken}`)
|
||
} else {
|
||
config.headers.Authorization = `Bearer ${newToken}`
|
||
}
|
||
config._retry = true
|
||
resolve(dbAxios.request(config))
|
||
},
|
||
(refreshError: unknown) => reject(refreshError)
|
||
)
|
||
})
|
||
}
|
||
|
||
isRefreshing = true
|
||
config._retry = true
|
||
try {
|
||
const result = await fetchRefreshToken(storedRefreshToken)
|
||
userStore.setToken(result.token, result.refreshToken)
|
||
onRefreshed(result.token)
|
||
if (typeof config.headers.set === 'function') {
|
||
config.headers.set('Authorization', `Bearer ${result.token}`)
|
||
} else {
|
||
config.headers.Authorization = `Bearer ${result.token}`
|
||
}
|
||
return dbAxios.request(config)
|
||
} catch (refreshError) {
|
||
onRefreshFailed(refreshError)
|
||
await userStore.logOut()
|
||
return Promise.reject(refreshError)
|
||
} finally {
|
||
isRefreshing = false
|
||
}
|
||
}
|
||
|
||
await userStore.logOut()
|
||
return Promise.reject(err)
|
||
}
|
||
|
||
const showErrorMessage = config?.showErrorMessage !== false
|
||
if (showErrorMessage) {
|
||
const detail = err.response?.data?.detail
|
||
const message =
|
||
typeof detail === 'string'
|
||
? detail
|
||
: Array.isArray(detail)
|
||
? detail.map((x: { msg?: string }) => x.msg).filter(Boolean).join(';')
|
||
: err.response?.data?.msg
|
||
ElMessage.error(message || err.message || '网络异常')
|
||
}
|
||
return Promise.reject(err)
|
||
}
|
||
)
|
||
|
||
function request<T = unknown>(config: AxiosRequestConfig) {
|
||
return dbAxios.request<any, T>(config)
|
||
}
|
||
|
||
const dbRequest = {
|
||
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 dbRequest
|