基于 Vue3 + 组合式 API 的图片标框(画框、标注、选框)完整实现,核心逻辑封装在 GetBoxes 组件里,复制就能用
一、功能说明
✅ 在图片上鼠标拖拽画矩形框
✅ 实时显示框坐标(x, y, width, height)
✅ 支持多个框同时显示
✅ 支持清空所有框
✅ 框可渲染在图片上方,不破坏原图
✅ 框可拖动移动位置
✅ 框可拖拽右下角调整大小
✅ 单个删除框
✅ 每个框自定义输入标注文字
✅ 每个框自动随机不同颜色
✅ 回显已保存的标框(直接传数组即可)
✅ 保留原有:多框、拖拽画框、坐标实时输出
二、使用方式
2.1把代码保存为 GetBoxes.vue
GetBoxes.vue
<template><divclass="box-container"><divclass="image-wrapper"ref="wrapperRef"@mousedown="startDraw"@mousemove="handleMouseMove"@mouseup="stopDraw"@mouseleave="stopDraw"><img ref="imgRef":src="imgUrl"alt="标框底图"@load="initCanvas"/><canvas ref="canvasRef"class="draw-canvas"></canvas><div v-for="(box, index) in boxes":key="index"class="box-label":style="{left:`${box.x}px`,top:`${box.y-28}px`,color:box.color,}"><input v-model="box.label"type="text"placeholder="输入标注"@mousedown.stop/><button@click.stop="deleteBox(index)">×</button></div></div><divclass="tool-bar"><button@click="clearAllBoxes">清空所有框</button><button@click="consoleLogBoxes">打印所有框数据</button><divclass="box-list"><h4>已标框({{boxes.length}}):</h4><div v-for="(box, index) in boxes":key="index"class="box-item":style="{ borderLeftColor: box.color }">框{{index+1}}:{{box.label||'未命名'}}<br/>x:{{box.x}},y:{{box.y}},w:{{box.width}},h:{{box.height}}</div></div></div></div></template><script setup>import{ref,onMounted,nextTick,onUnmounted}from'vue'constimgUrl=ref('https://picsum.photos/900/600')constwrapperRef=ref(null)constcanvasRef=ref(null)constimgRef=ref(null)letctx=nullconstboxes=ref([])constcurrentBox=ref(null)constisDrawing=ref(false)constisDragging=ref(false)constisResizing=ref(false)constdragStart=ref({x:0,y:0})constactiveIndex=ref(-1)// 随机边框颜色constrandomColor=()=>{constcolors=['#FF4757','#FF6B35','#F79F1F','#A3CB38','#00D2D3','#3742fa','#FDA7DF','#ED4C67','#1B9CFC','#F8EFBA','#58B19F','#D6A2E8',]returncolors[Math.floor(Math.random()*colors.length)]}// 初始化画布constinitCanvas=async()=>{awaitnextTick()constc=canvasRef.valueconstimg=imgRef.value c.width=img.offsetWidth c.height=img.offsetHeight ctx=c.getContext('2d')redrawCanvas()}// 鼠标移动 切换指针样式consthandleMouseMove=(e)=>{if(!ctx)returnconstrect=canvasRef.value.getBoundingClientRect()constmx=e.clientX-rect.leftconstmy=e.clientY-rect.top canvasRef.value.style.cursor='crosshair'for(leti=boxes.value.length-1;i>=0;i--){constb=boxes.value[i]constright=b.x+b.widthconstbottom=b.y+b.height// 缩放控制点if(mx>=right-12&&mx<=right&&my>=bottom-12&&my<=bottom){canvasRef.value.style.cursor='se-resize'break}// 边框区域if((mx>=b.x-2&&mx<=b.x+2&&my>=b.y&&my<=bottom)||(mx>=right-2&&mx<=right+2&&my>=b.y&&my<=bottom)||(my>=b.y-2&&my<=b.y+2&&mx>=b.x&&mx<=right)||(my>=bottom-2&&my<=bottom+2&&mx>=b.x&&mx<=right)){canvasRef.value.style.cursor='move'break}// 框内部if(mx>=b.x&&mx<=right&&my>=b.y&&my<=bottom){canvasRef.value.style.cursor='pointer'break}}drawing(e)}// 开始绘制、拖动、缩放conststartDraw=(e)=>{constrect=canvasRef.value.getBoundingClientRect()constmx=e.clientX-rect.leftconstmy=e.clientY-rect.topfor(leti=boxes.value.length-1;i>=0;i--){constb=boxes.value[i]constright=b.x+b.widthconstbottom=b.y+b.heightif(mx>=right-12&&mx<=right&&my>=bottom-12&&my<=bottom){isResizing.value=trueactiveIndex.value=ireturn}if(mx>=b.x&&mx<=right&&my>=b.y&&my<=bottom){isDragging.value=trueactiveIndex.value=i dragStart.value={x:mx-b.x,y:my-b.y}return}}isDrawing.value=truecurrentBox.value={x:mx,y:my,width:0,height:0,color:randomColor(),label:''}}// 绘制拖拽逻辑constdrawing=(e)=>{if(!ctx)returnconstrect=canvasRef.value.getBoundingClientRect()constmx=e.clientX-rect.leftconstmy=e.clientY-rect.topif(isDrawing.value&¤tBox.value){currentBox.value.width=mx-currentBox.value.x currentBox.value.height=my-currentBox.value.y}if(isDragging.value&&activeIndex.value>-1){constbox=boxes.value[activeIndex.value]box.x=mx-dragStart.value.x box.y=my-dragStart.value.y}if(isResizing.value&&activeIndex.value>-1){constbox=boxes.value[activeIndex.value]box.width=mx-box.x box.height=my-box.y}redrawCanvas()}// 结束操作conststopDraw=()=>{if(isDrawing.value&¤tBox.value){const{width,height}=currentBox.valueif(Math.abs(width)>8&&Math.abs(height)>8){boxes.value.push({...currentBox.value})}}isDrawing.value=falseisDragging.value=falseisResizing.value=falsecurrentBox.value=nullactiveIndex.value=-1redrawCanvas()}// 重绘画布constredrawCanvas=()=>{if(!ctx)returnctx.clearRect(0,0,canvasRef.value.width,canvasRef.value.height)boxes.value.forEach((box)=>{ctx.strokeStyle=box.color ctx.lineWidth=2ctx.strokeRect(box.x,box.y,box.width,box.height)ctx.fillStyle=box.colorconstrx=box.x+box.widthconstry=box.y+box.height ctx.fillRect(rx-6,ry-6,12,12)})if(currentBox.value){ctx.strokeStyle=currentBox.value.color ctx.strokeRect(currentBox.value.x,currentBox.value.y,currentBox.value.width,currentBox.value.height)}}// 删除单个框constdeleteBox=(index)=>{boxes.value.splice(index,1)redrawCanvas()}// 清空全部constclearAllBoxes=()=>{boxes.value=[]redrawCanvas()}// 打印框数据constconsoleLogBoxes=()=>{console.log('所有框数据:',JSON.parse(JSON.stringify(boxes.value)))}// 回显历史标注示例constloadSavedBoxes=()=>{constsavedData=[{x:50,y:50,width:120,height:100,color:'#FF4757',label:'人物'},{x:200,y:150,width:180,height:140,color:'#3742fa',label:'车辆'},]boxes.value=savedDataredrawCanvas()}//四舍五入函数,保留小数点后n位constcustomRound=(number,decimals)=>{constfactor=Math.pow(10,decimals);returnMath.round(number*factor)/factor;};onMounted(()=>{if(imgRef.value.complete)initCanvas()// loadSavedBoxes()})onUnmounted(()=>{ctx=null})</script><style scoped>.box-container{width:100%;max-width:900px;margin:20px auto;}.image-wrapper{position:relative;width:fit-content;}.draw-canvas{position:absolute;top:0;left:0;z-index:10;}img{display:block;max-width:100%;}.box-label{position:absolute;z-index:20;display:flex;gap:6px;align-items:center;}.box-label input{width:100px;padding:2px 6px;font-size:12px;border:1px solid #ddd;border-radius:4px;}.box-label button{background:#ff4757;color:white;border:none;width:18px;height:18px;font-size:12px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;}.tool-bar{margin-top:12px;display:flex;gap:10px;align-items:center;}button{padding:6px 12px;background:#3742fa;color:#fff;border:none;border-radius:4px;cursor:pointer;}.box-list{margin-top:10px;}.box-item{padding:6px 10px;margin:4px0;font-size:13px;border-left:4px solid #ddd;background:#f9f9f9;}</style>2.2在你的页面中直接引入使用:
<template><div><h3>图片标框工具</h3><GetBoxes/></div></template><script setup>importGetBoxesfrom'./GetBoxes.vue'</script>三、总结
Vue3 标准组合式 API写法
基于 Canvas 实现标框,性能好、不操作 DOM
代码可直接运行,坐标实时输出,支持多框、清空
只需要替换图片地址、接入接口就能用于项目