"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useAuth } from "@/hooks/use-auth"; import { useActiveClass } from "@/hooks/use-active-class"; import { deleteAPI, fetchAPI, getErrorMessage, postAPI, putAPI, uploadAPI, } from "@/lib/api"; import { Card, CardContent } from "@/components/ui/card"; import { ErrorState } from "@/components/error-state"; import { Pagination } from "@/components/pagination"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { ConfirmDialog } from "@/components/confirm-dialog"; import { toast } from "sonner"; import type { ClassPermission, InactiveMemberEntry, PageResponse, UserListItem, } from "@/lib/types"; import { CLASS_PERMISSIONS, ROLES } from "@/lib/constants"; import { hasClassPermission } from "@/lib/permissions"; type InviteCodeResponse = { invite_code: string; }; type MessageResponse = { message: string; }; type TeacherCreateRequest = { class_id: number; name: string; email: string; password: string; }; type TeacherCreateResponse = { message: string; user: UserListItem; }; type TeacherAssignRequest = { class_id: number; email: string; }; type TeacherAssignResponse = { message: string; user: UserListItem; }; const PERMISSION_GROUPS: Array<{ title: string; description: string; permissions: ClassPermission[]; }> = [ { title: "成员与组织", description: "管理班级成员、班委和基础可见范围", permissions: ["class_view", "member_view", "member_manage", "committee_manage"], }, { title: "内容与活动", description: "管理公告、动态、投票、排期和资源库", permissions: [ "announcement_manage", "timeline_manage", "vote_manage", "schedule_manage", "resource_manage", ], }, { title: "教学与财务", description: "管理作业、班费和模块开关", permissions: ["assignment_manage", "fund_manage", "module_manage"], }, ]; const COMMITTEE_PRESETS: Array<{ role: string; description: string; permissions: ClassPermission[]; }> = [ { role: "班长", description: "统筹班级日常管理与主要内容发布", permissions: [ "class_view", "member_view", "member_manage", "committee_manage", "announcement_manage", "timeline_manage", "vote_manage", "schedule_manage", "resource_manage", ], }, { role: "副班长", description: "协助班长处理班级组织和内容管理", permissions: [ "class_view", "member_view", "member_manage", "announcement_manage", "timeline_manage", "vote_manage", "schedule_manage", "resource_manage", ], }, { role: "学习委员", description: "负责学习通知、作业协同与课程安排", permissions: [ "class_view", "member_view", "announcement_manage", "assignment_manage", "schedule_manage", "resource_manage", ], }, { role: "组织委员", description: "负责活动组织、投票协同和排期推进", permissions: [ "class_view", "member_view", "timeline_manage", "vote_manage", "schedule_manage", "resource_manage", ], }, { role: "生活委员", description: "负责生活事务通知与班费相关管理", permissions: [ "class_view", "member_view", "announcement_manage", "fund_manage", "resource_manage", ], }, { role: "文体委员", description: "负责文体活动、动态发布与投票互动", permissions: [ "class_view", "member_view", "timeline_manage", "vote_manage", "schedule_manage", "resource_manage", ], }, ]; export default function MembersPage() { const { user } = useAuth(); const { activeClassId } = useActiveClass(); // Tab state const [activeTab, setActiveTab] = useState<"members" | "inactive">("inactive"); // Members state const [members, setMembers] = useState([]); const [membersLoading, setMembersLoading] = useState(true); const [membersError, setMembersError] = useState(null); const [membersPage, setMembersPage] = useState(1); const [membersTotalPages, setMembersTotalPages] = useState(1); // Inactive member state const [inactiveMembers, setInactiveMembers] = useState([]); const [inactiveLoading, setInactiveLoading] = useState(true); const [inactiveError, setInactiveError] = useState(null); const [inactivePage, setInactivePage] = useState(1); const [inactiveTotalPages, setInactiveTotalPages] = useState(1); const [inviteCode, setInviteCode] = useState(""); const [importOpen, setImportOpen] = useState(false); const [importText, setImportText] = useState(""); const [importing, setImporting] = useState(false); const [clearTarget, setClearTarget] = useState(false); const [teacherCreateOpen, setTeacherCreateOpen] = useState(false); const [teacherName, setTeacherName] = useState(""); const [teacherEmail, setTeacherEmail] = useState(""); const [teacherPassword, setTeacherPassword] = useState(""); const [teacherSubmitting, setTeacherSubmitting] = useState(false); const [teacherAssignOpen, setTeacherAssignOpen] = useState(false); const [teacherAssignEmail, setTeacherAssignEmail] = useState(""); const [teacherAssignSubmitting, setTeacherAssignSubmitting] = useState(false); const [memberManagerOpen, setMemberManagerOpen] = useState(false); const [managingMember, setManagingMember] = useState(null); const [memberRoleValue, setMemberRoleValue] = useState("student"); const [memberCommitteeValue, setMemberCommitteeValue] = useState(""); const [memberPermissionsValue, setMemberPermissionsValue] = useState([]); const [memberSubmitting, setMemberSubmitting] = useState(false); const fileInputRef = useRef(null); const isSuperAdmin = user?.role === "super_admin"; const canManageCommittee = hasClassPermission(user, "committee_manage", activeClassId); const canOpenMemberManager = isSuperAdmin || canManageCommittee; // Load members const loadMembers = useCallback(async () => { if (!activeClassId) { setMembers([]); setMembersLoading(false); return; } setMembersLoading(true); setMembersError(null); try { const res = await fetchAPI>( `/api/classes/${activeClassId}/members`, { status: "approved", page: String(membersPage), page_size: "20", } ); setMembers(res.items ?? []); setMembersTotalPages(res.total_pages ?? 1); } catch (err: unknown) { setMembersError(getErrorMessage(err, "加载失败")); } finally { setMembersLoading(false); } }, [activeClassId, membersPage]); // Load inactive members const loadInactiveMembers = useCallback(async () => { if (!activeClassId) { setInactiveMembers([]); setInviteCode(""); setInactiveLoading(false); return; } setInactiveLoading(true); setInactiveError(null); try { const [inactiveRes, codeRes] = await Promise.all([ fetchAPI>(`/api/classes/${activeClassId}/inactive-members`, { page: String(inactivePage), page_size: "50", }), fetchAPI(`/api/classes/${activeClassId}/invite-code`), ]); setInactiveMembers(inactiveRes.items ?? []); setInactiveTotalPages(inactiveRes.total_pages ?? 1); setInviteCode(codeRes.invite_code ?? ""); } catch (err: unknown) { setInactiveError(getErrorMessage(err, "加载失败")); } finally { setInactiveLoading(false); } }, [activeClassId, inactivePage]); useEffect(() => { if (!activeClassId) return; if (activeTab === "members") { void loadMembers(); return; } void loadInactiveMembers(); }, [activeClassId, activeTab, loadInactiveMembers, loadMembers]); // Member actions const handleRoleChange = async (userId: number, newRole: string) => { try { await putAPI(`/api/users/${userId}/role`, { role: newRole, }); toast.success( `角色已更新为 ${ROLES[newRole as keyof typeof ROLES] || newRole}` ); await loadMembers(); } catch (err: unknown) { toast.error(getErrorMessage(err, "操作失败")); } }; const handleCommitteeRoleChange = async (userId: number, newRole: string) => { try { await putAPI(`/api/users/${userId}/committee-role`, { class_id: activeClassId, committee_role: newRole === "__none__" ? null : newRole, }); toast.success(newRole === "__none__" ? "已清除班委标签" : `已设置班委标签: ${newRole}`); await loadMembers(); } catch (err: unknown) { toast.error(getErrorMessage(err, "操作失败")); } }; // Inactive member actions const handleCopyCode = () => { navigator.clipboard.writeText(inviteCode); toast.success("邀请码已复制"); }; const handleRegenerateCode = async () => { try { const res = await postAPI( `/api/classes/${activeClassId}/invite-code/regenerate` ); setInviteCode(res.invite_code); toast.success("邀请码已重新生成"); } catch (err: unknown) { toast.error(getErrorMessage(err, "操作失败")); } }; const handleTextImport = async () => { if (!importText.trim()) return; setImporting(true); try { // Parse text: each line is "学号 姓名" or "学号,姓名" or "学号\t姓名" const entries: { student_id: string; name: string }[] = []; const lines = importText.trim().split("\n"); for (const line of lines) { const parts = line.trim().split(/[,\t,\s]+/); if (parts.length >= 2) { entries.push({ student_id: parts[0], name: parts.slice(1).join("") }); } } if (entries.length === 0) { toast.error("未解析到有效数据,每行格式:学号 姓名"); return; } const res = await postAPI( `/api/classes/${activeClassId}/inactive-members/import`, { entries, } ); toast.success(res.message); setImportOpen(false); setImportText(""); await loadInactiveMembers(); } catch (err: unknown) { toast.error(getErrorMessage(err, "导入失败")); } finally { setImporting(false); } }; const handleFileUpload = async () => { const fileInput = fileInputRef.current; if (!fileInput?.files?.length) return; setImporting(true); try { const formData = new FormData(); formData.append("file", fileInput.files[0]); const res = await uploadAPI( `/api/classes/${activeClassId}/inactive-members/upload`, formData ); toast.success(res.message); setImportOpen(false); if (fileInput) fileInput.value = ""; await loadInactiveMembers(); } catch (err: unknown) { toast.error(getErrorMessage(err, "上传失败")); } finally { setImporting(false); } }; const handleDeleteInactiveMember = async (userId: number) => { try { await deleteAPI(`/api/classes/${activeClassId}/inactive-members/${userId}`); toast.success("已删除"); await loadInactiveMembers(); } catch (err: unknown) { toast.error(getErrorMessage(err, "删除失败")); } }; const handleClearRoster = async () => { try { const res = await postAPI(`/api/classes/${activeClassId}/inactive-members/clear`); toast.success(res.message); setClearTarget(false); await loadInactiveMembers(); } catch (err: unknown) { toast.error(getErrorMessage(err, "操作失败")); } }; const togglePermission = (permission: ClassPermission) => { setMemberPermissionsValue((prev) => prev.includes(permission) ? prev.filter((item) => item !== permission) : [...prev, permission] ); }; const applyPermissionPreset = (permissions: ClassPermission[]) => { setMemberPermissionsValue(Array.from(new Set(permissions))); }; const applyCommitteePreset = (presetRole: string) => { const preset = COMMITTEE_PRESETS.find((item) => item.role === presetRole); if (!preset) { return; } setMemberCommitteeValue(preset.role); applyPermissionPreset(preset.permissions); }; const handlePermissionsSave = async (userId: number, permissions: ClassPermission[]) => { try { await putAPI(`/api/users/${userId}/class-permissions`, { class_id: activeClassId, class_permissions: permissions, }); } catch (err: unknown) { toast.error(getErrorMessage(err, "操作失败")); throw err; } }; const resetTeacherForm = () => { setTeacherName(""); setTeacherEmail(""); setTeacherPassword(""); }; const resetTeacherAssignForm = () => { setTeacherAssignEmail(""); }; const handleCreateTeacher = async () => { if (!activeClassId) return; if (!teacherName.trim()) { toast.error("请输入老师姓名"); return; } if (!teacherEmail.trim()) { toast.error("请输入老师邮箱"); return; } if (teacherPassword.length < 8) { toast.error("初始密码至少 8 位"); return; } setTeacherSubmitting(true); try { const payload: TeacherCreateRequest = { class_id: activeClassId, name: teacherName.trim(), email: teacherEmail.trim(), password: teacherPassword, }; const res = await postAPI("/api/users/teachers", payload); toast.success(res.message); setTeacherCreateOpen(false); resetTeacherForm(); if (activeTab === "members") { await loadMembers(); } } catch (err: unknown) { toast.error(getErrorMessage(err, "创建老师失败")); } finally { setTeacherSubmitting(false); } }; const handleAssignTeacher = async () => { if (!activeClassId) return; if (!teacherAssignEmail.trim()) { toast.error("请输入老师邮箱"); return; } setTeacherAssignSubmitting(true); try { const payload: TeacherAssignRequest = { class_id: activeClassId, email: teacherAssignEmail.trim(), }; const res = await postAPI("/api/users/teachers/assign", payload); toast.success(res.message); setTeacherAssignOpen(false); resetTeacherAssignForm(); if (activeTab === "members") { await loadMembers(); } } catch (err: unknown) { toast.error(getErrorMessage(err, "分配老师失败")); } finally { setTeacherAssignSubmitting(false); } }; const openMemberManager = (member: UserListItem) => { setManagingMember(member); setMemberRoleValue(member.role); setMemberCommitteeValue(member.committee_role ?? ""); setMemberPermissionsValue(member.class_permissions ?? []); setMemberManagerOpen(true); }; const resetMemberManager = () => { setManagingMember(null); setMemberRoleValue("student"); setMemberCommitteeValue(""); setMemberPermissionsValue([]); setMemberSubmitting(false); }; const handleMemberManagerSave = async () => { if (!managingMember || !activeClassId) return; const originalRole = managingMember.role; const originalCommittee = managingMember.committee_role ?? ""; const originalPermissions = managingMember.class_permissions ?? []; const normalizedCommittee = memberCommitteeValue.trim(); const permissionsChanged = originalPermissions.length !== memberPermissionsValue.length || originalPermissions.some((permission) => !memberPermissionsValue.includes(permission)); setMemberSubmitting(true); try { if (isSuperAdmin && memberRoleValue !== originalRole) { await handleRoleChange(managingMember.id, memberRoleValue); } if (memberRoleValue !== "super_admin" && canManageCommittee) { if (normalizedCommittee !== originalCommittee) { await handleCommitteeRoleChange( managingMember.id, normalizedCommittee ? normalizedCommittee : "__none__" ); } if (permissionsChanged) { await handlePermissionsSave(managingMember.id, memberPermissionsValue); toast.success("成员设置已更新"); } else if (normalizedCommittee !== originalCommittee || memberRoleValue !== originalRole) { toast.success("成员设置已更新"); } } else if (memberRoleValue !== originalRole) { toast.success("成员设置已更新"); } setMemberManagerOpen(false); resetMemberManager(); await loadMembers(); } catch { // Errors are surfaced by the called actions. } finally { setMemberSubmitting(false); } }; return (
Members

成员管理

管理成员导入、激活码和班级成员权限

{!activeClassId ? (
{isSuperAdmin ? "请在顶部选择一个班级" : "您尚未分配班级"}
) : ( <> {/* Tab switcher */}
{activeTab === "inactive" && (
{/* Invite code section */}

成员激活码

{inviteCode || "—"}

将此激活码分享给成员,成员输入激活码和学号后即可激活账号

{/* Import actions */}
{ setImportOpen(open); if (!open) setImportText(""); }}> 导入成员