vue3封装图片预览组件-支持滚轮放大缩小

感谢大家的点赞和转发。试用期指导,项目开发,简历优化,毕业设计/论文,欢迎添加本人微      信。

 新人作者,欢迎关注和收藏👏🏻👏🏻

1.效果图

2.代码

<template>
  <div
    ref="container"
    class="image-viewer-container"
    :style="{ width: containerWidthStyle, height: containerHeightStyle }"
  >
    <div
      ref="imageWrapper"
      class="image-wrapper"
      :style="{
        transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
        cursor: isDragging
          ? 'grabbing'
          : scale > 1 ||
              (scale === props.initialScale &&
                (translateX !== 0 || translateY !== 0))
            ? 'grab'
            : 'default'
      }"
      @mousedown="startDrag"
      @mousemove="handleDrag"
      @mouseup="endDrag"
      @mouseleave="endDrag"
      @wheel.prevent="handleWheel"
    >
      <img
        ref="image"
        :src="src"
        :alt="alt"
        class="image"
        @load="handleImageLoad"
      />
      <!-- <div class="overlay"></div> -->
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from "vue";

const props = defineProps({
  src: {
    type: String,
    required: true
  },
  alt: {
    type: String,
    default: ""
  },
  initialScale: {
    type: Number,
    default: 1
  },
  maxScale: {
    type: Number,
    default: 30
  },
  minScale: {
    type: Number,
    default: 1
  },
  zoomStep: {
    type: Number,
    default: 0.1
  },
  containerWidth: {
    type: [Number, String],
    default: "100%" // 默认宽度,可以是数字或带单位的字符串
  },
  containerHeight: {
    type: [Number, String],
    default: "100%" //  默认高度,可以是数字或带单位的字符串
  },
  autoSize: {
    type: Boolean,
    default: false // 是否根据图片高度自适应
  }
});

const container = ref(null);
const imageWrapper = ref(null);
const image = ref(null);

const scale = ref(props.initialScale);
const translateX = ref(0);
const translateY = ref(0);
const isDragging = ref(false);
const startDragX = ref(0);
const startDragY = ref(0);
const rawContainerWidth = ref(props.containerWidth);
const rawContainerHeight = ref(props.containerHeight);
const containerNumWidth = ref(
  typeof props.containerWidth === "number" ? props.containerWidth : 300
);
const containerNumHeight = ref(
  typeof props.containerHeight === "number" ? props.containerHeight : 300
);
const imageWidth = ref(0);
const imageHeight = ref(0);

/**
 * 计算容器宽度样式
 */
const containerWidthStyle = computed(() => {
  if (typeof rawContainerWidth.value === "number") {
    return `${rawContainerWidth.value}px`;
  }
  return rawContainerWidth.value;
});

/**
 * 计算容器高度样式
 */
const containerHeightStyle = computed(() => {
  if (typeof rawContainerHeight.value === "number") {
    return `${rawContainerHeight.value}px`;
  }
  return rawContainerHeight.value;
});

const handleImageLoad = () => {
  imageWidth.value = image.value.naturalWidth;
  imageHeight.value = image.value.naturalHeight;
  if (props.autoSize) {
    rawContainerWidth.value = imageWidth.value;
    rawContainerHeight.value = imageHeight.value;
    containerNumWidth.value = imageWidth.value;
    containerNumHeight.value = imageHeight.value;
  }
  resetPosition();
};

const updateContainerSize = () => {
  if (!props.autoSize) {
    rawContainerWidth.value = props.containerWidth;
    rawContainerHeight.value = props.containerHeight;
    if (typeof props.containerWidth === "number") {
      containerNumWidth.value = props.containerWidth;
    }
    if (typeof props.containerHeight === "number") {
      containerNumHeight.value = props.containerHeight;
    }

    // 如果容器有尺寸,需要更新实际数值宽高,用于计算拖拽边界
    if (container.value) {
      const rect = container.value.getBoundingClientRect();
      if (typeof props.containerWidth !== "number") {
        containerNumWidth.value = rect.width;
      }
      if (typeof props.containerHeight !== "number") {
        containerNumHeight.value = rect.height;
      }
    }
  }
};

const resetPosition = () => {
  scale.value = props.initialScale;
  translateX.value = 0;
  translateY.value = 0;
};

const startDrag = e => {
  // if (
  //   scale.value > 1 ||
  //   (scale.value === props.initialScale &&
  //     (translateX.value !== 0 || translateY.value !== 0))
  // ) {
  isDragging.value = true;
  startDragX.value = e.clientX;
  startDragY.value = e.clientY;
  e.preventDefault();
  e.stopPropagation();
  document.addEventListener("mousemove", handleDrag);
  document.addEventListener("mouseup", endDrag);
  // }
};

const handleDrag = e => {
  if (isDragging.value) {
    e.preventDefault();
    e.stopPropagation();
    const dx = e.clientX - startDragX.value;
    const dy = e.clientY - startDragY.value;
    const newX = translateX.value + dx;
    const newY = translateY.value + dy;

    const maxX = (scale.value * imageWidth.value - containerNumWidth.value) / 2;
    const maxY =
      (scale.value * imageHeight.value - containerNumHeight.value) / 2;

    translateX.value = Math.max(-maxX, Math.min(maxX, newX));
    translateY.value = Math.max(-maxY, Math.min(maxY, newY));

    startDragX.value = e.clientX;
    startDragY.value = e.clientY;
  }
};

