"use client"; /** * 班级邀请码管理 - 客户端组件(ARCHITECTURE.md §7.3 / §9.4 / §10 P5) * * 数据契约: * - useClassInvitationCodes / useGenerateClassInvitationCode / useRevokeClassInvitationCode * ❌ schema 无对应根字段 → MSW 兜底(@contract-pending) * * 功能: * - 列出班级所有邀请码(含状态/有效期/使用次数) * - 生成自定义邀请码(可选有效期/次数/备注) * - 撤销邀请码(软删除) * - 复制邀请码到剪贴板 * * 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4 */ import { useState } from "react"; import { Ban, Copy, Plus } from "lucide-react"; import { useTranslations } from "next-intl"; import { useGenerateClassInvitationCode, useRevokeClassInvitationCode, useClassInvitationCodes, type ClassInvitationCode, } from "@/lib/api"; import { notify } from "@/shared/lib/notify"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/shared/components/ui/alert-dialog"; import { Badge } from "@/shared/components/ui/badge"; import { Button } from "@/shared/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/shared/components/ui/dialog"; import { Input } from "@/shared/components/ui/input"; import { FormField } from "@/features/admin/school/schools-client"; // ── ClassInvitationManagerDialog ──────────────────────────────── /** * 班级邀请码管理对话框。 * * - 列表查询:useClassInvitationCodes(classId) * - 生成:useGenerateClassInvitationCode,自定义有效期/次数/备注 * - 撤销:useRevokeClassInvitationCode,软删除 * - 复制:navigator.clipboard.writeText * * 子对话框:GenerateCodeDialog(生成新邀请码) */ export function ClassInvitationManagerDialog({ open, classId, className, onClose, }: { open: boolean; classId: string; className: string; onClose: () => void; }): React.ReactElement | null { const t = useTranslations("admin.school.classes.invitation"); const tCommon = useTranslations("common"); const { data: codes, loading, error, refetch, } = useClassInvitationCodes(classId); const revokeMutation = useRevokeClassInvitationCode(); const [generateOpen, setGenerateOpen] = useState(false); const [revokeTarget, setRevokeTarget] = useState( null, ); const handleCopy = async (code: string): Promise => { try { await navigator.clipboard.writeText(code); notify.success(t("copied")); } catch (err) { notify.error(tCommon("error.loadFailed", { message: String(err) })); } }; const handleRevoke = async (): Promise => { if (!revokeTarget) return; try { await revokeMutation.run(revokeTarget.id); notify.success(t("revokeSuccess")); setRevokeTarget(null); void refetch(); } catch (err) { notify.error(tCommon("error.loadFailed", { message: String(err) })); } }; const handleGenerated = (): void => { void refetch(); }; if (!open) return null; return ( { if (!o) onClose(); }} > {t("title")} {t("title")} {className ? (

{t("classLabel")}: {className}

) : null}
{loading ? (
{tCommon("loading")}
) : error ? (

{tCommon("error.loadFailed", { message: String(error) })}

) : codes && codes.length > 0 ? (
{codes.map((record) => ( ))}
{t("colCode")} {t("colStatus")} {t("colUsedCount")} {t("colExpiresAt")} {t("colNote")} {t("colActions")}
{record.code} {record.usedCount} {record.maxUses !== null ? ` / ${record.maxUses}` : ""} {record.expiresAt ? new Date(record.expiresAt).toLocaleString() : t("neverExpires")} {record.note ?? "-"}
{record.status === "active" ? ( ) : null}
) : (
{t("empty")}
)}
setGenerateOpen(false)} onCreated={handleGenerated} /> {revokeTarget ? ( { if (!o && !revokeMutation.loading) setRevokeTarget(null); }} > {t("revoke")} {t("revokeConfirm")} setRevokeTarget(null)} > {tCommon("form.cancel")} void handleRevoke()} > {revokeMutation.loading ? tCommon("form.processing") : t("revoke")} ) : null}
); } // ── InvitationStatusBadge ─────────────────────────────────────── /** * 邀请码状态徽章。 * * - active:默认变体(primary 色) * - 其他(used/expired/revoked):secondary 变体 */ function InvitationStatusBadge({ status, }: { status: string; }): React.ReactElement { const t = useTranslations("admin.school.classes.invitation"); const variant = status === "active" ? "default" : "secondary"; const label = status === "active" || status === "used" || status === "expired" || status === "revoked" ? t(`status.${status}`) : status; return {label}; } // ── GenerateCodeDialog ────────────────────────────────────────── /** * 生成新邀请码对话框。 * * 字段:有效期(小时)/ 最大使用次数 / 备注 * 全部可选,留空则生成无限制邀请码。 */ function GenerateCodeDialog({ open, classId, onClose, onCreated, }: { open: boolean; classId: string; onClose: () => void; onCreated: () => void; }): React.ReactElement | null { const t = useTranslations("admin.school.classes.invitation"); const tCommon = useTranslations("common"); const generateMutation = useGenerateClassInvitationCode(); const [expiresInHours, setExpiresInHours] = useState(""); const [maxUses, setMaxUses] = useState(""); const [note, setNote] = useState(""); const handleSubmit = async ( e: React.FormEvent, ): Promise => { e.preventDefault(); if (!classId) return; try { await generateMutation.run({ classId, expiresInHours: expiresInHours ? Number(expiresInHours) : null, maxUses: maxUses ? Number(maxUses) : null, note: note.trim() || null, }); notify.success(t("generateSuccess")); setExpiresInHours(""); setMaxUses(""); setNote(""); onCreated(); onClose(); } catch (err) { notify.error(tCommon("error.loadFailed", { message: String(err) })); } }; if (!open) return null; return ( { if (!o) onClose(); }} > {t("generateWithCustom")} {t("defaultDuration")} · {t("defaultMaxUses")}
setExpiresInHours(e.target.value)} placeholder={t("defaultDuration")} /> setMaxUses(e.target.value)} placeholder={t("defaultMaxUses")} /> setNote(e.target.value)} placeholder={t("customNotePlaceholder")} maxLength={255} />
); }