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
@@ -285,6 +285,69 @@ class DisplayManager {
|
||||
this.layout()
|
||||
}
|
||||
|
||||
// 窗口的当前全屏状态:以窗口为准,配置改了 kiosk 但窗口没重建时也能报对
|
||||
isKioskWindow(win) {
|
||||
if (!win || win.isDestroyed()) return false
|
||||
return Boolean(win.isKiosk() || win.isFullScreen())
|
||||
}
|
||||
|
||||
// 管理面板手动为某块屏开窗:已经开着就只聚焦,重建会打断正在展示的大屏
|
||||
openOn(display, { page, single } = {}) {
|
||||
const kiosk = !single && this.display.kiosk !== false
|
||||
const wantedPage = page || this.pageFor(0)
|
||||
|
||||
const existing = this.windows.get(display.id)
|
||||
if (existing && existing.win && !existing.win.isDestroyed()) {
|
||||
if (existing.win.isMinimized()) existing.win.restore()
|
||||
existing.win.show()
|
||||
existing.win.focus()
|
||||
// 已有窗口按它的真实状态报模式(可能与本次请求不同),面板才不会说反
|
||||
return { created: false, mode: this.isKioskWindow(existing.win) ? 'kiosk' : 'windowed' }
|
||||
}
|
||||
// 已销毁的窗口占着 map 会让后续 layout 静默跳过,先摘掉旧 entry 再建
|
||||
if (existing) {
|
||||
if (existing.reloadTimer) clearInterval(existing.reloadTimer)
|
||||
this.windows.delete(display.id)
|
||||
}
|
||||
|
||||
// 手动开的窗口也计入 enabled:服务停止时 closeAll 才会把它一起收走
|
||||
this.enabled = true
|
||||
this.windows.set(display.id, this.createWindow(display, 0, !kiosk, wantedPage))
|
||||
return { created: true, mode: kiosk ? 'kiosk' : 'windowed' }
|
||||
}
|
||||
|
||||
// 关掉某块屏上的大屏窗口:kiosk 窗口刻意忽略鼠标,误开后只能从这里收
|
||||
closeDisplay(displayId) {
|
||||
const entry = this.windows.get(displayId)
|
||||
if (!entry) return false
|
||||
this.destroyEntry(displayId, entry)
|
||||
return true
|
||||
}
|
||||
|
||||
// 只读枚举显示器与窗口状态,给管理面板的显示器菜单用,不改动任何窗口
|
||||
listDisplays() {
|
||||
const all = screen.getAllDisplays()
|
||||
const primary = screen.getPrimaryDisplay()
|
||||
const list = all.map((display, index) => {
|
||||
const entry = this.windows.get(display.id)
|
||||
const open = Boolean(entry && entry.win && !entry.win.isDestroyed())
|
||||
return {
|
||||
id: display.id,
|
||||
label: display.label || `显示器 ${display.id}`,
|
||||
primary: display.id === primary.id,
|
||||
// 一律给 DIP,与窗口坐标同一口径;物理像素对不上时看 scaleFactor
|
||||
width: display.bounds.width,
|
||||
height: display.bounds.height,
|
||||
scaleFactor: display.scaleFactor,
|
||||
open,
|
||||
page: open ? entry.page : this.pageFor(index),
|
||||
mode: open ? (this.isKioskWindow(entry.win) ? 'kiosk' : 'windowed') : null
|
||||
}
|
||||
})
|
||||
// 单屏机器上谈「扩展屏全屏」没有意义,与 pickDisplays 同一口径
|
||||
return { single: all.length <= 1, displays: list }
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return [...this.windows.entries()].map(([displayId, entry]) => ({
|
||||
displayId,
|
||||
|
||||
Reference in new issue
Block a user