"use client"; import { useEffect, useState } from "react"; import { Ban, Loader2, RotateCcw, ShieldCheck, Users as UsersIcon } from "lucide-react"; import { apiAdminSetUserBan, apiAdminSetUserRole, type Board } from "@/lib/api"; import { ROLES, ROLE_META } from "@/lib/roles"; import { toast } from "@/lib/toast"; import Modal from "@/components/Modal"; /** 角色与账号弹窗所需的最小用户形状(后台列表项 / 公开资料均可) */ export interface RoleAccountTarget { id: number; username: string; nickname: string; role: string; board_ids?: number[]; banned: boolean; } 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 [err, setErr] = useState(""); useEffect(() => { if (target) { setRole(target.role === ROLES.OWNER ? ROLES.USER : target.role); setBoardIds(target.board_ids ?? []); setBanned(!!target.banned); 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 willBan = banChanged && banned; const willUnban = banChanged && !banned; const submit = async () => { if (role === ROLES.BOARD_ADMIN && boardIds.length === 0) { setErr("请至少选择一个授权板块"); return; } if (!roleChanged && !banChanged) { onClose(); return; } setActing(true); try { let nextRole = target.role; let nextBoards = target.board_ids ?? []; let nextBanned = !!target.banned; 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; } const parts: string[] = []; if (roleChanged) parts.push("角色已更新"); if (willBan) parts.push("已封禁"); if (willUnban) parts.push("已解封"); toast(parts.join(" · ") || "已保存", "ok"); onSaved({ id: target.id, username: target.username, nickname: target.nickname, role: nextRole, board_ids: nextBoards, banned: nextBanned, }); } catch (e) { toast(e instanceof Error ? e.message : "操作失败"); } finally { setActing(false); } }; const saveLabel = willBan ? "保存并封禁" : willUnban ? "保存并解封" : "保存"; return ( @{target.username}({target.nickname}) 角色决定后台入口与前台管理能力;封禁立即强制下线。站长内容档案仍走列表入口。 } onClose={onClose} busy={acting} maxWidthClass="max-w-[500px]" panelClassName="p-6 max-h-[88vh] overflow-y-auto" ariaLabel="角色与账号" 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}

)}
)}

账号状态

{willBan && (

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

)}
); }