出品:扣子一号(coze1,闸门 admin-console/src)
时间:2026-09-15 夜班
范围声明:本席只落地 admin-console/src(a 端表单);下方 c 端(miniprogram/)与后端(backend/)补丁不在本席闸门,按夜班分派表交「我」(统筹代推人)代推/派发,不越闸门直改。
themePrimary 是治理快照 config-governance-v1 的第 6 个冻结字段(SNAPSHOT_FIELDS)。全链路现状:
| 环节 | 文件 | 现状 | 结论 |
|---|---|---|---|
| 治理写入/校验 | backend/app/routers/miniapp_config_governance.py | _validate_snapshot 要求 themePrimary 为 6 位 HEX;_sync_brand 把含 themePrimary 的完整 6 字段快照 json.dumps 写入 site_config('brand') JSONB | ✅ 已写 |
| 直写校验 | backend/app/services/site_config_registry.py L114 _validate_brand | 仅校验「brand 必须是对象」,字段全放行 | ✅ a 端直写 6 字段不被 422 |
| c 端读出口 | backend/app/routers/miniapp_site.py L71-85 get_site_config | 顶层只展平 5 字段(brandName/slogan/tibetanText/logoUrl/heroBgUrl),无 themePrimary;响应也不返回嵌套 brand 对象 | ❌ 断点 BLK-332-3(后端域) |
| a 端表单 | admin-console/src/services/brand.ts、pages/BrandConfig.tsx | 328 初版仅 5 字段 | ✅ 本件已补第 6 字段(见 §二) |
| c 端品牌层 | miniprogram/miniprogram/utils/brand.ts | BrandAssets 仅 5 字段,ensureBrandLoaded 只读顶层 5 字段 | ❌ 缺字段(c 端域) |
| c 端类型 | miniprogram/miniprogram/typings/api.d.ts L571 SiteConfig | 仅 5 品牌字段 | ❌ 缺类型 |
| c 端消费 | home 顶栏 hero 品牌区 / brand-foot / 分享卡 / 关于 | 全站强调色走 app.wxss --color-primary:#9B2915 常量,无运行时主题色 | ❌ 未消费 |
关键结论:themePrimary 数据已在库里(brand JSONB),但 c 端无任何读出口——GET /config/site 顶层不展平、也不回嵌套 brand。因此 c 端补丁(§三)必须以后端补展平(§四)为前置,二者需同批代推,单独上 c 端补丁读不到值(会安静走兜底转山红,视觉无变化但功能未生效)。
兜底色:c 端 app.wxss --color-primary = #9B2915(转山红),全链路缺省统一用它。
src/services/brand.tsBrandDraft 5→6 字段,新增 themePrimary;SiteBrandView 增加嵌套 brand? 读源兼容DEFAULT_THEME_PRIMARY='#9B2915'、normalizeHexColor()(容错:trim/补#/3 位展开/大写,非法归空)、isThemeColor()(空=用兜底;非空严格 6 位 HEX)validateBrand color 分支严格 6 位 HEX;siteViewToDraft 读源顺序:顶层 themePrimary → 嵌套 brand.themePrimary → 空;saveBrand 提交前归一为大写 6 位src/services/brand.fixture.ts:补 themePrimary 正常/嵌套读源/4 条非法样例 + 9 条归一化样例src/pages/BrandConfig.tsx + BrandConfig.css:新增 ColorField(取色器 + HEX 文本双通道 + 色块),C 端手机预览的品牌名/Logo 兜底底/CTA 按钮/分享卡色条实时跟随主题色scripts/selftest-brand.mjs:断言扩到 6 字段,74/74 全绿;npx tsc --noEmit 0 错误;npx vite build 0前置:§四 后端展平已上线同环境。以下 diff 基于 main 当前行号给出,代推时以实际为准。
miniprogram/typings/api.d.ts(SiteConfig 加字段) export interface SiteConfig {
brandName: string
slogan: string
tibetanText: string
logoUrl: string
heroBgUrl: string
+ /** 主题色 #RRGGBB(治理快照第 6 字段;后端展平,缺省由 c 端兜底转山红) */
+ themePrimary?: string
points: PointConfig[]
miniprogram/utils/brand.ts(品牌层扩字段 + 严格 HEX 归一) export interface BrandAssets {
brandName: string
slogan: string
tibetanText: string
logoUrl: string
heroBgUrl: string
+ themePrimary: string
}
+/** c 端主题色兜底:转山红,与 app.wxss --color-primary 一致 */
+export const DEFAULT_THEME_PRIMARY = '#9B2915'
+const HEX6 = /^#[0-9a-fA-F]{6}$/
+
export function defaultBrand(): BrandAssets {
return {
brandName: '神山秘境',
slogan: '',
tibetanText: '',
logoUrl: '',
heroBgUrl: '',
+ themePrimary: DEFAULT_THEME_PRIMARY,
}
}
@@
if (site) {
cache = {
brandName: site.brandName || defaultBrand().brandName,
slogan: site.slogan || '',
tibetanText: site.tibetanText || '',
logoUrl: site.logoUrl || '',
heroBgUrl: site.heroBgUrl || '',
+ themePrimary: HEX6.test(site.themePrimary || '')
+ ? (site.themePrimary as string).toUpperCase()
+ : DEFAULT_THEME_PRIMARY,
}
}
小程序不支持运行时改 wxss 变量选择器,但支持页面根节点 inline style 设置 CSS 变量,子节点 var(--theme-primary) 即可跟随,无需把每个色值改成 setData。
pages/home/home.ts onLoad:
ensureBrandLoaded().then(() => {
const b = getBrand()
- if (b.brandName) this.setData({ 't.brandName': b.brandName })
+ this.setData({
+ ...(b.brandName ? { 't.brandName': b.brandName } : {}),
+ themeVars: `--theme-primary:${b.themePrimary};`,
+ })
})
并在 data 里加 themeVars: ''。
pages/home/home.wxml 最外层节点注入变量,并把品牌区/品牌尾强调色改用变量:
-<view class="page">
+<view class="page" style="{{themeVars}}">
- <safe-img src="/images/draft/logo.png" mode="aspectFit" img-class="brand-logo" />
- <text class="brand-tibetan">གངས་རིན་པོ་ཆེ</text>
+ <safe-img src="{{b.logoUrl || '/images/draft/logo.png'}}" mode="aspectFit" img-class="brand-logo" />
+ <text class="brand-tibetan" style="color:var(--theme-primary);">{{b.tibetanText || 'གངས་རིན་པོ་ཆེ'}}</text>
(brand-foot 的 brand-name、以及顶栏主按钮/选中态如需跟随,同样把写死色值换成 var(--theme-primary);app.wxss 保留 --color-primary 作为未注入变量页面的兜底,建议 home.wxss 内加 .brand-name,.brand-tibetan{ color:var(--theme-primary, var(--color-primary)); }。)
注:logo/tibetan 运行时化超出纯 themePrimary 范围,321 品牌层本就预留了 logoUrl/tibetanText 但 home 仍硬编码——是否一并接线由统筹决定;最小补丁只做主题色,logo/tibetan 保持现状也可。
home.ts 目前未定义 onShareAppMessage,分享标题未用运行时品牌。分享消息卡片由微信渲染、无法注入自定义主题色,主题色在分享卡场景无可应用的 DOM;如后续做了自定义分享海报页(canvas),用 getBrand().themePrimary 作为海报主色即可。结论:本件分享卡无 c 端改色动作,仅在 a 端预览里示意。var(--theme-primary, var(--color-primary)),数据取 getBrand()。当前无文件可改,登记为待消费点。补丁已由本席在沙箱本地预验证(2026-09-15):临时应用 §3.1+§3.2(typings + brand.ts 读源层)与 §3.3(home.ts setData themeVars 消费层)后tsc --noEmit -p tsconfig.json基线 0 → 补丁后仍 0 错误;验证后已git checkout HEAD全部还原,c 端源码零改动零残留(未越闸门、未提交)。代推人应用后仍需跑一遍步骤 1 复核。
cd miniprogram && npm run typecheck(tsc --noEmit strict)0 错误(AGENTS §7 前端提交硬门槛)#0E7C66)→ 保存 → 治理 publish → c 端冷启动(清缓存/重新进入)→ home 顶栏品牌名/藏文/主按钮变绿#9B2915,不白屏不报错red):c 端 HEX6 判定失败,安静兜底转山红backend/app/routers/miniapp_site.py get_site_config L74-78 顶层补一行展平:
"logoUrl": brand.get("logoUrl", ""),
"heroBgUrl": brand.get("heroBgUrl", ""),
+ "themePrimary": brand.get("themePrimary", ""),
curl -s "$BASE/api/miniapp/config/site" | jq -r '.data.themePrimary',在一次治理 publish 非默认色后应输出该 HEX;空库时输出空串。