From 7c885c0b4b2e8b8eae6b1ed89d575faf901398a1 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 23 Sep 2026 15:59:51 +0800 Subject: [PATCH] =?UTF-8?q?chore(theme):=20=E5=90=8C=E6=AD=A5=20YschTheme?= =?UTF-8?q?=20=E5=86=8D=E7=94=9F=E4=BA=A7=E7=89=A9=20theme.ts=EF=BC=88?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=88=B3=2020260923-1454-9619fbf=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅版本戳注释行变化,正文与 20260921-1219-bb1637c 版一致;main 此前无该文件,本次新增。 --- src/utils/theme.ts | 79 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/utils/theme.ts diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..db090f4 --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,79 @@ +/** + * 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20260923-1454-9619fbf),勿手改。 + * 分发方式: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"; + +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()); +}