"use client"; import { X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { AdminField, AdminSettingsActions, AdminSettingsGroup, AdminSwitch } from "@/components/admin"; import { apiUpdateSiteSettings, apiOperations, type PublicSettings } from "@/lib/api"; import { BRAND_LIMITS, runeCount, siteDocumentTitle } from "@/lib/brand"; import { toast } from "@/lib/toast"; export default function BasicIdentityPanel({ initial, onSaved, onDirtyChange, }: { initial: PublicSettings; onSaved?: (next: PublicSettings) => void; onDirtyChange?: (dirty: boolean) => void; }) { const [name, setName] = useState(initial.site_name); const [siteURL, setSiteURL] = useState(""); useEffect(() => { apiOperations<{ site_url: string }>("/api/site-state") .then((s) => setSiteURL(s.site_url)) .catch(() => {}); }, []); const [wordmark, setWordmark] = useState(initial.site_wordmark); const [slogan, setSlogan] = useState(initial.site_slogan); const [sloganVisible, setSloganVisible] = useState(initial.brand_slogan_visible); const [desc, setDesc] = useState(initial.site_description); const [keywords, setKeywords] = useState(initial.site_keywords); const [kwInput, setKwInput] = useState(""); const [saved, setSaved] = useState({ name: initial.site_name, wordmark: initial.site_wordmark, slogan: initial.site_slogan, sloganVisible: initial.brand_slogan_visible, desc: initial.site_description, keywords: initial.site_keywords, }); const [saving, setSaving] = useState(false); const [nameError, setNameError] = useState(""); const [saveError, setSaveError] = useState(""); const saveGen = useRef(0); const dirty = name.trim() !== saved.name || wordmark.trim() !== saved.wordmark || slogan.trim() !== saved.slogan || sloganVisible !== saved.sloganVisible || desc.trim() !== saved.desc || JSON.stringify(keywords) !== JSON.stringify(saved.keywords); useEffect(() => { onDirtyChange?.(dirty); }, [dirty, onDirtyChange]); const preview = siteDocumentTitle(name, slogan); const addKeyword = (raw: string) => { const parts = raw .split(/[,,;;]/) .map((s) => s.trim()) .filter(Boolean); if (parts.length === 0) return; setSaveError(""); setKeywords((prev) => { const next = [...prev]; const seen = new Set(next.map((k) => k.toLowerCase())); for (const k of parts) { if (runeCount(k) > BRAND_LIMITS.keywordRunes) { toast(`关键词「${k}」超过 ${BRAND_LIMITS.keywordRunes} 字`); continue; } if (seen.has(k.toLowerCase())) continue; if (next.length >= BRAND_LIMITS.keywordCount) { toast(`最多 ${BRAND_LIMITS.keywordCount} 个关键词`); break; } seen.add(k.toLowerCase()); next.push(k); } return next; }); setKwInput(""); }; const discard = () => { setName(saved.name); setWordmark(saved.wordmark); setSlogan(saved.slogan); setSloganVisible(saved.sloganVisible); setDesc(saved.desc); setKeywords([...saved.keywords]); setKwInput(""); setNameError(""); setSaveError(""); }; const save = async () => { if (saving) return; const snap = { name: name.trim(), wordmark: wordmark.trim(), slogan: slogan.trim(), sloganVisible, desc: desc.trim(), keywords: [...keywords], }; if (!snap.name) { setNameError("显示名称不能为空"); toast("显示名称不能为空"); return; } setNameError(""); setSaveError(""); const ticket = ++saveGen.current; setSaving(true); try { const res = await apiUpdateSiteSettings({ site_name: snap.name, site_wordmark: snap.wordmark, site_slogan: snap.slogan, brand_slogan_visible: snap.sloganVisible, site_description: snap.desc, site_keywords: snap.keywords, }); if (ticket !== saveGen.current) return; setSaved({ name: res.site_name, wordmark: res.site_wordmark, slogan: res.site_slogan, sloganVisible: res.brand_slogan_visible, desc: res.site_description, keywords: res.site_keywords, }); setName((v) => (v.trim() === snap.name ? res.site_name : v)); setWordmark((v) => (v.trim() === snap.wordmark ? res.site_wordmark : v)); setSlogan((v) => (v.trim() === snap.slogan ? res.site_slogan : v)); setSloganVisible((v) => (v === snap.sloganVisible ? res.brand_slogan_visible : v)); setDesc((v) => (v.trim() === snap.desc ? res.site_description : v)); setKeywords((prev) => JSON.stringify(prev) === JSON.stringify(snap.keywords) ? res.site_keywords : prev, ); onSaved?.(res); toast("已保存", "ok"); } catch (e) { if (ticket !== saveGen.current) return; const msg = e instanceof Error ? e.message : "保存失败"; setSaveError(msg); toast(msg); } finally { if (ticket === saveGen.current) setSaving(false); } }; const clearSaveError = () => { if (saveError) setSaveError(""); }; return ( void save()} onDiscard={discard} /> } >
正式地址 {siteURL || "尚未部署 SITE_URL"}
{ setName(v); if (nameError) setNameError(""); clearSaveError(); }} placeholder="姜十三论坛" error={nameError} /> { setWordmark(v); clearSaveError(); }} placeholder="姜十三" />
{slogan.length}/{BRAND_LIMITS.slogan}
{ setSloganVisible((v) => !v); clearSaveError(); }} label="页眉显示标语" /> 页眉显示标语
{ setSlogan(e.target.value); clearSaveError(); }} placeholder="技术分享与讨论" />

浏览器标题预览 · {preview}

{ setDesc(v); clearSaveError(); }} placeholder="一句话介绍社区…" />
{keywords.length}/{BRAND_LIMITS.keywordCount}
{keywords.length > 0 ? (
{keywords.map((k) => ( ))}
) : null}
setKwInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === "," || e.key === ",") { e.preventDefault(); addKeyword(kwInput); } }} placeholder="回车添加" className="admin-field-input" aria-label="添加关键词" />
); }