- 新增全局 loading:自定义 apps/web-antd/loading.html 覆盖 inject-app-loading 的默认模板,Vue 挂载前就能播放品牌动画 - 新增 LottieLoading 组件,用于 SSO 回调等"白屏时间偏长"的运行时场景 - 换色方案:Lottie JSON 里原本 5 档硬编码绿色,按当前主题 colorPrimary 生成 5 档 HSL 亮度做指纹替换。挂载前从 localStorage preferences 读色, 挂载后读 CSS 变量 --primary,两条路径共用 public/lottie-theme-patch.js 一份 classic-JS 源,window.__LottieThemePatch__ 上暴露 - vite 插件:启动/构建时从 node_modules 把 lottie_light.min.js 拷到 public/ 供 loading.html <script> 加载;.gitignore 排除该产物 - LottieLoading 复用 loading.html 已经挂好的 window.lottie,不再把 ~170KB 播放器再打进 Vue 产物 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
4.9 KiB
JavaScript
152 lines
4.9 KiB
JavaScript
/**
|
||
* Lottie 主题色换色共享模块(单一真源)
|
||
*
|
||
* 同时供两处使用:
|
||
* 1. loading.html —— Vue 挂载前,用 localStorage 里缓存的 preferences 读主题色
|
||
* 2. LottieLoading.vue —— 组件运行时,直接读 CSS 变量 --primary
|
||
*
|
||
* JSON 里的动画原本是 5 档硬编码绿色,按此映射替换为主题色的 5 档亮度。
|
||
* 本文件不进打包,由 <script src="/lottie-theme-patch.js"> 引入,
|
||
* 会在 window 上挂 __LottieThemePatch__。
|
||
*/
|
||
(function (global) {
|
||
/** JSON 中硬编码的 5 档绿色(取 2 位小数指纹) -> 5 档亮度索引 */
|
||
var SHADE_MAP = {
|
||
'0.62,0.92,0.49': 0, // 最浅
|
||
'0.44,0.74,0.08': 1,
|
||
'0.30,0.62,0.04': 2,
|
||
'0.18,0.47,0.02': 3,
|
||
'0.14,0.39,0.02': 4, // 最深
|
||
};
|
||
|
||
/** 5 档亮度(从浅到深),覆盖主流主题色 */
|
||
var LIGHTNESSES = [72, 58, 48, 36, 26];
|
||
|
||
var FALLBACK_HSL = { h: 37, s: 100, l: 52 }; // 与 preferences/config.ts 默认色一致
|
||
var HSL_PARSE_RE = /([\d.]+)(?:deg)?[\s,]+([\d.]+)%?[\s,]+([\d.]+)%?/i;
|
||
var HSL_FN_RE = /hsl\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%?[\s,]+([\d.]+)%?\s*\)/i;
|
||
|
||
function hslToRgb(h, s, l) {
|
||
var hh = (((h % 360) + 360) % 360) / 360;
|
||
var ss = s / 100;
|
||
var ll = l / 100;
|
||
if (ss === 0) return [ll, ll, ll];
|
||
var q = ll < 0.5 ? ll * (1 + ss) : ll + ss - ll * ss;
|
||
var p = 2 * ll - q;
|
||
function f(t) {
|
||
var tt = t;
|
||
if (tt < 0) tt += 1;
|
||
if (tt > 1) tt -= 1;
|
||
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
||
if (tt < 1 / 2) return q;
|
||
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
||
return p;
|
||
}
|
||
return [f(hh + 1 / 3), f(hh), f(hh - 1 / 3)];
|
||
}
|
||
|
||
function rgbKey(arr) {
|
||
if (!arr || arr.length < 3) return '';
|
||
return arr[0].toFixed(2) + ',' + arr[1].toFixed(2) + ',' + arr[2].toFixed(2);
|
||
}
|
||
|
||
function buildShades(hsl) {
|
||
return LIGHTNESSES.map(function (l) {
|
||
return hslToRgb(hsl.h, hsl.s, l);
|
||
});
|
||
}
|
||
|
||
/** 就地替换 Lottie JSON 中的填充色(静态与关键帧两种形态) */
|
||
function patchColors(data, shades) {
|
||
function swap(arr) {
|
||
var idx = SHADE_MAP[rgbKey(arr)];
|
||
if (idx === undefined) return null;
|
||
var s = shades[idx];
|
||
return [s[0], s[1], s[2], arr[3] == null ? 1 : arr[3]];
|
||
}
|
||
var layers = (data && data.layers) || [];
|
||
for (var li = 0; li < layers.length; li++) {
|
||
var shapes = layers[li].shapes || [];
|
||
for (var si = 0; si < shapes.length; si++) {
|
||
var its = shapes[si].it;
|
||
if (!its) continue;
|
||
for (var ii = 0; ii < its.length; ii++) {
|
||
var item = its[ii];
|
||
if (item.ty !== 'fl' || !item.c) continue;
|
||
var k = item.c.k;
|
||
if (Array.isArray(k) && typeof k[0] === 'number') {
|
||
var v = swap(k);
|
||
if (v) item.c.k = v;
|
||
} else if (Array.isArray(k)) {
|
||
for (var ki = 0; ki < k.length; ki++) {
|
||
var kf = k[ki];
|
||
if (kf && kf.s && typeof kf.s[0] === 'number') {
|
||
var v2 = swap(kf.s);
|
||
if (v2) kf.s = v2;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
|
||
/**
|
||
* Vue 挂载前:扫 localStorage 里 preferences 缓存读 colorPrimary
|
||
* namespacePrefix 用于把扫描范围限定在当前 app 的 key 上,避免误命中其它应用
|
||
*/
|
||
function readPrimaryHslFromLocalStorage(namespacePrefix) {
|
||
try {
|
||
for (var i = 0; i < localStorage.length; i++) {
|
||
var k = localStorage.key(i);
|
||
if (!k) continue;
|
||
if (namespacePrefix && k.indexOf(namespacePrefix) !== 0) continue;
|
||
if (!k.endsWith('-preferences')) continue;
|
||
if (k.endsWith('-preferences-theme')) continue;
|
||
if (k.endsWith('-preferences-locale')) continue;
|
||
var raw = localStorage.getItem(k);
|
||
if (!raw) continue;
|
||
var parsed = JSON.parse(raw);
|
||
var color =
|
||
parsed &&
|
||
parsed.value &&
|
||
parsed.value.theme &&
|
||
parsed.value.theme.colorPrimary;
|
||
if (!color) continue;
|
||
var m = HSL_FN_RE.exec(color);
|
||
if (!m) continue;
|
||
return { h: +m[1], s: +m[2], l: +m[3] };
|
||
}
|
||
} catch (e) {
|
||
/* fall through to fallback */
|
||
}
|
||
return FALLBACK_HSL;
|
||
}
|
||
|
||
/** Vue 运行时:读 CSS 变量 --primary(格式 "H S% L%") */
|
||
function readPrimaryHslFromCss() {
|
||
try {
|
||
var raw = getComputedStyle(document.documentElement)
|
||
.getPropertyValue('--primary')
|
||
.trim();
|
||
if (!raw) return FALLBACK_HSL;
|
||
var m = HSL_PARSE_RE.exec(raw);
|
||
return m ? { h: +m[1], s: +m[2], l: +m[3] } : FALLBACK_HSL;
|
||
} catch (e) {
|
||
return FALLBACK_HSL;
|
||
}
|
||
}
|
||
|
||
global.__LottieThemePatch__ = {
|
||
SHADE_MAP: SHADE_MAP,
|
||
LIGHTNESSES: LIGHTNESSES,
|
||
hslToRgb: hslToRgb,
|
||
rgbKey: rgbKey,
|
||
buildShades: buildShades,
|
||
patchColors: patchColors,
|
||
readPrimaryHslFromLocalStorage: readPrimaryHslFromLocalStorage,
|
||
readPrimaryHslFromCss: readPrimaryHslFromCss,
|
||
};
|
||
})(window);
|