const endDrag = e => {
  if (isDragging.value) {
    if (e) {
      e.preventDefault();
      e.stopPropagation();
    }
    isDragging.value = false;
    document.removeEventListener("mousemove", handleDrag);
    document.removeEventListener("mouseup", endDrag);
  }
};

const handleWheel = e => {
  e.preventDefault();
  e.stopPropagation();

  if (e.deltaY < 0) {
    scale.value = Math.min(props.maxScale, scale.value + props.zoomStep);
  } else {
    scale.value = Math.max(props.minScale, scale.value - props.zoomStep);
  }

  // 不调用 resetPosition 函数,暂时隐藏该功能
  if (
    scale.value === props.initialScale &&
    translateX.value === 0 &&
    translateY.value === 0
  ) {
    resetPosition();
  }
  // 缩放时不改变图片位置,保持上一次的 translateX 和 translateY 值
};

onMounted(() => {
  updateContainerSize();
  window.addEventListener("resize", updateContainerSize);
});

onBeforeUnmount(() => {
  window.removeEventListener("resize", updateContainerSize);
  endDrag();
});
</script>

<style scoped>
.image-viewer-container {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  overflow: hidden;
  touch-action: pan-y;
  user-select: none;
}

.image-wrapper {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  width: 100%;
  height: auto;
  overflow: visible;
  cursor: pointer;
  transform-origin: center top;
  will-change: transform;
}

.image {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  user-select: none;
  object-fit: contain;
  -webkit-user-drag: none;
}
</style>

3.文档

图片查看器组件文档

一、组件概述

该图片查看器组件是一个基于 Vue 开发的图片展示工具,允许用户对图片进行缩放和拖拽操作。组件提供了灵活的配置选项,如初始缩放比例、最大缩放比例、缩放步长等,同时支持根据图片高度自适应容器大小。

二、组件结构

外层容器image-viewer-container 类的 div 元素,作为整个组件的容器,设置了固定的宽度和高度。

图片包装器image-wrapper 类的 div 元素,用于包裹图片,通过 CSS 变换属性 transform 实现缩放和位移。

图片元素image 类的 img 元素,展示实际的图片内容。

遮罩层overlay 类的 div 元素,覆盖在图片上方,提供视觉上的效果。

三、组件属性

属性名类型默认值描述
srcString图片的源地址,必填项。
altString""图片的替代文本。
initialScaleNumber1图片的初始缩放比例。
maxScaleNumber3图片的最大缩放比例。
minScaleNumber1图片的最小缩放比例。
zoomStepNumber0.1每次缩放的步长。
containerWidthNumber300容器的宽度,单位为像素。
containerHeightNumber300容器的高度,单位为像素。
autoSizeBooleanfalse是否根据图片高度自适应容器大小,若为 true,则容器大小会根据图片高度进行调整。

四、组件方法

handleImageLoad:在图片加载完成时调用,用于获取图片的自然宽度和高度,并根据 autoSize 属性调整容器大小。

updateContainerSize:根据 autoSize 属性更新容器的宽度和高度。

resetPosition:将图片的缩放比例和位移值重置为初始状态。

startDrag:在鼠标按下时调用,开始拖拽操作,记录鼠标按下时的位置,并添加 mousemove 和 mouseup 事件监听器。

handleDrag:在鼠标移动时调用,根据鼠标移动的距离更新图片的位移值,并确保位移值在可接受的范围内。

endDrag:在鼠标抬起或离开时调用,结束拖拽操作,移除 mousemove 和 mouseup 事件监听器。

handleWheel:在鼠标滚轮滚动时调用,根据滚轮的方向调整图片的缩放比例,并确保缩放比例在可接受的范围内。

五、事件处理

@mousedown:在鼠标按下时触发 startDrag 方法。

@mousemove:在鼠标移动时触发 handleDrag 方法。

@mouseup:在鼠标抬起时触发 endDrag 方法。

@mouseleave:在鼠标离开时触发 endDrag 方法。

@wheel.prevent:在鼠标滚轮滚动时触发 handleWheel 方法,并阻止默认的滚轮事件。

六、样式说明

image-viewer-container:设置为相对定位,溢出隐藏,禁止触摸操作和用户选择。

image-wrapper:设置为相对定位,变换原点为中心,设置 will-change: transform 以优化性能。

image:设置为块级元素,宽度为 100%,高度自适应,禁止用户选择和拖拽。

overlay:设置为绝对定位,覆盖整个图片区域,背景颜色为半透明黑色,指针事件为 none。

七、生命周期钩子函数

onMounted:在组件挂载完成时调用,更新容器大小,并添加 resize 事件监听器。

onBeforeUnmount:在组件卸载前调用,移除 resize 事件监听器,并结束拖拽操作。

 感谢大家的点赞和转发。试用期指导,项目开发,简历优化,毕业设计/论文,欢迎添加本人微       信。

 新人作者,欢迎关注和收藏👏🏻👏🏻

 觉得作者写的不错或者心情愉悦的老板也可以投币打赏,感谢观看,希望能给大家带来帮助 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值