完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AdminField, AdminSettingsActions, AdminSettingsGroup, AdminSettingsRow } from "@/components/admin";
|
||||
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
|
||||
import { AdminField, AdminSettingsActions, AdminSettingsGroup } from "@/components/admin";
|
||||
import { apiUpdateSiteSettings, apiOperations, type PublicSettings } from "@/lib/api";
|
||||
import { BRAND_LIMITS, runeCount, siteDocumentTitle } from "@/lib/brand";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
@@ -17,6 +17,12 @@ export default function BasicIdentityPanel({
|
||||
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 [desc, setDesc] = useState(initial.site_description);
|
||||
@@ -48,7 +54,10 @@ export default function BasicIdentityPanel({
|
||||
const preview = siteDocumentTitle(name, slogan);
|
||||
|
||||
const addKeyword = (raw: string) => {
|
||||
const parts = raw.split(/[,,;;]/).map((s) => s.trim()).filter(Boolean);
|
||||
const parts = raw
|
||||
.split(/[,,;;]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0) return;
|
||||
setSaveError("");
|
||||
setKeywords((prev) => {
|
||||
@@ -121,9 +130,11 @@ export default function BasicIdentityPanel({
|
||||
setWordmark((v) => (v.trim() === snap.wordmark ? res.site_wordmark : v));
|
||||
setSlogan((v) => (v.trim() === snap.slogan ? res.site_slogan : v));
|
||||
setDesc((v) => (v.trim() === snap.desc ? res.site_description : v));
|
||||
setKeywords((prev) => (JSON.stringify(prev) === JSON.stringify(snap.keywords) ? res.site_keywords : prev));
|
||||
setKeywords((prev) =>
|
||||
JSON.stringify(prev) === JSON.stringify(snap.keywords) ? res.site_keywords : prev,
|
||||
);
|
||||
onSaved?.(res);
|
||||
toast("基础资料已保存", "ok");
|
||||
toast("已保存", "ok");
|
||||
} catch (e) {
|
||||
if (ticket !== saveGen.current) return;
|
||||
const msg = e instanceof Error ? e.message : "保存失败";
|
||||
@@ -134,145 +145,147 @@ export default function BasicIdentityPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
if (saveError) setSaveError("");
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminSettingsGroup
|
||||
title="基础资料"
|
||||
description="用于页眉、浏览器标题、公开页摘要与搜索关键词。保存后对访客生效。"
|
||||
id="settings-identity"
|
||||
title="站点身份"
|
||||
footer={
|
||||
<AdminSettingsActions
|
||||
dirty={dirty}
|
||||
saving={saving}
|
||||
saveLabel="保存基础资料"
|
||||
saveLabel="保存"
|
||||
error={saveError}
|
||||
onSave={() => void save()}
|
||||
onDiscard={discard}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AdminSettingsRow label="显示名称" htmlFor="site-name" hintBelow="用于版权行与浏览器标题">
|
||||
<AdminField
|
||||
id="site-name"
|
||||
label="显示名称"
|
||||
embed
|
||||
className="admin-settings-control-text"
|
||||
maxLength={32}
|
||||
value={name}
|
||||
onChange={(v) => {
|
||||
setName(v);
|
||||
if (nameError) setNameError("");
|
||||
if (saveError) setSaveError("");
|
||||
}}
|
||||
placeholder="姜十三论坛"
|
||||
error={nameError}
|
||||
/>
|
||||
</AdminSettingsRow>
|
||||
<AdminSettingsRow
|
||||
label="字标"
|
||||
htmlFor="site-wordmark"
|
||||
hintBelow="无 Logo 或纯文字页眉时使用;留空则使用显示名称"
|
||||
>
|
||||
<AdminField
|
||||
id="site-wordmark"
|
||||
label="字标"
|
||||
embed
|
||||
className="admin-settings-control-text"
|
||||
maxLength={BRAND_LIMITS.wordmark}
|
||||
value={wordmark}
|
||||
onChange={(v) => {
|
||||
setWordmark(v);
|
||||
if (saveError) setSaveError("");
|
||||
}}
|
||||
placeholder="姜十三"
|
||||
/>
|
||||
</AdminSettingsRow>
|
||||
<AdminSettingsRow
|
||||
label="标语"
|
||||
htmlFor="site-slogan"
|
||||
hintBelow={
|
||||
<>
|
||||
出现在浏览器标题和页眉副标题。预览:
|
||||
<span style={{ color: "var(--ink)" }}>{preview}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="admin-identity-studio">
|
||||
<div className="admin-identity-url" role="group" aria-label="正式站点地址">
|
||||
<span className="admin-identity-url-label">正式地址</span>
|
||||
<code className="admin-identity-url-value">{siteURL || "尚未部署 SITE_URL"}</code>
|
||||
</div>
|
||||
|
||||
<div className="admin-identity-grid">
|
||||
<AdminField
|
||||
id="site-name"
|
||||
label="显示名称"
|
||||
className="admin-identity-field"
|
||||
maxLength={32}
|
||||
value={name}
|
||||
onChange={(v) => {
|
||||
setName(v);
|
||||
if (nameError) setNameError("");
|
||||
clearSaveError();
|
||||
}}
|
||||
placeholder="姜十三论坛"
|
||||
error={nameError}
|
||||
/>
|
||||
<AdminField
|
||||
id="site-wordmark"
|
||||
label="字标"
|
||||
className="admin-identity-field"
|
||||
maxLength={BRAND_LIMITS.wordmark}
|
||||
value={wordmark}
|
||||
onChange={(v) => {
|
||||
setWordmark(v);
|
||||
clearSaveError();
|
||||
}}
|
||||
placeholder="姜十三"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminField
|
||||
id="site-slogan"
|
||||
label="标语"
|
||||
embed
|
||||
className="admin-settings-control-text"
|
||||
className="admin-identity-field"
|
||||
maxLength={BRAND_LIMITS.slogan}
|
||||
value={slogan}
|
||||
onChange={(v) => {
|
||||
setSlogan(v);
|
||||
if (saveError) setSaveError("");
|
||||
clearSaveError();
|
||||
}}
|
||||
placeholder="技术分享与讨论"
|
||||
/>
|
||||
</AdminSettingsRow>
|
||||
<AdminSettingsRow label="简介" htmlFor="site-desc" hintBelow="用于页面摘要与搜索描述" wide>
|
||||
<p className="admin-identity-title-preview" aria-live="polite">
|
||||
浏览器标题预览 · <strong>{preview}</strong>
|
||||
</p>
|
||||
|
||||
<AdminField
|
||||
id="site-desc"
|
||||
label="简介"
|
||||
embed
|
||||
as="textarea"
|
||||
className="admin-settings-control-long"
|
||||
minHeight="96px"
|
||||
className="admin-identity-field"
|
||||
minHeight="72px"
|
||||
maxLength={160}
|
||||
value={desc}
|
||||
onChange={(v) => {
|
||||
setDesc(v);
|
||||
if (saveError) setSaveError("");
|
||||
clearSaveError();
|
||||
}}
|
||||
placeholder="一句话介绍社区定位…"
|
||||
placeholder="一句话介绍社区…"
|
||||
/>
|
||||
</AdminSettingsRow>
|
||||
<AdminSettingsRow
|
||||
label="SEO 关键词"
|
||||
htmlFor="seo-keyword-input"
|
||||
hintBelow={`用于搜索结果。最多 ${BRAND_LIMITS.keywordCount} 个,每个 ${BRAND_LIMITS.keywordRunes} 字。点击已添加的词可删除。`}
|
||||
wide
|
||||
>
|
||||
<div className="admin-settings-control-long">
|
||||
<div className="admin-settings-keywords">
|
||||
{keywords.map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
className="admin-settings-kw"
|
||||
onClick={() => {
|
||||
setKeywords((prev) => prev.filter((x) => x !== k));
|
||||
if (saveError) setSaveError("");
|
||||
}}
|
||||
>
|
||||
{k}
|
||||
<X size={12} />
|
||||
</button>
|
||||
))}
|
||||
{keywords.length === 0 ? <span className="meta text-[12.5px]">尚未添加</span> : null}
|
||||
|
||||
<div className="admin-identity-keywords">
|
||||
<div className="admin-identity-keywords-head">
|
||||
<label htmlFor="seo-keyword-input" className="field-label">
|
||||
关键词
|
||||
</label>
|
||||
<span className="meta text-[12px]">
|
||||
{keywords.length}/{BRAND_LIMITS.keywordCount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="admin-field-shell flex-1 min-w-0">
|
||||
<input
|
||||
id="seo-keyword-input"
|
||||
value={kwInput}
|
||||
onChange={(e) => 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="添加关键词"
|
||||
/>
|
||||
<div className="admin-identity-keywords-box">
|
||||
{keywords.length > 0 ? (
|
||||
<div className="admin-settings-keywords">
|
||||
{keywords.map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
className="admin-settings-kw"
|
||||
onClick={() => {
|
||||
setKeywords((prev) => prev.filter((x) => x !== k));
|
||||
clearSaveError();
|
||||
}}
|
||||
>
|
||||
{k}
|
||||
<X size={12} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="admin-identity-keywords-add">
|
||||
<div className="admin-field-shell flex-1 min-w-0">
|
||||
<input
|
||||
id="seo-keyword-input"
|
||||
value={kwInput}
|
||||
onChange={(e) => 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="添加关键词"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm shrink-0"
|
||||
onClick={() => addKeyword(kwInput)}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="btn btn-line btn-sm shrink-0" onClick={() => addKeyword(kwInput)}>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AdminSettingsRow>
|
||||
</div>
|
||||
</AdminSettingsGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user