From c9d5b8ea896300b8e14723d53ec6286e7e29e81a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 10 Sep 2026 02:03:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=8A=A5=E5=90=8D=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=96=B0=E5=A2=9E=E3=80=8C=E4=BB=8E=E5=B7=B2=E6=9C=89?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E5=8B=BE=E9=80=89=E6=8A=A5=E5=90=8D=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来只能上传 CSV/XLSX。现在同一个 tab 上方多一个选择器:选小项 → 组别 → 单位, 从该单位已有运动员里勾人,一键加入报名。 - 个人项目:勾 N 人 → N 条报名(一人一行) - 团体/对抗项目:勾 N 人 → 1 条报名,姓名拼进 member_names(再次提交即更新队员名单) - 已报名的人/单位会标「已报名」并禁止重复勾选 - 复用服务端唯一报名入口 POST /entries/import:勾选结果转成同格式 CSV 提交, 字段校验、按 (组别,单位,运动员) upsert、行级报错回显与表格导入完全一致 - 纯函数 entrySelection.ts(表头唯一来源 + RFC 4180 转义)+ 4 个单测; 表头定义从 ImportSection/mockBaseData 收敛到一处 - 号码布/种子仍走表格导入,界面与文档都写明 自检:vue-tsc + pnpm build 通过,前端 node --test 30/30。 --- docs/使用手册.md | 14 + .../admin/components/baseData/EntryPicker.vue | 291 ++++++++++++++++++ .../components/baseData/ImportSection.vue | 149 ++++----- .../components/baseData/entrySelection.ts | 62 ++++ .../admin/components/baseData/mockBaseData.ts | 7 +- web/src/admin/tests/entry-selection.test.ts | 53 ++++ 6 files changed, 499 insertions(+), 77 deletions(-) create mode 100644 web/src/admin/components/baseData/EntryPicker.vue create mode 100644 web/src/admin/components/baseData/entrySelection.ts create mode 100644 web/src/admin/tests/entry-selection.test.ts diff --git a/docs/使用手册.md b/docs/使用手册.md index 0058ba7..648b23e 100644 --- a/docs/使用手册.md +++ b/docs/使用手册.md @@ -269,11 +269,25 @@ ### 10.7 报名导入 +报名有两种方式,都在本页完成:**从已有名单勾选**适合零散补报,**表格导入**适合批量录入。 + +**从已有名单勾选** + +1. 依次选择小项、组别、单位; +2. 在运动员列表里按姓名 / 工号搜索并勾选: + - 个人项目:勾选的人各生成一条报名;已报名的运动员显示「已报名」且不可重复勾选; + - 团体 / 对抗项目:勾选的人合成一条报名,队员名单按列表顺序写入;该单位已报名时再次提交会更新队员名单; +3. 点「加入报名」,结果同样显示**新增 / 更新 / 跳过**数量。 + +**表格导入** + 1. 点「下载 CSV 模板」获取表头样例; 2. 表头必须包含:`event_code`、`category`、`team_code`、`athlete_name`、`bib`、`seed`、`status`、`member_names`(大小写不敏感); 3. 把 `.csv` 或 `.xlsx` 拖入虚线框,或点击选择文件(单个文件不超过 16MB;xlsx 只解析第一个工作表); 4. 点「开始导入」,页面显示**新增 / 更新 / 跳过**数量;匹配不到的行会跳过并回显行号,修正后可重新上传。 +号码布、种子等字段无法在勾选界面填写,仍需通过表格导入。 + --- ## 11. 导出 diff --git a/web/src/admin/components/baseData/EntryPicker.vue b/web/src/admin/components/baseData/EntryPicker.vue new file mode 100644 index 0000000..44ff65b --- /dev/null +++ b/web/src/admin/components/baseData/EntryPicker.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/web/src/admin/components/baseData/ImportSection.vue b/web/src/admin/components/baseData/ImportSection.vue index af6c153..e5b2ce7 100644 --- a/web/src/admin/components/baseData/ImportSection.vue +++ b/web/src/admin/components/baseData/ImportSection.vue @@ -2,15 +2,14 @@ import { computed, ref } from 'vue' import type { EntryImportResult } from '@/shared' import MxIcon from '../MxIcon.vue' +import EntryPicker from './EntryPicker.vue' import { useBaseData } from './useBaseData' import { useToastStore } from '../../stores/toast' +import { REQUIRED_COLUMNS } from './entrySelection' const data = useBaseData() const toast = useToastStore() -/** 与服务端 handlers_config.go 的 importColumns 一致;缺任何一列服务端会直接拒绝。 */ -const REQUIRED_COLUMNS = ['event_code', 'category', 'team_code', 'athlete_name', 'bib', 'seed', 'status', 'member_names'] - const TEMPLATE_CSV = [ REQUIRED_COLUMNS.join(','), 'M100,男子组,F1,张伟,101,,registered,', @@ -97,87 +96,91 @@ function downloadTemplate(): void {