|
|
@@ -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
|
|
|
+
|
|
|
+ // 获取容器的宽高
|
|
|
+ const { width, height } = mainContainerRef.value.getBoundingClientRect()
|
|
|
+ containerHeight.value = Number(height)
|
|
|
+ containerWidth.value = Number(width)
|
|
|
+
|
|
|
+ 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>
|