소스 검색

图片预览 图片画布组件更新

dengshaobo 1 개월 전
부모
커밋
796f69219f
5개의 변경된 파일1719개의 추가작업 그리고 591개의 파일을 삭제
  1. 384 471
      src/components/ActionImage.vue
  2. 1026 0
      src/components/ActionImages.vue
  3. 180 0
      src/styles/common.scss
  4. 107 112
      src/views/exam/components/PaperPreview.vue
  5. 22 8
      src/views/exam/scanDetail.vue

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 384 - 471
src/components/ActionImage.vue


+ 1026 - 0
src/components/ActionImages.vue

@@ -0,0 +1,1026 @@
+<template>
+  <div
+    class="main_container"
+    ref="mainContainerRef"
+    v-loading="loading"
+    element-loading-text="图片加载中……"
+    element-loading-spinner="el-icon-loading"
+    element-loading-background="#ffffff"
+    @mousedown="onMouseDown"
+    @mousemove="onMouseMove"
+    @mouseup="onMouseUp"
+    @wheel="onWheel"
+  >
+    <canvas
+      id="paperCanvas"
+      ref="paperCanvasRef"
+      class="paper_canvas"
+      @mouseleave="onCanvasLeave"
+    ></canvas>
+    <div id="imgContainer" class="img_container">
+      <img v-if="paperImgUrl" :src="paperImgUrl" />
+    </div>
+    <div class="no_paper_url" v-if="paperImgUrl == ''">
+      暂无数据
+      <!-- 请先制作模版 -->
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, onBeforeUnmount, onMounted } from 'vue'
+import { throttle } from 'lodash'
+// import { mmToPx } from '@/utils/common.ts' // 如果需要取消注释
+
+// 定义 Props
+const props = defineProps({
+  drawData: {
+    type: Array,
+    default: () => [],
+  }, // 画的边框的数据
+  paperInfo: {
+    type: Object,
+    default: () => null,
+  }, // 纸张大小
+  paperImgUrl: {
+    type: String,
+    default: '',
+  }, // 试卷地址相关信息
+  currentId: {
+    type: String,
+    default: '',
+  }, // 当前选中的id
+  isDrag: {
+    type: Boolean,
+    default: true,
+  }, // 是否可以拖动 默认可以拖动
+  isAbnormal: {
+    type: Boolean,
+    default: false,
+  }, // 是否异常处使用 区别 异常处理使用的地方 正常答案显示蓝色
+})
+
+// 定义 Emits
+const emit = defineEmits(['GetRectPoint'])
+
+// 响应式数据
+const mainContainerRef = ref(null)
+const paperCanvasRef = ref(null)
+
+const position = reactive({
+  x: 0,
+  y: 0,
+}) // 初始canvas图片位置
+
+const startX = ref(0) // 鼠标按下时的初始位置x坐标
+const startY = ref(0) // 鼠标按下时的初始位置y坐标
+const scale = ref(1) // 画布缩放倍数
+const isDragging = ref(false) // 是否拖动画布
+const isDrawing = ref(false) // 是否正在画线
+const drawType = ref(0) // 1画线 0 拖拽模式
+const image = ref(null)
+
+const paperImgInfo = reactive({
+  width: 0,
+  height: 0,
+})
+
+const canvasInfo = reactive({
+  width: 0,
+  height: 0,
+}) // 画布的大小
+
+const zoomRate = ref(0) // 图片的缩放比例
+const dpr = window.devicePixelRatio || 1
+const minScale = 0.7 // 最小缩放值
+const maxScale = 4 // 最大缩放值
+
+const rectPoint = reactive({
+  startX: 0,
+  startY: 0,
+  endX: 0,
+  endY: 0,
+}) // 矩形起始坐标点
+
+const showObjectArea = ref(false) // 是否显示对象区域
+const addObjectAreaOption = reactive({
+  derection: 1, // 排列方向 1 横线 2竖向
+  questionType: 1, // 题类型 1单选 2多选 3 判断
+  startNumber: 16, // 起始题号
+  endNumber: 20, // 结束题号
+  interval: 1, // 题号间隔 默认1
+  answerNumber: 4, // 选项个数 答案个数
+  answerWidth: 0, // 选项宽度
+  answerHeight: 0, // 选项高度
+}) // 添加选择题设置信息
+
+const currenPoint = reactive({
+  x: 0,
+  y: 0,
+  w: 0,
+  h: 0,
+})
+
+const containerWidth = ref(0) // 容器宽度
+const containerHeight = ref(0) // 容器高度
+const isInit = ref(true) // 是否是初始加载
+const isShowDraw = ref(true) // 是否显示画框
+const loading = ref(false) // 是否正在加载图片
+
+// 模拟 this.$global.floatNum,请根据实际项目替换为真实工具函数
+const floatNum = (num, fixed = 2) => {
+  if (typeof num !== 'number') return 0
+  return Number(num.toFixed(fixed))
+}
+
+// 方法定义
+
+// 显示画框
+const ShowDrawData = () => {
+  isShowDraw.value = !isShowDraw.value
+  drawImage()
+}
+
+// 初始数据加载
+const initData = () => {
+  if (!mainContainerRef.value) return
+  console.log('初始化数据加载图片地址信息',props.paperImgUrl)
+  // 获取容器的宽高
+  const { width, height } = mainContainerRef.value.getBoundingClientRect()
+  containerHeight.value = Number(height)
+  containerWidth.value = Number(width)
+  console.log('初始化数据加载容器宽高',containerWidth.value,containerHeight.value)
+  image.value = new Image()
+  image.value.src = props.paperImgUrl
+
+  // 初始化获取试卷数据
+  if (props.paperInfo?.width && props.paperInfo?.height) {
+    // 设置初始数据
+    paperImgInfo.width = props.paperInfo.width // 获取宽
+    paperImgInfo.height = props.paperInfo.height // 获取高
+    
+    image.value.onload = () => {
+      loading.value = false // 图片加载完成
+      // 更新缩放率
+      updateZoomAndPaperInfo()
+      // 更新画布尺寸
+      updateCanvasSize()
+      // 计算中心位置使图片居中 初始加载图片是居中
+      if (isInit.value) {
+        centerCanvas()
+      }
+      // 加载图片
+      loadImage()
+    }
+  } else {
+    // 没有值 默认图片的宽高
+    image.value.onload = () => {
+      paperImgInfo.width = image.value.width // 获取宽
+      paperImgInfo.height = image.value.height // 获取高
+      loading.value = false // 图片加载完成
+      // 更新缩放率
+      updateZoomAndPaperInfo()
+      // 更新画布尺寸
+      updateCanvasSize()
+      // 计算中心位置使图片居中
+      if (isInit.value) {
+        centerCanvas()
+      }
+      // 加载图片
+      loadImage()
+    }
+  }
+}
+
+// 切换试卷图片
+const chanagePaperImage = () => {
+  // 更新缩放率
+  updateZoomAndPaperInfo()
+  // 更新画布尺寸
+  updateCanvasSize()
+  // 加载图片
+  loadImage()
+}
+
+// 适合屏幕
+const fitScreen = () => {
+  scale.value = 1
+  isInit.value = true
+  initData() // 初始化屏幕加载
+}
+
+// 加载图片
+const loadImage = () => {
+  drawImage() // 绘制边框数据
+}
+
+// 图片宽高坐标变化
+const ImageInfoChange = () => {
+  const { width, height } = paperImgInfo
+  const imgDom = document.getElementById('imgContainer')
+  if (imgDom) {
+    imgDom.style.width = width * zoomRate.value * scale.value + 'px'
+    imgDom.style.height = height * zoomRate.value * scale.value + 'px'
+    imgDom.style.left = position.x + 'px'
+    imgDom.style.top = position.y + 'px'
+  }
+}
+
+// 图片加载完成后绘制图片
+const drawImage = () => {
+  const canvas = paperCanvasRef.value
+  if (!canvas) return
+  
+  const ctx = canvas.getContext('2d')
+  if (!ctx) return
+
+  ctx.clearRect(0, 0, canvasInfo.width, canvasInfo.height) // 清除边框数据
+  ImageInfoChange() // 用image图片替代背景
+
+  for (let i = 0; i < props.drawData.length; i++) {
+    let point = {
+      x: props.drawData[i].x * zoomRate.value * scale.value,
+      y: props.drawData[i].y * zoomRate.value * scale.value,
+      width: props.drawData[i].w * zoomRate.value * scale.value,
+      height: props.drawData[i].h * zoomRate.value * scale.value,
+      blockName: props.drawData[i].blockName,
+      page: props.drawData[i].page,
+      blockArea: props.drawData[i].blockArea,
+    }
+
+    // 外边框数据 mm单位 比如系统卡 使用
+    if (props.drawData[i].usedCardType == 1) {
+      // 系统卡
+      const imageInfo = paperImgInfo
+      let offsexPoint = {
+        x: props.drawData[i].x - 30,
+        y: props.drawData[i].y - 25,
+        w: props.drawData[i].w,
+        h: props.drawData[i].h,
+      } // 相对与第一个黑块的坐标
+      
+      let templateInfo = {
+        width: 794 - 30 * 2,
+        height: 1123 - 25 * 2,
+      } // 模板去掉边框的尺寸
+
+      // 如果长大于宽 就是A3 否则就是A4
+      if (paperImgInfo.width > paperImgInfo.height) {
+        // A3 1588*1123
+        offsexPoint = {
+          x: props.drawData[i].x - 30,
+          y: props.drawData[i].y - 25,
+          w: props.drawData[i].w,
+          h: props.drawData[i].h,
+        }
+        templateInfo = {
+          width: 1588 - 30 * 2,
+          height: 1123 - 25 * 2,
+        }
+      } else {
+        // A4 794*1123
+        offsexPoint = {
+          x: props.drawData[i].x - 30,
+          y: props.drawData[i].y - 25,
+          w: props.drawData[i].w,
+          h: props.drawData[i].h,
+        }
+        templateInfo = {
+          width: 794 - 30 * 2,
+          height: 1123 - 25 * 2,
+        }
+      }
+
+      // 系统卡 需要根据模板的坐标进行转换
+      const x = parseFloat(
+        ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
+      )
+      const y = parseFloat(
+        ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
+      )
+      const w = parseFloat(
+        ((props.drawData[i].w / templateInfo.width) * imageInfo.width).toFixed(2)
+      )
+      const h = parseFloat(
+        ((props.drawData[i].h / templateInfo.height) * imageInfo.height).toFixed(2)
+      )
+
+      point = {
+        x: x * zoomRate.value * scale.value,
+        y: y * zoomRate.value * scale.value,
+        width: w * zoomRate.value * scale.value,
+        height: h * zoomRate.value * scale.value,
+        blockName: props.drawData[i].blockName,
+        page: props.drawData[i].page || 1,
+        blockArea: props.drawData[i].blockArea,
+      }
+    }
+
+    // 当前表格选择的边框换蓝色显示
+    if (props.currentId == props.drawData[i].id) {
+      ctx.strokeStyle = 'blue'
+      ctx.fillStyle = 'blue'
+      ctx.lineWidth = 2
+    } else {
+      ctx.strokeStyle = 'red'
+      ctx.fillStyle = 'red'
+      ctx.lineWidth = 2
+    }
+
+    const topiclist = props.drawData[i].topiclist || []
+    
+    if (topiclist.length > 0) {
+      const topicName = String(topiclist?.[0]?.topicName)
+      // 客观题有题目则不显示外边框
+      if (topicName.includes('选做区')) {
+        ctx.strokeRect(point.x, point.y, point.width, point.height) // 绘制边框
+      }
+      if (topiclist[0].index < 0) {
+        // 系统卡选做题显示边框
+        ctx.strokeRect(point.x, point.y, point.width, point.height) // 绘制边框
+      }
+    } else {
+      ctx.lineWidth = 2
+      ctx.strokeRect(point.x, point.y, point.width, point.height) // 绘制边框
+    }
+
+    // 客观题小题加载
+    if (props.drawData[i].topiclist) {
+      const topiclist = props.drawData[i].topiclist
+      let derection = 1 // 排列方向 1 横向 2竖向
+      
+      if (props.drawData[i].areaOption) {
+        try {
+          const option = JSON.parse(props.drawData[i].areaOption)
+          derection = option.derection
+        } catch (e) {
+          console.error('解析 areaOption 失败', e)
+        }
+      }
+
+      if (isShowDraw.value) {
+        topiclist.forEach((topicItem) => {
+          ctx.font = '15px Arial'
+          ctx.fillStyle = 'red'
+          ctx.textAlign = 'left'
+
+          if (String(topicItem.topicName).includes('选做区')) {
+            // 选做题不显示题目文字
+          } else {
+            let namePoint = {
+              x: topicItem.answerList[0].x,
+              y: topicItem.answerList[0].y,
+            }
+
+            // 绘制文字 系统卡
+            if (props.drawData[i].usedCardType == 1) {
+              const imageInfo = paperImgInfo
+              let offsexPoint = {
+                x: topicItem.answerList[0].x - 30,
+                y: topicItem.answerList[0].y - 25,
+              }
+              let templateInfo = {
+                width: 794 - 30 * 2,
+                height: 1123 - 25 * 2,
+              }
+
+              if (paperImgInfo.width > paperImgInfo.height) {
+                // A3
+                offsexPoint = {
+                  x: topicItem.answerList[0].x - 30,
+                  y: topicItem.answerList[0].y - 25,
+                }
+                templateInfo = {
+                  width: 1588 - 30 * 2,
+                  height: 1123 - 25 * 2,
+                }
+              } else {
+                // A4
+                offsexPoint = {
+                  x: topicItem.answerList[0].x - 30,
+                  y: topicItem.answerList[0].y - 25,
+                }
+                templateInfo = {
+                  width: 794 - 30 * 2,
+                  height: 1123 - 25 * 2,
+                }
+              }
+
+              namePoint.x = parseFloat(
+                ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
+              )
+              namePoint.y = parseFloat(
+                ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
+              )
+            }
+
+            if (props.isAbnormal) {
+              if (topicItem.hasAbnormal) {
+                ctx.fillStyle = 'red'
+              } else {
+                ctx.fillStyle = 'blue'
+              }
+            } else {
+              ctx.fillStyle = 'red'
+              if (props.currentId == props.drawData[i].id) {
+                ctx.fillStyle = 'blue'
+              } else {
+                ctx.fillStyle = 'red'
+              }
+            }
+
+            if (derection == 1 || derection == 3) {
+              // 横向 或者横纵向
+              ctx.fillText(
+                topicItem.topicName,
+                parseFloat(namePoint.x - 30) * zoomRate.value * scale.value,
+                parseFloat(namePoint.y + 15) * zoomRate.value * scale.value
+              )
+            } else {
+              // 竖向
+              ctx.fillText(
+                topicItem.topicName,
+                parseFloat(namePoint.x + 1) * zoomRate.value * scale.value,
+                parseFloat(namePoint.y - 22) * zoomRate.value * scale.value
+              )
+            }
+          }
+
+          topicItem.answerList.forEach((answerItem) => {
+            let obj = {
+              x: answerItem.x * zoomRate.value * scale.value,
+              y: answerItem.y * zoomRate.value * scale.value,
+              width: answerItem.w * zoomRate.value * scale.value,
+              height: answerItem.h * zoomRate.value * scale.value,
+            }
+
+            // 系统卡
+            if (props.drawData[i].usedCardType == 1) {
+              const imageInfo = paperImgInfo
+              let offsexPoint = {
+                x: answerItem.x - 30,
+                y: answerItem.y - 25,
+                w: answerItem.w,
+                h: answerItem.h,
+              }
+              let templateInfo = {
+                width: 794 - 30 * 2,
+                height: 1123 - 25 * 2,
+              }
+
+              if (paperImgInfo.width > paperImgInfo.height) {
+                // A3
+                offsexPoint = {
+                  x: answerItem.x - 30,
+                  y: answerItem.y - 25,
+                  w: answerItem.w,
+                  h: answerItem.h,
+                }
+                templateInfo = {
+                  width: 1588 - 30 * 2,
+                  height: 1123 - 25 * 2,
+                }
+              } else {
+                // A4
+                offsexPoint = {
+                  x: answerItem.x - 30,
+                  y: answerItem.y - 25,
+                  w: answerItem.w,
+                  h: answerItem.h,
+                }
+                templateInfo = {
+                  width: 794 - 30 * 2,
+                  height: 1123 - 25 * 2,
+                }
+              }
+
+              const x = parseFloat(
+                ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
+              )
+              const y = parseFloat(
+                ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
+              )
+              const w = parseFloat(
+                ((answerItem.w / templateInfo.width) * imageInfo.width).toFixed(2)
+              )
+              const h = parseFloat(
+                ((answerItem.h / templateInfo.height) * imageInfo.height).toFixed(2)
+              )
+
+              obj = {
+                x: x * zoomRate.value * scale.value,
+                y: y * zoomRate.value * scale.value,
+                width: w * zoomRate.value * scale.value,
+                height: h * zoomRate.value * scale.value,
+              }
+            }
+
+            ctx.lineWidth = 1
+            
+            // 选中 绘制带背景阴影的边框
+            if (topicItem.hasAbnormal) {
+              // 有异常的
+              if (answerItem.isCheck) {
+                ctx.fillStyle = 'rgba(255,0,0,0.3)' // 红色背景
+                ctx.strokeStyle = 'red' // 红色边框
+                ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
+                ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
+              } else {
+                ctx.fillStyle = 'red' // 半透明红色背景
+                ctx.strokeStyle = 'red' // 边框的颜色
+                ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
+              }
+            } else {
+              if (props.isAbnormal) {
+                ctx.fillStyle = 'rgba(0,0,255,0.3)' // 半透明蓝色背景
+                ctx.strokeStyle = 'blue' // 蓝色边框
+              } else {
+                ctx.fillStyle = 'rgba(255,0,0,0.3)' // 红色背景
+                ctx.strokeStyle = 'red' // 红色边框
+
+                if (props.currentId == props.drawData[i].id) {
+                  ctx.strokeStyle = 'blue'
+                } else {
+                  ctx.strokeStyle = 'red'
+                }
+              }
+
+              // 选中
+              if (answerItem?.isCheck) {
+                ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
+                ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
+              } else {
+                ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
+              }
+            }
+          })
+        })
+      }
+    }
+
+    // 主观题划分区加载
+    if (props.drawData[i].scoreList) {
+      const scoreList = props.drawData[i].scoreList
+      scoreList.forEach((scoreItem) => {
+        ctx.font = '15px Arial'
+        ctx.fillStyle = 'red'
+        ctx.textAlign = 'left'
+
+        let obj = {
+          x: scoreItem.x * zoomRate.value * scale.value,
+          y: scoreItem.y * zoomRate.value * scale.value,
+          width: scoreItem.w * zoomRate.value * scale.value,
+          height: scoreItem.h * zoomRate.value * scale.value,
+        }
+
+        // 系统卡
+        if (props.drawData[i].usedCardType == 1) {
+          const imageInfo = paperImgInfo
+          let offsexPoint = {
+            x: scoreItem.x - 30,
+            y: scoreItem.y - 25,
+            w: scoreItem.w,
+            h: scoreItem.h,
+          }
+          let templateInfo = {
+            width: 794 - 30 * 2,
+            height: 1123 - 25 * 2,
+          }
+
+          if (paperImgInfo.width > paperImgInfo.height) {
+            // A3
+            offsexPoint = {
+              x: scoreItem.x - 30,
+              y: scoreItem.y - 25,
+              w: scoreItem.w,
+              h: scoreItem.h,
+            }
+            templateInfo = {
+              width: 1588 - 30 * 2,
+              height: 1123 - 25 * 2,
+            }
+          } else {
+            // A4
+            offsexPoint = {
+              x: scoreItem.x - 30,
+              y: scoreItem.y - 25,
+              w: scoreItem.w,
+              h: scoreItem.h,
+            }
+            templateInfo = {
+              width: 794 - 30 * 2,
+              height: 1123 - 25 * 2,
+            }
+          }
+
+          const x = parseFloat(
+            ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
+          )
+          const y = parseFloat(
+            ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
+          )
+          const w = parseFloat(
+            ((scoreItem.w / templateInfo.width) * imageInfo.width).toFixed(2)
+          )
+          const h = parseFloat(
+            ((scoreItem.h / templateInfo.height) * imageInfo.height).toFixed(2)
+          )
+
+          obj = {
+            x: x * zoomRate.value * scale.value,
+            y: y * zoomRate.value * scale.value,
+            width: w * zoomRate.value * scale.value,
+            height: h * zoomRate.value * scale.value,
+          }
+        }
+
+        ctx.lineWidth = 1
+        ctx.fillStyle = scoreItem.color + '50' // 红色背景
+        ctx.strokeStyle = scoreItem.color // 红色边框
+        
+        if (scoreItem.isCheck) {
+          ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
+        }
+        ctx.strokeRect(obj.x, obj.y, obj.width, obj.height) // 绘制答案边框
+      })
+    }
+
+    ctx.fillStyle = 'rgba(255,0,0,1)' // 半透明红色背景
+    ctx.font = '15px Arial'
+    ctx.textAlign = 'left'
+    
+    // 绘制文字 定位去和客观题组不用显示
+    if (point.blockArea != 2 && point.blockArea != 8) {
+      ctx.fillText(point.blockName, point.x, point.y - 5)
+    }
+  }
+  ctx.restore()
+}
+
+// 更新缩放率
+const updateZoomAndPaperInfo = () => {
+  const widthZoomRate = containerWidth.value / paperImgInfo.width
+  const heightZoomRate = containerHeight.value / paperImgInfo.height
+  // 选择较小的缩放率作为基准,确保图像完整显示在容器内
+  zoomRate.value = Math.min(widthZoomRate, heightZoomRate)
+}
+
+// 更新画布尺寸
+const updateCanvasSize = () => {
+  canvasInfo.width = Math.round(paperImgInfo.width * zoomRate.value * scale.value)
+  canvasInfo.height = Math.round(paperImgInfo.height * zoomRate.value * scale.value)
+  
+  if (paperCanvasRef.value) {
+    paperCanvasRef.value.width = canvasInfo.width
+    paperCanvasRef.value.height = canvasInfo.height
+  }
+}
+
+// 中心化画布
+const centerCanvas = () => {
+  position.x = (containerWidth.value - canvasInfo.width) / 2
+  position.y = (containerHeight.value - canvasInfo.height) / 2
+  
+  if (paperCanvasRef.value) {
+    paperCanvasRef.value.style.left = `${position.x}px`
+    paperCanvasRef.value.style.top = `${position.y}px`
+  }
+  isInit.value = false // 后面不在执行居中操作
+}
+
+// 全局鼠标释放事件处理
+const onGlobalMouseUp = () => {
+  isDragging.value = false
+}
+
+// 鼠标按下事件
+const onMouseDown = (event) => {
+  // 只响应左键点击(button值为0表示左键)
+  if (event.button !== 0) {
+    isDragging.value = false
+    return
+  }
+  
+  // 确保在开始新的拖拽前,先重置拖拽状态
+  isDragging.value = false
+  
+  if (drawType.value == 0) {
+    if (props.isDrag) {
+      isDragging.value = true
+      startX.value = event.clientX - position.x
+      startY.value = event.clientY - position.y
+    }
+  }
+  
+  if (drawType.value == 1) {
+    if (event.target.id != 'paperCanvas') {
+      isDragging.value = true
+      startX.value = event.clientX - position.x
+      startY.value = event.clientY - position.y
+    }
+  }
+}
+
+// 鼠标移动事件
+const onMouseMove = (event) => {
+  if (isDragging.value) {
+    // 在拖拽模式下,移动画布
+    position.x = event.clientX - startX.value
+    position.y = event.clientY - startY.value
+    
+    if (paperCanvasRef.value) {
+      paperCanvasRef.value.style.left = `${position.x}px`
+      paperCanvasRef.value.style.top = `${position.y}px`
+    }
+    ImageInfoChange()
+  }
+}
+
+// 鼠标抬起事件
+const onMouseUp = (event) => {
+  // 只响应左键释放
+  if (event && event.button !== 0) {
+    return
+  }
+  isDragging.value = false // 停止拖拽
+}
+
+// 鼠标滚轮事件
+const onWheel = (event) => {
+  event.preventDefault()
+  // 计算新的缩放比例
+  const delta = event.deltaY < 0 ? 1 : -1
+  const newScale = scale.value + delta * 0.1
+  const clampedScale = Math.max(minScale, Math.min(maxScale, newScale))
+  
+  // 如果缩放值没有变化,则直接返回
+  if (clampedScale === scale.value) return
+  
+  // 获取鼠标在容器中的位置
+  const containerRect = mainContainerRef.value.getBoundingClientRect()
+  const mouseX = event.clientX - containerRect.left
+  const mouseY = event.clientY - containerRect.top
+  
+  // 计算鼠标相对于当前画布的位置
+  const mouseRelativeToCanvasX = (mouseX - position.x) / scale.value
+  const mouseRelativeToCanvasY = (mouseY - position.y) / scale.value
+  
+  // 更新缩放比例
+  scale.value = clampedScale
+  
+  // 更新画布尺寸
+  updateCanvasSize()
+  
+  // 重新计算画布位置,使缩放围绕鼠标点进行
+  position.x = mouseX - mouseRelativeToCanvasX * scale.value
+  position.y = mouseY - mouseRelativeToCanvasY * scale.value
+  
+  // 应用新的位置
+  if (paperCanvasRef.value) {
+    paperCanvasRef.value.style.left = `${position.x}px`
+    paperCanvasRef.value.style.top = `${position.y}px`
+  }
+  
+  // 更新图片位置信息
+  ImageInfoChange()
+  
+  // 重新绘制内容
+  drawImage()
+}
+
+// 鼠标复位
+const MouseReset = () => {
+  drawType.value = 0
+  const canvas = paperCanvasRef.value
+  if (canvas) {
+    canvas.style.cursor = 'pointer'
+    canvas.onmousedown = null
+    canvas.onmousemove = null
+    canvas.onmouseup = null
+  }
+}
+
+// 框选答案区域
+const SelectionBox = () => {
+  drawType.value = 1
+  const canvas = paperCanvasRef.value
+  if (canvas) {
+    canvas.style.cursor = 'crosshair'
+    canvas.onmousedown = onCanvasDown
+    canvas.onmousemove = onCanvasMove
+    canvas.onmouseup = onCanvasUp
+  }
+}
+
+// 画圈模式
+const PaintingCircle = () => {
+  drawType.value = 1
+  const canvas = paperCanvasRef.value
+  if (canvas) {
+    canvas.style.cursor = 'crosshair'
+    canvas.onmousedown = onCanvasDown
+    canvas.onmousemove = onCanvasMove
+    canvas.onmouseup = onCanvasUp
+  }
+}
+
+// 开始画答案区域圈
+const StartPaintingCircle = (obj) => {
+  Object.assign(addObjectAreaOption, obj)
+  drawType.value = 1
+  const canvas = paperCanvasRef.value
+  if (canvas) {
+    canvas.style.cursor = 'crosshair'
+    canvas.onmousedown = onCanvasDown
+    canvas.onmousemove = onCanvasMove
+    canvas.onmouseup = onCanvasUp
+  }
+}
+
+// canvas上按下事件
+const onCanvasDown = (e) => {
+  const canvas = paperCanvasRef.value
+  if (canvas) canvas.style.cursor = 'crosshair'
+  
+  isDrawing.value = true
+  isDragging.value = false // 禁止拖拽
+  
+  rectPoint.startX = e.offsetX
+  rectPoint.startY = e.offsetY
+  rectPoint.endX = e.offsetX
+  rectPoint.endY = e.offsetY
+}
+
+// canvas上移动事件
+const onCanvasMove = (e) => {
+  if (isDrawing.value) {
+    const canvas = paperCanvasRef.value
+    if (!canvas) return
+    
+    const ctx = canvas.getContext('2d')
+    if (!ctx) return
+
+    // 更新当前鼠标位置
+    rectPoint.endX = e.offsetX
+    rectPoint.endY = e.offsetY
+    
+    // 清除之前的矩形
+    ctx.clearRect(0, 0, canvas.width, canvas.height)
+    // 重新绘制之前的边框数据
+    drawImage()
+    
+    // 设置矩形样式
+    ctx.strokeStyle = 'blue'
+    ctx.lineWidth = 1 // 画框的线的粗细
+    
+    // 计算矩形的起点和宽高,处理反向框选
+    const startX = rectPoint.startX
+    const startY = rectPoint.startY
+    const width = e.offsetX - startX
+    const height = e.offsetY - startY
+    
+    // 绘制矩形(可以处理负宽度和高度)
+    ctx.strokeRect(startX, startY, width, height)
+  }
+}
+
+// canvas抬起事件
+const onCanvasUp = () => {
+  isDrawing.value = false
+  
+  // 处理反向框选,确保宽度和高度为正数
+  let startX = rectPoint.startX
+  let startY = rectPoint.startY
+  let endX = rectPoint.endX
+  let endY = rectPoint.endY
+  
+  // 确保起点坐标是左上角,终点坐标是右下角
+  let actualStartX = Math.min(startX, endX)
+  let actualStartY = Math.min(startY, endY)
+  let actualEndX = Math.max(startX, endX)
+  let actualEndY = Math.max(startY, endY)
+  
+  // 计算实际的宽度和高度(确保为正数)
+  let width = Math.abs(actualEndX - actualStartX)
+  let height = Math.abs(actualEndY - actualStartY)
+  
+  const point = {
+    x: floatNum(actualStartX / zoomRate.value / scale.value),
+    y: floatNum(actualStartY / zoomRate.value / scale.value),
+    w: floatNum(width / zoomRate.value / scale.value),
+    h: floatNum(height / zoomRate.value / scale.value),
+    unit: 'px',
+  }
+  
+  Object.assign(currenPoint, point)
+  
+  // 只有当宽度和高度都大于0时才触发事件
+  if (point.w > 0 && point.h > 0) {
+    emit('GetRectPoint', point)
+  }
+}
+
+// 鼠标离开画布事件
+const onCanvasLeave = () => {
+  // 如果正在绘制,自动结束绘制
+  if (isDrawing.value) {
+    onCanvasUp()
+  }
+}
+
+// 监听窗口大小变化,重新计算表格高度 使用节流防止频繁改变窗口大小导致计算量过大而页面卡顿
+const handleResize = throttle(() => {
+  // 如果需要重新计算,可以在这里 uncomment
+  // if (mainContainerRef.value) {
+  //   const { width, height } = mainContainerRef.value.getBoundingClientRect()
+  //   containerWidth.value = width
+  //   containerHeight.value = height
+  //   updateZoomAndPaperInfo()
+  //   updateCanvasSize()
+  //   centerCanvas()
+  //   loadImage()
+  // }
+}, 500)
+
+// Watchers
+watch(
+  () => props.paperImgUrl,
+  (newVal) => {
+    loading.value = true // 加载图片
+    initData() // 初始化数据
+    setTimeout(() => {
+      if (!props.paperImgUrl) {
+        loading.value = false // 加载图片完成
+      }
+    }, 100)
+  },
+  { deep: true, immediate: true }
+)
+
+watch(
+  () => props.drawData,
+  () => {
+    drawImage() // 更新边框数据并重新绘制
+  },
+  { deep: true }
+)
+
+watch(
+  () => props.currentId,
+  () => {
+    drawImage() // 更新边框数据并重新绘制
+  }
+)
+
+// Lifecycle Hooks
+onMounted(() => {
+  window.addEventListener('resize', handleResize)
+  // 添加全局鼠标释放事件监听器,处理异常情况
+  window.addEventListener('mouseup', onGlobalMouseUp)
+  
+  // 初始数据处理加载 如定位 居中 等 第一次居中
+  initData()
+})
+
+onBeforeUnmount(() => {
+  // 移除监听 防止内存泄漏
+  window.removeEventListener('resize', handleResize)
+  window.removeEventListener('mouseup', onGlobalMouseUp)
+})
+
+// 暴露方法给父组件(如果需要)
+defineExpose({
+  ShowDrawData,
+  fitScreen,
+  MouseReset,
+  SelectionBox,
+  PaintingCircle,
+  StartPaintingCircle,
+})
+</script>
+
+<style lang="scss" scoped>
+.paper_canvas {
+  position: absolute;
+  cursor: pointer;
+  background-color: transparent;
+  z-index: 10; // 使canvas在图片上方
+  // border:1px solid green;
+}
+
+.img_container {
+  position: absolute;
+  cursor: pointer;
+  z-index: 9;
+  // border:1px solid red;
+  img {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.main_container {
+  position: relative;
+  overflow: hidden;
+}
+</style>

+ 180 - 0
src/styles/common.scss

@@ -2250,6 +2250,186 @@ body {
   }
 }
 
+//预览弹窗公共样式 背景透明
+.preview_dialog {
+   background: transparent !important;
+  //饿了么样式覆盖
+  .el-dialog__header {
+    display: none;
+  }
+
+  .el-dialog {
+    border-radius: 0;
+  }
+
+  //饿了么内容样式覆盖
+  .el-dialog__body {
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    padding: 0 !important;
+    width: 100%;
+    height: 100%;
+    margin: auto;
+    // min-height: 99px;
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    position: relative;
+  }
+
+  .preview_tool {
+    position: absolute;
+    width: 446px;
+    left: calc(50% - 223px);
+    height: 66px;
+    background: #FFFFFF;
+    border-radius: 10px 10px 10px 10px;
+    border: 1px solid #E4E7ED;
+    display: flex;
+    text-align: center;
+    align-items: center;
+    justify-content: space-around;
+    bottom: 12px;
+    padding-left: 10px;
+    padding-right: 10px;
+
+    .tool_item {
+      width: auto;
+      height: 46px;
+
+      color: #666666;
+      cursor: pointer;
+      text-align: center;
+
+      .item_icon {
+        font-weight: 400;
+        font-size: 25px;
+        color: #666666;
+
+        .iconfont {
+          font-size: 24px !important;
+        }
+      }
+
+      .item_title {
+        font-weight: 400;
+        font-size: 14px;
+        color: #666666;
+      }
+    }
+
+    .tool_item:hover {
+      .item_icon {
+        font-weight: 400;
+        font-size: 25px;
+        color: #2E64FA;
+
+        .iconfont {
+          font-size: 24px !important;
+        }
+      }
+
+      .item_title {
+        font-weight: 400;
+        font-size: 14px;
+        color: #2E64FA;
+      }
+    }
+  }
+
+  .preview_image {
+    width: 100%;
+    height: 80%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-top: 0;
+    position: relative;
+
+    img {
+      height: 100%;
+      object-fit: contain;
+    }
+
+    //关闭图片按钮
+    .preview_close {
+      position: absolute;
+      width: 48px;
+      height: 48px;
+      display: flex;
+      border-radius: 50%;
+      top: 0;
+      right: 60px;
+      // background-color: #000000;
+      background: rgba(0, 0, 0, 0.4);
+      text-align: center;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+
+      i {
+        font-size: 30px;
+        color: #fff;
+
+      }
+    }
+
+    //上一个
+    .preview_last {
+      position: absolute;
+      width: 48px;
+      height: 48px;
+      top: 50%;
+      display: flex;
+      border-radius: 50%;
+      background: rgba(0, 0, 0, 0.4);
+      text-align: center;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      left: 60px;
+
+      i {
+        font-size: 30px;
+        color: #fff;
+
+      }
+    }
+
+    //下一个
+    .preview_next {
+      position: absolute;
+      width: 48px;
+      height: 48px;
+      top: 50%;
+
+      right: 60px;
+      display: flex;
+      border-radius: 50%;
+      background: rgba(0, 0, 0, 0.4);
+      text-align: center;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+
+      i {
+        font-size: 30px;
+        color: #fff;
+
+      }
+    }
+
+    .button_diasble {
+      i {
+        color: gray !important;
+      }
+    }
+
+
+  }
+}
+
 .dialog_table {
 }
 

+ 107 - 112
src/views/exam/components/paperView.vue → src/views/exam/components/PaperPreview.vue

@@ -1,64 +1,58 @@
 <template>
-  <el-dialog
-    title=""
-    :visible.sync="showDialog"
-    class="preview_dialog"
+  <el-dialog 
+    title="" 
+    v-model="showDialog" 
+    class="preview_dialog" 
     :modal-append-to-body="false"
     append-to-body
-    fullscreen
+    fullscreen 
     height="100%"
   >
     <div class="preview_image">
-      <div v-if="showDraw" class="preview_canvas">
-        <!-- Vue3中 ref 绑定到变量 -->
-        <!-- <ActionImage
-          ref="ActionCanvasRef"
-          :isDrag="true"
-          :paperImgUrl="currenImageUrl"
+      <div v-if="showDraw && showDialog" class="preview_canvas">
+        <ActionImage 
+          ref="ActionCanvasRef" 
+          :isDrag="true" 
+          :paperImgUrl="currenImageUrl" 
           :drawData="currentDrawData"
-        ></ActionImage> -->
-        <div>
-            <image
-              :src="currenImageUrl"
-              alt=""></image>
-        </div>
+        ></ActionImage>
       </div>
       <div v-else>
-        <img
-          :src="currenImageUrl"
-          alt=""
-          :style="{
-            transform: `scale(${zoomLevel}) rotate(${rotateAngle}deg)`,
-            transition: 'transform 0.2s',
+        <img 
+          :src="currenImageUrl" 
+          alt="" 
+          :style="{ 
+            transform: `scale(${zoomLevel}) rotate(${rotateAngle}deg)`, 
+            transition: 'transform 0.2s' 
           }"
         />
       </div>
