feat: 新增管理员用户管理功能与账号封禁体系

此提交完成了全站管理员后台核心功能开发:
1.  新增AdminUserService与管理接口,支持用户列表查询、角色变更与封禁解封
2.  重构认证链路,统一账号封禁错误码与处理逻辑
3.  新增全局强制下线机制,账号被封禁时自动弹出告知弹窗并清理登录态
4.  重构管理员后台路由与布局,统一权限校验逻辑
5.  优化通知轮询逻辑,页面切回前台时立即刷新未读计数
6.  合并原公告、外观管理页到统一后台面板
This commit is contained in:
2026-09-15 02:52:26 +08:00
parent 24c6dea38c
commit 6da4309453
20 changed files with 2248 additions and 105 deletions

View File

@@ -18,12 +18,11 @@ import {
Home,
User as UserIcon,
ChevronDown,
Megaphone,
Palette,
ShieldCheck,
} from "lucide-react";
import { apiLogout, apiMe, type User } from "@/lib/api";
import { hasAuthCookieHint } from "@/lib/cookies";
import { onUserUpdate } from "@/lib/userEvents";
import { onUserUpdate, onForceLogout } from "@/lib/userEvents";
import NotificationBell from "./NotificationBell";
import Avatar from "./Avatar";
@@ -60,6 +59,21 @@ export default function Header({
setUser((prev) => (prev ? { ...prev, ...patch } : prev));
}), []);
// 强制下线(当前唯一来源:账号被管理员封禁)。
// 统一复用登出清理:尽力调用 /api/logout 清除 HttpOnly cookie(该端点不要求
// 有效登录态),本地立即游客化并 router.refresh 校正 SSR 区域,
// 封禁告知弹窗由 AccountGuard 负责,避免状态与 UI 清理分散
useEffect(() => onForceLogout(() => {
setMenuOpen(false);
apiLogout()
.catch(() => {})
.finally(() => {
setUser(null);
setUnread(0);
router.refresh();
});
}), [router]);
// 挂载后静默校正一次(仅一次):覆盖 SSR 期间后端不可用、旧 Path refresh
// 半登录等边缘情况。正常 F5 下结果与 SSR 一致,不产生可见闪动;
// 无可读 CSRF cookie 说明必然是游客,跳过请求
@@ -281,14 +295,9 @@ export default function Header({
)}
{user && <MobileLink href="/settings">账号设置</MobileLink>}
{user?.role === "admin" && (
<>
<MobileLink href="/admin/announcements">
<span className="inline-flex items-center gap-2"><Megaphone size={15} /> 公告管理</span>
</MobileLink>
<MobileLink href="/admin/appearance">
<span className="inline-flex items-center gap-2"><Palette size={15} /> 外观设置</span>
</MobileLink>
</>
<MobileLink href="/admin">
<span className="inline-flex items-center gap-2"><ShieldCheck size={15} /> 管理面板</span>
</MobileLink>
)}
{!user && <MobileLink href="/login">登录</MobileLink>}
{!user && <MobileLink href="/register">注册</MobileLink>}
@@ -449,11 +458,8 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
</div>
{user.role === "admin" && (
<div className="p-1.5" style={{ borderTop: "1px solid var(--line)" }}>
<Link href="/admin/announcements" onClick={() => setOpen(false)} className={itemCls} role="menuitem">
<Megaphone size={15} style={{ color: "var(--accent)" }} /> 公告管理
</Link>
<Link href="/admin/appearance" onClick={() => setOpen(false)} className={itemCls} role="menuitem">
<Palette size={15} style={{ color: "var(--accent)" }} /> 外观设置
<Link href="/admin" onClick={() => setOpen(false)} className={itemCls} role="menuitem">
<ShieldCheck size={15} style={{ color: "var(--accent)" }} /> 管理面板
</Link>
</div>
)}