一.index.ts
import type { RequestOptions, Result } from '#/axios';
import type { AxiosResponse, AxiosInstance } from 'axios';
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
import axios from 'axios';
import { clone } from 'lodash-es';
import { ContentTypeEnum, RequestEnum } from '@/settings/enum';
import { getEnvSetting } from '@/utils/env';
import { useI18n } from '@/hooks/web/useI18n';
import { useMessage } from '@/utils/lib/msgbox';
import { useErrorLogStoreWithOut } from '@/store/modules/errorLog';
import { useUserStoreWithOut } from '@/store/modules/user';
import { deepMerge, setObjToUrlParams, isString } from '@/utils';
import { AxiosRetry } from '@/utils/http/axiosRetry';
import { VAxios } from './Axios';
import { formatRequestDate, joinTimestamp, isSuccess, checkStatus } from './helper';
import { useLocale } from '@/locales/useLocale';
export * from './helper';
const { createMessage, createMsgbox } = useMessage();
const transform: AxiosTransform = {
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { t } = useI18n();
const { isTransformResponse, isReturnNativeResponse } = options;
if (isReturnNativeResponse) return res;
if (!isTransformResponse) return res.data;
const { data } = res;
const defMsg = t('sys.api.apiRequestFailed');
if (!data) throw new Error(defMsg);
const { code, data: result, msg: message = defMsg } = data;
const hasSuccess = data && Reflect.has(data, 'code') && isSuccess(code);
if (hasSuccess) return result;
checkStatus(code, message, options.errorMessageMode);
throw new Error(message);
},
beforeRequestHook: (config, options) => {
const { apiUrl, joinParamsToUrl, formatDate, joinTime = true } = options;
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const params = config.params || {};
const data = config.data || false;
formatDate && data && !isString(data) && formatRequestDate(data);
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isString(params)) {
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
} else {
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
config.params = undefined;
}
} else {
if (!isString(params)) {
formatDate && formatRequestDate(params);
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length || config.data instanceof FormData)
) {
config.data = data;
config.params = params;
} else {
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data),
);
}
} else {
config.url = config.url + params;
config.params = undefined;
}
if (config.data instanceof FormData) {
config.headers = {
...config.headers,
'Content-Type': ContentTypeEnum.FORM_DATA,
};
}
}
return config;
},
requestInterceptors: (config, options) => {
const { getToken: token, getUserInfo } = useUserStoreWithOut();
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
(config as Recordable).headers.Authorization = options.authenticationScheme
? `${options.authenticationScheme} ${token}`
: token;
}
const { getLocale } = useLocale();
(config as Recordable).headers['Accept-Language'] =
{
'zh-cn': 'zh-CN',
en: 'en-US',
}[getLocale.value] || getLocale.value;
if (getUserInfo) {
const Header = JSON.stringify({
id: getUserInfo?.empNumber,
timestamp: Date.now(),
isExternal: false,
});
(config as Recordable).headers['x-byd-header'] = Header;
}
return config;
},
responseInterceptors: (res: AxiosResponse<any>) => {
return res;
},
responseInterceptorsCatch: (axiosInstance: AxiosInstance, error: any) => {
const { t } = useI18n();
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addAjaxErrorInfo(error);
const { response, code, message, config } = error || {};
const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
const msg: string = response?.data?.error?.message ?? '';
const err: string = error?.toString?.() ?? '';
let errMessage = '';
if (axios.isCancel(error)) return Promise.reject(error);
try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
errMessage = t('sys.api.apiTimeoutMessage');
}
if (err?.includes('Network Error')) {
errMessage = t('sys.api.networkExceptionMsg');
}
if (errMessage) {
if (errorMessageMode === 'modal') {
createMsgbox({ title: t('sys.api.errorTip'), message: errMessage });
} else if (errorMessageMode === 'message') {
createMessage.error(errMessage);
}
return Promise.reject(error);
}
} catch (error) {
throw new Error(error as unknown as string);
}
checkStatus(error?.response?.status, msg, errorMessageMode);
const retryRequest = new AxiosRetry();
const { isOpenRetry } = config.requestOptions.retryRequest;
config.method?.toUpperCase() === RequestEnum.GET &&
isOpenRetry &&
retryRequest.retry(axiosInstance, error);
return Promise.reject(error);
},
};
function createAxios(opt?: Partial<CreateAxiosOptions>) {
const { apiUrl = '' } = getEnvSetting();
return new VAxios(
deepMerge(
{
authenticationScheme: '',
timeout: 60 * 1000,
headers: { 'Content-Type': ContentTypeEnum.JSON },
transform: clone(transform),
requestOptions: {
isReturnNativeResponse: false,
isTransformResponse: true,
joinParamsToUrl: false,
formatDate: true,
errorMessageMode: 'message',
apiUrl: apiUrl,
joinTime: true,
ignoreCancelToken: true,
withToken: true,
retryRequest: {
isOpenRetry: false,
count: 5,
waitTime: 100,
},
},
},
opt || {},
),
);
}
export const defHttp = createAxios();
export const otherHttp = createAxios({
requestOptions: {
apiUrl: '',
},
});
二.Axios.ts
import type {
AxiosRequestConfig,
AxiosInstance,
AxiosResponse,
AxiosError,
InternalAxiosRequestConfig,
} from 'axios';
import type { RequestOptions, Result, UploadFileParams } from '#/axios';
import type { CreateAxiosOptions } from './axiosTransform';
import axios from 'axios';
import qs from 'qs';
import { AxiosCanceler } from './axiosCancel';
import { isFunction } from '@/utils/lang/is';
import { cloneDeep } from 'lodash-es';
import { ContentTypeEnum, RequestEnum } from '@/settings/enum';
export * from './axiosTransform';
export class VAxios {
private axiosInstance: AxiosInstance;
private readonly options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) {
this.options = options;
this.axiosInstance = axios.create(options);
this.setupInterceptors();
}
private createAxios(config: CreateAxiosOptions): void {
this.axiosInstance = axios.create(config);
}
private getTransform() {
const { transform } = this.options;
return transform;
}
getAxios(): AxiosInstance {
return this.axiosInstance;
}
configAxios(config: CreateAxiosOptions) {
if (!this.axiosInstance) {
return;
}
this.createAxios(config);
}
setHeader(headers: any): void {
if (!this.axiosInstance) {
return;
}
Object.assign(this.axiosInstance.defaults.headers, headers);
}
private setupInterceptors() {
const {
axiosInstance,
options: { transform },
} = this;
if (!transform) {
return;
}
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch,
} = transform;
const axiosCanceler = new AxiosCanceler();
this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const { requestOptions } = this.options;
const ignoreCancelToken = requestOptions?.ignoreCancelToken ?? true;
!ignoreCancelToken && axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options);
}
return config;
}, undefined);
requestInterceptorsCatch &&
isFunction(requestInterceptorsCatch) &&
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
res && axiosCanceler.removePending(res.config);
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res);
}
return res;
}, undefined);
responseInterceptorsCatch &&
isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, (error) => {
return responseInterceptorsCatch(axiosInstance, error);
});
}
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
const customFilename = params.name || 'file';
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
} else {
formData.append(customFilename, params.file);
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key];
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, params.data![key]);
});
}
return this.axiosInstance.request<T>({
...config,
method: 'POST',
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
ignoreCancelToken: true,
},
});
}
supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers;
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
if (
contentType !== ContentTypeEnum.FORM_URLENCODED ||
!Reflect.has(config, 'data') ||
config.method?.toUpperCase() === RequestEnum.GET
) {
return config;
}
return {
...config,
data: qs.stringify(config.data, { arrayFormat: 'brackets' }),
};
}
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'GET' }, options);
}
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'POST' }, options);
}
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PUT' }, options);
}
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'DELETE' }, options);
}
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config);
if (config.cancelToken) {
conf.cancelToken = config.cancelToken;
}
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
conf.requestOptions = opt;
conf = this.supportFormData(conf);
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
if (transformResponseHook && isFunction(transformResponseHook)) {
try {
const ret = transformResponseHook(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('request error!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error | AxiosError) => {
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt));
return;
}
if (axios.isAxiosError(e)) {
}
reject(e);
});
});
}
}
三.axiosTransform.ts
import type {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import type { RequestOptions, Result } from '#/axios';
export interface CreateAxiosOptions extends AxiosRequestConfig {
authenticationScheme?: string;
transform?: AxiosTransform;
requestOptions?: RequestOptions;
}
export abstract class AxiosTransform {
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
transformResponseHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>;
requestInterceptors?: (
config: InternalAxiosRequestConfig,
options: CreateAxiosOptions,
) => InternalAxiosRequestConfig;
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
requestInterceptorsCatch?: (error: Error) => void;
responseInterceptorsCatch?: (axiosInstance: AxiosInstance, error: Error) => void;
}
四.axiosCancel.ts
import type { AxiosRequestConfig } from 'axios';
const pendingMap = new Map<string, AbortController>();
const getPendingUrl = (config: AxiosRequestConfig): string => {
return [config.method, config.url].join('&');
};
export class AxiosCanceler {
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const url = getPendingUrl(config);
const controller = new AbortController();
config.signal = config.signal || controller.signal;
if (!pendingMap.has(url)) {
pendingMap.set(url, controller);
}
}
removeAllPending() {
pendingMap.forEach((abortController) => {
if (abortController) {
abortController.abort();
}
});
this.reset();
}
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
const abortController = pendingMap.get(url);
if (abortController) {
abortController.abort(url);
}
pendingMap.delete(url);
}
}
reset() {
pendingMap.clear();
}
}
五.axiosRetry.ts
import { AxiosError, AxiosInstance } from 'axios';
export class AxiosRetry {
retry(axiosInstance: AxiosInstance, error: AxiosError) {
const { config } = error.response;
const { waitTime, count } = config?.requestOptions?.retryRequest ?? {};
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= count) {
return Promise.reject(error);
}
config.__retryCount += 1;
delete config.headers;
return this.delay(waitTime).then(() => axiosInstance(config));
}
private delay(waitTime: number) {
return new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
六.helper.ts
import type { ErrorMessageMode } from '#/axios';
import { ElMessageBoxOptions } from 'element-plus';
import { useDebounceFn } from '@vueuse/core';
import { isObject, isString } from '@/utils/lang/is';
import { ResultSucessCodes, ResultEnum } from '@/settings/enum';
import { useI18n } from '@/hooks/web/useI18n';
import { useUserStoreWithOut } from '@/store/modules/user';
import { useMessage } from '@/utils/lib/msgbox';
import { getEnvSetting } from '@/utils/env';
const { useMock } = getEnvSetting();
const { createMessage, createMsgbox } = useMessage();
const error = createMessage.error;
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export function joinTimestamp<T extends boolean>(
join: boolean,
restful: T,
): T extends true ? string : object;
export function joinTimestamp(join: boolean, restful = false): string | object {
if (!join) {
return restful ? '' : {};
}
const now = new Date().getTime();
if (restful) {
return `?_t=${now}`;
}
return { _t: now };
}
export function formatRequestDate(params: Recordable) {
if (Object.prototype.toString.call(params) !== '[object Object]') {
return;
}
for (const key in params) {
const format = params[key]?.format ?? null;
if (format && typeof format === 'function') {
params[key] = params[key].format(DATE_TIME_FORMAT);
}
if (isString(key)) {
const value = params[key];
if (value) {
try {
params[key] = isString(value) ? value.trim() : value;
} catch (error: any) {
throw new Error(error);
}
}
}
if (isObject(params[key])) {
formatRequestDate(params[key]);
}
}
}
export const isSuccess = (code) => ResultSucessCodes.indexOf(code) > -1;
const TOKEN_INVALID_CODE = [
ResultEnum.UNAUTHORIZED,
ResultEnum.FORBIDDEN,
ResultEnum.TOKEN_INVALID,
ResultEnum.BYD_HEADER_INVALID,
];
export function checkStatus(
status: number | string,
msg: string,
errorMessageMode: ErrorMessageMode = 'message',
) {
const { t } = useI18n();
if (useMock) status = '0000';
let errMessage = msg;
switch (+status) {
case 401:
errMessage = msg || t('sys.api.errMsg401');
break;
case 403:
errMessage = t('sys.api.errMsg403');
break;
case 404:
errMessage = t('sys.api.errMsg404');
break;
case 405:
errMessage = t('sys.api.errMsg405');
break;
case 408:
errMessage = t('sys.api.errMsg408');
break;
case 500:
errMessage = msg || t('sys.api.errMsg500');
break;
case 501:
errMessage = t('sys.api.errMsg501');
break;
case 502:
errMessage = t('sys.api.errMsg502');
break;
case 503:
errMessage = t('sys.api.errMsg503');
break;
case 504:
errMessage = t('sys.api.errMsg504');
break;
case 505:
errMessage = t('sys.api.errMsg505');
break;
}
if (errMessage) {
const tokenInvalid = TOKEN_INVALID_CODE.includes(+status);
if (errorMessageMode === 'modal' || tokenInvalid) {
const opt: ElMessageBoxOptions = { title: t('sys.api.errorTip'), message: errMessage };
if (tokenInvalid) {
opt.type = 'warning';
opt.draggable = true;
opt.showCancelButton = true;
opt.confirmButtonText = t('login.signIn');
opt.callback = (v) => {
const userStore = useUserStoreWithOut();
if (v == 'confirm') userStore.logout();
};
}
showDebounceModal(opt);
} else if (errorMessageMode === 'message') {
error({ message: errMessage, key: `global_error_message_status_${status}` });
}
}
}
export const showDebounceModal = useDebounceFn(
(opt: ElMessageBoxOptions) => createMsgbox(opt),
500,
);