Files
aiot-platform-ui/apps/web-antd/public/lottie-theme-patch.js
lzh 6b8626907e feat(@vben/web-antd): Lottie 品牌加载动画(主题色自适应)
- 新增全局 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>
2026-04-23 20:26:48 +08:00

152 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);