feat: 人员年龄/号码布、图片上传、多显示器菜单、Console 两栏、exe 改名等一批改动
人员与报名:
- 人员唯一标识改为「证件号 或 号码布」二选一(0013/0014 迁移 + 导入匹配/回填/冲突)
- 人员新增「年龄」(0015 迁移):表单/列表/导入模板表头 11 列;冲突比较只在两边
都有值且不同时才算差异,旧数据不误报
- 冲突决策键改为「证件号优先 → 号码布」(前端原先只用 idCard,号码布行会互相覆盖)
- 报名选择器支持无证件号但有号码布的运动员
大屏与 Console:
- 大屏控制新增「显示器」菜单:可给指定显示器重建全屏窗口;单屏/主显示器强制窗口化,
限制在 Console 侧判定,不信任页面传参
- Console 服务管理窗口改左右两栏,运行日志独占右栏满高;窗口默认尺寸 1280×860
- 大屏右上角时钟精确到秒(同时把刷新间隔 15s 改为 1s)
图片与配置:
- 新增图片上传 POST /api/uploads(recorder+,png/jpg/webp/gif,≤5MiB)与匿名静态
访问 GET /uploads/{name}(防目录穿越);文件落赛事项目目录 uploads/,随赛事包
一起导出/导入
- 「赛事信息」新增赛事会标 / 大屏会标 / 大屏背景图三个上传项
- 参赛单位 logo 输入框右侧加上传按钮
关于页与构建:
- 构建时间打通:version.BuildTime + /api/system/about 返回 + build.bat 注入
(用 PowerShell 取时间戳,避免本地化 %DATE% 的非 ASCII 星期)
安装包:
- 主程序改名 Scoreboard_Console.exe(win.executableName;productName/显示名不变)
- 记住上次安装目录:自定义 NSIS 宏优先沿用自存路径(卸载不清除),并在 customInit
里补写回 electron-builder 的键,避免安装模式页把 $INSTDIR 打回默认
自检:服务端 12 包全绿;前端 vue-tsc + build 通过、node --test 58/58;console 12 文件通过。
This commit is contained in:
1 parent
24fff8dd08
commit
6483acf9ad
50 files changed
+2980
-395
No files matched your search
@@ -144,6 +144,17 @@ export interface EntriesApi {
|
||||
resolveImport(file: File, decisions: ImportConflictDecision[]): Promise<EntryImportResult>
|
||||
}
|
||||
|
||||
/** POST /api/uploads 的响应:path 是服务端存储后的路径,url 与其相同,可直接放进 <img src>。 */
|
||||
export interface UploadResult {
|
||||
path: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface UploadsApi {
|
||||
/** 仅图片(png / jpeg / webp / gif),单文件上限由服务端限制。 */
|
||||
upload(file: File): Promise<UploadResult>
|
||||
}
|
||||
|
||||
export interface ResultsApi {
|
||||
list(query: ResultQuery): Promise<Page<Result>>
|
||||
saveDraft(payload: SaveDraftPayload): Promise<Result[]>
|
||||
@@ -201,6 +212,12 @@ export interface SystemApi {
|
||||
backup(): Promise<BackupResult>
|
||||
}
|
||||
|
||||
/** GET /api/system/about 的响应(recorder 及以上可读)。注入字段缺失或未注入时为空串。 */
|
||||
export interface SystemAbout {
|
||||
internalSignature?: string
|
||||
buildTime?: string
|
||||
}
|
||||
|
||||
interface Http {
|
||||
request<T>(path: string, options?: RequestOptions): Promise<T>
|
||||
requestVoid(path: string, options?: RequestOptions): Promise<void>
|
||||
@@ -324,6 +341,7 @@ export class ApiClient {
|
||||
readonly auditLogs: AuditApi
|
||||
readonly rules: RulesApi
|
||||
readonly system: SystemApi
|
||||
readonly uploads: UploadsApi
|
||||
|
||||
private readonly baseUrl: string
|
||||
private readonly credentials: RequestCredentials
|
||||
@@ -429,6 +447,14 @@ export class ApiClient {
|
||||
this.system = {
|
||||
backup: () => http.request<BackupResult>('/backup', { method: 'POST' }),
|
||||
}
|
||||
|
||||
this.uploads = {
|
||||
upload: (file) => {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
return http.request<UploadResult>('/uploads', { method: 'POST', body: form })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 健康检查不走 {ok,data} 包装,直接返回 `{ok,version}`(05 第 7 节)。 */
|
||||
|
||||
@@ -102,8 +102,8 @@ export function formatDateTime(iso: string | null): string {
|
||||
/** ISO → `HH:mm`,大屏顶栏时钟用。 */
|
||||
export function formatClock(iso: string | null): string {
|
||||
const date = toDate(iso)
|
||||
if (date === null) return '--:--'
|
||||
return `${DATE_PAD(date.getHours())}:${DATE_PAD(date.getMinutes())}`
|
||||
if (date === null) return '--:--:--'
|
||||
return `${DATE_PAD(date.getHours())}:${DATE_PAD(date.getMinutes())}:${DATE_PAD(date.getSeconds())}`
|
||||
}
|
||||
|
||||
/** 带时区后缀(Z 或 ±HH:MM)的时间串按原样解析。 */
|
||||
|
||||
@@ -24,5 +24,5 @@ test('formatSeconds 在 fractionDigits=0 时秒数仍补足两位', () => {
|
||||
|
||||
test('formatDateTime 对空值返回占位', () => {
|
||||
assert.equal(formatDateTime(null), '—')
|
||||
assert.equal(formatClock(null), '--:--')
|
||||
assert.equal(formatClock(null), '--:--:--')
|
||||
})
|
||||
+14
-2
@@ -27,8 +27,12 @@ export interface Athlete {
|
||||
gender: Gender | null
|
||||
teamId: number
|
||||
employeeNo: string | null
|
||||
/** 人员唯一标识(证件号);历史数据可能为空。 */
|
||||
/** 人员唯一标识之一(证件号);与 bib 二者取其一即可,历史数据可能都为空。 */
|
||||
idCard: string | null
|
||||
/** 人员唯一标识之一(号码布);与 idCard 二者取其一即可。 */
|
||||
bib: string | null
|
||||
/** 年龄;可空,历史数据与团体报名都可能没有。 */
|
||||
age: number | null
|
||||
active: boolean
|
||||
}
|
||||
|
||||
@@ -156,17 +160,23 @@ export interface EntryWithResult extends Entry {
|
||||
export interface ImportConflictFields {
|
||||
name: string
|
||||
gender: Gender | null
|
||||
/** 年龄;两侧都有值且不同才算差异,空值视为未指定。 */
|
||||
age: number | null
|
||||
teamCode: string
|
||||
employeeNo: string | null
|
||||
/** 人员级号码布;参与冲突差异展示。 */
|
||||
bib: string | null
|
||||
}
|
||||
|
||||
export type ImportConflictField = keyof ImportConflictFields
|
||||
|
||||
/** 同一证件号在库里与报名表中的人员信息不一致;本轮不落库,等用户决策。 */
|
||||
/** 同一证件号 / 号码布在库里与报名表中的人员信息不一致;本轮不落库,等用户决策。 */
|
||||
export interface ImportConflict {
|
||||
/** 报名表原始行号(含表头,从 1 开始)。 */
|
||||
row: number
|
||||
idCard: string
|
||||
/** 报名表该行的号码布;证件号为空时它就是这条冲突的决策键。 */
|
||||
bib: string
|
||||
athleteId: number
|
||||
existing: ImportConflictFields
|
||||
incoming: ImportConflictFields
|
||||
@@ -176,8 +186,10 @@ export interface ImportConflict {
|
||||
/** keep-existing = 保留库里已有的人员字段;keep-new = 用表里的字段更新该人员。 */
|
||||
export type ConflictAction = 'keep-existing' | 'keep-new'
|
||||
|
||||
/** 决策键由服务端判定:证件号优先,为空时取号码布,因此两个字段都要带。 */
|
||||
export interface ImportConflictDecision {
|
||||
idCard: string
|
||||
bib: string
|
||||
action: ConflictAction
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user