-
+      
       <div class="preview_close" @click="CloseDialog()">
-        <i class="el-icon-close"></i>
+        <el-icon><Close /></el-icon>
       </div>
-      <div
-        class="preview_last"
-        :class="currentIndex == 0 ? 'button_diasble' : ''"
-        v-if="imageList.length > 1"
+      <div 
+        class="preview_last" 
+        :class="currentIndex == 0 ? 'button_diasble' : ''" 
+        v-if="imageList.length > 1" 
         @click="LastImage()"
       >
-        <i class="el-icon-arrow-left"></i>
+        <el-icon><ArrowLeft /></el-icon>
       </div>
-      <div
-        class="preview_next"
-        :class="currentIndex == imageList.length - 1 ? 'button_diasble' : ''"
-        v-if="imageList.length > 1"
+      <div 
+        class="preview_next" 
+        :class="currentIndex == imageList.length - 1 ? 'button_diasble' : ''" 
+        v-if="imageList.length > 1" 
         @click="NextImage()"
       >
-        <i class="el-icon-arrow-right"></i>
+        <el-icon><ArrowRight /></el-icon>
       </div>
     </div>
     <div class="preview_tool" v-if="showTool">
-      <div
-        class="tool_item"
-        v-for="(item, index) in toolList"
-        :key="'tool_' + index"
+      <div 
+        class="tool_item" 
+        v-for="(item, index) in toolList" 
+        :key="'tool_' + index" 
         @click="ToolClick(item)"
       >
         <div class="item_icon">
