From 8a27f5a4fb85fced32172f0178bef534b33900b0 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 11 Sep 2026 22:51:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E4=BF=AE=E5=A4=8D=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E6=95=B0=E6=8D=AE=E3=80=8C=E8=BF=90=E5=8A=A8=E5=91=98?= =?UTF-8?q?=E3=80=8D=E9=A1=B5=E6=95=B4=E5=9D=97=E7=A9=BA=E7=99=BD=EF=BC=88?= =?UTF-8?q?TDZ=20=E5=B4=A9=E6=BA=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AthletesSection 里 filteredIds 在第 73 行引用了第 272 行才声明的 filtered, 而第 84 行的 watch(filteredIds, ...) 在注册时会同步求值一次源(Vue 行为), setup 阶段就访问到 TDZ 中的 filtered → ReferenceError: Cannot access 'filtered' before initialization → 整个分区渲染失败, 页面只剩标签栏、下面全白。自 631c8dd(上一轮)起 dev_0.1.6/0.1.7/0.1.8 均带此缺陷。 修法:把 filtered 的定义上移到 filteredIds 之前(只依赖三个筛选 ref,上移安全), 并加注释说明为什么不能挪回去。 验证:真 Chromium(Playwright)逐个打开 14 个管理端路由 + 7 个基础数据标签, console 错误与 pageerror 均为 0;「运动员」标签正常渲染出表格与空态。 版本 dev_0.1.8 → dev_0.1.9(热修,P2 批次另行发布)。 --- VERSION | 2 +- console/package.json | 2 +- .../components/baseData/AthletesSection.vue | 40 ++++++++++--------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/VERSION b/VERSION index a527171..3380654 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -dev_0.1.8 +dev_0.1.9 diff --git a/console/package.json b/console/package.json index 29b92cc..cd0c517 100644 --- a/console/package.json +++ b/console/package.json @@ -1,7 +1,7 @@ { "name": "scoreboard-console", "productName": "Scoreboard Console", - "version": "0.1.8", + "version": "0.1.9", "private": true, "author": "AskaEth", "description": "赛事积分榜 · 桌面宿主:守护 Go 服务端并把大屏投到扩展屏", diff --git a/web/src/admin/components/baseData/AthletesSection.vue b/web/src/admin/components/baseData/AthletesSection.vue index 794f19a..4ff671a 100644 --- a/web/src/admin/components/baseData/AthletesSection.vue +++ b/web/src/admin/components/baseData/AthletesSection.vue @@ -70,6 +70,28 @@ const batchPurgeOpen = ref(false) /** 彻底删除的预览(dryRun 响应):提交前把「会删掉什么、重算哪些小项」摆给操作员看。 */ const bulkPurgePreview = ref(null) +// filtered 必须声明在 filteredIds / watch 之前: +// watch 注册时会同步求值一次源(Vue 的行为),若 filtered 还在 TDZ 里, +// setup 会直接抛 "Cannot access 'filtered' before initialization", +// 整个分区渲染不出来(页面只剩标签栏、下面全空白)。2026-09-11 实机复现。 +const filtered = computed(() => { + const keyword = search.value.trim().toLowerCase() + return data.athletes.value.filter((athlete) => { + if (teamFilter.value !== 'all' && String(athlete.teamId) !== teamFilter.value) return false + if (statusFilter.value === 'active' && !athlete.active) return false + if (statusFilter.value === 'inactive' && athlete.active) return false + if (keyword === '') return true + const team = data.teamById(athlete.teamId) + return ( + athlete.name.toLowerCase().includes(keyword) || + (athlete.employeeNo ?? '').toLowerCase().includes(keyword) || + (athlete.idCard ?? '').toLowerCase().includes(keyword) || + (athlete.bib ?? '').toLowerCase().includes(keyword) || + (team?.name ?? '').toLowerCase().includes(keyword) + ) + }) +}) + const filteredIds = computed(() => filtered.value.map((athlete) => athlete.id)) const selectedCount = computed(() => selectedIds.value.size) const selectedAthletes = computed(() => data.athletes.value.filter((athlete) => selectedIds.value.has(athlete.id))) @@ -269,24 +291,6 @@ function toAge(value: string | number): number | null { return Number.isFinite(parsed) ? parsed : Number.NaN } -const filtered = computed(() => { - const keyword = search.value.trim().toLowerCase() - return data.athletes.value.filter((athlete) => { - if (teamFilter.value !== 'all' && String(athlete.teamId) !== teamFilter.value) return false - if (statusFilter.value === 'active' && !athlete.active) return false - if (statusFilter.value === 'inactive' && athlete.active) return false - if (keyword === '') return true - const team = data.teamById(athlete.teamId) - return ( - athlete.name.toLowerCase().includes(keyword) || - (athlete.employeeNo ?? '').toLowerCase().includes(keyword) || - (athlete.idCard ?? '').toLowerCase().includes(keyword) || - (athlete.bib ?? '').toLowerCase().includes(keyword) || - (team?.name ?? '').toLowerCase().includes(keyword) - ) - }) -}) - const activeCount = computed(() => data.athletes.value.filter((athlete) => athlete.active).length) function openCreate(): void {