| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026 |
- <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)
- 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>
|