YD_Event.Web/scripts/website-competition-workbook-test.mjs
ltx 9d07d9eb51 feat(website): 增加官网赛事信息管理与 Excel 模板
新增隐藏的官网赛事信息后台路由和编辑页面。支持从 Excel 拖拽或选择导入草稿、下载完整示例及当前数据,维护中英文赛事信息、公告、当前/检录场次、赛程与发布状态;通过已有 /api/website-competition 接口加载和保存,失败时提供重试与提示。

新增 Excel 工作簿生成与解析:覆盖赛事设置、赛程、全能/挑战/Battle 成绩、选手和纪录,并校验表头、编号及淘汰赛数据;兼容旧模板和 Excel 时间单元格。新增接口响应解包及工作簿往返、错误场景测试。

验证:node scripts/website-competition-response-test.mjs、node scripts/website-competition-workbook-test.mjs、npm run build:local 与 git diff --cached --check 均通过。此提交只含前端;运行时依赖后端 /api/website-competition 及其服务端密钥配置。
2026-10-19 15:58:41 +08:00

71 lines
4.0 KiB
JavaScript

import assert from 'node:assert/strict';
import XLSX from 'xlsx-js-style';
import { parseCompetitionWorkbook, workbookBytes } from '../src/views/website/competition/workbook.mjs';
const parsed = parseCompetitionWorkbook(workbookBytes(), { currentId: 'M999', callingId: 'M999', upcomingCount: 1 });
assert.equal(parsed.competition.nameZh, '跃动跳绳示例赛(请替换)');
assert.equal(parsed.notice.enabled, true);
assert.equal(parsed.published, false);
assert.equal(parsed.schedule.items[0].id, 'M001');
assert.equal(parsed.schedule.items.length, 8);
assert.equal(parsed.schedule.currentId, 'M006');
assert.equal(parsed.schedule.callingId, 'M007');
assert.equal(parsed.schedule.upcomingCount, 3);
assert.equal(parsed.results.overall[0].entries[0].overallRank, 1);
assert.equal(parsed.results.overall[0].entries[0].qualified, true);
assert.equal(parsed.results.challenge[0].rankings[0].playerId, 'A001');
assert.equal(parsed.results.challenge[0].qualifier[0].playerId, 'A001');
assert.equal(parsed.results.battle[0].rounds[0].matches[0].winner, 'A001');
assert.deepEqual(parsed.results.battle[0].rounds.map((round) => round.matches.length), [8, 4, 2, 1]);
assert.deepEqual(parsed.results.battle[0].rounds.map((round) => round.nameEn), ['Round of 16', 'Quarterfinals', 'Semifinals', 'Final']);
assert.equal(parsed.results.battle[0].rounds[1].matches[0].aFrom, 'B001');
assert.equal(parsed.results.battle[0].rounds[1].matches[0].bFrom, 'B002');
assert.equal(parsed.results.battle[0].rounds[3].matches[0].winner, 'A001');
assert.equal(parsed.players.length, 16);
assert.equal(parsed.records.length, 4);
const winners = new Map();
for (const round of parsed.results.battle[0].rounds) for (const match of round.matches) {
const a = match.aPlayer || winners.get(match.aFrom);
const b = match.bPlayer || winners.get(match.bFrom);
assert.ok(a && b, `${match.id} 双方选手已确定`);
assert.ok([a, b].includes(match.winner), `${match.id} 获胜者来自本场`);
winners.set(match.id, match.winner);
}
const current = parseCompetitionWorkbook(workbookBytes(parsed));
assert.deepEqual(current.competition, parsed.competition);
assert.deepEqual(current.notice, parsed.notice);
assert.deepEqual(current.schedule, parsed.schedule);
assert.deepEqual(current.results, parsed.results);
assert.deepEqual(current.players, parsed.players);
assert.deepEqual(current.records, parsed.records);
const edited = XLSX.read(workbookBytes(), { type: 'array' });
edited.Sheets['赛事设置'].A2.v = '我的正式赛事';
edited.Sheets['赛事设置'].M2.v = '是';
const editedParsed = parseCompetitionWorkbook(XLSX.write(edited, { bookType: 'xlsx', type: 'array' }));
assert.equal(editedParsed.competition.nameZh, '我的正式赛事');
assert.equal(editedParsed.published, true);
const legacy = XLSX.read(workbookBytes(), { type: 'array' });
legacy.SheetNames = legacy.SheetNames.filter((name) => name !== '赛事设置');
delete legacy.Sheets['赛事设置'];
const legacyParsed = parseCompetitionWorkbook(XLSX.write(legacy, { bookType: 'xlsx', type: 'array' }), { currentId: 'M002' });
assert.equal(legacyParsed.schedule.currentId, 'M002');
assert.equal('competition' in legacyParsed, false);
const excelTime = XLSX.read(workbookBytes(), { type: 'array' });
excelTime.Sheets['赛程'].B2 = { t: 'n', v: 0.375, z: 'hh:mm' };
assert.equal(parseCompetitionWorkbook(XLSX.write(excelTime, { bookType: 'xlsx', type: 'array' })).schedule.items[0].time, '09:00');
const duplicate = XLSX.read(workbookBytes(), { type: 'array' });
XLSX.utils.sheet_add_aoa(duplicate.Sheets['选手'], [['A001', '重复选手', '', 'A', '男']], { origin: -1 });
assert.throws(() => parseCompetitionWorkbook(XLSX.write(duplicate, { bookType: 'xlsx', type: 'array' })), /选手编号重复/);
const broken = XLSX.read(workbookBytes(), { type: 'array' });
broken.Sheets['赛程'].A1.v = '错误表头';
assert.throws(() => parseCompetitionWorkbook(XLSX.write(broken, { bookType: 'xlsx', type: 'array' })), /表头与示例模板不一致/);
console.log('官网赛事 Excel 模板生成、导入、回写与错误校验通过');