Files
jiang13-bbs/frontend/app/admin/settings/SettingsAdmin.tsx
freefire dd7bd010b8 feat(admin): 完善站点设置、内容回收站与后台菜单分组
新增站点设置与内容管理(含已删除恢复/清除),用户页改用独有运营指标,并按概览/内容/成员与经济/站点重组侧栏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 05:56:27 +08:00

180 lines
5.4 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 { useState } from "react";
import {
Globe,
Loader2,
Save,
UserPlus,
} from "lucide-react";
import {
apiUpdateSiteSettings,
type PublicSettings,
} from "@/lib/api";
import { toast } from "@/lib/toast";
function Switch({
checked,
disabled,
onChange,
label,
}: {
checked: boolean;
disabled?: boolean;
onChange: () => void;
label: string;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
disabled={disabled}
onClick={onChange}
className="shrink-0 relative h-7 w-12 rounded-full transition-colors disabled:opacity-60"
style={{ background: checked ? "var(--accent)" : "var(--line-2)" }}
>
<span
className="absolute top-0.5 left-0.5 h-6 w-6 rounded-full bg-[var(--panel)] shadow transition-transform"
style={{ transform: checked ? "translateX(1.25rem)" : "translateX(0)" }}
/>
</button>
);
}
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 [savedName, setSavedName] = useState(initial.site_name);
const [savedDesc, setSavedDesc] = useState(initial.site_description);
const [saving, setSaving] = useState(false);
const [togglingReg, setTogglingReg] = useState(false);
const dirty =
siteName.trim() !== savedName || siteDescription.trim() !== savedDesc;
const saveIdentity = async () => {
if (saving) return;
const name = siteName.trim();
if (!name) {
toast("站点名称不能为空");
return;
}
setSaving(true);
try {
const res = await apiUpdateSiteSettings({
site_name: name,
site_description: siteDescription.trim(),
});
setSiteName(res.site_name);
setSiteDescription(res.site_description);
setSavedName(res.site_name);
setSavedDesc(res.site_description);
toast("站点信息已保存", "ok");
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setSaving(false);
}
};
const toggleRegister = async () => {
if (togglingReg) return;
const next = !allowRegister;
setTogglingReg(true);
try {
const res = await apiUpdateSiteSettings({ allow_register: next });
setAllowRegister(res.allow_register);
toast(res.allow_register ? "已开放注册" : "已关闭注册", "ok");
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setTogglingReg(false);
}
};
return (
<div className="max-w-3xl">
<div className="mb-5">
<h1
className="text-[22px] sm:text-[26px] font-extrabold tracking-tight flex items-center gap-2.5"
style={{ color: "var(--ink)" }}
>
<span
className="w-9 h-9 rounded-xl flex items-center justify-center"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
>
<Globe size={18} />
</span>
站点设置
</h1>
<p className="meta mt-1.5 text-[12.5px]">
配置站点名称、简介与注册开关;主题色请到「外观」页调整
</p>
</div>
<section className="j13-module p-5 sm:p-6 mb-5">
<h2 className="text-[15px] font-semibold mb-1" style={{ color: "var(--ink)" }}>
基本信息
</h2>
<p className="meta text-[12.5px] mb-4">用于页眉品牌、页脚版权与 SEO 元信息</p>
<label className="field-label" htmlFor="site-name">
站点名称
</label>
<input
id="site-name"
className="field mb-4"
maxLength={32}
value={siteName}
onChange={(e) => setSiteName(e.target.value)}
placeholder="姜十三论坛"
/>
<label className="field-label" htmlFor="site-desc">
站点简介
</label>
<textarea
id="site-desc"
className="field min-h-[96px] mb-4"
maxLength={160}
value={siteDescription}
onChange={(e) => setSiteDescription(e.target.value)}
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>
</section>
<section className="j13-module p-5 sm:p-6 mb-5 flex items-start justify-between gap-4 flex-wrap">
<div className="min-w-0 flex-1">
<h2 className="text-[15px] font-semibold flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
<UserPlus size={15} /> 开放注册
</h2>
<p className="meta text-[12.5px] mt-1 leading-relaxed">
关闭后,游客将无法提交注册;已有账号仍可登录。适用于内测或维护期。
</p>
</div>
<Switch
checked={allowRegister}
disabled={togglingReg}
onChange={() => void toggleRegister()}
label="开放注册"
/>
</section>
</div>
);
}