feat(theme+规范): 管理端接入 yh-theme 主题层并按 bsch 应用页规范升级页面骨架
- 主题层: 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 通过
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* 管理端主题档位状态回归(yh-theme 迁移方案 §6.5):
|
||||
* 持久化读写、脏数据回退默认档、applyThemeState 类名幂等互斥。
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
DEFAULT_THEME_STATE,
|
||||
THEME_STORAGE_KEY,
|
||||
applyThemeState,
|
||||
initTheme,
|
||||
loadThemeState,
|
||||
parseThemeState,
|
||||
saveThemeState,
|
||||
} from "@/utils/theme";
|
||||
|
||||
function memStore(init: Record<string, string> = {}) {
|
||||
const map = new Map(Object.entries(init));
|
||||
return {
|
||||
getItem: (k: string) => map.get(k) ?? null,
|
||||
setItem: (k: string, v: string) => void map.set(k, v),
|
||||
_dump: () => Object.fromEntries(map),
|
||||
};
|
||||
}
|
||||
|
||||
describe("parseThemeState", () => {
|
||||
it("null/坏 JSON/非对象一律回退默认档", () => {
|
||||
expect(parseThemeState(null)).toEqual(DEFAULT_THEME_STATE);
|
||||
expect(parseThemeState("{oops")).toEqual(DEFAULT_THEME_STATE);
|
||||
expect(parseThemeState("[1,2]")).toEqual(DEFAULT_THEME_STATE);
|
||||
});
|
||||
it("单字段非法只回退该字段,其余保留", () => {
|
||||
const s = parseThemeState(JSON.stringify({ theme: "neon", gradOn: false }));
|
||||
expect(s.theme).toBe("blue");
|
||||
expect(s.gradOn).toBe(false);
|
||||
expect(s.sideDeep).toBe(true);
|
||||
});
|
||||
it("全合法字段按存档恢复", () => {
|
||||
const raw = JSON.stringify({ theme: "teal", sideDeep: false, gradOn: false, sideFollow: false });
|
||||
expect(parseThemeState(raw)).toEqual({ theme: "teal", sideDeep: false, gradOn: false, sideFollow: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("load/saveThemeState", () => {
|
||||
it("save 后 load 往返一致,键名约定 yh-theme", () => {
|
||||
const store = memStore();
|
||||
const state = { theme: "ocean" as const, sideDeep: true, gradOn: false, sideFollow: true };
|
||||
saveThemeState(state, store);
|
||||
expect(Object.keys(store._dump())).toEqual([THEME_STORAGE_KEY]);
|
||||
expect(loadThemeState(store)).toEqual(state);
|
||||
});
|
||||
it("storage 缺失/抛错不崩溃(隐私模式兜底)", () => {
|
||||
expect(loadThemeState(undefined)).toEqual(DEFAULT_THEME_STATE);
|
||||
const broken = { getItem: () => null, setItem: vi.fn(() => { throw new Error("quota"); }) };
|
||||
expect(() => saveThemeState(DEFAULT_THEME_STATE, broken)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyThemeState", () => {
|
||||
const root = () => document.documentElement;
|
||||
|
||||
beforeEach(() => {
|
||||
root().className = "";
|
||||
delete root().dataset.theme;
|
||||
});
|
||||
|
||||
it("默认档挂 data-theme=blue + side-deep + grad-on,且不出现互斥类", () => {
|
||||
applyThemeState(DEFAULT_THEME_STATE, root());
|
||||
expect(root().dataset.theme).toBe("blue");
|
||||
expect(root().classList).toContain("side-deep");
|
||||
expect(root().classList).not.toContain("side-light");
|
||||
expect(root().classList).toContain("grad-on");
|
||||
expect(root().classList).not.toContain("grad-off");
|
||||
expect(root().classList).not.toContain("side-plain");
|
||||
});
|
||||
|
||||
it("反复切换幂等:任意顺序调用后类名集合恰好匹配最后状态", () => {
|
||||
applyThemeState({ theme: "violet", sideDeep: false, gradOn: true, sideFollow: false }, root());
|
||||
applyThemeState({ theme: "ink", sideDeep: true, gradOn: false, sideFollow: true }, root());
|
||||
expect(root().dataset.theme).toBe("ink");
|
||||
expect([...root().classList].sort()).toEqual(["grad-off", "side-deep"]);
|
||||
});
|
||||
|
||||
it("initTheme 读 localStorage 恢复现场(启动引导路径)", () => {
|
||||
window.localStorage.setItem(
|
||||
THEME_STORAGE_KEY,
|
||||
JSON.stringify({ theme: "teal", sideDeep: false, gradOn: true, sideFollow: true })
|
||||
);
|
||||
const st = initTheme();
|
||||
expect(st.theme).toBe("teal");
|
||||
expect(root().dataset.theme).toBe("teal");
|
||||
expect([...root().classList].sort()).toEqual(["grad-on", "side-light"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user