@@ -74,14 +68,15 @@
 
 <script setup>
 import { ref, watch, onBeforeUnmount } from 'vue'
-import ActionImage from '@/components/ActionImage.vue' // 制作模板画布组件
+import { Close, ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
+import ActionImage from '@/components/ActionImage.vue'
 
 // 定义 Props
 const props = defineProps({
-  value: {
+  modelValue: {
     type: Boolean,
     required: true,
-  }, // 是否显示弹窗
+  },
   imageList: {
     type: Array,
     default: () => [],
@@ -89,43 +84,67 @@ const props = defineProps({
   index: {
     type: Number,
     default: 0,
-  }, // 默认显示的图片索引
+  },
   showTool: {
     type: Boolean,
     default: false,
-  }, // 是否显示工具栏
+  },
   showDraw: {
     type: Boolean,
     default: false,
-  }, // 是否显示绘图框
+  },
   answerData: {
     type: Object,
     default: () => ({}),
-  }, // 答案
+  },
 })
 
 // 定义 Emits
-const emit = defineEmits(['input', 'GetRotateAngle'])
+const emit = defineEmits(['update:modelValue', 'GetRotateAngle'])
 
 // 响应式数据
-const currenImageUrl = ref('') // 当前图片url
-const currentPageNo = ref(1) // 当前页码 默认第一页
-const currentIndex = ref(0) // 当前图片索引
+const currenImageUrl = ref('')
+const currentPageNo = ref(1)
+const currentIndex = ref(0)
 
 const toolList = ref([
-  { title: '放大', icon: 'icon_fangda', value: '1' },
-  { title: '缩小', icon: 'icon_suoxiao', value: '2' },
-  { title: '逆时针1°', icon: 'icon_nishizhen1', value: '3' },
-  { title: '顺时针1°', icon: 'icon_shunshizhen1', value: '4' },
-  { title: '逆时针90°', icon: 'icon_nishizhen90', value: '5' },
-  { title: '顺时针90°', icon: 'icon_shunshizhen90', value: '6' },
+  {
+    title: '放大',
+    icon: 'icon_fangda',
+    value: '1',
+  },
+  {
+    title: '缩小',
+    icon: 'icon_suoxiao',
+    value: '2',
+  },
+  {
+    title: '逆时针1°',
+    icon: 'icon_nishizhen1',
+    value: '3',
+  },
+  {
+    title: '顺时针1°',
+    icon: 'icon_shunshizhen1',
+    value: '4',
+  },
+  {
+    title: '逆时针90°',
+    icon: 'icon_nishizhen90',
+    value: '5',
+  },
+  {
+    title: '顺时针90°',
+    icon: 'icon_shunshizhen90',
+    value: '6',
+  },
 ])
 
