| | |
| | | import { useEffect, useRef, useState } from "react"; |
| | | import { Button, Card, Form, Input, Select, Table } from "antd"; |
| | | import { Button, Card, Form, Input, message, Select, Table } from "antd"; |
| | | import { useNavigate, useSearchParams } from "react-router-dom"; |
| | | import { api } from "../api"; |
| | | import { |
| | |
| | | writeScoreListQuery, |
| | | type ScoreListQuery, |
| | | } from "../scoreQueryParams"; |
| | | import { useSystemSettings } from "../systemSettings"; |
| | | import type { UserMe } from "../App"; |
| | | import "./ScoreQueryPage.css"; |
| | | |
| | | type ScoreRow = { |
| | | examinee_id: string; |
| | | name: string; |
| | | id_card: string; |
| | | student_no: string; |
| | | name: string | null; |
| | | id_card: string | null; |
| | | subject_name: string; |
| | | score: number; |
| | | attempt_id: string; |
| | |
| | | subject: query.subject, |
| | | }); |
| | | const [tableScrollY, setTableScrollY] = useState(320); |
| | | const [user, setUser] = useState<UserMe | null>(null); |
| | | const { settings } = useSystemSettings(); |
| | | |
| | | const hidePrivacy = |
| | | user?.role === "grader" && !settings.grader_can_see_privacy; |
| | | |
| | | useEffect(() => { |
| | | api |
| | | .get<UserMe>("/api/auth/me") |
| | | .then((r) => setUser(r.data)) |
| | | .catch(() => setUser(null)); |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | api.get<{ items: string[] }>("/api/subjects").then((r) => setSubjects(r.data.items)); |
| | |
| | | }, [searchParams, form]); |
| | | |
| | | const load = async () => { |
| | | const { data: res } = await api.get("/api/scores", { |
| | | params: { page, page_size: pageSize, ...filters }, |
| | | }); |
| | | const params: Record<string, string | number> = { page, page_size: pageSize }; |
| | | if (!hidePrivacy) { |
| | | if (filters.name) params.name = filters.name; |
| | | if (filters.id_card) params.id_card = filters.id_card; |
| | | } |
| | | if (filters.subject) params.subject = filters.subject; |
| | | const { data: res } = await api.get("/api/scores", { params }); |
| | | setData(res.items); |
| | | setTotal(res.total); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | load(); |
| | | }, [page, pageSize, filters]); |
| | | }, [page, pageSize, filters, hidePrivacy]); |
| | | |
| | | useEffect(() => { |
| | | const el = tableWrapRef.current; |
| | |
| | | setSearchParams(writeScoreListQuery(q), { replace: true }); |
| | | }; |
| | | |
| | | const [studentNo, setStudentNo] = useState(""); |
| | | const handleLookup = async () => { |
| | | if (!studentNo.trim()) return; |
| | | try { |
| | | const { data } = await api.get<{ attempt_id: string }>( |
| | | `/api/attempts/lookup/${encodeURIComponent(studentNo.trim().toUpperCase())}` |
| | | ); |
| | | nav(attemptDetailPath(data.attempt_id, listQuery())); |
| | | } catch { |
| | | message.error("考生编号不存在"); |
| | | } |
| | | }; |
| | | |
| | | const columns = [ |
| | | { |
| | | title: "序号", |
| | | width: 64, |
| | | fixed: "left" as const, |
| | | render: (_: unknown, __: unknown, index: number) => (page - 1) * pageSize + index + 1, |
| | | }, |
| | | { title: "考生编号", dataIndex: "student_no", width: 180, fixed: "left" as const }, |
| | | ...(hidePrivacy |
| | | ? [] |
| | | : [ |
| | | { 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: string | null) => v || "—" }, |
| | | { title: "结束考试时间", dataIndex: "end_time", width: 170, render: (v: string | null) => v || "—" }, |
| | | { title: "成绩", dataIndex: "score", width: 80 }, |
| | | { |
| | | title: "操作", |
| | | width: 80, |
| | | fixed: "right" as const, |
| | | render: (_: unknown, row: ScoreRow) => ( |
| | | <Button |
| | | type="link" |
| | | onClick={() => nav(attemptDetailPath(row.attempt_id, listQuery()))} |
| | | > |
| | | 查看 |
| | | </Button> |
| | | ), |
| | | }, |
| | | ]; |
| | | |
| | | return ( |
| | | <div className="score-query-page"> |
| | | <Card bordered className="score-query-page__card"> |
| | |
| | | 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> |
| | | {!hidePrivacy && ( |
| | | <> |
| | | <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 |
| | |
| | | <Button type="primary" htmlType="submit">查询</Button> |
| | | </Form.Item> |
| | | </Form> |
| | | <div style={{ marginBottom: 12, display: "flex", gap: 8, alignItems: "center" }}> |
| | | <Input |
| | | placeholder="输入考生编号直接查看答卷" |
| | | value={studentNo} |
| | | onChange={(e) => setStudentNo(e.target.value.toUpperCase())} |
| | | onPressEnter={handleLookup} |
| | | style={{ width: 200, fontFamily: "monospace" }} |
| | | maxLength={10} |
| | | /> |
| | | <Button onClick={handleLookup} disabled={!studentNo.trim()}>查看答卷</Button> |
| | | </div> |
| | | <div ref={tableWrapRef} className="score-query-page__table-wrap"> |
| | | <Table |
| | | rowKey="attempt_id" |
| | | scroll={{ x: 1100, y: tableScrollY }} |
| | | scroll={{ x: hidePrivacy ? 800 : 1100, y: tableScrollY }} |
| | | dataSource={data} |
| | | pagination={{ |
| | | current: page, |
| | |
| | | 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> |
| | | ), |
| | | }, |
| | | ]} |
| | | columns={columns} |
| | | /> |
| | | </div> |
| | | </Card> |