"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Palette, Check, Loader2, Save, Undo2, RotateCcw, ChevronRight, Pipette, } from "lucide-react"; import { apiUpdateSiteSettings } from "@/lib/api"; import { toast } from "@/lib/toast"; import { ACCENT_PRESETS, DEFAULT_ACCENT, applyAccentTheme, deriveAccentPair, findPreset, normalizeHex, } from "@/lib/theme"; // 后台「外观设置」:站点主题色预设 + 自定义取色 + 实时预览。 // 修改时立即对整页实时预览(仅本地),保存后才写库并对所有用户生效。 export default function AppearanceAdmin({ initialAccent }: { initialAccent: string }) { const router = useRouter(); // 已保存值:空串=内置默认(靛蓝) const [savedHex, setSavedHex] = useState(initialAccent || ""); const [draft, setDraft] = useState(initialAccent || DEFAULT_ACCENT); const [saving, setSaving] = useState(false); const savedRef = useRef(savedHex); savedRef.current = savedHex; const normalized = useMemo(() => normalizeHex(draft), [draft]); const pair = useMemo( () => (normalized ? deriveAccentPair(normalized) : null), [normalized] ); const preset = useMemo(() => (normalized ? findPreset(normalized) : null), [normalized]); // 保存提交值:默认靛蓝落库为空串(前端不注入覆盖,保持内置令牌单一来源) const submitHex = normalized && normalized !== DEFAULT_ACCENT ? normalized : ""; const dirty = (savedHex || DEFAULT_ACCENT) !== (normalized || ""); const invalid = draft.trim() !== "" && !normalized; // 实时预览:合法颜色即应用;离开页面时还原为已保存值,避免未保存预览残留 useEffect(() => { if (normalized) applyAccentTheme(normalized); return () => { applyAccentTheme(savedRef.current || null); }; }, [normalized]); const handleSave = async () => { if (!normalized || saving) return; setSaving(true); try { const res = await apiUpdateSiteSettings(submitHex); const next = (res.accent || "").toLowerCase(); setSavedHex(next); applyAccentTheme(next || null); router.refresh(); toast(next ? "主题色已保存并对全站生效" : "已恢复默认主题色"); } catch (e) { toast(e instanceof Error ? e.message : "保存失败", "error"); applyAccentTheme(savedRef.current || null); } finally { setSaving(false); } }; const discard = () => { setDraft(savedHex || DEFAULT_ACCENT); applyAccentTheme(savedHex || null); }; return (
{/* 面包屑 */}

外观设置

配置站点主题色,全站按钮、链接、选中态会统一联动

{/* 预设色板 */}

预设主题色

浅/暗两套色板均已调校,保证文字对比度达标

{ACCENT_PRESETS.map((p) => { const active = normalized === p.light; const isDefault = p.light === DEFAULT_ACCENT; return ( ); })}
{/* 自定义颜色 */}

自定义颜色

取色器选色或输入 HEX 值;浅色下对比度过低时会自动校正,保证按钮与文字可读

{/* 原生取色器 */} {/* HEX 文本输入 */}
# setDraft("#" + e.target.value.replace(/[^0-9a-fA-F#]/g, ""))} className="flex-1 min-w-0 bg-transparent outline-none ml-1 uppercase" style={{ color: "var(--ink)" }} maxLength={7} spellCheck={false} aria-label="主题色 HEX 值" />
{pair && (
浅色 {pair.light} 暗色 {pair.dark}
)}
{invalid && (

颜色格式无效,请输入 6 位 HEX 值(如 3B5BDB)

)} {pair?.adjusted && (

该颜色与白底对比度不足,浅色模式已自动校正为 {pair.light}(暗色模式同步派生 {pair.dark})

)} {preset && preset.light === DEFAULT_ACCENT && (

当前为内置默认色,保存时会清除自定义配置

)}
{/* 实时预览(整页已实时换肤,此处展示典型组件) */}

实时预览

e.preventDefault()}> 文字链接示例
未选中 选中标签 导航选中态
选中的板块卡片
{/* 操作栏 */}

{dirty ? "有未保存的改动,整页正处于实时预览状态" : "设置已是保存状态"}

{dirty && ( )}
); }