Files
lucky_shop/common/js/merge-theme.js
ZF sun 555443dc44 feat: add remote theme override support
实现了全量的主题远端覆写功能,包含:
1. 新增主题合并工具类,支持多桶安全合并与原型链过滤
2. 新增主题远端覆写运行时,支持多租户、多端、三态缓存
3. 改造Vuex主题加载逻辑,实现本地预设+远端覆写的合并应用
4. 改造App.vue首屏主题加载流程,支持并行拉取与缓存预热
5. 新增主题更新、租户切换的事件监听与自动刷新逻辑
2026-06-11 14:55:32 +08:00

82 lines
2.9 KiB
JavaScript

/**
* 主题 token 合并工具
*
* 与 lang.js::applyOverrides 同构(都是"远端扁平覆写 map"按 key 路径合并),
* 差异:
* - 主题 token 是"扁平叶子"(如 main_color / bg_color),没有嵌套层级
* - 需要支持"三桶叠加":presetBase(本地预设) < brand(品牌色) < userMode(夜间/浅色)
* - value 是色值字符串(也允许空串 = 显式清空)
*
* 危险 key 过滤:拦截 __proto__/constructor/prototype,防止恶意覆写污染对象原型链
*/
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
/**
* 把单层 map 的 keys 过滤掉危险段(白名单:值必须是 string,null/undefined/其它跳过)
* - 输入: 远端扁平 map { 'main_color': '#FF4646', '...': null, ... }
* - 输出: 净化后的 map(只保留 string 值,空串也是合法覆写)
*/
export function sanitizeFlatMap(flatMap) {
const out = {};
if (!flatMap || typeof flatMap !== 'object') return out;
for (const key in flatMap) {
if (!Object.prototype.hasOwnProperty.call(flatMap, key)) continue;
// 危险 key 过滤
if (FORBIDDEN_KEYS.has(String(key))) continue;
const val = flatMap[key];
if (typeof val !== 'string') continue;
out[key] = val;
}
return out;
}
/**
* 合并多桶 map
* @param {...object} maps 从低优先级到高优先级依次传入
* 例: mergeThemeBuckets(presetBase, brand, dark) = preset 被 brand 覆盖,brand 被 dark 覆盖
* @returns {object}
*/
export function mergeThemeBuckets(...maps) {
const out = {};
for (const m of maps) {
if (!m || typeof m !== 'object') continue;
for (const k in m) {
if (!Object.prototype.hasOwnProperty.call(m, k)) continue;
if (FORBIDDEN_KEYS.has(String(k))) continue;
const v = m[k];
if (typeof v !== 'string') continue;
out[k] = v;
}
}
return out;
}
/**
* 一站式合并:本地预设(已选中的 theme 子集) + brand 桶 + userMode 桶
* @param {object} preset 本地 style_color.js 中选中的 theme(如 'default' 的所有 token)
* @param {object} brandOverrides brand 桶(任何模式都生效)
* @param {object} userModeOverrides userMode 桶(light/dark,叠加在 brand 之上)
* @returns {object} { tokens, sources }
* tokens: 最终扁平 map
* sources: 每个 key 的来源(用于调试/可视化) 'preset' | 'brand' | 'userMode'
*/
export function composeFinalTokens(preset, brandOverrides, userModeOverrides) {
const tokens = {};
const sources = {};
const presetMap = sanitizeFlatMap(preset);
const brandMap = sanitizeFlatMap(brandOverrides);
const modeMap = sanitizeFlatMap(userModeOverrides);
for (const k in presetMap) { tokens[k] = presetMap[k]; sources[k] = 'preset'; }
for (const k in brandMap) { tokens[k] = brandMap[k]; sources[k] = 'brand'; }
for (const k in modeMap) { tokens[k] = modeMap[k]; sources[k] = 'userMode'; }
return { tokens, sources };
}
export default {
sanitizeFlatMap,
mergeThemeBuckets,
composeFinalTokens,
};