cpms_operation_platform/src/hooks/useRequest.ts

171 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-07-14 10:31:17 +08:00
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { ref, Ref, watch, WatchSource, onUnmounted, computed } from 'vue';
export interface UseRequestReturn<T> {
/** 纯净的数据,不包含后端外层壳 */
data: Ref<T | undefined>;
/** 加载状态 */
loading: Ref<boolean>;
/** 错误对象 */
error: Ref<Error | null>;
/** 手动触发请求 */
run: (...args: any[]) => Promise<T | undefined>;
/** 取消请求 */
cancel: () => void;
/** 重置数据 */
reset: () => void;
}
export function useRequest<T>(
fetcher: (...args: any[]) => Promise<any>,
options: {
/** 是否手动触发,默认 false (即自动触发) */
manual?: boolean;
/** 初始数据 */
initialData?: T;
/** 依赖数组,变化时重新请求 */
refreshDeps?: WatchSource[];
/**
*
* data res.data
*/
formatResult?: (res: any) => T;
/** 是否准备好可以发起请求,默认 true */
ready?: Ref<boolean>;
} = {},
): UseRequestReturn<T> & {
/** 获取当前 AbortController 的 signal可传递给 fetcher */
getSignal: () => AbortSignal | undefined;
} {
const {
manual = false,
initialData,
refreshDeps,
formatResult = (res) => (res && typeof res === 'object' && 'data' in res ? res.data : res),
ready = ref(true),
} = options;
const data = ref<T | undefined>(initialData);
const loading = ref(false);
const error = ref<Error | null>(null);
let abortController: AbortController | null = null;
let isUnmounted = false;
onUnmounted(() => {
isUnmounted = true;
if (abortController) {
abortController.abort();
}
});
const run = async (...args: any[]): Promise<T | undefined> => {
if (isUnmounted) return undefined;
// 取消之前的请求
if (abortController) {
abortController.abort();
}
// 创建新的 AbortController
abortController = new AbortController();
const currentSignal = abortController.signal;
loading.value = true;
error.value = null;
try {
// 将 signal 作为最后一个参数传递给 fetcher
// 调用方可以通过最后一个参数获取 signal: async (...args, { signal }) => {...}
const res = await fetcher(...args, { signal: currentSignal });
if (currentSignal.aborted || isUnmounted) {
return undefined;
}
const formattedData = formatResult(res);
if (!isUnmounted) {
data.value = formattedData;
}
return formattedData;
} catch (e: any) {
if (e.name === 'AbortError' || e.message?.includes('aborted')) {
return undefined;
}
if (!isUnmounted) {
error.value = e;
}
return undefined;
} finally {
// 只有当前请求未被取消时才重置 loading
if (!currentSignal.aborted && !isUnmounted) {
loading.value = false;
}
// 如果这是当前活动的 controller清理引用
if (abortController?.signal === currentSignal) {
abortController = null;
}
}
};
const cancel = () => {
if (abortController) {
abortController.abort();
loading.value = false;
abortController = null;
}
};
const reset = () => {
cancel();
data.value = initialData;
error.value = null;
};
// 使用 shouldAutoRun 统一管理自动请求,避免 refreshDeps 和 immediate watch 重复触发 run
const shouldAutoRun = computed(() => !manual && ready.value);
let hasAutoRun = false; // 标记是否已自动执行过,防止重复触发
watch(
shouldAutoRun,
(val) => {
if (val && !hasAutoRun) {
hasAutoRun = true;
run();
} else if (!val) {
// 当条件不满足时重置 hasAutoRun使下次满足条件时能再次自动执行
hasAutoRun = false;
}
},
{ immediate: true },
);
// refreshDeps 变化时重新请求(初始化时不再重复触发)
if (refreshDeps && refreshDeps.length > 0) {
watch(
refreshDeps,
() => {
if (shouldAutoRun.value) {
run();
}
},
{ deep: true },
);
}
const getSignal = () => abortController?.signal;
return {
// @ts-ignore
data,
loading,
error,
run,
cancel,
reset,
getSignal,
};
}