build / build (push) Successful in 4m19s
- 接入点收敛为 index.html <head> 内一段同步 loader(env-config 引导之后、title/入口之前),
document.write 保 parser-blocking;通道 init 已吸收 FOUC 门 + manifest 预取 + DCL 挂 <link>
+ href 去重交棒,主题逻辑升级全走 /static,不再需要重新构建本应用
- 域名四档:staticHost > baseHost > VITE_THEME_HOST(dev 跨源直连专用,需通道 CORS)> 同源相对。
不复用 VITE_BASE_HOST:它与 SSO/API 共用、.env.dev 里本就非空,复用会让 dev 立刻跨源被 CORS 拦;
build 产物不读 .env,占位符留字面量 → 置空 → 同源(产物零环境值烧入,与 2f423c2 口径一致)
- 删 plugins/vite-plugin-theme(-420 行 vendor)+ package.json/lock 的 file: 依赖,
根治 CI 容器解析不到 file:../../YschTheme 那类 npm ci 秒失败
- vite.config:保留 dev /static 代理(联调便利,changeOrigin 顺带改写 Host,等价旧插件的
headers.Host 补偿),注释端口纠错 5174 → 9527;theme.ts 同步到通道版本 20260929-1112-1b1724d
- 已验:npm run build 通过,dist/index.html 注入段存活且无 B 类痕迹,
YschTheme npm run smoke 断言 3 张主题表按序挂载、FOUC 门先于首帧、manifest 只拉一次
80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
/**
|
||
* 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20260929-1112-1b1724d),勿手改。
|
||
* 分发方式:YschTheme `npm run sync` 拷进各端 src/utils/theme.ts(与 ThemeSwitcher.vue 配套,MD5 对齐报告)。
|
||
* 运行时档位类名由 /static/js/yh-theme-init.js 在 <head> 内先行挂好,本模块只负责读写与切换。
|
||
*/
|
||
export type ThemeName = "blue" | "ocean" | "ink" | "violet" | "teal";
|
||
|
||
/** 定稿默认档:blue + side-deep + grad-on + 侧栏跟随(D1 拍板 deep) */
|
||
export interface ThemeState {
|
||
theme: ThemeName;
|
||
/** 侧栏深色(false=浅色档) */
|
||
sideDeep: boolean;
|
||
/** 顶栏渐变(联动侧栏竖向渐变) */
|
||
gradOn: boolean;
|
||
/** 侧栏颜色跟随顶栏品牌色(false=中性灰 side-plain) */
|
||
sideFollow: boolean;
|
||
}
|
||
|
||
interface StorageLike {
|
||
getItem(k: string): string | null;
|
||
setItem(k: string, v: string): void;
|
||
}
|
||
|
||
export const THEME_NAMES: readonly ThemeName[] = ["blue", "ocean", "ink", "violet", "teal"];
|
||
|
||
export const THEME_STORAGE_KEY = "yh-theme";
|
||
|
||
export const DEFAULT_THEME_STATE: ThemeState = {
|
||
theme: "blue",
|
||
sideDeep: true,
|
||
gradOn: true,
|
||
sideFollow: true
|
||
};
|
||
|
||
/** 宽松解析:任一字段非法/缺失即回退默认档(脏数据不阻塞启动) */
|
||
export function parseThemeState(raw: string | null): ThemeState {
|
||
if (!raw) return { ...DEFAULT_THEME_STATE };
|
||
try {
|
||
const o = JSON.parse(raw);
|
||
if (!o || typeof o !== "object") return { ...DEFAULT_THEME_STATE };
|
||
return {
|
||
theme: THEME_NAMES.includes(o.theme) ? o.theme : DEFAULT_THEME_STATE.theme,
|
||
sideDeep: typeof o.sideDeep === "boolean" ? o.sideDeep : DEFAULT_THEME_STATE.sideDeep,
|
||
gradOn: typeof o.gradOn === "boolean" ? o.gradOn : DEFAULT_THEME_STATE.gradOn,
|
||
sideFollow: typeof o.sideFollow === "boolean" ? o.sideFollow : DEFAULT_THEME_STATE.sideFollow
|
||
};
|
||
} catch {
|
||
return { ...DEFAULT_THEME_STATE };
|
||
}
|
||
}
|
||
|
||
export function loadThemeState(storage: StorageLike | undefined = globalThis.localStorage): ThemeState {
|
||
if (!storage) return { ...DEFAULT_THEME_STATE };
|
||
return parseThemeState(storage.getItem(THEME_STORAGE_KEY));
|
||
}
|
||
|
||
export function saveThemeState(state: ThemeState, storage: StorageLike | undefined = globalThis.localStorage): void {
|
||
try {
|
||
storage?.setItem(THEME_STORAGE_KEY, JSON.stringify(state));
|
||
} catch {
|
||
/* 隐私模式/配额失败静默:仅丢持久化,不丢本次会话生效 */
|
||
}
|
||
}
|
||
|
||
/** 把状态落到根元素档位类名(幂等:先清后挂),返回传入的 state 便于链式 */
|
||
export function applyThemeState(state: ThemeState, root: HTMLElement = document.documentElement): ThemeState {
|
||
root.dataset.theme = state.theme;
|
||
root.classList.toggle("side-deep", state.sideDeep);
|
||
root.classList.toggle("side-light", !state.sideDeep);
|
||
root.classList.toggle("grad-on", state.gradOn);
|
||
root.classList.toggle("grad-off", !state.gradOn);
|
||
root.classList.toggle("side-plain", !state.sideFollow);
|
||
return state;
|
||
}
|
||
|
||
/** 启动引导:各端 main.ts(或本仓 init 脚本的启动段)于挂载前调用,消除首帧闪色 */
|
||
export function initTheme(): ThemeState {
|
||
return applyThemeState(loadThemeState());
|
||
}
|