-const zoomLevel = ref(1) // 图片缩放比例
-const maxZoom = ref(3) // 最大缩放比例
-const minZoom = ref(0.5) // 最小缩放比例
-const zoomStep = ref(0.1) // 每次缩放的步进
-const rotateAngle = ref(0) // 图片旋转角度
+const zoomLevel = ref(1)
+const maxZoom = ref(3)
+const minZoom = ref(0.5)
+const zoomStep = ref(0.1)
+const rotateAngle = ref(0)
 
 const currentPageInfo = ref({})
 const currentDrawData = ref([])
@@ -133,38 +152,29 @@ const currentDrawData = ref([])
 // Ref 引用
 const ActionCanvasRef = ref(null)
 
-// Computed: showDialog
-// 在 Vue3 script setup 中,通常通过 watch props.value 或直接在使用处判断
-// 由于 el-dialog 使用 :visible.sync,我们需要一个内部状态或计算属性来双向绑定
-// 这里为了保持逻辑清晰,我们直接使用 props.value 作为显示依据,并通过 emit 更新
-// 注意:el-dialog 的 visible.sync 在 Vue3 + Element Plus 中通常改为 v-model
-// 如果使用的是 Element UI (Vue2版本库),则保持 :visible.sync 并手动处理 input 事件
-
-const showDialog = ref(props.value)
+// 计算属性:showDialog
+const showDialog = ref(props.modelValue)
 
