fix(admin): 修复基础数据「运动员」页整块空白(TDZ 崩溃)

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 批次另行发布)。
This commit is contained in:
AskaEth committed 2026-09-11 22:51:11 +08:00
1 parent ab7662b7f1
commit 8a27f5a4fb
3 files changed
+24 -20

No files matched your search

+1 -1
View File
@@ -1 +1 @@
dev_0.1.8
dev_0.1.9
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "scoreboard-console",
"productName": "Scoreboard Console",
"version": "0.1.8",
"version": "0.1.9",
"private": true,
"author": "AskaEth",
"description": "赛事积分榜 · 桌面宿主:守护 Go 服务端并把大屏投到扩展屏",
@@ -70,6 +70,28 @@ const batchPurgeOpen = ref(false)
/** 彻底删除的预览(dryRun 响应):提交前把「会删掉什么、重算哪些小项」摆给操作员看。 */
const bulkPurgePreview = ref<AthleteBatchResult | null>(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 {