"use client"; import { useEffect, useState, useRef } from "react"; import { useAuth } from "@/hooks/use-auth"; import { useActiveClass } from "@/hooks/use-active-class"; import { fetchAPI, putAPI, postAPI, deleteAPI, 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 { 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 { UserListItem, RosterEntry } from "@/lib/types"; import { USER_STATUS, ROLES } from "@/lib/constants"; export default function MembersPage() { const { user } = useAuth(); const { activeClassId } = useActiveClass(); // Tab state const [activeTab, setActiveTab] = useState<"members" | "roster">("roster"); // 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); const [filter, setFilter] = useState("all"); // Roster state const [roster, setRoster] = useState([]); const [rosterLoading, setRosterLoading] = useState(true); const [rosterError, setRosterError] = useState(null); const [rosterPage, setRosterPage] = useState(1); const [rosterTotalPages, setRosterTotalPages] = 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 fileInputRef = useRef(null); const isSuperAdmin = user?.role === "super_admin"; // Load members const loadMembers = async () => { if (!activeClassId) { setMembersLoading(false); return; } setMembersLoading(true); setMembersError(null); try { const res = await fetchAPI(`/api/classes/${activeClassId}/members`, { page: String(membersPage), page_size: "20", }); setMembers(res.items || []); setMembersTotalPages(res.total_pages || 1); } catch (err: any) { setMembersError(err.message || "加载失败"); } finally { setMembersLoading(false); } }; // Load roster const loadRoster = async () => { if (!activeClassId) { setRosterLoading(false); return; } setRosterLoading(true); setRosterError(null); try { const [rosterRes, codeRes] = await Promise.all([ fetchAPI(`/api/classes/${activeClassId}/roster`, { page: String(rosterPage), page_size: "50", }), fetchAPI(`/api/classes/${activeClassId}/invite-code`), ]); setRoster(rosterRes.items || []); setRosterTotalPages(rosterRes.total_pages || 1); setInviteCode(codeRes.invite_code || ""); } catch (err: any) { setRosterError(err.message || "加载失败"); } finally { setRosterLoading(false); } }; useEffect(() => { if (!activeClassId) return; if (activeTab === "members") loadMembers(); else loadRoster(); }, [activeClassId, activeTab, membersPage, rosterPage]); // Member actions const handleStatusChange = async ( userId: number, newStatus: string, role?: string ) => { try { await putAPI(`/api/users/${userId}/status`, { status: newStatus, role: role || undefined, }); toast.success( `已更新状态为 ${USER_STATUS[newStatus as keyof typeof USER_STATUS] || newStatus}` ); loadMembers(); } catch (err: any) { toast.error(err.message || "操作失败"); } }; const handleRoleChange = async (userId: number, newRole: string) => { try { await putAPI(`/api/users/${userId}/status`, { status: "approved", role: newRole, }); toast.success( `角色已更新为 ${ROLES[newRole as keyof typeof ROLES] || newRole}` ); loadMembers(); } catch (err: any) { toast.error(err.message || "操作失败"); } }; const getStatusBadge = (status: string) => { const variants: Record = { approved: "default", disabled: "secondary", }; return ( {USER_STATUS[status as keyof typeof USER_STATUS] || status} ); }; // Roster 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: any) { toast.error(err.message || "操作失败"); } }; 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}/roster/import`, { entries, }); toast.success(res.message); setImportOpen(false); setImportText(""); loadRoster(); } catch (err: any) { toast.error(err.message || "导入失败"); } 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}/roster/upload`, formData); toast.success((res as any).message); setImportOpen(false); if (fileInput) fileInput.value = ""; loadRoster(); } catch (err: any) { toast.error(err.message || "上传失败"); } finally { setImporting(false); } }; const handleDeleteRoster = async (rosterId: number) => { try { await deleteAPI(`/api/classes/${activeClassId}/roster/${rosterId}`); toast.success("已删除"); loadRoster(); } catch (err: any) { toast.error(err.message || "删除失败"); } }; const handleClearRoster = async () => { try { const res = await postAPI(`/api/classes/${activeClassId}/roster/clear`); toast.success(res.message); setClearTarget(false); loadRoster(); } catch (err: any) { toast.error(err.message || "操作失败"); } }; const filteredMembers = filter === "all" ? members : members.filter((m) => m.status === filter); return (

成员与花名册

管理班级花名册、邀请码和已注册成员

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

班级邀请码

{inviteCode || "—"}

将此邀请码分享给学生,学生注册时输入邀请码+学号即可加入班级

{/* Import actions */}
{ setImportOpen(open); if (!open) setImportText(""); }}> 导入花名册