From 7531ea29e134b0f8c21ddc54a9568af4294625d2 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 10 Sep 2026 01:58:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E7=BB=84=E5=88=AB=E5=A1=AB?= =?UTF-8?q?=E4=BA=86=E5=B9=B4=E9=BE=84=E5=90=8E=E3=80=8C=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E3=80=8D=E6=97=A0=E5=8F=8D=E5=BA=94=EF=BC=88?= =?UTF-8?q?number=20=E5=AD=97=E6=AE=B5=E8=A2=AB=E5=BD=93=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E5=A4=84=E7=90=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vue 3 对 的 v-model 会自动 parseFloat(空值仍为 ''), 所以填了年龄后 row.ageMin/ageMax 是 number,而校验里写的是 row.ageMin.trim(), 点击处理器抛 TypeError 直接中断 —— 界面毫无反应;留空时是 '',.trim() 正常,所以能保存。 - CategoriesSection:新增 toAge() 统一取值(String() 包一层,非法值返回 NaN 交给校验标红), validate 与 buildInputs 都改用它;CategoryRow 的数值字段类型如实标注为 string | number - AdjustmentsPage:调整分值的 formDelta.value.trim() 同样会崩,改为 String(...).trim() 自检:vue-tsc + pnpm build 通过,前端 node --test 26/26。 --- .../components/baseData/CategoriesSection.vue | 31 ++++++++++++++----- web/src/admin/pages/AdjustmentsPage.vue | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/web/src/admin/components/baseData/CategoriesSection.vue b/web/src/admin/components/baseData/CategoriesSection.vue index b99e11c..beccabf 100644 --- a/web/src/admin/components/baseData/CategoriesSection.vue +++ b/web/src/admin/components/baseData/CategoriesSection.vue @@ -15,11 +15,12 @@ const toast = useToastStore() interface CategoryRow { key: number name: string - multiplier: string + // type="number" 的 v-model 会被 Vue 自动转成 number(空值仍是 ''),这里如实标注 + multiplier: string | number gender: '' | Gender - ageMin: string - ageMax: string - sortOrder: string + ageMin: string | number + ageMax: string | number + sortOrder: string | number } interface RowError { @@ -97,6 +98,20 @@ function moveRow(index: number, delta: number): void { rows.value = next } +/** + * 年龄取值。 + * + * `type="number"` 的 v-model 会被 Vue 自动转成 number(空值仍是 ''),所以不能直接 `.trim()`: + * 填了数字后字段是 number,`.trim()` 抛 TypeError,点击处理器中断,表现为「点了没反应」。 + * 非法输入返回 NaN,交给 validate 标红。 + */ +function toAge(value: string | number): number | null { + const text = String(value).trim() + if (text === '') return null + const parsed = Number(text) + return Number.isFinite(parsed) ? parsed : Number.NaN +} + function validate(): boolean { const next: Record = {} const seen = new Set() @@ -112,8 +127,8 @@ function validate(): boolean { rowError.multiplier = '权重为 0–100 的正数' } - const ageMin = row.ageMin.trim() === '' ? null : Number(row.ageMin) - const ageMax = row.ageMax.trim() === '' ? null : Number(row.ageMax) + const ageMin = toAge(row.ageMin) + const ageMax = toAge(row.ageMax) if (ageMin !== null && (!Number.isInteger(ageMin) || ageMin < 0 || ageMin > 120)) rowError.age = '年龄为 0–120 的整数' else if (ageMax !== null && (!Number.isInteger(ageMax) || ageMax < 0 || ageMax > 120)) rowError.age = '年龄为 0–120 的整数' else if (ageMin !== null && ageMax !== null && ageMin > ageMax) rowError.age = '起始年龄不能大于结束年龄' @@ -129,8 +144,8 @@ function buildInputs(): EventCategoryInput[] { name: row.name.trim(), multiplier: Number(row.multiplier), gender: row.gender === '' ? null : row.gender, - ageMin: row.ageMin.trim() === '' ? null : Number(row.ageMin), - ageMax: row.ageMax.trim() === '' ? null : Number(row.ageMax), + ageMin: toAge(row.ageMin), + ageMax: toAge(row.ageMax), sortOrder: Number(row.sortOrder) || index + 1, })) } diff --git a/web/src/admin/pages/AdjustmentsPage.vue b/web/src/admin/pages/AdjustmentsPage.vue index d74605b..079db16 100644 --- a/web/src/admin/pages/AdjustmentsPage.vue +++ b/web/src/admin/pages/AdjustmentsPage.vue @@ -181,7 +181,7 @@ function openCreate(): void { function validate(): string | null { if (formTeamId.value === null) return '请选择单位' const delta = Number(formDelta.value) - if (formDelta.value.trim() === '' || !Number.isFinite(delta)) return '请填写分值' + if (String(formDelta.value).trim() === '' || !Number.isFinite(delta)) return '请填写分值' if (delta === 0) return '分值不能为 0' if (formReason.value.trim() === '') return '理由必填' return null