1. 新增theme-override.js实现主题远端拉取逻辑 2. 新增empty-style-stub.js作为主题包占位符,减少主包~75KB 3. 在vue.config.js中添加构建期主题stub替换逻辑 4. 重构config-external.js的主题配置,新增remote-only模式支持 5. 重构store的applyThemeWithRemote方法,适配远端主题拉取 6. 在bootstrap中新增主题初始化和异步拉取逻辑
213 lines
7.7 KiB
JavaScript
213 lines
7.7 KiB
JavaScript
// 外置配置管理
|
||
// 用于管理需要外置的配置项,如语言包、主题配置等
|
||
|
||
// 语言包配置
|
||
export const langConfig = {
|
||
// 语言列表 — 与后端 Lang.php::overrides() 白名单对齐
|
||
// 增删需要后端同步改 LangOverrideModel::normalizeAppType 旁的 $allow_lang
|
||
langList: ['zh-cn', 'en-us'],
|
||
// 默认语言
|
||
defaultLang: 'zh-cn',
|
||
// 语言包加载方式:'lazy'(按需加载)或 'preload'(预加载)
|
||
loadMode: 'lazy',
|
||
// 数据源开关
|
||
// 数据源开关
|
||
// local : 纯本地包,忽略 remote
|
||
// remote : 本地兜底 + 后端 KV 覆写(首屏强依赖,见 bootTimeout)
|
||
// hybrid : 同 remote,后端不可用时用本地
|
||
// remote-only : 完全依赖后端 /api/lang/bundle,本地 lang/ 目录可删除
|
||
source: 'remote-only',
|
||
// 远端覆写相关
|
||
remote: {
|
||
overridesUrl: '/api/lang/overrides',
|
||
// bundle 接口(remote-only 模式下使用)
|
||
bundleUrl: '/api/lang/bundle',
|
||
// diff 接口(预留)
|
||
diffUrl: '/api/lang/diff',
|
||
// 首屏强依赖远端翻译(多租户文案强隔离),阻塞启动
|
||
// 但必须有上限避免白屏
|
||
// 2026-07-02: 从 2000 改为 5000 — 慢启动场景下,给首屏渲染更多时间
|
||
bootTimeout: 5000,
|
||
// 单次请求超时(ms)
|
||
// 2026-07-02: 从 4000 改为 60000 — 远端首次冷启动可能慢(CDN 边缘冷缓存/跨网段回源/MySQL JOIN)
|
||
// 配合 loadBundleTree 的"迟到数据补救"机制,即便超时也能拿到远端最终返回的数据
|
||
requestTimeout: 60000,
|
||
// 缓存有效期(秒),与后端 Cache::set 600s 对齐;过期后后台刷新
|
||
cacheTtl: 600,
|
||
// 本地缓存 key 前缀: <prefix><uniacid>_<appType>_<lang>
|
||
// 三维隔离,与 lang.js::getOverrideStorageKey / getOverrideMemKey 保持一致
|
||
storagePrefix: 'LANG_OVR_',
|
||
// 切租户/uniacid 时强制重拉
|
||
refreshOnUniacidChange: true,
|
||
// 签名校验密钥,留空不校验(需与后端 LANG_SIGN_SECRET 一致)
|
||
signSecret: '',
|
||
}
|
||
};
|
||
|
||
// 主题配置
|
||
export const themeConfig = {
|
||
// 主题列表 — 与 style_color.js 顶层的 preset 名严格一致(8 个)
|
||
// 关键: 这里列出的名字必须能在 style_color.js (或后端 style_color.php 镜像) 中找到对应 preset
|
||
// 历史上曾包含 'red' 但 style_color.js 从未定义 'red' preset, 切到 'red' 主题时
|
||
// loadTheme() 会返回 undefined → 走 stub default 兜底样式, 远端也无法补齐
|
||
// 2026-07-07 移除 'red' 以保持一致性
|
||
themeList: ['default', 'green', 'blue', 'pink', 'gold', 'purple', 'yellow', 'black'],
|
||
// 默认主题
|
||
defaultTheme: 'default',
|
||
// 远端 token 覆写相关(与 langConfig 同构)
|
||
// source : 'local' | 'remote' | 'hybrid' | 'remote-only'
|
||
// remote.* : 首屏拉取 + 缓存 + 三态机 + 多租户多端隔离
|
||
overrideRemote: {
|
||
overridesUrl: '/api/theme/overrides',
|
||
// 全量 preset 拉取(remote-only 模式下使用)
|
||
// 返回 8 个 preset 完整 token,替代本地 style_color.js
|
||
presetsUrl: '/api/theme/presets',
|
||
bootTimeout: 5000,
|
||
requestTimeout: 60000,
|
||
// 远端全量 preset 缓存 key 前缀
|
||
storagePresetsPrefix: 'THEME_PRESETS_',
|
||
storagePrefix: 'THEME_OVR_',
|
||
refreshOnUniacidChange: true,
|
||
// source: 'remote-only' 时,webpack 会用 NormalModuleReplacementPlugin
|
||
// 把 style_color.js 替换为 empty-style-stub.js(只含 emergency fallback)
|
||
// 节省主包 ~75KB(完整 8 preset × ~50 token = 80KB → stub 7KB)
|
||
//
|
||
// 与 lang/ 目录的 remote-only 模式完全同构:
|
||
// - 构建期: webpack 替换为 stub
|
||
// - 运行时: ThemeOverride.loadAllPresets() 异步拉 /api/theme/presets
|
||
// - 首屏: 走 stub 的 default preset,不白屏
|
||
// - 切换: 切到非 default preset 时,等远端拉完再应用(由 applyThemeWithRemote
|
||
// 订阅 theme:presets-updated 事件驱动)
|
||
source: 'remote-only',
|
||
}
|
||
};
|
||
|
||
// API配置
|
||
export const apiConfig = {
|
||
// API请求超时时间
|
||
timeout: 30000,
|
||
// 是否开启重试
|
||
retry: true,
|
||
// 重试次数
|
||
retryCount: 3,
|
||
// 重试间隔时间(毫秒)
|
||
retryDelay: 1000
|
||
};
|
||
|
||
// 配置外置管理类
|
||
class ConfigExternal {
|
||
constructor() {
|
||
this.loadedConfigs = {};
|
||
this.loadPromises = {};
|
||
}
|
||
|
||
/**
|
||
* 加载语言包
|
||
* @param {string} lang - 语言类型
|
||
* @returns {Promise} - 加载结果
|
||
*/
|
||
async loadLang(lang = langConfig.defaultLang) {
|
||
if (this.loadedConfigs[`lang_${lang}`]) {
|
||
return this.loadedConfigs[`lang_${lang}`];
|
||
}
|
||
|
||
if (this.loadPromises[`lang_${lang}`]) {
|
||
return this.loadPromises[`lang_${lang}`];
|
||
}
|
||
|
||
this.loadPromises[`lang_${lang}`] = new Promise((resolve, reject) => {
|
||
try {
|
||
// 动态加载语言包
|
||
const langData = require(`@/lang/${lang}/common.js`).lang;
|
||
this.loadedConfigs[`lang_${lang}`] = langData;
|
||
resolve(langData);
|
||
} catch (error) {
|
||
// console.error(`加载语言包 ${lang} 失败:`, error);
|
||
reject(error);
|
||
}
|
||
});
|
||
|
||
return this.loadPromises[`lang_${lang}`];
|
||
}
|
||
|
||
/**
|
||
* 加载页面语言包
|
||
* @param {string} lang - 语言类型
|
||
* @param {string} pagePath - 页面路径
|
||
* @returns {Promise} - 加载结果
|
||
*/
|
||
async loadPageLang(lang = langConfig.defaultLang, pagePath) {
|
||
const key = `page_${lang}_${pagePath}`;
|
||
if (this.loadedConfigs[key]) {
|
||
return this.loadedConfigs[key];
|
||
}
|
||
|
||
if (this.loadPromises[key]) {
|
||
return this.loadPromises[key];
|
||
}
|
||
|
||
this.loadPromises[key] = new Promise((resolve, reject) => {
|
||
try {
|
||
// 动态加载页面语言包
|
||
const pageLangData = require(`@/lang/${lang}/${pagePath}.js`).lang;
|
||
this.loadedConfigs[key] = pageLangData;
|
||
resolve(pageLangData);
|
||
} catch (error) {
|
||
console.error(`加载页面语言包 ${lang}/${pagePath} 失败:`, error);
|
||
resolve({});
|
||
}
|
||
});
|
||
|
||
return this.loadPromises[key];
|
||
}
|
||
|
||
/**
|
||
* 加载主题配置(异步方式)
|
||
* 关键: 在 remote-only 模式下, style_color.js 已被 webpack 替换为 empty-style-stub.js,
|
||
* 只含 1 个 default preset. 其它 7 个 preset 由 ThemeOverride.loadAllPresets()
|
||
* 异步拉取并合并到本地缓存. 这里的 loadTheme 优先从缓存取,缓存未命中时回退到 stub.
|
||
*
|
||
* @param {string} theme - 主题名称
|
||
* @returns {Promise} - 加载结果
|
||
*/
|
||
async loadTheme(theme = themeConfig.defaultTheme) {
|
||
// 1) 先查已加载的内存缓存(包括远端拉取后写入的)
|
||
if (this.loadedConfigs[`theme_${theme}`]) {
|
||
return this.loadedConfigs[`theme_${theme}`];
|
||
}
|
||
|
||
// 2) remote-only 模式下,从 ThemeOverride 内存缓存(由 loadAllPresets 写入)取
|
||
try {
|
||
const ThemeOverride = require('./theme-override.js').default;
|
||
const remotePresets = ThemeOverride.getAllPresets();
|
||
if (remotePresets && remotePresets[theme]) {
|
||
this.loadedConfigs[`theme_${theme}`] = remotePresets[theme];
|
||
return remotePresets[theme];
|
||
}
|
||
} catch (e) {
|
||
// ignore — ThemeOverride 不可用时继续走 require stub
|
||
}
|
||
|
||
// 3) 兜底: require style_color.js (local 模式全量, remote-only 模式 stub)
|
||
if (this.loadPromises[`theme_${theme}`]) {
|
||
return this.loadPromises[`theme_${theme}`];
|
||
}
|
||
|
||
this.loadPromises[`theme_${theme}`] = new Promise((resolve, reject) => {
|
||
try {
|
||
// 动态加载主题配置 (webpack 替换为 stub 时,只返回 1 个 default preset)
|
||
const themeData = require(`@/common/js/style_color.js`)['default'][theme];
|
||
this.loadedConfigs[`theme_${theme}`] = themeData;
|
||
resolve(themeData);
|
||
} catch (error) {
|
||
console.error(`加载主题 ${theme} 失败:`, error);
|
||
reject(error);
|
||
}
|
||
});
|
||
|
||
return this.loadPromises[`theme_${theme}`];
|
||
}
|
||
}
|
||
|
||
export default new ConfigExternal();
|