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>
This commit is contained in:
lzh
2026-04-23 20:26:48 +08:00
parent 348e40e9c2
commit 6b8626907e
11 changed files with 403 additions and 10 deletions

View File

@@ -0,0 +1,151 @@
/**
* 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);