Răsfoiți Sursa

轨迹图修改

zhangjie 1 an în urmă
părinte
comite
476c18b581

+ 9 - 1
src/api/studentTrackPage.ts

@@ -1,5 +1,5 @@
 import { httpApp } from "@/plugins/axiosApp";
-import { Task } from "@/types";
+import { Task, CardData } from "@/types";
 
 /** 查看单个学生的试卷轨迹 */
 export async function getSingleStudentTaskOfStudentTrack(studentId: string) {
@@ -9,3 +9,11 @@ export async function getSingleStudentTaskOfStudentTrack(studentId: string) {
     { params: { studentId } }
   );
 }
+/** 查看单个学生的试卷的题卡 */
+export async function getSingleStudentCardData(studentId: string) {
+  return httpApp.post<CardData>(
+    "/api/admin/mark/track/getCard",
+    {},
+    { params: { studentId } }
+  );
+}

+ 282 - 2
src/features/student/studentInspect/MarkBody.vue

@@ -24,6 +24,32 @@
             :dx="0"
             :dy="0"
           />
+          <!-- 客观题答案标记 -->
+          <template v-if="item.answerTags">
+            <div
+              v-for="(tag, tindex) in item.answerTags"
+              :key="`tag-${tindex}`"
+              :style="tag.style"
+            >
+              {{ tag.answer }}
+            </div>
+          </template>
+          <!-- 试题评分明细 -->
+          <template v-if="item.markDetail">
+            <div
+              v-for="(minfo, mindex) in item.markDetail"
+              :key="`mark-${mindex}`"
+              :style="minfo.style"
+              class="mark-info"
+            >
+              <div>
+                <p v-for="user in minfo.users" :key="user.userId">
+                  评卷员:{{ user.userName }},评分:{{ user.score }}
+                </p>
+              </div>
+              <h3>得分:{{ minfo.score }},满分:{{ minfo.maxScore }}</h3>
+            </div>
+          </template>
           <hr class="image-seperator" />
         </div>
       </div>
@@ -36,10 +62,11 @@
 import { reactive, watch } from "vue";
 import { store } from "@/store/store";
 import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
-import type { SpecialTag, Track, ColorMap } from "@/types";
+import type { SpecialTag, Track, ColorMap, PaperRecogData } from "@/types";
 import { useTimers } from "@/setups/useTimers";
-import { loadImage, addHeaderTrackColorAttr } from "@/utils/utils";
+import { loadImage, addHeaderTrackColorAttr, calcSum } from "@/utils/utils";
 import { dragImage } from "@/features/mark/use/draggable";
