双图版本(模板对比)
业务描述:模板与图片对比,只操作模板框选的位置进行色差对比,传框选坐标位置给后端,返回对比结果显示
draw.js文件:
- 新增了 createUuid,和求取两个数组差集的方法getArrayDifference
- 新增了两个参数:是否可删除delEnable, 是否可新增newEnable-‘1’是可,‘0’是不可
/*** 画布中绘制矩形* 参数: cav-画布对象 list-矩形数组 i-选中矩形下标**/
let globalZoom = 1/* 操作执行方法分发 */
export function draw(cav, list, i, zoom,) {globalZoom = zoom || globalZoom// 画布初始化let ctx = cav.getContext('2d')ctx.strokeStyle = 'red'ctx.lineWidth = 2// 变量初始化let sX = 0 // 鼠标X坐标let sY = 0 // 鼠标Y坐标/**鼠标移动进行第一层判断, 区分情况: 无矩形, 已有矩形无选中, 已有选中矩形*/cav.onmousemove = function (em) {sX = em.offsetXsY = em.offsetYlet iem = undefined // 鼠标移动时临时存储当前鼠标所在矩形的下标if (list.length === 0) {// **** 无矩形 ****// 绘制新矩形newDraw(cav, ctx, list)} else if (i === undefined) {// **** 已有矩形无选中 ****// 判断鼠标位置list.forEach(function (value, index, array) {if (value.w > 0 &&value.h > 0 &&sX > value.x &&sX < value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// 鼠标在右下方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem)}if (value.w < 0 &&value.h > 0 &&sX < value.x &&sX > value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// 鼠标在左下方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem)}if (value.w > 0 &&value.h < 0 &&sX > value.x &&sX < value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// 鼠标在右上方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem)}if (value.w < 0 &&value.h < 0 &&sX < value.x &&sX > value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// 鼠标在左上方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem)}if (iem === undefined) {// 鼠标不在矩形中newDraw(cav, ctx, list)}})} else {// **** 已有选中矩形 ****// 判断鼠标位置for (let index = 0; index < list.length; index++) {let value = list[index]if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + 5 && sY > value.y - 5) {// *** 鼠标在起点角 ***if (index === i) {changeDraw(cav, ctx, list, i, 1)break}} else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + 5 && sY > value.y - 5) {// *** 鼠标在起点横向角 ***if (index === i) {changeDraw(cav, ctx, list, i, 2)break}} else if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {// *** 鼠标在起点纵向角 ***if (index === i) {changeDraw(cav, ctx, list, i, 3)break}} else if (sX < value.x + value.w + 5 &&sX > value.x + value.w - 5 &&sY < value.y + value.h + 5 &&sY > value.y + value.h - 5) {// *** 鼠标在终点角 ***if (index === i) {changeDraw(cav, ctx, list, i, 4)break}} else if (value.w > 0 &&value.h > 0 &&sX > value.x &&sX < value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// *** 鼠标在右下方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index)break} else if (value.w < 0 &&value.h > 0 &&sX < value.x &&sX > value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// *** 鼠标在左下方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index)break} else if (value.w > 0 &&value.h < 0 &&sX > value.x &&sX < value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// *** 鼠标在右上方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index)break} else if (value.w < 0 &&value.h < 0 &&sX < value.x &&sX > value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// *** 鼠标在左上方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index)break} else {if (iem === undefined) {// *** 鼠标不在矩形中 ***newDraw(cav, ctx, list)}}}}/* 鼠标移出画布区域时保存选中矩形下标(如有) */cav.onmouseout = function (eo) {if (i !== undefined) {// 初始化draw(cav, list, i)}}}// console.log(cav, list, i);return list
}/* 编辑矩形四个角 */
function changeDraw(cav, ctx, list, i, site) {cav.style.cursor = 'pointer'// site: 操作矩形角的位置, 1-起点 2-起点横向 3-起点纵向 4-终点let mark = list[i]/* 按下鼠标左键 */cav.onmousedown = function (ed) {// 保存鼠标落下位置的X, Y坐标, firefox中鼠标移动后ed.offsetX ed.offsetY会变成 0, 需要使用临时参数存储起来let sX = ed.offsetX // 起点X坐标let sY = ed.offsetY // 起点Y坐标/* 移动鼠标 */cav.onmousemove = function (em) {// 计算绘制数据let iframe = {}switch (site) {case 1:iframe = {x: em.offsetX,y: em.offsetY,w: mark.w - (em.offsetX - sX),h: mark.h - (em.offsetY - sY)}breakcase 2:iframe = {x: mark.x,y: mark.y + (em.offsetY - sY),w: mark.w + (em.offsetX - sX),h: mark.h - (em.offsetY - sY)}breakcase 3:iframe = {x: mark.x + (em.offsetX - sX),y: mark.y,w: mark.w - (em.offsetX - sX),h: mark.h + (em.offsetY - sY)}breakcase 4:iframe = {x: mark.x,y: mark.y,w: mark.w + (em.offsetX - sX),h: mark.h + (em.offsetY - sY)}break}list.splice(i, 1, iframe)// 重新绘制reDraw(cav, ctx, list, i)}/* 鼠标离开矩形区 */cav.onmouseout = function (eo) {// 重新绘制reDraw(cav, ctx, list)// 初始化draw(cav, list)}/* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */delDraw(cav, ctx, list, i)}
}/* 绘制新矩形 */
function newDraw(cav, ctx, list) {cav.style.cursor = 'crosshair'// 初始化变量let start = false // 画框状态, false时不执行画框操作let sX = 0 // 起点X坐标let sY = 0 // 起点Y坐标/* 按下鼠标左键 */cav.onmousedown = function (ed) {/* 使用变量 */start = truesX = ed.offsetXsY = ed.offsetY/* 重置按键监听, 防止选中取消后仍可删除 */delDraw(cav, ctx, list, null)/* 鼠标移动 */cav.onmousemove = function (em) {if (start) {// 重新绘制reDraw(cav, ctx, list)// 设置边框为虚线ctx.beginPath()ctx.setLineDash([8, 4])ctx.rect(sX, sY, em.offsetX - sX, em.offsetY - sY)ctx.stroke()}}/* 鼠标抬起 */cav.onmouseup = function (eu) {if (start && Math.abs(eu.offsetX - sX) > 10 && Math.abs(eu.offsetY - sY) > 10) {// 改变矩形数组let frame = {x: sX,y: sY,w: eu.offsetX - sX,h: eu.offsetY - sY}list.push(frame)// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list)} else {// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list)}}/* 鼠标离开矩形区 */cav.onmouseout = function (eo) {if (start && Math.abs(eo.offsetX - sX) > 10 && Math.abs(eo.offsetY - sY) > 10) {// 改变矩形数组let frame = {x: sX,y: sY,w: eo.offsetX - sX,h: eo.offsetY - sY}list.push(frame)// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list)} else {// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list)}}}
}/* 选中矩形, 重绘矩形, 并分发后续事件 */
function judgeDraw(cav, ctx, list, iem) {cav.style.cursor = 'default'// 初始化变量let sX = 0 // 起点X坐标let sY = 0 // 起点Y坐标/* 按下鼠标左键 */cav.onmousedown = function (ed) {sX = ed.offsetXsY = ed.offsetY// 更改选中状态, 重绘矩形reDraw(cav, ctx, list, iem)/* 当仅点击选中矩形便抬起鼠标后, 重新初始化画布 */cav.onmouseup = function () {// 重绘矩形reDraw(cav, ctx, list, iem)// 初始化draw(cav, list, iem)}/* 按住拖动鼠标, 移动选中矩形*/moveDraw(cav, ctx, list, iem, sX, sY)/* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */delDraw(cav, ctx, list, iem)}
}/* 移动矩形 */
function moveDraw(cav, ctx, list, i, sX, sY) {let mark = list[i]cav.onmousemove = function (em) {let iframe = {x: mark.x + (em.offsetX - sX),y: mark.y + (em.offsetY - sY),w: mark.w,h: mark.h}list.splice(i, 1, iframe)/* 监听键盘, 使矩形在移动后仍可删除, 在点击未移动过的矩形时仍有一次监听 */delDraw(cav, ctx, list, i)// 重新绘制reDraw(cav, ctx, list, i)}cav.onmouseup = function () {// 重绘矩形reDraw(cav, ctx, list, i)// 初始化draw(cav, list, i)}
}/* 删除矩形 */
function delDraw(cav, ctx, list, i) {/* 按键事件 */if (i === null) {// i为null时阻止按键监听事件冒泡document.onkeydown = function (k) {return false}} else {// 监听按键事件document.onkeydown = function (k) {let key = k.keyCode || k.whichif ((key == 46 || key == 8) && i !== null) {if (list.length >= 1) {// 删除数组元素list.splice(i, 1)// 重绘矩形reDraw(cav, ctx, list)} else {/* 矩形数组长度为0, 已将矩形框全部删除 */ctx.clearRect(0, 0, cav.width, cav.height)}// 重置监听状态, 防止删除完毕后, 按键监听不消失delDraw(cav, ctx, list, null)// 重绘矩形reDraw(cav, ctx, list)// 初始化draw(cav, list)}}}
}/* 重绘所有矩形 */
function reDraw(cav, ctx, list, i) {ctx.setLineDash([8, 0]) // 设置边框为实线ctx.clearRect(0, 0, cav.width, cav.height)// 绘制未选中部分list.forEach(function (value, index, array) {if (i === undefined || index != i) {ctx.beginPath()ctx.strokeStyle = 'red'ctx.rect(value.x, value.y, value.w, value.h)ctx.fillStyle = 'red'if (globalZoom <= 0.5) {ctx.font = '14px sans-serif'} else {ctx.font = '20px sans-serif'}ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)ctx.stroke()}})// 绘制已选中部分list.forEach(function (value, index, array) {if (index === i) {/* 绘制方框 */ctx.beginPath()ctx.strokeStyle = 'red'ctx.rect(value.x, value.y, value.w, value.h)ctx.fillStyle = 'RGBA(102,102,102,0.2)'ctx.fillRect(value.x, value.y, value.w, value.h)ctx.fillStyle = 'red'if (globalZoom <= 0.5) {ctx.font = '14px sans-serif'} else {ctx.font = '20px sans-serif'}ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)ctx.stroke()// 绘制四个角的圆圈ctx.beginPath()ctx.strokeStyle = 'red'ctx.arc(value.x, value.y, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画起点实心圆ctx.stroke()ctx.beginPath()ctx.arc(value.x, value.y + value.h, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画起点纵向实心圆ctx.stroke()ctx.beginPath()ctx.arc(value.x + value.w, value.y + value.h, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画起点横向实心圆ctx.stroke()ctx.beginPath()ctx.arc(value.x + value.w, value.y, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画终点实心圆ctx.stroke()}})
}
ColorDifference.vue文件
<template><a-spin tip="Loading..." :spinning="spinning"><a-card title="色差差异检测" :bordered="false"><a-checkbox v-model:checked="checked" style="margin-bottom: 15px">是否进行全局色差检测</a-checkbox><div class="difference-wrap"><a-uploadv-model:file-list="templateFileList"list-type="picture-card"class="content-upload":show-upload-list="false":openFileDialogOnClick="!templateUrl":before-upload="beforeUpload":maxCount="1"@change="handleTemplateChange"><divv-if="templateUrl"class="content-wrap":class="[temContentWrapClassFlag ? 'content-wrap-flag' : '']"ref="contentWrapRef"><div class="tem-content" ref="temContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }"><img :src="templateUrl" /><canvas ref="temMarkCanvasRef"></canvas></div></div><div v-else><plus-outlined style="font-size: 28px"></plus-outlined><div class="ant-upload-text">上传模板</div></div></a-upload><a-uploadv-model:file-list="imgFileList"list-type="picture-card"class="content-upload":maxCount="1":show-upload-list="false":openFileDialogOnClick="!imgUrl":before-upload="beforeUpload"@change="handleImageChange"><div v-if="imgUrl" class="content-wrap" :class="[imgContentWrapClassFlag ? 'content-wrap-flag' : '']"><div class="img-content" ref="imgContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }"><img :src="imgUrl" /><canvas ref="imgMarkCanvasRef"></canvas></div></div><div v-else><plus-outlined style="font-size: 28px"></plus-outlined><div class="ant-upload-text">上传图片</div></div></a-upload></div><div class="actionBar"><a-button class="btn" :disabled="zoom === 2" type="primary" @click="scaleLarge">放大</a-button><a-button class="btn" :disabled="zoom === 0.1" type="primary" @click="scaleSmall">缩小</a-button><a-uploadv-if="templateUrl"v-model:file-list="templateFileList":maxCount="1":before-upload="beforeUpload"@change="renewTemplate"><a-button class="btn" type="primary">上传模板</a-button></a-upload><a-uploadv-if="imgUrl"v-model:file-list="imgFileList":before-upload="beforeUpload":maxCount="1"@change="renewImg"><a-button class="btn" type="primary">上传图片</a-button></a-upload><a-button class="btn" type="primary" @click="handleTest">开始检测</a-button></div></a-card><a-card title="检测结果" :bordered="false" v-if="responseData" style="margin-top: 20px"><div style="width: 80vw"><div v-if="responseData.image_template" style="margin-bottom: 20px"><img :src="responseData.image_template" style="zoom: 50%" alt="Loaded Image" /></div><div v-if="responseData.check_all" class="result"><span>全局检测结果:</span><pre style="white-space: pre-wrap">{{ responseData.all_content }}</pre></div><div v-for="item in responseData.contents" :key="item.id"><span>序号{{ item.id }}:</span><pre style="white-space: pre-wrap">{{ item.result }}</pre></div></div></a-card></a-spin>
</template>
<script setup>
import { draw } from '../lib/draw' // 矩形绘制方法
import { PlusOutlined } from '@ant-design/icons-vue'
import debounce from 'xe-utils/debounce'
import { testColorDiff } from '../api/index'
import { message } from 'ant-design-vue'const checked = ref(false)
const spinning = ref(false)
const responseData = ref(null) //接口成功响应数据
const contentWrapRef = ref() // 用来获取最外层的div高度
const zoom = ref(1) //缩放比例/* 模板 */
const templateUrl = ref()
const templateFileList = ref([])
const temContentRef = ref(null) //模板外层
const temMarkCanvasRef = ref(null) //模板画布
const temMarkList = ref([]) //模板标记数组
const temShowFlag = ref(false) //模板初始化完成标识
const temContentWrapClassFlag = ref(false) // 模板外层样式生效标识
// 模版上传
const handleTemplateChange = (info) => {templateUrl.value = URL.createObjectURL(info.file)initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}
// 重新上传模板
const renewTemplate = (info) => {temMarkList.value = []handleTemplateChange(info)
}/* 图片 */
const imgUrl = ref('')
const imgFileList = ref([])
const imgContentRef = ref(null) //图片外层
const imgMarkCanvasRef = ref(null) //图片画布
const imgShowFlag = ref(false) //图片初始化完成标识
const imgContentWrapClassFlag = ref(false) // 图片外层样式生效标识
const imgCtx2D = ref() // 图片画布实例
// 图片上传
const handleImageChange = (info) => {imgUrl.value = URL.createObjectURL(info.file)InitImgDrawRect()
}
// 重新上传图片
const renewImg = (info) => {handleImageChange(info)
}watch(() => temMarkList.value,(newVal, oldVal) => {if (imgUrl.value) {debounce(InitImgDrawRect(), 1000)}},{ deep: true }
)// 关闭自动上传
const beforeUpload = (file) => {return false
}
// 处理坐标数据给后端接口
const handleMarkPoint = () => {const pointArr = []temMarkList.value.forEach((item, index) => {pointArr.push({id: index + 1,left_x: item.x/zoom.value,left_y: item.y/zoom.value,right_x: item.x/zoom.value + item.w/zoom.value,right_y: item.y/zoom.value + item.h/zoom.value,result: ''})})console.log('pointArr',pointArr)return JSON.stringify(pointArr)
}
// 开始检测
const handleTest = () => {console.log('temMarkList.value', temMarkList.value, temMarkList.value.length)if (!templateUrl.value) {message.error('请上传模板')return} else if (!imgUrl.value) {message.error('请上传图片')return} else if (temMarkList.value.length === 0 && !checked.value) {message.error('请进行框选')return}
// spinning.value = trueconst formData = new FormData()formData.append('template', templateFileList.value[0].originFileObj)formData.append('file', imgFileList.value[0].originFileObj)formData.append('points_json', handleMarkPoint())// 模版色差/* testColorDiff(formData, { check_all: checked.value ? 1 : 0 }).then((res) => {spinning.value = falseresponseData.value = res.data.dataresponseData.value.image_template = `data:image/png;base64,${res.data.data.image_template}`}).catch(() => {spinning.value = false}) */
}/* 画布操作 */
// 标记内容数组//画布初始化
const initCanvas = (contentRef, canvasRef, markListName, showFlag, contentWrapClassFlag) => {setTimeout(() => {nextTick(() => {const contentWrapHeight = contentWrapRef.value.offsetHeight// 初始化canvas宽高let cav = canvasRef.valuecav.width = contentRef.value.offsetWidth * zoom.valuecav.height = contentRef.value.offsetHeight * zoom.valueconsole.log('cav.width', zoom, cav.width)let ctx = cav.getContext('2d')ctx.strokeStyle = 'red'cav.style.cursor = 'crosshair'if (contentWrapHeight > cav.height) {// 说明图片高度小于容器高度contentWrapClassFlag.value = true} else {contentWrapClassFlag.value = false}showFlag.value = trueconsole.log('markListName.value ', markListName.value)// 计算使用变量let list = markListName.value // 画框数据集合, 用于服务端返回的数据显示和绘制的矩形保存// 若list长度不为0, 则显示已标记框if (list.length !== 0) {list.forEach(function (value, index, array) {// 遍历绘制所有标记框ctx.rect(value.x, value.y, value.w, value.h)if (zoom.value <= 0.5) {ctx.font = '14px sans-serif'} else {ctx.font = '20px sans-serif'}ctx.fillStyle = 'red'ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)ctx.stroke()})}// 调用封装的绘制方法draw(cav, list, undefined,zoom.value)})}, 500)
}
// 图片不让操作,初始化显示矩形框
const InitImgDrawRect = () => {setTimeout(() => {nextTick(() => {const contentWrapHeight = contentWrapRef.value.offsetHeight// 初始化canvas宽高let cav = imgMarkCanvasRef.valuecav.width = imgContentRef.value.offsetWidth * zoom.valuecav.height = imgContentRef.value.offsetHeight * zoom.valueimgCtx2D.value = cav.getContext('2d')imgCtx2D.value.strokeStyle = 'red'// cav.style.cursor = 'crosshair'if (contentWrapHeight > cav.height) {// 说明图片高度小于容器高度imgContentWrapClassFlag.value = true} else {imgContentWrapClassFlag.value = false}imgShowFlag.value = true// 计算使用变量let list = temMarkList.value // 画框数据集合, 用于服务端返回的数据显示和绘制的矩形保存if (list.length !== 0) {list.forEach(function (value, index, array) {// 遍历绘制所有标记框imgCtx2D.value.rect(value.x, value.y, value.w, value.h)if (zoom.value <= 0.5) {imgCtx2D.value.font = '14px sans-serif'} else {imgCtx2D.value.font = '20px sans-serif'}imgCtx2D.value.fillStyle = 'red'imgCtx2D.value.fillText(index + 1, value.x + value.w / 2, value.y - 5)imgCtx2D.value.stroke()})}})}, 500)
}//放大
const scaleLarge = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom += 10zoom.value = innerZoom / 100console.log('zoom', zoom.value)temContentRef.value.style.zoom = innerZoom + '%'temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value - 0.1)) * zoom.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.h = (item.h / (zoom.value - 0.1)) * zoom.value})if (imgContentRef.value) {imgContentRef.value.style.zoom = innerZoom + '%'}initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}// //缩小
const scaleSmall = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom -= 10zoom.value = innerZoom / 100console.log('zoom', zoom.value)temContentRef.value.style.zoom = innerZoom + '%'temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value + 0.1)) * zoom.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.h = (item.h / (zoom.value + 0.1)) * zoom.value})if (imgContentRef.value) {imgContentRef.value.style.zoom = innerZoom + '%'}initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}//滚轮缩放(TODO:未实现缩放后再画矩形鼠标位置发生变化)
// const rollImg = () => {
// /* 获取当前页面的缩放比
// 若未设置zoom缩放比,则为默认100%,即1,原图大小
// */
// console.log(document.getElementById('bigImg').style)
// var zoom = parseInt(document.getElementById('bigImg').style.zoom) || 100
// /* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动 */
// zoom += event.wheelDelta / 12
// /* 最小范围 和 最大范围 的图片缩放尺度 */
// if (zoom >= 100 && zoom < 250) {
// document.getElementById('bigImg').style.zoom = zoom + '%'
// }
// return false
// }
</script>
<style lang="less" scoped>
.difference-wrap {width: 80vw;height: 56vh;display: flex;.content-upload {width: calc(50% - 5px);:deep(.ant-upload) {width: 100%;height: 100%;}}.content-upload:first-child {margin-right: 5px;}.content-upload:last-child {margin-left: 5px;}.content-wrap {height: 100%;width: 100%;overflow: auto;}.content-wrap-flag::after {content: '';/*让伪元素撑起高度*/height: 100%;display: inline-block;vertical-align: middle;}.tem-content,.img-content {display: inline-block;position: relative;height: auto;width: auto;vertical-align: middle;canvas {position: absolute;top: 0;left: 0;z-index: 10;width: 100%;height: 100%;}}
}
.actionBar {display: flex;justify-content: flex-end;.btn {margin: 20px 10px 0;}.btn:last-child {margin-right: 0;}
}
.result {span {display: inline-block;width: 120px;}
}
</style>
结果显示
扩展,可在图片上进行拖拽操作,模板和图片的点位不一定一一对应
业务上,在之前的基础上进行扩展,可在图片上进行拖拽操作,不可在图片上进行新增和删除操作,若在图片上拖拽新的位置,则以自身为准,模板上对应的框不在可控图片上对应的框,将两组数据传给后端
draw.js
/*** 画布中绘制矩形* 参数: cav-画布对象 list-矩形数组 i-选中矩形下标**/
let globalZoom = 1 //缩放/* 操作执行方法分发 */
export function draw(cav, list, i, delEnable, newEnable, zoom) {globalZoom = zoom || globalZoom// 画布初始化let ctx = cav.getContext('2d')ctx.strokeStyle = 'red'ctx.lineWidth = 2// 变量初始化let sX = 0 // 鼠标X坐标let sY = 0 // 鼠标Y坐标/**鼠标移动进行第一层判断, 区分情况: 无矩形, 已有矩形无选中, 已有选中矩形*/cav.onmousemove = function (em) {sX = em.offsetXsY = em.offsetYlet iem = undefined // 鼠标移动时临时存储当前鼠标所在矩形的下标if (list.length === 0) {// **** 无矩形 ****// 绘制新矩形newDraw(cav, ctx, list, delEnable, newEnable, zoom)} else if (i === undefined) {// **** 已有矩形无选中 ****// 判断鼠标位置list.forEach(function (value, index, array) {if (value.w > 0 &&value.h > 0 &&sX > value.x &&sX < value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// 鼠标在右下方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)}if (value.w < 0 &&value.h > 0 &&sX < value.x &&sX > value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// 鼠标在左下方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)}if (value.w > 0 &&value.h < 0 &&sX > value.x &&sX < value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// 鼠标在右上方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)}if (value.w < 0 &&value.h < 0 &&sX < value.x &&sX > value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// 鼠标在左上方向生成的矩形中iem = indexjudgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)}if (iem === undefined) {// 鼠标不在矩形中newDraw(cav, ctx, list, delEnable, newEnable, zoom)}})} else {// **** 已有选中矩形 ****// 判断鼠标位置for (let index = 0; index < list.length; index++) {let value = list[index]if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + 5 && sY > value.y - 5) {// *** 鼠标在起点角 ***if (index === i) {changeDraw(cav, ctx, list, i, 1, delEnable, newEnable, zoom)break}} else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + 5 && sY > value.y - 5) {// *** 鼠标在起点横向角 ***if (index === i) {changeDraw(cav, ctx, list, i, 2, delEnable, newEnable, zoom)break}} else if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {// *** 鼠标在起点纵向角 ***if (index === i) {changeDraw(cav, ctx, list, i, 3, delEnable, newEnable, zoom)break}} else if (sX < value.x + value.w + 5 &&sX > value.x + value.w - 5 &&sY < value.y + value.h + 5 &&sY > value.y + value.h - 5) {// *** 鼠标在终点角 ***if (index === i) {changeDraw(cav, ctx, list, i, 4, delEnable, newEnable, zoom)break}} else if (value.w > 0 &&value.h > 0 &&sX > value.x &&sX < value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// *** 鼠标在右下方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)break} else if (value.w < 0 &&value.h > 0 &&sX < value.x &&sX > value.x + value.w &&sY > value.y &&sY < value.y + value.h) {// *** 鼠标在左下方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)break} else if (value.w > 0 &&value.h < 0 &&sX > value.x &&sX < value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// *** 鼠标在右上方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)break} else if (value.w < 0 &&value.h < 0 &&sX < value.x &&sX > value.x + value.w &&sY < value.y &&sY > value.y + value.h) {// *** 鼠标在左上方向生成的矩形中 ***iem = indexjudgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)break} else {if (iem === undefined) {// *** 鼠标不在矩形中 ***newDraw(cav, ctx, list, delEnable, newEnable, zoom)}}}}/* 鼠标移出画布区域时保存选中矩形下标(如有) */cav.onmouseout = function (eo) {if (i !== undefined) {// 初始化draw(cav, list, i, delEnable, newEnable, zoom)}}}return list
}/* 编辑矩形四个角 */
function changeDraw(cav, ctx, list, i, site, delEnable, newEnable, zoom) {cav.style.cursor = 'pointer'// site: 操作矩形角的位置, 1-起点 2-起点横向 3-起点纵向 4-终点let mark = list[i]/* 按下鼠标左键 */cav.onmousedown = function (ed) {// 保存鼠标落下位置的X, Y坐标, firefox中鼠标移动后ed.offsetX ed.offsetY会变成 0, 需要使用临时参数存储起来let sX = ed.offsetX // 起点X坐标let sY = ed.offsetY // 起点Y坐标/* 移动鼠标 */cav.onmousemove = function (em) {// 计算绘制数据let iframe = {}switch (site) {case 1:iframe = {uuid: mark.uuid,x: em.offsetX,y: em.offsetY,w: mark.w - (em.offsetX - sX),h: mark.h - (em.offsetY - sY)}breakcase 2:iframe = {uuid: mark.uuid,x: mark.x,y: mark.y + (em.offsetY - sY),w: mark.w + (em.offsetX - sX),h: mark.h - (em.offsetY - sY)}breakcase 3:iframe = {uuid: mark.uuid,x: mark.x + (em.offsetX - sX),y: mark.y,w: mark.w - (em.offsetX - sX),h: mark.h + (em.offsetY - sY)}breakcase 4:iframe = {uuid: mark.uuid,x: mark.x,y: mark.y,w: mark.w + (em.offsetX - sX),h: mark.h + (em.offsetY - sY)}break}list.splice(i, 1, iframe)// 重新绘制reDraw(cav, ctx, list, i)}/* 鼠标离开矩形区 */cav.onmouseout = function (eo) {// 重新绘制reDraw(cav, ctx, list)// 初始化draw(cav, list, undefined, delEnable, newEnable, zoom)}/* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */delDraw(cav, ctx, list, i, delEnable, newEnable, zoom)}
}/* 绘制新矩形 */
function newDraw(cav, ctx, list, delEnable, newEnable, zoom) {if (newEnable === '1') {cav.style.cursor = 'crosshair'// 初始化变量let start = false // 画框状态, false时不执行画框操作let sX = 0 // 起点X坐标let sY = 0 // 起点Y坐标/* 按下鼠标左键 */cav.onmousedown = function (ed) {/* 使用变量 */start = truesX = ed.offsetXsY = ed.offsetY/* 重置按键监听, 防止选中取消后仍可删除 */delDraw(cav, ctx, list, null, delEnable, newEnable, zoom)/* 鼠标移动 */cav.onmousemove = function (em) {if (start) {// 重新绘制reDraw(cav, ctx, list)// 设置边框为虚线ctx.beginPath()ctx.setLineDash([8, 4])ctx.rect(sX, sY, em.offsetX - sX, em.offsetY - sY)ctx.stroke()}}/* 鼠标抬起 */cav.onmouseup = function (eu) {if (start && Math.abs(eu.offsetX - sX) > 10 && Math.abs(eu.offsetY - sY) > 10) {// 改变矩形数组let frame = {uuid: createUuid(),x: sX,y: sY,w: eu.offsetX - sX,h: eu.offsetY - sY}list.push(frame)// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list, undefined, delEnable, newEnable, zoom)} else {// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list, undefined, delEnable, newEnable, zoom)}}/* 鼠标离开矩形区 */cav.onmouseout = function (eo) {if (start && Math.abs(eo.offsetX - sX) > 10 && Math.abs(eo.offsetY - sY) > 10) {// 改变矩形数组let frame = {uuid: createUuid(),x: sX,y: sY,w: eo.offsetX - sX,h: eo.offsetY - sY}list.push(frame)// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list, undefined, delEnable, newEnable, zoom)} else {// 重新绘制reDraw(cav, ctx, list)// 改变画框状态start = false// 初始化draw(cav, list, undefined, delEnable, newEnable, zoom)}}}}
}/* 选中矩形, 重绘矩形, 并分发后续事件 */
function judgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom) {cav.style.cursor = 'default'// 初始化变量let sX = 0 // 起点X坐标let sY = 0 // 起点Y坐标/* 按下鼠标左键 */cav.onmousedown = function (ed) {sX = ed.offsetXsY = ed.offsetY// 更改选中状态, 重绘矩形reDraw(cav, ctx, list, iem)/* 当仅点击选中矩形便抬起鼠标后, 重新初始化画布 */cav.onmouseup = function () {// 重绘矩形reDraw(cav, ctx, list, iem)// 初始化draw(cav, list, iem, delEnable, newEnable, zoom)}/* 按住拖动鼠标, 移动选中矩形*/moveDraw(cav, ctx, list, iem, sX, sY, delEnable, newEnable, zoom)/* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */delDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)}
}/* 移动矩形 */
function moveDraw(cav, ctx, list, i, sX, sY, delEnable, newEnable, zoom) {let mark = list[i]cav.onmousemove = function (em) {let iframe = {uuid: mark.uuid,x: mark.x + (em.offsetX - sX),y: mark.y + (em.offsetY - sY),w: mark.w,h: mark.h}list.splice(i, 1, iframe)/* 监听键盘, 使矩形在移动后仍可删除, 在点击未移动过的矩形时仍有一次监听 */delDraw(cav, ctx, list, i, delEnable, newEnable, zoom)// 重新绘制reDraw(cav, ctx, list, i)}cav.onmouseup = function () {// 重绘矩形reDraw(cav, ctx, list, i)// 初始化draw(cav, list, i, delEnable, newEnable, zoom)}
}/* 删除矩形 */
function delDraw(cav, ctx, list, i, delEnable, newEnable, zoom) {if (delEnable == 1) {/* 按键事件 */if (i === null) {// i为null时阻止按键监听事件冒泡document.onkeydown = function (k) {return false}} else {// 监听按键事件document.onkeydown = function (k) {let key = k.keyCode || k.whichif ((key == 46 || key == 8) && i !== null) {if (list.length >= 1) {// 删除数组元素list.splice(i, 1)// 重绘矩形reDraw(cav, ctx, list)} else {/* 矩形数组长度为0, 已将矩形框全部删除 */ctx.clearRect(0, 0, cav.width, cav.height)}// 重置监听状态, 防止删除完毕后, 按键监听不消失delDraw(cav, ctx, list, null, delEnable, newEnable, zoom)// 重绘矩形reDraw(cav, ctx, list)// 初始化draw(cav, list, undefined, delEnable, newEnable, zoom)}}}}
}/* 重绘所有矩形 */
function reDraw(cav, ctx, list, i) {ctx.setLineDash([8, 0]) // 设置边框为实线ctx.clearRect(0, 0, cav.width, cav.height)// 绘制未选中部分list.forEach(function (value, index, array) {if (i === undefined || index != i) {ctx.beginPath()ctx.strokeStyle = 'red'ctx.rect(value.x, value.y, value.w, value.h)ctx.fillStyle = 'red'if (globalZoom <= 0.5) {ctx.font = '14px sans-serif'} else {ctx.font = '20px sans-serif'}ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)ctx.stroke()}})// 绘制已选中部分list.forEach(function (value, index, array) {if (index === i) {/* 绘制方框 */ctx.beginPath()ctx.strokeStyle = 'red'ctx.rect(value.x, value.y, value.w, value.h)ctx.fillStyle = 'RGBA(102,102,102,0.2)'ctx.fillRect(value.x, value.y, value.w, value.h)ctx.fillStyle = 'red'if (globalZoom <= 0.5) {ctx.font = '14px sans-serif'} else {ctx.font = '20px sans-serif'}ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)ctx.stroke()// 绘制四个角的圆圈ctx.beginPath()ctx.strokeStyle = 'red'ctx.arc(value.x, value.y, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画起点实心圆ctx.stroke()ctx.beginPath()ctx.arc(value.x, value.y + value.h, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画起点纵向实心圆ctx.stroke()ctx.beginPath()ctx.arc(value.x + value.w, value.y + value.h, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画起点横向实心圆ctx.stroke()ctx.beginPath()ctx.arc(value.x + value.w, value.y, 4, 0, Math.PI * 2)ctx.fillStyle = 'red'ctx.fill() // 画终点实心圆ctx.stroke()}})
}
/*** 生成 通用唯一编码* @param len 指定长度* @param radix 基数*/
function createUuid(len, radix) {var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')var uuid = []var iradix = radix || chars.lengthif (len) {// Compact formfor (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]} else {// rfc4122, version 4 formvar r// rfc4122 requires these charactersuuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'uuid[14] = '4'// Fill in random data. At i==19 set the high bits of clock sequence as// per rfc4122, sec. 4.1.5for (i = 0; i < 36; i++) {if (!uuid[i]) {r = 0 | (Math.random() * 16)uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r]}}}return uuid.join('')
}// 根据某一字段求arr2对象数组的差集,arr2的长度要比arr1的长度长,扩展,需要比对多组字段key2,key3,key4
export const getArrayDifference = function (arr1, arr2, ...keys) {const result = []for (let i = 0; i < arr2.length; i++) {const obj = arr2[i]const unique1 = obj[keys[0]]let isExist = falsefor (let j = 0; j < arr1.length; j++) {const aj = arr1[j]const unique2 = aj[keys[0]]if (keys.length > 1) {const flag = keys.reduce((pre, cur) => pre && obj[cur] === aj[cur], true)if (flag) {isExist = truebreak}} else {if (unique2 === unique1) {isExist = truebreak}}}if (!isExist) {result.push(obj)}}return result
}
ColorDifference.vue文件
- 添加temMarkList watch监听事件
- 放大、缩小事件添加图片逻辑
- 多处细小改动
<template><a-spin tip="Loading..." :spinning="spinning"><a-card title="色差差异检测" :bordered="false"><a-checkbox v-model:checked="checked" style="margin-bottom: 15px">是否进行全局色差检测</a-checkbox><div class="difference-wrap"><a-uploadv-model:file-list="templateFileList"list-type="picture-card"class="content-upload":show-upload-list="false":openFileDialogOnClick="!templateUrl":before-upload="beforeUpload":maxCount="1"@change="handleTemplateChange"><divv-if="templateUrl"class="content-wrap":class="[temContentWrapClassFlag ? 'content-wrap-flag' : '']"ref="contentWrapRef"><div class="tem-content" ref="temContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }"><img :src="templateUrl" /><canvas ref="temMarkCanvasRef"></canvas></div></div><div v-else><plus-outlined style="font-size: 28px"></plus-outlined><div class="ant-upload-text">上传模板</div></div></a-upload><a-uploadv-model:file-list="imgFileList"list-type="picture-card"class="content-upload":maxCount="1":show-upload-list="false":openFileDialogOnClick="!imgUrl":before-upload="beforeUpload"@change="handleImageChange"><div v-if="imgUrl" class="content-wrap" :class="[imgContentWrapClassFlag ? 'content-wrap-flag' : '']"><div class="img-content" ref="imgContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }"><img :src="imgUrl" /><canvas ref="imgMarkCanvasRef"></canvas></div></div><div v-else><plus-outlined style="font-size: 28px"></plus-outlined><div class="ant-upload-text">上传图片</div></div></a-upload></div><div class="actionBar"><a-button class="btn" :disabled="zoom === 2" type="primary" @click="scaleLarge">放大</a-button><a-button class="btn" :disabled="zoom === 0.1" type="primary" @click="scaleSmall">缩小</a-button><a-uploadv-if="templateUrl"v-model:file-list="templateFileList":maxCount="1":before-upload="beforeUpload"@change="renewTemplate"><a-button class="btn" type="primary">上传模板</a-button></a-upload><a-uploadv-if="imgUrl"v-model:file-list="imgFileList":before-upload="beforeUpload":maxCount="1"@change="renewImg"><a-button class="btn" type="primary">上传图片</a-button></a-upload><a-button class="btn" type="primary" @click="handleTest">开始检测</a-button></div></a-card><a-card title="检测结果" :bordered="false" v-if="responseData" style="margin-top: 20px"><div style="width: 80vw"><div v-if="responseData.image_template" style="margin-bottom: 20px"><img :src="responseData.image_template" style="zoom: 50%" alt="Loaded Image" /></div><div v-if="responseData.check_all" class="result"><span>全局检测结果:</span><pre style="white-space: pre-wrap">{{ responseData.all_content }}</pre></div><div v-for="item in responseData.contents" :key="item.id"><span>序号{{ item.id }}:</span><pre style="white-space: pre-wrap">{{ item.result }}</pre></div></div></a-card></a-spin>
</template>
<script setup>
import { draw, getArrayDifference } from '../lib/draw' // 矩形绘制方法
import { PlusOutlined } from '@ant-design/icons-vue'
import debounce from 'xe-utils/debounce'
import clone from 'xe-utils/clone'
import { testColorDiff } from '../api/index'
import { message } from 'ant-design-vue'const checked = ref(false)
const spinning = ref(false)
const responseData = ref(null) //接口成功响应数据
const contentWrapRef = ref() // 用来获取最外层的div高度
const zoom = ref(1) //缩放比例/* 模板 */
const templateUrl = ref()
const templateFileList = ref([])
const temContentRef = ref(null) //模板外层
const temMarkCanvasRef = ref(null) //模板画布
const temMarkList = ref([]) //模板标记数组
const oldTemMarkList = ref([]) //模板标记旧值
const temShowFlag = ref(false) //模板初始化完成标识
const temContentWrapClassFlag = ref(false) // 模板外层样式生效标识
// 模版上传
const handleTemplateChange = (info) => {templateUrl.value = URL.createObjectURL(info.file)initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}
// 重新上传模板
const renewTemplate = (info) => {temMarkList.value = []handleTemplateChange(info)
}/* 图片 */
const imgUrl = ref('')
const imgFileList = ref([])
const imgContentRef = ref(null) //图片外层
const imgMarkCanvasRef = ref(null) //图片画布
const imgMarkList = ref([]) //图片标记数组
const imgShowFlag = ref(false) //图片初始化完成标识
const imgContentWrapClassFlag = ref(false) // 图片外层样式生效标识
// 图片上传
const handleImageChange = (info) => {imgUrl.value = URL.createObjectURL(info.file)InitImgDrawRect(temMarkList.value)
}
// 重新上传图片
const renewImg = (info) => {handleImageChange(info)
}watch(() => clone(temMarkList.value, true),debounce((newVal, oldVal) => {// console.log('newVal', newVal)// console.log('oldVal', oldVal)// console.log('oldTemMarkList', oldTemMarkList.value)// console.log('temMarkList.value', temMarkList.value)// console.log('imgMarkList.value', imgMarkList.value)let newMarkList = [...imgMarkList.value]const newLen = newVal.lengthconst oldLen = oldVal.lengthif (newLen > oldLen) {// 新增新的矩形newMarkList.push(newVal[newLen - 1])} else if (newLen < oldLen) {// 删除矩形const resArr = getArrayDifference(newVal, oldTemMarkList.value, 'uuid') //找到删除的那个矩形const dealArr = []newMarkList.forEach((item) => {if (item.uuid !== resArr[0].uuid) {dealArr.push(item)}})newMarkList = [...dealArr]} else if (newLen === oldLen) {// 移动矩形或者放大缩小矩形const resArr = getArrayDifference(newVal, oldTemMarkList.value, 'x', 'y', 'w', 'h') //找到移动的那个矩形旧值console.log('resArr', resArr)if (resArr.length === 0) {// 可能是放大,缩小操作return}const newRes = newVal.find((item) => item.uuid === resArr[0].uuid) //找到移动的那个矩形新值const dealArr = []newMarkList.forEach((item) => {if (item.uuid !== resArr[0].uuid) {dealArr.push(item)} else {console.log('resArr[0]', resArr[0], item)if (resArr[0].x === item.x && resArr[0].y === item.y && resArr[0].w === item.w && resArr[0].h === item.h) {// 如果图片上的框与模板上框的旧值相等,说明,图片上的框没有移动过,则,同步模板上的框dealArr.push(newRes)} else {// 不相等,说明,图片上的框没有移动过,则,保持图片上的框dealArr.push(item)}}})newMarkList = [...dealArr]}if (imgUrl.value) {InitImgDrawRect(newMarkList)}}, 500),{ deep: true }
)// 关闭自动上传
const beforeUpload = (file) => {return false
}
// 处理坐标数据给后端接口
const handleMarkPoint = () => {// 这时候需要传两组数据,看后端如何定义,图片和模板一一对应const pointArr = []temMarkList.value.forEach((item, index) => {const imgItem = imgMarkList.value[index]pointArr.push({id: index + 1,template: {left_x: item.x / zoom.value,left_y: item.y / zoom.value,right_x: item.x / zoom.value + item.w / zoom.value,right_y: item.y / zoom.value + item.h / zoom.value},image: {left_x: imgItem.x / zoom.value,left_y: imgItem.y / zoom.value,right_x: imgItem.x / zoom.value + imgItem.w / zoom.value,right_y: imgItem.y / zoom.value + imgItem.h / zoom.value},result: ''})})return JSON.stringify(pointArr)
}
// 开始检测
const handleTest = () => {if (!templateUrl.value) {message.error('请上传模板')return} else if (!imgUrl.value) {message.error('请上传图片')return} else if (temMarkList.value.length === 0 && !checked.value) {message.error('请进行框选')return}// spinning.value = trueconst formData = new FormData()formData.append('template', templateFileList.value[0].originFileObj)formData.append('file', imgFileList.value[0].originFileObj)formData.append('points_json', handleMarkPoint())// 模版色差/* testColorDiff(formData, { check_all: checked.value ? 1 : 0 }).then((res) => {spinning.value = falseresponseData.value = res.data.dataresponseData.value.image_template = `data:image/png;base64,${res.data.data.image_template}`}).catch(() => {spinning.value = false}) */
}/* 画布操作 */
// 标记内容数组//画布初始化
const initCanvas = (contentRef,canvasRef,markListName,showFlag,contentWrapClassFlag,delEnable = '1',newEnable = '1'
) => {setTimeout(() => {nextTick(() => {const contentWrapHeight = contentWrapRef.value.offsetHeight// 初始化canvas宽高let cav = canvasRef.valuecav.width = contentRef.value.offsetWidth * zoom.valuecav.height = contentRef.value.offsetHeight * zoom.valuecav.style.cursor = 'crosshair'let ctx = cav.getContext('2d')ctx.strokeStyle = 'red'if (contentWrapHeight > cav.height) {// 说明图片高度小于容器高度contentWrapClassFlag.value = true} else {contentWrapClassFlag.value = false}showFlag.value = true// 计算使用变量let list = markListName.value // 画框数据集合, 用于服务端返回的数据显示和绘制的矩形保存// 若list长度不为0, 则显示已标记框if (list.length !== 0) {list.forEach(function (value, index, array) {// 遍历绘制所有标记框ctx.rect(value.x, value.y, value.w, value.h)if (zoom.value <= 0.5) {ctx.font = '14px sans-serif'} else {ctx.font = '20px sans-serif'}ctx.fillStyle = 'red'ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)ctx.stroke()})}// 调用封装的绘制方法draw(cav, list, undefined, delEnable, newEnable, zoom.value)})}, 500)
}
// 图片不让操作,初始化显示矩形框
const InitImgDrawRect = (markList) => {oldTemMarkList.value = clone(temMarkList.value) //在这里存上模板标记的旧值,好在移动缩小放大时进行比较imgMarkList.value = clone(markList)initCanvas(imgContentRef, imgMarkCanvasRef, imgMarkList, imgShowFlag, imgContentWrapClassFlag, '0', '0')
}//放大
const scaleLarge = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom += 10zoom.value = innerZoom / 100if (imgContentRef.value) {imgContentRef.value.style.zoom = innerZoom + '%'imgMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value - 0.1)) * zoom.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.h = (item.h / (zoom.value - 0.1)) * zoom.value})}temContentRef.value.style.zoom = innerZoom + '%'temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value - 0.1)) * zoom.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.h = (item.h / (zoom.value - 0.1)) * zoom.value})initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}// //缩小
const scaleSmall = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom -= 10zoom.value = innerZoom / 100if (imgContentRef.value) {imgContentRef.value.style.zoom = innerZoom + '%'imgMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value + 0.1)) * zoom.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.h = (item.h / (zoom.value + 0.1)) * zoom.value})// InitImgDrawRect(imgMarkList.value) 此处不需要调用,会在temMarkList的watch监听里更新图片标记点}temContentRef.value.style.zoom = innerZoom + '%'temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value + 0.1)) * zoom.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.h = (item.h / (zoom.value + 0.1)) * zoom.value})initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}//滚轮缩放(TODO:未实现缩放后再画矩形鼠标位置发生变化)
// const rollImg = () => {
// /* 获取当前页面的缩放比
// 若未设置zoom缩放比,则为默认100%,即1,原图大小
// */
// console.log(document.getElementById('bigImg').style)
// var zoom = parseInt(document.getElementById('bigImg').style.zoom) || 100
// /* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动 */
// zoom += event.wheelDelta / 12
// /* 最小范围 和 最大范围 的图片缩放尺度 */
// if (zoom >= 100 && zoom < 250) {
// document.getElementById('bigImg').style.zoom = zoom + '%'
// }
// return false
// }
</script>
<style lang="less" scoped>
.difference-wrap {width: 80vw;height: 56vh;display: flex;.content-upload {width: calc(50% - 5px);:deep(.ant-upload) {width: 100%;height: 100%;}}.content-upload:first-child {margin-right: 5px;}.content-upload:last-child {margin-left: 5px;}.content-wrap {height: 100%;width: 100%;overflow: auto;}.content-wrap-flag::after {content: '';/*让伪元素撑起高度*/height: 100%;display: inline-block;vertical-align: middle;}.tem-content,.img-content {display: inline-block;position: relative;height: auto;width: auto;vertical-align: middle;canvas {position: absolute;top: 0;left: 0;z-index: 10;width: 100%;height: 100%;}}
}
.actionBar {display: flex;justify-content: flex-end;.btn {margin: 20px 10px 0;}.btn:last-child {margin-right: 0;}
}
.result {span {display: inline-block;width: 120px;}
}
</style>
结果显示
双向操作同步
在上一个功能基础上,修改以下内容
ColorDifference.vue
- watch函数
- 图片画布操作函数
- 放大、缩小、数据保存的处理函数,只需要处理temMarkList
...watch(() => clone(temMarkList.value, true),debounce((newVal, oldVal) => {if (imgUrl.value) {InitImgDrawRect()}initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)}, 500),{ deep: true }
)
...const InitImgDrawRect = (markList) => {
// oldTemMarkList.value = clone(temMarkList.value) //在这里存上模板标记的旧值,好在移动缩小放大时进行比较
// imgMarkList.value = clone(markList)initCanvas(imgContentRef, imgMarkCanvasRef, temMarkList, imgShowFlag, imgContentWrapClassFlag, '1', '1')
}
const handleMarkPoint = () => {// 这时候需要传两组数据,看后端如何定义,图片和模板一一对应const pointArr = []temMarkList.value.forEach((item, index) => {const imgItem = imgMarkList.value[index]pointArr.push({id: index + 1,left_x: item.x / zoom.value,left_y: item.y / zoom.value,right_x: item.x / zoom.value + item.w / zoom.value,right_y: item.y / zoom.value + item.h / zoom.value,result: ''})})console.log('pointArr', pointArr)return JSON.stringify(pointArr)
}
//放大
const scaleLarge = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom += 10zoom.value = innerZoom / 100temContentRef.value.style.zoom = innerZoom + '%'if (imgUrl.value) {imgContentRef.value.style.zoom = innerZoom + '%'}temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value - 0.1)) * zoom.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.h = (item.h / (zoom.value - 0.1)) * zoom.value})initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}// //缩小
const scaleSmall = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom -= 10zoom.value = innerZoom / 100temContentRef.value.style.zoom = innerZoom + '%'if (imgUrl.value) {imgContentRef.value.style.zoom = innerZoom + '%'}temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value + 0.1)) * zoom.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.h = (item.h / (zoom.value + 0.1)) * zoom.value})initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}...
扩展-滚轮放大缩小
在上一个功能基础上,修改以下内容
ColorDifference.vue
- 在标签外层加上滚轮事件
- 添加滚轮事件方法
- 外层容器不允许出现滚动条
...<divclass="tem-content"ref="temContentRef":style="{ opacity: temShowFlag ? 1 : 0 }"@mousewheel="rollImg()"><img :src="templateUrl" /><canvas ref="temMarkCanvasRef"></canvas></div><divclass="img-content"ref="imgContentRef":style="{ opacity: temShowFlag ? 1 : 0 }"@mousewheel="rollImg()"><img :src="imgUrl" /><canvas ref="imgMarkCanvasRef"></canvas></div>
//滚轮缩放
const rollImg = () => {if (!templateUrl.value) {return}/* 获取当前页面的缩放比若未设置zoom缩放比,则为默认100%,即1,原图大小*/let innerZoom = parseInt(temContentRef.value.style.zoom) || 100if ((innerZoom === 10 && event.wheelDelta < 0) || (innerZoom === 200 && event.wheelDelta > 0)) {// 最小值,最大值零界点处理return}innerZoom += event.wheelDelta / 12zoom.value = innerZoom / 100/* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动 *//* 最小范围 和 最大范围 的图片缩放尺度 */if (event.wheelDelta > 0 && innerZoom <= 200) {// 放大temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value - 0.1)) * zoom.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.h = (item.h / (zoom.value - 0.1)) * zoom.value})} else if (event.wheelDelta < 0 && innerZoom >= 10) {// 缩小temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value + 0.1)) * zoom.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.h = (item.h / (zoom.value + 0.1)) * zoom.value})}temContentRef.value.style.zoom = innerZoom + '%'if (imgUrl.value) {imgContentRef.value.style.zoom = innerZoom + '%'}initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)return false
}
<style lang="less" scoped>.content-wrap {height: 100%;width: 100%;// overflow: auto;overflow: hidden;}
</style>
...
结果显示