Files
Scoreboard/console/main/config.test.js
T
AskaEth 9ef68c2589 feat(console,admin): kiosk 不再点击穿透 + 压住任务栏;大屏控制页新增「局域网大屏」(链接/复制/二维码)dev_0.4.0
## ① 现场问题: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 例。
2026-09-12 16:25:04 +08:00

88 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict'
const assert = require('node:assert/strict')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const test = require('node:test')
const { DEFAULTS, loadConfig } = require('./config')
function makeDir(t) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scoreboard-config-'))
t.after(() => fs.rmSync(dir, { recursive: true, force: true }))
return dir
}
// 现场机器上那份用户配置:内容原样写进文件,读的也必须是文件里真实写了什么
function loadWith(t, raw) {
const dir = makeDir(t)
const file = path.join(dir, 'config.json')
fs.writeFileSync(file, typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2), 'utf8')
return loadConfig(file)
}
test('缺省的 allowClickThrough 不穿透(默认值就是 false)', (t) => {
assert.equal(DEFAULTS.display.allowClickThrough, false)
const cfg = loadWith(t, { display: {} })
assert.equal(cfg.values.display.allowClickThrough, false)
assert.deepEqual(cfg.notices, [])
})
test('显式写 allowClickThrough: true 才穿透,且不产生废弃提示', (t) => {
const cfg = loadWith(t, { display: { allowClickThrough: true } })
assert.equal(cfg.values.display.allowClickThrough, true)
assert.deepEqual(cfg.notices, [])
})
// 本次修复的核心回归钉子:老机器升级后,用户配置文件里那个旧版模板写下的
// ignoreMouse: true 必须彻底失效(userpaths.js 不会覆盖用户文件,只能靠不读旧键)
test('老配置里的旧键 ignoreMouse 已失效:既不生效、也不再是穿透开关', (t) => {
const cfg = loadWith(t, { display: { ignoreMouse: true } })
assert.equal('ignoreMouse' in DEFAULTS.display, false, 'DEFAULTS 里不能再有旧键')
assert.equal(cfg.values.display.allowClickThrough, false, '旧键 true 不能把穿透带回来')
assert.equal(cfg.values.display.ignoreMouse, undefined, '旧键不该被 merge 进最终配置')
// 但必须留下一条一次性提示,让现场知道该改哪个键
assert.equal(cfg.notices.length, 1)
const [notice] = cfg.notices
assert.match(notice, /display\.ignoreMouse/)
assert.match(notice, /display\.allowClickThrough/)
assert.match(notice, /废弃/, '要明说这个键已经不用了')
assert.match(notice, /不穿透/, '要明说现在的默认是不穿透')
assert.match(notice, /true/, '要给出「确实想穿透」的写法')
})
test('旧键提示只跟着「文件里写了旧键」出现,缺省或新键都不提示', (t) => {
// 旧版模板整份配置(现场最常见的形态):只提示一次,不会按重排/每帧刷
const legacyTemplate = loadWith(t, {
display: { selection: 'all-secondary', kiosk: true, ignoreMouse: true }
})
assert.equal(legacyTemplate.notices.length, 1)
// 新模板(已去掉旧键、写上 new key)不再提示
const currentTemplate = loadWith(t, {
display: { selection: 'all-secondary', kiosk: true, allowClickThrough: false }
})
assert.deepEqual(currentTemplate.notices, [])
// 两个键都在:仍然按「有旧键」提示一次(提示说的是旧键不生效,与新键无关)
const both = loadWith(t, { display: { ignoreMouse: true, allowClickThrough: true } })
assert.equal(both.notices.length, 1)
assert.equal(both.values.display.allowClickThrough, true, '新键显式 true 仍然算数')
})
test('配置文件读不出来/内容不是对象时不报废弃键、也不炸', (t) => {
const missing = loadConfig(path.join(makeDir(t), 'nope.json'))
assert.deepEqual(missing.notices, [])
assert.equal(missing.warning !== null, true)
assert.equal(missing.values.display.allowClickThrough, false)
const broken = loadWith(t, '{ 这不是 JSON')
assert.deepEqual(broken.notices, [])
assert.deepEqual(loadWith(t, 'null').notices, [])
assert.deepEqual(loadWith(t, '[1,2,3]').notices, [])
})