Răsfoiți Sursa

我的考试 扫描试卷 定位异常组件更新

dengshaobo 1 lună în urmă
părinte
comite
f33bed02fa
5 a modificat fișierele cu 784 adăugiri și 87 ștergeri
  1. 1 1
      .env.production
  2. 9 0
      src/api/exam.ts
  3. 606 0
      src/components/PositionCanvas.vue
  4. 167 86
      src/views/exam/abnormal/abnormal.vue
  5. 1 0
      vite.config.ts

+ 1 - 1
.env.production

@@ -1,4 +1,4 @@
 # 生成环境配置
 
-VITE_API_BASE_URL = https://www.k12100.com
+VITE_API_BASE_URL = https://dev3.k12100.net/teaching
 VITE_RSA_PUBLIC_KEY = MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmkP5hUxr7x3t76s9kiVrP1acGVp3JFyPsSoZzV+FvFSc78rfLSwEhj0WkUC0O+entDZ/hnvX1pSPsY8VwXtc20n5pSuo/G1HzrFSUCmFD9HWX3RRrFTHGn+G01CQswO/2JZokJQzbTsNDUnlK/w//NYQvXhxOhF9fM/S1L73CkYPIabHmIM1Rc8uik66XlTXrb9+gDQ8Uon+QZTGo3BzxWceONYYnVZZXdFQtzvaHLI5av8LSU5MLp03faX8fYlfB5FwyBL+dQZRVyTP09pbESn30487tsa0/+0pRZ1D+4Tsc2cjIpuXEg1IKGLrs0TSX78T1fROt/JcZ/lt3Tn9xAgMBAAECggEADEAD4/PgaSQuIWVWY4cQth4p46JSe86o7/L9tb8jkR1UmlDJBxoTE09jadmAq10H2rpwljI16zk88WBTqya+1IDWio2aaIPxFLtBOyRaCpxAazMp1I6puF3iRhNHYMFXfoJ88BKv3i8PHNKS8zMeDHcxcLrVUi6iSpKeG8pPkLjEqY0eccYjTUOyh2Q8YmLLwO3+lOOYrD9TcUZ1NqsZCqOI3pzLNONYatqaPprqiuZLUcloiO7D1OJ52LtTzlP2z7/jXM4fwUZK1/9U79zMYmjJifIORY9UiildkGaKzYs3G1ZAKiLKBehf7tGzorU0+tSIF99voyjhed286fMeqQKBgQDn7wH6oadnfZL5NknEaioua4GJ0LhBeZCiaLReYs116554zzBubb4Y752DTE4ka8LKsSIcnCqNbc/b6q0Kw+yThJJ5F/nFgaCzKHfhImqtslolqW5K1UVwUZ9RdFUeeqt8w+CtcnrJs/jIswv7XOnfHm6VKd0gSlpbnAPEVua75wKBgQD+farEb4PCNsOTaLQYcy0dSDh68lBskc5k+kMYmDsUloaOQel2S9ufEEYxZgy4Y2b0U9Y8nEJByFBoKZW+Mplp+hH9OWaK/QpMylh1uwoN44jUyRAKhR211kBZMXcgbIjr49PxG261OydxIyu9ntcJApVpHK30qzR+DBRD7H8+5wKBgQDGz/hQUZXgfqIoAkNFnQO/euQ1sLbhWUWEEmDar7MTq//R6zjG0EettGi/Df/F9KGrgh+NishnJ4SQLSBcJAp9gZzVNJoklbOdH8lzMT9k2Yew1QX4G81ENJNvDVuRnvG1J2tHAuUCVcWitOhGdiT732hHcPVeIp5F/Py1pxBubQKBgQDlVvJxm90tRJTzXsQN1J2vacocYgpADRXmwfF9VJLJdu1DffqadLoymkPneIO2Fz5MqNDERj0fcxmjBPbBNHA0pPtZLEVQs8B4e1FEp43j/kztFVSzZkrj93R97KniOm0Zx3LUMViPUgO1XXCprV8z63QiCYpql27yuIf6vkHduQKBgEe/BLwVHxAxmGzt/k0t9TZYn4MMQd5nx1UWlFzI+mNkNQqNC5cLGWarazeHC2YdcaYAsSMfi11NTvnVY79EbXYomxSBoN3Y63ML7W6Cj9fInVtUF6j002sdRyfey+e2BQtYiUHb9xW6A7sf4LyLZJ64LGYIh1uqWQc2iUeUsSWQ

+ 9 - 0
src/api/exam.ts

@@ -308,4 +308,13 @@ export const markPositionAbnormal= (data:any):Promise<ApiResponse> => {
     method: 'post',
     data
   })
+}
+
+// 34. 扫描学生  客观题异常  校正
+export const correctObjectiveQuestion= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_scan/validate_objective_question',
+    method: 'post',
+    data
+  })
 }

+ 606 - 0
src/components/PositionCanvas.vue

