## ① 现场问题:kiosk 窗口点击穿透(原设计方向反了) `displays.js` 原来对多屏 kiosk 调 `setIgnoreMouseEvents(true)`,注释写的是 「防止现场误触」——但该 API 的语义是「本窗口忽略鼠标」,点击会**落到它后面的 窗口或桌面**,现场想防的误触正好变成误点别的东西;被忽略鼠标的窗口也点不活。 现在窗口**默认吞掉鼠标、不穿透**。 配置项**改名**为 `display.allowClickThrough`(缺省 false;显式 true 才穿透)。 **旧键 `display.ignoreMouse` 完全不读**——老机器的配置文件里还写着旧版模板留下的 `ignoreMouse: true`,而配置策略是「已存在不覆盖」,只改默认值等于老机器升级后 照样穿透;改名才能让默认值真正生效。启动时若发现旧键会记一条中文提示(只记一次), 并在合并后的配置里把它剥掉,防止将来有人再读到那个陈年 true。 (不是自动改写用户配置文件:无法区分「操作员刻意写的 true」与「旧模板留下的 true」。) ## ② 现场问题:任务栏盖住 kiosk 窗口、被盖住后抢不回前台 原顺序是 `setKiosk(true)` 之后又 `setBounds(...)`,在 Windows 上会把窗口从真正的 全屏态拽回「恰好铺满该屏的无边框窗口」→ 不是 topmost,任务栏与拖过去的窗口都盖得住。 现在固定为 **先 `setBounds` 定位 → 再 `setKiosk(true)` → 最后 `setAlwaysOnTop(true, 'screen-saver')`**(默认层级 `floating` 在 Windows 上仍画在任务栏下面),并加了 「已达标就不重复进出 kiosk」的守卫避免重排白闪;非 kiosk 分支会摘掉置顶。 Win 真机行为需现场复核,代码/桩级已验(含顺序与层级的断言)。 ## ③ 新功能:大屏控制页「局域网大屏」卡片 - 服务端新增 `GET /api/system/network`(recorder 及以上):回本机可用局域网 IPv4 (排除回环/link-local/IPv6,去重,按 192.168/10/172.16-31 优先排序)+ 真实监听端口 + 路径;无地址回 `[]` 不报错。 - 卡片:每个地址一条可点击链接 + 复制按钮,第一个地址配二维码(148px)。 - **二维码是离线自实现的纯 TS 编码器**(版本 1–40 自动、ECC L/M/Q/H、8 种掩码择优), 正确性用 **Python OpenCV 真解码**验收:29/29 条解出原文(含中文与边界), SVG 渲染后再解码 4/4,并把验证过的矩阵冻结成仓库参照用例。 - 复制按**非安全上下文**处理:面板常从 `http://192.168.x.x` 打开,那里 `navigator.clipboard` 是 undefined,自动回退 `execCommand('copy')`。 ## ④ 真机联调抓出并修掉一个时序缺陷(真浏览器 + 真服务端) `DisplayPage` 的 `refreshLan()` 在挂载时跑,而 `auth` 是异步加载的——那一刻 `auth.readOnly` 必然为 true,卡片把「当前为只读访客」写进一个**普通 ref** 就返回了, 身份落定后没有任何东西再触发它:**管理员登录后卡片永远停在访客态, `/api/system/network` 一次都没被请求**。 改为「gate 由 computed 直接渲染 + watch gate 变 null 时重跑加载」,并补回归钉子。 (同时修正了一条把该错误结构当成正确行为的源码断言用例。) 端到端证据:真服务 + 局域网 IP(真非安全上下文)→ 卡片列出两个地址、点击复制变 「已复制」(走 execCommand 回退)→ 浏览器渲染的二维码截图经 cv2 解出 `http://10.50.101.85:19861/display`。 ## 文档 部署指南配置表与多屏行为、使用手册「大屏控制」与局域网设备段、devdocs 05/07/10/13、 升级说明新增 dev_0.4.0(含旧键废弃与启动提示)。**无数据迁移**,旧赛事包照常导入。 自检:服务端 13 包 ×2;前端 vue-tsc + 645 例 ×2 + build;Console 20 文件 + 68 例。
210 lines
24 KiB
Markdown
210 lines
24 KiB
Markdown
# Scoreboard Console
|
||
|
||
赛事积分榜系统的桌面宿主(Electron)。它只做六件事:
|
||
|
||
1. 拉起并守护同目录(或配置指定)的 `scoreboard.exe`,把它的 stdout/stderr 接管到管理窗口与日志文件;
|
||
2. 用 `screen.getAllDisplays()` 枚举显示器,把大屏页面投到扩展屏;
|
||
3. `powerSaveBlocker.start('prevent-display-sleep')` 防熄屏;
|
||
4. 提供启动/停止/状态/日志的管理 GUI 和托盘菜单;
|
||
5. 默认注册登录项但不启用(不随系统启动),可在服务管理窗口的「开机自启」开关里开启;
|
||
6. 在服务管理窗口展示服务端在线的大屏实例(`GET /api/display/instances`)。
|
||
|
||
规范来源:`devdocs/06-大屏端设计.md` 第 6 节、`devdocs/10-部署与现场方案.md` 第 4 节。
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
console/
|
||
├── package.json # main 指向 main/index.js;electron-builder 配置
|
||
├── config.json # 运行配置模板(打包后首次启动复制到用户数据目录,改这里的只在开发态生效)
|
||
├── main/
|
||
│ ├── index.js # 应用入口:窗口 / 托盘 / IPC / 生命周期
|
||
│ ├── config.js # 配置加载与合并、exe 路径解析
|
||
│ ├── datadir.js # 数据目录解析 + 旧数据一次性迁移
|
||
│ ├── datadir.test.js # 数据目录的单元测试
|
||
│ ├── userpaths.js # 配置/规则目录解析 + 首次复制迁移(config.json / config.yaml / rules)
|
||
│ ├── userpaths.test.js # 配置/规则路径的单元测试
|
||
│ ├── service.js # 服务端子进程守护:spawn / 日志转发 / 健康检查 / 自动重启
|
||
│ ├── service.test.js # 守护逻辑的单元测试(注入假 spawn/时钟/HTTP)
|
||
│ ├── instances.js # 已连接大屏实例看板:复用管理面板登录态读 /api/display/instances
|
||
│ ├── account.js # 管理员账号重置:参数校验 + 调 scoreboard.exe passwd
|
||
│ ├── displays.js # 大屏窗口管理:选屏、kiosk、排布、24h 重载
|
||
│ ├── displays.test.js # 大屏窗口模式与 URL 一致性的回归测试
|
||
│ ├── log-bus.js # 日志环形缓冲 + 落文件 + 广播
|
||
│ └── tray-icon.js # 托盘图标(base64 内嵌,无外部资源文件)
|
||
├── scripts/
|
||
│ └── check-syntax.js # pnpm check:跨平台递归 node --check
|
||
├── extra/ # 打包前由 deploy/prepare-extra.mjs 暂存 使用说明.txt 与 报名名单导入规范.txt(extraFiles 指向它)
|
||
└── renderer/
|
||
├── index.html # 管理窗口(含自绘标题栏)
|
||
├── styles.css
|
||
├── app.js
|
||
├── preload.js # 管理窗口的 contextBridge 白名单
|
||
├── display-preload.js # 大屏窗口的 window.displayShell 白名单(窗口级全屏状态,供页面收放自绘标题栏)
|
||
└── shell-preload.js # 服务端管理面板窗口的 window.desktopShell 白名单(窗口控制 + 显示器菜单)
|
||
```
|
||
|
||
## 运行方式
|
||
|
||
```bash
|
||
cd console
|
||
pnpm install # 首次需要下载 Electron
|
||
pnpm start # 等价于 electron .
|
||
```
|
||
|
||
开发时把 `scoreboard.exe` 放在以下任一位置都能被找到(`config.json` 里的相对路径会按顺序探测):
|
||
|
||
1. `console/`
|
||
2. 当前工作目录
|
||
3. 仓库根目录
|
||
4. `dist/`
|
||
|
||
现场(部署包)形态:
|
||
|
||
```
|
||
scoreboard-windows-amd64/
|
||
├── Scoreboard_Console.exe
|
||
├── resources/app.asar # 内含 main/ renderer/ config.json
|
||
├── config.json # exe 旁边的配置模板(首次启动复制到用户数据目录)
|
||
├── scoreboard.exe
|
||
├── config.yaml # 服务端配置模板(同上)
|
||
├── rules/ # 规则包模板(同上)
|
||
└── logs/console.log # 运行时生成(日志仍留在安装目录)
|
||
```
|
||
|
||
数据、配置与规则都不在安装目录里,都在 `%LOCALAPPDATA%\ScoreboardConsole\` 下(见下文两节)。
|
||
|
||
打包后 `config.json` 读取顺序:`SCOREBOARD_CONSOLE_CONFIG` 环境变量 → 用户数据目录 `<用户根>\console\config.json`(首次启动从 exe 旁边的模板复制)。开发态(`pnpm start`)三者都不接管,仍用仓库里的 `console/config.json`。
|
||
|
||
## 数据目录(重要)
|
||
|
||
electron-builder 生成的卸载器结尾是 `RMDir /r $INSTDIR`,而**覆盖安装会先静默跑一遍旧版卸载器**,所以装在安装目录里的 `data\` 会被整体删掉;`%APPDATA%\Scoreboard Console` 卸载时同样会删,因此也不能用 `app.getPath('userData')`。最终方案:
|
||
|
||
| 场景 | 数据目录 |
|
||
| --- | --- |
|
||
| 打包态,`server.dataDir` 留空(默认) | `%LOCALAPPDATA%\ScoreboardConsole\data`(卸载器不碰这个位置) |
|
||
| 配置了 `server.dataDir` | 配什么用什么:绝对路径直接用;相对路径以安装目录为基准 |
|
||
| 开发态(`pnpm start`),`server.dataDir` 留空 | 不接管,沿用服务端默认(工作目录下的 `data`),仓库里的 `data/` 不受影响 |
|
||
|
||
- Console 把最终目录通过服务端的 `-data <绝对路径>` 传下去,`passwd` 子命令带同一个参数,保证改的是同一个库里的账号;
|
||
- 迁移只在**打包态**做一次:**目标目录为空(或不存在)且 `<安装目录>\data` 有数据**时,用 `fs.renameSync` 把旧目录整体改名过去(同盘秒完成)。跨盘、权限不足等任何失败都只记日志,旧数据原地保留;
|
||
- **「没有旧数据」与「读不到旧目录」分开处理**:旧目录不存在/存在但空才是 `no-legacy`(不写日志);旧目录读不出来(EACCES、ENOTDIR 等)返回 `legacy-unreadable` 并写一条「读不到旧数据目录」警告,绝不当成「没有旧数据」静默跳过(回归测试见 `main/datadir.test.js`);
|
||
- 两边都有数据时**不做任何动作**,只写一条醒目日志提示人工确认,两份都不会被自动删除;
|
||
- 迁移结果与最终数据目录都写进 `logs/console.log`(搜「数据目录」「已把旧数据从」);
|
||
- 数据目录若仍在安装目录内,启动时会额外打一条警告。
|
||
|
||
改完 `datadir.js` 跑 `node --test main/datadir.test.js`(Node 内置测试运行器,无额外依赖)。
|
||
|
||
## 配置与规则目录(重要)
|
||
|
||
同样因为卸载器会删掉整个安装目录,随包的 `config.json`、`config.yaml`、`rules\` 也不放在安装目录里,而是跟数据目录同一基座:
|
||
|
||
| 内容 | 打包态(`planUserPaths` mode=`user`) | 开发态(mode=`install`) |
|
||
| --- | --- | --- |
|
||
| Console 配置 | `<用户根>\console\config.json` | 仓库 `console/config.json` |
|
||
| 服务端配置 | `<用户根>\config.yaml` | 仓库 `config.yaml` 的位置不变 |
|
||
| 规则目录 | `<用户根>\rules\` | 仓库 `rules/` |
|
||
|
||
(`<用户根>` = `%LOCALAPPDATA%\ScoreboardConsole`,与数据目录同一处。)
|
||
|
||
- `userpaths.js` 只做三件事:解析这三个落点、首次复制、失败回落;`planUserPaths` 是纯函数(只算路径),`prepareUserPaths` 才碰磁盘,便于单测;
|
||
- **首次运行复制**:目标不存在且安装目录里有那份模板 → `fs.copyFileSync` / `fs.cpSync`(规则目录先拷到 `<target>.tmp-<pid>` 再整体 rename,失败不会留下半份)。安装目录那份**留作模板**,不做移动;
|
||
- **已存在就绝不覆盖**:用户目录里已经有的那份一律优先,既不覆盖也不删除(用户改过的配置、自己加的规则包都不会被模板顶掉);
|
||
- 拉起服务端时 `-config` 的值被换成 `<用户根>\config.yaml` 的绝对路径(`withConfigArg`:参数里有 `-config` 就改值,没有就补一条),`passwd` 用同一份;这样服务端读的配置不再取决于 cwd;
|
||
- 任一项目标目录建不出来或不可写时只记日志并**回落到安装目录里那份**(`serverConfig` 回落时返回 `null`,拉起服务端时沿用 `server.args` 原值),绝不抛异常、绝不删用户文件;
|
||
- 拿不到 `%LOCALAPPDATA%` 时三项全回落安装目录并记一条警告;
|
||
- 日志文件 `logs/console.log` 是个例外:仍然相对**部署包根目录**(打包态 = 安装目录,开发态 = `console/`)解析,不跟着配置搬(易失的排查材料,搬了反而与文档、现场习惯不一致)。
|
||
|
||
改完 `userpaths.js` 跑 `node --test main/*.test.js`。
|
||
|
||
## 行为说明
|
||
|
||
| 场景 | 行为 |
|
||
| --- | --- |
|
||
| 只有一块屏 | 窗口化(屏幕 80% 大小、居中),不做多屏编排,**不跟随** `display-metrics-changed`(分辨率/缩放变化不重排,行为冻结);`display-added` / `display-removed` 一直听着,电视插回来会自动排布回 kiosk。监听常驻、只有 `closeAll()`(服务停止 / 应用退出)才摘(回归测试见 `main/displays.test.js`);`F11` 手动切换全屏,窗口级全屏状态经 `display-preload.js` 下发给页面,页面据此收起自绘标题栏。窗口化窗口**不置顶、不穿透**:F11 全屏完全由用户控制,置顶会把它变成一块收不掉、盖住一切的覆盖层 |
|
||
| 模式与 URL | **必须永远一致**:模式(窗口化 / kiosk)记在窗口的 entry 上,谁改模式谁按新模式重载 URL。`layout()` 只对排布自己开的窗口套用全局 `single`,管理面板手动开的窗口保持自己的模式——否则会出现「窗口化窗口被改成 kiosk 全屏、URL 里还留着 `windowed=1`」这种页面认知与窗口真实模式脱钩、标题栏画反的问题(回归测试见 `main/displays.test.js`)|
|
||
| 多块屏 | 除内置屏外的每块屏一个 kiosk 窗口,按 `display.bounds`(DIP)定位,光标由页面 CSS 隐藏。窗口**默认吞掉鼠标、不穿透**:`setIgnoreMouseEvents(true)` 的语义是「本窗口忽略鼠标」,点击会落到它后面的窗口或桌面上(现场想防的误触反而变成误点别的东西),被忽略鼠标的窗口也点不活、抢不回前台;只有确实要把大屏当纯展示层、同一块屏上还要操作别的窗口时才显式配 `display.allowClickThrough: true`。这个开关是从 `display.ignoreMouse` **改名**来的:旧版默认穿透,而且旧版模板已经把 `ignoreMouse: true` 写进了现场机器的用户配置(`userpaths.js` 不覆盖用户文件),只改默认值救不了老机器——换个新键名,旧键自然失效;启动时若发现配置里还留着旧键,会在 `logs/console.log` 里记一条提示(只记一次),说明它已废弃、现在默认不穿透、想穿透该写哪个键。窗口**置顶在 `screen-saver` 层级**(Windows 上 `setAlwaysOnTop(true)` 的默认层级 `floating` 仍画在任务栏下面,只有 pop-up-menu 及以上才压得住任务栏),所以别的窗口拖到这块屏上盖不住它、任务栏也盖不住它,被盖住后点一下就能回到最前。调用顺序**必须**是「先 `setBounds` 定位 → 再 `setKiosk(true)` → 最后置顶」:反过来会把窗口从真正的全屏态拽回「恰好铺满该屏的无边框窗口」,既不是 topmost、也压不住任务栏(回归测试见 `main/displays.test.js`)|
|
||
| 选屏 | 运行时枚举,不写死坐标;`selection` 可选 `all-secondary`(默认,用 `display.internal` 排除内置屏)/ `all` / `specified` |
|
||
| 页面锁定 | 每块屏各锁一页,URL 形如 `http://127.0.0.1:19540/display?page=standings&lock=1&screen=<id>&instance=<host>-<id>`,页序按 `display.pages` 轮转 |
|
||
| 防熄屏 | Console 启动即生效,退出时释放;前台后台都算 |
|
||
| 服务端就绪 | 首次 `/api/health` 通过后开大屏窗口;等待超过 `display.waitForServerMs` 也先开,避免现场只见桌面 |
|
||
| 服务端崩溃 | 按 `restartDelayMs` **线性**退避(第 N 次等 N × `restartDelayMs`,最多 5 倍)自动重启(`maxRestarts=0` 为无限);持续健康 `healthyResetMs` 后崩溃计数清零;恢复健康后自动重载大屏 |
|
||
| 服务端按设计退出 | 切换赛事 / 一键恢复时服务端**先回执 200 再自己优雅退出**(退出码 0)。这种退出**不当崩溃**:状态是「重启中」(黄灯,不是红灯)、崩溃计数不 +1、同样按 `restartDelayMs` 自动拉起。判定见 `service.js` 的 `isPlannedExit`:退出码 0 **且**落在预期退出窗口内(最后一次健康检查通过后最多 15 秒,HealthInterval 更大时按两个探测周期放宽)——刚起来就退 0 或卡死很久才退 0 都仍按崩溃处理 |
|
||
| 数据目录 | 默认 `%LOCALAPPDATA%\ScoreboardConsole\data`(安装目录之外,覆盖安装/卸载都不删),启动时按 `-data <绝对路径>` 交给服务端;打包态首次启动做一次性迁移(见「数据目录」一节),开发态不接管 |
|
||
| 配置与规则 | 同样在 `%LOCALAPPDATA%\ScoreboardConsole\`(`console\config.json`、`config.yaml`、`rules\`),打包态首次从安装目录复制、之后只用用户目录那份;拉起服务端时 `-config` 传绝对路径(见「配置与规则目录」一节) |
|
||
| 停止服务 | 先 `POST server.shutdownPath` 并带 `X-Shutdown-Token`(启动时用 `crypto.randomBytes(32)` 生成,经环境变量 `SCOREBOARD_SHUTDOWN_TOKEN` 只交给该子进程)请求服务端优雅关闭;受理后最多等 `shutdownTimeoutMs`,超时或接口不可用(404/连接失败)再强杀 |
|
||
| 已连接大屏实例 | 管理窗口打开时每 5 秒读一次服务端 `GET /api/display/instances`(复用管理面板的 `scoreboard_session` Cookie,需记录员及以上),显示实例数、页面名与在线状态;窗口隐藏即停表,未登录/服务未启动时显示占位文案 |
|
||
| 关管理窗口 | 只隐藏到托盘,服务端继续运行;退出必须走托盘「退出」 |
|
||
| 开机自启 | 默认「已注册但未启用」;在服务管理窗口的「开机自启」开关里控制(`startup.openAtLogin`),仅打包后的 Windows/macOS 生效,开发态不注册 |
|
||
| 窗口外观 | 管理窗口与服务端管理面板窗口都是 `frame: false` 无边框,标题栏与最小化/最大化/关闭三键由页面自绘(拖拽区 `-webkit-app-region: drag`,按钮 `no-drag`,双击标题栏切最大化,关闭键 hover 变红) |
|
||
| 服务端管理面板 | 用独立无边框 Electron 窗口加载 `http://127.0.0.1:<port>/admin`(`shell-preload.js` 暴露 `window.desktopShell`),不再调系统浏览器;面板页在浏览器里直接打开时标题栏自动不渲染 |
|
||
| 显示器菜单 | 面板页的「大屏控制 → 显示器」卡片经 `display:list` / `display:open` / `display:close` / `display:relayout` 指挥本机开窗:按屏创建、聚焦、收窗。**全屏只在「多于一块屏 + 非主屏」时放行**,判定在主进程做,页面传 `fullscreen: true` 也会被降级为窗口化;面板页只拿到这几条白名单通道,拿不到 `ipcRenderer` |
|
||
| 管理员账号 | 主进程 `spawn` 同目录 `scoreboard.exe passwd -config <同一份配置> -data <同一个数据目录> -user <当前用户名> -new-username <新用户名> -password <新密码>`(留空 = 不改 / 随机生成)。本机进程、不经 HTTP,因此免登录验证;用户名/密码含空格、制表符或换行会被主进程拒绝,密码不进日志。`passwd` 会作废该用户全部会话,UI 提交前二次确认并提示 |
|
||
| 日志 | 内存环形缓冲(`log.maxLines`)+ 文件 `logs/console.log`(超 `maxFileBytes` 轮转一份 `.1`,轮转前会删掉旧 `.1` 以兼容 Windows);文件固定落在部署包根目录(打包态 = 安装目录),不随配置搬到用户数据目录;管理窗口按同一个 `log.maxLines` 截断(超上限从头部丢,长跑不变卡) |
|
||
| 服务管理窗口的路径显示 | 「服务端程序」「配置文件」两行分别是解析出的 `scoreboard.exe` 与 Console 自己那份 `config.json` 的绝对路径(打包态后者在用户数据目录里,见上一节) |
|
||
|
||
## 配置项(`config.json`;打包态生效的是用户数据目录里那份)
|
||
|
||
| 键 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `server.exe` | `scoreboard.exe` | 相对路径会以部署包根目录(打包态 = exe 所在目录,开发态 = `console/`)、当前目录、上级目录、`dist\` 依次探测,也可写绝对路径 |
|
||
| `server.args` | `["-config","config.yaml"]` | 传给服务端的参数;其中 `-config` 的值会被换成解析出的绝对路径(`<用户根>\config.yaml`),其余参数(如 `-project`)原样传 |
|
||
| `server.dataDir` | `""` | 数据目录;空串 = 用默认位置(打包态 `%LOCALAPPDATA%\ScoreboardConsole\data`),填了就按它来(相对路径以部署包根目录为基准)。Console 会把它作为 `-data <绝对路径>` 追加到服务端参数后 |
|
||
| `server.host` / `server.port` | `127.0.0.1` / `19540` | 健康检查、关闭握手与管理面板地址(`0.0.0.0` 会自动换成回环地址探测) |
|
||
| `server.autoStart` | `true` | Console 启动后是否立刻拉起服务端 |
|
||
| `server.healthIntervalMs` | `5000` | 健康检查间隔 |
|
||
| `server.restartDelayMs` | `3000` | 重启基础退避:**第 N 次等 N × 本值**(×1..×5,线性,不是翻倍) |
|
||
| `server.maxRestarts` | `0` | 0 = 无限重启 |
|
||
| `server.shutdownPath` | `/api/shutdown` | 优雅关闭握手接口(需服务端实现并校验 `X-Shutdown-Token`),不可用时回退强杀 |
|
||
| `server.shutdownTimeoutMs` | `5000` | 服务端受理关闭请求后等待其自行退出的上限,超过则强杀 |
|
||
| `server.healthyResetMs` | `60000` | 连续健康多久后清零崩溃计数 |
|
||
| `display.selection` | `all-secondary` | `all-secondary` / `all` / `specified` |
|
||
| `display.displays` | `[]` | `specified` 时生效,填 display id 或 0 起序号 |
|
||
| `display.pages` | `["standings","medals","individual","results"]` | 按屏序轮转锁定;只填大屏支持的页名 |
|
||
| `display.pageParam` | `page` | 页面 URL 参数名 |
|
||
| `display.kiosk` | `true` | 多屏时是否 kiosk(单屏恒窗口化) |
|
||
| `display.allowClickThrough` | `false` | 多屏 kiosk 窗口是否**允许点击穿透**(`setIgnoreMouseEvents(true)`)。写 `true` 后该窗口忽略所有鼠标事件:点击落到它后面的窗口/桌面(现场误触会点到别的东西),大屏也点不活、被盖住后抢不回前台。只把大屏当纯展示层、且要在同一块屏上操作别的窗口时才开 |
|
||
| `display.ignoreMouse` | — | **已废弃 → 见 `display.allowClickThrough`**。旧版默认 `true` 且被旧模板写进现场机器的用户配置,只改默认值救不了老机器,所以换了键名;本键现在**完全不读**,配置文件里留着它只会让启动日志多一条提示,不影响行为 |
|
||
| `display.reloadEveryHours` | `24` | 定时重载兜底,0 = 关闭 |
|
||
| `display.waitForServerMs` | `20000` | 服务端未就绪时开大屏的兜底等待 |
|
||
| `display.backgroundColor` | `#0b0f1a` | 窗口底色,减少加载白闪 |
|
||
| `startup.openAtLogin` | `false` | 默认注册但不启用(任务管理器可见、已禁用);在服务管理窗口「开机自启」开关里控制(开发态不注册) |
|
||
| `admin.width` / `admin.height` | `1280` / `860` | 管理窗口初始尺寸(服务端管理面板窗口按 `width` 不小于 1100 取用) |
|
||
| `log.maxLines` / `log.file` / `log.maxFileBytes` | `2000` / `logs/console.log` / `4194304` | 日志缓冲与文件;`log.file` 相对部署包根目录解析(不随配置搬到用户数据目录) |
|
||
|
||
配置缺项自动用内置默认值,文件不存在或 JSON 语法错误时回落到全默认值,并在管理窗口顶部给出提示。配置里如果还留着已改名的旧键(目前是 `display.ignoreMouse`),启动时会在 `logs/console.log` 里记一条中文提示(只在启动记一次,不随大屏重排重复刷),说明该键已废弃、现在默认不穿透、要穿透该写哪个键;旧键的值一律忽略,**不会**被自动改写进用户的配置文件。
|
||
|
||
## 打包(electron-builder)
|
||
|
||
`package.json` 里已写好 `build` 段:
|
||
|
||
```bash
|
||
cd console
|
||
pnpm install
|
||
pnpm check # 跨平台语法检查(scripts/check-syntax.js)
|
||
pnpm dist # electron-builder --win --x64 → ../dist/console/
|
||
pnpm pack # 只出目录,不生成安装包,便于现场调试
|
||
```
|
||
|
||
- 产物:NSIS 安装包 + zip,文件名形如 `Scoreboard Console-<版本>-x64.*`。`<版本>` 不是手写的:`deploy/prepare-extra.mjs` 在打包前读仓库根 `VERSION`(当前 `dev_0.1.8`)去掉 `dev_` 前缀写进 `console/package.json` 的 `version`,`artifactName` 用 `${productName}-${version}-${arch}.${ext}` 展开 —— 也就是说**这里不要手改版本号**,改仓库根的 `VERSION`。
|
||
- `extraFiles` 会把 `../dist/scoreboard.exe`、`../config.yaml`、`config.json`、`../rules` 拷到安装目录 —— 这四样里的后三样(`config.json`、`config.yaml`、`rules/`)在打包态只当**模板**:首次启动时复制到用户数据目录,之后 Console 与 `backup.bat` 都只用用户目录那份(见「配置与规则目录」一节)。`config.json` 缺失时 Console 会回落到内置默认值。
|
||
- 部署包根目录的说明书是可选的:`deploy/prepare-extra.mjs` 在打包前按一张清单暂存到 `console/extra/`(`extraFiles` 会把该目录内容平铺到安装目录根):`docs/使用手册.md` → `使用说明.txt`,`docs/报名名单导入规范.md` → `报名名单导入规范.txt`(各自优先取 `docs/` 下的同名 `.txt`)。缺哪个就在构建日志里注明并跳过。
|
||
- 图标在 `resources/icon.ico`(`package.json` 的 `win.icon` 指向它)。
|
||
- 主程序文件名由 `win.executableName` 决定,现在是 `Scoreboard_Console.exe`;`productName` 仍是 `Scoreboard Console`(显示名、`userData` 目录不变)。
|
||
- `nsis.include` 指向 `resources/installer-custom.nsh`(构建时由 `deploy/prepare-extra.mjs` 生成,已 gitignore):许可页顶部文案 + 记住上次安装目录(重装时默认沿用,不退回系统默认路径)。
|
||
- 已在 Windows 构建机上通过 `deploy\build.bat` 实跑(含 `pnpm dist`,产出 NSIS 安装包 + zip);Linux 开发机未安装 Electron/electron-builder。
|
||
|
||
## 尚未实机验证
|
||
|
||
以下内容只做了静态语法检查,需要在 Windows + 真实多显示器环境跑一遍:
|
||
|
||
- 多屏 kiosk 定位与 `display-metrics-changed` 重排(改分辨率 / 缩放 / 插拔 HDMI)。
|
||
- kiosk 窗口的置顶(`setAlwaysOnTop(true, 'screen-saver')`)与任务栏覆盖:能否稳定压住任务栏、别的窗口拖到该屏上会不会盖住它、点击后能否回到最前——`screen-saver` 层级在 Windows 上的实际效果只能在真机上验;`setKiosk` / `setBounds` 的先后顺序同理(Linux 开发机没有 Electron 运行时)。
|
||
- 管理面板显示器菜单:真正的 kiosk 全屏 / 窗口化结果、`screen.getAllDisplays()` 的 `label` 与主屏判定、`display:list` 返回的 scaleFactor 是否与现场一致。
|
||
- `powerSaveBlocker` 在长时间后台下的实际效果。
|
||
- 托盘图标在 Windows 任务栏的显示效果(16px 缩放)。
|
||
- `scoreboard.exe` 真实拉起、日志转发、健康检查与崩溃重启的端到端链路。
|
||
- 数据目录迁移实机路径:老版本安装目录里的 `data\` 是否被整体搬到 `%LOCALAPPDATA%\ScoreboardConsole\data`、跨盘安装(安装目录在 D 盘、`%LOCALAPPDATA%` 在 C 盘)时的失败日志与「旧数据原地保留」表现。
|
||
- 配置/规则首次复制实机路径:老版本升级到本版本后,安装目录里的 `config.json` / `config.yaml` / `rules\` 是否被复制到 `%LOCALAPPDATA%\ScoreboardConsole\` 下对应位置、改过配置后覆盖安装是否仍然生效、`%LOCALAPPDATA%` 不可写(或被杀毒软件拦截)时是否只记日志并回落到安装目录里那份。
|
||
- electron-builder 打包已通过 `deploy\build.bat` 实跑;NSIS 安装包在真机上的安装 / 卸载流程尚未验证。
|