feat(admin): 站点外观 - 支持 Tab 分类与主题渐变按钮配置

This commit is contained in:
ddCat
2026-09-18 23:28:57 +08:00
parent f77bd2bfa0
commit 3cb1423f7e
27 changed files with 869 additions and 375 deletions

View File

@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
import { getAntThemeConfig, getWorkspaceAntThemeConfig } from "../src/lib/app-theme";
import { DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition } from "../src/lib/skin-themes";
import { getIsolatedAdminAntTheme } from "../src/pages/admin/theme/admin-ant-theme";
describe("shared action colors and focus feedback", () => {
for (const dark of [false, true]) {
test(`classic solid tokens remain monochrome beneath the button fill adapter in ${dark ? "dark" : "light"} mode`, () => {
const product = getAntThemeConfig(dark, DEFAULT_CLASSIC_SKIN);
const admin = getIsolatedAdminAntTheme(dark, DEFAULT_CLASSIC_SKIN);
expect(product.components?.Button?.colorPrimary).toBe(dark ? "#f5f5f5" : "#171717");
expect(admin.components?.Button?.colorPrimary).toBe(product.components?.Button?.colorPrimary);
expect(admin.components?.Button?.primaryColor).toBe(product.components?.Button?.primaryColor);
});
test(`custom primary action states reach product and admin in ${dark ? "dark" : "light"} mode`, () => {
const skin = duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"]);
Object.assign(skin.tokens[dark ? "dark" : "light"], {
primary: "#123456", primaryHover: "#234567", primaryActive: "#345678", primaryForeground: "#abcdef",
});
for (const theme of [getAntThemeConfig(dark, skin), getIsolatedAdminAntTheme(dark, skin)]) {
expect(theme.components?.Button).toMatchObject({
colorPrimary: "#123456", colorPrimaryHover: "#234567", colorPrimaryActive: "#345678", primaryColor: "#abcdef",
});
expect(theme.token?.controlOutlineWidth).toBe(0);
expect(theme.components?.Input?.activeShadow).toBe("none");
expect(theme.components?.InputNumber?.activeShadow).toBe("none");
expect(theme.components?.DatePicker?.activeShadow).toBe("none");
expect(theme.components?.Select?.activeOutlineColor).toBe("transparent");
}
});
}
test("workspace density overrides inherit action colors and shadow-free focus", () => {
const theme = getWorkspaceAntThemeConfig();
expect(theme.components?.Button?.colorPrimary).toBeUndefined();
expect(theme.components?.Button?.primaryColor).toBeUndefined();
expect(theme.components?.Input?.activeShadow).toBeUndefined();
});
});

View File

@@ -4,7 +4,7 @@ import { applySkinTheme, DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition, getSkinA
import { normalizePublicAppearance } from "../src/stores/use-appearance-store";
describe("site appearance and editable skin library", () => {
test("classic is immutable and keeps the existing runtime token system unchanged", () => {
test("classic preserves foundation tokens and applies only primary button fills", () => {
expect(DEFAULT_CLASSIC_SKIN.locked).toBe(true);
expect(getSkinAntOverrides(DEFAULT_CLASSIC_SKIN, "light")).toEqual({});
expect(getSkinAntOverrides(DEFAULT_CLASSIC_SKIN, "dark")).toEqual({});
@@ -22,7 +22,9 @@ describe("site appearance and editable skin library", () => {
} as unknown as Document;
applySkinTheme(DEFAULT_CLASSIC_SKIN, "light", target);
expect(removed.length).toBeGreaterThan(60);
expect(assigned.size).toBe(0);
expect(assigned.size).toBe(4);
expect(assigned.get("--button-primary-bg")).toBe("linear-gradient(115deg, #6554df, #386fbc)");
expect(assigned.has("--background")).toBe(false);
expect(target.documentElement.dataset.skin).toBe("classic");
});
@@ -142,7 +144,7 @@ describe("site appearance and editable skin library", () => {
expect(storeSource).toContain('setMeta(targetDocument, "property", "og:title"');
expect(footerSource).toContain("https://beian.miit.gov.cn/");
expect(footerSource).toContain('rel="noopener noreferrer"');
expect(pageSource).toContain('title="5. 皮肤主题"');
expect(pageSource).toContain('title="皮肤主题"');
expect(editorSource).toContain("从默认新建");
expect(editorSource).toContain("复制当前");
expect(editorSource).toContain("删除这套主题");

View File

@@ -0,0 +1,55 @@
import { describe, expect, test } from "bun:test";
import { applySkinTheme, DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition, getSkinButtonAppearance, isSkinButtonFill, normalizeSkinDefinition } from "../src/lib/skin-themes";
describe("theme primary button fills", () => {
test("classic uses the reference gradient in both modes without changing selection colors", () => {
for (const mode of ["light", "dark"] as const) {
expect(getSkinButtonAppearance(DEFAULT_CLASSIC_SKIN, mode)).toEqual({
background: "linear-gradient(115deg, #6554df, #386fbc)",
hover: "linear-gradient(115deg, #5744cf, #356bbb)",
active: "linear-gradient(115deg, #4938b8, #2c5da5)", foreground: "#ffffff",
});
}
expect(DEFAULT_CLASSIC_SKIN.tokens.light.selected).toBe("#e8e8e8");
});
test("copies have independent fills and switching to solid clears every gradient state", () => {
const skin = duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"]);
skin.tokens.buttons.light.angle = 45;
skin.tokens.buttons.light.start = "#123456";
expect(DEFAULT_CLASSIC_SKIN.tokens.buttons.light.angle).toBe(115);
expect(skin.tokens.buttons.dark.angle).toBe(115);
expect(getSkinButtonAppearance(skin, "light").background).toBe("linear-gradient(45deg, #123456, #386fbc)");
const values = new Map<string, string>();
const doc = { documentElement: { dataset: {}, style: {
removeProperty: (key: string) => values.delete(key), setProperty: (key: string, value: string) => values.set(key, value),
} } } as unknown as Document;
applySkinTheme(skin, "light", doc);
skin.tokens.buttons.light.mode = "solid";
Object.assign(skin.tokens.light, { primary: "#123456", primaryHover: "#234567", primaryActive: "#345678", primaryForeground: "#ffffff" });
applySkinTheme(skin, "light", doc);
expect(values.get("--button-primary-bg")).toBe("#123456");
expect(values.get("--button-primary-hover-bg")).toBe("#234567");
expect(values.get("--button-primary-active-bg")).toBe("#345678");
applySkinTheme(DEFAULT_CLASSIC_SKIN, "dark", doc);
expect(values.has("--background")).toBe(false);
expect(values.get("--button-primary-fg")).toBe("#ffffff");
});
test("legacy custom themes retain solid actions; legacy classic gains its default gradient", () => {
for (const original of [DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"])]) {
const legacy = JSON.parse(JSON.stringify(original));
delete legacy.tokens.buttons;
expect(normalizeSkinDefinition(legacy).tokens.buttons.light.mode).toBe(original.id === "classic" ? "gradient" : "solid");
}
});
test("invalid modes, angles and CSS injection cannot become runtime gradients", () => {
for (const patch of [{ mode: "url(x)" }, { angle: 361 }, { angle: -1 }, { angle: 1.5 }, { angle: NaN }, { start: "red; background:url(x)" }, { foreground: "white" }]) {
const skin = duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"]);
Object.assign(skin.tokens.buttons.light, patch);
expect(isSkinButtonFill(skin.tokens.buttons.light)).toBe(false);
expect(getSkinButtonAppearance(normalizeSkinDefinition(skin), "light").background).toBe(skin.tokens.light.primary);
}
});
});