@kirkw/vue3-image-hotzone

A Vue 3 component for creating image hotzones with drag, resize and move support
597
Install
npm install @kirkw/vue3-image-hotzone
Latest Version:1.0.3
Vue:3
License:MIT
Last Updated:Sep 8, 2026
Links: GitHub  ·  npm
Maintainer: manongguai

Vue3 Image Hotzone

一个功能强大的 Vue 3 图片热区编辑组件,支持拖拽绘制、移动、缩放热区,提供丰富的配置选项和事件系统。

基于 Composition API 开发,使用 <script setup> 语法,完美支持 Vue 3。

特性

  • 🖱️ 拖拽绘制 - 在图片上按住鼠标拖拽即可创建热区,支持任意方向滑动
  • 📐 自由缩放 - 8个手柄(四角+四边中点)支持全方位调整热区大小
  • 🔄 拖拽移动 - 点击热区内部可拖拽移动位置
  • 📱 响应式适配 - 基于百分比坐标存储,适配所有分辨率和设备
  • 🎨 主题定制 - 完整的配色系统,支持预设主题和自定义样式
  • ⚙️ 灵活配置 - 丰富的配置选项,满足各种业务需求
  • 🎯 独立热区控制 - 支持禁用、隐藏、锁定单个热区
  • 📦 数据导入导出 - 支持 JSON 格式的热区数据导入导出
  • ⌨️ 键盘快捷键 - 支持 Delete/Backspace 删除,Escape 取消选择
  • ⚡ 高性能 - 使用 requestAnimationFrame 优化拖拽性能,防抖更新
  • 🔌 完善的事件系统 - 覆盖所有交互事件,方便外部监听和处理
  • 🏷️ 标签居中显示 - 热区名称居中显示,支持自定义样式
  • 🚀 Composition API - 使用 Vue 3 Composition API 和 <script setup> 语法

安装

npm install vue3-image-hotzone
快速开始
全局注册
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import Vue3ImageHotzone from 'vue3-image-hotzone'

const app = createApp(App)
app.use(Vue3ImageHotzone)
app.mount('#app')
局部注册
vue
<template>
  <ImageHotzone
    ref="hotzoneRef"
    :src="imageUrl"
    v-model:zones="zones"
    @zone-created="onZoneCreated"
  />
</template>

<script setup>
import { ref } from 'vue'
import ImageHotzone from 'vue3-image-hotzone'

const hotzoneRef = ref(null)
const imageUrl = ref('https://picsum.photos/id/1015/800/500')
const zones = ref([])

// 调用组件方法
const addHotzone = () => {
  hotzoneRef.value?.addZone({
    x: 40, y: 40, width: 20, height: 20,
    label: '新热区'
  })
}

const onZoneCreated = (zone) => {
  console.log('热区创建:', zone)
}
</script>
Vue2 与 Vue3 使用差异
特性	Vue2	Vue3
安装包名	vue2-image-hotzone	vue3-image-hotzone
双向绑定	:zones.sync="zones"	v-model:zones="zones"
模板中组件名	<image-hotzone>	<ImageHotzone> 或 <image-hotzone>
获取组件实例	this.$refs.hotzone	hotzoneRef.value
全局注册	Vue.component()	app.use()
引入方式	import ImageHotzone from 'vue2-image-hotzone'	import ImageHotzone from 'vue3-image-hotzone'
Props 参数
src
类型: String

必填: true

说明: 图片地址

zones / v-model:zones
类型: Array

默认值: []

说明: 热区数据数组,支持 v-model:zones 双向绑定

热区数据结构:

javascript
{
  id: 'unique_id',           // 唯一标识(可选,不传自动生成)
  x: 20,                     // X坐标(百分比 0-100)
  y: 15,                     // Y坐标(百分比 0-100)
  width: 25,                 // 宽度(百分比 0-100)
  height: 30,                // 高度(百分比 0-100)
  label: '热区名称',          // 标签文本(显示在热区中间)
  title: '热区标题',          // 标题(label的备用字段)
  data: {},                  // 自定义数据对象
  disabled: false,           // 是否禁用(禁用后不可交互)
  visible: true,             // 是否可见
  draggable: true,           // 是否可拖拽移动
  resizable: true,           // 是否可调整大小
  style: {}                  // 自定义样式对象
}
theme
类型: Object

