"use client" import type { JSX } from "react" import { useRef, useState } from "react" import { Upload, Download, FileSpreadsheet, Loader2, AlertCircle, CheckCircle2 } from "lucide-react" import { toast } from "sonner" import { useTranslations } from "next-intl" import { Button } from "@/shared/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/shared/components/ui/dialog" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/shared/components/ui/select" import { Label } from "@/shared/components/ui/label" import { Input } from "@/shared/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/shared/components/ui/table" import { downloadBase64File } from "@/shared/lib/download" import { safeActionCall } from "@/shared/lib/action-utils" import { downloadGradeImportTemplateAction, importGradesFromExcelAction, } from "../actions-import" import type { GradeImportResult } from "../import-export" interface ExcelImportDialogProps { /** 班级 ID(必填) */ classId: string /** 班级列表(供切换) */ classes: Array<{ id: string; name: string }> /** 科目列表 */ subjects: Array<{ id: string; name: string }> /** 触发按钮渲染函数 */ trigger?: (open: () => void) => JSX.Element /** 导入成功后的回调(通常用于刷新列表) */ onSuccess?: () => void } interface ImportFormState { classId: string subjectId: string title: string examId: string fullScore: string type: string semester: string } const INITIAL_FORM: ImportFormState = { classId: "", subjectId: "", title: "", examId: "", fullScore: "100", type: "exam", semester: "1", } /** * P3-10: 成绩 Excel 批量导入对话框。 * * 工作流程: * 1. 选择班级 → 自动下载模板(含学生姓名示例) * 2. 填写参数(科目、评估标题、满分等) * 3. 上传填好的 Excel 文件 * 4. 点击"开始导入"调用 Server Action * 5. 显示导入结果(成功数 / 失败数 / 失败明细) */ export function ExcelImportDialog({ classId: initialClassId, classes, subjects, trigger, onSuccess, }: ExcelImportDialogProps): JSX.Element { const t = useTranslations("grades") const [isOpen, setIsOpen] = useState(false) const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false) const [isImporting, setIsImporting] = useState(false) const [file, setFile] = useState(null) const [form, setForm] = useState({ ...INITIAL_FORM, classId: initialClassId, subjectId: subjects[0]?.id ?? "", }) const [result, setResult] = useState(null) const fileInputRef = useRef(null) const reset = () => { setForm({ ...INITIAL_FORM, classId: initialClassId, subjectId: subjects[0]?.id ?? "" }) setFile(null) setResult(null) if (fileInputRef.current) { fileInputRef.current.value = "" } } const handleOpenChange = (open: boolean) => { setIsOpen(open) if (!open) { reset() } } const handleDownloadTemplate = async () => { if (!form.classId) { toast.error(t("excelImport.errorSelectClass")) return } setIsDownloadingTemplate(true) const r = await safeActionCall( () => downloadGradeImportTemplateAction({ classId: form.classId }), { onError: () => toast.error(t("excelImport.errorTemplateDownload")), onFinally: () => setIsDownloadingTemplate(false), } ) if (r?.success && r.data) { downloadBase64File(r.data.buffer, r.data.filename) toast.success(t("excelImport.templateDownloaded")) } else if (r) { toast.error(r.message ?? t("excelImport.errorTemplateDownload")) } } const handleFileChange = (e: React.ChangeEvent) => { const f = e.target.files?.[0] ?? null setFile(f) setResult(null) } const handleSubmit = async () => { if (!file) { toast.error(t("excelImport.errorNoFile")) return } if (!form.classId || !form.subjectId || !form.title) { toast.error(t("excelImport.errorMissingFields")) return } setIsImporting(true) const formData = new FormData() formData.append("file", file) formData.append("classId", form.classId) formData.append("subjectId", form.subjectId) formData.append("title", form.title) if (form.examId) formData.append("examId", form.examId) if (form.fullScore) formData.append("fullScore", form.fullScore) if (form.type) formData.append("type", form.type) if (form.semester) formData.append("semester", form.semester) const r = await safeActionCall( () => importGradesFromExcelAction(null, formData), { onError: () => toast.error(t("excelImport.errorImport")), onFinally: () => setIsImporting(false), } ) if (r?.success && r.data) { setResult(r.data) if (r.data.failedCount === 0) { toast.success(t("excelImport.successAllImported", { count: r.data.successCount })) } else { toast.warning(t("excelImport.successPartial", { success: r.data.successCount, failed: r.data.failedCount, })) } onSuccess?.() } else if (r) { toast.error(r.message ?? t("excelImport.errorImport")) if (r.data) { setResult(r.data) } } } return ( {trigger ? ( {trigger(() => setIsOpen(true))} ) : ( )} {t("excelImport.dialogDescription")}
{/* 第一步:下载模板 */}

{t("excelImport.step1Title")}

{t("excelImport.step1Description")}

{/* 第二步:填写参数 */}

{t("excelImport.step2Title")}

setForm({ ...form, title: e.target.value })} placeholder={t("excelImport.assessmentTitlePlaceholder")} />
setForm({ ...form, fullScore: e.target.value })} />
setForm({ ...form, examId: e.target.value })} placeholder={t("excelImport.examIdPlaceholder")} />
{/* 第三步:上传文件 */}

{t("excelImport.step3Title")}

{t("excelImport.fileHint")}

{/* 导入结果 */} {result ? (

{result.failedCount === 0 ? (

{t("excelImport.resultSuccess")} {result.successCount}
{t("excelImport.resultFailed")} {result.failedCount}
{result.unmatchedStudents.length > 0 ? (

{t("excelImport.unmatchedStudents")}

{result.unmatchedStudents.join(", ")}

) : null} {result.invalidRows.length > 0 ? (
{t("excelImport.columnRow")} {t("excelImport.columnStudentName")} {t("excelImport.columnScore")} {t("excelImport.columnErrors")} {result.invalidRows.slice(0, 20).map((row) => ( {row.row} {row.studentName || "—"} {row.score} {row.errors.join("; ")} ))} {result.invalidRows.length > 20 ? ( {t("excelImport.moreErrors", { count: result.invalidRows.length - 20, })} ) : null}
) : null}
) : null}
) }