fix(admin): useTable 请求竞态守卫(旧请求不清 loading/不写错误态)

- fetchData 加请求序号令牌 fetchSeq:请求被新请求取代后,旧响应不再
  改动任何状态——修复两类竞态:旧请求取消分支把 loading 提前清成 idle、
  旧请求失败分支把新请求在途数据清空并置 error
- 与 dashboard reqSeq 修法同口径(id===seq 才写数据/清 loading),
  供访客列表等分页筛选页复用

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-15 19:58:14 +08:00
parent 5b3297337e
commit 3c54ad69c1
+18 -5
View File
@@ -197,6 +197,9 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
// 请求取消控制器
let abortController: AbortController | null = null
// 请求序号令牌(竞态守卫):请求被更新的请求取代后,不得再写数据/清 loading/置错误态
let fetchSeq = 0
// 缓存清理定时器
let cacheCleanupTimer: NodeJS.Timeout | null = null
@@ -288,6 +291,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
// 创建新的取消控制器
const currentController = new AbortController()
abortController = currentController
const currentSeq = ++fetchSeq
// 状态机:进入 loading 状态
loadingState.value = 'loading'
@@ -391,15 +395,24 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
return standardResponse
} catch (err) {
// 竞态守卫:已被更新请求取代的旧请求不再改动任何状态
// (防旧 finally 提前清 loading、旧失败响应清空新请求的数据)
const superseded = currentSeq !== fetchSeq
if (err instanceof Error && err.message === '请求已取消') {
// 请求被取消,回到 idle 状态
loadingState.value = 'idle'
// 请求被取消,回到 idle 状态(仅当它仍是最新请求,如显式 cancelRequest)
if (!superseded) loadingState.value = 'idle'
return { records: [], total: 0, current: 1, size: 10 }
}
// 状态机:请求失败,进入 error 状态
loadingState.value = 'error'
data.value = []
if (!superseded) {
// 状态机:请求失败,进入 error 状态
loadingState.value = 'error'
data.value = []
}
if (superseded) {
// 旧请求的失败交给新请求兜底,不触发错误回调
throw err instanceof Error ? err : new Error(String(err))
}
const tableError = handleError(err, '获取表格数据失败')
throw tableError
} finally {