/** * 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20261006-2013-75c1d10),勿手改。 * 分发方式:YschTheme `npm run sync` 拷进各端 src/utils/theme.ts(与 ThemeSwitcher.vue 配套,MD5 对齐报告)。 * 运行时档位类名由 /static/js/yh-theme-init.js 在 内先行挂好,本模块只负责读写与切换。 */ 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"; /* 学校默认档存储键(UserHome 登录时写入;值格式与 yh-theme 完全相同,仅无"用户自选"语义) */ export const SCHOOL_DEFAULT_STORAGE_KEY = "yh-theme-sch-default"; 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 }; } } /** 严格读取:四字段全部合法才返回状态,否则 null(合成链"值非法→回落下一级"用;用户键仍走宽松解析保现状) */ function readThemeStateOrNull(raw: string | null): ThemeState | null { if (!raw) return null; try { const o = JSON.parse(raw); if (!o || typeof o !== "object") return null; if (!THEME_NAMES.includes(o.theme)) return null; if (typeof o.sideDeep !== "boolean" || typeof o.gradOn !== "boolean" || typeof o.sideFollow !== "boolean") return null; return { theme: o.theme, sideDeep: o.sideDeep, gradOn: o.gradOn, sideFollow: o.sideFollow }; } catch { return null; } } /** URL 覆盖参数(?yh-theme= / ?yh-side=deep|light / ?yh-grad=on|off):仅预览/排障用, 非法值忽略(对应字段不覆盖),且只影响"读哪一档"、绝不写回 localStorage。 */ function readThemeOverrides(): { theme?: ThemeName; sideDeep?: boolean; gradOn?: boolean } | null { let out = null; try { const search = (globalThis.location && globalThis.location.search) || ""; const get = (name) => { const m = new RegExp("[?&]" + name + "=([^&]*)").exec(search); return m ? decodeURIComponent(m[1]) : null; }; const t = get("yh-theme"); if (t && THEME_NAMES.includes(t)) { out = out || {}; out.theme = t; } const s = get("yh-side"); if (s === "deep" || s === "light") { out = out || {}; out.sideDeep = s === "deep"; } const g = get("yh-grad"); if (g === "on" || g === "off") { out = out || {}; out.gradOn = g === "on"; } } catch { /* 无 location 环境(测试/SSR):无覆盖 */ } return out; } /** * 生效档合成(唯一实现,init 启动段与 UserHome theme.ts 共用): * URL 覆盖 > localStorage["yh-theme"](用户自选,存在即用——宽松修补,与历史行为一致) * > localStorage["yh-theme-sch-default"](学校默认,整体合法才生效) * > DEFAULT_THEME_STATE。 * 键不存在/解析失败/值非法均静默回落,不报错不阻塞首帧。 */ export function resolveEffectiveThemeState(storage: StorageLike | undefined = globalThis.localStorage): ThemeState { let state = null; if (storage) { const userRaw = storage.getItem(THEME_STORAGE_KEY); if (userRaw) state = parseThemeState(userRaw); if (!state) state = readThemeStateOrNull(storage.getItem(SCHOOL_DEFAULT_STORAGE_KEY)); } if (!state) state = { ...DEFAULT_THEME_STATE }; const ov = readThemeOverrides(); if (ov) { if (ov.theme) state = { ...state, theme: ov.theme }; if (ov.sideDeep != null) state = { ...state, sideDeep: ov.sideDeep }; if (ov.gradOn != null) state = { ...state, gradOn: ov.gradOn }; } return 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()); }