- 主题层: scripts/gen-yh-theme.mjs 生成 assets/styles/yh-theme.css(构建期静态化 color-mix), 5 主题档×侧栏深浅×渐变×side-plain 挂 html 档位类名; main.ts 于 icons.css 后引入并 initTheme() 消首帧闪色 - utils/theme.ts(+8 用例): localStorage(yh-theme) 持久化 + <html> 档位类名幂等切换 - ThemeSwitcher.vue(+6 用例): 顶栏配色胶囊(5 色网格+3 开关), 定稿 07-seam-pure 同款皮肤 - SchoolLayout: 顶栏 logo 徽标+配色切换器挂载; 站点身份色带退役, 子站橙由 yh-theme .band-sub::before 融合 - useTableHeight 响应化(+5 用例): ResizeObserver(body) 重算修复窗口缩放冻结, 常量 56/56 档不动 - bsch 规范骨架: 操作区 fix mb-12/次按钮 fr, Table 补 link-table table-bottom-box, 文章关键字 200px 即输即搜(300ms 防抖去 enter-button), 附件分页 customPageCount 记忆+page-box fr, 轮播展示位移入搜索区, 友链分组改 tabs-wrapper+Tabs card, 五表单校验失败统一"请填写红色星号内容!" - 142 用例全绿, vite build 通过
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
/**
|
||
* 管理端主题状态(yh-theme,定稿 07-seam-pure.html,方案《管理端整体主题迁移方案》§6.5)。
|
||
* 档位全部挂在 <html>:data-theme × side-deep|side-light × grad-on|grad-off × side-plain,
|
||
* 皮肤规则在 assets/styles/yh-theme.css(生成物)。与门户皮肤包(templates/themes)正交。
|
||
*/
|
||
export const THEME_NAMES = ["blue", "ocean", "ink", "violet", "teal"] as const;
|
||
export type ThemeName = (typeof THEME_NAMES)[number];
|
||
|
||
/** 定稿默认档:blue + side-deep + grad-on + 侧栏跟随(D1 拍板 deep) */
|
||
export interface ThemeState {
|
||
theme: ThemeName;
|
||
/** 侧栏深色(false=浅色档) */
|
||
sideDeep: boolean;
|
||
/** 顶栏渐变(联动侧栏竖向渐变) */
|
||
gradOn: boolean;
|
||
/** 侧栏颜色跟随顶栏品牌色(false=中性灰 side-plain) */
|
||
sideFollow: boolean;
|
||
}
|
||
|
||
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 };
|
||
}
|
||
}
|
||
|
||
interface StorageLike {
|
||
getItem(k: string): string | null;
|
||
setItem(k: string, v: string): void;
|
||
}
|
||
|
||
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 于挂载前调用,消除首帧闪色 */
|
||
export function initTheme(): ThemeState {
|
||
return applyThemeState(loadThemeState());
|
||
}
|