默认值: {}

说明: 主题配色配置

javascript
{
  // 绘制中虚线框
  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',
  handleBorderRadius: '3px',
  handleShadow: '0 1px 4px rgba(0,0,0,0.15)',
  
  // 标签样式
  labelColor: '#333333',
  labelFontSize: '12px',
  labelFontWeight: '600',
  labelBackgroundColor: 'transparent',
  labelPadding: '2px 4px',
  labelMaxWidth: '90%',
  
  // 过渡动画
  transitionDuration: '0s',
  transitionTiming: 'ease'
}
config
类型: Object

默认值: {}

说明: 功能配置

javascript
{
  minSize: 2,              // 最小热区尺寸(百分比)
  maxZones: 20,            // 最大热区数量
  showLabel: true,         // 是否显示标签
  enableResize: true,      // 是否允许调整大小
  enableMove: true,        // 是否允许移动
  enableDraw: true,        // 是否允许绘制新热区
  enableDelete: true,      // 是否允许键盘删除
  showHandles: true,       // 是否显示拖拽手柄
  allowOverlap: true,      // 是否允许热区重叠
  snapToGrid: false,       // 是否吸附到网格
  gridSize: 5,             // 网格大小(百分比)
  useAnimationFrame: true, // 使用RAF优化性能
  debounceUpdate: true     // 防抖更新
}
事件列表
事件名	参数	说明
zone-created	zone	热区创建
zone-updated	zone	热区更新
zone-deleted	zone	热区删除
zone-click	zone	点击热区
zone-focus	zone	热区获得焦点
zone-hover	zone	鼠标进入热区
zone-leave	zone	鼠标离开热区
draw-start	{x, y}	开始绘制
drawing	{x, y, width, height}	绘制中
draw-end	{x, y, width, height, valid}	绘制结束
drag-start	{zone, handle, type}	开始拖拽
moving	{zone, originalZone}	移动中
resizing	{zone, handle, originalZone}	调整大小中
drag-end	{zone, type}	拖拽结束
max-reached	max	达到最大数量
selection-cleared	-	清除选择
all-cleared	-	清除所有热区
zones-imported	count	导入完成
resize	{width, height}	容器尺寸变化
公共方法
通过 ref 调用组件方法:

vue
<script setup>
import { ref } from 'vue'

const hotzoneRef = ref(null)

// 所有方法都通过 hotzoneRef.value 调用
const doSomething = () => {
  hotzoneRef.value?.addZone({...})
}
</script>
添加热区
javascript
// 添加单个热区
hotzoneRef.value.addZone({
  x: 20, y: 20, width: 30, height: 30,
  label: '新热区',
  data: { type: 'custom' }
})

// 批量添加
hotzoneRef.value.addZones([
  { x: 10, y: 10, width: 20, height: 20, label: '热区1' },
  { x: 40, y: 40, width: 25, height: 25, label: '热区2' }
])
更新热区
javascript
hotzoneRef.value.updateZone('zone_id', {
  label: '新名称',
  x: 30, y: 30,
  data: { updated: true }
})
删除热区
javascript
// 删除指定热区
hotzoneRef.value.removeZone('zone_id')

// 清除所有
hotzoneRef.value.clearAll()
获取热区
javascript
// 获取激活的热区
const zone = hotzoneRef.value.getActiveZone()

// 获取指定热区
const zone = hotzoneRef.value.getZone('zone_id')

