import { useEffect, useState } from "react";
|
import { Alert, Button, Card, Form, Input, Table, Typography, Upload, message, Modal, Tag } from "antd";
|
import { UploadOutlined } from "@ant-design/icons";
|
import { api } from "../api";
|
|
type Batch = {
|
id: number;
|
subject_name: string;
|
exam_id: number;
|
exam_ids: number[];
|
zip_filename: string;
|
imported_at: string;
|
status: string;
|
attempt_count: number;
|
paper_name?: string;
|
};
|
|
export default function AdminImportPage() {
|
const [batches, setBatches] = useState<Batch[]>([]);
|
const [file, setFile] = useState<File | null>(null);
|
const [loading, setLoading] = useState(false);
|
|
const load = () => api.get<{ items: Batch[] }>("/api/admin/import/batches").then((r) => setBatches(r.data.items));
|
|
useEffect(() => {
|
load();
|
}, []);
|
|
return (
|
<div>
|
<Card title="导入数据" style={{ marginBottom: 16 }}>
|
<Alert
|
type="info"
|
showIcon
|
style={{ marginBottom: 16 }}
|
message="导入要求说明"
|
description={
|
<Typography.Paragraph style={{ marginBottom: 0 }}>
|
<ul style={{ margin: "8px 0 0", paddingLeft: 20 }}>
|
<li>请上传 <strong>ZIP 压缩包</strong>,内含导出 SQL(仅 <code>.sql</code> 文件,UTF-8 编码)。</li>
|
<li>
|
必须包含 5 类数据(文件名需能识别,推荐与样例一致):
|
<ul>
|
<li>考生:<code>examination_examinee</code>(勿与答案表混淆)</li>
|
<li>考试考生 / 成绩:<code>examination_exam_examinee</code></li>
|
<li>试卷:<code>examination_paper_source</code></li>
|
<li>试题明细:<code>examination_paper_item</code></li>
|
<li>考生答案:<code>examination_examinee_exam_answer</code></li>
|
</ul>
|
</li>
|
<li>
|
一个 ZIP 可包含<strong>多个 exam_id</strong>(同卷多场次):ZIP 内须为<strong>同一份试卷</strong>(相同{" "}
|
<code>external_paper_id + revision</code>),系统将共用一套试题判分。
|
</li>
|
<li>
|
若 ZIP 内含<strong>多份不同试卷</strong>,须额外提供 <code>examination_exam</code> SQL,用{" "}
|
<code>paper_source_id</code> 标明每场考试使用的试卷;否则无法导入。
|
</li>
|
<li>
|
每个 <code>exam_id</code> 在全库仅能导入一次;已存在的试卷(相同{" "}
|
<code>external_paper_id + revision</code>)会复用题库,仅新增场次与成绩。
|
</li>
|
<li>
|
<strong>科目名称</strong>由管理员填写,用于成绩列表筛选;清除请用下方「导入历史」按批次操作(会删除该 ZIP 对应的全部场次数据)。
|
</li>
|
<li>导入后将按试题明细与答案自动判分,并以明细汇总结果更新总分(可与原 SQL 中 score 对照)。</li>
|
</ul>
|
</Typography.Paragraph>
|
}
|
/>
|
<Form
|
layout="vertical"
|
onFinish={async (v) => {
|
if (!file) {
|
message.warning("请选择 ZIP 文件");
|
return;
|
}
|
const fd = new FormData();
|
fd.append("subject_name", v.subject_name);
|
fd.append("file", file);
|
setLoading(true);
|
try {
|
const { data } = await api.post("/api/admin/import", fd);
|
message.success("导入成功");
|
Modal.info({ title: "导入报告", content: JSON.stringify(data.report, null, 2), width: 600 });
|
setFile(null);
|
load();
|
} catch (e: unknown) {
|
const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || "导入失败";
|
message.error(msg);
|
} finally {
|
setLoading(false);
|
}
|
}}
|
>
|
<Form.Item name="subject_name" label="科目名称" rules={[{ required: true }]}>
|
<Input placeholder="导入后用于成绩列表科目筛选" />
|
</Form.Item>
|
<Form.Item label="ZIP 文件" required>
|
<Upload
|
beforeUpload={(f) => {
|
setFile(f);
|
return false;
|
}}
|
maxCount={1}
|
onRemove={() => setFile(null)}
|
>
|
<Button icon={<UploadOutlined />}>选择文件</Button>
|
</Upload>
|
</Form.Item>
|
<Button type="primary" htmlType="submit" loading={loading}>开始导入</Button>
|
</Form>
|
</Card>
|
<Card title="导入历史">
|
<Table
|
rowKey="id"
|
scroll={{ x: 960 }}
|
dataSource={batches}
|
columns={[
|
{ title: "科目", dataIndex: "subject_name" },
|
{
|
title: "exam_id",
|
dataIndex: "exam_ids",
|
render: (ids: number[] | undefined, row) => {
|
const list = ids?.length ? ids : row.exam_id != null ? [row.exam_id] : [];
|
return list.map((id) => <Tag key={id}>{id}</Tag>);
|
},
|
},
|
{ title: "试卷", dataIndex: "paper_name", ellipsis: true },
|
{ title: "文件", dataIndex: "zip_filename", ellipsis: true },
|
{ title: "导入时间", dataIndex: "imported_at" },
|
{ title: "考生数", dataIndex: "attempt_count" },
|
{
|
title: "操作",
|
render: (_, row) => {
|
const ids = row.exam_ids?.length ? row.exam_ids : row.exam_id != null ? [row.exam_id] : [];
|
return (
|
<Button
|
danger
|
type="link"
|
onClick={() => {
|
Modal.confirm({
|
title: "清除本次导入的全部数据?",
|
content: `科目:${row.subject_name},exam_id:${ids.join("、") || "—"}。将删除该批次下全部场次与成绩,不可恢复。`,
|
onOk: async () => {
|
await api.delete(`/api/admin/import/batches/${row.id}`);
|
message.success("已清除");
|
load();
|
},
|
});
|
}}
|
>
|
清除
|
</Button>
|
);
|
},
|
},
|
]}
|
/>
|
</Card>
|
</div>
|
);
|
}
|