@@ -0,0 +1,606 @@
+<template>
+
+  <div class="main_container" ref="mainContainer" v-loading="loading" element-loading-text="图片加载中……" element-loading-spinner="el-icon-loading" element-loading-background="#ffffff"  @wheel="onWheel">
+   
+      <canvas  ref="paperCanvas" class="paper_canvas"   :width="canvasInfo.width" :height="canvasInfo.height" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag"></canvas>
+        
+  </div>
+  
+</template>
+<script setup>
+import { ref, reactive, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
+import { throttle } from 'lodash';
+
+// props定义
+const props = defineProps({
+  paperType: {
+    type: String,
+    default: 'student'
+  },//类型 模板 还是学生
+
+  usedCardType: {
+    type: [Number, String],
+    default: 1
+  },//系统卡 还是三方卡   0-未制卡 1-系统卡 2-三方卡
+
+  paperImgUrl: {
+    type: String,
+    default: ''
+  },//试卷地址
+
+  positionList: {
+    type: Array,
+    default: () => []
+  }//定位点坐标列表 {x:0,y:0}
+});
+
+// emit定义
+const emit = defineEmits(['GetCutPosition']);
+
+// 鼠标拖动相关
+const startX = ref(0);//鼠标按下时的初始位置x坐标
+const startY = ref(0);//鼠标按下时的初始位置y坐标
+const currentX = ref(0);//试卷的位置  相对于canvas
+const currentY = ref(0);//试卷的位置  相对于canvas
+const scale = ref(1);//画布缩放倍数
+const isDragging = ref(false);
+const loading = ref(false);//加载loading
+
+// 图片对象
+const image = new Image();
+
+// 试卷显示的大小
+const paperImgInfo = reactive({
+  width: 0,
+  height: 0
+});
+
+// 画布的大小
+const canvasInfo = reactive({
+  width: 1240,
+  height: 1754
+});
+
+// 显示的图片的大小
+const imageSize = reactive({
+  width: 0,
+  height: 0
+});
+
+// canvas相关
+const paperCanvas = ref(null);//试卷画布
+const paperCtx = ref(null);
+const zoomRate = ref(1);//图片的缩放比例
+
+// 定位点坐标
+const point1 = reactive({
+  x: 30,
+  y: 25
+});//定位一坐标点
+
+const point2 = reactive({
+  x: 0,
+  y: 0
+});//定位2的坐标点
+
+const point3 = reactive({
+  x: 33,
+  y: 44
+});//定位3的坐标
+
+const point4 = reactive({
+  x: 0,
+  y: 0
+});//定位4的坐标
+
+// 定位点拖动状态
+const isDragging1 = ref(false);//是否可以拖动定位1
+const isDragging2 = ref(false);//是否可以拖动定位2
+const isDragging3 = ref(false);//是否可以拖动定位3
+const isDragging4 = ref(false);//是否可以拖动定位4
+
+// 旋转相关
+const rotationAngle = ref(0);//初始旋转角度
+const rotate = ref(0);//选择记录  0 未旋转  1 顺时针90  2 顺时针180  3 顺时针270
+const iconWidth = ref(40);//图标宽度
+const iconHeight = ref(40);//图标高度
+
+// 重绘控制
+const redrawPending = ref(false);//用与防止多次请求重绘画布
+const animationId = ref(null);
+
+// 容器引用
+const mainContainer = ref(null);
+
+// 监听试卷地址变化
+watch(() => props.paperImgUrl, () => {
+  console.log("地址变化了", props.paperImgUrl);
+  console.log("打印positionList", props.positionList);
+  InitCanvasData();//初始化数据
+});
+
+// 监听定位点坐标变化
+watch(() => props.positionList, (newVal) => {
+  console.log("坐标变化了", props.positionList);
+  // 只在有图片且画布已初始化时才更新标注位置
+  if (image.src && paperCtx.value && newVal && newVal.length > 0) {
+    if (props.usedCardType == 1) {
+      // 系统卡:更新4个定位点
+      if (newVal.length >= 4) {
+        Object.assign(point1, newVal[0]);
+        Object.assign(point2, newVal[1]);
+        Object.assign(point3, newVal[2]);
+        Object.assign(point4, newVal[3]);
+      }
+    } else {
+      // 三方卡:更新2个定位点
+      if (newVal.length >= 2) {
+        Object.assign(point1, newVal[0]);
+        Object.assign(point3, newVal[2]);
+      }
+    }
+    
+    // 只重绘定位点,不重新加载图片
+    RequestRedraw();
+  }
+});
+
+// 组件挂载
+onMounted(() => {
+  InitCanvasData();//初始化数据
+});
+
+// 组件卸载前
+onBeforeUnmount(() => {
+  // 取消未完成的动画请求
+  if (animationId.value) {
+    cancelAnimationFrame(animationId.value);
+  }
+});
+
+// 适应屏幕
+const FitScreen = () => {
+  InitCanvasData();//初始化数据
+};
+
+// 加载图片
+const LoadImage = () => {
+  image.src = props.paperImgUrl;
+  image.crossOrigin = "Anonymous";//设置跨域属性  不设置跨域 导出的图片没有图片
+  image.onload = () => {
+    DrawImage();
+    DrawPosition();//加载定位点坐标
+  };
+};
+
+// 初始画布数据加载
+const InitCanvasData = () => {
+  let { width, height } = mainContainer.value.getBoundingClientRect();
+  loading.value = true;
+  console.log("打印容器宽高", width, height);
+  rotationAngle.value = 0;
+  canvasInfo.width = width;
+  canvasInfo.height = height;
+  paperCanvas.value = mainContainer.value.querySelector('.paper_canvas');
+  paperCtx.value = paperCanvas.value.getContext('2d');
+  console.log("打印this.paperCtx", paperCtx.value);
+  image.src = props.paperImgUrl;
+
+  image.crossOrigin = "Anonymous";//设置跨域属性  不设置跨域 导出的图片没有图片
+  image.onload = () => {
+    // 使用 nextTick 确保 DOM 更新后再执行计算
+    nextTick(() => {
+      console.log("图片加载成功", image.width, image.height);
+      
+      // 优化缩放比例计算逻辑
+      const containerPadding = 20; // 容器内边距
+      let widthScale = (canvasInfo.width - containerPadding) / image.width;
+      let heightScale = (canvasInfo.height - containerPadding) / image.height;
+      
+      // 选择较小的缩放比例以确保图片完整显示
+      zoomRate.value = Math.min(widthScale, heightScale);
+      scale.value = 1;//初始默认1
+      console.log("打印初始缩放比例是", zoomRate.value);
+      
+      // 计算图片显示的宽高
+      paperImgInfo.width = image.width * zoomRate.value;
+      paperImgInfo.height = image.height * zoomRate.value;
+      console.log("打印图片的地址", props.paperImgUrl);
+      
+      // 计算图片的位置
+      currentX.value = (canvasInfo.width - paperImgInfo.width * scale.value) / 2;
+      currentY.value = (canvasInfo.height - paperImgInfo.height * scale.value) / 2;
+      
+      // 坐标位置 左上 右上 右下 左下   黑块的坐标  30 25  黑块的宽高 25 15
+      if (props.usedCardType == 1) {
+        console.log("系统卡,定位点", props.positionList);
+        if (props.positionList.length > 0) {
+          // 系统卡
+          Object.assign(point1, props.positionList[0]);//左上
+          Object.assign(point2, props.positionList[1]);//右上
+          Object.assign(point3, props.positionList[2]);//右下
+          Object.assign(point4, props.positionList[3]);//左下
+        } else {
+          // 系统卡默认坐标
+          Object.assign(point1, { x: 30, y: 25 });//左上
+          Object.assign(point2, { x: image.width - 30, y: 25 });//右上
+          Object.assign(point3, { x: image.width - 30, y: image.height - 25 });//右下
+          Object.assign(point4, { x: 30, y: image.height - 25 });//左下
+        }
+      } else {
+        console.log("三方卡,定位点为空,使用默认坐标");
+        // 三方卡
+        if (props.positionList.length > 0) {
+          Object.assign(point1, props.positionList[0]);//左上
+          Object.assign(point2, props.positionList[1]);//右上
+          Object.assign(point3, props.positionList[2]);//右下
+          Object.assign(point4, props.positionList[3]);//左下
+        } else {
+          // 三方卡默认坐标
+          Object.assign(point1, { x: 30, y: 25 });//左上
+          Object.assign(point2, { x: image.width - 30, y: 25 });//右上
+          Object.assign(point3, { x: image.width - 30, y: image.height - 25 });//右下
+          Object.assign(point4, { x: 30, y: image.height - 25 });//左下
+        }
+      }
+      
+      setTimeout(() => { 
+        loading.value = false;
+      }, 500);
+      
+      // 使用 Promise 确保所有计算完成后再加载图片
+      Promise.resolve().then(() => {
+        LoadImage();
+      }).catch(error => {
+        console.error('加载图片时出错:', error);
+      });
+    });
+  };
+
+  setTimeout(() => { 
+    loading.value = false;
+  }, 2500);
+};
+
+// 根据地址获取旋转角度
+const GetRotationFromUrl = (url) => {
+  if (!url) return 0;
+  
+  // 匹配 x-oss-process=image/rotate,xxx 格式的旋转参数
+  const rotateRegex = /x-oss-process=image\/rotate,(\d+)/;
+  const match = url.match(rotateRegex);
+  
+  if (match && match[1]) {
+    return parseInt(match[1], 10);
+  }
+  
+  return 0;
+};
+
+// 判断鼠标位置是否在定位点的图标内
+const CheckPointInIcon = (x, y, point) => {
+  const iconW = iconWidth.value * scale.value;
+  const iconH = iconHeight.value * scale.value;
+  const rect = {
+    left: point.x * zoomRate.value * scale.value + currentX.value - iconW / 2,
+    right: point.x * zoomRate.value * scale.value + currentX.value + iconW / 2,
+    top: point.y * zoomRate.value * scale.value + currentY.value - iconH / 2,
+    bottom: point.y * zoomRate.value * scale.value + currentY.value + iconH / 2,
+  };
+  
+  let result = false;
+  if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
+    result = true;
+  }
+  return result;
+};
+
+// 选择图片  顺时针旋转
+const ClockwiseRotateImage = () => {
+  // 每次点击时增加旋转角度
+  rotationAngle.value += 90;
+  RequestRedraw();//请求重绘
+};
+
+// 逆时针旋转图片
+const AnticlockwiseRotateImage = () => {
+  // 每次点击时增加旋转角度
+  rotationAngle.value -= 90;
+  rotate.value -= 1;
+  RequestRedraw();//请求重绘
+};
+
+// 放大图片
+const ZoomIn = (event) => {
+  // 如果有鼠标事件,则以鼠标位置为中心进行缩放
+  let mouseX, mouseY;
+  if (event) {
+    const rect = mainContainer.value.getBoundingClientRect();
+    mouseX = event.clientX - rect.left;
+    mouseY = event.clientY - rect.top;
+  } else {
+    // 如果没有鼠标事件,默认以图片中心点缩放
+    mouseX = canvasInfo.width / 2;
+    mouseY = canvasInfo.height / 2;
+  }
+
+  // 计算鼠标在图片上的相对位置 (0-1之间)
+  const relativeX = (mouseX - currentX.value) / (paperImgInfo.width * scale.value);
+  const relativeY = (mouseY - currentY.value) / (paperImgInfo.height * scale.value);
+
+  // 向上滚动放大
+  const oldScale = scale.value;
+  scale.value *= 1.1;
+  // 确保缩放比例不小于最小值
+  if (scale.value > 2.5) {
+    scale.value = 2.5;
+  }
+
+  // 如果确实发生了缩放
+  if (scale.value !== oldScale) {
+    // 根据相对位置调整图片位置,使鼠标位置保持不变
+    currentX.value = mouseX - (relativeX * paperImgInfo.width * scale.value);
+    currentY.value = mouseY - (relativeY * paperImgInfo.height * scale.value);
+  }
+  RequestRedraw();//请求重绘
+};
+
+// 缩小图片
+const ZoomOut = (event) => {
+  // 如果有鼠标事件,则以鼠标位置为中心进行缩放
+  let mouseX, mouseY;
+  if (event) {
+    const rect = mainContainer.value.getBoundingClientRect();
+    mouseX = event.clientX - rect.left;
+    mouseY = event.clientY - rect.top;
+  } else {
+    // 如果没有鼠标事件,默认以图片中心点缩放
+    mouseX = canvasInfo.width / 2;
+    mouseY = canvasInfo.height / 2;
+  }
+
+  // 计算鼠标在图片上的相对位置 (0-1之间)
+  const relativeX = (mouseX - currentX.value) / (paperImgInfo.width * scale.value);
+  const relativeY = (mouseY - currentY.value) / (paperImgInfo.height * scale.value);
+  
+  // 向下滚动缩小
+  const oldScale = scale.value;
+  scale.value /= 1.1;
+  
+  // 确保缩放比例不小于最小值
+  if (scale.value < 0.5) {
+    scale.value = 0.5;
+  }
+
+  // 如果确实发生了缩放
+  if (scale.value !== oldScale) {
+    // 根据相对位置调整图片位置,使鼠标位置保持不变
+    currentX.value = mouseX - (relativeX * paperImgInfo.width * scale.value);
+    currentY.value = mouseY - (relativeY * paperImgInfo.height * scale.value);
+  }
+  RequestRedraw();//请求重绘
+};
+
+// 绘制图片
+const DrawImage = () => {
+  // 图片加载完成后在canvas上绘制图片
+  if (image) {
+    // 清除之前绘制的内容
+    paperCtx.value.clearRect(0, 0, canvasInfo.width, canvasInfo.height);
+    paperCtx.value.drawImage(image, currentX.value, currentY.value, paperImgInfo.width * scale.value, paperImgInfo.height * scale.value);
+  }
+};
+
+// 加载定位图标
+const DrawPosition = () => {
+  let positionImg = new Image();
+  positionImg.src = require('../assets/position_icon.png');
+  positionImg.onload = () => {
+    // 重新计算坐标的位置点
+    const iconW = iconWidth.value * scale.value; 
+    const iconH = iconHeight.value * scale.value;
+    let pointlist = [point1, point2, point3, point4];
+    
+    if (props.positionList.length > 0) {
+      pointlist = props.positionList;
+      if (props.usedCardType == 1) {
+        // 系统卡
+        pointlist = props.positionList;
+      } else {
+        // 三方卡
+        pointlist = [point1, point3];
+      }
+    } else {
+      if (props.usedCardType == 1) {
+        // 系统卡
+        pointlist = [point1, point2, point3, point4];
+      } else {
+        // 三方卡
+        pointlist = [point1, point3];
+      }
+    }
+
+    pointlist.forEach((point) => {
+      // 修改为使用相对于原始图片的px单位坐标
+      const x = currentX.value + point.x * zoomRate.value * scale.value - iconW / 2;
+      const y = currentY.value + point.y * zoomRate.value * scale.value - iconH / 2;
+      paperCtx.value.drawImage(positionImg, x, y, iconW, iconH);
+    });
+  };
+};
+
+// 请求重绘
+const RequestRedraw = (point) => {
+  if (!redrawPending.value) {
+    redrawPending.value = true;
+    animationId.value = requestAnimationFrame(() => {
+      if (point) {
+        LoadImage();//重新加载
+      } else {
+        DrawImage();
+        DrawPosition();
+      }
+      redrawPending.value = false;
+    });
+  }
+};
+
+// 开始拖动时记录起始位置和偏移量
+const startDrag = (event) => {
+  startX.value = event.clientX - currentX.value;
+  startY.value = event.clientY - currentY.value;
+  const rect = paperCanvas.value.getBoundingClientRect();
+  const mouseX = event.clientX - rect.left;
+  const mouseY = event.clientY - rect.top;
+  
+  if (CheckPointInIcon(mouseX, mouseY, point1)) {
+    if (props.paperType == 'template') {
+      return;//模板阻止拖动
+    }
+    event.stopPropagation();//阻止事件冒泡
+    // 判断是否可以拖动
+    isDragging1.value = true;
+    isDragging2.value = false;
+    isDragging3.value = false;
+    isDragging4.value = false;
+    isDragging.value = false;
+    paperCanvas.value.style.cursor = "pointer";
+  } else if (CheckPointInIcon(mouseX, mouseY, point2)) {
+    if (props.paperType == 'template') {
+      return;//模板阻止拖动
+    }
+    event.stopPropagation();//阻止事件冒泡
+    isDragging1.value = false;
+    isDragging2.value = true;
+    isDragging3.value = false;
+    isDragging4.value = false;
+    isDragging.value = false;
+  } else if (CheckPointInIcon(mouseX, mouseY, point3)) {
+    if (props.paperType == 'template') {
+      return;//模板阻止拖动
+    }
+    event.stopPropagation();//阻止事件冒泡
+    isDragging1.value = false;
+    isDragging2.value = false;
+    isDragging3.value = true;
+    isDragging4.value = false;
+    isDragging.value = false;
+  } else if (CheckPointInIcon(mouseX, mouseY, point4)) {
+    if (props.paperType == 'template') {
+      return;//模板阻止拖动
+    }
+    event.stopPropagation();//阻止事件冒泡
+    isDragging1.value = false;
+    isDragging2.value = false;
+    isDragging3.value = false;
+    isDragging4.value = true;
+    isDragging.value = false;
+  } else {
+    isDragging1.value = false;
+    isDragging2.value = false;
+    isDragging3.value = false;
+    isDragging4.value = false;
+    isDragging.value = true;
+  }
+};
+
+// 方法结束时重置拖动状态
+const stopDrag = () => {
+  isDragging.value = false;
+  isDragging1.value = false;
+  isDragging2.value = false;
+  isDragging3.value = false;
+  isDragging4.value = false;
+};
+
+// 拖动过程更新图片位置并重新绘制
+const onDrag = (event) => {
+  if (isDragging.value) {
+    const newX = event.clientX - startX.value;
+    const newY = event.clientY - startY.value;
+    currentX.value = newX;
+    currentY.value = newY;
+    RequestRedraw();//请求重绘
+  }
+  
+  const rect = paperCanvas.value.getBoundingClientRect();
+  const mouseX = event.clientX - rect.left;
+  const mouseY = event.clientY - rect.top;
+
+  // 这里需要返回原图的px坐标 定位块中心点的坐标
+  const iconW = iconWidth.value * scale.value;
+  const iconH = iconHeight.value * scale.value;
+  
+  const newPoint = {
+    x: Math.round((mouseX - currentX.value) / (zoomRate.value * scale.value)),
+    y: Math.round((mouseY - currentY.value) / (zoomRate.value * scale.value))
+  };
+  
+  // 坐标点一
+  if (isDragging1.value) {
+    Object.assign(point1, newPoint);
+    RequestRedraw(point1);//请求重绘
+    let list = [point1, point2, point3, point4];
+    emit("GetCutPosition", list);
+  }
+  if (isDragging2.value) {
+    Object.assign(point2, newPoint);
+    RequestRedraw(point2);//请求重绘
+    let list = [point1, point2, point3, point4];
+    emit("GetCutPosition", list);
+  }
+  if (isDragging3.value) {
+    Object.assign(point3, newPoint);
+    RequestRedraw(point3);//请求重绘
+    let list = [point1, point2, point3, point4];
+    emit("GetCutPosition", list);
+  }
+  if (isDragging4.value) {
+    Object.assign(point4, newPoint);
+    RequestRedraw(point4);//请求重绘
+    let list = [point1, point2, point3, point4];
+    emit("GetCutPosition", list);
+  }
+};
+
+// 处理鼠标滚轮事件
+const onWheel = throttle(function(event) {
+  event.preventDefault();
+
+  // 计算新的缩放比例
+  if (event.deltaY < 0) {
+    // 向上滚动放大
+    ZoomIn(event);
+  } else {
+    ZoomOut(event);
+  }
+}, 100);//节流 100毫秒内最多执行一次
+
+// 监听窗口大小变化,重新计算表格高度 使用节流防止频繁改变窗口大小导致计算量过大而页面卡顿
+const handleResize = throttle(function() {
+  // 窗口变化重新计算canvas的宽高
+  let { width, height } = mainContainer.value.getBoundingClientRect();
+}, 500); // 节流 500 毫秒内最多执行一次
+
+// 暴露方法给父组件
+defineExpose({
+  FitScreen,
+  ClockwiseRotateImage,
+  AnticlockwiseRotateImage
+});
+</script>
+<style lang="scss" scoped>
+.main_container
+{
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+#paperCanvas
+{
+  position: absolute;
+  cursor: pointer;
+  transition: transform 0.3s ease-in-out;/* 添加平滑过渡效果 */
+}
+</style>

+ 167 - 86
src/views/exam/abnormal/abnormal.vue

@@ -72,7 +72,7 @@
                         </div>
                     </div>
                     <div class="content_canvas">
-                        <!-- <PositionCanvas ref="PaperCanvas" :usedCardType="usedCardType" :positionList="targetPoints"  :paperImgUrl="currentPaperUrl"  @GetCutPosition="GetCutStudentPosition" paperType="student" v-if="currentPaperUrl"> </PositionCanvas> -->
+                        <PositionCanvas ref="PaperCanvas" :usedCardType="usedCardType" :positionList="targetPoints"  :paperImgUrl="currentPaperUrl"  @GetCutPosition="GetCutStudentPosition" paperType="student" v-if="currentPaperUrl"> </PositionCanvas>
                     </div>
                 </div>
 
@@ -197,7 +197,7 @@
                     </el-input>
                     <el-button  class="editor_item" @click="OpenAddStudentDialog" >新增考生</el-button>
                 </div>
-                <el-table  :data="searchStudetList" border :max-height="searchTableHeight" stripe style="width: 100%;">
+                <el-table  :data="searchStudetList" border :height="numberTableHeight" stripe style="width: 100%;">
                 <el-table-column align="center"  prop="studentName" width="70"  label="姓名">
                         <template v-slot="scope">
                             <div class="table_row_value" :title="scope.row.studentName">
@@ -241,51 +241,7 @@
                 
             </div>
         </div>
-        <!-- 缺页异常区域 -->
-        <div class="right_center" v-show="abnormalType==7">
-            <div class="page_jg_20" ></div>
-            <div class="right_table">   
-                <el-table @row-click="PendingListRowClick" :row-class-name="TableRowClassName"  border  :data="losePageData.pendingList" stripe  :height="losePagetableHeight" align="left" >
-                    <el-table-column align="center" width="60" type="index" label="序号"></el-table-column>  
-                    <el-table-column align="center"  width="55" label="批次">
-                        <template v-slot="scope">
-                        {{GetPositionSelNumber(scope.row)}}
-                        </template>
-                    </el-table-column>
-                    <el-table-column align="center" prop="studentName" width="70" label="姓名">
-                        <template v-slot="scope">
-                            <span v-if="scope.row.studentName">{{scope.row.studentName}} </span>
-                            <span v-else>-</span>
-                        </template>
-                    </el-table-column>  
-                    <el-table-column align="center" prop="cardNumber" label="考号">
-                        <template v-slot="scope">
-                            <span v-if="scope.row.cardNumber">{{scope.row.cardNumber}} </span>
-                            <span v-else>-</span>
-                        </template>
-                    </el-table-column>
-                    <el-table-column align="center" prop="examDate" width="40" label="页数">
-                        <template v-slot="scope">
-                            <span v-if="scope.row.scanPictureVOS">{{scope.row.scanPictureVOS.length}} </span>
-                            <span v-else>-</span>
-                        </template>
-                    </el-table-column>
-                </el-table>
-                
-            </div>
-            <div class="right_bottom_button el_button">
-                <div class="page_jg_20"></div>
-                <div class="bottom_button_two">
-                    <el-button @click="MarkPositionAbnormal" class="abnormal_other_btn">标记定位异常</el-button>
-                    <el-button @click="MarkExamNumberAbnormal" class="abnormal_other_btn">标记考号异常</el-button>
-                </div>
-                <div class="bottom_button_item">
-                    <!-- <el-button @click="recyleMethod" class="recycle_bin_btn">回收站</el-button> -->
-                    <!-- 缺页异常删除 需要用delete_batch_pic 接口 -->
-                    <el-button @click="LosePageDelete()" class="delete_paper_btn_100">删除当前试卷</el-button>
-                </div>
-            </div>
-        </div>
+
         <!-- 客观题异常区域 -->
         <div class="right_center" v-show="abnormalType==4">
             <!-- 已处理未开发完成 暂时先屏蔽切换 -->
@@ -371,10 +327,10 @@ import { onMounted,onUnmounted, ref, computed, nextTick, watch } from 'vue';
 import { ElMessage,ElMessageBox } from 'element-plus'
 
 // 引入可能需要的 API (请根据实际路径调整)
-import { getExamAbnormalList,getSearchScanStudents,claimAnswerCard, updateAbnormalStatus,markPositionAbnormal, deletePaper } from '@/api/exam'
+import { getExamAbnormalList,getSearchScanStudents,claimAnswerCard, correctObjectiveQuestion,markPositionAbnormal, deletePaper } from '@/api/exam'
 
 // 引入子组件 (确保路径正确)
-// import PositionCanvas from './components/PositionCanvas.vue' // 假设路径
+import PositionCanvas from '@/components/PositionCanvas.vue' // 假设路径
 import ActionImage from '@/components/ActionImage.vue'// 画布图片组件
 // import MarkScore from './components/MarkScore.vue'           // 假设路径
 
@@ -475,9 +431,9 @@ const searchTimestamp=0;//search请求时间戳
 
 // 表格高度与Key
 const tableHeightRight = ref(200)//定位表格高度
-const losePagetableHeight = ref(200)
-const searchTableHeight = ref(200)
-const objectTableHeight = ref(200)
+const numberTableHeight = ref(200);//考号异常表格高度
+
+const objectTableHeight = ref(200);//客观题异常处理表格高度
 const subjectiveTableHeight = ref(200)
 const tableKey = ref(0)
 
@@ -493,22 +449,15 @@ const subjectiveTableData = ref<any[]>([])
 const choiceHeader = ref([])
 
 // Canvas Refs
-const PositionCanvas = ref()
+
 const PaperCanvas = ref()
 const ActionCanvas = ref()
+const objectQuestionTable = ref<HTMLElement | null>(null);
+const numberPendingTable=ref<HTMLElement | null>(null);//考号异常待处理表格高度
 
 // ==================== 计算属性 ====================
 
-// 示例:如果需要动态计算某些表格数据
-const pendingListComputed = computed(() => {
-    if (abnormalType.value === 2) return positionData.value.pendingList
-    if (abnormalType.value === 3) return numberData.value.pendingList
-    if (abnormalType.value === 7) return losePageData.value.pendingList
-    if (abnormalType.value === 4) return rightTab.value === 'pending' ? objectData.value.pendingList : objectData.value.processedList
-    if (abnormalType.value === 5) return rightTab.value === 'pending' ? choiceData.value.pendingList : choiceData.value.processedList
-    if (abnormalType.value === 6) return rightTab.value === 'pending' ? subjectiveData.value.pendingList : subjectiveData.value.processedList
-    return []
-})
+
 
 // ==================== 方法定义 ====================
 
@@ -517,6 +466,19 @@ const AbnormalChanage = (type: number) => {
     abnormalType.value = type
     // TODO: 加载对应类型的异常列表数据
     console.log('切换异常类型:', type)
+    
+    //定位异常处理
+    if(type==2)
+    {
+        console.log('定位异常',positionData.value)
+        if(positionData.value.pendingList.length>0)
+        {
+            currentIndex.value=0;
+            currentItem.value=positionData.value.pendingList[currentIndex.value];//当前学生数据
+            currentPaperList.value=currentItem.value.scanPictureVOS || [];//试卷图片列表
+            currentPaperUrl.value=currentPaperList.value[0].recognizeUrl;//当前图片地址
+        }
+    }
 
     //考号异常 3
     if(type==3)
@@ -529,22 +491,30 @@ const AbnormalChanage = (type: number) => {
             currentPaperList.value=currentItem.value.scanPictureVOS || [];//试卷图片列表
             currentPaperUrl.value=currentPaperList.value[0].recognizeUrl;//当前图片地址
             currentImagePageNo.value=currentPaperList.value[0].pageNo;//当前图片的页码
-
         }
     }
+    //客观题异常
     if(type==4)
     {
         console.log('客观题异常',objectData.value)
-        if(objectData.value.pendingList.length>0)
+        let studentList=rightTab.value=='pending'?objectData.value.pendingList:objectData.value.processedList;
+        if(studentList.length>0)
         {
-            currentIndex.value=0;
-            currentItem.value=objectData.value.pendingList[currentIndex.value];//当前学生数据
+
+            currentItem.value=studentList[currentIndex.value];//当前学生数据
             currentPaperList.value=currentItem.value.scanPictureVOS || [];//试卷图片列表
-            currentPaperUrl.value=currentPaperList.value[0].recognizeUrl;//
+            currentPaperUrl.value=currentPaperList.value[0].recognizeUrl;//当前图片的地址
+            currentImagePageNo.value=currentPaperList.value[0].pageNo;//当前图片的页码
+
+            //客观题校正列表数据
+            currentQuestionList.value=JSON.parse(JSON.stringify(currentItem.value.objectiveQuestionVOS || []));
+
+            console.log('客观题异常',currentItem.value)
+            ObjectDataChange();//客观题答案回显
         }
 
     }
-
+    CalculateTableHeight();//计算表格高度
 }
 
 // 2. 刷新异常列表
