双图版本(模板对比)
业务描述:模板与图片对比,只操作模板框选的位置进行色差对比,传框选坐标位置给后端,返回对比结果显示
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.offsetX
sY = em.offsetY
let 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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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)
}
break
case 2:
iframe = {
x: mark.x,
y: mark.y + (em.offsetY - sY),
w: mark.w + (em.offsetX - sX),
h: mark.h - (em.offsetY - sY)
}
break
case 3:
iframe = {
x: mark.x + (em.offsetX - sX),
y: mark.y,
w: mark.w - (em.offsetX - sX),
h: mark.h + (em.offsetY - sY)
}
break
case 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 = true
sX = ed.offsetX
sY = 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.offsetX
sY = 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.which
if ((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-upload
v-model:file-list="templateFileList"
list-type="picture-card"
class="content-upload"
:show-upload-list="false"
:openFileDialogOnClick="!templateUrl"
:before-upload="beforeUpload"
:maxCount="1"
@change="handleTemplateChange"
>
<div
v-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-upload
v-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-upload
v-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-upload
v-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 = true
const 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 = false
responseData.value = res.data.data
responseData.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.value
cav.width = contentRef.value.offsetWidth * zoom.value
cav.height = contentRef.value.offsetHeight * zoom.value
console.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 = true
console.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.value
cav.width = imgContentRef.value.offsetWidth * zoom.value
cav.height = imgContentRef.value.offsetHeight * zoom.value
imgCtx2D.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) || 100
innerZoom += 10
zoom.value = innerZoom / 100
console.log('zoom', zoom.value)
temContentRef.value.style.zoom = innerZoom + '%'
temMarkList.value.forEach((item) => {
item.x = (item.x / (zoom.value - 0.1)) * zoom.value
item.y = (item.y / (zoom.value - 0.1)) * zoom.value
item.w = (item.w / (zoom.value - 0.1)) * zoom.value
item.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) || 100
innerZoom -= 10
zoom.value = innerZoom / 100
console.log('zoom', zoom.value)
temContentRef.value.style.zoom = innerZoom + '%'
temMarkList.value.forEach((item) => {
item.x = (item.x / (zoom.value + 0.1)) * zoom.value
item.y = (item.y / (zoom.value + 0.1)) * zoom.value
item.w = (item.w / (zoom.value + 0.1)) * zoom.value
item.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.offsetX
sY = em.offsetY
let 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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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 = index
judgeDraw(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)
}
break
case 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)
}
break
case 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)
}
break
case 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 = true
sX = ed.offsetX
sY = 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.offsetX
sY = 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.which
if ((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 i
radix = radix || chars.length
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]
} else {
// rfc4122, version 4 form
var r
// rfc4122 requires these characters
uuid[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.5
for (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 = false
for (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 = true
break
}
} else {
if (unique2 === unique1) {
isExist = true
break
}
}
}
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-upload
v-model:file-list="templateFileList"
list-type="picture-card"
class="content-upload"
:show-upload-list="false"
:openFileDialogOnClick="!templateUrl"
:before-upload="beforeUpload"
:maxCount="1"
@change="handleTemplateChange"
>
<div
v-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-upload
v-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-upload
v-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-upload
v-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.length
const oldLen = oldVal.length
if (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 = true
const 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 = false
responseData.value = res.data.data
responseData.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.value
cav.width = contentRef.value.offsetWidth * zoom.value
cav.height = contentRef.value.offsetHeight * zoom.value
cav.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) || 100
innerZoom += 10
zoom.value = innerZoom / 100
if (imgContentRef.value) {
imgContentRef.value.style.zoom = innerZoom + '%'
imgMarkList.value.forEach((item) => {
item.x = (item.x / (zoom.value - 0.1)) * zoom.value
item.y = (item.y / (zoom.value - 0.1)) * zoom.value
item.w = (item.w / (zoom.value - 0.1)) * zoom.value
item.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.value
item.y = (item.y / (zoom.value - 0.1)) * zoom.value
item.w = (item.w / (zoom.value - 0.1)) * zoom.value
item.h = (item.h / (zoom.value - 0.1)) * zoom.value
})
initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}
// //缩小
const scaleSmall = () => {
let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
innerZoom -= 10
zoom.value = innerZoom / 100
if (imgContentRef.value) {
imgContentRef.value.style.zoom = innerZoom + '%'
imgMarkList.value.forEach((item) => {
item.x = (item.x / (zoom.value + 0.1)) * zoom.value
item.y = (item.y / (zoom.value + 0.1)) * zoom.value
item.w = (item.w / (zoom.value + 0.1)) * zoom.value
item.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.value
item.y = (item.y / (zoom.value + 0.1)) * zoom.value
item.w = (item.w / (zoom.value + 0.1)) * zoom.value
item.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) || 100
innerZoom += 10
zoom.value = innerZoom / 100
temContentRef.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.value
item.y = (item.y / (zoom.value - 0.1)) * zoom.value
item.w = (item.w / (zoom.value - 0.1)) * zoom.value
item.h = (item.h / (zoom.value - 0.1)) * zoom.value
})
initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}
// //缩小
const scaleSmall = () => {
let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
innerZoom -= 10
zoom.value = innerZoom / 100
temContentRef.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.value
item.y = (item.y / (zoom.value + 0.1)) * zoom.value
item.w = (item.w / (zoom.value + 0.1)) * zoom.value
item.h = (item.h / (zoom.value + 0.1)) * zoom.value
})
initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}
...
扩展-滚轮放大缩小
在上一个功能基础上,修改以下内容
ColorDifference.vue
- 在标签外层加上滚轮事件
- 添加滚轮事件方法
- 外层容器不允许出现滚动条
...
<div
class="tem-content"
ref="temContentRef"
:style="{ opacity: temShowFlag ? 1 : 0 }"
@mousewheel="rollImg()"
>
<img :src="templateUrl" />
<canvas ref="temMarkCanvasRef"></canvas>
</div>
<div
class="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) || 100
if ((innerZoom === 10 && event.wheelDelta < 0) || (innerZoom === 200 && event.wheelDelta > 0)) {
// 最小值,最大值零界点处理
return
}
innerZoom += event.wheelDelta / 12
zoom.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.value
item.y = (item.y / (zoom.value - 0.1)) * zoom.value
item.w = (item.w / (zoom.value - 0.1)) * zoom.value
item.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.value
item.y = (item.y / (zoom.value + 0.1)) * zoom.value
item.w = (item.w / (zoom.value + 0.1)) * zoom.value
item.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>
...
结果显示