feat(theme): 主题支持按页面分别配装饰,生成器改用真实页面画布

原来的生成器只能配「背景 + 人物 + 道具」三件套,预览还是自己画的仿制页面 ——
跟真实页面不像,也没法给不同页面配不同内容。

机制层:
- internal/context.pageScope() 按路径归类,输出 <html data-page-scope>:
  box / question / admin / profile / vote(首页与 /explore 不加载主题,不列)
- /user/questions 单独归为 admin:题目列表信息密,和设置表单的装饰诉求不同
- base.css 装饰槽位 6 -> 10,背景 8 层 -> 12 层;主题按作用域覆盖时同样写满 10 个
- 新增 TestDecoSlotCount:逐文件逐声明检查 10 个值。少写一个不报错,
  只会让图层与 size/position 错位(整页重复同一张图 / 人物跑到左上角),注释守不住

生成器:
- 画板改成 iframe 加载 outputs/preview-*.html(真实模板渲染),配 serve.py 起本地服务
  解决 file:// 下父页面读不到 iframe DOM 的问题
- 八个页面作用域(去掉不生效的首页/探索),每个可独立配背景与最多 10 层装饰
- 图层类型:图片 / 径向光斑 / 线性光;9 宫格锚点 + 像素偏移,可在画板上直接拖
- 位置统一写 calc(0%|50%|100% ± Npx):百分比是相对 (容器-图片),
  不用 left/bottom 关键字混搭,避免三值语法非法导致整条声明作废
- 配色只填 6 个基础色,其余 4 个必需令牌按方向派生(surface-2 往 text 靠,
  浅色变深、深色变亮,方向自动正确)

配套:
- zz_preview_test.go 增加单题页 / 题目列表 / 投票页预览,作为生成器画板
- docs/theme-page-scope.md 记录作用域取值、选择器特异性与 10 槽位约束
- StaticVersion 20261020e(base.css 变了)

