feat: 新增管理员用户管理功能与账号封禁体系
此提交完成了全站管理员后台核心功能开发: 1. 新增AdminUserService与管理接口,支持用户列表查询、角色变更与封禁解封 2. 重构认证链路,统一账号封禁错误码与处理逻辑 3. 新增全局强制下线机制,账号被封禁时自动弹出告知弹窗并清理登录态 4. 重构管理员后台路由与布局,统一权限校验逻辑 5. 优化通知轮询逻辑,页面切回前台时立即刷新未读计数 6. 合并原公告、外观管理页到统一后台面板
This commit is contained in:
169
frontend/app/admin/AdminNav.tsx
Normal file
169
frontend/app/admin/AdminNav.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
Megaphone,
|
||||
Palette,
|
||||
ShieldCheck,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
import type { User } from "@/lib/api";
|
||||
import Avatar from "@/components/Avatar";
|
||||
|
||||
// 后台导航项:exact 为精确匹配(仪表盘 /admin),其余按路径前缀匹配
|
||||
const NAV_ITEMS: { href: string; label: string; icon: LucideIcon; exact?: boolean }[] = [
|
||||
{ href: "/admin", label: "仪表盘", icon: LayoutDashboard, exact: true },
|
||||
{ href: "/admin/users", label: "用户管理", icon: Users },
|
||||
{ href: "/admin/announcements", label: "公告管理", icon: Megaphone },
|
||||
{ href: "/admin/appearance", label: "外观设置", icon: Palette },
|
||||
];
|
||||
|
||||
function isActive(pathname: string, href: string, exact?: boolean) {
|
||||
return exact ? pathname === href : pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 桌面端后台侧边栏:品牌头 + 分组导航 + 返回站点 + 管理员身份卡。
|
||||
* 选中态复用全站导航语言(--nav-active-bg 淡底 + accent 文字),
|
||||
* 与 j13-bcard / 顶部 pill 保持一致。
|
||||
*/
|
||||
export function AdminSidebar({ user }: { user: User }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="panel p-3">
|
||||
{/* 品牌头 */}
|
||||
<div className="flex items-center gap-2.5 px-2 pt-1.5 pb-3.5" style={{ borderBottom: "1px solid var(--line)" }}>
|
||||
<span
|
||||
className="w-9 h-9 rounded-xl flex items-center justify-center shrink-0"
|
||||
style={{ background: "var(--accent)", color: "var(--accent-on)" }}
|
||||
>
|
||||
<ShieldCheck size={18} />
|
||||
</span>
|
||||
<span className="leading-tight min-w-0">
|
||||
<span className="block text-[14.5px] font-extrabold tracking-tight truncate" style={{ color: "var(--ink)" }}>
|
||||
管理面板
|
||||
</span>
|
||||
<span className="block text-[10.5px] font-semibold mt-0.5 tracking-wide" style={{ color: "var(--ink-3)" }}>
|
||||
J13 ADMIN
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 导航 */}
|
||||
<nav className="flex flex-col gap-1 pt-3 pb-2" aria-label="后台导航">
|
||||
<p className="field-label px-2 pb-1.5" style={{ marginBottom: 0 }}>
|
||||
站点管理
|
||||
</p>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(pathname, item.href, item.exact);
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-[13.5px] font-semibold transition-colors"
|
||||
style={
|
||||
active
|
||||
? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }
|
||||
: { color: "var(--ink-2)" }
|
||||
}
|
||||
>
|
||||
<Icon size={16} className="shrink-0" style={{ color: active ? "var(--nav-active-ink)" : "var(--ink-3)" }} />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* 返回前台 */}
|
||||
<div className="pt-2" style={{ borderTop: "1px solid var(--line)" }}>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-[13.5px] font-semibold transition-colors hover:bg-[var(--panel-2)]"
|
||||
style={{ color: "var(--ink-2)" }}
|
||||
>
|
||||
<ArrowLeft size={16} className="shrink-0" style={{ color: "var(--ink-3)" }} />
|
||||
返回站点
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 管理员身份卡 */}
|
||||
<div className="flex items-center gap-2.5 mt-2 px-2.5 py-2.5 rounded-xl" style={{ background: "var(--panel-2)" }}>
|
||||
<Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={32} />
|
||||
<div className="min-w-0 leading-tight">
|
||||
<p className="text-[13px] font-bold truncate" style={{ color: "var(--ink)" }}>
|
||||
{user.nickname}
|
||||
</p>
|
||||
<p className="text-[11px] font-semibold truncate" style={{ color: "var(--ink-3)" }}>
|
||||
@{user.username}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className="ml-auto shrink-0 inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10.5px] font-bold leading-[1.6]"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
<ShieldCheck size={11} />
|
||||
管理员
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端后台导航:横向滚动 pill,吸附在全站 Header 下方,
|
||||
* 窄屏下保持与桌面侧栏同等的切换能力且不占垂直空间。
|
||||
*/
|
||||
export function AdminNavMobile() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="lg:hidden sticky z-30 -mx-4 sm:-mx-6 px-4 sm:px-6 py-2.5 mb-4"
|
||||
style={{
|
||||
top: 68,
|
||||
background: "color-mix(in srgb, var(--bg) 88%, transparent)",
|
||||
backdropFilter: "blur(14px)",
|
||||
WebkitBackdropFilter: "blur(14px)",
|
||||
borderBottom: "1px solid var(--line)",
|
||||
}}
|
||||
>
|
||||
<nav className="flex items-center gap-2 overflow-x-auto" aria-label="后台导航">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(pathname, item.href, item.exact);
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className="shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] font-semibold transition-colors"
|
||||
style={
|
||||
active
|
||||
? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }
|
||||
: { color: "var(--ink-2)", background: "var(--panel)", border: "1px solid var(--line)" }
|
||||
}
|
||||
>
|
||||
<Icon size={14} />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<Link
|
||||
href="/"
|
||||
className="shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] font-semibold transition-colors"
|
||||
style={{ color: "var(--ink-2)", background: "var(--panel)", border: "1px solid var(--line)" }}
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
返回站点
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user