sysmgr-admin/src/composables/useRequest.ts
liuyun 77c9c3fd25 fix: useRequest 请求序号防竞态 + ty-crud 搜索/重置去重
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:46:13 +08:00

115 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { onMounted, onScopeDispose, ref, watch, type Ref } from 'vue'
import { isCancel } from 'axios'
/** fetcher:业务参数在前,AbortSignal 固定作为最后一个参数传入(用于取消请求)。 */
export type RequestFetcher<TData, TParams extends unknown[]> = (...args: [...TParams, AbortSignal]) => Promise<TData>
export interface UseRequestOptions<TData, TParams extends unknown[]> {
/** true 时挂载不自动请求,需手动 run。 */
manual?: boolean
/** 首次自动请求 / run() 无参时使用的参数。 */
defaultParams?: TParams
/** 响应式依赖,变化时自动用上次参数重新请求。 */
refreshDeps?: unknown[]
onBefore?: (params: TParams) => void
onSuccess?: (data: TData, params: TParams) => void
onError?: (error: unknown, params: TParams) => void
onFinally?: (params: TParams) => void
}
export interface UseRequestResult<TData, TParams extends unknown[]> {
data: Ref<TData | undefined>
loading: Ref<boolean>
error: Ref<unknown>
/** 手动触发(成功返回 data,失败/取消返回 undefined)。 */
run: (...args: TParams) => Promise<TData | undefined>
/** 用上一次的参数重新请求。 */
refresh: () => Promise<TData | undefined>
/** 取消当前请求。 */
cancel: () => void
/** 本地修改 data(不回发请求)。 */
mutate: (data: TData | ((prev: TData | undefined) => TData)) => void
}
/**
* 数据请求 Hook(类似 ahooks 的 useRequest):
* 管理 data/loading/error 状态,支持手动触发、依赖刷新、取消与本地修改。
*/
export function useRequest<TData, TParams extends unknown[]>(
fetcher: RequestFetcher<TData, TParams>,
options: UseRequestOptions<TData, TParams> = {},
): UseRequestResult<TData, TParams> {
const { manual = false, defaultParams = [] as unknown as TParams, refreshDeps } = options
const data = ref<TData>()
const loading = ref(false)
const error = ref<unknown>()
let lastParams: TParams = defaultParams
let controller: AbortController | null = null
let seq = 0
async function run(...args: TParams): Promise<TData | undefined> {
controller?.abort()
controller = new AbortController()
const mySeq = ++seq
lastParams = args.length > 0 ? args : lastParams
loading.value = true
error.value = undefined
options.onBefore?.(lastParams)
try {
const result = await fetcher(...lastParams, controller.signal)
if (mySeq !== seq) return undefined // 已被更新的请求取代
data.value = result
options.onSuccess?.(result, lastParams)
return result
} catch (e) {
if (isCanceled(e)) return undefined
if (mySeq !== seq) return undefined
error.value = e
options.onError?.(e, lastParams)
return undefined
} finally {
if (mySeq === seq) loading.value = false
options.onFinally?.(lastParams)
}
}
function refresh(): Promise<TData | undefined> {
return run(...lastParams)
}
function cancel(): void {
controller?.abort()
}
function mutate(next: TData | ((prev: TData | undefined) => TData)): void {
data.value = typeof next === 'function' ? (next as (prev: TData | undefined) => TData)(data.value) : next
}
if (refreshDeps) {
watch(refreshDeps, () => {
refresh()
})
}
if (!manual) {
onMounted(() => {
run(...defaultParams)
})
}
onScopeDispose(() => {
controller?.abort()
})
return { data, loading, error, run, refresh, cancel, mutate }
}
/** 判断是否 axios 主动取消(AbortController)。 */
function isCanceled(e: unknown): boolean {
return isCancel(e)
}