// 获取所有热区
const zones = hotzoneRef.value.getAllZones()
设置激活热区
javascript
hotzoneRef.value.setActiveZone('zone_id')
hotzoneRef.value.setActiveZone(null)  // 取消选择
控制热区状态
javascript
// 禁用/启用
hotzoneRef.value.disableZone('zone_id')
hotzoneRef.value.enableZone('zone_id')

// 隐藏/显示
hotzoneRef.value.hideZone('zone_id')
hotzoneRef.value.showZone('zone_id')
数据导入导出
javascript
// 导出
const data = hotzoneRef.value.exportZones()
console.log(JSON.stringify(data, null, 2))

// 导入
hotzoneRef.value.importZones(data)
获取容器尺寸
javascript
const size = hotzoneRef.value.getSize()
console.log(size.width, size.height)
完整示例(Vue3 Composition API)
vue
<template>
  <div class="demo">
    <h2>图片热区编辑器</h2>
    
    <div class="toolbar">
      <button @click="addHotzone">添加热区</button>
      <button @click="addMultipleHotzones">批量添加</button>
      <button @click="exportData">导出数据</button>
      <button @click="importData">导入数据</button>
      <button @click="clearAll">清除全部</button>
    </div>
    
    <!-- 热区组件 -->
    <ImageHotzone
      ref="hotzoneRef"
      :src="imageUrl"
      v-model:zones="zones"
      :theme="theme"
      :config="config"
      @zone-created="onZoneCreated"
      @zone-updated="onZoneUpdated"
      @zone-deleted="onZoneDeleted"
      @zone-click="onZoneClick"
      @max-reached="onMaxReached"
    />
    
    <!-- 热区列表 -->
    <div class="zone-list" v-if="zones.length > 0">
      <h3>热区列表 ({{ zones.length }})</h3>
      <div v-for="zone in zones" :key="zone.id" class="zone-item">
        <span>{{ zone.label }}</span>
        <span>({{ zone.x.toFixed(1) }}%, {{ zone.y.toFixed(1) }}%)</span>
        <button @click="hotzoneRef?.setActiveZone(zone.id)">选择</button>
        <button @click="hotzoneRef?.removeZone(zone.id)">删除</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ImageHotzone from 'vue3-image-hotzone'

const hotzoneRef = ref(null)
const imageUrl = ref('https://picsum.photos/id/1015/800/500')
const zones = ref([])
const theme = ref({})
const config = ref({
  minSize: 2,
  maxZones: 20,
  showLabel: true,
  enableResize: true,
  enableMove: true,
  enableDraw: true,
  showHandles: true
})

const addHotzone = () => {
  hotzoneRef.value?.addZone({
    x: 40, y: 40, width: 20, height: 20,
    label: `热区-${Date.now() % 1000}`,
    data: { created: new Date().toISOString() }
  })
}

const addMultipleHotzones = () => {
  hotzoneRef.value?.addZones([
    { x: 10, y: 10, width: 20, height: 20, label: '区域A' },
    { x: 40, y: 40, width: 20, height: 20, label: '区域B' },
    { x: 70, y: 60, width: 15, height: 20, label: '区域C' }
  ])
}

const exportData = () => {
  const data = hotzoneRef.value?.exportZones()
  if (data) {
    const json = JSON.stringify(data, null, 2)
    console.log(json)
    navigator.clipboard?.writeText(json)
  }
}

const importData = () => {
  const json = prompt('请粘贴热区数据(JSON格式):')
  if (json) {
    try {
      const data = JSON.parse(json)
      hotzoneRef.value?.importZones(data)
    } catch (e) {
      alert('JSON解析失败')
    }
  }
}

const clearAll = () => {
  if (confirm('确定清除所有热区?')) {
    hotzoneRef.value?.clearAll()
  }
}

const onZoneCreated = (zone) => console.log('创建:', zone)
const onZoneUpdated = (zone) => console.log('更新:', zone)
const onZoneDeleted = (zone) => console.log('删除:', zone)
const onZoneClick = (zone) => console.log('点击:', zone)
const onMaxReached = (max) => alert(`最多只能添加 ${max} 个热区`)
</script>

