fix(entry): 补报名弹窗的搜索框支持「号码布」;版本 dev_0.5.7
## 现场问题(主人原话)
> 成绩录入界面的补报名按钮窗口里面的搜索框只能查姓名和工号,不能查号码布,加一下。
号码布是本系统里**运动员的唯一标识**(定稿口径),现场拿号码布找人是常态,所以这条是刚需。
## 改了什么
- 抽出纯函数 `athleteMatchesKeyword(athlete, keyword)` 作为**选人搜索的唯一口径**:姓名、工号、**号码布** 三维并列;
关键字归一(`trim()` + `toLowerCase()`)与仓库既有写法逐字一致,空搜索仍不筛人。
- `EntryPicker.vue` **两处**搜索都改成走它:
- `filteredAthletes`(个人报名名单)
- `membersByTeam`(团体队员名单)
- 两个搜索框的提示文字从「搜索姓名 / 工号」改成「**搜索姓名 / 工号 / 号码布**」。
- ⚠️ 收口成一个函数的意义:两个框**不会再各走各的** —— 这次就是因为两处各写了一份过滤条件,改一处漏一处。
## 验证
- **先红后绿**:新增 `web/src/admin/tests/athlete-search-bib.test.ts`(号码布能搜到、姓名/工号不回归、大小写与空格处理、空搜索不筛人)。
- 全量自检两轮**全绿、`EXIT=0`**:服务端 14 包 / 前端 929 例 0 fail / Console 68 例 0 fail。
## 顺带
`debug/使用说明/` 里落后的 4 份(使用手册 / 升级说明 / 报名名单导入规范 / 使用说明.txt)已补刷,与 `docs/` 逐份一致;
`docs/升级说明.md` 新增 `dev_0.5.7` 一节。
## 版本
`dev_0.5.6` → **`dev_0.5.7`**(无数据迁移);安装器素材同步重生成(照例只动 `brand.bmp` 两张)。
This commit is contained in:
1 parent
99c91f99f7
commit
d11cab31db
9 files changed
+134
-24
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 309 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
@@ -1 +1 @@
|
||||
0.5.6
|
||||
0.5.7
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "scoreboard-console",
|
||||
"productName": "Scoreboard Console",
|
||||
"version": "0.5.6",
|
||||
"version": "0.5.7",
|
||||
"private": true,
|
||||
"author": "AskaEth",
|
||||
"description": "赛事积分榜 · 桌面宿主:守护 Go 服务端并把大屏投到扩展屏",
|
||||
|
||||
+21
-1
@@ -6,7 +6,27 @@
|
||||
|
||||
---
|
||||
|
||||
## dev_0.5.6(当前版本)
|
||||
## dev_0.5.7(当前版本)
|
||||
|
||||
### 一句话
|
||||
|
||||
**没有数据迁移,升级后不用重配任何东西**:修好一个小地方 —— 成绩录入页**补报名弹窗里的搜索框**以前只能按**姓名 / 工号**找人,现在也能按**号码布**搜了。号码布是本系统里运动员的唯一标识,现场拿号码布找人是常态。
|
||||
|
||||
### ① 改了什么
|
||||
|
||||
- **补报名弹窗**里有两个搜索框(一个筛**个人报名**的名单,一个筛**团体报名**的队员名单),原先都只匹配**姓名**与**工号**;现在**同时匹配号码布**。两个框的提示文字也相应改成「搜索姓名 / 工号 / 号码布」。
|
||||
- 匹配逻辑收口成一个共用函数(姓名、工号、号码布三维并列),所以两个框的口径**不会再各走各的**。
|
||||
- 关键字处理与原来一致(去掉首尾空格、忽略大小写、空搜索不筛人)。
|
||||
|
||||
### ② 升级后核对
|
||||
|
||||
1. 打开成绩录入页 → 点「补报名」→ 在搜索框里输入某个运动员的**号码布**,应该能筛出那个人;
|
||||
2. 在团体报名的「队员」选择里输号码布,同样应该能筛出来;
|
||||
3. 其余照旧:**不需要重新导入规则包,也不需要重配任何东西**。
|
||||
|
||||
---
|
||||
|
||||
## dev_0.5.6
|
||||
|
||||
### 一句话
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { submitEntryAthletes } from '../../pages/entryAthletes'
|
||||
import { useBaseData } from './useBaseData'
|
||||
import { useToastStore } from '../../stores/toast'
|
||||
import {
|
||||
athleteMatchesKeyword,
|
||||
buildSelectionRows,
|
||||
buildTeamSelectionRows,
|
||||
buildTeamSelections,
|
||||
@@ -108,15 +109,10 @@ const teamAthletes = computed(() =>
|
||||
data.athletes.value.filter((athlete) => athlete.active && String(athlete.teamId) === individualTeamId.value),
|
||||
)
|
||||
|
||||
const filteredAthletes = computed(() => {
|
||||
const keyword = search.value.trim().toLowerCase()
|
||||
if (keyword === '') return teamAthletes.value
|
||||
return teamAthletes.value.filter(
|
||||
(athlete) =>
|
||||
athlete.name.toLowerCase().includes(keyword) ||
|
||||
(athlete.employeeNo ?? '').toLowerCase().includes(keyword),
|
||||
)
|
||||
})
|
||||
/** 按姓名 / 工号 / 号码布过滤本单位的人:号码布是人员的唯一标识,现场拿号码布找人是常态。 */
|
||||
const filteredAthletes = computed(() =>
|
||||
teamAthletes.value.filter((athlete) => athleteMatchesKeyword(athlete, search.value)),
|
||||
)
|
||||
|
||||
/** 勾选顺序可能与列表顺序不同,提交前统一按列表顺序取,团体队员名单才稳定。 */
|
||||
const checkedAthletes = computed(() => teamAthletes.value.filter((athlete) => checked.value.includes(athlete.id)))
|
||||
@@ -137,19 +133,12 @@ const pickedTeamCount = computed(() => teamSelections.value.length)
|
||||
/** 「全选」的范围:启用中且还没报名的单位。 */
|
||||
const selectableTeams = computed(() => selectableTeamIds(data.teams.value, registeredTeams.value))
|
||||
|
||||
/** 展开的单位里可勾选的队员;搜索只作用于展开的名单。 */
|
||||
/** 展开的单位里可勾选的队员;搜索只作用于展开的名单,口径与上面的个人报名名单同一份。 */
|
||||
const membersByTeam = computed(() => {
|
||||
const keyword = memberSearch.value.trim().toLowerCase()
|
||||
const map = new Map<number, Athlete[]>()
|
||||
for (const athlete of data.athletes.value) {
|
||||
if (!athlete.active) continue
|
||||
if (
|
||||
keyword !== '' &&
|
||||
!athlete.name.toLowerCase().includes(keyword) &&
|
||||
!(athlete.employeeNo ?? '').toLowerCase().includes(keyword)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (!athleteMatchesKeyword(athlete, memberSearch.value)) continue
|
||||
const list = map.get(athlete.teamId)
|
||||
if (list === undefined) map.set(athlete.teamId, [athlete])
|
||||
else list.push(athlete)
|
||||
@@ -543,7 +532,7 @@ async function submitTeam(): Promise<void> {
|
||||
</select>
|
||||
<div class="mx-input-wrap bd-search">
|
||||
<span class="mx-input-icon"><MxIcon name="search" :size="15" /></span>
|
||||
<input v-model="search" class="mx-input" placeholder="搜索姓名 / 工号" />
|
||||
<input v-model="search" class="mx-input" placeholder="搜索姓名 / 工号 / 号码布" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bd-toolbar-right">
|
||||
@@ -615,7 +604,7 @@ async function submitTeam(): Promise<void> {
|
||||
<span class="bd-note">已勾选 {{ pickedTeamCount }} 个参赛单位</span>
|
||||
<div v-if="expandedTeams.length > 0" class="mx-input-wrap bd-search">
|
||||
<span class="mx-input-icon"><MxIcon name="search" :size="15" /></span>
|
||||
<input v-model="memberSearch" class="mx-input" placeholder="搜索队员姓名 / 工号" />
|
||||
<input v-model="memberSearch" class="mx-input" placeholder="搜索队员姓名 / 工号 / 号码布" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bd-toolbar-right">
|
||||
|
||||
@@ -217,6 +217,31 @@ export function selectionAllowsSubmit(mode: SelectionMode, chosenCount: number):
|
||||
return mode === 'team' || chosenCount > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 选人搜索框的匹配口径:**姓名 / 工号 / 号码布**任一命中即算(大小写不敏感、首尾空格忽略)。
|
||||
*
|
||||
* 号码布必须在内:它是本系统里人员的唯一标识(先认证件号,没有证件号就看号码布),
|
||||
* 现场拿着号码布找人是常态,只能按姓名 / 工号搜等于搜不到人。
|
||||
*
|
||||
* 工号与号码布都可能为空(老数据两个都没填、老服务端也不下发),一律当空串处理。
|
||||
* 关键字全为空白 = 没在搜,返回 true(不能把名单筛空)。
|
||||
*
|
||||
* 搜索框是文本框,这里只做字符串比较;**不要**把它的取值喂给 `type="number"` 的输入框
|
||||
* (那种框的 v-model 会被 Vue 转成 number,清空才留 '',字符串方法会在渲染期抛错)。
|
||||
*/
|
||||
export function athleteMatchesKeyword(
|
||||
athlete: { name: string; employeeNo?: string | null; bib?: string | null },
|
||||
keyword: string,
|
||||
): boolean {
|
||||
const needle = keyword.trim().toLowerCase()
|
||||
if (needle === '') return true
|
||||
return (
|
||||
athlete.name.toLowerCase().includes(needle) ||
|
||||
(athlete.employeeNo ?? '').toLowerCase().includes(needle) ||
|
||||
(athlete.bib ?? '').toLowerCase().includes(needle)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 报名行的展示文案:运动员姓名 → 团体队员首人 → 单位简称 / 全称。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { athleteMatchesKeyword } from '../components/baseData/entrySelection'
|
||||
|
||||
/**
|
||||
* 现场报的问题:**补报名弹窗里的搜索框只能查姓名和工号,查不了号码布**。
|
||||
*
|
||||
* 号码布在本系统里是人员的唯一标识(先认证件号,没有证件号就看号码布),
|
||||
* 现场拿着号码布找人更是常态,所以补报名的两处搜索(个人报名的名单、团体报名的队员名单)
|
||||
* 都必须带上这一维。
|
||||
*
|
||||
* SFC 跑不进 `node --test`(无 jsdom,见 entryGuards.ts:6),所以分两层钉:
|
||||
* 1. 匹配口径本身抽成 entrySelection.ts 里的纯函数,在这里逐条测;
|
||||
* 2. 组件里的两处接线用源码契约断言(两处必须都走同一个函数,不许各写一份)。
|
||||
*/
|
||||
|
||||
const PICKER = readFileSync(new URL('../components/baseData/EntryPicker.vue', import.meta.url), 'utf8')
|
||||
|
||||
/** 一名可以按 姓名 / 工号 / 号码布 三路搜到的人。 */
|
||||
const athlete = { name: '张伟', employeeNo: 'E11', bib: 'A001' }
|
||||
|
||||
test('号码布能搜到:号码布是唯一标识,现场拿号码布找人是常态', () => {
|
||||
assert.equal(athleteMatchesKeyword(athlete, 'A001'), true)
|
||||
// 号码布常常是纯数字,同样要能按前缀 / 片段搜到。
|
||||
assert.equal(athleteMatchesKeyword({ name: '李强', employeeNo: null, bib: '1234' }, '12'), true)
|
||||
})
|
||||
|
||||
test('姓名与工号照旧能搜到:这一维只加不减', () => {
|
||||
assert.equal(athleteMatchesKeyword(athlete, '张'), true)
|
||||
assert.equal(athleteMatchesKeyword(athlete, '张伟'), true)
|
||||
assert.equal(athleteMatchesKeyword(athlete, 'E11'), true)
|
||||
assert.equal(athleteMatchesKeyword(athlete, 'e11'), true, '工号大小写不敏感')
|
||||
})
|
||||
|
||||
test('大小写与首尾空格的处理与既有搜索框一致:两边都 trim + toLowerCase', () => {
|
||||
assert.equal(athleteMatchesKeyword(athlete, 'a001'), true, '号码布大小写不敏感')
|
||||
assert.equal(athleteMatchesKeyword(athlete, ' A001 '), true, '首尾空格先去掉')
|
||||
assert.equal(athleteMatchesKeyword({ name: 'Zhang Wei', employeeNo: null, bib: null }, ' zhang '), true)
|
||||
// 全是空白的输入框等于没搜:不能把名单筛空。
|
||||
assert.equal(athleteMatchesKeyword(athlete, ''), true)
|
||||
assert.equal(athleteMatchesKeyword(athlete, ' '), true)
|
||||
})
|
||||
|
||||
test('搜不到的关键字照旧返回 false(号码布不误伤别的行)', () => {
|
||||
assert.equal(athleteMatchesKeyword(athlete, 'A002'), false)
|
||||
assert.equal(athleteMatchesKeyword(athlete, '王芳'), false)
|
||||
})
|
||||
|
||||
test('工号 / 号码布为空的人不会抛错(老数据可能两个都没有)', () => {
|
||||
const bare = { name: '无标识', employeeNo: null, bib: null }
|
||||
assert.equal(athleteMatchesKeyword(bare, '无'), true)
|
||||
assert.equal(athleteMatchesKeyword(bare, '101'), false)
|
||||
// 缺字段(老服务端不下发)同样当空串,不抛。
|
||||
assert.equal(athleteMatchesKeyword({ name: '王芳' }, '101'), false)
|
||||
})
|
||||
|
||||
test('补报名两处搜索框都接上号码布:个人报名名单 + 团体队员名单', () => {
|
||||
// 口径只有一份实现:两处都调它,避免以后只改一处、两处慢慢分叉。
|
||||
const calls = [...PICKER.matchAll(/athleteMatchesKeyword\(/g)].length
|
||||
assert.equal(calls, 2, '个人报名的 filteredAthletes 与团体报名的 membersByTeam 各一处')
|
||||
assert.match(PICKER, /import \{[\s\S]*?athleteMatchesKeyword[\s\S]*?\} from '\.\/entrySelection'/)
|
||||
// 旧的「姓名 / 工号」两维写法不许再留在组件里。
|
||||
assert.doesNotMatch(PICKER, /\(athlete\.employeeNo \?\? ''\)\.toLowerCase\(\)\.includes\(keyword\)/)
|
||||
})
|
||||
|
||||
test('两处搜索框的提示文案都写上了号码布', () => {
|
||||
// 用 includes 而不是 assert.match:失败时不要把那 800 行源码原样打出来。
|
||||
assert.ok(PICKER.includes('placeholder="搜索姓名 / 工号 / 号码布"'), '个人报名的搜索框要写清能按号码布搜')
|
||||
assert.ok(PICKER.includes('placeholder="搜索队员姓名 / 工号 / 号码布"'), '团体队员的搜索框同理')
|
||||
})
|
||||
|
||||
test('号码布匹配不下沉到 v-model 上:搜索框是文本框,不进 type="number" 的坑', () => {
|
||||
// 组件里不许出现「把输入值当数字用」的写法(.trim()/字符串方法会在渲染期抛错)。
|
||||
assert.doesNotMatch(PICKER, /type="number"[\s\S]{0,120}v-model="(search|memberSearch)"/)
|
||||
})
|
||||
Reference in new issue
Block a user