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:
1 parent
2d207fd654
commit
407abec3b0
15 files changed
+759
-81
No files matched your search
@@ -99,8 +99,9 @@ onUnmounted(() => {
|
||||
|
||||
<div class="screen">
|
||||
<div class="stage" :style="[stageStyle, themeVars]">
|
||||
<!-- 兜底渐变与背景图互斥:两者都是 absolute inset:0 同层,同时存在时后画的渐变会整块盖住图片 -->
|
||||
<img v-if="background" class="bg" :src="background" alt="" />
|
||||
<div class="bg-fallback" />
|
||||
<div v-else class="bg-fallback" />
|
||||
|
||||
<div class="content">
|
||||
<TopBar />
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { compileTemplate, parse } from 'vue/compiler-sfc'
|
||||
|
||||
// 大屏层次:背景图 / 兜底渐变在最底,.content 压在其上,DetailOverlay 再压住 .content。
|
||||
// 项目没有 jsdom 与组件测试工具,这里直接编译 App.vue 的模板取 AST,
|
||||
// 用渲染条件(v-if/v-else 分支)而不是文本匹配来断言,改坏层次会直接失败。
|
||||
const APP_SOURCE = readFileSync(new URL('../App.vue', import.meta.url), 'utf8')
|
||||
const OVERLAY_SOURCE = readFileSync(new URL('../components/DetailOverlay.vue', import.meta.url), 'utf8')
|
||||
|
||||
// @vue/compiler-core 的 NodeTypes 没有从 compiler-sfc 再导出,按枚举值取用。
|
||||
const NODE_ELEMENT = 1
|
||||
const NODE_IF = 9
|
||||
|
||||
interface AstAttr {
|
||||
type: number
|
||||
name?: string
|
||||
value?: { content?: string }
|
||||
}
|
||||
|
||||
interface AstNode {
|
||||
type: number
|
||||
tag?: string
|
||||
props?: AstAttr[]
|
||||
children?: AstNode[]
|
||||
branches?: { children: AstNode[] }[]
|
||||
}
|
||||
|
||||
interface FoundElement {
|
||||
tag: string
|
||||
classes: string[]
|
||||
/** 是否位于 v-if/v-else 分支内:false 表示无条件渲染。 */
|
||||
conditional: boolean
|
||||
}
|
||||
|
||||
function classTokens(node: AstNode): string[] {
|
||||
const attr = node.props?.find((prop) => prop.type === 6 && prop.name === 'class')
|
||||
return attr?.value?.content?.split(/\s+/).filter(Boolean) ?? []
|
||||
}
|
||||
|
||||
/** 收集整棵子树里的元素;v-if 分支内的元素标记为 conditional。 */
|
||||
function collectElements(node: AstNode, conditional: boolean, out: FoundElement[]): void {
|
||||
if (node.type === NODE_IF) {
|
||||
for (const branch of node.branches ?? []) {
|
||||
for (const child of branch.children) collectElements(child, true, out)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (node.type === NODE_ELEMENT) {
|
||||
out.push({ tag: node.tag ?? '', classes: classTokens(node), conditional })
|
||||
}
|
||||
for (const child of node.children ?? []) collectElements(child, conditional, out)
|
||||
}
|
||||
|
||||
function collect(node: AstNode): FoundElement[] {
|
||||
const out: FoundElement[] = []
|
||||
collectElements(node, false, out)
|
||||
return out
|
||||
}
|
||||
|
||||
/** 只展开一层 v-if 分支,得到同一父级下的元素(用于核对兄弟顺序)。 */
|
||||
function topLevelElements(node: AstNode): FoundElement[] {
|
||||
const out: FoundElement[] = []
|
||||
const push = (child: AstNode): void => {
|
||||
if (child.type === NODE_IF) {
|
||||
for (const branch of child.branches ?? []) for (const inner of branch.children) push(inner)
|
||||
return
|
||||
}
|
||||
if (child.type === NODE_ELEMENT) out.push({ tag: child.tag ?? '', classes: classTokens(child), conditional: false })
|
||||
}
|
||||
for (const child of node.children ?? []) push(child)
|
||||
return out
|
||||
}
|
||||
|
||||
/** 条件渲染节点藏在普通元素里,也可能嵌在别的分支里,两种都要走到。 */
|
||||
function findIfNode(node: AstNode, match: (candidate: AstNode) => boolean): AstNode | null {
|
||||
if (node.type === NODE_IF && match(node)) return node
|
||||
for (const child of node.children ?? []) {
|
||||
const hit = findIfNode(child, match)
|
||||
if (hit !== null) return hit
|
||||
}
|
||||
for (const branch of node.branches ?? []) {
|
||||
for (const child of branch.children) {
|
||||
const hit = findIfNode(child, match)
|
||||
if (hit !== null) return hit
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** AST 的根是 RootNode,模板里的最外层元素是它的第一个子节点。 */
|
||||
function rootElement(ast: AstNode): AstNode {
|
||||
const root = ast.children?.[0]
|
||||
if (root == null) throw new Error('模板缺少根元素')
|
||||
return root
|
||||
}
|
||||
|
||||
function templateAst(source: string): AstNode {
|
||||
const { descriptor, errors } = parse(source, { filename: 'App.vue' })
|
||||
assert.equal(errors.length, 0, 'App.vue 应能被解析')
|
||||
const template = descriptor.template
|
||||
if (template == null) throw new Error('App.vue 缺少 <template>')
|
||||
const built = compileTemplate({
|
||||
source: template.content,
|
||||
filename: 'App.vue',
|
||||
id: 'display-app',
|
||||
})
|
||||
assert.deepEqual(built.errors, [], 'App.vue 模板应能编译')
|
||||
const ast = built.ast
|
||||
if (ast === undefined) throw new Error('模板未产出 AST')
|
||||
return ast as unknown as AstNode
|
||||
}
|
||||
|
||||
function styleText(source: string): string {
|
||||
const { descriptor } = parse(source, { filename: 'style.vue' })
|
||||
const block = descriptor.styles[0]
|
||||
if (block == null) throw new Error('缺少 <style>')
|
||||
return block.content
|
||||
}
|
||||
|
||||
function cssRule(style: string, selector: string): string {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const match = new RegExp(`${escaped}\\s*\\{([^}]*)\\}`).exec(style)
|
||||
assert.ok(match !== null, `样式里应存在 ${selector} 规则`)
|
||||
return match[1] ?? ''
|
||||
}
|
||||
|
||||
function cssZIndex(rule: string): number | null {
|
||||
const match = /z-index:\s*(-?\d+)/.exec(rule)
|
||||
return match === null ? null : Number(match[1])
|
||||
}
|
||||
|
||||
const APP_AST = templateAst(APP_SOURCE)
|
||||
|
||||
test('兜底渐变与背景图互斥,不会盖住图片', () => {
|
||||
const elements = collect(APP_AST)
|
||||
const bg = elements.find((el) => el.classes.includes('bg'))
|
||||
const fallback = elements.find((el) => el.classes.includes('bg-fallback'))
|
||||
assert.ok(bg !== undefined, '模板里应有 .bg 背景图')
|
||||
assert.equal(bg.conditional, true, '.bg 应只在配置了背景图时渲染')
|
||||
assert.ok(fallback !== undefined, '模板里应保留 .bg-fallback')
|
||||
assert.equal(fallback.conditional, true, '.bg-fallback 必须挂在 else 分支上')
|
||||
|
||||
// 同一个 v-if/v-else:有图走 img 分支,没图才走渐变分支。
|
||||
const ifNode = findIfNode(APP_AST, (candidate) =>
|
||||
(candidate.branches ?? []).some((branch) => classTokens(branch.children[0] ?? { type: 0 }).includes('bg')),
|
||||
)
|
||||
assert.ok(ifNode !== null, '背景图与兜底渐变应同属一个条件渲染节点')
|
||||
assert.equal(ifNode.branches?.length, 2, '应是一对 v-if / v-else 分支')
|
||||
const [yes, no] = ifNode.branches ?? []
|
||||
assert.deepEqual(classTokens(yes?.children[0] ?? { type: 0 }), ['bg'])
|
||||
assert.deepEqual(classTokens(no?.children[0] ?? { type: 0 }), ['bg-fallback'])
|
||||
})
|
||||
|
||||
test('背景层压在内容与弹层之下', () => {
|
||||
const appStyle = styleText(APP_SOURCE)
|
||||
const contentZ = cssZIndex(cssRule(appStyle, '.content'))
|
||||
assert.ok(contentZ !== null && contentZ >= 1, '.content 应显式抬到背景层之上')
|
||||
|
||||
for (const selector of ['.bg', '.bg-fallback']) {
|
||||
const z = cssZIndex(cssRule(appStyle, selector))
|
||||
assert.ok(z === null || z < contentZ, `${selector} 不得声明高于 .content 的 z-index`)
|
||||
}
|
||||
|
||||
const overlayZ = cssZIndex(cssRule(styleText(OVERLAY_SOURCE), '.overlay'))
|
||||
assert.ok(overlayZ !== null && overlayZ > contentZ, 'DetailOverlay 应压在 .content 之上')
|
||||
})
|
||||
|
||||
test('自绘标题栏在舞台之外,缩放后的舞台盖不住它', () => {
|
||||
const top = topLevelElements(rootElement(APP_AST))
|
||||
assert.deepEqual(
|
||||
top.map((el) => el.tag),
|
||||
['WindowedTitleBar', 'div'],
|
||||
'WindowedTitleBar 应是 .root 的首个子级,且与 .screen 平级',
|
||||
)
|
||||
assert.deepEqual(top[1]?.classes, ['screen'])
|
||||
assert.match(cssRule(styleText(APP_SOURCE), '.screen'), /overflow:\s*hidden/, '.screen 应裁剪溢出的舞台')
|
||||
})
|
||||
Reference in new issue
Block a user