Files
mayfly-go/frontend/src/hooks/useRequest.ts

267 lines
9.5 KiB
TypeScript
Raw Normal View History

import router from '@/router';
import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string';
import { ElMessage } from 'element-plus';
2025-04-26 17:37:09 +08:00
import { createFetch, UseFetchReturn } from '@vueuse/core';
import Api from '@/common/Api';
import { Result, ResultEnum } from '@/common/request';
import config from '@/common/config';
2025-04-26 17:37:09 +08:00
import { ref, unref } from 'vue';
import { URL_401 } from '@/router/staticRouter';
import openApi from '@/common/openApi';
2024-11-20 22:43:53 +08:00
import { useThemeConfig } from '@/store/themeConfig';
import JSONBig from 'json-bigint';
const baseUrl: string = config.baseApiUrl;
// 配置 JSONBig将大数int64/uint64转为字符串避免精度丢失
const JSONBigString = JSONBig({ storeAsString: true });
const useCustomFetch = createFetch({
baseUrl: baseUrl,
combination: 'chain',
options: {
immediate: false,
timeout: 600000,
// beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
async beforeFetch({ url, options }) {
const token = getToken();
const headers = new Headers(options.headers || {});
if (token) {
headers.set('Authorization', token);
headers.set('ClientId', getClientId());
}
2024-11-20 22:43:53 +08:00
const themeConfig = useThemeConfig().themeConfig;
// 如果不是 FormData才设置 Content-Type
if (!(options.body instanceof FormData)) {
headers.set('Content-Type', 'application/json');
}
2024-11-20 22:43:53 +08:00
headers.set('Accept-Language', themeConfig?.globalI18n);
options.headers = headers;
return { url, options };
},
async afterFetch(ctx: any) {
// 使用 json-bigint 解析响应数据,解决 int64/uint64 精度丢失问题
const responseText = await ctx.response.text();
try {
ctx.data = JSONBigString.parse(responseText);
} catch (err) {
// 如果解析失败,尝试使用原生 JSON.parse
try {
ctx.data = JSON.parse(responseText);
} catch {
ctx.data = responseText;
}
}
return ctx;
},
},
});
interface EsReq {
esProxyReq?: boolean;
}
export interface RequestOptions extends RequestInit, EsReq {}
2026-04-21 17:22:21 +08:00
export function useApiFetch<T, P = any>(api: Api, params?: P, reqOptions?: RequestOptions) {
const currentParam = ref(params);
const uaf: any = useCustomFetch<T>(api.url, {
async beforeFetch({ url, options }) {
options.method = api.method;
let paramsValue = unref(currentParam);
let apiUrl = url;
// 简单判断该url是否是restful风格
if (apiUrl.indexOf('{') != -1 && paramsValue) {
apiUrl = templateResolve(apiUrl, paramsValue);
}
2024-03-02 19:08:19 +08:00
if (api.beforeHandler) {
paramsValue = await api.beforeHandler(paramsValue);
2024-03-02 19:08:19 +08:00
}
// post和put使用json格式传参如果是FormData则直接使用
const method = options.method?.toLowerCase();
if ((method === 'post' || method === 'put') && paramsValue) {
if (paramsValue instanceof FormData) {
options.body = paramsValue;
// 对于 FormData删除 Content-Type header让浏览器自动设置 multipart/form-data 和 boundary
if (options.headers instanceof Headers) {
options.headers.delete('Content-Type');
} else if (options.headers && typeof options.headers === 'object') {
delete (options.headers as any)['Content-Type'];
}
} else {
options.body = JSON.stringify(paramsValue);
}
} else if (paramsValue && method !== 'post' && method !== 'put') {
const searchParam = new URLSearchParams();
Object.keys(paramsValue).forEach((key) => {
const val = paramsValue[key];
if (val) {
searchParam.append(key, val);
}
});
apiUrl = `${apiUrl}?${searchParam.toString()}`;
}
// 确保 FormData 的 body 不被 reqOptions 覆盖
const finalOptions = {
...options,
...reqOptions,
};
// 如果原始 options.body 是 FormData优先保留
if (options.body instanceof FormData) {
finalOptions.body = options.body;
// 对于 FormData不要设置 Content-Type让浏览器自动设置 multipart/form-data 和 boundary
if (finalOptions.headers instanceof Headers) {
finalOptions.headers.delete('Content-Type');
} else if (finalOptions.headers && typeof finalOptions.headers === 'object') {
delete (finalOptions.headers as any)['Content-Type'];
}
}
return {
url: apiUrl,
options: finalOptions,
};
},
onFetchError: (ctx: { data: any }) => {
if (reqOptions?.esProxyReq) {
// 使用 json-bigint 解析错误响应
try {
const errorText = typeof ctx.data === 'string' ? ctx.data : JSON.stringify(ctx.data);
uaf.data = { value: JSONBigString.parse(errorText) };
} catch {
uaf.data = { value: ctx.data };
}
return Promise.resolve(uaf.data);
}
return ctx;
},
2026-04-21 17:22:21 +08:00
});
2025-04-26 17:37:09 +08:00
// 统一处理后的返回结果如果直接使用uaf.data则数据会出现由{code: x, data: {}} -> data 的变化导致某些结果绑定报错
const data = ref<T | null>(null);
return {
2026-04-21 17:22:21 +08:00
execute: async function (executeParam?: P) {
if (executeParam !== undefined) {
currentParam.value = executeParam;
}
await execCustomFetch(uaf, reqOptions);
2025-04-26 17:37:09 +08:00
data.value = uaf.data.value;
},
isFetching: uaf.isFetching,
2025-04-26 17:37:09 +08:00
data: data,
abort: uaf.abort,
};
}
let refreshingToken = false;
let queue: any[] = [];
async function execCustomFetch(uaf: UseFetchReturn<any>, reqOptions?: RequestOptions) {
try {
await uaf.execute(true);
} catch (e: any) {
if (!reqOptions?.esProxyReq) {
const rejectPromise = Promise.reject(e);
if (e?.name == 'AbortError') {
console.log('请求已取消');
return rejectPromise;
}
const respStatus = uaf.response.value?.status;
if (respStatus == 404) {
ElMessage.error('url not found');
return rejectPromise;
}
if (respStatus == 500) {
ElMessage.error('server error');
return rejectPromise;
}
console.error(e);
ElMessage.error('network error');
return rejectPromise;
}
}
const result: Result & { error: any; status: number } = uaf.data.value as any;
if (!result) {
2025-04-26 17:37:09 +08:00
ElMessage.error('network request failed');
return Promise.reject(result);
}
// es代理请求
if (reqOptions?.esProxyReq) {
uaf.data.value = result;
return Promise.resolve(result);
}
const resultCode = result.code;
// 如果返回为成功结果则将结果的data赋值给响应式data
if (resultCode === ResultEnum.SUCCESS) {
uaf.data.value = result.data;
return;
}
// 如果是accessToken失效则使用refreshToken刷新token
if (resultCode == ResultEnum.ACCESS_TOKEN_INVALID) {
if (refreshingToken) {
// 请求加入队列等待, 防止并发多次请求refreshToken
return new Promise((resolve) => {
queue.push(() => {
resolve(execCustomFetch(uaf, reqOptions));
});
});
}
try {
refreshingToken = true;
const res = await openApi.refreshToken({ refresh_token: getRefreshToken() });
saveToken(res.token);
saveRefreshToken(res.refresh_token);
// 重新缓存后端用户权限code
await openApi.getPermissions();
// 执行accessToken失效的请求
queue.forEach((resolve: any) => {
resolve();
});
} catch (e: any) {
clearUser();
} finally {
refreshingToken = false;
queue = [];
}
await execCustomFetch(uaf, reqOptions);
return;
}
// 如果提示没有权限,则跳转至无权限页面
if (resultCode === ResultEnum.NO_PERMISSION) {
await router.push({
path: URL_401,
});
return Promise.reject(result);
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && resultCode != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
uaf.error.value = new Error(result.msg);
}
return Promise.reject(result);
}