From ced65527d503b2170d4831bcb5e833f7249919a2 Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Tue, 30 Dec 2025 01:25:17 -0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/request/cancel.ts | 58 ++++++++++ src/utils/request/core.ts | 208 ++++++++++++++++++++++++++++++++++++ src/utils/request/index.ts | 16 +++ 3 files changed, 282 insertions(+) create mode 100644 src/utils/request/cancel.ts create mode 100644 src/utils/request/core.ts create mode 100644 src/utils/request/index.ts diff --git a/src/utils/request/cancel.ts b/src/utils/request/cancel.ts new file mode 100644 index 0000000..f63a223 --- /dev/null +++ b/src/utils/request/cancel.ts @@ -0,0 +1,58 @@ +import type { AxiosRequestConfig, Canceler } from 'axios' +import axios from 'axios' + +// Used to store the identification and cancellation function of each request +let pendingMap = new Map() + +const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&') + +export class AxiosCanceler { + /** + * Add request + * @param {Object} config + */ + addPending(config: AxiosRequestConfig): void { + this.removePending(config) + const url = getPendingUrl(config) + config.cancelToken = config.cancelToken + || new axios.CancelToken((cancel) => { + if (!pendingMap.has(url)) { + // If there is no current request in pending, add it + pendingMap.set(url, cancel) + } + }) + } + + /** + * @description: Clear all pending + */ + removeAllPending(): void { + pendingMap.forEach((cancel) => { + cancel?.() + }) + pendingMap.clear() + } + + /** + * Removal request + * @param {Object} config + */ + removePending(config: AxiosRequestConfig): void { + const url = getPendingUrl(config) + + if (pendingMap.has(url)) { + // If there is a current request identifier in pending, + // the current request needs to be cancelled and removed + const cancel = pendingMap.get(url) + cancel && cancel(url) + pendingMap.delete(url) + } + } + + /** + * @description: reset + */ + reset(): void { + pendingMap = new Map() + } +} diff --git a/src/utils/request/core.ts b/src/utils/request/core.ts new file mode 100644 index 0000000..3779cb4 --- /dev/null +++ b/src/utils/request/core.ts @@ -0,0 +1,208 @@ +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import axios from 'axios' +import { AxiosCanceler } from './cancel' +import router from '@/router' +import { toast } from 'vue-sonner' +import { HiAesUtil } from './HiAesUtil.ts' +const encryptionKey = 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx' +function redirectLogin() { + localStorage.removeItem('Authorization') + router.push('/') +} +export interface ExtraConfig { + /** + * 默认值 1 错误等级 0-忽略,1-warning,2-error + */ + errorLevel?: 0 | 1 | 2 + /** + * 默认true 是否携带token + */ + withToken?: boolean + /** + * 默认true 是否处理响应 + */ + handleResponse?: boolean + /** + * 默认false 是否取消重复请求 + */ + cancelRepetition?: boolean + /** + * 默认false 是否返回axios完整响应 + */ + originResponseData?: boolean + /** + * 默认token 存储key + */ + tokenKey?: string + /** + * 获取token的方法 + */ + getToken?: () => string + /** + * 自定义header方法,此方法返回的header会覆盖默认的header + */ + formatHeader?: (header: Record) => Record +} + +export interface RequestConfig extends AxiosRequestConfig { + extraConfig?: ExtraConfig +} + +interface ResponseType extends AxiosResponse { + config: RequestConfig +} + +export default class Request { + public axiosInstance: AxiosInstance + + private config: RequestConfig + + constructor(config: RequestConfig) { + this.config = config + this.axiosInstance = axios.create(config) + this.init() + } + + static defaultConfig: Required = { + errorLevel: 2, + withToken: true, + handleResponse: true, + cancelRepetition: false, + originResponseData: false, + tokenKey: 'token', + formatHeader: (headers) => { + return headers + }, + getToken: () => '', + } + + private errorReport(lv: number, message: string) { + toast(message) + } + + private init() { + const axiosCanceler = new AxiosCanceler() + + this.axiosInstance.interceptors.request.use( + (config: RequestConfig) => { + const mergeExtraConfig = { + ...Request.defaultConfig, + ...this.config.extraConfig, + ...config.extraConfig, + } + + if (config.data && !(config.data instanceof FormData)) { + const plainText = JSON.stringify(config.data) + // 加密后,config.data 会变成 { data: '...', time: '...' } + config.data = HiAesUtil.encryptData(plainText, encryptionKey) + } + + config.headers = mergeExtraConfig.formatHeader({ + ...this.config.headers, + ...config.headers, + ...(mergeExtraConfig.withToken && { + [mergeExtraConfig.tokenKey]: mergeExtraConfig.getToken(), + }), + }) + config.extraConfig = mergeExtraConfig + if (mergeExtraConfig.cancelRepetition) { + axiosCanceler.addPending(config) + } + return config + }, + (error) => { + this.errorReport(error?.config.extraConfig.errorLevel, error) + return Promise.reject(error) + }, + ) + + this.axiosInstance.interceptors.response.use( + (response: ResponseType) => { + const { data, config } = response + let responseData = response.data.data + + // 假设后端返回格式为 { data: "base64...", time: "..." } + if (responseData && responseData.data && responseData.time) { + try { + const decryptedStr = HiAesUtil.decryptData( + responseData.data, + responseData.time, + encryptionKey, + ) + // 解密后转化为 JSON 对象供后续业务使用 + responseData = JSON.parse(decryptedStr) + } catch (e) { + console.error('解密失败:', e) + return Promise.reject({ message: '数据解密异常' }) + } + } + axiosCanceler.removePending(config) + if (data.code == 401) { + redirectLogin() + return + } + const resData = config.extraConfig?.originResponseData ? response : responseData + if (config.extraConfig?.handleResponse) { + if (data.code === 200) { + return resData + } + this.errorReport( + config.extraConfig.errorLevel ?? 2, + response.data?.msg ?? data?.error ?? '未知错误', + ) + return Promise.reject({ + ...data, + message: response.data?.msg ?? data?.error ?? '未知错误', + }) + } else { + return resData + } + }, + (error) => { + const status = error?.response?.status + const code = error?.code + let message = error?.message + if (status === 401) { + // message = '未登录或登录状态失效' + redirectLogin() + return + } + if (code === 'ECONNABORTED') { + message = '网络环境太差,请求超时' + } else if (code === 'Network Error' || message === 'Network Error') { + if (error.response) { + message = `${error.response.status}:network连接失败,请求中断` + } else { + message = '网络好像出现问题了' + } + } + if (error.__CANCEL__) { + console.warn('request canceled', error?.message) + } else { + this.errorReport(error?.config?.extraConfig?.errorLevel, message) + } + return Promise.reject(error) + }, + ) + } + + public get(url: string, params?: Record, config?: RequestConfig): Promise { + return this.axiosInstance.get(url, { ...config, params }) + } + + public post(url: string, data?: D, config?: RequestConfig): Promise { + return this.axiosInstance.post(url, data, { ...config }) + } + + public put(url: string, data?: D, config?: RequestConfig): Promise { + return this.axiosInstance.put(url, data, { ...config }) + } + + public patch(url: string, data?: D, config?: RequestConfig): Promise { + return this.axiosInstance.patch(url, data, { ...config }) + } + + public delete(url: string, params?: D, config?: RequestConfig): Promise { + return this.axiosInstance.delete(url, { ...config, params }) + } +} diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts new file mode 100644 index 0000000..eafe3c5 --- /dev/null +++ b/src/utils/request/index.ts @@ -0,0 +1,16 @@ +import Request from './core' +export * from './core' +const baseUrl = import.meta.env.VITE_APP_BASE_URL + +const request = new Request({ + baseURL: baseUrl, + timeout: 6000, + headers: {}, + extraConfig: { + /** 这里是核心配置,一般不需要再去修改request/core.ts */ + tokenKey: 'Authorization', + getToken: () => localStorage.getItem('Authorization') || '', + }, +}) + +export default request