From e3e6db2d9974588f91ac1567a58fa4846655ce1b Mon Sep 17 00:00:00 2001
From: yearning <10538594+wangweifeng1999@user.noreply.gitee.com>
Date: 星期三, 30 九月 2026 12:54:55 +0800
Subject: [PATCH] 1.判断题优化;2.新增考评员是否能查看考生隐私开关

---
 frontend/src/pages/AttemptReviewPage.tsx |   61 +++++++++++++++++++++++++-----
 1 files changed, 50 insertions(+), 11 deletions(-)

diff --git a/frontend/src/pages/AttemptReviewPage.tsx b/frontend/src/pages/AttemptReviewPage.tsx
index 7e503ff..23e6503 100644
--- a/frontend/src/pages/AttemptReviewPage.tsx
+++ b/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, "");
+}
+
+/** 鍒ゆ柇棰樼瓟妗堟牸寮忓寲锛歍鈫掓纭紙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 "姝g‘锛圓锛�";
+    if (s === "F" || s === "B") return "閿欒锛圔锛�";
+  }
+  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)}
       >
-        <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>
+        {!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">姝g‘</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.examinee_name}</Descriptions.Item>
-            <Descriptions.Item label="璇佷欢鍙�">{data.id_card}</Descriptions.Item>
+            <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}
             />

--
Gitblit v1.8.0