Files
jiang13-bbs/frontend/app/admin/settings/BasicIdentityPanel.tsx
2026-09-19 04:14:13 +08:00

136 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { Loader2, Save } from "lucide-react";
import { useState } from "react";
import { AdminField, AdminSection } from "@/components/admin";
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
import { BRAND_LIMITS, siteDocumentTitle } from "@/lib/brand";
import { toast } from "@/lib/toast";
export default function BasicIdentityPanel({
initial,
onSaved,
}: {
initial: PublicSettings;
onSaved?: (next: PublicSettings) => void;
}) {
const [name, setName] = useState(initial.site_name);
const [wordmark, setWordmark] = useState(initial.site_wordmark);
const [slogan, setSlogan] = useState(initial.site_slogan);
const [desc, setDesc] = useState(initial.site_description);
const [saved, setSaved] = useState({
name: initial.site_name,
wordmark: initial.site_wordmark,
slogan: initial.site_slogan,
desc: initial.site_description,
});
const [saving, setSaving] = useState(false);
const dirty =
name.trim() !== saved.name ||
wordmark.trim() !== saved.wordmark ||
slogan.trim() !== saved.slogan ||
desc.trim() !== saved.desc;
const preview = siteDocumentTitle(name, slogan);
const save = async () => {
if (saving) return;
const nextName = name.trim();
if (!nextName) {
toast("显示名称不能为空");
return;
}
setSaving(true);
try {
const res = await apiUpdateSiteSettings({
site_name: nextName,
site_wordmark: wordmark.trim(),
site_slogan: slogan.trim(),
site_description: desc.trim(),
});
setName(res.site_name);
setWordmark(res.site_wordmark);
setSlogan(res.site_slogan);
setDesc(res.site_description);
setSaved({
name: res.site_name,
wordmark: res.site_wordmark,
slogan: res.site_slogan,
desc: res.site_description,
});
onSaved?.(res);
toast("基本信息已保存", "ok");
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setSaving(false);
}
};
return (
<div className="admin-settings-grid">
<AdminSection title="名称与字标" description="显示名称用于版权与标题;无 Logo 时页眉改用字标">
<div className="flex flex-col gap-4">
<AdminField
id="site-name"
label="显示名称"
maxLength={32}
value={name}
onChange={setName}
placeholder="姜十三论坛"
/>
<AdminField
id="site-wordmark"
label="字标"
hint="留空则回退为显示名称,最多 16 字"
maxLength={BRAND_LIMITS.wordmark}
value={wordmark}
onChange={setWordmark}
placeholder="姜十三"
/>
</div>
</AdminSection>
<AdminSection
title="标语与简介"
description="标语出现在浏览器标题和页眉副标题"
extra={
<button
type="button"
className="btn btn-primary btn-sm"
disabled={!dirty || saving}
onClick={() => void save()}
>
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
保存
</button>
}
>
<div className="flex flex-col gap-4">
<AdminField
id="site-slogan"
label="标语"
maxLength={BRAND_LIMITS.slogan}
value={slogan}
onChange={setSlogan}
placeholder="技术分享与讨论"
/>
<p className="meta text-[12.5px]">
标题预览:<span style={{ color: "var(--ink)" }}>{preview}</span>
</p>
<AdminField
id="site-desc"
label="简介"
as="textarea"
minHeight="96px"
maxLength={160}
value={desc}
onChange={setDesc}
placeholder="一句话介绍社区定位…"
/>
</div>
</AdminSection>
</div>
);
}