-// 监听 props.value 变化同步内部状态
+// 监听 props.modelValue 变化
 watch(
-  () => props.value,
+  () => props.modelValue,
   (newVal) => {
     showDialog.value = newVal
     if (newVal) {
       currentIndex.value = props.index
-      LoadPaperInfo() // 加载页面相关数据
+      LoadPaperInfo()
     }
   }
 )
 
-// 监听 showDialog 内部变化(如点击遮罩关闭)同步回父组件
+// 监听 showDialog 内部变化同步回父组件
 watch(showDialog, (newVal) => {
-  emit('input', newVal)
+  emit('update:modelValue', newVal)
 })
 
-// 方法定义
-
 // 关闭弹窗
 const CloseDialog = () => {
   showDialog.value = false
-  // 关闭时发送回当前的旋转角度
   const obj = {
     currentIndex: currentIndex.value,
     rotateAngle: rotateAngle.value,
@@ -183,6 +193,8 @@ const GetReturn = () => {
 
 // 加载页面相关信息
 const LoadPaperInfo = () => {
+
+    console.log('打印当前图片列表', props.imageList)
   if (!props.imageList || props.imageList.length === 0) return
   
   const currentItem = props.imageList[currentIndex.value]
@@ -191,9 +203,10 @@ const LoadPaperInfo = () => {
   currenImageUrl.value = currentItem.url
   currentPageNo.value = currentItem.pageNo
   rotateAngle.value = currentItem.rotateAngle || 0
+  console.log('打印当前图片信息', currentItem)
   
   if (props.showDraw) {
-    LoadDrawData() // 加载画框信息
+    LoadDrawData()
   }
 }
 
@@ -201,7 +214,7 @@ const LoadPaperInfo = () => {
 const LastImage = () => {
   if (currentIndex.value > 0) {
     currentIndex.value--
-    LoadPaperInfo() // 加载页面相关数据
+    LoadPaperInfo()
   }
 }
 
@@ -209,7 +222,7 @@ const LastImage = () => {
 const NextImage = () => {
   if (currentIndex.value < props.imageList.length - 1) {
     currentIndex.value++
-    LoadPaperInfo() // 加载页面相关数据
+    LoadPaperInfo()
   }
 }
 
@@ -255,32 +268,32 @@ const ZoomOut = () => {
 // 逆时针旋转1度
 const RotateCounterClockwiseOne = () => {
   rotateAngle.value -= 1
-  GetReturn() // 获取返回角度
+  GetReturn()
 }
 
 // 顺时针旋转1度
 const RotateClockwiseOne = () => {
   rotateAngle.value += 1
-  GetReturn() // 获取返回角度
+  GetReturn()
 }
 
 // 逆时针旋转90度
 const RotateCounterClockwise90 = () => {
   rotateAngle.value -= 90
-  GetReturn() // 获取返回角度
+  GetReturn()
 }
 
 // 顺时针旋转90度
 const RotateClockwise90 = () => {
   rotateAngle.value += 90
-  GetReturn() // 获取返回角度
+  GetReturn()
 }
 
 // 加载绘图数据
 const LoadDrawData = () => {
   try {
     const templateDataStr = localStorage.getItem('templateData')
-    const usedCardTypeStr = localStorage.getItem('usedCardType') // 卡类型
+    const usedCardTypeStr = localStorage.getItem('usedCardType')
     
     if (!templateDataStr) return
     
@@ -311,7 +324,6 @@ const LoadDrawData = () => {
     }
 
     if (usedCardType == 1) {
-      // 系统卡
       currentPageInfo.value = {
         width: paperInfo.width - 30,
         height: paperInfo.height - 25,
@@ -319,7 +331,6 @@ const LoadDrawData = () => {
     }
 
     if (currentTemplateItem.examCardGroupVOS) {
-      // 找到客观题的切块数据 (blockArea == 8)
       const list = currentTemplateItem.examCardGroupVOS.filter(
         (item) => item.blockArea == 8
       )
@@ -334,23 +345,20 @@ const LoadDrawData = () => {
             return
           }
           
-          const unit = 'px' // 默认px单位
+          const unit = 'px'
           let topiclist = []
           
           if (item.groupTopicList) {
-            // 处理答案数据
             item.groupTopicList.forEach((topic) => {
               const answerValue = props.answerData[topic.topicName]
               
               if (answerValue) {
-                // 将多个字母拆分成数组(兼容多种分隔符)
                 const answerLetters = answerValue.match(/[A-Za-z]/g) || []
                 
                 answerLetters.forEach((letter) => {
                   let answerIndex = GetAnswerIndexToLetter(letter)
 
                   if (topic.topicType == 3) {
-                    // 判断题
                     answerIndex = GetAnswerIndexToLetterTF(letter)
                   }
                   
@@ -373,16 +381,15 @@ const LoadDrawData = () => {
             page: currentPageNo.value,
             blockName: item.name,
             id: item.groupId,
-            unit: unit, // 单位
+            unit: unit,
             usedCardType: usedCardType,
-            areaOption: item.groupConfig, // 添加选择题设置信息
+            areaOption: item.groupConfig,
             topiclist: topiclist,
           }
           drawList.push(obj)
         }
       })
 
-      // 找到选做题的切块数据 (blockArea == 9)
       const list2 = currentTemplateItem.examCardGroupVOS.filter(
         (item) => item.blockArea == 9
       )
@@ -397,17 +404,14 @@ const LoadDrawData = () => {
             return
           }
           
-          const unit = 'px' // 默认px单位
+          const unit = 'px'
           let topiclist = []
           
           if (item.groupTopicList) {
-            // 处理答案数据
             item.groupTopicList.forEach((topic) => {
               const answerValue = props.answerData[topic.topicName]
               
-              // 对于选做题,只需要检查值是否存在且不是null或undefined
               if (answerValue !== undefined && answerValue !== null) {
-                // 检查answerValue是否为有效数字
                 if (!isNaN(answerValue) && answerValue !== '') {
                   const answerIndex = answerValue - 1
                   if (answerIndex >= 0 && topic.answerList[answerIndex]) {
@@ -429,9 +433,9 @@ const LoadDrawData = () => {
             page: currentPageNo.value,
             blockName: item.name,
             id: item.groupId,
-            unit: unit, // 单位
+            unit: unit,
             usedCardType: usedCardType,
-            areaOption: item.groupConfig, // 添加选择题设置信息
+            areaOption: item.groupConfig,
             topiclist: topiclist,
           }
           drawList.push(obj)
@@ -463,17 +467,8 @@ const GetAnswerIndexToLetterTF = (value) => {
 }
 
 // 生命周期
-// mounted 逻辑已在 watch(props.value) 中处理初始加载
-// 如果需要初始加载且 value 默认为 true,可以在这里调用
-// onMounted(() => {
-//   if (props.value) {
-//     currentIndex.value = props.index
-//     LoadPaperInfo()
-//   }
-// })
-
 onBeforeUnmount(() => {
-  // 清理工作,如果有定时器或事件监听器在此移除
+  // 清理工作
 })
 </script>
 
@@ -486,11 +481,12 @@ onBeforeUnmount(() => {
   align-items: center;
   margin-top: 0;
   position: relative;
-  background-color: red;
 }
+
 .preview_canvas {
   width: 100%;
   height: 100%;
+  
   .main_container {
     width: calc(100% - 240px);
     margin: auto;
@@ -502,7 +498,6 @@ onBeforeUnmount(() => {
     display: flex;
     justify-content: center;
     align-items: center;
-    // border:1px solid red;
 
     .paper_template {
       position: absolute;

+ 22 - 8
src/views/exam/scanDetail.vue

@@ -127,7 +127,7 @@
             </div>
         </div>
     </div>
-    <PaperView v-model="showPaperDialog" :imageList="PaperImageList"></PaperView>
+    <PaperPreview v-model="showPaperDialog" :imageList="previewImageList" :showDraw="true" :answerData="previewAnswerData"></PaperPreview>
   </div>
 </template>
 <script lang="ts" setup>
@@ -137,7 +137,7 @@ import { onMounted ,ref,computed,nextTick,onUnmounted} from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus'
 
 import { getBatchDetailList,getBatchDetailImage } from '@/api/exam'
-import PaperView from './components/PaperView.vue'
+import PaperPreview from './components/PaperPreview.vue'
 
 // 定义选项接口类型
 interface OptionItem {
@@ -183,7 +183,8 @@ const imageHeight=ref(500);
 const isImportStudent=ref(false);//是否导入了学生名单
 const showSelectStudent=ref(false);//是否显示选择学生名单弹窗
 const showPaperDialog=ref(false);//显示图片详情弹窗
-const PaperImageList=ref([]);//图片列表
+const previewImageList=ref([]);//图片列表
+const previewAnswerData=ref({});
 // 打开导入学生名单
 const OpenImportStudent = () => {
     showSelectStudent.value=true;
@@ -278,11 +279,24 @@ const LoadData = () => {
 
 
 //查看图片
-const OpenViewPaper = (image:any) => {
-
-    console.log("查看图片",image);
-    let imageUrl=image.scanPictureVOS[0].recognizeUrl;
-    PaperImageList.value.push(imageUrl);
+const OpenViewPaper = (item:any) => {
+
+    console.log("查看图片",item);
+    previewImageList.value=[];//初始清空
+    item.scanPictureVOS.forEach((image:any)=>{
+        console.log("image",image);
+        let obj={
+            url:image.recognizeUrl,
+            pageNo:image.pageNo,//页面页码
+        };
+        previewImageList.value.push(obj);
+    })
+    previewAnswerData.value={ ...item.objectiveQuestionMap, ...item.selectiveQuestionMap };
+    console.log("previewImageList",previewImageList.value);
+    console.log("previewAnswerData",previewAnswerData.value);
+    // // 合并两个对象
+    // this.previewAnswerData = { ...item.objectiveQuestionMap, ...item.selectiveQuestionMap };
+    // this.showPaperPreview=true;
     showPaperDialog.value=true;
 }
 

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.