"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { Ban, ExternalLink, Loader2, RotateCcw, ShieldCheck, Users as UsersIcon } from "lucide-react"; import { apiAdminSetUserBan, apiAdminSetUserRole, apiAdminSetUserMessages, type Board } from "@/lib/api"; import { useUrlStyle } from "@/components/SiteBrandProvider"; import { userPath } from "@/lib/urlStyle"; import { ROLES, ROLE_META, roleMeta } from "@/lib/roles"; import { formatDate } from "@/lib/format"; import { toast } from "@/lib/toast"; import Avatar from "@/components/Avatar"; import Modal from "@/components/Modal"; import RoleBadge from "@/components/RoleBadge"; import BoardIcon from "@/components/BoardIcon"; /** 角色与账号弹窗所需的最小用户形状(后台列表项 / 公开资料均可) */ export interface RoleAccountTarget { id: number; username: string; nickname: string; role: string; board_ids?: number[]; banned: boolean; can_manage_messages?: boolean; avatar?: string; email?: string; points?: number; created_at?: string; } interface RoleOption { value: string; title: string; desc: string; token: string; } /** * 角色授权 + 封禁状态弹窗。 * 后台用户管理与前台 StaffUserMenu 共用,避免两套表单。 */ export default function RoleAccountModal({ target, boards, canGrantSuper, acting, onClose, onSaved, setActing, }: { target: RoleAccountTarget | null; boards: Board[]; canGrantSuper: boolean; acting: boolean; onClose: () => void; onSaved: (next: RoleAccountTarget) => void; setActing: (v: boolean) => void; }) { const grantKeys = [ ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.BOARD_ADMIN, ROLES.USER, ] as const; const options: RoleOption[] = grantKeys .filter((k) => canGrantSuper || k !== ROLES.SUPER_ADMIN) .map((k) => { const meta = ROLE_META[k]; return { value: k, title: meta.label, desc: meta.grantDesc || "", token: meta.colorToken, }; }); const [role, setRole] = useState(ROLES.USER); const [boardIds, setBoardIds] = useState([]); const [banned, setBanned] = useState(false); const [manageMessages, setManageMessages] = useState(false); const [err, setErr] = useState(""); useEffect(() => { if (target) { setRole(target.role === ROLES.OWNER ? ROLES.USER : target.role); setBoardIds(target.board_ids ?? []); setBanned(!!target.banned); setManageMessages(!!target.can_manage_messages); setErr(""); } }, [target]); if (!target) return null; const toggleBoard = (id: number) => { setBoardIds((prev) => (prev.includes(id) ? prev.filter((b) => b !== id) : [...prev, id])); }; const sameBoards = JSON.stringify([...(target.board_ids ?? [])].sort((a, b) => a - b)) === JSON.stringify([...boardIds].sort((a, b) => a - b)); const roleChanged = role !== target.role || (role === ROLES.BOARD_ADMIN && !sameBoards); const banChanged = banned !== !!target.banned; const messagesChanged = canGrantSuper && target.role !== ROLES.OWNER && target.role !== ROLES.SUPER_ADMIN && manageMessages !== !!target.can_manage_messages; const willBan = banChanged && banned; const willUnban = banChanged && !banned; const submit = async () => { if (role === ROLES.BOARD_ADMIN && boardIds.length === 0) { setErr("请至少选择一个授权板块"); return; } if (!roleChanged && !banChanged && !messagesChanged) { onClose(); return; } setActing(true); try { let nextRole = target.role; let nextBoards = target.board_ids ?? []; let nextBanned = !!target.banned; let nextMessages = !!target.can_manage_messages; if (roleChanged) { const res = await apiAdminSetUserRole( target.id, role, role === ROLES.BOARD_ADMIN ? boardIds : [] ); nextRole = res.user.role; nextBoards = res.user.board_ids ?? []; } if (banChanged) { const res = await apiAdminSetUserBan(target.id, banned); nextBanned = !!res.user.banned; } if (messagesChanged) { const res = await apiAdminSetUserMessages(target.id, manageMessages); nextMessages = !!res.user.can_manage_messages; } const parts: string[] = []; if (roleChanged) parts.push("角色已更新"); if (willBan) parts.push("已封禁"); if (willUnban) parts.push("已解封"); if (messagesChanged) parts.push(manageMessages ? "已授予消息管理" : "已撤销消息管理"); toast(parts.join(" · ") || "已保存", "ok"); onSaved({ ...target, role: nextRole, board_ids: nextBoards, banned: nextBanned, can_manage_messages: nextMessages, }); onClose(); } catch (e) { setErr(e instanceof Error ? e.message : "保存失败"); } finally { setActing(false); } }; const saveLabel = willBan ? "保存并封禁" : willUnban ? "保存并解封" : "保存"; const displayName = target.nickname || target.username; const currentMeta = roleMeta(target.role); return ( } onClose={onClose} busy={acting} maxWidthClass="max-w-[540px]" panelClassName="p-6 max-h-[88vh] flex flex-col overflow-hidden" bodyClassName="overflow-y-auto flex-1 min-h-0 -mx-1 px-1" ariaLabel={`设置 ${displayName} 的角色与账号`} footer={ <> } >

管理角色

{options.map((o) => { const active = role === o.value; return ( ); })}
{role === ROLES.BOARD_ADMIN && (

授权板块 *

{boards.length === 0 ? (

板块加载中…

) : (
{boards.map((b) => { const checked = boardIds.includes(b.id); return ( ); })}
)} {err && (

{err}

)}
)} {canGrantSuper && target.role !== ROLES.OWNER && target.role !== ROLES.SUPER_ADMIN && (

消息管理

)}

账号状态

{willBan && (

封禁后立即强制下线,无法登录、发帖与回复;已发布内容保留。角色变更与封禁将一并保存。

)}
); } function IdentityHeader({ target, displayName, currentMetaLabel, }: { target: RoleAccountTarget; displayName: string; currentMetaLabel: string; }) { const urlStyle = useUrlStyle(); const metaBits: string[] = [`ID ${target.id}`]; if (typeof target.points === "number") { metaBits.push(`积分 ${target.points.toLocaleString("zh-CN")}`); } if (target.created_at) { metaBits.push(`注册 ${formatDate(target.created_at)}`); } return (

角色与账号

{/* 描边用 box-shadow,勿把实色垫在默认头像后面:.av-* 底是半透明,会透出垫色把字母对比打翻 */}

{displayName}

@{target.username} {target.email ? ` · ${target.email}` : ""}

{target.banned ? "已封禁" : "正常"}

{metaBits.join(" · ")} 当前角色 {currentMetaLabel}

查看个人主页
); }