Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | 5x 5x 5x 5x 5x 1x 1x 1x 1x 5x 4x 2x 2x | // 统一 HTTP client(axios)。
// 后端约定:业务返回体固定为 { code, msg, data },成功 code=200;
// 仅 Spring Security 层未认证会返回真实 401。
import axios from 'axios'
import { userManager, signinRedirect } from '@/utils/oidc'
interface ApiResult<T> {
code: number
msg: string
data: T
}
/** 错误分类:界面据此决定文案与是否可重试(认证类跳登录,其余可重试)。 */
export type ApiErrorKind = 'auth' | 'timeout' | 'network' | 'server' | 'business'
/**
* 统一错误类型。原先一律 `new Error(string)`,页面无法区分 401 / 超时 / 业务失败,
* 只能把后端 msg 原样抛给用户。
*/
export class ApiError extends Error {
readonly kind: ApiErrorKind
readonly httpStatus?: number
readonly code?: number
constructor(
message: string,
kind: ApiErrorKind,
extra: { httpStatus?: number; code?: number } = {}
) {
super(message)
this.name = 'ApiError'
this.kind = kind
this.httpStatus = extra.httpStatus
this.code = extra.code
}
}
const instance = axios.create({
baseURL: '/csf/api',
// 不加超时:后端 hang 住时页面 loading 会永久转圈
timeout: 15000
})
instance.interceptors.request.use(async (config) => {
const user = await userManager.getUser()
if (user && !user.expired) {
config.headers.Authorization = `Bearer ${user.access_token}`
}
return config
})
// 防止多个并发请求同时 401 触发多次跳转
let redirecting = false
instance.interceptors.response.use(
(response) => response,
(error) => {
const status: number | undefined = error.response?.status
if (status === 401) {
if (!redirecting && window.location.pathname !== '/auth/callback') {
redirecting = true
// 跳转失败(IdP 不可达)时必须复位,否则此后再也不重定向,页面卡死在错误态
signinRedirect(window.location.pathname + window.location.search).catch(() => {
redirecting = false
})
}
throw new ApiError('未登录或登录已过期', 'auth', { httpStatus: 401 })
}
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
throw new ApiError('请求超时,请检查网络后重试', 'timeout')
}
if (!error.response) {
throw new ApiError('网络连接失败,请检查网络', 'network')
}
throw new ApiError(error.message || '请求失败', 'server', { httpStatus: status })
}
throw new ApiError(error?.message || '网络请求失败', 'network')
}
)
// 解包 { code, msg, data }:非 200 抛业务错误,成功只返回 data
export function unwrap<T>(body: ApiResult<T>): T {
if (!body) throw new ApiError('服务返回为空', 'server')
if (body.code !== 200) {
throw new ApiError(body.msg || '请求失败', 'business', { code: body.code })
}
return body.data
}
export async function get<T>(path: string): Promise<T> {
const res = await instance.get<ApiResult<T>>(path)
return unwrap(res.data)
}
export async function post<T>(path: string, body?: unknown): Promise<T> {
const res = await instance.post<ApiResult<T>>(path, body)
return unwrap(res.data)
}
|