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

+169
View File
@@ -0,0 +1,169 @@
'use strict'
const fs = require('fs')
const path = require('path')
const { LOCAL_BASE } = require('./datadir')
// 这三样以前随安装包落在安装目录里,但都属于「用户改出来的东西」,必须跟数据目录一样
// 挪到安装目录之外:卸载器结尾是 `RMDir /r $INSTDIR`,覆盖安装又会先静默跑一遍旧卸载器,
// 放在安装目录里等于用户改一次丢一次。
// console\config.json → <用户根>\console\config.json
// config.yaml → <用户根>\config.yaml
// rules\ → <用户根>\rules\
// 用户根与数据目录同一基座:%LOCALAPPDATA%\ScoreboardConsole\
const CONSOLE_SUBDIR = 'console'
const CONSOLE_CONFIG_FILE = 'config.json'
const SERVER_CONFIG_FILE = 'config.yaml'
const RULES_SUBDIR = 'rules'
const ITEM_NAMES = ['consoleConfig', 'serverConfig', 'rulesDir']
const ITEM_LABEL = {
consoleConfig: 'Console 配置文件',
serverConfig: '服务端配置文件',
rulesDir: '规则目录'
}
// 用户根目录;拿不到 %LOCALAPPDATA% 时返回 null,由调用方回落到安装目录
function resolveUserRoot(env) {
const localAppData = env ? env.LOCALAPPDATA : ''
const value = typeof localAppData === 'string' ? localAppData.trim() : ''
return value ? path.join(value, LOCAL_BASE) : null
}
// 解析落点,纯函数不碰磁盘。mode='user' 才接管到用户目录;
// 其余情况(开发态、拿不到 %LOCALAPPDATA%)三项一律用安装目录里随包的那份,
// 行为与以前完全一致 —— 开发态不能动仓库里的文件。
function planUserPaths({ installDir, userRoot, isPackaged }) {
const install = path.resolve(installDir || '.')
const templates = {
consoleConfig: path.join(install, CONSOLE_CONFIG_FILE),
serverConfig: path.join(install, SERVER_CONFIG_FILE),
rulesDir: path.join(install, RULES_SUBDIR)
}
const items = {}
const targets = {
consoleConfig: path.join(install, CONSOLE_CONFIG_FILE),
serverConfig: path.join(install, SERVER_CONFIG_FILE),
rulesDir: path.join(install, RULES_SUBDIR)
}
const reason = !isPackaged ? 'dev' : userRoot ? null : 'no-localappdata'
if (!reason) {
targets.consoleConfig = path.join(userRoot, CONSOLE_SUBDIR, CONSOLE_CONFIG_FILE)
targets.serverConfig = path.join(userRoot, SERVER_CONFIG_FILE)
targets.rulesDir = path.join(userRoot, RULES_SUBDIR)
}
for (const name of ITEM_NAMES) {
items[name] = { target: path.resolve(targets[name]), template: path.resolve(templates[name]) }
}
return {
mode: reason ? 'install' : 'user',
reason,
root: userRoot ? path.resolve(userRoot) : null,
installDir: install,
items
}
}
// 目标目录必须真的能写:mkdir 只保证目录建出来,权限不足或路径被占要在这一步就暴露,
// 好让调用方回落到安装目录,而不是等到写配置文件时才失败
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true })
fs.accessSync(dir, fs.constants.W_OK)
}
function tryRemove(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true })
} catch {
// 清不掉就留着:只是本次拷贝留下的临时目录,下次会原地重建
}
}
// 规则目录先拷到临时目录再整体改名到位:中途失败不会留下半个 rules\,
// 否则用户目录里一旦存在同名目录,下次启动就按「已存在」跳过、再也不补拷
function copyDirOnce(template, target) {
const tmp = `${target}.tmp-${process.pid}`
try {
tryRemove(tmp)
fs.cpSync(template, tmp, { recursive: true })
fs.renameSync(tmp, target)
} catch (err) {
tryRemove(tmp)
throw err
}
}
// 按 plan 落盘,返回最终生效的路径。判定只有两条:
// 目标不存在 + 安装目录里有 → 复制过去(安装目录那份留作模板,反正卸载器会删);
// 目标已存在 → 一律用用户目录那份,绝不覆盖用户改过的东西。
// 任一目录建不出来 / 拷不动都只记日志,该项继续用安装目录里的那份,绝不删用户文件、
// 也绝不因此启动失败。
function prepareUserPaths({ plan, log }) {
const notify = typeof log === 'function' ? log : () => {}
const result = {
mode: plan.mode,
reason: plan.reason,
root: plan.mode === 'user' ? plan.root : null,
consoleConfig: plan.items.consoleConfig.template,
rulesDir: plan.items.rulesDir.template,
// null = 不接管:拉起服务端与 passwd 时沿用 config.json 里 -config 的原值
serverConfig: null
}
if (plan.mode !== 'user') {
if (plan.reason === 'no-localappdata') {
notify(
'警告:拿不到 %LOCALAPPDATA%,config.json / config.yaml / rules 继续用安装目录里的那份,' +
'覆盖安装会连同它们一起丢失'
)
}
return result
}
for (const name of ITEM_NAMES) {
const { target, template } = plan.items[name]
const label = ITEM_LABEL[name]
if (path.resolve(target) === path.resolve(template)) continue
try {
ensureDir(path.dirname(target))
if (!fs.existsSync(target)) {
if (!fs.existsSync(template)) {
notify(`警告:${label}在用户目录与安装目录里都不存在(${template}),本次跳过复制`)
} else {
if (name === 'rulesDir') copyDirOnce(template, target)
else fs.copyFileSync(template, target)
notify(`已把 ${label} 复制到 ${target}(安装目录里那份留作模板)`)
}
}
} catch (err) {
notify(
`警告:准备 ${label} 失败(${err.message}),继续用安装目录里的 ${template};` +
'覆盖安装会丢失对它的改动'
)
continue
}
if (name === 'serverConfig') result.serverConfig = target
else result[name] = target
}
return result
}
// 服务端读哪份 config.yaml 由 Console 定:config.json 里的 server.args 只声明「有个 -config 参数」,
// 它的值在拉起时换成上面解析出的绝对路径(不传 configPath 时原样返回,开发态就走这条路)
function withConfigArg(args, configPath) {
const list = Array.isArray(args) ? [...args] : []
if (typeof configPath !== 'string' || configPath === '') return list
const index = list.indexOf('-config')
if (index !== -1 && typeof list[index + 1] === 'string') list[index + 1] = configPath
else list.push('-config', configPath)
return list
}
module.exports = { resolveUserRoot, planUserPaths, prepareUserPaths, withConfigArg }