import axios, { type AxiosInstance, type AxiosRequestConfig, type CreateAxiosDefaults } from 'axios' import { useAccountStore } from '@/stores' import { eventBus, AppEvents } from '@/utils' import type { ApiResponse, InternalRequestConfig, RequestErrorEvent } from './types' /** * 替换 URL 中的路径参数占位符 * * @example * resolvePathParams('/user/:id/posts', { id: 1 }) // → '/user/1/posts' */ function resolvePathParams(url: string, pathParams?: Record): string { if (!pathParams) return url return url.replace(/:(\w+)/g, (_, key) => { const value = pathParams[key] if (value === undefined) { throw new Error(`路径参数 "${key}" 缺失`) } return String(value) }) } /** * 基于 axios 封装的网络请求客户端 * * 不依赖任何 UI 库,错误通过事件总线发布,由外部订阅处理。 * * @example * ```ts * // GET 请求 * const res = await requestClient.get('/users', { page: 1 }) * * // POST 请求 * const res = await requestClient.post('/auth/login', { username, password }) * ``` */ class RequestClient { /** axios 实例 */ private instance: AxiosInstance constructor(config?: CreateAxiosDefaults) { this.instance = axios.create({ baseURL: '/api', timeout: 15000, headers: { 'Content-Type': 'application/json' }, ...config }) this.setupInterceptors() } /** 注册请求和响应拦截器 */ private setupInterceptors(): void { // === 请求拦截器 === this.instance.interceptors.request.use( (config) => { const reqConfig = config as InternalRequestConfig // 替换路径参数 :param → 实际值 if (reqConfig.pathParams && config.url) { config.url = resolvePathParams(config.url, reqConfig.pathParams) } // 跳过鉴权时不注入 token if (!reqConfig?.skipAuth) { const accountStore = useAccountStore() if (accountStore.token) { config.headers.Authorization = `Bearer ${accountStore.token}` } } return config }, (error) => { return Promise.reject(error) } ) // === 响应拦截器 === // 拦截器内部将 axiosResponse 解包为 ApiResponse, // 返回类型与 axios 类型签名不匹配,但公共方法已声明正确返回类型。 this.instance.interceptors.response.use( // @ts-expect-error 拦截器解包 axiosResponse → ApiResponse (response) => { const data = response.data as ApiResponse // 业务成功 if (data.code === 0) { return data } // 业务错误事件载荷 const errorEvent: RequestErrorEvent = { type: 'business', code: data.code, message: data.message || '请求失败' } // 401 → 额外发布未授权事件,解耦路由 if (data.code === 401) { eventBus.emit(AppEvents.UNAUTHORIZED) } // 发布请求错误事件,由 errorUI 模块订阅展示 eventBus.emit(AppEvents.REQUEST_ERROR, errorEvent) return Promise.reject(data) }, (error) => { let errorEvent: RequestErrorEvent if (error.response?.status === 401) { eventBus.emit(AppEvents.UNAUTHORIZED) errorEvent = { type: 'http', code: 401, message: '登录已过期,请重新登录' } } else if (!error.response) { errorEvent = { type: 'network', message: '网络异常,请检查网络连接' } } else { errorEvent = { type: 'http', code: error.response.status, message: `请求错误 (${error.response.status})` } } eventBus.emit(AppEvents.REQUEST_ERROR, errorEvent) return Promise.reject(error) } ) } /** * 通用请求方法 * * 适用于路径参数、混合参数等复杂场景。 * * @example * ```ts * // 路径参数 + 查询参数 * requestClient.request({ * method: 'GET', * url: '/user/:id/posts', * params: { page: 1 }, * pathParams: { id: 1 } * }) * * // 路径参数 + 请求体 * requestClient.request({ * method: 'POST', * url: '/user/:id/post', * data: body, * pathParams: { id: 1 } * }) * * // 自定义请求头 + 跳过鉴权 * requestClient.request({ * method: 'POST', * url: '/public/upload', * data: formData, * headers: { 'Content-Type': 'multipart/form-data' }, * requestOptions: { skipAuth: true } * }) * ``` */ request(config: InternalRequestConfig): Promise> { return this.instance.request(config) } /** * GET 请求 * @param url - 请求地址 * @param params - 查询参数 * @param config - 额外配置(pathParams、requestOptions 等) */ get( url: string, params?: Record, config?: InternalRequestConfig ): Promise> { return this.instance.get(url, { params, ...config }) } /** * POST 请求 * @param url - 请求地址 * @param data - 请求体 * @param config - 额外配置(pathParams、requestOptions 等) */ post( url: string, data?: unknown, config?: InternalRequestConfig ): Promise> { return this.instance.post(url, data, config) } /** * PUT 请求 * @param url - 请求地址 * @param data - 请求体 * @param config - 额外配置(pathParams、requestOptions 等) */ put( url: string, data?: unknown, config?: InternalRequestConfig ): Promise> { return this.instance.put(url, data, config) } /** * DELETE 请求 * @param url - 请求地址 * @param config - 额外配置(pathParams、requestOptions 等) */ delete(url: string, config?: InternalRequestConfig): Promise> { return this.instance.delete(url, config) } /** * PATCH 请求 * @param url - 请求地址 * @param data - 请求体 * @param config - 额外配置(pathParams、requestOptions 等) */ patch( url: string, data?: unknown, config?: InternalRequestConfig ): Promise> { return this.instance.patch(url, data, config) } } /** 请求客户端单例 */ export const requestClient = new RequestClient()