yearning
19 小时以前 e3e6db2d9974588f91ac1567a58fa4846655ce1b
frontend/src/pages/AttemptReviewPage.tsx
@@ -20,6 +20,8 @@
  readScoreListQuery,
  type ScoreListQuery,
} from "../scoreQueryParams";
import { useSystemSettings } from "../systemSettings";
import type { UserMe } from "../App";
import "./AttemptReviewPage.css";
const { useBreakpoint } = Grid;
@@ -30,6 +32,7 @@
type Q = {
  external_item_id: string;
  part_name?: string;
  item_type?: number;
  item_order?: number;
  title_html?: string;
  detail_json?: string;
@@ -44,6 +47,7 @@
type Attempt = {
  attempt_id: string;
  student_no?: string;
  examinee_name?: string;
  id_card?: string;
  score: number;
@@ -61,6 +65,17 @@
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) {
@@ -197,6 +212,7 @@
  attempt_id: string;
  name: string;
  id_card: string;
  student_no: string;
  subject_name: string;
  score: number;
};
@@ -206,11 +222,13 @@
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;
}) {
@@ -303,12 +321,16 @@
        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>
@@ -330,7 +352,7 @@
              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>
@@ -359,6 +381,17 @@
  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) => {
@@ -491,8 +524,8 @@
                    <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}
@@ -515,8 +548,13 @@
      {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>
@@ -562,6 +600,7 @@
            <AttemptRoster
              activeAttemptId={attemptId}
              listQuery={listQuery}
              hidePrivacy={hidePrivacy}
              onSelectAttempt={selectAttempt}
              onSearch={searchRoster}
            />