From 3c54ad69c19349be592d9648691f1790bedcac2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 15 Sep 2026 19:58:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20useTable=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E7=AB=9E=E6=80=81=E5=AE=88=E5=8D=AB=EF=BC=88=E6=97=A7=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E4=B8=8D=E6=B8=85=20loading/=E4=B8=8D=E5=86=99?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=80=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetchData 加请求序号令牌 fetchSeq:请求被新请求取代后,旧响应不再 改动任何状态——修复两类竞态:旧请求取消分支把 loading 提前清成 idle、 旧请求失败分支把新请求在途数据清空并置 error - 与 dashboard reqSeq 修法同口径(id===seq 才写数据/清 loading), 供访客列表等分页筛选页复用 Co-Authored-By: Claude Code --- admin/src/hooks/core/useTable.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/admin/src/hooks/core/useTable.ts b/admin/src/hooks/core/useTable.ts index d9def4f..3506519 100644 --- a/admin/src/hooks/core/useTable.ts +++ b/admin/src/hooks/core/useTable.ts @@ -197,6 +197,9 @@ function useTableImpl Promise>( // 请求取消控制器 let abortController: AbortController | null = null + // 请求序号令牌(竞态守卫):请求被更新的请求取代后,不得再写数据/清 loading/置错误态 + let fetchSeq = 0 + // 缓存清理定时器 let cacheCleanupTimer: NodeJS.Timeout | null = null @@ -288,6 +291,7 @@ function useTableImpl Promise>( // 创建新的取消控制器 const currentController = new AbortController() abortController = currentController + const currentSeq = ++fetchSeq // 状态机:进入 loading 状态 loadingState.value = 'loading' @@ -391,15 +395,24 @@ function useTableImpl Promise>( 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 {