feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,30 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import { Globe, Loader2, MessageSquare, Save, UserPlus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Globe,
|
||||
Loader2,
|
||||
MessageSquare,
|
||||
Save,
|
||||
UserPlus,
|
||||
ExternalLink,
|
||||
Shield,
|
||||
Mail,
|
||||
HardDrive,
|
||||
Ban,
|
||||
Wrench,
|
||||
Palette,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import {
|
||||
AdminField,
|
||||
AdminPageHeader,
|
||||
AdminSection,
|
||||
AdminSettingRow,
|
||||
AdminSwitch,
|
||||
} from "@/components/admin";
|
||||
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
type SectionId =
|
||||
| "basic"
|
||||
| "content"
|
||||
| "brand"
|
||||
| "footer"
|
||||
| "security"
|
||||
| "mail"
|
||||
| "storage"
|
||||
| "words"
|
||||
| "maintenance";
|
||||
|
||||
const NAV: { id: SectionId; label: string; ready?: boolean }[] = [
|
||||
{ id: "basic", label: "基本信息", ready: true },
|
||||
{ id: "content", label: "内容与互动", ready: true },
|
||||
{ id: "brand", label: "品牌与 SEO" },
|
||||
{ id: "footer", label: "页脚与合规" },
|
||||
{ id: "security", label: "安全与限流" },
|
||||
{ id: "mail", label: "邮件" },
|
||||
{ id: "storage", label: "对象存储" },
|
||||
{ id: "words", label: "敏感词" },
|
||||
{ id: "maintenance", label: "系统维护" },
|
||||
];
|
||||
|
||||
function SwitchRow({
|
||||
title,
|
||||
description,
|
||||
control,
|
||||
bordered,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
description: string;
|
||||
control: ReactNode;
|
||||
bordered?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-start justify-between gap-4 flex-wrap py-4 first:pt-0 last:pb-0"
|
||||
style={bordered ? { borderBottom: "1px solid var(--line)" } : undefined}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[14.5px] font-medium flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
|
||||
{title}
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-1 leading-relaxed">{description}</p>
|
||||
</div>
|
||||
<div className="shrink-0">{control}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComingSoonCard({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AdminSection title={title} description={description}>
|
||||
<div
|
||||
className="rounded-[var(--r-sm)] px-4 py-10 text-center"
|
||||
style={{ background: "var(--panel-2)", color: "var(--ink-3)" }}
|
||||
>
|
||||
<span className="inline-flex mb-2 opacity-80" aria-hidden>
|
||||
{icon}
|
||||
</span>
|
||||
<p className="text-[14px]" style={{ color: "var(--ink-2)" }}>
|
||||
即将推出
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-1">此分区尚未开放配置</p>
|
||||
</div>
|
||||
</AdminSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsAdmin({ initial }: { initial: PublicSettings }) {
|
||||
const [siteName, setSiteName] = useState(initial.site_name);
|
||||
const [siteDescription, setSiteDescription] = useState(initial.site_description);
|
||||
const [allowRegister, setAllowRegister] = useState(initial.allow_register);
|
||||
const [allowComments, setAllowComments] = useState(initial.allow_comments !== false);
|
||||
const [postLinkNewTab, setPostLinkNewTab] = useState(initial.post_link_new_tab !== false);
|
||||
const [savedName, setSavedName] = useState(initial.site_name);
|
||||
const [savedDesc, setSavedDesc] = useState(initial.site_description);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [togglingReg, setTogglingReg] = useState(false);
|
||||
const [togglingComments, setTogglingComments] = useState(false);
|
||||
const [togglingLink, setTogglingLink] = useState(false);
|
||||
const [active, setActive] = useState<SectionId>("basic");
|
||||
|
||||
const dirty = siteName.trim() !== savedName || siteDescription.trim() !== savedDesc;
|
||||
|
||||
useEffect(() => {
|
||||
const ids = NAV.map((n) => n.id);
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||
if (visible[0]?.target?.id) {
|
||||
const id = visible[0].target.id.replace(/^settings-/, "") as SectionId;
|
||||
if (ids.includes(id)) setActive(id);
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px", threshold: [0, 0.25, 0.5] }
|
||||
);
|
||||
ids.forEach((id) => {
|
||||
const el = document.getElementById(`settings-${id}`);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const scrollTo = (id: SectionId) => {
|
||||
setActive(id);
|
||||
document.getElementById(`settings-${id}`)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
};
|
||||
|
||||
const saveIdentity = async () => {
|
||||
if (saving) return;
|
||||
const name = siteName.trim();
|
||||
@@ -80,84 +199,196 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
|
||||
}
|
||||
};
|
||||
|
||||
const togglePostLinkNewTab = async () => {
|
||||
if (togglingLink) return;
|
||||
const next = !postLinkNewTab;
|
||||
setTogglingLink(true);
|
||||
try {
|
||||
const res = await apiUpdateSiteSettings({ post_link_new_tab: next });
|
||||
setPostLinkNewTab(res.post_link_new_tab !== false);
|
||||
toast(res.post_link_new_tab !== false ? "帖子与评论外链将在新标签打开" : "帖子与评论外链将在当前页打开", "ok");
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "保存失败");
|
||||
} finally {
|
||||
setTogglingLink(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div>
|
||||
<AdminPageHeader
|
||||
icon={Globe}
|
||||
title="站点设置"
|
||||
description="配置站点名称、简介与注册/评论开关;主题色、代码折叠与动画请到「外观」页调整"
|
||||
description="配置站点名称、互动开关与后续扩展分区;主题色请到「外观」页调整"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<AdminSection title="基本信息" description="用于页眉品牌、页脚版权与 SEO 元信息">
|
||||
<AdminField
|
||||
id="site-name"
|
||||
className="mb-4"
|
||||
label="站点名称"
|
||||
maxLength={32}
|
||||
value={siteName}
|
||||
onChange={setSiteName}
|
||||
placeholder="姜十三论坛"
|
||||
/>
|
||||
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start">
|
||||
<nav
|
||||
className="w-full lg:w-52 shrink-0 lg:sticky lg:top-4 flex lg:flex-col gap-1 overflow-x-auto lg:overflow-visible pb-1 lg:pb-0"
|
||||
aria-label="设置分区"
|
||||
>
|
||||
{NAV.map((item) => {
|
||||
const isActive = active === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => scrollTo(item.id)}
|
||||
className="shrink-0 text-left px-3 py-2 rounded-[var(--r-sm)] text-[13.5px] transition-colors whitespace-nowrap"
|
||||
style={{
|
||||
background: isActive ? "var(--accent-soft)" : "transparent",
|
||||
color: isActive ? "var(--accent)" : "var(--ink-2)",
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
}}
|
||||
aria-current={isActive ? "true" : undefined}
|
||||
>
|
||||
{item.label}
|
||||
{!item.ready && (
|
||||
<span className="ml-1.5 text-[10.5px]" style={{ color: "var(--ink-3)" }}>
|
||||
即将
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<AdminField
|
||||
id="site-desc"
|
||||
className="mb-4"
|
||||
label="站点简介"
|
||||
as="textarea"
|
||||
minHeight="96px"
|
||||
maxLength={160}
|
||||
value={siteDescription}
|
||||
onChange={setSiteDescription}
|
||||
placeholder="一句话介绍社区定位…"
|
||||
/>
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-4 w-full">
|
||||
<div id="settings-basic" className="scroll-mt-4">
|
||||
<AdminSection title="基本信息" description="用于页眉品牌、页脚版权与 SEO 元信息">
|
||||
<AdminField
|
||||
id="site-name"
|
||||
className="mb-4"
|
||||
label="站点名称"
|
||||
maxLength={32}
|
||||
value={siteName}
|
||||
onChange={setSiteName}
|
||||
placeholder="姜十三论坛"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={!dirty || saving}
|
||||
onClick={() => void saveIdentity()}
|
||||
>
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
保存基本信息
|
||||
</button>
|
||||
<AdminField
|
||||
id="site-desc"
|
||||
className="mb-4"
|
||||
label="站点简介"
|
||||
as="textarea"
|
||||
minHeight="96px"
|
||||
maxLength={160}
|
||||
value={siteDescription}
|
||||
onChange={setSiteDescription}
|
||||
placeholder="一句话介绍社区定位…"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={!dirty || saving}
|
||||
onClick={() => void saveIdentity()}
|
||||
>
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
保存基本信息
|
||||
</button>
|
||||
</div>
|
||||
</AdminSection>
|
||||
</div>
|
||||
</AdminSection>
|
||||
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<UserPlus size={15} /> 开放注册
|
||||
</>
|
||||
}
|
||||
description="关闭后,游客将无法提交注册;已有账号仍可登录。适用于内测或维护期。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={allowRegister}
|
||||
disabled={togglingReg}
|
||||
onChange={() => void toggleRegister()}
|
||||
label="开放注册"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div id="settings-content" className="scroll-mt-4">
|
||||
<AdminSection title="内容与互动" description="控制注册、评论与链接打开方式">
|
||||
<SwitchRow
|
||||
bordered
|
||||
title={
|
||||
<>
|
||||
<UserPlus size={15} /> 开放注册
|
||||
</>
|
||||
}
|
||||
description="关闭后,游客将无法提交注册;已有账号仍可登录。适用于内测或维护期。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={allowRegister}
|
||||
disabled={togglingReg}
|
||||
onChange={() => void toggleRegister()}
|
||||
label="开放注册"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SwitchRow
|
||||
bordered
|
||||
title={
|
||||
<>
|
||||
<MessageSquare size={15} /> 开放评论
|
||||
</>
|
||||
}
|
||||
description="关闭后前台不展示评论入口、评论数量与已有评论,并提示本站不支持评论。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={allowComments}
|
||||
disabled={togglingComments}
|
||||
onChange={() => void toggleComments()}
|
||||
label="开放评论"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SwitchRow
|
||||
title={
|
||||
<>
|
||||
<ExternalLink size={15} /> 外链新标签打开
|
||||
</>
|
||||
}
|
||||
description="开启后,帖子与评论中的 Markdown 外链在新标签打开;关闭则当前页跳转。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={postLinkNewTab}
|
||||
disabled={togglingLink}
|
||||
onChange={() => void togglePostLinkNewTab()}
|
||||
label="外链新标签打开"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</AdminSection>
|
||||
</div>
|
||||
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<MessageSquare size={15} /> 开放评论
|
||||
</>
|
||||
}
|
||||
description="关闭后前台不展示评论入口、评论数量与已有评论,并提示本站不支持评论。适用于备案审核等合规需要。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={allowComments}
|
||||
disabled={togglingComments}
|
||||
onChange={() => void toggleComments()}
|
||||
label="开放评论"
|
||||
<div id="settings-brand" className="scroll-mt-4">
|
||||
<ComingSoonCard
|
||||
title="品牌与 SEO"
|
||||
description="站点图标、OG 图与结构化元数据"
|
||||
icon={<Palette size={22} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div id="settings-footer" className="scroll-mt-4">
|
||||
<ComingSoonCard
|
||||
title="页脚与合规"
|
||||
description="备案号、版权声明与合规链接"
|
||||
icon={<FileText size={22} />}
|
||||
/>
|
||||
</div>
|
||||
<div id="settings-security" className="scroll-mt-4">
|
||||
<ComingSoonCard
|
||||
title="安全与限流"
|
||||
description="登录/注册速率与发帖冷静期等"
|
||||
icon={<Shield size={22} />}
|
||||
/>
|
||||
</div>
|
||||
<div id="settings-mail" className="scroll-mt-4">
|
||||
<ComingSoonCard title="邮件" description="SMTP 与通知邮件模板" icon={<Mail size={22} />} />
|
||||
</div>
|
||||
<div id="settings-storage" className="scroll-mt-4">
|
||||
<ComingSoonCard
|
||||
title="对象存储"
|
||||
description="附件与头像的云端存储配置"
|
||||
icon={<HardDrive size={22} />}
|
||||
/>
|
||||
</div>
|
||||
<div id="settings-words" className="scroll-mt-4">
|
||||
<ComingSoonCard title="敏感词" description="内容过滤词库与命中策略" icon={<Ban size={22} />} />
|
||||
</div>
|
||||
<div id="settings-maintenance" className="scroll-mt-4">
|
||||
<ComingSoonCard
|
||||
title="系统维护"
|
||||
description="维护模式、缓存与诊断工具"
|
||||
icon={<Wrench size={22} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user