Pārlūkot izejas kodu

feat: 任务轨迹查看

zhangjie 2 mēneši atpakaļ
vecāks
revīzija
1b48eec50d

+ 21 - 11
src/assets/styles/pages.scss

@@ -1852,46 +1852,55 @@
 // modal-task-detail
 .modal-task-detail {
   &.is-fullscreen.el-dialog {
+    .el-dialog__title {
+      display: inline-block;
+      margin-right: 20px;
+    }
     .el-dialog__header {
       border-bottom: 1px solid #eee;
     }
     .el-dialog__body {
-      padding-top: 70px;
-      height: calc(100vh - 54px); // 54px 是头部高度
+      padding: 70px 20px 20px;
+      height: 100%;
       overflow: hidden;
+      background: $--color-background;
     }
   }
   .task-detail-content {
     display: flex;
     height: 100%;
     width: 100%;
+    justify-content: space-between;
     .left-panel,
     .right-panel {
-      width: 50%;
+      width: calc(50% - 8px);
       height: 100%;
       overflow-y: auto;
       padding: 20px;
       box-sizing: border-box;
+      border-radius: 4px;
+      background-color: #fff;
     }
     .left-panel {
-      border-right: 1px solid #eee;
-      background-color: #f5f7fa; // 背景色以便区分
+      background-color: #fff;
+      background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
+        linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
+        linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
+        linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
+      background-size: 10px 10px;
+      background-position: 0 0, 0 5px, 5px -5px, -5px 0px;
       .image-container {
-        position: relative; // 为轨迹点定位提供基准
+        display: inline-block;
+        position: relative;
         margin-bottom: 20px;
-        width: 100%; // 图片宽度自适应容器
         img {
           display: block;
           max-width: 100%;
           height: auto;
-          border: 1px solid #ccc;
         }
         .track-point {
           position: absolute;
           color: red;
-          background-color: rgba(255, 255, 255, 0.7);
-          padding: 1px 3px;
-          border-radius: 3px;
           white-space: nowrap;
           &.track-header {
             color: blue;
@@ -1912,6 +1921,7 @@
           margin-top: 0;
           margin-bottom: 10px;
           font-size: 16px;
+          font-weight: bold;
         }
         .error-msg {
           color: #f56c6c;

+ 3 - 3
src/modules/mark/components/markDetail/MarkDetailTask.vue

@@ -197,7 +197,7 @@
     </div>
     <modal-task-detail
       ref="ModalTaskDetail"
-      :task-id="currentTaskId"
+      :task="currentTask"
     ></modal-task-detail>
   </div>
 </template>
@@ -236,7 +236,7 @@ export default {
       dataList: [],
       questions: [],
       MARK_TASK_STATUS,
-      currentTaskId: "", // 用于存储当前要查看详情的任务ID
+      currentTask: {},
     };
   },
   mounted() {
@@ -271,7 +271,7 @@ export default {
       this.toPage(1);
     },
     toViewDetail(row) {
-      this.currentTaskId = row.id;
+      this.currentTask = row;
       this.$nextTick(() => {
         this.$refs.ModalTaskDetail.open();
       });

+ 68 - 58
src/modules/mark/components/markDetail/ModalTaskDetail.vue

@@ -1,38 +1,41 @@
 <template>
   <el-dialog
     custom-class="modal-task-detail"
-    title="题目详情"
     :visible.sync="modalVisible"
     fullscreen
     append-to-body
     :close-on-click-modal="false"
     @close="handleClose"
   >
+    <div slot="title">
+      <h2 class="el-dialog__title">题目详情</h2>
+      <span>学生:{{ task.studentName }}({{ task.studentCode }})</span>
+      <span class="ml-4">题目:{{ task.questionNumber }}</span>
+      <span v-if="task.userName" class="ml-4"
+        >评卷员:{{ task.userName }}({{ task.loginName }})</span
+      >
+      <button class="el-dialog__headerbtn" @click="cancel"></button>
+    </div>
+
     <div v-loading="loading" class="task-detail-content">
-      <div class="left-panel">
+      <div class="left-panel" :style="leftPanelStyle">
         <!-- 答题卡裁切图片展示区域 -->
-        <div
-          v-for="(crop, index) in croppedImages"
-          :key="index"
-          class="image-container"
-        >
-          <img
-            :ref="'imageRef' + index"
-            :src="crop.src"
-            @load="onImageLoad(index)"
-          />
-          <div
-            v-for="track in crop.tracks"
-            :key="track.id"
-            class="track-point"
-            :style="getTrackStyle(track, index)"
-            :class="track.type === 'header' ? 'track-header' : 'track-normal'"
-          >
-            {{ track.score }}
+        <div v-for="(crop, index) in croppedImages" :key="index">
+          <div class="image-container">
+            <img :ref="'imageRef' + index" :src="crop.src" />
+            <div
+              v-for="track in crop.tracks"
+              :key="track.id"
+              class="track-point"
+              :style="getTrackStyle(track, index)"
+              :class="track.type === 'header' ? 'track-header' : 'track-normal'"
+            >
+              {{ track.score }}
+            </div>
           </div>
         </div>
       </div>
-      <div class="right-panel">
+      <div v-if="aiMarked" class="right-panel">
         <!-- OCR 结果展示区域 -->
         <div class="ocr-section section-box">
           <h3>OCR 识别结果</h3>
@@ -96,9 +99,11 @@ import { markDetailTaskDetail } from "../../api";
 export default {
   name: "ModalTaskDetail",
   props: {
-    taskId: {
-      type: String,
-      required: true,
+    task: {
+      type: Object,
+      default() {
+        return {};
+      },
     },
   },
   data() {
@@ -116,7 +121,6 @@ export default {
       },
       croppedImages: [], // 处理后的裁切图信息和轨迹信息
       originalImageDimensions: {}, // 存储原始图片尺寸 { url: { width, height } }
-      imageLoadPromises: [], // 存储图片加载的 Promise
     };
   },
   computed: {
@@ -126,14 +130,26 @@ export default {
         ? this.taskDetail.markAiQuestionParam.pointList || []
         : this.taskDetail.markAiQuestionParam.levelList || [];
     },
+    aiMarked() {
+      return (
+        this.taskDetail.markAiQuestionParam ||
+        this.taskDetail.ocr?.result.length > 0 ||
+        this.taskDetail.ocr?.errorMsg
+      );
+    },
+    leftPanelStyle() {
+      return this.aiMarked ? {} : { width: "100%" };
+    },
   },
   methods: {
     open() {
       this.modalVisible = true;
       this.fetchTaskDetail();
     },
-    handleClose() {
+    cancel() {
       this.modalVisible = false;
+    },
+    handleClose() {
       // 重置数据
       this.taskDetail = {
         sheetUrls: [],
@@ -145,25 +161,23 @@ export default {
       };
       this.croppedImages = [];
       this.originalImageDimensions = {};
-      this.imageLoadPromises = [];
     },
     async fetchTaskDetail() {
-      if (!this.taskId) return;
+      if (!this.task.id) return;
       this.loading = true;
-      try {
-        const res = await markDetailTaskDetail(this.taskId);
-        this.taskDetail = res || this.taskDetail; // 使用默认值防止 res 为 null
-        await this.processImageData();
-      } catch (error) {
-        console.error("获取题目详情失败:", error);
+
+      const res = await markDetailTaskDetail(this.task.id).catch(() => {});
+      if (!res) {
         this.$message.error("获取题目详情失败");
-      } finally {
-        this.loading = false;
+        return;
       }
+      this.taskDetail = res || this.taskDetail; // 使用默认值防止 res 为 null
+      await this.processImageData();
+      this.loading = false;
     },
     // 预加载所有原图并获取尺寸
     preloadOriginalImages() {
-      this.imageLoadPromises = this.taskDetail.sheetUrls.map((url) => {
+      const imageLoadPromises = this.taskDetail.sheetUrls.map((url) => {
         return new Promise((resolve, reject) => {
           if (this.originalImageDimensions[url]) {
             resolve(this.originalImageDimensions[url]);
@@ -181,7 +195,7 @@ export default {
           img.src = url;
         });
       });
-      return Promise.all(this.imageLoadPromises);
+      return Promise.all(imageLoadPromises);
     },
 
     async processImageData() {
@@ -241,7 +255,6 @@ export default {
           cropParams: cropParams, // 裁切区域在原图的像素坐标和尺寸
           picData: pic, // 保留原始 picList 数据
           tracks: [], // 该裁切图上的轨迹点
-          displayDimensions: { width: 0, height: 0 }, // 图片在页面上显示的实际尺寸
         });
       }
 
@@ -259,7 +272,12 @@ export default {
 
       for (const track of allTracks) {
         const targetCropIndex = crops.findIndex(
-          (crop) => crop.picData.i === track.offsetIndex
+          (crop) =>
+            crop.picData.i === track.offsetIndex &&
+            this.checkPointInArea(
+              { x: track.offsetX, y: track.offsetY },
+              crop.cropParams
+            )
         );
         if (targetCropIndex !== -1) {
           crops[targetCropIndex].tracks.push(track);
@@ -268,25 +286,17 @@ export default {
 
       this.croppedImages = crops;
     },
-
-    onImageLoad(index) {
-      // 图片加载完成后获取其在页面上的实际显示尺寸
-      this.$nextTick(() => {
-        const imgRef = this.$refs["imageRef" + index];
-        if (imgRef && imgRef[0]) {
-          this.croppedImages[index].displayDimensions = {
-            width: imgRef[0].offsetWidth,
-            height: imgRef[0].offsetHeight,
-          };
-          // 强制更新,以便重新计算轨迹位置
-          this.$forceUpdate();
-        }
-      });
+    checkPointInArea(point, area) {
+      return (
+        point.x >= area.x &&
+        point.x <= area.x + area.w &&
+        point.y >= area.y &&
+        point.y <= area.y + area.h
+      );
     },
-
     getTrackStyle(track, cropIndex) {
       const crop = this.croppedImages[cropIndex];
-      if (!crop || !crop.displayDimensions.width || !crop.cropParams.w) {
+      if (!crop || !crop.cropParams.w) {
         return { display: "none" }; // 图片未加载或尺寸无效
       }
 
@@ -307,8 +317,8 @@ export default {
         left: `${displayX}%`,
         top: `${displayY}%`,
         transform: "translate(-50%, -50%)", // 使坐标点位于数字中心
-        fontSize: "14px", // 固定字体大小
-        fontWeight: "bold",
+        fontSize: "24px", // 固定字体大小
+        fontWeight: "bold", // 字体加粗
         zIndex: 10,
       };
     },