fix: 大屏背景图被兜底渐变盖住 + 配置/规则也搬到用户数据目录

1) 大屏背景图不生效:App.vue 里 .bg 与 .bg-fallback 都是 absolute inset:0,
   而兜底渐变是不透明的且在图片之后绘制 → 配了背景图也只看得到渐变。
   改成 v-if/v-else 互斥(用无头 Chromium 真渲染对照验证:修复后取到图片像素,
   回退版本只剩渐变)。新增 background-layer.test.ts 锁住层次关系。

2) 规则/config 仍在安装目录,覆盖安装/卸载会连配置一起删:现在三样都落在
   %LOCALAPPDATA%\ScoreboardConsole\(与数据目录同一基座):
   - console\config.json(Console 读写都用这份)
   - config.yaml(Console 用绝对路径 -config 传给服务端与 passwd)
   - rules\(首次从安装目录复制过去,之后只认用户目录那份)
   首次运行复制迁移、已存在不覆盖、不可写则回落安装目录并记日志(不影响启动);
   开发态行为不变。备份脚本与文档同步。
   注意 resolveExecutable 必须显式传部署包根目录,否则会去用户目录找 scoreboard.exe。

自检:服务端 11 包全绿;console 语法 16 文件 + node:test 20/20;
前端 vue-tsc + build、node --test 69/69。
This commit is contained in:
AskaEth committed 2026-09-10 10:06:21 +08:00
1 parent 2d207fd654
commit 407abec3b0
15 files changed
+759 -81

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
'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 {
resolveUserRoot,
planUserPaths,
prepareUserPaths,
withConfigArg
} = require('./userpaths')
// 用绝对路径字符串拼测试目录,避免依赖运行平台的盘符写法
const INSTALL = path.resolve('/scoreboard-install')
const LOCAL = path.resolve('/localappdata')
const USER_ROOT = path.join(LOCAL, 'ScoreboardConsole')
function makeRoot(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'scoreboard-userpaths-'))
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
return root
}
function writeFile(file, text) {
fs.mkdirSync(path.dirname(file), { recursive: true })
fs.writeFileSync(file, text)
}
function readFile(file) {
return fs.readFileSync(file, 'utf8')
}
// 打包态 + 有 %LOCALAPPDATA% 是「接管」的唯一组合
function planFor({ installDir = INSTALL, env = { LOCALAPPDATA: LOCAL }, isPackaged = true } = {}) {
return planUserPaths({ installDir, userRoot: resolveUserRoot(env), isPackaged })
}
test('打包态三项都落到用户根下,与数据目录同一基座', () => {
const plan = planFor()
assert.equal(plan.mode, 'user')
assert.equal(plan.root, USER_ROOT)
assert.equal(plan.items.consoleConfig.target, path.join(USER_ROOT, 'console', 'config.json'))
assert.equal(plan.items.serverConfig.target, path.join(USER_ROOT, 'config.yaml'))
assert.equal(plan.items.rulesDir.target, path.join(USER_ROOT, 'rules'))
})
test('开发态不接管,三项都指向安装目录里随包的那份', () => {
const plan = planFor({ isPackaged: false })
assert.equal(plan.mode, 'install')
assert.equal(plan.reason, 'dev')
assert.equal(plan.items.consoleConfig.target, path.join(INSTALL, 'config.json'))
assert.equal(plan.items.serverConfig.target, path.join(INSTALL, 'config.yaml'))
assert.equal(plan.items.rulesDir.target, path.join(INSTALL, 'rules'))
})
test('拿不到 %LOCALAPPDATA% 时回落安装目录并记日志', () => {
const plan = planFor({ env: {} })
assert.equal(plan.mode, 'install')
assert.equal(plan.reason, 'no-localappdata')
const lines = []
const paths = prepareUserPaths({ plan, log: (line) => lines.push(line) })
assert.equal(paths.consoleConfig, path.join(INSTALL, 'config.json'))
assert.equal(paths.rulesDir, path.join(INSTALL, 'rules'))
assert.equal(paths.serverConfig, null)
assert.match(lines.join('\n'), /拿不到 %LOCALAPPDATA%/)
})
test('首次运行把随包的三项复制过去,安装目录那份留作模板', (t) => {
const root = makeRoot(t)
const installDir = path.join(root, 'install')
const userRoot = path.join(root, 'localappdata', 'ScoreboardConsole')
writeFile(path.join(installDir, 'config.json'), '{"server":{"port":19541}}')
writeFile(path.join(installDir, 'config.yaml'), 'server:\n port: 19541\n')
writeFile(path.join(installDir, 'rules', 'cup.yaml'), 'format: scoreboard-rules\n')
writeFile(path.join(installDir, 'rules', 'README.md'), '说明\n')
const lines = []
const paths = prepareUserPaths({
plan: planUserPaths({ installDir, userRoot, isPackaged: true }),
log: (line) => lines.push(line)
})
assert.equal(paths.consoleConfig, path.join(userRoot, 'console', 'config.json'))
assert.equal(paths.serverConfig, path.join(userRoot, 'config.yaml'))
assert.equal(paths.rulesDir, path.join(userRoot, 'rules'))
assert.equal(readFile(paths.consoleConfig), '{"server":{"port":19541}}')
assert.equal(readFile(paths.serverConfig), 'server:\n port: 19541\n')
assert.equal(readFile(path.join(userRoot, 'rules', 'cup.yaml')), 'format: scoreboard-rules\n')
assert.equal(readFile(path.join(userRoot, 'rules', 'README.md')), '说明\n')
// 复制不是移动:安装目录那三份必须原样留着当模板
assert.equal(readFile(path.join(installDir, 'config.json')), '{"server":{"port":19541}}')
assert.equal(readFile(path.join(installDir, 'config.yaml')), 'server:\n port: 19541\n')
assert.equal(fs.existsSync(path.join(installDir, 'rules', 'cup.yaml')), true)
assert.equal(lines.filter((line) => line.startsWith('已把 ')).length, 3)
})
test('用户目录已有那份就一律用它,不覆盖也不重拷', (t) => {
const root = makeRoot(t)
const installDir = path.join(root, 'install')
const userRoot = path.join(root, 'localappdata', 'ScoreboardConsole')
writeFile(path.join(installDir, 'config.json'), '{"server":{"port":1111}}')
writeFile(path.join(installDir, 'config.yaml'), 'server:\n port: 1111\n')
writeFile(path.join(installDir, 'rules', 'cup.yaml'), 'template\n')
writeFile(path.join(userRoot, 'console', 'config.json'), '{"server":{"port":2222}}')
writeFile(path.join(userRoot, 'config.yaml'), 'server:\n port: 2222\n')
writeFile(path.join(userRoot, 'rules', 'mine.yaml'), 'mine\n')
const lines = []
const paths = prepareUserPaths({
plan: planUserPaths({ installDir, userRoot, isPackaged: true }),
log: (line) => lines.push(line)
})
assert.equal(readFile(paths.consoleConfig), '{"server":{"port":2222}}')
assert.equal(readFile(paths.serverConfig), 'server:\n port: 2222\n')
assert.equal(readFile(path.join(userRoot, 'rules', 'mine.yaml')), 'mine\n')
// 用户目录里的 rules\ 已存在(哪怕内容和模板不同)→ 不补拷模板里的 cup.yaml
assert.equal(fs.existsSync(path.join(userRoot, 'rules', 'cup.yaml')), false)
assert.equal(lines.filter((line) => line.startsWith('已把 ')).length, 0)
})
test('用户目录写不进去时记日志并回落到安装目录,不抛异常', (t) => {
const root = makeRoot(t)
const installDir = path.join(root, 'install')
writeFile(path.join(installDir, 'config.json'), '{}')
writeFile(path.join(installDir, 'config.yaml'), 'server: {}\n')
writeFile(path.join(installDir, 'rules', 'cup.yaml'), 'template\n')
// 用户根的位置被一个文件占着:mkdir/rename 必然失败(ENOTDIR)
const userRoot = path.join(root, 'blocker')
writeFile(userRoot, 'not a dir')
const lines = []
const paths = prepareUserPaths({
plan: planUserPaths({ installDir, userRoot, isPackaged: true }),
log: (line) => lines.push(line)
})
assert.equal(paths.consoleConfig, path.join(installDir, 'config.json'))
assert.equal(paths.serverConfig, null)
assert.equal(paths.rulesDir, path.join(installDir, 'rules'))
assert.equal(readFile(paths.consoleConfig), '{}')
assert.equal(readFile(path.join(installDir, 'rules', 'cup.yaml')), 'template\n')
assert.equal(lines.filter((line) => line.includes('继续用安装目录里的')).length, 3)
})
test('两边都没有的那一份跳过复制,路径仍指向用户目录', (t) => {
const root = makeRoot(t)
const installDir = path.join(root, 'install')
const userRoot = path.join(root, 'localappdata', 'ScoreboardConsole')
fs.mkdirSync(installDir, { recursive: true })
const lines = []
const paths = prepareUserPaths({
plan: planUserPaths({ installDir, userRoot, isPackaged: true }),
log: (line) => lines.push(line)
})
// 不接管就没人去读用户目录,服务端会按 config.Load 的默认值跑
assert.equal(paths.serverConfig, path.join(userRoot, 'config.yaml'))
assert.equal(fs.existsSync(paths.serverConfig), false)
assert.equal(lines.filter((line) => line.includes('本次跳过复制')).length, 3)
})
test('规则目录拷贝到位后不留临时目录,重复运行不重做', (t) => {
const root = makeRoot(t)
const installDir = path.join(root, 'install')
const userRoot = path.join(root, 'localappdata', 'ScoreboardConsole')
writeFile(path.join(installDir, 'config.json'), '{}\n')
writeFile(path.join(installDir, 'config.yaml'), 'server: {}\n')
writeFile(path.join(installDir, 'rules', 'cup.yaml'), 'template\n')
const plan = planUserPaths({ installDir, userRoot, isPackaged: true })
const first = prepareUserPaths({ plan, log: () => {} })
assert.deepEqual(fs.readdirSync(userRoot).sort(), ['config.yaml', 'console', 'rules'])
assert.deepEqual(fs.readdirSync(first.rulesDir), ['cup.yaml'])
// 第二次启动:目标都在了,不再复制(用户改过的内容不会被模板顶掉)
writeFile(path.join(userRoot, 'config.yaml'), 'server:\n port: 3333\n')
const second = prepareUserPaths({ plan, log: () => {} })
assert.deepEqual(second, first)
assert.equal(readFile(path.join(userRoot, 'config.yaml')), 'server:\n port: 3333\n')
})
test('-config 的值换成解析出的绝对路径,其余参数原样保留', () => {
assert.deepEqual(withConfigArg(['-config', 'config.yaml'], '/u/config.yaml'), ['-config', '/u/config.yaml'])
assert.deepEqual(
withConfigArg(['-config', 'config.yaml', '-project', 'cup'], '/u/config.yaml'),
['-config', '/u/config.yaml', '-project', 'cup']
)
// 参数里没写 -config 就补一条,否则用户目录里那份永远不会被读到
assert.deepEqual(withConfigArg(['-project', 'cup'], '/u/config.yaml'), ['-project', 'cup', '-config', '/u/config.yaml'])
// 不接管(开发态)原样返回
assert.deepEqual(withConfigArg(['-config', 'config.yaml'], null), ['-config', 'config.yaml'])
assert.deepEqual(withConfigArg(null, '/u/config.yaml'), ['-config', '/u/config.yaml'])
})