@kirkw/vue2-image-hotzone 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Vue2ImageHotzone=t():e.Vue2ImageHotzone=t()}(this,()=>(()=>{var e={861(e,t,n){"use strict";n.r(t);var i=n(601),r=n.n(i),o=n(314),a=n.n(o)()(r());a.push([e.id,"\n.hotzone-wrapper[data-v-658dd234] {\n position: relative;\n display: inline-block;\n width: 100%;\n cursor: crosshair;\n line-height: 0;\n user-select: none;\n}\n.hotzone-wrapper img[data-v-658dd234] {\n width: 100%;\n height: auto;\n display: block;\n pointer-events: none;\n user-select: none;\n}\n.drawing-rect[data-v-658dd234] {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n z-index: 5;\n}\n.zone-rect[data-v-658dd234] {\n position: absolute;\n top: 0;\n left: 0;\n box-sizing: border-box;\n z-index: 10;\n}\n.zone-label[data-v-658dd234] {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n text-align: center;\n pointer-events: none;\n z-index: 25;\n}\n.handle[data-v-658dd234] {\n position: absolute;\n z-index: 20;\n box-sizing: border-box;\n}\n\n/* 手柄位置 */\n.handle.nw[data-v-658dd234] { top: -5px; left: -5px; cursor: nw-resize;\n}\n.handle.n[data-v-658dd234] { top: -5px; left: calc(50% - 5px); cursor: n-resize;\n}\n.handle.ne[data-v-658dd234] { top: -5px; right: -5px; cursor: ne-resize;\n}\n.handle.e[data-v-658dd234] { top: calc(50% - 5px); right: -5px; cursor: e-resize;\n}\n.handle.se[data-v-658dd234] { bottom: -5px; right: -5px; cursor: se-resize;\n}\n.handle.s[data-v-658dd234] { bottom: -5px; left: calc(50% - 5px); cursor: s-resize;\n}\n.handle.sw[data-v-658dd234] { bottom: -5px; left: -5px; cursor: sw-resize;\n}\n.handle.w[data-v-658dd234] { top: calc(50% - 5px); left: -5px; cursor: w-resize;\n}\n",""]);const s=a;n.d(t,["default",0,s])},314(e){"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var n="",i=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),i&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),i&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n}).join("")},t.i=function(e,n,i,r,o){"string"==typeof e&&(e=[[null,e,void 0]]);var a={};if(i)for(var s=0;s<this.length;s++){var d=this[s][0];null!=d&&(a[d]=!0)}for(var l=0;l<e.length;l++){var h=[].concat(e[l]);i&&a[h[0]]||(void 0!==o&&(void 0===h[5]||(h[1]="@layer".concat(h[5].length>0?" ".concat(h[5]):""," {").concat(h[1],"}")),h[5]=o),n&&(h[2]?(h[1]="@media ".concat(h[2]," {").concat(h[1],"}"),h[2]=n):h[2]=n),r&&(h[4]?(h[1]="@supports (".concat(h[4],") {").concat(h[1],"}"),h[4]=r):h[4]="".concat(r)),t.push(h))}},t}},601(e){"use strict";e.exports=function(e){return e[1]}},0(e,t,n){var i=n(861);i.__esModule&&(i=i.default),"string"==typeof i&&(i=[[e.id,i,""]]),i.locals&&(e.exports=i.locals),(0,n(534).A)("5d0ec59d",i,!1,{})},534(e,t,n){"use strict";function i(e,t){for(var n=[],i={},r=0;r<t.length;r++){var o=t[r],a=o[0],s={id:e+":"+r,css:o[1],media:o[2],sourceMap:o[3]};i[a]?i[a].parts.push(s):n.push(i[a]={id:a,parts:[s]})}return n}n.d(t,{A:()=>v});var r="undefined"!=typeof document;if("undefined"!=typeof DEBUG&&DEBUG&&!r)throw new Error("vue-style-loader cannot be used in a non-browser environment. Use { target: 'node' } in your Webpack config to indicate a server-rendering environment.");var o={},a=r&&(document.head||document.getElementsByTagName("head")[0]),s=null,d=0,l=!1,h=function(){},u=null,c="data-vue-ssr-id",f="undefined"!=typeof navigator&&/msie [6-9]\b/.test(navigator.userAgent.toLowerCase());function v(e,t,n,r){l=n,u=r||{};var a=i(e,t);return p(a),function(t){for(var n=[],r=0;r<a.length;r++){var s=a[r];(d=o[s.id]).refs--,n.push(d)}for(t?p(a=i(e,t)):a=[],r=0;r<n.length;r++){var d;if(0===(d=n[r]).refs){for(var l=0;l<d.parts.length;l++)d.parts[l]();delete o[d.id]}}}}function p(e){for(var t=0;t<e.length;t++){var n=e[t],i=o[n.id];if(i){i.refs++;for(var r=0;r<i.parts.length;r++)i.parts[r](n.parts[r]);for(;r<n.parts.length;r++)i.parts.push(m(n.parts[r]));i.parts.length>n.parts.length&&(i.parts.length=n.parts.length)}else{var a=[];for(r=0;r<n.parts.length;r++)a.push(m(n.parts[r]));o[n.id]={id:n.id,refs:1,parts:a}}}}function g(){var e=document.createElement("style");return e.type="text/css",a.appendChild(e),e}function m(e){var t,n,i=document.querySelector("style["+c+'~="'+e.id+'"]');if(i){if(l)return h;i.parentNode.removeChild(i)}if(f){var r=d++;i=s||(s=g()),t=y.bind(null,i,r,!1),n=y.bind(null,i,r,!0)}else i=g(),t=x.bind(null,i),n=function(){i.parentNode.removeChild(i)};return t(e),function(i){if(i){if(i.css===e.css&&i.media===e.media&&i.sourceMap===e.sourceMap)return;t(e=i)}else n()}}var b,w=(b=[],function(e,t){return b[e]=t,b.filter(Boolean).join("\n")});function y(e,t,n,i){var r=n?"":i.css;if(e.styleSheet)e.styleSheet.cssText=w(t,r);else{var o=document.createTextNode(r),a=e.childNodes;a[t]&&e.removeChild(a[t]),a.length?e.insertBefore(o,a[t]):e.appendChild(o)}}function x(e,t){var n=t.css,i=t.media,r=t.sourceMap;if(i&&e.setAttribute("media",i),u.ssrId&&e.setAttribute(c,t.id),r&&(n+="\n/*# sourceURL="+r.sources[0]+" */",n+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(r))))+" */"),e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}}};const t={};function n(i){const r=t[i];if(void 0!==r)return r.exports;const o=t[i]={id:i,exports:{}};return e[i](o,o.exports,n),o.exports}n.n=e=>{const t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{if(Array.isArray(t))for(var i=0;i<t.length;){var r=t[i++],o=t[i++];n.o(e,r)?0===o&&i++:0===o?Object.defineProperty(e,r,{enumerable:!0,value:t[i++]}):Object.defineProperty(e,r,{enumerable:!0,get:o})}else for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};let i={};return(()=>{"use strict";n.d(i,{default:()=>u});var e=function(){var e=this,t=e._self._c;return t("div",{ref:"wrapper",staticClass:"hotzone-wrapper",on:{mousedown:e.onWrapperMouseDown,touchstart:function(t){return t.preventDefault(),e.onWrapperTouchStart.apply(null,arguments)}}},[t("img",{ref:"img",attrs:{src:e.src,draggable:"false"},on:{load:e.updateSize}}),e._v(" "),e.drawing?t("div",{staticClass:"drawing-rect",style:e.drawingStyle}):e._e(),e._v(" "),e._l(e.localZones,function(n){return t("div",{key:n.id,staticClass:"zone-rect",class:{active:e.activeZone===n.id,hover:e.hoveredZone===n.id&&e.activeZone!==n.id},style:e.getZoneStyle(n),on:{mousedown:function(t){return t.stopPropagation(),e.onZoneMouseDown(t,n)},mouseenter:function(t){return e.onZoneMouseEnter(n)},mouseleave:function(t){return e.onZoneMouseLeave(n)},touchstart:function(t){return t.stopPropagation(),t.preventDefault(),e.onZoneTouchStart(t,n)}}},[e.mergedConfig.showLabel&&(n.label||n.title)?t("span",{staticClass:"zone-label",style:e.getLabelStyle(n)},[e._v("\n "+e._s(n.label||n.title||"")+"\n ")]):e._e(),e._v(" "),e.mergedConfig.enableResize&&e.mergedConfig.showHandles?e._l(e.handles,function(i){return t("span",{key:i,class:["handle",i],style:e.getHandleStyle(n),on:{mousedown:function(t){return t.stopPropagation(),e.onHandleMouseDown(t,n,i)},touchstart:function(t){return t.stopPropagation(),t.preventDefault(),e.onHandleTouchStart(t,n,i)}}})}):e._e()],2)})],2)};e._withStripped=!0;var t=["borderColor","fillColor","activeBorderColor","activeFillColor"];function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,i)}return n}function a(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?o(Object(n),!0).forEach(function(t){s(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):o(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function s(e,t,n){return(t=function(e){var t=function(e){if("object"!=r(e)||!e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var n=t.call(e,"string");if("object"!=r(n))return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"==r(t)?t:t+""}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const d={name:"ImageHotzone",props:{src:{type:String,required:!0},zones:{type:Array,default:function(){return[]}},theme:{type:Object,default:function(){return{}}},config:{type:Object,default:function(){return{}}}},data:function(){return{localZones:[],wrapperWidth:0,wrapperHeight:0,drawing:!1,drawStart:{x:0,y:0},drawCurrent:{x:0,y:0},activeZone:null,hoveredZone:null,dragData:null,handles:["nw","n","ne","e","se","s","sw","w"],updatingFromParent:!1,updatingFromLocal:!1,drawEventsBound:!1,dragEventsBound:!1,resizeTimer:null,updateTimer:null,rafId:null}},computed:{themeConfig:function(){return a(a({},{drawingLineColor:"#4f6ef7",drawingLineWidth:2,drawingFillColor:"rgba(79, 110, 247, 0.15)",zoneBorderColor:"#4f6ef7",zoneBorderWidth:2,zoneFillColor:"rgba(79, 110, 247, 0.2)",zoneBorderStyle:"solid",zoneOpacity:1,zoneActiveBorderColor:"#e53e3e",zoneActiveFillColor:"rgba(229, 62, 62, 0.15)",zoneActiveBorderStyle:"solid",zoneActiveShadow:"0 0 0 2px rgba(229, 62, 62, 0.3)",zoneHoverBorderColor:"#718096",zoneHoverFillColor:"rgba(113, 128, 150, 0.2)",handleSize:10,handleColor:"#ffffff",handleBorderColor:"#4f6ef7",handleBorderWidth:2,handleActiveBorderColor:"#e53e3e",handleHoverColor:"#4f6ef7",handleActiveHoverColor:"#e53e3e",handleBorderRadius:"3px",handleShadow:"0 1px 4px rgba(0,0,0,0.15)",labelColor:"#333333",labelFontSize:"12px",labelFontWeight:"600",labelBackgroundColor:"transparent",labelPadding:"2px 4px",labelBorderRadius:"0px",labelMaxWidth:"90%",labelTextShadow:"none",transitionDuration:"0s",transitionTiming:"ease"}),this.theme)},mergedConfig:function(){return a(a({},{minSize:2,maxZones:20,showLabel:!0,enableResize:!0,enableMove:!0,enableDraw:!0,enableDelete:!0,showHandles:!0,allowOverlap:!0,snapToGrid:!1,gridSize:5,useAnimationFrame:!0,debounceUpdate:!0}),this.config)},drawingStyle:function(){if(!this.drawing)return{display:"none"};var e=this.drawStart.x,t=this.drawStart.y,n=this.drawCurrent.x,i=this.drawCurrent.y,r=this.themeConfig;return{left:Math.min(e,n)+"px",top:Math.min(t,i)+"px",width:Math.abs(n-e)+"px",height:Math.abs(i-t)+"px",border:"".concat(r.drawingLineWidth,"px dashed ").concat(r.drawingLineColor),backgroundColor:r.drawingFillColor}}},watch:{zones:{immediate:!0,handler:function(e){var t=this;this.updatingFromLocal?this.updatingFromLocal=!1:(this.updatingFromParent=!0,e&&Array.isArray(e)&&(this.localZones=e.map(function(e){return t.normalizeZone(e)})),this.$nextTick(function(){t.updatingFromParent=!1}))}},localZones:{deep:!0,handler:function(e){var t=this;this.updatingFromParent||(this.mergedConfig.debounceUpdate?(clearTimeout(this.updateTimer),this.updateTimer=setTimeout(function(){t.emitZonesUpdate(e)},50)):this.emitZonesUpdate(e))}}},mounted:function(){var e=this;this.$nextTick(function(){e.updateSize()}),window.addEventListener("resize",this.onResize),document.addEventListener("keydown",this.onKeyDown)},beforeDestroy:function(){window.removeEventListener("resize",this.onResize),document.removeEventListener("keydown",this.onKeyDown),this.unbindDrawEvents(),this.unbindDragEvents(),clearTimeout(this.resizeTimer),clearTimeout(this.updateTimer),this.rafId&&cancelAnimationFrame(this.rafId)},methods:{emitZonesUpdate:function(e){var t=this;this.updatingFromLocal=!0;var n=e.map(function(e){return t.cleanZoneData(e)});this.$emit("update:zones",n),this.$emit("change",n)},normalizeZone:function(e){return{id:e.id||this.generateId(),x:Number(e.x)||0,y:Number(e.y)||0,width:Number(e.width)||0,height:Number(e.height)||0,label:e.label||e.title||"",title:e.title||e.label||"",data:e.data||{},style:e.style||{},disabled:e.disabled||!1,visible:!1!==e.visible,draggable:!1!==e.draggable,resizable:!1!==e.resizable}},cleanZoneData:function(e){return{id:e.id,x:Number(e.x.toFixed(2)),y:Number(e.y.toFixed(2)),width:Number(e.width.toFixed(2)),height:Number(e.height.toFixed(2)),label:e.label,title:e.title,data:e.data,disabled:e.disabled,visible:e.visible,draggable:e.draggable,resizable:e.resizable,style:e.style}},generateId:function(){return"zone_"+Date.now()+"_"+Math.random().toString(36).substr(2,9)},updateSize:function(){var e=this.$refs.wrapper;if(e){var t=this.wrapperWidth,n=this.wrapperHeight;this.wrapperWidth=e.offsetWidth,this.wrapperHeight=e.offsetHeight,t===this.wrapperWidth&&n===this.wrapperHeight||this.$emit("resize",{width:this.wrapperWidth,height:this.wrapperHeight})}},onResize:function(){var e=this;clearTimeout(this.resizeTimer),this.resizeTimer=setTimeout(function(){e.updateSize()},100)},getMousePos:function(e){var t=this.$refs.wrapper;if(!t)return{x:0,y:0};var n=t.getBoundingClientRect(),i=e.touches?e.touches[0].clientX:e.clientX,r=e.touches?e.touches[0].clientY:e.clientY;return{x:i-n.left,y:r-n.top}},toPercent:function(e,t){if(0===this.wrapperWidth||0===this.wrapperHeight)return{x:0,y:0};var n=e/this.wrapperWidth*100,i=t/this.wrapperHeight*100;if(this.mergedConfig.snapToGrid){var r=this.mergedConfig.gridSize||5;n=Math.round(n/r)*r,i=Math.round(i/r)*r}return{x:n,y:i}},getZoneStyle:function(e){var n,i,o,s,d,l,h,u=this.themeConfig,c=this.activeZone===e.id,f=this.hoveredZone===e.id&&!c;if(c)n=(null===(l=e.style)||void 0===l?void 0:l.activeBorderColor)||u.zoneActiveBorderColor,i=(null===(h=e.style)||void 0===h?void 0:h.activeFillColor)||u.zoneActiveFillColor,o=u.zoneActiveBorderStyle,s=u.zoneActiveShadow,d=1;else if(f)n=u.zoneHoverBorderColor,i=u.zoneHoverFillColor,o=u.zoneBorderStyle,s="none",d=1;else{var v,p;n=(null===(v=e.style)||void 0===v?void 0:v.borderColor)||u.zoneBorderColor,i=(null===(p=e.style)||void 0===p?void 0:p.fillColor)||u.zoneFillColor,o=u.zoneBorderStyle,s="none",d=u.zoneOpacity}var g={left:e.x/100*this.wrapperWidth+"px",top:e.y/100*this.wrapperHeight+"px",width:e.width/100*this.wrapperWidth+"px",height:e.height/100*this.wrapperHeight+"px",border:"".concat(u.zoneBorderWidth,"px ").concat(o," ").concat(n),backgroundColor:i,boxShadow:s,opacity:!1!==e.visible?d:0,cursor:e.disabled?"not-allowed":this.mergedConfig.enableMove&&!1!==e.draggable?"move":"default",pointerEvents:e.disabled?"none":"auto",transition:c||f?"box-shadow ".concat(u.transitionDuration," ").concat(u.transitionTiming):"none"};if(e.style&&"object"===r(e.style)){var m=e.style,b=(m.borderColor,m.fillColor,m.activeBorderColor,m.activeFillColor,function(e,t){if(null==e)return{};var n,i,r=function(e,t){if(null==e)return{};var n={};for(var i in e)if({}.hasOwnProperty.call(e,i)){if(-1!==t.indexOf(i))continue;n[i]=e[i]}return n}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(i=0;i<o.length;i++)n=o[i],-1===t.indexOf(n)&&{}.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}(m,t));return a(a({},g),b)}return g},getHandleStyle:function(e){var t=this.themeConfig,n=this.activeZone===e.id,i=t.handleSize;return this.mergedConfig.showHandles?{width:i+"px",height:i+"px",backgroundColor:t.handleColor,border:"".concat(t.handleBorderWidth,"px solid ").concat(n?t.handleActiveBorderColor:t.handleBorderColor),borderRadius:t.handleBorderRadius,boxShadow:t.handleShadow,transition:"none"}:{display:"none"}},getLabelStyle:function(e){var t,n,i=this.themeConfig,r=this.activeZone===e.id;return{color:(null===(t=e.style)||void 0===t?void 0:t.labelColor)||i.labelColor,fontSize:i.labelFontSize,fontWeight:i.labelFontWeight,backgroundColor:(null===(n=e.style)||void 0===n?void 0:n.labelBackgroundColor)||i.labelBackgroundColor,padding:i.labelPadding,borderRadius:i.labelBorderRadius,maxWidth:i.labelMaxWidth,textShadow:i.labelTextShadow,position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",textAlign:"center",pointerEvents:"none",zIndex:25,lineHeight:"1.2",opacity:r?1:.8}},onKeyDown:function(e){"Delete"!==e.key&&"Backspace"!==e.key||!this.activeZone||this.mergedConfig.enableDelete&&(e.preventDefault(),this.removeZone(this.activeZone),this.$emit("zone-deleted-by-keyboard",this.activeZone)),"Escape"===e.key&&(this.activeZone=null,this.unbindDragEvents(),this.unbindDrawEvents(),this.drawing=!1,this.dragData=null,this.$emit("selection-cleared"))},bindDrawEvents:function(){this.drawEventsBound||(document.addEventListener("mousemove",this.onDrawMove),document.addEventListener("mouseup",this.onDrawEnd),document.addEventListener("touchmove",this.onDrawMove,{passive:!1}),document.addEventListener("touchend",this.onDrawEnd),this.drawEventsBound=!0)},unbindDrawEvents:function(){this.drawEventsBound&&(document.removeEventListener("mousemove",this.onDrawMove),document.removeEventListener("mouseup",this.onDrawEnd),document.removeEventListener("touchmove",this.onDrawMove),document.removeEventListener("touchend",this.onDrawEnd),this.drawEventsBound=!1)},bindDragEvents:function(){this.dragEventsBound||(document.addEventListener("mousemove",this.onDragMove),document.addEventListener("mouseup",this.onDragEnd),document.addEventListener("touchmove",this.onDragMove,{passive:!1}),document.addEventListener("touchend",this.onDragEnd),this.dragEventsBound=!0)},unbindDragEvents:function(){this.dragEventsBound&&(document.removeEventListener("mousemove",this.onDragMove),document.removeEventListener("mouseup",this.onDragEnd),document.removeEventListener("touchmove",this.onDragMove),document.removeEventListener("touchend",this.onDragEnd),this.dragEventsBound=!1)},onWrapperMouseDown:function(e){0===e.button&&this.mergedConfig.enableDraw&&this.startDraw(e)},onWrapperTouchStart:function(e){this.mergedConfig.enableDraw&&this.startDraw(e)},startDraw:function(e){if(this.unbindDragEvents(),this.dragData=null,this.localZones.length>=(this.mergedConfig.maxZones||20))this.$emit("max-reached",this.mergedConfig.maxZones);else{var t=this.getMousePos(e);this.drawing=!0,this.drawStart=t,this.drawCurrent=t,this.activeZone=null,this.$emit("draw-start",{x:t.x,y:t.y}),this.bindDrawEvents()}},onDrawMove:function(e){var t=this;if(this.drawing)if(e.preventDefault(),this.mergedConfig.useAnimationFrame){if(this.rafId)return;this.rafId=requestAnimationFrame(function(){t.rafId=null,t.drawCurrent=t.getMousePos(e);var n=t.drawStart.x,i=t.drawStart.y,r=t.drawCurrent.x,o=t.drawCurrent.y,a=Math.min(n,r),s=Math.min(i,o),d=Math.abs(r-n),l=Math.abs(o-i);t.$emit("drawing",{x:a,y:s,width:d,height:l})})}else{this.drawCurrent=this.getMousePos(e);var n=this.drawStart.x,i=this.drawStart.y,r=this.drawCurrent.x,o=this.drawCurrent.y,a=Math.min(n,r),s=Math.min(i,o),d=Math.abs(r-n),l=Math.abs(o-i);this.$emit("drawing",{x:a,y:s,width:d,height:l})}else this.unbindDrawEvents()},onDrawEnd:function(){if(this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.drawing){this.unbindDrawEvents();var e=this.drawStart.x,t=this.drawStart.y,n=this.drawCurrent.x,i=this.drawCurrent.y,r=Math.min(e,n),o=Math.min(t,i),s=Math.abs(n-e),d=Math.abs(i-t),l=(this.mergedConfig.minSize||2)/100*Math.min(this.wrapperWidth,this.wrapperHeight);if(this.$emit("draw-end",{x:r,y:o,width:s,height:d,valid:s>=l&&d>=l}),s>=l&&d>=l){var h=this.toPercent(r,o),u=this.toPercent(s,d),c={id:this.generateId(),x:h.x,y:h.y,width:u.x,height:u.y,label:"热区".concat(this.localZones.length+1),title:"热区".concat(this.localZones.length+1),data:{},disabled:!1,visible:!0,draggable:!0,resizable:!0,style:{}};this.localZones.push(c),this.activeZone=c.id,this.$emit("zone-created",a({},c))}this.drawing=!1}else this.unbindDrawEvents()},onZoneMouseEnter:function(e){this.hoveredZone=e.id,this.$emit("zone-hover",a({},e))},onZoneMouseLeave:function(e){this.hoveredZone===e.id&&(this.hoveredZone=null,this.$emit("zone-leave",a({},e)))},onZoneMouseDown:function(e,t){0===e.button&&this.mergedConfig.enableMove&&(t.disabled||!1===t.draggable||(this.startDrag(e,t,null),this.$emit("zone-click",a({},t))))},onZoneTouchStart:function(e,t){this.mergedConfig.enableMove&&(t.disabled||!1===t.draggable||(this.startDrag(e,t,null),this.$emit("zone-click",a({},t))))},onHandleMouseDown:function(e,t,n){0===e.button&&this.mergedConfig.enableResize&&(t.disabled||!1===t.resizable||this.startDrag(e,t,n))},onHandleTouchStart:function(e,t,n){this.mergedConfig.enableResize&&(t.disabled||!1===t.resizable||this.startDrag(e,t,n))},startDrag:function(e,t,n){this.unbindDrawEvents(),this.drawing=!1,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null);var i=this.activeZone;this.activeZone=t.id,i!==t.id&&this.$emit("zone-focus",a({},t));var r=this.getMousePos(e);this.dragData={zoneId:t.id,startMouse:r,startZone:a({},t),handle:n},this.$emit("drag-start",{zone:a({},t),handle:n,type:n?"resize":"move"}),this.bindDragEvents()},onDragMove:function(e){var t=this;if(this.dragData)if(e.preventDefault(),this.mergedConfig.useAnimationFrame){if(this.rafId)return;this.rafId=requestAnimationFrame(function(){t.rafId=null,t.processDrag(e)})}else this.processDrag(e);else this.unbindDragEvents()},processDrag:function(e){var t=this;if(this.dragData){var n=this.getMousePos(e),i=(n.x-this.dragData.startMouse.x)/this.wrapperWidth*100,r=(n.y-this.dragData.startMouse.y)/this.wrapperHeight*100,o=this.localZones.findIndex(function(e){return e.id===t.dragData.zoneId});if(-1===o)return this.unbindDragEvents(),void(this.dragData=null);var s=a({},this.localZones[o]),d=this.dragData.startZone,l=this.mergedConfig.minSize||2;if(this.dragData.handle){var h=this.dragData.handle,u=d.x,c=d.y,f=d.width,v=d.height;h.includes("e")&&(f+=i),h.includes("w")&&(u+=i,f-=i),h.includes("s")&&(v+=r),h.includes("n")&&(c+=r,v-=r),f<l&&(h.includes("w")&&(u=d.x+d.width-l),f=l),v<l&&(h.includes("n")&&(c=d.y+d.height-l),v=l),u<0&&(f+=u,u=0),c<0&&(v+=c,c=0),u+f>100&&(f=100-u),c+v>100&&(v=100-c),s=a(a({},s),{},{x:u,y:c,width:f,height:v}),this.$emit("resizing",{zone:a({},s),handle:h,originalZone:a({},d)})}else{var p=d.x+i,g=d.y+r;p=Math.max(0,Math.min(p,100-s.width)),g=Math.max(0,Math.min(g,100-s.height)),s.x=p,s.y=g,this.$emit("moving",{zone:a({},s),originalZone:a({},d)})}this.$set(this.localZones,o,s),this.$emit("zone-updated",a({},s))}},onDragEnd:function(){var e=this;if(this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.dragData){var t=this.localZones.find(function(t){return t.id===e.dragData.zoneId});t&&this.$emit("drag-end",{zone:a({},t),type:this.dragData.handle?"resize":"move"})}this.unbindDragEvents(),this.dragData=null},addZone:function(e){if(this.localZones.length>=(this.mergedConfig.maxZones||20))return this.$emit("max-reached",this.mergedConfig.maxZones),null;var t=this.normalizeZone({id:e.id||this.generateId(),x:e.x||10,y:e.y||10,width:e.width||20,height:e.height||20,label:e.label||"热区".concat(this.localZones.length+1),title:e.title||e.label||"热区".concat(this.localZones.length+1),data:e.data||{},disabled:e.disabled||!1,visible:!1!==e.visible,draggable:!1!==e.draggable,resizable:!1!==e.resizable,style:e.style||{}});return t.x=Math.max(0,Math.min(t.x,100-t.width)),t.y=Math.max(0,Math.min(t.y,100-t.height)),this.localZones.push(t),this.activeZone=t.id,this.$emit("zone-created",a({},t)),t},addZones:function(e){var t=this;return Array.isArray(e)?e.map(function(e){return t.addZone(e)}).filter(Boolean):[]},updateZone:function(e,t){var n=this.localZones.findIndex(function(t){return t.id===e});if(-1===n)return!1;var i=this.localZones[n],r=a({},i);return void 0!==t.x&&(r.x=Number(t.x)),void 0!==t.y&&(r.y=Number(t.y)),void 0!==t.width&&(r.width=Number(t.width)),void 0!==t.height&&(r.height=Number(t.height)),void 0!==t.label&&(r.label=t.label),void 0!==t.title&&(r.title=t.title),void 0!==t.data&&(r.data=a(a({},i.data),t.data)),void 0!==t.disabled&&(r.disabled=t.disabled),void 0!==t.visible&&(r.visible=t.visible),void 0!==t.draggable&&(r.draggable=t.draggable),void 0!==t.resizable&&(r.resizable=t.resizable),void 0!==t.style&&(r.style=a(a({},i.style),t.style)),r.x=Math.max(0,Math.min(r.x,100-r.width)),r.y=Math.max(0,Math.min(r.y,100-r.height)),this.$set(this.localZones,n,r),this.$emit("zone-updated",a({},r)),!0},setActiveZone:function(e){if(null===e)return this.activeZone=null,void this.$emit("selection-cleared");var t=this.localZones.find(function(t){return t.id===e});t&&(this.activeZone=e,this.$emit("zone-focus",a({},t)))},getActiveZone:function(){var e=this;if(!this.activeZone)return null;var t=this.localZones.find(function(t){return t.id===e.activeZone});return t?a({},t):null},getZone:function(e){var t=this.localZones.find(function(t){return t.id===e});return t?a({},t):null},getAllZones:function(){return this.localZones.map(function(e){return a({},e)})},removeZone:function(e){var t=this.localZones.findIndex(function(t){return t.id===e});if(-1!==t){var n=this.localZones.splice(t,1)[0];return this.activeZone===e&&(this.activeZone=null),this.hoveredZone===e&&(this.hoveredZone=null),this.$emit("zone-deleted",n),!0}return!1},clearAll:function(){this.unbindDrawEvents(),this.unbindDragEvents(),this.drawing=!1,this.dragData=null,this.activeZone=null,this.hoveredZone=null,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.localZones=[],this.$emit("all-cleared")},exportZones:function(){var e=this;return this.localZones.map(function(t){return e.cleanZoneData(t)})},importZones:function(e){Array.isArray(e)&&(this.clearAll(),this.addZones(e),this.$emit("zones-imported",this.localZones.length))},getSize:function(){return{width:this.wrapperWidth,height:this.wrapperHeight}},disableZone:function(e){this.updateZone(e,{disabled:!0})},enableZone:function(e){this.updateZone(e,{disabled:!1})},hideZone:function(e){this.updateZone(e,{visible:!1})},showZone:function(e){this.updateZone(e,{visible:!0})}}};n(0);var l=function(e,t,n,i,r,o){var a,s="function"==typeof e?e.options:e;if(t&&(s.render=t,s.staticRenderFns=[],s._compiled=!0),o&&(s._scopeId="data-v-"+o),a)if(s.functional){s._injectStyles=a;var d=s.render;s.render=function(e,t){return a.call(t),d(e,t)}}else{var l=s.beforeCreate;s.beforeCreate=l?[].concat(l,a):[a]}return{exports:e,options:s}}(d,e,0,0,0,"658dd234");const h=l.exports;h.install=function(e){e.component(h.name,h)},"undefined"!=typeof window&&window.Vue&&window.Vue.use(h);const u=h})(),i=i.default,i})());
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@kirkw/vue2-image-hotzone",
3
+ "version": "1.0.0",
4
+ "description": "A Vue 2 component for creating image hotzones with drag, resize and move support",
5
+ "main": "dist/vue2-image-hotzone.js",
6
+ "module": "dist/vue2-image-hotzone.js",
7
+ "unpkg": "dist/vue2-image-hotzone.js",
8
+ "jsdelivr": "dist/vue2-image-hotzone.js",
9
+ "files": [
10
+ "dist",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "registry": "https://registry.npmjs.org/"
17
+ },
18
+ "scripts": {
19
+ "dev": "webpack serve --config webpack.config.js",
20
+ "build": "webpack --config webpack.config.js",
21
+ "build:prod": "webpack --config webpack.config.js --mode=production",
22
+ "prepublishOnly": "npm run build:prod"
23
+ },
24
+ "keywords": [
25
+ "vue",
26
+ "vue2",
27
+ "image",
28
+ "hotzone",
29
+ "hotspot",
30
+ "image-map",
31
+ "image-annotation",
32
+ "vue-component"
33
+ ],
34
+ "author": "Your Name <your.email@example.com>",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/yourusername/vue2-image-hotzone.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/yourusername/vue2-image-hotzone/issues"
42
+ },
43
+ "homepage": "https://github.com/yourusername/vue2-image-hotzone#readme",
44
+ "peerDependencies": {
45
+ "vue": "^2.6.0"
46
+ },
47
+ "devDependencies": {
48
+ "@babel/core": "^7.29.7",
49
+ "@babel/preset-env": "^7.29.7",
50
+ "babel-loader": "^8.4.1",
51
+ "css-loader": "^6.11.0",
52
+ "vue-loader": "^15.11.1",
53
+ "vue-template-compiler": "^2.7.16",
54
+ "webpack": "^5.108.3",
55
+ "webpack-cli": "^5.1.4",
56
+ "webpack-dev-server": "^4.15.2"
57
+ }
58
+ }
@@ -0,0 +1,1046 @@
1
+ <template>
2
+ <div
3
+ class="hotzone-wrapper"
4
+ ref="wrapper"
5
+ @mousedown="onWrapperMouseDown"
6
+ @touchstart.prevent="onWrapperTouchStart"
7
+ >
8
+ <img
9
+ :src="src"
10
+ ref="img"
11
+ @load="updateSize"
12
+ draggable="false"
13
+ />
14
+
15
+ <!-- 绘制中的矩形 -->
16
+ <div
17
+ v-if="drawing"
18
+ class="drawing-rect"
19
+ :style="drawingStyle"
20
+ ></div>
21
+
22
+ <!-- 已创建的热区 -->
23
+ <div
24
+ v-for="zone in localZones"
25
+ :key="zone.id"
26
+ class="zone-rect"
27
+ :class="{
28
+ active: activeZone === zone.id,
29
+ hover: hoveredZone === zone.id && activeZone !== zone.id
30
+ }"
31
+ :style="getZoneStyle(zone)"
32
+ @mousedown.stop="onZoneMouseDown($event, zone)"
33
+ @mouseenter="onZoneMouseEnter(zone)"
34
+ @mouseleave="onZoneMouseLeave(zone)"
35
+ @touchstart.stop.prevent="onZoneTouchStart($event, zone)"
36
+ >
37
+ <!-- 标签居中显示 - 无背景 -->
38
+ <span
39
+ v-if="mergedConfig.showLabel && (zone.label || zone.title)"
40
+ class="zone-label"
41
+ :style="getLabelStyle(zone)"
42
+ >
43
+ {{ zone.label || zone.title || '' }}
44
+ </span>
45
+
46
+ <!-- 8个拖拽手柄 -->
47
+ <template v-if="mergedConfig.enableResize && mergedConfig.showHandles">
48
+ <span
49
+ v-for="handle in handles"
50
+ :key="handle"
51
+ :class="['handle', handle]"
52
+ :style="getHandleStyle(zone)"
53
+ @mousedown.stop="onHandleMouseDown($event, zone, handle)"
54
+ @touchstart.stop.prevent="onHandleTouchStart($event, zone, handle)"
55
+ ></span>
56
+ </template>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <script>
62
+ export default {
63
+ name: 'ImageHotzone',
64
+ props: {
65
+ // 图片地址
66
+ src: {
67
+ type: String,
68
+ required: true
69
+ },
70
+ // 热区数据(支持.sync修饰符)
71
+ zones: {
72
+ type: Array,
73
+ default: () => []
74
+ },
75
+ // 主题配置
76
+ theme: {
77
+ type: Object,
78
+ default: () => ({})
79
+ },
80
+ // 功能配置
81
+ config: {
82
+ type: Object,
83
+ default: () => ({})
84
+ }
85
+ },
86
+ data() {
87
+ return {
88
+ localZones: [],
89
+ wrapperWidth: 0,
90
+ wrapperHeight: 0,
91
+ drawing: false,
92
+ drawStart: { x: 0, y: 0 },
93
+ drawCurrent: { x: 0, y: 0 },
94
+ activeZone: null,
95
+ hoveredZone: null,
96
+ dragData: null,
97
+ handles: ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'],
98
+ updatingFromParent: false,
99
+ updatingFromLocal: false,
100
+ drawEventsBound: false,
101
+ dragEventsBound: false,
102
+ // 性能优化:缓存计算
103
+ resizeTimer: null,
104
+ updateTimer: null,
105
+ // requestAnimationFrame ID
106
+ rafId: null
107
+ }
108
+ },
109
+ computed: {
110
+ // 合并默认主题和用户自定义主题
111
+ themeConfig() {
112
+ const defaultTheme = {
113
+ // 绘制中虚线框
114
+ drawingLineColor: '#4f6ef7',
115
+ drawingLineWidth: 2,
116
+ drawingFillColor: 'rgba(79, 110, 247, 0.15)',
117
+
118
+ // 热区默认状态
119
+ zoneBorderColor: '#4f6ef7',
120
+ zoneBorderWidth: 2,
121
+ zoneFillColor: 'rgba(79, 110, 247, 0.2)',
122
+ zoneBorderStyle: 'solid',
123
+ zoneOpacity: 1,
124
+
125
+ // 热区激活状态
126
+ zoneActiveBorderColor: '#e53e3e',
127
+ zoneActiveFillColor: 'rgba(229, 62, 62, 0.15)',
128
+ zoneActiveBorderStyle: 'solid',
129
+ zoneActiveShadow: '0 0 0 2px rgba(229, 62, 62, 0.3)',
130
+
131
+ // 热区悬停状态
132
+ zoneHoverBorderColor: '#718096',
133
+ zoneHoverFillColor: 'rgba(113, 128, 150, 0.2)',
134
+
135
+ // 手柄样式
136
+ handleSize: 10,
137
+ handleColor: '#ffffff',
138
+ handleBorderColor: '#4f6ef7',
139
+ handleBorderWidth: 2,
140
+ handleActiveBorderColor: '#e53e3e',
141
+ handleHoverColor: '#4f6ef7',
142
+ handleActiveHoverColor: '#e53e3e',
143
+ handleBorderRadius: '3px',
144
+ handleShadow: '0 1px 4px rgba(0,0,0,0.15)',
145
+
146
+ // 标签样式 - 默认无背景
147
+ labelColor: '#333333',
148
+ labelFontSize: '12px',
149
+ labelFontWeight: '600',
150
+ labelBackgroundColor: 'transparent',
151
+ labelPadding: '2px 4px',
152
+ labelBorderRadius: '0px',
153
+ labelMaxWidth: '90%',
154
+ labelTextShadow: 'none',
155
+
156
+ // 过渡动画
157
+ transitionDuration: '0s',
158
+ transitionTiming: 'ease'
159
+ }
160
+
161
+ return { ...defaultTheme, ...this.theme }
162
+ },
163
+
164
+ // 合并默认配置和用户配置
165
+ mergedConfig() {
166
+ const defaultConfig = {
167
+ minSize: 2,
168
+ maxZones: 20,
169
+ showLabel: true,
170
+ enableResize: true,
171
+ enableMove: true,
172
+ enableDraw: true,
173
+ enableDelete: true,
174
+ showHandles: true,
175
+ allowOverlap: true,
176
+ snapToGrid: false,
177
+ gridSize: 5,
178
+ // 性能优化选项
179
+ useAnimationFrame: true,
180
+ debounceUpdate: true,
181
+ }
182
+
183
+ return { ...defaultConfig, ...this.config }
184
+ },
185
+
186
+ drawingStyle() {
187
+ if (!this.drawing) return { display: 'none' }
188
+ const x1 = this.drawStart.x
189
+ const y1 = this.drawStart.y
190
+ const x2 = this.drawCurrent.x
191
+ const y2 = this.drawCurrent.y
192
+
193
+ const theme = this.themeConfig
194
+
195
+ return {
196
+ left: Math.min(x1, x2) + 'px',
197
+ top: Math.min(y1, y2) + 'px',
198
+ width: Math.abs(x2 - x1) + 'px',
199
+ height: Math.abs(y2 - y1) + 'px',
200
+ border: `${theme.drawingLineWidth}px dashed ${theme.drawingLineColor}`,
201
+ backgroundColor: theme.drawingFillColor
202
+ }
203
+ }
204
+ },
205
+ watch: {
206
+ zones: {
207
+ immediate: true,
208
+ handler(val) {
209
+ if (this.updatingFromLocal) {
210
+ this.updatingFromLocal = false
211
+ return
212
+ }
213
+
214
+ this.updatingFromParent = true
215
+
216
+ if (val && Array.isArray(val)) {
217
+ this.localZones = val.map(z => this.normalizeZone(z))
218
+ }
219
+
220
+ this.$nextTick(() => {
221
+ this.updatingFromParent = false
222
+ })
223
+ }
224
+ },
225
+ localZones: {
226
+ deep: true,
227
+ handler(val) {
228
+ if (this.updatingFromParent) {
229
+ return
230
+ }
231
+
232
+ // 防抖更新
233
+ if (this.mergedConfig.debounceUpdate) {
234
+ clearTimeout(this.updateTimer)
235
+ this.updateTimer = setTimeout(() => {
236
+ this.emitZonesUpdate(val)
237
+ }, 50)
238
+ } else {
239
+ this.emitZonesUpdate(val)
240
+ }
241
+ }
242
+ }
243
+ },
244
+ mounted() {
245
+ this.$nextTick(() => {
246
+ this.updateSize()
247
+ })
248
+ window.addEventListener('resize', this.onResize)
249
+ document.addEventListener('keydown', this.onKeyDown)
250
+ },
251
+ beforeDestroy() {
252
+ window.removeEventListener('resize', this.onResize)
253
+ document.removeEventListener('keydown', this.onKeyDown)
254
+ this.unbindDrawEvents()
255
+ this.unbindDragEvents()
256
+ clearTimeout(this.resizeTimer)
257
+ clearTimeout(this.updateTimer)
258
+ if (this.rafId) {
259
+ cancelAnimationFrame(this.rafId)
260
+ }
261
+ },
262
+ methods: {
263
+ emitZonesUpdate(val) {
264
+ this.updatingFromLocal = true
265
+ const cleanZones = val.map(z => this.cleanZoneData(z))
266
+ this.$emit('update:zones', cleanZones)
267
+ this.$emit('change', cleanZones)
268
+ },
269
+
270
+ // ========== 数据标准化 ==========
271
+ normalizeZone(zone) {
272
+ return {
273
+ id: zone.id || this.generateId(),
274
+ x: Number(zone.x) || 0,
275
+ y: Number(zone.y) || 0,
276
+ width: Number(zone.width) || 0,
277
+ height: Number(zone.height) || 0,
278
+ label: zone.label || zone.title || '',
279
+ title: zone.title || zone.label || '',
280
+ data: zone.data || {},
281
+ style: zone.style || {},
282
+ disabled: zone.disabled || false,
283
+ visible: zone.visible !== false,
284
+ draggable: zone.draggable !== false,
285
+ resizable: zone.resizable !== false,
286
+ }
287
+ },
288
+
289
+ cleanZoneData(zone) {
290
+ return {
291
+ id: zone.id,
292
+ x: Number(zone.x.toFixed(2)),
293
+ y: Number(zone.y.toFixed(2)),
294
+ width: Number(zone.width.toFixed(2)),
295
+ height: Number(zone.height.toFixed(2)),
296
+ label: zone.label,
297
+ title: zone.title,
298
+ data: zone.data,
299
+ disabled: zone.disabled,
300
+ visible: zone.visible,
301
+ draggable: zone.draggable,
302
+ resizable: zone.resizable,
303
+ style: zone.style
304
+ }
305
+ },
306
+
307
+ generateId() {
308
+ return 'zone_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
309
+ },
310
+
311
+ // ========== 尺寸计算 ==========
312
+ updateSize() {
313
+ const wrapper = this.$refs.wrapper
314
+ if (wrapper) {
315
+ const oldWidth = this.wrapperWidth
316
+ const oldHeight = this.wrapperHeight
317
+
318
+ this.wrapperWidth = wrapper.offsetWidth
319
+ this.wrapperHeight = wrapper.offsetHeight
320
+
321
+ if (oldWidth !== this.wrapperWidth || oldHeight !== this.wrapperHeight) {
322
+ this.$emit('resize', {
323
+ width: this.wrapperWidth,
324
+ height: this.wrapperHeight
325
+ })
326
+ }
327
+ }
328
+ },
329
+
330
+ onResize() {
331
+ clearTimeout(this.resizeTimer)
332
+ this.resizeTimer = setTimeout(() => {
333
+ this.updateSize()
334
+ }, 100)
335
+ },
336
+
337
+ // ========== 坐标转换 ==========
338
+ getMousePos(e) {
339
+ const wrapper = this.$refs.wrapper
340
+ if (!wrapper) return { x: 0, y: 0 }
341
+ const rect = wrapper.getBoundingClientRect()
342
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX
343
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY
344
+ return {
345
+ x: clientX - rect.left,
346
+ y: clientY - rect.top
347
+ }
348
+ },
349
+
350
+ toPercent(px, py) {
351
+ if (this.wrapperWidth === 0 || this.wrapperHeight === 0) {
352
+ return { x: 0, y: 0 }
353
+ }
354
+
355
+ let x = (px / this.wrapperWidth) * 100
356
+ let y = (py / this.wrapperHeight) * 100
357
+
358
+ if (this.mergedConfig.snapToGrid) {
359
+ const gridSize = this.mergedConfig.gridSize || 5
360
+ x = Math.round(x / gridSize) * gridSize
361
+ y = Math.round(y / gridSize) * gridSize
362
+ }
363
+
364
+ return { x, y }
365
+ },
366
+
367
+ getZoneStyle(zone) {
368
+ const theme = this.themeConfig
369
+ const isActive = this.activeZone === zone.id
370
+ const isHovered = this.hoveredZone === zone.id && !isActive
371
+
372
+ let borderColor, fillColor, borderStyle, shadow, opacity
373
+
374
+ if (isActive) {
375
+ borderColor = zone.style?.activeBorderColor || theme.zoneActiveBorderColor
376
+ fillColor = zone.style?.activeFillColor || theme.zoneActiveFillColor
377
+ borderStyle = theme.zoneActiveBorderStyle
378
+ shadow = theme.zoneActiveShadow
379
+ opacity = 1
380
+ } else if (isHovered) {
381
+ borderColor = theme.zoneHoverBorderColor
382
+ fillColor = theme.zoneHoverFillColor
383
+ borderStyle = theme.zoneBorderStyle
384
+ shadow = 'none'
385
+ opacity = 1
386
+ } else {
387
+ borderColor = zone.style?.borderColor || theme.zoneBorderColor
388
+ fillColor = zone.style?.fillColor || theme.zoneFillColor
389
+ borderStyle = theme.zoneBorderStyle
390
+ shadow = 'none'
391
+ opacity = theme.zoneOpacity
392
+ }
393
+
394
+ const baseStyle = {
395
+ left: (zone.x / 100 * this.wrapperWidth) + 'px',
396
+ top: (zone.y / 100 * this.wrapperHeight) + 'px',
397
+ width: (zone.width / 100 * this.wrapperWidth) + 'px',
398
+ height: (zone.height / 100 * this.wrapperHeight) + 'px',
399
+ border: `${theme.zoneBorderWidth}px ${borderStyle} ${borderColor}`,
400
+ backgroundColor: fillColor,
401
+ boxShadow: shadow,
402
+ opacity: zone.visible !== false ? opacity : 0,
403
+ cursor: zone.disabled ? 'not-allowed' : (this.mergedConfig.enableMove && zone.draggable !== false ? 'move' : 'default'),
404
+ pointerEvents: zone.disabled ? 'none' : 'auto',
405
+ transition: isActive || isHovered ? `box-shadow ${theme.transitionDuration} ${theme.transitionTiming}` : 'none'
406
+ }
407
+
408
+ if (zone.style && typeof zone.style === 'object') {
409
+ const { borderColor: bc, fillColor: fc, activeBorderColor: abc, activeFillColor: afc, ...restStyle } = zone.style
410
+ return { ...baseStyle, ...restStyle }
411
+ }
412
+
413
+ return baseStyle
414
+ },
415
+
416
+ getHandleStyle(zone) {
417
+ const theme = this.themeConfig
418
+ const isActive = this.activeZone === zone.id
419
+ const size = theme.handleSize
420
+
421
+ if (!this.mergedConfig.showHandles) {
422
+ return { display: 'none' }
423
+ }
424
+
425
+ return {
426
+ width: size + 'px',
427
+ height: size + 'px',
428
+ backgroundColor: theme.handleColor,
429
+ border: `${theme.handleBorderWidth}px solid ${isActive ? theme.handleActiveBorderColor : theme.handleBorderColor}`,
430
+ borderRadius: theme.handleBorderRadius,
431
+ boxShadow: theme.handleShadow,
432
+ transition: 'none'
433
+ }
434
+ },
435
+
436
+ getLabelStyle(zone) {
437
+ const theme = this.themeConfig
438
+ const isActive = this.activeZone === zone.id
439
+
440
+ return {
441
+ color: zone.style?.labelColor || theme.labelColor,
442
+ fontSize: theme.labelFontSize,
443
+ fontWeight: theme.labelFontWeight,
444
+ backgroundColor: zone.style?.labelBackgroundColor || theme.labelBackgroundColor,
445
+ padding: theme.labelPadding,
446
+ borderRadius: theme.labelBorderRadius,
447
+ maxWidth: theme.labelMaxWidth,
448
+ textShadow: theme.labelTextShadow,
449
+ // 居中定位
450
+ position: 'absolute',
451
+ top: '50%',
452
+ left: '50%',
453
+ transform: 'translate(-50%, -50%)',
454
+ whiteSpace: 'nowrap',
455
+ overflow: 'hidden',
456
+ textOverflow: 'ellipsis',
457
+ textAlign: 'center',
458
+ pointerEvents: 'none',
459
+ zIndex: 25,
460
+ lineHeight: '1.2',
461
+ // 激活状态加粗
462
+ opacity: isActive ? 1 : 0.8
463
+ }
464
+ },
465
+
466
+ // ========== 键盘事件 ==========
467
+ onKeyDown(e) {
468
+ if ((e.key === 'Delete' || e.key === 'Backspace') && this.activeZone) {
469
+ if (this.mergedConfig.enableDelete) {
470
+ e.preventDefault()
471
+ this.removeZone(this.activeZone)
472
+ this.$emit('zone-deleted-by-keyboard', this.activeZone)
473
+ }
474
+ }
475
+
476
+ if (e.key === 'Escape') {
477
+ this.activeZone = null
478
+ this.unbindDragEvents()
479
+ this.unbindDrawEvents()
480
+ this.drawing = false
481
+ this.dragData = null
482
+ this.$emit('selection-cleared')
483
+ }
484
+ },
485
+
486
+ // ========== 事件绑定/解绑管理 ==========
487
+ bindDrawEvents() {
488
+ if (this.drawEventsBound) return
489
+
490
+ document.addEventListener('mousemove', this.onDrawMove)
491
+ document.addEventListener('mouseup', this.onDrawEnd)
492
+ document.addEventListener('touchmove', this.onDrawMove, { passive: false })
493
+ document.addEventListener('touchend', this.onDrawEnd)
494
+
495
+ this.drawEventsBound = true
496
+ },
497
+
498
+ unbindDrawEvents() {
499
+ if (!this.drawEventsBound) return
500
+
501
+ document.removeEventListener('mousemove', this.onDrawMove)
502
+ document.removeEventListener('mouseup', this.onDrawEnd)
503
+ document.removeEventListener('touchmove', this.onDrawMove)
504
+ document.removeEventListener('touchend', this.onDrawEnd)
505
+
506
+ this.drawEventsBound = false
507
+ },
508
+
509
+ bindDragEvents() {
510
+ if (this.dragEventsBound) return
511
+
512
+ document.addEventListener('mousemove', this.onDragMove)
513
+ document.addEventListener('mouseup', this.onDragEnd)
514
+ document.addEventListener('touchmove', this.onDragMove, { passive: false })
515
+ document.addEventListener('touchend', this.onDragEnd)
516
+
517
+ this.dragEventsBound = true
518
+ },
519
+
520
+ unbindDragEvents() {
521
+ if (!this.dragEventsBound) return
522
+
523
+ document.removeEventListener('mousemove', this.onDragMove)
524
+ document.removeEventListener('mouseup', this.onDragEnd)
525
+ document.removeEventListener('touchmove', this.onDragMove)
526
+ document.removeEventListener('touchend', this.onDragEnd)
527
+
528
+ this.dragEventsBound = false
529
+ },
530
+
531
+ // ========== 绘制新热区 ==========
532
+ onWrapperMouseDown(e) {
533
+ if (e.button !== 0) return
534
+ if (!this.mergedConfig.enableDraw) return
535
+ this.startDraw(e)
536
+ },
537
+
538
+ onWrapperTouchStart(e) {
539
+ if (!this.mergedConfig.enableDraw) return
540
+ this.startDraw(e)
541
+ },
542
+
543
+ startDraw(e) {
544
+ this.unbindDragEvents()
545
+ this.dragData = null
546
+
547
+ if (this.localZones.length >= (this.mergedConfig.maxZones || 20)) {
548
+ this.$emit('max-reached', this.mergedConfig.maxZones)
549
+ return
550
+ }
551
+
552
+ const pos = this.getMousePos(e)
553
+ this.drawing = true
554
+ this.drawStart = pos
555
+ this.drawCurrent = pos
556
+ this.activeZone = null
557
+
558
+ this.$emit('draw-start', { x: pos.x, y: pos.y })
559
+
560
+ this.bindDrawEvents()
561
+ },
562
+
563
+ onDrawMove(e) {
564
+ if (!this.drawing) {
565
+ this.unbindDrawEvents()
566
+ return
567
+ }
568
+ e.preventDefault()
569
+
570
+ if (this.mergedConfig.useAnimationFrame) {
571
+ if (this.rafId) return
572
+ this.rafId = requestAnimationFrame(() => {
573
+ this.rafId = null
574
+ this.drawCurrent = this.getMousePos(e)
575
+
576
+ const x1 = this.drawStart.x
577
+ const y1 = this.drawStart.y
578
+ const x2 = this.drawCurrent.x
579
+ const y2 = this.drawCurrent.y
580
+
581
+ const left = Math.min(x1, x2)
582
+ const top = Math.min(y1, y2)
583
+ const width = Math.abs(x2 - x1)
584
+ const height = Math.abs(y2 - y1)
585
+
586
+ this.$emit('drawing', { x: left, y: top, width, height })
587
+ })
588
+ } else {
589
+ this.drawCurrent = this.getMousePos(e)
590
+
591
+ const x1 = this.drawStart.x
592
+ const y1 = this.drawStart.y
593
+ const x2 = this.drawCurrent.x
594
+ const y2 = this.drawCurrent.y
595
+
596
+ const left = Math.min(x1, x2)
597
+ const top = Math.min(y1, y2)
598
+ const width = Math.abs(x2 - x1)
599
+ const height = Math.abs(y2 - y1)
600
+
601
+ this.$emit('drawing', { x: left, y: top, width, height })
602
+ }
603
+ },
604
+
605
+ onDrawEnd() {
606
+ if (this.rafId) {
607
+ cancelAnimationFrame(this.rafId)
608
+ this.rafId = null
609
+ }
610
+
611
+ if (!this.drawing) {
612
+ this.unbindDrawEvents()
613
+ return
614
+ }
615
+
616
+ this.unbindDrawEvents()
617
+
618
+ const x1 = this.drawStart.x
619
+ const y1 = this.drawStart.y
620
+ const x2 = this.drawCurrent.x
621
+ const y2 = this.drawCurrent.y
622
+
623
+ const left = Math.min(x1, x2)
624
+ const top = Math.min(y1, y2)
625
+ const width = Math.abs(x2 - x1)
626
+ const height = Math.abs(y2 - y1)
627
+
628
+ const minPx = (this.mergedConfig.minSize || 2) / 100 * Math.min(this.wrapperWidth, this.wrapperHeight)
629
+
630
+ this.$emit('draw-end', {
631
+ x: left, y: top, width, height,
632
+ valid: width >= minPx && height >= minPx
633
+ })
634
+
635
+ if (width >= minPx && height >= minPx) {
636
+ const percentPos = this.toPercent(left, top)
637
+ const percentSize = this.toPercent(width, height)
638
+
639
+ const newZone = {
640
+ id: this.generateId(),
641
+ x: percentPos.x,
642
+ y: percentPos.y,
643
+ width: percentSize.x,
644
+ height: percentSize.y,
645
+ label: `热区${this.localZones.length + 1}`,
646
+ title: `热区${this.localZones.length + 1}`,
647
+ data: {},
648
+ disabled: false,
649
+ visible: true,
650
+ draggable: true,
651
+ resizable: true,
652
+ style: {}
653
+ }
654
+
655
+ this.localZones.push(newZone)
656
+ this.activeZone = newZone.id
657
+ this.$emit('zone-created', { ...newZone })
658
+ }
659
+
660
+ this.drawing = false
661
+ },
662
+
663
+ // ========== 热区交互事件 ==========
664
+ onZoneMouseEnter(zone) {
665
+ this.hoveredZone = zone.id
666
+ this.$emit('zone-hover', { ...zone })
667
+ },
668
+
669
+ onZoneMouseLeave(zone) {
670
+ if (this.hoveredZone === zone.id) {
671
+ this.hoveredZone = null
672
+ this.$emit('zone-leave', { ...zone })
673
+ }
674
+ },
675
+
676
+ onZoneMouseDown(e, zone) {
677
+ if (e.button !== 0) return
678
+ if (!this.mergedConfig.enableMove) return
679
+ if (zone.disabled || zone.draggable === false) return
680
+ this.startDrag(e, zone, null)
681
+ this.$emit('zone-click', { ...zone })
682
+ },
683
+
684
+ onZoneTouchStart(e, zone) {
685
+ if (!this.mergedConfig.enableMove) return
686
+ if (zone.disabled || zone.draggable === false) return
687
+ this.startDrag(e, zone, null)
688
+ this.$emit('zone-click', { ...zone })
689
+ },
690
+
691
+ onHandleMouseDown(e, zone, handle) {
692
+ if (e.button !== 0) return
693
+ if (!this.mergedConfig.enableResize) return
694
+ if (zone.disabled || zone.resizable === false) return
695
+ this.startDrag(e, zone, handle)
696
+ },
697
+
698
+ onHandleTouchStart(e, zone, handle) {
699
+ if (!this.mergedConfig.enableResize) return
700
+ if (zone.disabled || zone.resizable === false) return
701
+ this.startDrag(e, zone, handle)
702
+ },
703
+
704
+ startDrag(e, zone, handle) {
705
+ this.unbindDrawEvents()
706
+ this.drawing = false
707
+ if (this.rafId) {
708
+ cancelAnimationFrame(this.rafId)
709
+ this.rafId = null
710
+ }
711
+
712
+ const prevActiveZone = this.activeZone
713
+ this.activeZone = zone.id
714
+
715
+ if (prevActiveZone !== zone.id) {
716
+ this.$emit('zone-focus', { ...zone })
717
+ }
718
+
719
+ const pos = this.getMousePos(e)
720
+
721
+ this.dragData = {
722
+ zoneId: zone.id,
723
+ startMouse: pos,
724
+ startZone: { ...zone },
725
+ handle: handle
726
+ }
727
+
728
+ this.$emit('drag-start', {
729
+ zone: { ...zone },
730
+ handle: handle,
731
+ type: handle ? 'resize' : 'move'
732
+ })
733
+
734
+ this.bindDragEvents()
735
+ },
736
+
737
+ onDragMove(e) {
738
+ if (!this.dragData) {
739
+ this.unbindDragEvents()
740
+ return
741
+ }
742
+ e.preventDefault()
743
+
744
+ if (this.mergedConfig.useAnimationFrame) {
745
+ if (this.rafId) return
746
+ this.rafId = requestAnimationFrame(() => {
747
+ this.rafId = null
748
+ this.processDrag(e)
749
+ })
750
+ } else {
751
+ this.processDrag(e)
752
+ }
753
+ },
754
+
755
+ processDrag(e) {
756
+ if (!this.dragData) return
757
+
758
+ const pos = this.getMousePos(e)
759
+ const dx = (pos.x - this.dragData.startMouse.x) / this.wrapperWidth * 100
760
+ const dy = (pos.y - this.dragData.startMouse.y) / this.wrapperHeight * 100
761
+
762
+ const zoneIndex = this.localZones.findIndex(z => z.id === this.dragData.zoneId)
763
+ if (zoneIndex === -1) {
764
+ this.unbindDragEvents()
765
+ this.dragData = null
766
+ return
767
+ }
768
+
769
+ let zone = { ...this.localZones[zoneIndex] }
770
+ const start = this.dragData.startZone
771
+ const minSize = this.mergedConfig.minSize || 2
772
+
773
+ if (this.dragData.handle) {
774
+ const handle = this.dragData.handle
775
+ let { x, y, width, height } = start
776
+
777
+ if (handle.includes('e')) width += dx
778
+ if (handle.includes('w')) { x += dx; width -= dx }
779
+ if (handle.includes('s')) height += dy
780
+ if (handle.includes('n')) { y += dy; height -= dy }
781
+
782
+ if (width < minSize) {
783
+ if (handle.includes('w')) x = start.x + start.width - minSize
784
+ width = minSize
785
+ }
786
+ if (height < minSize) {
787
+ if (handle.includes('n')) y = start.y + start.height - minSize
788
+ height = minSize
789
+ }
790
+
791
+ if (x < 0) { width += x; x = 0 }
792
+ if (y < 0) { height += y; y = 0 }
793
+ if (x + width > 100) width = 100 - x
794
+ if (y + height > 100) height = 100 - y
795
+
796
+ zone = { ...zone, x, y, width, height }
797
+
798
+ this.$emit('resizing', {
799
+ zone: { ...zone },
800
+ handle: handle,
801
+ originalZone: { ...start }
802
+ })
803
+ } else {
804
+ let newX = start.x + dx
805
+ let newY = start.y + dy
806
+
807
+ newX = Math.max(0, Math.min(newX, 100 - zone.width))
808
+ newY = Math.max(0, Math.min(newY, 100 - zone.height))
809
+
810
+ zone.x = newX
811
+ zone.y = newY
812
+
813
+ this.$emit('moving', {
814
+ zone: { ...zone },
815
+ originalZone: { ...start }
816
+ })
817
+ }
818
+
819
+ this.$set(this.localZones, zoneIndex, zone)
820
+ this.$emit('zone-updated', { ...zone })
821
+ },
822
+
823
+ onDragEnd() {
824
+ if (this.rafId) {
825
+ cancelAnimationFrame(this.rafId)
826
+ this.rafId = null
827
+ }
828
+
829
+ if (this.dragData) {
830
+ const zone = this.localZones.find(z => z.id === this.dragData.zoneId)
831
+ if (zone) {
832
+ this.$emit('drag-end', {
833
+ zone: { ...zone },
834
+ type: this.dragData.handle ? 'resize' : 'move'
835
+ })
836
+ }
837
+ }
838
+
839
+ this.unbindDragEvents()
840
+ this.dragData = null
841
+ },
842
+
843
+ // ========== 公共方法 ==========
844
+ addZone(zoneData) {
845
+ if (this.localZones.length >= (this.mergedConfig.maxZones || 20)) {
846
+ this.$emit('max-reached', this.mergedConfig.maxZones)
847
+ return null
848
+ }
849
+
850
+ const newZone = this.normalizeZone({
851
+ id: zoneData.id || this.generateId(),
852
+ x: zoneData.x || 10,
853
+ y: zoneData.y || 10,
854
+ width: zoneData.width || 20,
855
+ height: zoneData.height || 20,
856
+ label: zoneData.label || `热区${this.localZones.length + 1}`,
857
+ title: zoneData.title || zoneData.label || `热区${this.localZones.length + 1}`,
858
+ data: zoneData.data || {},
859
+ disabled: zoneData.disabled || false,
860
+ visible: zoneData.visible !== false,
861
+ draggable: zoneData.draggable !== false,
862
+ resizable: zoneData.resizable !== false,
863
+ style: zoneData.style || {}
864
+ })
865
+
866
+ newZone.x = Math.max(0, Math.min(newZone.x, 100 - newZone.width))
867
+ newZone.y = Math.max(0, Math.min(newZone.y, 100 - newZone.height))
868
+
869
+ this.localZones.push(newZone)
870
+ this.activeZone = newZone.id
871
+ this.$emit('zone-created', { ...newZone })
872
+
873
+ return newZone
874
+ },
875
+
876
+ addZones(zonesData) {
877
+ if (!Array.isArray(zonesData)) return []
878
+ return zonesData.map(data => this.addZone(data)).filter(Boolean)
879
+ },
880
+
881
+ updateZone(zoneId, updates) {
882
+ const index = this.localZones.findIndex(z => z.id === zoneId)
883
+ if (index === -1) return false
884
+
885
+ const zone = this.localZones[index]
886
+ const updatedZone = { ...zone }
887
+
888
+ if (updates.x !== undefined) updatedZone.x = Number(updates.x)
889
+ if (updates.y !== undefined) updatedZone.y = Number(updates.y)
890
+ if (updates.width !== undefined) updatedZone.width = Number(updates.width)
891
+ if (updates.height !== undefined) updatedZone.height = Number(updates.height)
892
+ if (updates.label !== undefined) updatedZone.label = updates.label
893
+ if (updates.title !== undefined) updatedZone.title = updates.title
894
+ if (updates.data !== undefined) updatedZone.data = { ...zone.data, ...updates.data }
895
+ if (updates.disabled !== undefined) updatedZone.disabled = updates.disabled
896
+ if (updates.visible !== undefined) updatedZone.visible = updates.visible
897
+ if (updates.draggable !== undefined) updatedZone.draggable = updates.draggable
898
+ if (updates.resizable !== undefined) updatedZone.resizable = updates.resizable
899
+ if (updates.style !== undefined) updatedZone.style = { ...zone.style, ...updates.style }
900
+
901
+ updatedZone.x = Math.max(0, Math.min(updatedZone.x, 100 - updatedZone.width))
902
+ updatedZone.y = Math.max(0, Math.min(updatedZone.y, 100 - updatedZone.height))
903
+
904
+ this.$set(this.localZones, index, updatedZone)
905
+ this.$emit('zone-updated', { ...updatedZone })
906
+
907
+ return true
908
+ },
909
+
910
+ setActiveZone(zoneId) {
911
+ if (zoneId === null) {
912
+ this.activeZone = null
913
+ this.$emit('selection-cleared')
914
+ return
915
+ }
916
+ const zone = this.localZones.find(z => z.id === zoneId)
917
+ if (zone) {
918
+ this.activeZone = zoneId
919
+ this.$emit('zone-focus', { ...zone })
920
+ }
921
+ },
922
+
923
+ getActiveZone() {
924
+ if (!this.activeZone) return null
925
+ const zone = this.localZones.find(z => z.id === this.activeZone)
926
+ return zone ? { ...zone } : null
927
+ },
928
+
929
+ getZone(zoneId) {
930
+ const zone = this.localZones.find(z => z.id === zoneId)
931
+ return zone ? { ...zone } : null
932
+ },
933
+
934
+ getAllZones() {
935
+ return this.localZones.map(z => ({ ...z }))
936
+ },
937
+
938
+ removeZone(zoneId) {
939
+ const index = this.localZones.findIndex(z => z.id === zoneId)
940
+ if (index !== -1) {
941
+ const removed = this.localZones.splice(index, 1)[0]
942
+ if (this.activeZone === zoneId) this.activeZone = null
943
+ if (this.hoveredZone === zoneId) this.hoveredZone = null
944
+ this.$emit('zone-deleted', removed)
945
+ return true
946
+ }
947
+ return false
948
+ },
949
+
950
+ clearAll() {
951
+ this.unbindDrawEvents()
952
+ this.unbindDragEvents()
953
+ this.drawing = false
954
+ this.dragData = null
955
+ this.activeZone = null
956
+ this.hoveredZone = null
957
+ if (this.rafId) {
958
+ cancelAnimationFrame(this.rafId)
959
+ this.rafId = null
960
+ }
961
+ this.localZones = []
962
+ this.$emit('all-cleared')
963
+ },
964
+
965
+ exportZones() {
966
+ return this.localZones.map(z => this.cleanZoneData(z))
967
+ },
968
+
969
+ importZones(zonesData) {
970
+ if (!Array.isArray(zonesData)) return
971
+ this.clearAll()
972
+ this.addZones(zonesData)
973
+ this.$emit('zones-imported', this.localZones.length)
974
+ },
975
+
976
+ getSize() {
977
+ return { width: this.wrapperWidth, height: this.wrapperHeight }
978
+ },
979
+
980
+ disableZone(zoneId) { this.updateZone(zoneId, { disabled: true }) },
981
+ enableZone(zoneId) { this.updateZone(zoneId, { disabled: false }) },
982
+ hideZone(zoneId) { this.updateZone(zoneId, { visible: false }) },
983
+ showZone(zoneId) { this.updateZone(zoneId, { visible: true }) }
984
+ }
985
+ }
986
+ </script>
987
+
988
+ <style scoped>
989
+ .hotzone-wrapper {
990
+ position: relative;
991
+ display: inline-block;
992
+ width: 100%;
993
+ cursor: crosshair;
994
+ line-height: 0;
995
+ user-select: none;
996
+ }
997
+
998
+ .hotzone-wrapper img {
999
+ width: 100%;
1000
+ height: auto;
1001
+ display: block;
1002
+ pointer-events: none;
1003
+ user-select: none;
1004
+ }
1005
+
1006
+ .drawing-rect {
1007
+ position: absolute;
1008
+ top: 0;
1009
+ left: 0;
1010
+ pointer-events: none;
1011
+ z-index: 5;
1012
+ }
1013
+
1014
+ .zone-rect {
1015
+ position: absolute;
1016
+ top: 0;
1017
+ left: 0;
1018
+ box-sizing: border-box;
1019
+ z-index: 10;
1020
+ }
1021
+
1022
+ .zone-label {
1023
+ white-space: nowrap;
1024
+ overflow: hidden;
1025
+ text-overflow: ellipsis;
1026
+ text-align: center;
1027
+ pointer-events: none;
1028
+ z-index: 25;
1029
+ }
1030
+
1031
+ .handle {
1032
+ position: absolute;
1033
+ z-index: 20;
1034
+ box-sizing: border-box;
1035
+ }
1036
+
1037
+ /* 手柄位置 */
1038
+ .handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
1039
+ .handle.n { top: -5px; left: calc(50% - 5px); cursor: n-resize; }
1040
+ .handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
1041
+ .handle.e { top: calc(50% - 5px); right: -5px; cursor: e-resize; }
1042
+ .handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
1043
+ .handle.s { bottom: -5px; left: calc(50% - 5px); cursor: s-resize; }
1044
+ .handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
1045
+ .handle.w { top: calc(50% - 5px); left: -5px; cursor: w-resize; }
1046
+ </style>
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import ImageHotzone from './components/ImageHotzone.vue'
2
+
3
+ // 插件安装方法
4
+ ImageHotzone.install = function(Vue) {
5
+ Vue.component(ImageHotzone.name, ImageHotzone)
6
+ }
7
+
8
+ // 支持标签引入
9
+ if (typeof window !== 'undefined' && window.Vue) {
10
+ window.Vue.use(ImageHotzone)
11
+ }
12
+
13
+ export default ImageHotzone
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 生成唯一ID
3
+ * @returns {string}
4
+ */
5
+ export function generateId() {
6
+ return 'zone_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
7
+ }
8
+
9
+ /**
10
+ * 标准化热区数据
11
+ * @param {Object} zone
12
+ * @returns {Object}
13
+ */
14
+ export function normalizeZone(zone) {
15
+ return {
16
+ id: zone.id || generateId(),
17
+ x: Number(zone.x) || 0,
18
+ y: Number(zone.y) || 0,
19
+ width: Number(zone.width) || 0,
20
+ height: Number(zone.height) || 0,
21
+ label: zone.label || zone.title || '',
22
+ title: zone.title || zone.label || '',
23
+ data: zone.data || {},
24
+ style: zone.style || {},
25
+ disabled: zone.disabled || false,
26
+ visible: zone.visible !== false,
27
+ draggable: zone.draggable !== false,
28
+ resizable: zone.resizable !== false
29
+ }
30
+ }
31
+
32
+ /**
33
+ * 清理热区数据(移除内部属性)
34
+ * @param {Object} zone
35
+ * @returns {Object}
36
+ */
37
+ export function cleanZoneData(zone) {
38
+ return {
39
+ id: zone.id,
40
+ x: Number(zone.x.toFixed(2)),
41
+ y: Number(zone.y.toFixed(2)),
42
+ width: Number(zone.width.toFixed(2)),
43
+ height: Number(zone.height.toFixed(2)),
44
+ label: zone.label,
45
+ title: zone.title,
46
+ data: zone.data,
47
+ disabled: zone.disabled,
48
+ visible: zone.visible,
49
+ draggable: zone.draggable,
50
+ resizable: zone.resizable,
51
+ style: zone.style
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 像素转百分比
57
+ * @param {number} px
58
+ * @param {number} containerSize
59
+ * @param {boolean} snapToGrid
60
+ * @param {number} gridSize
61
+ * @returns {number}
62
+ */
63
+ export function pxToPercent(px, containerSize, snapToGrid = false, gridSize = 5) {
64
+ if (containerSize === 0) return 0
65
+ let percent = (px / containerSize) * 100
66
+ if (snapToGrid) {
67
+ percent = Math.round(percent / gridSize) * gridSize
68
+ }
69
+ return percent
70
+ }
71
+
72
+ /**
73
+ * 获取鼠标/触摸位置
74
+ * @param {Event} e
75
+ * @param {HTMLElement} container
76
+ * @returns {{x: number, y: number}}
77
+ */
78
+ export function getMousePos(e, container) {
79
+ if (!container) return { x: 0, y: 0 }
80
+ const rect = container.getBoundingClientRect()
81
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX
82
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY
83
+ return {
84
+ x: clientX - rect.left,
85
+ y: clientY - rect.top
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 限制数值范围
91
+ * @param {number} value
92
+ * @param {number} min
93
+ * @param {number} max
94
+ * @returns {number}
95
+ */
96
+ export function clamp(value, min, max) {
97
+ return Math.max(min, Math.min(value, max))
98
+ }
99
+
100
+ /**
101
+ * 合并默认配置
102
+ * @param {Object} defaultConfig
103
+ * @param {Object} userConfig
104
+ * @returns {Object}
105
+ */
106
+ export function mergeConfig(defaultConfig, userConfig) {
107
+ return { ...defaultConfig, ...userConfig }
108
+ }