From 3f96c9c6502243b06efd4bb34a189e72e3a5557c Mon Sep 17 00:00:00 2001
From: yearning <10538594+wangweifeng1999@user.noreply.gitee.com>
Date: 星期一, 28 九月 2026 19:24:55 +0800
Subject: [PATCH] 复制项目
---
frontend/src/pages/AdminImportPage.tsx | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 163 insertions(+), 0 deletions(-)
diff --git a/frontend/src/pages/AdminImportPage.tsx b/frontend/src/pages/AdminImportPage.tsx
new file mode 100644
index 0000000..86e0834
--- /dev/null
+++ b/frontend/src/pages/AdminImportPage.tsx
@@ -0,0 +1,163 @@
+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> 鏂囦欢锛孶TF-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}锛宔xam_id锛�${ids.join("銆�") || "鈥�"}銆傚皢鍒犻櫎璇ユ壒娆′笅鍏ㄩ儴鍦烘涓庢垚缁╋紝涓嶅彲鎭㈠銆俙,
+ onOk: async () => {
+ await api.delete(`/api/admin/import/batches/${row.id}`);
+ message.success("宸叉竻闄�");
+ load();
+ },
+ });
+ }}
+ >
+ 娓呴櫎
+ </Button>
+ );
+ },
+ },
+ ]}
+ />
+ </Card>
+ </div>
+ );
+}
--
Gitblit v1.8.0