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

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

View File

@@ -3,7 +3,7 @@ import { IconButton, SegmentedControl, Select, Tooltip } from "@/pages/admin/ui/
import { Check, Copy, LockKeyhole, Moon, Plus, Sparkles, Sun, Trash2 } from "lucide-react";
import { useMemo, useState } from "react";
import { SKIN_COLOR_GROUPS, SKIN_COMPONENT_NUMBER_FIELDS, skinSwatches, type SkinComponentTokens, type SkinDefinition, type SkinModeTokens, type SkinThemeMode } from "@/lib/skin-themes";
import { SKIN_COLOR_GROUPS, SKIN_COMPONENT_NUMBER_FIELDS, SKIN_BUTTON_COLOR_FIELDS, getSkinButtonAppearance, skinSwatches, type SkinButtonFill, type SkinComponentTokens, type SkinDefinition, type SkinModeTokens, type SkinThemeMode } from "@/lib/skin-themes";
import { cn } from "@/lib/utils";
import { AdminStatusBadge } from "@/pages/admin/components/admin-ui";
@@ -30,6 +30,10 @@ export function SkinThemeEditor({
const selected = useMemo(() => themes.find((theme) => theme.id === selectedID) || themes[0], [selectedID, themes]);
if (!selected) return null;
const editorDisabled = disabled || selected.locked;
const buttonFill = selected.tokens.buttons[mode];
const updateButton = (patch: Partial<SkinButtonFill>) => onChange({ ...selected, tokens: { ...selected.tokens,
buttons: { ...selected.tokens.buttons, [mode]: { ...buttonFill, ...patch } },
} });
const updateIdentity = (patch: Partial<Pick<SkinDefinition, "name" | "description">>) => onChange({ ...selected, ...patch });
const updateColor = (key: keyof SkinModeTokens, value: string) =>
@@ -115,7 +119,7 @@ export function SkinThemeEditor({
{selected.locked ? (
<div className="admin-skin-locked-note">
<LockKeyhole className="size-4" aria-hidden="true" />
<span>经典黑白始终保留项目原始视觉,不能改名、修改或删除。点击“从默认新建”即可复制全部参数后自由调整。</span>
<span>经典黑白采用黑白基础配色与紫蓝渐变主按钮,不能改名、修改或删除。点击“从默认新建”即可复制全部参数后自由调整。</span>
</div>
) : null}
@@ -164,6 +168,27 @@ export function SkinThemeEditor({
<SkinThemePreview theme={selected} mode={mode} />
<details className="admin-skin-token-group" open>
<summary><span>主按钮填充</span><small>{mode === "light" ? "浅色" : "深色"}模式</small></summary>
<div className="admin-skin-component-grid">
<label className="admin-skin-number-field">
<span>填充方式</span>
<Select value={buttonFill.mode} disabled={editorDisabled}
options={[{ label: "纯色(使用主操作颜色)", value: "solid" }, { label: "渐变", value: "gradient" }]}
onChange={(value) => updateButton({ mode: value })} />
</label>
{buttonFill.mode === "gradient" ? <label className="admin-skin-number-field">
<span>渐变角度</span>
<InputNumber aria-label="渐变角度" value={buttonFill.angle} min={0} max={360} precision={0} addonAfter="°" disabled={editorDisabled}
onChange={(value) => { if (typeof value === "number") updateButton({ angle: value }); }} />
</label> : null}
</div>
{buttonFill.mode === "gradient" ? <div className="admin-skin-color-grid">
{SKIN_BUTTON_COLOR_FIELDS.map(({ key, label }) => <ColorTokenField key={key} label={label} help="仅影响主按钮,不改变选中态、开关和危险操作" value={buttonFill[key]} disabled={editorDisabled}
onChange={(value) => updateButton({ [key]: value.trim().toLowerCase() })} />)}
</div> : null}
</details>
<div className="admin-skin-color-groups">
{SKIN_COLOR_GROUPS.map((group, index) => (
<details key={group.key} className="admin-skin-token-group" open={index < 2}>
@@ -240,6 +265,7 @@ function ColorTokenField({ label, help, value, disabled, onChange }: { label: st
function SkinThemePreview({ theme, mode }: { theme: SkinDefinition; mode: SkinThemeMode }) {
const color = theme.tokens[mode];
const button = getSkinButtonAppearance(theme, mode);
const component = theme.tokens.components;
const shadow = component.shadowStyle === "none" ? "none" : component.shadowStyle === "strong" ? "0 18px 44px #0000003d" : "0 10px 28px #00000024";
return (
@@ -264,7 +290,7 @@ function SkinThemePreview({ theme, mode }: { theme: SkinDefinition; mode: SkinTh
>
输入框状态
</span>
<span className="admin-skin-live-preview-button" style={{ height: component.controlHeight, background: color.primary, color: color.primaryForeground, borderRadius: component.buttonRadius, fontWeight: component.buttonFontWeight }}>
<span className="admin-skin-live-preview-button" style={{ height: component.controlHeight, background: button.background, color: button.foreground, borderRadius: component.buttonRadius, fontWeight: component.buttonFontWeight }}>
主要操作
</span>
<span className="admin-skin-live-preview-switches" aria-label="开关开启与关闭状态预览">