Vue3图片缩放拖拽组件全攻略

Panzoom 图片缩放拖拽组件使用文档

基于 Vue 3 + @panzoom/panzoom 的图片缩放拖拽组件,支持滚轮缩放和鼠标拖拽,大图自动缩小完整显示,小图 1:1 居中。


目录

  1. 依赖安装
  2. [Props 说明](#Props 说明)
  3. 完整代码
  4. 使用步骤
  5. 功能说明

1. 依赖安装

bash 复制代码
npm install @panzoom/panzoom

2. Props 说明

Prop 类型 必填 默认值 说明
imageSrc String "" 图片地址(支持远程 URL 或 URL.createObjectURL 的 Blob URL)

3. 完整代码

ProcessDialog.vue

vue 复制代码
<template>
  <!-- 固定高度容器,作为 panzoom 的视口 -->
  <div ref="containerRef" class="panzoom-viewport">
    <!-- panzoom 控制的目标元素:transform 在这里生效 -->
    <div ref="panzoomRef" class="panzoom-layer">
      <img
        v-if="imageSrc"
        :src="imageSrc"
        alt="图片"
        class="draggable-image"
        @load="onImageLoad"
      />
      <el-empty v-else description="暂无图片" />
    </div>
  </div>
</template>

<script setup>
import { ref, watch, nextTick, onBeforeUnmount } from "vue"
import Panzoom from "@panzoom/panzoom"

/**
 * Props
 * - imageSrc: 图片地址(ObjectURL 或远程 URL)
 */
const props = defineProps({
  imageSrc: { type: String, default: "" }
})

// ==================== DOM 引用 ====================
const containerRef = ref(null) // 固定高度视口容器
const panzoomRef = ref(null)   // panzoom 控制的内容层
let panzoomInstance = null

// ==================== Panzoom 实例创建 ====================

/** 创建并配置 panzoom 实例
 *  @param {HTMLElement} element - 被控制的内容层 DOM
 *  @returns {Panzoom} panzoom 实例
 */
function createPanzoom(element) {
  return Panzoom(element, {
    maxScale: 5,          // 最大放大 5 倍
    minScale: 0.05,       // 最小缩小到 5%
    step: 0.15,           // 滚轮每格缩放步长
    contain: false,        // 不限制内容边界,允许自由拖拽
    /* 鼠标/触摸事件处理:避免拖拽时误选文本 */
    handleStartEvent: (e) => {
      // 多指触摸 → 允许缩放
      if (e.touches && e.touches.length > 1) return true
      // 非左键 / 非 touch / 非滚轮 → 不拦截
      if (e.button !== 0 && e.type !== "touchstart" && e.type !== "wheel") return true
      e.preventDefault()
      e.stopPropagation()
    }
  })
}

// ==================== 自适应缩放 ====================

/**
 * 内容自适应容器:大图缩小、小图居中,保持完整显示
 * 计算公式:
 *   scale = min(容器宽 / 内容宽, 容器高 / 内容高, 1)
 *   panX  = (容器宽 - 内容宽 × scale) / 2
 *   panY  = (容器高 - 内容高 × scale) / 2
 *
 * @param {Panzoom} instance - panzoom 实例
 * @param {HTMLElement} layerEl - 内容层 DOM
 */
function autoFit(instance, layerEl) {
  if (!instance || !layerEl || !containerRef.value) return
  const pw = containerRef.value.clientWidth
  const ph = containerRef.value.clientHeight
  const cw = layerEl.scrollWidth
  const ch = layerEl.scrollHeight

  if (cw <= 0 || ch <= 0) return

  // 刚好完整显示的比例,最大不超过 1(小图不放大)
  const scale = Math.min(pw / cw, ph / ch, 1)

  // 先归零 → 设置缩放 → 居中(避免位移累积误差)
  instance.pan({ x: 0, y: 0 })
  instance.setScale(scale)
  instance.pan({
    x: (pw - cw * scale) / 2,
    y: (ph - ch * scale) / 2
  })
}

// ==================== 图片加载回调 ====================

/** 图片加载完成后初始化 panzoom + 自适应居中 */
function onImageLoad() {
  if (!panzoomRef.value || !containerRef.value) return

  // 销毁旧实例,避免重复创建
  if (panzoomInstance) {
    panzoomInstance.destroy()
    panzoomInstance = null
  }

  panzoomInstance = createPanzoom(panzoomRef.value)

  // 滚轮事件绑定到外层容器,确保鼠标在容器内任意位置都能缩放
  containerRef.value.addEventListener("wheel", panzoomInstance.zoomWithWheel, { passive: false })

  // 等浏览器完成布局后自适应
  nextTick(() => {
    requestAnimationFrame(() => autoFit(panzoomInstance, panzoomRef.value))
  })
}

// ==================== 监听 imageSrc 变化 ====================

watch(
  () => props.imageSrc,
  (src) => {
    if (src) {
      nextTick(() => {
        // 如果图片已缓存(loaded),直接初始化
        if (panzoomRef.value?.querySelector("img")?.complete) {
          onImageLoad()
        }
      })
    }
  }
)

// ==================== 组件销毁时清理 ====================
onBeforeUnmount(() => {
  if (containerRef.value && panzoomInstance) {
    containerRef.value.removeEventListener("wheel", panzoomInstance.zoomWithWheel)
  }
  if (panzoomInstance) {
    panzoomInstance.destroy()
    panzoomInstance = null
  }
})
</script>

<style scoped lang="scss">
/* ======== 视口容器:固定高度,裁剪溢出 ======== */
.panzoom-viewport {
  width: 100%;
  height: 65vh;
  overflow: hidden;
  position: relative;
  background: #f5f6f8;
  border: 1px solid #e8eaed;
  border-radius: 4px;
  user-select: none; /* 拖拽时不选中文本 */
}

/* ======== panzoom 控制的内容层:transform 在这里生效 ======== */
.panzoom-layer {
  display: inline-block;
  transform-origin: 0 0; /* 以左上角为缩放原点,配合 autoFit 的 pan 居中 */
}

/* ======== 图片样式 ======== */
.draggable-image {
  display: block;
  max-width: none;  /* 允许图片超出容器,由 panzoom 控制缩放 */
}
</style>

4. 使用步骤

第 1 步:安装依赖

bash 复制代码
npm install @panzoom/panzoom

第 2 步:在父组件中使用

vue 复制代码
<template>
  <div style="padding: 20px;">
    <ProcessDialog :image-src="imageUrl" />
  </div>
</template>

<script setup>
import { ref } from "vue"
import ProcessDialog from "@/views/mytodolist/components/dialogs/ProcessDialog.vue"

// 可以是远程 URL
const imageUrl = ref("https://example.com/flow-diagram.png")

// 或者通过后端 Blob 接口获取
import { diagram } from "@/api/mytodolist/index"

async function loadImage(processInstanceId) {
  const res = await diagram({ processInstanceId })
  imageUrl.value = URL.createObjectURL(res) // Blob → ObjectURL
}
</script>

5. 功能说明

5.1 滚轮缩放

  • 鼠标悬停在图片上,滚动滚轮即可缩放
  • 以鼠标位置为中心点缩放,缩放步长 15%
  • 最大放大 5 倍,最小缩小到 5%

5.2 鼠标拖拽

  • 按住鼠标左键拖动图片
  • 可拖出容器边界(contain: false
  • 支持触摸屏手势拖拽

5.3 自适应显示

图片大小 效果
图片 > 容器 自动缩小到完整显示,居中
图片 < 容器 保持 1:1 原始大小,居中不放大

5.4 生命周期

  • 图片加载完成后自动初始化 panzoom + 自适应居中
  • imageSrc 变化时重新初始化
  • 组件销毁时自动清理 panzoom 实例和事件监听

5.5 autoFit 缩放计算公式

复制代码
scale = min(容器宽 / 图片宽, 容器高 / 图片高, 1)
          ↑                                             ↑
     取宽高比中较小的一个                           不超过 1(不大)

panX = (容器宽 - 图片宽 × scale) / 2
panY = (容器高 - 图片高 × scale) / 2
相关推荐
IT_陈寒14 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
掘金酱14 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家14 小时前
Windows 上同时安装多个 node 版本
前端
晓说前端15 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
大龄秃头程序员15 小时前
iOS 客户端视角扫盲:WKWebView 里 window、messageHandlers 与 Native 回调到底怎么工作?
javascript
爱勇宝15 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
GuWenyue15 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
妙码生花15 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十五):前端远程下拉输入组件
前端·javascript·vue.js
明月_清风15 小时前
全栈工程师必会技术栈:从入门到架构的完整成长地图 🗺️
前端·后端·全栈