图片预览、拖拽和缩放组件分享

业务场景

项目中不需要点击小图然后展示大图,类似于elementui中的Image图片组件。适用于直接展示大图,支持拖拽和缩放的场景,比如:用户需要比对两种数据的图片展示,左右两侧进行展示。

效果图

在这里插入图片描述

使用方式
  1. 在components文件中新建image-view文件夹

    <!-- index.vue -->
    
    <!--
     @author: duanfc
     @time: 2024-09-02 12:00:00
     @description: 图片展示组件
     @path: /demo
     @lastChange: duanfc
    -->
    
    <template>
    	<div class="image-show">
            <!-- ACTIONS -->
            <div class="el-image-viewer__btn el-image-viewer__actions">
                <div class="el-image-viewer__actions__inner">
                    <i class="el-icon-zoom-out" @click="handleActions('zoomOut')"></i>
                    <i class="el-icon-zoom-in" @click="handleActions('zoomIn')"></i>
                    <i class="el-image-viewer__actions__divider"></i>
                    <i :class="mode.icon" @click="toggleMode"></i>
                    <i class="el-image-viewer__actions__divider"></i>
                    <i class="el-icon-refresh-left" @click="handleActions('anticlocelise')"></i>
                    <i class="el-icon-refresh-right" @click="handleActions('clocelise')"></i>
                </div>
            </div>
            <!-- CANVAS -->
    		<div class="image-viewer__canvas">
    			<img
    				ref="imgRef"
    				class="image-viewer__img"
    				:src="currentImg"
    				:style="imgStyle"
    				@load="handleImgLoad"
    				@error="handleImgError"
    				@mousedown="handleMouseDown"
    			/>
    		</div>
    	</div>
    </template>
    
    <script lang='ts'>
    import useCommon from "@/hooks/use-common";
    import {
    	ref,
    	reactive,
    	defineComponent,
    	onMounted,
    	computed,
    	toRefs,
    } from "@vue/composition-api";
    import useResizeSearch from "@/hooks/use-resizeSearch";
    import api from "@/api";
    import request from "@/axios/fetch";
    import { on, off } from "./utils/dom";
    import { rafThrottle, isFirefox } from "./utils/util";
    
    const Mode = {
    	CONTAIN: {
    		name: "contain",
    		icon: "el-icon-full-screen",
    	},
    	ORIGINAL: {
    		name: "original",
    		icon: "el-icon-c-scale-to-original",
    	},
    };
    const mousewheelEventName = isFirefox() ? "DOMMouseScroll" : "mousewheel";
    
    export default defineComponent({
    	name: "imageShow",
    	components: {},
    	props: {
    		url: {
    			type: String,
    			default: "",
    		},
    	},
    	setup(props) {
    		const { proxy } = useCommon(); // 作为this使用
    		const { isXLCol } = useResizeSearch();
    
    		const imgRef = ref(null);
    		const transform = reactive({
    			scale: 1,
    			deg: 0,
    			offsetX: 0,
    			offsetY: 0,
    			enableTransition: false,
    		});
    		const loading = ref(false);
    		const mode = ref(Mode.CONTAIN);
    
    		const currentImg = computed(() => {
    			return props.url;
    		});
    
    		const handleImgLoad = () => {
    			loading.value = false;
    		};
    		const handleImgError = (e) => {
    			loading.value = false;
    			e.target.alt = "加载失败";
    		};
    		const handleMouseDown = (e) => {
    			if (loading.value || e.button !== 0) return;
    
    			const { offsetX, offsetY } = transform;
    			const startX = e.pageX;
    			const startY = e.pageY;
    			const _dragHandler = rafThrottle((ev) => {
    				transform.offsetX = offsetX + ev.pageX - startX;
    				transform.offsetY = offsetY + ev.pageY - startY;
    			});
    			on(imgRef.value, "mousemove", _dragHandler);
    			on(imgRef.value, "mouseup", () => {
    				off(imgRef.value, "mousemove", _dragHandler);
    			});
    			e.preventDefault();
    		};
    		const handleActions = (action, options = {}) => {
    			if (loading.value) return;
    			const { zoomRate, rotateDeg, enableTransition } = {
    				zoomRate: 0.2,
    				rotateDeg: 90,
    				enableTransition: true,
    				...options,
    			};
    			switch (action) {
    				case "zoomOut":
    					if (transform.scale > 0.2) {
    						transform.scale = parseFloat(
    							(transform.scale - zoomRate).toFixed(3)
    						);
    					}
    					break;
    				case "zoomIn":
    					transform.scale = parseFloat(
    						(transform.scale + zoomRate).toFixed(3)
    					);
    					break;
    				case "clocelise":
    					transform.deg += rotateDeg;
    					break;
    				case "anticlocelise":
    					transform.deg -= rotateDeg;
    					break;
    			}
    			transform.enableTransition = enableTransition;
    		};
    		const imgStyle = computed(() => {
    			const { scale, deg, offsetX, offsetY, enableTransition } =
    				transform;
    			const style = {
    				transform: `scale(${scale}) rotate(${deg}deg)`,
    				transition: enableTransition ? "transform .3s" : "",
    				"margin-left": `${offsetX}px`,
    				"margin-top": `${offsetY}px`,
                    maxWidth: undefined,
                    maxHeight: undefined,
    			};
    			if (mode.value === Mode.CONTAIN) {
    				style.maxWidth = style.maxHeight = "100%";
    			}
    			return style;
    		});
    		const deviceSupportInstall = () => {
    			const _mouseWheelHandler = rafThrottle((e) => {
    				e.stopPropagation(); // 阻止事件传播
    				const delta = e.wheelDelta ? e.wheelDelta : -e.detail;
    				if (delta > 0) {
    					handleActions("zoomIn", {
    						zoomRate: 0.015,
    						enableTransition: false,
    					});
    				} else {
    					handleActions("zoomOut", {
    						zoomRate: 0.015,
    						enableTransition: false,
    					});
    				}
    			});
    			on(imgRef.value, mousewheelEventName, _mouseWheelHandler);
    		};
            const reset = () => {
                transform.scale = 1;
                transform.deg = 0;
                transform.offsetX = 0;
                transform.offsetY = 0;
                transform.enableTransition = false;
            }
            const toggleMode = () => {
                if (loading.value) return;
                const modeNames = Object.keys(Mode);
                const modeValues = Object.values(Mode);
                const index = modeValues.indexOf(mode.value);
                const nextIndex = (index + 1) % modeNames.length;
                mode.value = Mode[modeNames[nextIndex]];
                reset();
            }
    
    		onMounted(() => {
    			deviceSupportInstall();
    		});
    		return {
    			imgRef,
    			currentImg,
    			handleImgLoad,
    			handleImgError,
    			handleMouseDown,
    			imgStyle,
                handleActions,
                mode,
                toggleMode,
    		};
    	},
    });
    </script>
    
    <style lang="less" scoped>
    .image-show {
    	height: 100%;
    	width: 100%;
        position: relative;
        overflow: hidden;
    	.image-viewer__canvas {
    		width: 100%;
    		height: 100%;
    		display: -webkit-box;
    		display: -ms-flexbox;
    		display: flex;
    		-webkit-box-pack: center;
    		-ms-flex-pack: center;
    		justify-content: center;
    		-webkit-box-align: center;
    		-ms-flex-align: center;
    		align-items: center;
    		.image-viewer__img {
    			height: 100%; /* 高度铺满父容器 */
    			width: auto; /* 宽度自适应 */
    			object-fit: contain; /* 图片自适应容器 */
    		}
    	}
        .el-image-viewer__btn {
            position: absolute;
            z-index: 1;
            display: -webkit-box;
            display: -ms-flexbox;
            display: flex;
            -webkit-box-align: center;
            -ms-flex-align: center;
            align-items: center;
            -webkit-box-pack: center;
            -ms-flex-pack: center;
            justify-content: center;
            border-radius: 50%;
            opacity: .8;
            cursor: pointer;
            -webkit-box-sizing: border-box;
            box-sizing: border-box;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none
        }
        .el-image-viewer__actions {
            left: 50%;
            bottom: 30px;
            -webkit-transform: translateX(-50%);
            transform: translateX(-50%);
            width: 282px;
            height: 44px;
            padding: 0 23px;
            background-color: #606266;
            border-color: #fff;
            border-radius: 22px;
    
            .el-image-viewer__actions__inner {
                width: 100%;
                height: 100%;
                text-align: justify;
                cursor: default;
                font-size: 23px;
                color: #fff;
                display: -webkit-box;
                display: -ms-flexbox;
                display: flex;
                -webkit-box-align: center;
                -ms-flex-align: center;
                align-items: center;
                -ms-flex-pack: distribute;
                justify-content: space-around
            }
        }
    }
    </style>
    
    <!-- utils/dom.js -->
    
    import Vue from "vue";
    
    const isServer = Vue.prototype.$isServer;
    
    /* istanbul ignore next */
    export const on = (function () {
      if (!isServer && document.addEventListener) {
        return function (element, event, handler) {
          if (element && event && handler) {
            element.addEventListener(event, handler, false);
          }
        };
      } else {
        return function (element, event, handler) {
          if (element && event && handler) {
            element.attachEvent("on" + event, handler);
          }
        };
      }
    })();
    
    /* istanbul ignore next */
    export const off = (function () {
      if (!isServer && document.removeEventListener) {
        return function (element, event, handler) {
          if (element && event) {
            element.removeEventListener(event, handler, false);
          }
        };
      } else {
        return function (element, event, handler) {
          if (element && event) {
            element.detachEvent("on" + event, handler);
          }
        };
      }
    })();
    
    /* istanbul ignore next */
    export function addClass(el, cls) {
      if (!el) return;
      var curClass = el.className;
      var classes = (cls || "").split(" ");
    
      for (var i = 0, j = classes.length; i < j; i++) {
        var clsName = classes[i];
        if (!clsName) continue;
    
        if (el.classList) {
          el.classList.add(clsName);
        } else if (!hasClass(el, clsName)) {
          curClass += " " + clsName;
        }
      }
      if (!el.classList) {
        el.setAttribute("class", curClass);
      }
    }
    
    /* istanbul ignore next */
    export function removeClass(el, cls) {
      if (!el || !cls) return;
      var classes = cls.split(" ");
      var curClass = " " + el.className + " ";
    
      for (var i = 0, j = classes.length; i < j; i++) {
        var clsName = classes[i];
        if (!clsName) continue;
    
        if (el.classList) {
          el.classList.remove(clsName);
        } else if (hasClass(el, clsName)) {
          curClass = curClass.replace(" " + clsName + " ", " ");
        }
      }
      if (!el.classList) {
        el.setAttribute("class", trim(curClass));
      }
    }
    
    /* istanbul ignore next */
    export function hasClass(el, cls) {
      if (!el || !cls) return false;
      if (cls.indexOf(" ") !== -1)
        throw new Error("className should not contain space.");
      if (el.classList) {
        return el.classList.contains(cls);
      } else {
        return (" " + el.className + " ").indexOf(" " + cls + " ") > -1;
      }
    }
    
    // const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
    // const MOZ_HACK_REGEXP = /^moz([A-Z])/;
    const ieVersion = isServer ? 0 : Number(document.documentMode);
    
    /* istanbul ignore next */
    const trim = function (string) {
      return (string || "").replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, "");
    };
    
    /* istanbul ignore next */
    // const camelCase = function(name) {
    //   return name.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
    //     return offset ? letter.toUpperCase() : letter;
    //   }).replace(MOZ_HACK_REGEXP, 'Moz$1');
    // };
    
    /* istanbul ignore next */
    export const getStyle = ieVersion < 9 ? function(element, styleName) {
      if (isServer) return;
      if (!element || !styleName) return null;
      // styleName = camelCase(styleName);
      if (styleName === 'float') {
        styleName = 'styleFloat';
      }
      try {
        switch (styleName) {
          case 'opacity':
            try {
              return element.filters.item('alpha').opacity / 100;
            } catch (e) {
              return 1.0;
            }
          default:
            return (element.style[styleName] || element.currentStyle ? element.currentStyle[styleName] : null);
        }
      } catch (e) {
        return element.style[styleName];
      }
    } : function(element, styleName) {
      if (isServer) return;
      if (!element || !styleName) return null;
      // styleName = camelCase(styleName);
      if (styleName === 'float') {
        styleName = 'cssFloat';
      }
      try {
        var computed = document.defaultView.getComputedStyle(element, '');
        return element.style[styleName] || computed ? computed[styleName] : null;
      } catch (e) {
        return element.style[styleName];
      }
    };
    
    <!-- utils/util.js -->
    
    import Vue from "vue";
    
    export function rafThrottle(fn) {
      let locked = false;
      return function (...args) {
        if (locked) return;
        locked = true;
        window.requestAnimationFrame(() => {
          fn.apply(this, args);
          locked = false;
        });
      };
    }
    
    export const isFirefox = function () {
      return (
        !Vue.prototype.$isServer && !!window.navigator.userAgent.match(/firefox/i)
      );
    };
    
  2. 在main.js文件加入如下代码

    import Image from "@/components/image-view/index.vue";
    Vue.component("ImageView", Image);
    
  3. 使用

    <template>
    	<div class="image-demo">
    		<ImageView url="https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg" />
    	</div>
    </template>
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值