| | |
| | | readScoreListQuery, |
| | | type ScoreListQuery, |
| | | } from "../scoreQueryParams"; |
| | | import { useSystemSettings } from "../systemSettings"; |
| | | import type { UserMe } from "../App"; |
| | | import "./AttemptReviewPage.css"; |
| | | |
| | | const { useBreakpoint } = Grid; |
| | |
| | | type Q = { |
| | | external_item_id: string; |
| | | part_name?: string; |
| | | item_type?: number; |
| | | item_order?: number; |
| | | title_html?: string; |
| | | detail_json?: string; |
| | |
| | | |
| | | type Attempt = { |
| | | attempt_id: string; |
| | | student_no?: string; |
| | | examinee_name?: string; |
| | | id_card?: string; |
| | | score: number; |
| | |
| | | |
| | | function stripHtml(html: string) { |
| | | return html.replace(/<[^>]+>/g, ""); |
| | | } |
| | | |
| | | /** 判断题答案格式化:T→正确(A),F→错误(B) */ |
| | | function formatAnswer(raw: string | undefined, itemType?: number): string { |
| | | if (!raw) return "—"; |
| | | if (itemType === 320) { |
| | | const s = raw.trim().toUpperCase(); |
| | | if (s === "T" || s === "A") return "正确(A)"; |
| | | if (s === "F" || s === "B") return "错误(B)"; |
| | | } |
| | | return raw; |
| | | } |
| | | |
| | | function isAnswered(q: Q) { |
| | |
| | | attempt_id: string; |
| | | name: string; |
| | | id_card: string; |
| | | student_no: string; |
| | | subject_name: string; |
| | | score: number; |
| | | }; |
| | |
| | | function AttemptRoster({ |
| | | activeAttemptId, |
| | | listQuery, |
| | | hidePrivacy, |
| | | onSelectAttempt, |
| | | onSearch, |
| | | }: { |
| | | activeAttemptId: string; |
| | | listQuery: ScoreListQuery; |
| | | hidePrivacy: boolean; |
| | | onSelectAttempt: (attemptId: string) => void; |
| | | onSearch: (filters: Pick<ScoreListQuery, "name" | "id_card" | "subject">) => void; |
| | | }) { |
| | |
| | | wrapperCol={{ flex: "1 1 0" }} |
| | | onFinish={(v) => onSearch(v)} |
| | | > |
| | | {!hidePrivacy && ( |
| | | <> |
| | | <Form.Item name="name" label="姓名" className="attempt-review__roster-field"> |
| | | <Input allowClear /> |
| | | </Form.Item> |
| | | <Form.Item name="id_card" label="证件号" className="attempt-review__roster-field"> |
| | | <Input allowClear /> |
| | | </Form.Item> |
| | | </> |
| | | )} |
| | | <Form.Item name="subject" label="科目" className="attempt-review__roster-field"> |
| | | <Select allowClear options={subjects.map((s) => ({ value: s, label: s }))} /> |
| | | </Form.Item> |
| | |
| | | if (row.attempt_id !== activeAttemptId) onSelectAttempt(row.attempt_id); |
| | | }} |
| | | > |
| | | <div className="attempt-review__roster-name">{row.name || "—"}</div> |
| | | <div className="attempt-review__roster-name">{hidePrivacy ? row.student_no : (row.name || "—")}</div> |
| | | <div className="attempt-review__roster-meta"> |
| | | <span>{row.score} 分</span> |
| | | <span className="attempt-review__roster-subject">{row.subject_name}</span> |
| | |
| | | const detailScrollRef = useRef<HTMLDivElement>(null); |
| | | const screens = useBreakpoint(); |
| | | const isWide = !!screens.lg; |
| | | const { settings } = useSystemSettings(); |
| | | const [user, setUser] = useState<UserMe | null>(null); |
| | | |
| | | 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)); |
| | | }, []); |
| | | |
| | | const selectAttempt = useCallback( |
| | | (id: string) => { |
| | |
| | | <li key={opt.identifier}>{opt.identifier}. {stripHtml(opt.text)}</li> |
| | | ))} |
| | | </ul> |
| | | <p>考生答案:{q.user_answer || "—"}</p> |
| | | <p>参考答案:{q.reference_answer || "—"}</p> |
| | | <p>考生答案:{formatAnswer(q.user_answer, q.item_type)}</p> |
| | | <p>参考答案:{formatAnswer(q.reference_answer, q.item_type)}</p> |
| | | <p> |
| | | {q.is_correct ? <Tag color="success">正确</Tag> : <Tag color="error">错误</Tag>} |
| | | 得分:{q.score} / {q.max_score ?? 1} |
| | |
| | | {data && ( |
| | | <Card className="attempt-review__header" size="small"> |
| | | <Descriptions column={{ xs: 1, sm: 2 }} size="small"> |
| | | <Descriptions.Item label="考生编号">{data.student_no}</Descriptions.Item> |
| | | {!hidePrivacy && ( |
| | | <> |
| | | <Descriptions.Item label="姓名">{data.examinee_name}</Descriptions.Item> |
| | | <Descriptions.Item label="证件号">{data.id_card}</Descriptions.Item> |
| | | </> |
| | | )} |
| | | <Descriptions.Item label="科目">{data.subject_name}</Descriptions.Item> |
| | | <Descriptions.Item label="总分">{data.score} / {data.paper_total_score}</Descriptions.Item> |
| | | <Descriptions.Item label="试卷">{data.paper_name}</Descriptions.Item> |
| | |
| | | <AttemptRoster |
| | | activeAttemptId={attemptId} |
| | | listQuery={listQuery} |
| | | hidePrivacy={hidePrivacy} |
| | | onSelectAttempt={selectAttempt} |
| | | onSearch={searchRoster} |
| | | /> |