<style>
body {
  margin: 0;
  padding: 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  background: #f5f7fa;
}

.demo {
  max-width: 900px;
  margin: 0 auto;
}

.toolbar {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.toolbar button {
  padding: 8px 16px;
  background: #4f6ef7;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}

.toolbar button:hover {
  background: #3b5de7;
}

.zone-list {
  margin-top: 20px;
  padding: 16px;
  background: white;
  border-radius: 8px;
}

.zone-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  background: #f7fafc;
  border-radius: 6px;
  margin-bottom: 8px;
  gap: 10px;
}

.zone-item button {
  padding: 4px 10px;
  background: #edf2f7;
  color: #4a5568;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.zone-item button:hover {
  background: #e2e8f0;
}
</style>
预设主题
javascript
// 默认蓝色
const defaultTheme = {}

// 暗色主题
const darkTheme = {
  zoneBorderColor: '#718096',
  zoneFillColor: 'rgba(113, 128, 150, 0.2)',
  zoneActiveBorderColor: '#fbd38d',
  zoneActiveFillColor: 'rgba(251, 211, 141, 0.15)',
  labelColor: '#e2e8f0'
}

// 绿色主题
const greenTheme = {
  zoneBorderColor: '#38a169',
  zoneFillColor: 'rgba(56, 161, 105, 0.2)',
  zoneActiveBorderColor: '#f56565',
  zoneActiveFillColor: 'rgba(245, 101, 101, 0.15)'
}

// 海洋主题(圆形手柄)
const oceanTheme = {
  zoneBorderColor: '#0077b6',
  zoneFillColor: 'rgba(0, 119, 182, 0.2)',
  zoneActiveBorderColor: '#ff6b6b',
  handleBorderRadius: '50%',
  handleSize: 12
}
键盘快捷键
快捷键	说明
Delete / Backspace	删除当前激活的热区
Escape	取消当前选择 / 取消绘制
TypeScript 支持
如需 TypeScript 支持,可以添加类型声明文件:

typescript
// vue3-image-hotzone.d.ts
declare module 'vue3-image-hotzone' {
  import { DefineComponent } from 'vue'
  
  interface Zone {
    id?: string
    x: number
    y: number
    width: number
    height: number
    label?: string
    title?: string
    data?: Record<string, any>
    disabled?: boolean
    visible?: boolean
    draggable?: boolean
    resizable?: boolean
    style?: Record<string, any>
  }
  
  interface Theme {
    drawingLineColor?: string
    zoneBorderColor?: string
    zoneFillColor?: string
    zoneActiveBorderColor?: string
    zoneActiveFillColor?: string
    handleSize?: number
    [key: string]: any
  }
  
  interface Config {
    minSize?: number
    maxZones?: number
    showLabel?: boolean
    enableResize?: boolean
    enableMove?: boolean
    enableDraw?: boolean
    [key: string]: any
  }
  
  const ImageHotzone: DefineComponent<{
    src: string
    zones?: Zone[]
    theme?: Theme
    config?: Config
    onZoneCreated?: (zone: Zone) => void
    onZoneUpdated?: (zone: Zone) => void
    onZoneDeleted?: (zone: Zone) => void
    [key: string]: any
  }>
  
  export default ImageHotzone
}

浏览器兼容性 Chrome 60+

Firefox 60+

Safari 12+

Edge 79+

iOS Safari 12+

Android Chrome 60+

License MIT

Related Packages

@kirkw/vue2-image-hotzone

A Vue 2 component for creating image hotzones with drag, resize and move support

610 0
vue-load-image

A vue component for showing loader during image loading

129,890 68
vue-inner-image-zoom

A Vue.js component for magnifying an image within its parent container 🔎

135,953 36

Version History

Version Released Vue License
1.0.3 ^3.2.0 MIT
1.0.2 ^3.2.0 MIT
1.0.1 ^3.2.0 MIT
1.0.0 ^3.2.0 MIT