feat(bx-mobile): add bxClient and retarget business APIs to shanxia2.0

Remove legacy bx user/role/menu/auth APIs. Wire auth store to portal
temporarily so the app still typechecks; IAM admin pages use local stubs
until Task 6 drops their routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-09 16:06:12 +08:00
1 parent c7a8930ad6
commit 3f1faeae34
9 files changed
+340 -262

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
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'
}
}
const BX_COLLECTION_ROOT =
/^\/(reimburses|advances|payments|workflows|expense-items|invoices|invoice-wallets|invoice-ledgers)$/
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