feat: 分批发布 + 实时赛况页 + 显式批次层 + 成绩框实时提示 + 窄屏适配 + 版本号单一真相
## 分批发布(现场:100 米跑道只有 8 条,不能等所有人跑完才发) - 空成绩从"错误"降级为"提示",不再阻塞发布;真会算错(写法无法识别、秒位 ≥ 60、 手动名次缺名次)仍挡住。发布确认框说明"只发已填的 M 人,其余保持未发布"。 - 服务端核实:publishResults/computeEventScore 本就不会因缺成绩报错 (collectCandidates 直接跳过没有 results 的报名项),根因在前端防呆。 ## 实时赛况(新大屏页 + 定向接口) - 新增 `GET /api/display/live`(guest 可读):返回「当前项目」判定 + 该项目**全量**已发布成绩 (不受快照 latestResults 的 20 行截断)。回退链:manual 指定 > 排期命中 > 最近发布。 设置键 display.live_events / display.live_source。 - 大屏新页 `live`「实时赛况」:1 个项目单栏、2 个自动左右分栏;失败保留上一次画面 (下一场开始前持续显示上一场);30s 轮询(判定依赖当前时间)+ 快照变更触发。 - 管理端「大屏控制」新增「实时赛况」卡;该卡走 PUT /api/settings(admin-only), 故对非管理员整组置灰 + 提示条(不新增角色)。 ## 显式批次层(组别之下) - 迁移 0019 `entries.heat`;批次仅是录入/展示分组,**不参与算分**(有"分批与不分批结果 完全一致"的断言守住)。批量设置接口 + 导入 CSV 可选列 heat + 录入页批次下拉 + 一键均分(按 N 人切片,已有批次号接着往后排)。 ## 成绩输入框实时悬浮提示 - 聚焦格旁的浮动气泡:pointer-events:none、不占布局不改行高、贴底自动上翻; 合法显示解析结果(1:06 → = 66 秒)、可疑/不合法显示原因并带那一格原文; 与发布防呆共用同一套纯函数(entryGuards.ts),口径不会分叉。 ## 窄屏 / 手机端布局 - 真浏览器(Playwright + chromium)在 1440/1024/768/390 四宽度逐页审计 + 溢出探针, 四轮迭代修复;桌面端 1440 前后像素差异 < 0.25%(无回归);修掉如「修改密码」输入框 塌成 22px 这类真问题。 ## 版本号单一真相 - 新增仓库根 `VERSION`:构建时注入二进制(-X main.version)并同步 console/package.json 的版本(产物名跟随)。源码里不再有人工维护的版本号(兜底值改为 `dev`,故意不写数字)。 测试:服务端 13 包全绿;前端 vue-tsc 通过、249 例全过;Console 18 文件。 docs/ 与 devdocs/ 同步(FAQ 到 Q80、新增 devdocs/16 自定义大屏模板设计文档)。
This commit is contained in:
1 parent
e5e0944d26
commit
08a5740fb2
64 files changed
+4968
-270
No files matched your search
@@ -20,11 +20,12 @@ import {
|
||||
const data = useBaseData()
|
||||
const toast = useToastStore()
|
||||
|
||||
// heat 列是批次:留空 = 未分批,也可以直接填正整数(如 1、2)。
|
||||
const TEMPLATE_CSV = [
|
||||
REQUIRED_COLUMNS.join(','),
|
||||
'M100,男子组,F1,张伟,110101199001011234,男,26,101,,registered,',
|
||||
'W100,女子组,F2,王芳,,女,,202,,registered,',
|
||||
'BB,混合组,F2,,,,,,种子1,registered,第一分公司代表队',
|
||||
'M100,男子组,F1,张伟,110101199001011234,男,26,101,,1,registered,',
|
||||
'W100,女子组,F2,王芳,,女,,202,,2,registered,',
|
||||
'BB,混合组,F2,,,,,,种子1,,registered,第一分公司代表队',
|
||||
].join('\n')
|
||||
|
||||
const MAX_SIZE = 16 * 1024 * 1024
|
||||
@@ -189,8 +190,9 @@ function downloadTemplate(): void {
|
||||
<div class="bd-warn">
|
||||
<MxIcon name="warning" :size="14" />
|
||||
<span>
|
||||
表头为:{{ REQUIRED_COLUMNS.join(' / ') }}(大小写不敏感)。id_card、gender、age 可缺省,
|
||||
表头为:{{ REQUIRED_COLUMNS.join(' / ') }}(大小写不敏感)。id_card、gender、age、heat 可缺省,
|
||||
但有姓名的行必须填证件号或号码布之一,否则该行会报错并跳过。年龄可留空,填了必须是 0–120 的整数。
|
||||
heat 是批次号:可留空(表示未分批),填了必须是 1–999 的整数;批次只用于录入 / 展示分组,不影响名次与积分。
|
||||
证件号与号码布都是人员唯一标识:
|
||||
先按证件号匹配,未命中再按号码布匹配,仍未命中则按「姓名 + 单位」匹配库里证件号为空的人并回填。
|
||||
</span>
|
||||
|
||||
@@ -13,7 +13,11 @@ import type {
|
||||
ImportConflictField,
|
||||
} from '@/shared'
|
||||
|
||||
/** 与服务端 handlers_config.go 的 importColumns 一致,同时也是 CSV 模板表头。 */
|
||||
/**
|
||||
* 与服务端 handlers_config.go 的 importColumns 一致,同时也是 CSV 模板表头。
|
||||
*
|
||||
* heat 是批次列:可选,留空表示「未分批」;批次只做录入 / 展示分组,不参与算分。
|
||||
*/
|
||||
export const REQUIRED_COLUMNS = [
|
||||
'event_code',
|
||||
'category',
|
||||
@@ -24,12 +28,13 @@ export const REQUIRED_COLUMNS = [
|
||||
'age',
|
||||
'bib',
|
||||
'seed',
|
||||
'heat',
|
||||
'status',
|
||||
'member_names',
|
||||
] as const
|
||||
|
||||
/** 可选列:表头缺失不报错,但 athlete_name 非空的行必须带 id_card 或 bib 之一。 */
|
||||
export const OPTIONAL_COLUMNS = ['id_card', 'gender', 'age'] as const
|
||||
export const OPTIONAL_COLUMNS = ['id_card', 'gender', 'age', 'heat'] as const
|
||||
|
||||
/** 必须存在的表头;服务端缺任一列会直接拒绝整份文件。 */
|
||||
const OPTIONAL_SET: readonly string[] = OPTIONAL_COLUMNS
|
||||
@@ -92,6 +97,8 @@ export function buildSelectionRows(input: SelectionInput): string[][] {
|
||||
'',
|
||||
athlete.bib ?? '',
|
||||
'',
|
||||
// 批次留空 = 未分批:批次在录入页按现场编排(批量设置 / 一键均分)。
|
||||
'',
|
||||
status,
|
||||
'',
|
||||
])
|
||||
@@ -142,6 +149,8 @@ export function buildTeamSelectionRows(input: TeamSelectionInput): string[][] {
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
// 批次留空 = 未分批,与个人项目一致。
|
||||
'',
|
||||
status,
|
||||
team.memberNames.join('/'),
|
||||
])
|
||||
|
||||
Reference in new issue
Block a user