news 2026/5/26 1:50:32

Vue3 图片标框功能实现方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3 图片标框功能实现方案

基于 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&&currentBox.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&&currentBox.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
代码可直接运行,坐标实时输出,支持多框、清空
只需要替换图片地址、接入接口就能用于项目

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/26 1:50:30

回头看:Coding Agent 才是通往 AGI 的那条窄门

约 3200 字 / 阅读时间 12 分钟一、开场&#xff1a;当年我们都猜错了方向 如果时间倒回 2023 年初&#xff0c;你去硅谷任何一场酒会&#xff0c;问一句"AGI 会从哪条路走出来&#xff1f;"&#xff0c;最常听到的答案有三种&#xff1a;多模态、具身机器人、通用 A…

作者头像 李华
网站建设 2026/5/26 1:50:01

Rust Go C# PHP等编程语言就业前景与学习指南

针对除Python、Java、JavaScript之外的&#xff0c;在就业市场上有显著需求的编程语言&#xff0c;我将按相同的结构进行详细分析&#xff0c;重点介绍Rust、Go和C#&#xff0c;并补充其他值得关注的语言。 下表概括了这些语言的核心特征与就业前景&#xff1a; 编程语言核心…

作者头像 李华
网站建设 2026/5/26 1:47:01

C 语言卡壳在温度换算?初学者必看:不是智商低

一、学 C 语言半年&#xff0c;竟栽在一道小学级换算题上有位网友的编程求学路&#xff0c;戳中了无数初学者的痛处。他鼓足勇气重拾编程&#xff0c;目标明确要学好 C 语言&#xff0c;甚至特意入手了经典教材《C 程序设计语言》&#xff0c;却连第一章前半部分都没啃完&#…

作者头像 李华
网站建设 2026/5/26 1:44:06

ARM架构CONSTRAINED UNPREDICTABLE行为解析与应对

1. ARM架构中的CONSTRAINED UNPREDICTABLE行为解析在处理器架构设计中&#xff0c;UNPREDICTABLE行为通常指架构规范未明确定义的执行结果&#xff0c;可能导致不可预期的系统状态。ARM架构通过引入CONSTRAINED UNPREDICTABLE机制&#xff0c;将这类行为限制在特定范围内&#…

作者头像 李华
网站建设 2026/5/26 1:42:02

G-Helper终极指南:如何彻底掌控你的华硕笔记本性能与能耗

G-Helper终极指南&#xff1a;如何彻底掌控你的华硕笔记本性能与能耗 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook,…

作者头像 李华