feat(admin): 报名导入新增「从已有名单勾选报名」
原来只能上传 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。
This commit is contained in:
1 parent
7531ea29e1
commit
c9d5b8ea89
6 files changed
+499
-77
No files matched your search
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 「从已有名单勾选报名」的纯函数部分。
|
||||
*
|
||||
* 之所以复用导入通道的表头与 CSV 格式:服务端只认 /entries/import 这一个报名入口,
|
||||
* 勾选结果转成同格式 CSV 后走同一个接口,字段校验、按 (组别, 单位, 运动员) upsert
|
||||
* 与行级报错回显都与文件导入完全一致,不必为选择器新增服务端接口。
|
||||
*/
|
||||
|
||||
/** 与服务端 handlers_config.go 的 importColumns 一致;缺任何一列服务端会直接拒绝。 */
|
||||
export const REQUIRED_COLUMNS = [
|
||||
'event_code',
|
||||
'category',
|
||||
'team_code',
|
||||
'athlete_name',
|
||||
'bib',
|
||||
'seed',
|
||||
'status',
|
||||
'member_names',
|
||||
] as const
|
||||
|
||||
export type SelectionMode = 'individual' | 'team'
|
||||
|
||||
export interface SelectionInput {
|
||||
eventCode: string
|
||||
categoryName: string
|
||||
teamCode: string
|
||||
status?: string
|
||||
mode: SelectionMode
|
||||
athletes: { name: string }[]
|
||||
/** 团体项目队员名单;缺省时按 athletes 的列表顺序取姓名。 */
|
||||
memberNames?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成与导入表头一致的二维数组。
|
||||
*
|
||||
* 个人项目一人一行;团体 / 对抗项目整体一行,队员姓名进 member_names。
|
||||
* 服务端按 (组别, 单位, 运动员) upsert,团体行运动员为空,因此同组别 + 单位只留一条报名。
|
||||
*/
|
||||
export function buildSelectionRows(input: SelectionInput): string[][] {
|
||||
const status = input.status ?? 'registered'
|
||||
const rows: string[][] = [[...REQUIRED_COLUMNS]]
|
||||
if (input.mode === 'individual') {
|
||||
for (const athlete of input.athletes) {
|
||||
rows.push([input.eventCode, input.categoryName, input.teamCode, athlete.name, '', '', status, ''])
|
||||
}
|
||||
return rows
|
||||
}
|
||||
const names = input.memberNames ?? input.athletes.map((athlete) => athlete.name)
|
||||
rows.push([input.eventCode, input.categoryName, input.teamCode, '', '', '', status, names.join('/')])
|
||||
return rows
|
||||
}
|
||||
|
||||
/** 转成 CSV 文本;服务端用标准 csv 解析器读取,转义必须符合 RFC 4180。 */
|
||||
export function toCsv(rows: string[][]): string {
|
||||
return rows.map((row) => row.map(escapeField).join(',')).join('\n')
|
||||
}
|
||||
|
||||
/** 字段含逗号 / 引号 / 换行时用双引号包裹,内部引号翻倍。 */
|
||||
function escapeField(field: string): string {
|
||||
return /[",\n\r]/.test(field) ? `"${field.replace(/"/g, '""')}"` : field
|
||||
}
|
||||
Reference in new issue
Block a user