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:
AskaEth committed 2026-09-11 04:05:02 +08:00
1 parent e5e0944d26
commit 08a5740fb2
64 files changed
+4968 -270

No files matched your search

+12 -3
View File
@@ -27,6 +27,11 @@ call pnpm build || exit /b 1
cd ..
echo === [3/4] server build ===
REM version: repo-root VERSION is the single source of truth. The Node script
REM writes deploy\version.gen.bat (pure ASCII, for cmd) and syncs the installer
REM version in console\package.json. Must run before the go build below.
node deploy\prepare-extra.mjs || exit /b 1
call deploy\version.gen.bat || exit /b 1
cd server
if not exist ..\dist mkdir ..\dist
REM internal signature is injected from a machine-local file (gitignored, never in repo).
@@ -36,6 +41,9 @@ REM deploy\signature.build.txt, and for /f reads that file directly (no re-encod
node ..\deploy\prepare-signature.mjs || exit /b 1
set "INTERNAL_SIGNATURE="
if exist "..\deploy\signature.build.txt" for /f "usebackq delims= eol=" %%A in ("..\deploy\signature.build.txt") do if not defined INTERNAL_SIGNATURE set "INTERNAL_SIGNATURE=%%A"
REM compile-time version: -X main.version targets package main (no import path),
REM unlike the two vars below which are import-path-qualified.
set "VERSION_LDFLAG=-X 'main.version=%SCOREBOARD_VERSION%'"
REM build time for the About page. Do NOT use %DATE%/%TIME%: a localized Windows
REM prints non-ASCII weekday names, which cmd mis-parses (see the header note).
set "BUILD_TIME="
@@ -43,14 +51,15 @@ for /f "usebackq delims=" %%T in (`powershell -NoProfile -Command "(Get-Date).To
set "BUILD_TIME_LDFLAG=-X 'git.yeij.top/AskaEth/Scoreboard/server/internal/version.BuildTime=%BUILD_TIME%'"
if defined INTERNAL_SIGNATURE (
REM single quotes keep spaces inside the value intact for go's ldflags parser
go build -ldflags "-X 'git.yeij.top/AskaEth/Scoreboard/server/internal/version.InternalSignature=%INTERNAL_SIGNATURE%' %BUILD_TIME_LDFLAG%" -o ..\dist\scoreboard.exe . || exit /b 1
go build -ldflags "-X 'git.yeij.top/AskaEth/Scoreboard/server/internal/version.InternalSignature=%INTERNAL_SIGNATURE%' %VERSION_LDFLAG% %BUILD_TIME_LDFLAG%" -o ..\dist\scoreboard.exe . || exit /b 1
node ..\deploy\verify-signature.mjs || exit /b 1
echo internal signature injected
) else (
go build -ldflags "%BUILD_TIME_LDFLAG%" -o ..\dist\scoreboard.exe . || exit /b 1
go build -ldflags "%VERSION_LDFLAG% %BUILD_TIME_LDFLAG%" -o ..\dist\scoreboard.exe . || exit /b 1
echo no internal signature
)
echo build time %BUILD_TIME%
echo version %SCOREBOARD_VERSION%
cd ..
echo === [4/4] console package (electron-builder) ===
@@ -64,7 +73,7 @@ if not exist "rules" (
echo [ERROR] rules dir missing - electron-builder extraFiles needs it
exit /b 1
)
node deploy\prepare-extra.mjs || exit /b 1
REM prepare-extra.mjs already ran in step 3 (version, user docs, installer script).
cd console
REM console\pnpm-workspace.yaml uses allowBuilds so electron's install script is
REM allowed to run; do NOT add --ignore-scripts here, it makes pnpm run fail its
+49 -2
View File
@@ -1,7 +1,9 @@
// 构建前准备两样东西:
// 构建前准备三样东西:
// 1) 把 docs/ 下的使用说明与报名名单导入规范暂存到 console/extra/,供 electron-builder 的 extraFiles 平铺到部署包根目录;
// 2) 生成安装器自定义脚本(console/resources/installer-custom.nsh):许可页顶部文案(署名来自构建机本地文件)
// + 记住上次安装目录(重装时沿用,而不是退回系统默认路径)。
// + 记住上次安装目录(重装时沿用,而不是退回系统默认路径);
// 3) 读仓库根 VERSION 文件:生成 deploy/version.gen.bat(供 build.bat 注入 Go 二进制)
// 并同步 console/package.json 的 version(决定安装包/免安装包的文件名)。
// 用 Node 而不是批处理:中文文件名/中文内容在 .bat 里会被 cmd 解析错乱。
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
@@ -104,3 +106,48 @@ writeFileSync(nshFile, nsh, 'utf8')
// 改名前生成的旧脚本已经没人引用,构建机上顺手清掉,免得看起来还在用。
rmSync(legacyNshFile, { force: true })
console.log(` installer custom script ready (${existsSync(signatureFile) ? 'with' : 'without'} internal signature)`)
// ---- 3. version -------------------------------------------------------------
// 版本号只维护仓库根的 VERSION 文件(唯一真相):这里读出来分两路用——
// deploy/version.gen.bat 给 build.bat,编译期用 -X main.version 写进二进制;
// console/package.json electron-builder 据此给安装包/免安装包命名。
// 二进制里的版本在编译那一刻定死,运行期不再读任何文件。
const versionFile = join(root, 'VERSION')
if (!existsSync(versionFile)) {
console.error(' [version] missing VERSION file at repo root')
process.exit(1)
}
const version = (readFileSync(versionFile, 'utf8')
.replace(/^\uFEFF/, '')
.split(/\r?\n/)
.find((line) => line.trim() !== '') ?? '').trim()
if (version === '') {
console.error(' [version] VERSION file is empty')
process.exit(1)
}
// 必须纯 ASCII:cmd.exe 会错解 .bat 里的非 ASCII 文本(见本文件头部说明)。
// 用 CRLF:与 .gitattributes 里 *.bat 的约定一致,cmd 在 Windows 上按 CRLF 读最稳。
// 只 set 不加 setlocal:build.bat 用 call 调它,变量要留在调用方的环境里。
const genBatFile = join(root, 'deploy', 'version.gen.bat')
const genBat = [
'@echo off',
'REM Generated by deploy/prepare-extra.mjs from the VERSION file - do not edit.',
`set "SCOREBOARD_VERSION=${version}"`,
'',
].join('\r\n')
writeFileSync(genBatFile, genBat, 'utf8')
console.log(` version ${version} -> deploy/version.gen.bat`)
// console/package.json 的 version 是 semver,不带 dev_ 前缀(dev_ 只用于二进制版本号)。
// 逐字替换而不是 JSON.parse/stringify:保住原有缩进与末尾换行,改动最小。
const pkgVersion = version.replace(/^dev_/, '')
const pkgFile = join(root, 'console', 'package.json')
const pkgRaw = readFileSync(pkgFile, 'utf8')
const pkgNext = pkgRaw.replace(/^(\s*"version"\s*:\s*)"[^"]*"/m, `$1"${pkgVersion}"`)
if (pkgNext === pkgRaw) {
console.log(` console/package.json version is ${pkgVersion}`)
} else {
writeFileSync(pkgFile, pkgNext, 'utf8')
console.log(` console/package.json version -> ${pkgVersion}`)
}