验证:生成器导出的 CSS 通过 TestThemeCSSComplete + TestDecoSlotCount;
真实箱主页计算样式为 12 层对齐;临时主题验证「question 覆盖为 none」生效、
未覆盖时回退全局。
This commit is contained in:
tamakyi
2026-10-20 10:53:44 +08:00
parent feb5b1f78d
commit ddee221de2
14 changed files with 1748 additions and 796 deletions
+12 -7
View File
@@ -33,9 +33,12 @@
视口比例一变,贴在图片边缘的插画就会被裁掉)。
⚠️ 固定 6 个槽位:background-* 各列表的层数必须恒定(1 图案 + 6 装饰 + 1 背景 = 8),
缺省用 none 占位;值的个数一变,多背景的分层就会整体错位。 */
--tb-deco: none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top, left top;
/* 装饰层:固定 10 个槽位。主题可按页面作用域整体替换(html[data-page-scope="..."]),
但每组都必须写满 10 个值 —— 数量不足会让 background-* 列表与图层错位。 */
--tb-deco: none, none, none, none, none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top, left top, left top,
left top, left top, left top;
}
html[data-theme="dark"] {
--tb-bg: #1a1d23;
@@ -273,13 +276,15 @@
body {
font-family: var(--tb-font);
background-color: var(--tb-bg);
/* 主题的整页背景:四角装饰 + 平铺图案 + cover 背景图(共 6 层,顺序即绘制顺序) */
/* 主题的整页背景:平铺图案(1) + 装饰层(10) + cover 背景图(1) = 12 层,顺序即绘制顺序。
下面四个列表的层数必须与 12 对齐:少一个就会循环取值,导致图层与 size/position 错位。 */
background-image: var(--tb-page-pattern), var(--tb-deco), var(--tb-page-image);
background-size: var(--tb-page-pattern-size), var(--tb-deco-size), cover;
background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat,
no-repeat;
background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat,
no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
background-position: left top, var(--tb-deco-position), center center;
background-attachment: scroll, fixed, fixed, fixed, fixed, fixed, fixed, fixed;
background-attachment: scroll, fixed, fixed, fixed, fixed, fixed, fixed, fixed, fixed, fixed,
fixed, fixed;
color: var(--tb-text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+37 -10
View File
@@ -35,10 +35,11 @@
页面背景 --tb-page-image 整页背景图(cover,fixed)
--tb-page-pattern 整页图案(平铺,可叠加在背景图之上)
--tb-page-pattern-size 图案平铺尺寸(默认 auto)
装饰 --tb-deco 四角装饰图,固定 6 个槽位(必须写满 6 个,缺的填 none);
固定像素尺寸、不随视口裁切,适合放人物/摆件。默认 none。
--tb-deco-size 对应每张的尺寸,如 auto 200px, auto 150px, auto, auto, auto, auto
--tb-deco-position 对应每张的位置,如 left 1% bottom 0, right 1% top 4%, left top, …
装饰 --tb-deco 装饰图层,固定 10 个槽位(必须写满 10 个,缺的填 none);
固定像素尺寸、不随视口裁切,适合放人物/摆件/光斑。默认 none。
值可以是图片,也可以是 linear-gradient / radial-gradient(纯"光")。
--tb-deco-size 对应每层的尺寸,如 auto 400px, 100% 100%, auto, …(10 个)
--tb-deco-position 对应每层的位置,如 calc(50% - 524px) calc(100% - 36px), …(10 个)
── 素材规格(放 R2,不需要重新编译) ─────────────────────────────
页面背景图 2400×1600(3:2)或 2560×1440,WebP q80,≤300KB;明暗各一张:
@@ -81,12 +82,18 @@ html[data-theme-style="SLUG"] {
--tb-page-pattern: none;
--tb-page-pattern-size: auto;
/* 四角装饰(放人物/摆件)。固定 6 个槽位,必须写满,缺的填 none——
/* 装饰层(放人物/摆件/光)。固定 10 个槽位,必须写满,缺的填 none——
值一多一少,多背景各列表就错位(会出现整页重复同一张图)。
尺寸用「auto <高度>」,位置可写 left 2% bottom 0 / right 2% top 6% / left 2% center。 */
--tb-deco: none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top, left top;
尺寸用「auto <高度>」;位置统一写 calc:
calc(0% + 40px) 左对齐 + 40px
calc(50% + 0px) 居中
calc(100% - 36px) 右/下对齐 - 36px(百分比是相对「容器 − 图片」的剩余空间)
不要用 left / bottom 关键字混搭 —— 「水平居中 + 垂直贴底」会变成三值语法,
一旦写法不合规范整条声明作废,图层静默掉到左上角。 */
--tb-deco: none, none, none, none, none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top,
left top, left top, left top, left top, left top;
}
/* 2) 深色(必须)—— 深浅两套必须成对,缺一段会让深色用户看到「浅色的深色主题」 */
@@ -112,7 +119,27 @@ html[data-theme="dark"][data-theme-style="SLUG"] {
--tb-page-pattern-size: auto;
}
/* 3) 组件微调(可选,默认整段注释掉)
/* 3) 按页面作用域覆盖(可选)
服务端会按请求路径给 <html> 打 data-page-scope,取值:
(无属性 = 全局)/ box(箱主页) / question(单题页) / admin(/user/questions)
/ profile(设置页等 /user/* ) / vote(投票页)
典型用法:「问题页正文长,人物退场;题目列表字更密,只留一点光」。
html[data-theme-style="SLUG"][data-page-scope="question"] {
--tb-page-image: none;
--tb-deco: none, none, none, none, none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top,
left top, left top, left top, left top, left top;
}
⚠️ 覆盖块里同样要写满 10 个槽位。
⚠️ 深色要再写一条三重选择器,否则深色下会被浅色覆盖块污染:
html[data-theme="dark"][data-theme-style="SLUG"][data-page-scope="question"] { … }
⚠️ 没写覆盖块的页面自动沿用全局,不会出现「某页装饰消失」。
首页 / 与 /explore 不加载主题 css,配了也不会生效。 */
/* 4) 组件微调(可选,默认整段注释掉)
只允许「外观」层面的补充:边框、阴影、封面遮罩浓度等。
禁止改尺寸/间距/定位 —— 那属于布局,主题不管。 */
/*
+14 -9
View File
@@ -47,12 +47,15 @@ html[data-theme-style="shiroko"] {
url("/static/images/themes/shiroko/props/bike.webp?v=20261020d"),
linear-gradient(112deg, rgba(255, 250, 236, 0.50) 0%, rgba(255, 250, 236, 0.22) 20%, rgba(255, 250, 236, 0) 44%),
radial-gradient(ellipse 560px 170px at 28% 94%, rgba(255, 248, 228, 0.80) 0%, rgba(255, 248, 228, 0) 72%),
linear-gradient(0deg, rgba(110, 128, 156, 0.10) 0%, rgba(110, 128, 156, 0) 24%);
--tb-deco-size: auto 400px, 96px 13px, auto 259px, 100% 100%, 100% 100%, 100% 100%;
linear-gradient(0deg, rgba(110, 128, 156, 0.10) 0%, rgba(110, 128, 156, 0) 24%),
none, none, none, none;
--tb-deco-size: auto 400px, 96px 13px, auto 259px, 100% 100%, 100% 100%, 100% 100%,
auto, auto, auto, auto;
--tb-deco-position: left max(10px, calc(50% - 524px)) bottom 36px,
left max(22px, calc(50% - 512px)) bottom 29px,
left calc(50% - 612px) bottom 20px,
left top, left top, left top;
left top, left top, left top,
left top, left top, left top, left top;
}
html[data-theme="dark"][data-theme-style="shiroko"] {
@@ -77,9 +80,10 @@ html[data-theme="dark"][data-theme-style="shiroko"] {
--tb-page-image: linear-gradient(180deg, #161d26 0%, #141a22 60%, #11161d 100%);
--tb-page-pattern: url("/static/images/themes/shiroko/pattern-dark.svg?v=20261020d");
--tb-page-pattern-size: 340px 340px;
--tb-deco: none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top, left top;
--tb-deco: none, none, none, none, none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top, left top, left top,
left top, left top, left top;
}
/* 窄屏:卡片占满宽度,两侧没有视觉空间 —— 换成竖版场景,人物退场 */
@@ -87,9 +91,10 @@ html[data-theme="dark"][data-theme-style="shiroko"] {
html[data-theme-style="shiroko"] {
--tb-page-image: url("/static/images/themes/shiroko/scene-mobile.webp?v=20261020d");
--tb-surface: rgba(255, 255, 255, 0.92);
--tb-deco: none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top, left top;
--tb-deco: none, none, none, none, none, none, none, none, none, none;
--tb-deco-size: auto, auto, auto, auto, auto, auto, auto, auto, auto, auto;
--tb-deco-position: left top, left top, left top, left top, left top, left top, left top,
left top, left top, left top;
}
}