"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Palette, Check, Loader2, Save, Undo2, RotateCcw, Pipette, } from "lucide-react"; import { AdminPageHeader, AdminSection } from "@/components/admin"; 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({ accent: 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 ( ); })}
自定义颜色 } description="取色器选色或输入 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 && (

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

)}
未选中 选中标签 导航选中态
选中的板块卡片

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

{dirty && ( )}
); }