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/ScoreQueryPage.tsx |  169 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 169 insertions(+), 0 deletions(-)

diff --git a/frontend/src/pages/ScoreQueryPage.tsx b/frontend/src/pages/ScoreQueryPage.tsx
new file mode 100644
index 0000000..e3f3141
--- /dev/null
+++ b/frontend/src/pages/ScoreQueryPage.tsx
@@ -0,0 +1,169 @@
+import { useEffect, useRef, useState } from "react";
+import { Button, Card, Form, Input, Select, Table } from "antd";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { api } from "../api";
+import {
+  attemptDetailPath,
+  readScoreListQuery,
+  writeScoreListQuery,
+  type ScoreListQuery,
+} from "../scoreQueryParams";
+import "./ScoreQueryPage.css";
+
+type ScoreRow = {
+  examinee_id: string;
+  name: string;
+  id_card: string;
+  subject_name: string;
+  score: number;
+  attempt_id: string;
+  start_time?: string | null;
+  end_time?: string | null;
+};
+
+export default function ScoreQueryPage() {
+  const nav = useNavigate();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const query = readScoreListQuery(searchParams);
+  const [form] = Form.useForm<ScoreListQuery>();
+  const tableWrapRef = useRef<HTMLDivElement>(null);
+  const [subjects, setSubjects] = useState<string[]>([]);
+  const [data, setData] = useState<ScoreRow[]>([]);
+  const [total, setTotal] = useState(0);
+  const [page, setPage] = useState(query.page ?? 1);
+  const [pageSize, setPageSize] = useState(query.page_size ?? 20);
+  const [filters, setFilters] = useState<Pick<ScoreListQuery, "name" | "id_card" | "subject">>({
+    name: query.name,
+    id_card: query.id_card,
+    subject: query.subject,
+  });
+  const [tableScrollY, setTableScrollY] = useState(320);
+
+  useEffect(() => {
+    api.get<{ items: string[] }>("/api/subjects").then((r) => setSubjects(r.data.items));
+  }, []);
+
+  useEffect(() => {
+    const q = readScoreListQuery(searchParams);
+    setPage(q.page ?? 1);
+    setPageSize(q.page_size ?? 20);
+    setFilters({ name: q.name, id_card: q.id_card, subject: q.subject });
+    form.setFieldsValue({
+      name: q.name,
+      id_card: q.id_card,
+      subject: q.subject,
+    });
+  }, [searchParams, form]);
+
+  const load = async () => {
+    const { data: res } = await api.get("/api/scores", {
+      params: { page, page_size: pageSize, ...filters },
+    });
+    setData(res.items);
+    setTotal(res.total);
+  };
+
+  useEffect(() => {
+    load();
+  }, [page, pageSize, filters]);
+
+  useEffect(() => {
+    const el = tableWrapRef.current;
+    if (!el) return;
+    const update = () => {
+      const h = el.clientHeight;
+      setTableScrollY(Math.max(120, h - 56 - 52));
+    };
+    update();
+    const ro = new ResizeObserver(update);
+    ro.observe(el);
+    return () => ro.disconnect();
+  }, []);
+
+  const listQuery = (): ScoreListQuery => ({
+    ...filters,
+    page,
+    page_size: pageSize,
+  });
+
+  const pushQuery = (q: ScoreListQuery) => {
+    setSearchParams(writeScoreListQuery(q), { replace: true });
+  };
+
+  return (
+    <div className="score-query-page">
+      <Card bordered className="score-query-page__card">
+        <Form
+          form={form}
+          layout="inline"
+          className="score-query-page__form"
+          onFinish={(v) => {
+            pushQuery({ ...v, page: 1, page_size: pageSize });
+          }}
+        >
+          <Form.Item name="name" label="濮撳悕">
+            <Input allowClear style={{ width: 120 }} />
+          </Form.Item>
+          <Form.Item name="id_card" label="璇佷欢鍙�">
+            <Input allowClear style={{ width: 180 }} />
+          </Form.Item>
+          <Form.Item name="subject" label="绉戠洰">
+            <Select
+              allowClear
+              style={{ width: 220 }}
+              options={subjects.map((s) => ({ value: s, label: s }))}
+            />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">鏌ヨ</Button>
+          </Form.Item>
+        </Form>
+        <div ref={tableWrapRef} className="score-query-page__table-wrap">
+          <Table
+            rowKey="attempt_id"
+            scroll={{ x: 1100, y: tableScrollY }}
+            dataSource={data}
+            pagination={{
+              current: page,
+              pageSize,
+              total,
+              showSizeChanger: true,
+              showTotal: (t) => `鍏� ${t} 鏉,
+              position: ["bottomRight"],
+              onChange: (p, ps) => {
+                pushQuery({ ...filters, page: p, page_size: ps });
+              },
+            }}
+            columns={[
+              {
+                title: "搴忓彿",
+                width: 64,
+                fixed: "left",
+                render: (_, __, index) => (page - 1) * pageSize + index + 1,
+              },
+              { title: "濮撳悕", dataIndex: "name", width: 100 },
+              { title: "璇佷欢鍙�", dataIndex: "id_card", width: 180 },
+              { title: "绉戠洰", dataIndex: "subject_name", width: 200, ellipsis: true },
+              { title: "寮�濮嬭�冭瘯鏃堕棿", dataIndex: "start_time", width: 170, render: (v) => v || "鈥�" },
+              { title: "缁撴潫鑰冭瘯鏃堕棿", dataIndex: "end_time", width: 170, render: (v) => v || "鈥�" },
+              { title: "鎴愮哗", dataIndex: "score", width: 80 },
+              {
+                title: "鎿嶄綔",
+                width: 80,
+                fixed: "right",
+                render: (_, row) => (
+                  <Button
+                    type="link"
+                    onClick={() => nav(attemptDetailPath(row.attempt_id, listQuery()))}
+                  >
+                    鏌ョ湅
+                  </Button>
+                ),
+              },
+            ]}
+          />
+        </div>
+      </Card>
+    </div>
+  );
+}

--
Gitblit v1.8.0