@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.
- package/dist/vue2-image-hotzone.js +1 -0
- package/package.json +58 -0
- package/src/components/ImageHotzone.vue +1046 -0
- package/src/index.js +13 -0
- package/src/utils/index.js +108 -0
|
@@ -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
|
+
}
|