@@ -565,10 +535,10 @@ const ShowDrawData = () => {
     // TODO: 更新 ActionImage 的 drawData 显示状态
 }
 
-// 5. 划分异常显示类型切换
-const SubjectDataChange = () => {
+// 5. 客观题答案回显数据设置切换
+const ObjectDataChange = () => {
     // TODO: 根据 isShowDrawType 重新渲染右侧列表或 Canvas
-    console.log('切换显示类型:', isShowDrawType.value)
+    console.log('客观题答案回显', )
 }
 
 // 6. 模板页码切换
@@ -641,13 +611,15 @@ const PendingListRowClick = (row: any, column: any, event: Event) => {
     if(abnormalType.value==4)
     {
         currentItem.value=row;
-
+        let studentList=rightTab.value=='pending'?objectData.value.pendingList:objectData.value.processedList;
+        currentIndex.value=studentList.findIndex((item:any)=>item.id==currentItem.value.id);
+        
         currentPaperList.value=currentItem.value.scanPictureVOS || [];//试卷图片列表
         currentPaperUrl.value=currentPaperList.value[0].recognizeUrl;//当前图片地址
         currentImagePageNo.value=currentPaperList.value[0].pageNo;//当前图片的页码
-
         currentQuestionList.value=JSON.parse(JSON.stringify(currentItem.value.objectiveQuestionVOS || []));
-        console.log('客观题校正异常数据',currentQuestionList.value)
+        console.log('客观题校正异常数据',currentQuestionList.value);
+        console.log('客观题currentIndex',currentIndex.value);
     }
 }
 
@@ -932,8 +904,10 @@ const LosePageDelete = () => {
 
 // 26. 切换待处理/已处理 Tab
 const ChangeTab = (tab: string) => {
-    rightTab.value = tab
+    rightTab.value = tab;
+    AbnormalChanage(abnormalType.value);
     // TODO: 可能需要重新加载数据或过滤
+    CalculateTableHeight();//计算表格高度
 }
 
 // 27. 清空选择 (客观题)
@@ -956,26 +930,105 @@ const GetLetterToNumberTF = (letter: string) => {
     return letter
 }
 
+//将答案转换成数组
+const GetAnswerArray = (selectValue:any)=>{
+    // 处理各种边界情况
+    if (!selectValue || selectValue === '') {
+        return [];
+    }
+    // 使用正则表达式分割,过滤掉空值
+    return selectValue.split(/[,\s]+/).filter((answer: string) => answer.trim() !== '');
+}
+
+
 // 29. 客观题 - 判断是否存在
 const IsCunzai = (selectValue: any, value: any) => {
     // TODO: 判断 selectValue 中是否包含 value
-    return selectValue === value
+    
+    let result=false;
+    if(selectValue.includes(value))
+    {
+        result=true;
+    }
+    return result;
 }
 
 // 30. 客观题 - 选择答案
-const ChooseAnswer = (row: any, value: any) => {
+const ChooseAnswer = (row: any, answer: any) => {
     // TODO: 更新 row.selectValue
-    console.log('选择答案:', value)
+    console.log('选择题:', row)
+    console.log('选择答案:', answer)
+    let answers = GetAnswerArray(row.selectValue)
+    const index = answers.indexOf(answer)
+    if (index > -1) {
+        // 如果已存在,则移除
+        answers.splice(index, 1)
+    } else {
+        // 否则添加
+        answers.push(answer)
+    }
+    // 更新回 row.selectValue,并排序(可选)
+    row.selectValue = answers.sort().join(',')
+    // 调用更新方法
+    ObjectDataChange()
 }
 
 // 31. 客观题校正完成
-const ObjectiveCheck = () => {
+const ObjectiveCheck = async () => {
+    
+    if(!currentItem.value?.id) {
+        ElMessage.warning('请选择待处理的异常!')
+        return
+    }
+    const params = {
+        id: currentItem.value.id,
+        objectiveQuestionDTOS: currentQuestionList.value
+    };
+    
     correctionLoading.value = true
-    // TODO: 提交客观题修正结果
-    setTimeout(() => {
+    console.log("校正客观题参数", params)
+    
+    try {
+        const res = await correctObjectiveQuestion(params)
         correctionLoading.value = false
-        ElMessage.success('校正成功')
-    }, 1000)
+        
+        if (res.code === 200) {
+            ElMessage({
+                type: 'success',
+                message: '校正成功'
+            })
+            
+            let studentList = rightTab.value === 'pending' 
+                ? objectData.value.pendingList 
+                : objectData.value.processedList
+            
+            console.log("打印学生列表", studentList)
+            console.log("打印当前学生索引", currentIndex.value)
+            
+            // if (currentIndex.value < studentList.length - 1) {
+            //     if(rightTab.value === 'processed') {
+            //         currentIndex.value = currentIndex.value + 1
+            //     }
+            // } else {
+            //     console.log("已经是最后一个学生")
+            //     currentIndex.value = 0
+            // }
+            console.log("打印当前索引", currentIndex.value)
+            GetAbnormalList()
+        } else {
+            ElMessage({
+                type: 'error',
+                message: res.msg || '校正失败'
+            })
+        }
+    } catch (error) {
+        correctionLoading.value = false
+        console.error('校正客观题失败:', error)
+        ElMessage({
+            type: 'error',
+            message: '校正失败,请重试'
+        })
+    }
 }
 
 // 32. 选做题 - 选择答案
@@ -1081,6 +1134,7 @@ const GetAbnormalList = async () => {
             };//异常数据
             console.log("打印异常数据",abnormalObj.value);
             console.log("打印考号异常数据",numberData);
+            AbnormalChanage(abnormalType.value);
         }
 
 
@@ -1129,9 +1183,36 @@ onUnmounted(() => {
 const CalculateTableHeight = () => {
     nextTick(() => {
 
-        tableHeightRight.value = window.innerHeight - 220;
+        tableHeightRight.value = window.innerHeight - 220;//定位表格的高度
+        if(abnormalType.value==2)
+        {
+            //考号异常表格高度计算
+            const tableContainerEl = numberPendingTable.value;
+            const numberRect = tableContainerEl && 
+                            typeof tableContainerEl.getBoundingClientRect === 'function' ? 
+                            tableContainerEl.getBoundingClientRect() : null;
+            const containerHeight = numberRect ? numberRect.height : 200;
+            console.log("打印表格高度",containerHeight);
+
+            numberTableHeight.value=window.innerHeight - containerHeight - 345;
+
+        }
+        if(abnormalType.value==4)
+        {
+            //客观题高度计算
+            const tableContainerEl = objectQuestionTable.value
+            const objectRect = tableContainerEl && 
+                            typeof tableContainerEl.getBoundingClientRect === 'function' ? 
+                            tableContainerEl.getBoundingClientRect() : null
+            const containerHeight = objectRect ? objectRect.height : 200;
+            console.log("打印表格高度",containerHeight);
+
+            objectTableHeight.value=window.innerHeight - containerHeight - 270;
+        }
        
-        // 其他高度同理
+        
+
+
     })
 }
 

+ 1 - 0
vite.config.ts

@@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
 import path from 'path' // 引入 Node.js 的 path 模块
 // https://vite.dev/config/
 export default defineConfig({
+  base: process.env.NODE_ENV === 'production' ? '/scoring/' : '/', // 添加这行配置
   plugins: [vue()],
   resolve: {
     alias: {