feat(admin): 完善站点设置、内容回收站与后台菜单分组
新增站点设置与内容管理(含已删除恢复/清除),用户页改用独有运营指标,并按概览/内容/成员与经济/站点重组侧栏。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
179
frontend/app/admin/settings/SettingsAdmin.tsx
Normal file
179
frontend/app/admin/settings/SettingsAdmin.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
21
frontend/app/admin/settings/page.tsx
Normal file
21
frontend/app/admin/settings/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import { isSuperOrOwner } from "@/lib/roles";
|
||||
import Forbidden from "../Forbidden";
|
||||
import SettingsAdmin from "./SettingsAdmin";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "站点设置",
|
||||
};
|
||||
|
||||
export default async function AdminSettingsPage() {
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const me = await getMeCached(cookie || undefined);
|
||||
if (!me.user || !isSuperOrOwner(me.user.role)) {
|
||||
return <Forbidden text="站点设置仅超级管理员和站长可操作。" />;
|
||||
}
|
||||
const settings = await getPublicSettingsCached();
|
||||
return <SettingsAdmin initial={settings} />;
|
||||
}
|
||||
Reference in New Issue
Block a user