136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
"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>
|
||
);
|
||
}
|