+import { maxNum } from "@/utils/utils";
 
 interface SliceImage {
   url: string;
@@ -48,6 +75,8 @@ interface SliceImage {
   originalImageWidth: number;
   originalImageHeight: number;
   width: string; // 图片在整个图片列表里面的宽度比例
+  answerTags?: AnswerTagItem[];
+  markDetail?: MarkDetailItem[];
 }
 
 const { origImageUrls = "sliceUrls" } = defineProps<{
@@ -134,6 +163,9 @@ async function processImage() {
 
   maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
 
+  // 解析各试题答题区域以及评分
+  const markDetailList = parseMarkDetailList();
+
   const trackLists = (store.currentTask.questionList || [])
     // .map((q) => q.trackList)
     .map((q) => {
@@ -163,6 +195,7 @@ async function processImage() {
             (t) => t.offsetIndex === indexInSliceUrls
           )
         );
+    const answerTags = paserRecogData(image, indexInSliceUrls - 1);
 
     sliceImagesWithTrackList.push({
       url,
@@ -171,10 +204,242 @@ async function processImage() {
       originalImageWidth: image.naturalWidth,
       originalImageHeight: image.naturalHeight,
       width: (image.naturalWidth / maxImageWidth) * 100 + "%",
+      answerTags,
+      markDetail: markDetailList[indexInSliceUrls - 1],
     });
   }
 }
 
+// 解析客观题答案展示位置
+interface AnswerTagItem {
+  mainNumber: number;
+  subNumber: string;
+  answer: string;
+  style: Record<string, string>;
+}
+function paserRecogData(imgDom: HTMLImageElement, imageIndex): AnswerTagItem[] {
+  if (
+    !store.currentTask.recogDatas?.length ||
+    !store.currentTask.recogDatas[imageIndex]
+  )
+    return [];
+
+  const answerMap = store.currentTask.answerMap || {};
+  const { naturalWidth, naturalHeight } = imgDom;
+  const recogData: PaperRecogData = JSON.parse(
+    window.atob(store.currentTask.recogDatas[imageIndex])
+  );
+  const answerTags: AnswerTagItem[] = [];
+  // const optionsBlocks = [];
+  recogData.question.forEach((question) => {
+    question.fill_result.forEach((result) => {
+      const tagSize = result.fill_size[1];
+      const fillPositions = result.fill_position.map((pos) => {
+        return pos.split(",").map((n) => n * 1);
+      });
+
+      const offsetLt = result.fill_size.map((item) => item * 0.4);
+      const tagLeft =
+        maxNum(fillPositions.map((pos) => pos[0])) +
+        result.fill_size[0] -
+        offsetLt[0];
+      const tagTop = fillPositions[0][1] - offsetLt[1];
+
+      answerTags.push({
+        mainNumber: result.main_number,
+        subNumber: result.sub_number,
+        answer: answerMap[`${result.main_number}_${result.sub_number}`],
+        style: {
+          height: ((100 * tagSize) / naturalHeight).toFixed(4) + "%",
+          fontSize: ((100 * 20) / tagSize).toFixed(4) + "%",
+          left: ((100 * tagLeft) / naturalWidth).toFixed(4) + "%",
+          top: ((100 * tagTop) / naturalHeight).toFixed(4) + "%",
+          position: "absolute",
+          color: "#f53f3f",
+          lineHeight: 1,
+          zIndex: 9,
+        },
+      });
+
+      // 测试:选项框
+      // fillPositions.forEach((fp, index) => {
+      //   optionsBlocks.push({
+      //     mainNumber: result.main_number,
+      //     subNumber: result.sub_number,
+      //     filled: !!result.fill_option[index],
+      //     style: {
+      //       width:
+      //         ((100 * result.fill_size[0]) / naturalWidth).toFixed(4) + "%",
+      //       height:
+      //         ((100 * result.fill_size[1]) / naturalHeight).toFixed(4) + "%",
+      //       left:
+      //         ((100 * (fp[0] - offsetLt[0])) / naturalWidth).toFixed(4) + "%",
+      //       top:
+      //         ((100 * (fp[1] - offsetLt[1])) / naturalHeight).toFixed(4) + "%",
+      //       position: "absolute",
+      //       border: "1px solid #f53f3f",
+      //       background: result.fill_option[index]
+      //         ? "rgba(245, 63, 63, 0.5)"
+      //         : "transparent",
+      //       zIndex: 9,
+      //     },
+      //   });
+      // });
+    });
+  });
+
+  return answerTags;
+}
+
+interface QuestionItem {
+  mainNumber: number;
+  subNumber: number | string;
+}
+interface QuestionArea {
+  i: number;
+  x: number;
+  y: number;
+  w: number;
+  h: number;
+  qStruct: string;
+}
+function parseQuestionAreas(questions: QuestionItem[]) {
+  if (!questions.length || !store.currentTask.cardData?.length) return [];
+
+  let pictureConfigs: QuestionArea[] = [];
+  const structs = questions.map(
+    (item) => `${item.mainNumber}_${item.subNumber}`
+  );
+  store.currentTask.cardData.forEach((page, pindex) => {
+    page.exchange.answer_area.forEach((area) => {
+      const [x, y, w, h] = area.area;
+      const qStruct = `${area.main_number}_${area.sub_number}`;
+
+      const pConfig: QuestionArea = {
+        i: pindex + 1,
+        x,
+        y,
+        w,
+        h,
+        qStruct,
+      };
+
+      if (typeof area.sub_number === "number") {
+        if (!structs.includes(qStruct)) return;
+        pictureConfigs.push(pConfig);
+        return;
+      }
+      // 复合区域处理,比如填空题,多个小题合并为一个区域
+      if (typeof area.sub_number === "string") {
+        const areaStructs = area.sub_number
+          .split(",")
+          .map((subNumber) => `${area.main_number}_${subNumber}`);
+        if (
+          structs.some((struct) => areaStructs.includes(struct)) &&
+          !pictureConfigs.find((item) => item.qStruct === qStruct)
+        ) {
+          pictureConfigs.push(pConfig);
+        }
+      }
+    });
+  });
+  // console.log(pictureConfigs);
+
+  // 合并相邻区域
+  pictureConfigs.sort((a, b) => {
+    return a.i - b.i || a.x - b.x || a.y - b.y;
+  });
+  let combinePictureConfigList: QuestionArea[] = [];
+  let prevConfig = null;
+  pictureConfigs.forEach((item, index) => {
+    if (!index) {
+      prevConfig = { ...item };
+      combinePictureConfigList.push(prevConfig);
+      return;
+    }
+
+    const elasticRate = 0.01;
+    if (
+      prevConfig.i === item.i &&
+      prevConfig.y + prevConfig.h + elasticRate >= item.y &&
+      prevConfig.w === item.w &&
+      prevConfig.x === item.x
+    ) {
+      prevConfig.h = item.y + item.h - prevConfig.y;
+    } else {
+      prevConfig = { ...item };
+      combinePictureConfigList.push(prevConfig);
+    }
+  });
+  // console.log(combinePictureConfigList);
+  return combinePictureConfigList;
+}
+
+// 解析各试题答题区域以及评分
+interface MarkDetailItem {
+  mainNumber: number;
+  subNumber: string;
+  score: number;
+  maxScore: number;
+  users: Array<{
+    userId: string;
+    userName: string;
+    scores: number[];
+    score: number;
+  }>;
+  area: QuestionArea;
+  style: Record<string, string>;
+}
+function parseMarkDetailList(): Array<MarkDetailItem[]> {
+  const dataList: Array<MarkDetailItem[]> = [];
+
+  (store.currentTask.questionList || []).forEach((question) => {
+    const areas = parseQuestionAreas([question]);
+    if (!areas.length) return;
+    const area = areas[0];
+    if (!dataList[area.i - 1]) {
+      dataList[area.i - 1] = [];
+    }
+
+    const userMap = {};
+    question.trackList.forEach((track) => {
+      if (!userMap[track.userId]) {
+        userMap[track.userId] = {
+          userId: track.userId,
+          userName: track.userName,
+          scores: [],
+        };
+      }
+      userMap[track.userId].scores.push(track.score);
+    });
+
+    const users = Object.values(userMap).map((user) => {
+      return { ...user, score: calcSum(user.scores) };
+    });
+
+    dataList[area.i - 1].push({
+      mainNumber: question.mainNumber,
+      subNumber: question.subNumber,
+      score: question.score,
+      maxScore: question.maxScore,
+      users,
+      area,
+      style: {
+        position: "absolute",
+        left: (100 * area.x).toFixed(4) + "%",
+        top: (100 * area.y).toFixed(4) + "%",
+        width: (100 * area.w).toFixed(4) + "%",
+        color: "#f53f3f",
+        fontSize: "14px",
+        lineHeight: 1,
+        zIndex: 9,
+      },
+    });
+  });
+
+  return dataList;
+}
+
 // should not render twice at the same time
 let renderLock = false;
 const renderPaperAndMark = async () => {
@@ -284,4 +549,19 @@ const answerPaperScale = $computed(() => {
 .image-seperator {
   border: 2px solid rgba(120, 120, 120, 0.1);
 }
+.mark-info {
+  display: flex;
+  justify-content: space-between;
+}
+.mark-info h3 {
+  font-size: 20px;
+  font-weight: bold;
+  line-height: 1;
+  color: #f53f3f;
+}
+.mark-info p {
+  margin: 0;
+  line-height: 20px;
+  font-weight: bold;
+}
 </style>

+ 30 - 6
src/features/student/studentTrack/StudentTrack.vue

@@ -45,7 +45,11 @@ import MinimapModal from "@/features/mark/MinimapModal.vue";
 import MarkTool from "@/features/mark/MarkTool.vue";
 import MarkBody from "../studentInspect/MarkBody.vue";
 import { message } from "ant-design-vue";
-import { getSingleStudentTaskOfStudentTrack } from "@/api/studentTrackPage";
+import {
+  getSingleStudentTaskOfStudentTrack,
+  getSingleStudentCardData,
+} from "@/api/studentTrackPage";
+import { studentObjectiveConfirmData } from "@/api/checkPage";
 import vls from "@/utils/storage";
 import { doLogout } from "@/api/markPage";
 
@@ -59,13 +63,28 @@ async function updateTask() {
   const mkey = "fetch_task_key";
   void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
   let res = await getSingleStudentTaskOfStudentTrack(studentId);
-  void message.success({
-    content: res.data.studentId ? "获取成功" : "无任务",
-    key: mkey,
-  });
 
   if (res.data.studentId) {
-    let rawTask = res.data;
+    // 获取客观题选项信息
+    const objectiveRes = await studentObjectiveConfirmData(studentId);
+    const objectiveData = objectiveRes.data;
+
+    const answerMap = {};
+    objectiveData.answers.forEach((item) => {
+      answerMap[`${item.mainNumber}_${item.subNumber}`] = item.answer;
+    });
+
+    // 获取题卡数据
+    const cardRes = await getSingleStudentCardData(studentId);
+    const cardData = cardRes.data.content
+      ? JSON.parse(cardRes.data.content)
+      : { pages: [] };
+
+    const rawTask = res.data;
+    rawTask.answerMap = answerMap;
+    rawTask.recogDatas = objectiveData.sheetUrls.map((item) => item.recogData);
+    rawTask.cardData = cardData.pages;
+
     rawTask.sheetUrls = rawTask.sheetUrls || [];
     // rawTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
     rawTask.sliceUrls = [...rawTask.sheetUrls];
@@ -77,6 +96,11 @@ async function updateTask() {
   } else {
     store.message = res.data.message;
   }
+
+  void message.success({
+    content: res.data.studentId ? "获取成功" : "无任务",
+    key: mkey,
+  });
 }
 
 async function fetchTask() {

+ 10 - 0
src/types/index.ts

@@ -230,6 +230,10 @@ export interface Task extends RawTask {
   // 是否是科组长任务
   // 主观题检查时,科组长特殊标记使用headerTagList
   __isMarkLeader: string;
+  // 轨迹图中可能存在客观题填涂框数据,答案数据,题卡数据等
+  answerMap?: Record<string, string>;
+  recogDatas?: string[];
+  cardData?: Array<any>;
 }
 
 interface RawQuestion {
@@ -290,6 +294,7 @@ export interface Track {
   /** 是否此处未作答,未作答时,score默认是-0分 */
   unanswered: boolean;
   userId?: string;
+  userName?: string;
   color?: string;
   isByMultMark?: boolean;
   // 是否是科组长评卷轨迹
@@ -570,3 +575,8 @@ export interface PaperRecogData {
     }>;
   }>;
 }
+
+export interface CardData {
+  id: string;
+  content: string;
+}

+ 24 - 0
src/utils/utils.ts

@@ -455,3 +455,27 @@ export function maxNum(dataList: number[]): number {
   if (!dataList.length) return 0;
   return Math.max.apply(null, dataList);
 }
+
+/**
+ * 百以下数字转中文汉字
+ * @param {Number} num 大于0的100以下数字,其他数值直接转字符串
+ *
+ * @returns {String}
+ */
+export function numberToChinese(num) {
+  if (num >= 100 || num <= 0) return num + "";
+  const cnNums = "一二三四五六七八九十".split("");
+  if (num <= 10) return cnNums[num - 1];
+
+  return (num + "")
+    .split("")
+    .map((item) => item * 1)
+    .map((item, index) => {
+      if (index) {
+        return !item ? "" : cnNums[item - 1];
+      } else {
+        return item === 1 ? "" : cnNums[item - 1];
+      }
+    })
+    .join("十");
+}