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

news2024/9/24 0:16:19
业务场景

项目中不需要点击小图然后展示大图,类似于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>
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2126597.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宏任务和微任务+超全面试真题

概念 微任务和宏任务是在异步编程中经常使用的概念&#xff0c;用于管理任务的执行顺序和优先级。 宏任务&#xff1a;setTimeout, setInterval&#xff0c;I/O 操作和 UI 渲染等。微任务&#xff1a; Promise 回调、async/await等 微任务通常比宏任务具有更高的优先级。 执…

S7-1500替代S7-300全解析系列

硬件篇上 01 概述工控人加入PLC工业自动化精英社群 2022年十月初的时候&#xff0c;想必工控圈的小伙伴们都被S7-300系列即将于2023年10月1日退市的消息刷屏了吧&#xff1f;倒退到2020年的10月1日&#xff0c;同样伴随我们多年的ET200S系列也已经悄无声息地退市了。在感叹经…

GEE 将本地 GeoJSON 文件上传到谷歌资产

在地理信息系统&#xff08;GIS&#xff09;领域&#xff0c;Google Earth Engine&#xff08;GEE&#xff09;是一个强大的平台&#xff0c;它允许用户处理和分析大规模地理空间数据。本文将介绍如何使用 Python 脚本批量上传本地 GeoJSON 文件到 GEE 资产存储&#xff0c;这对…

Qt (16)【Qt 事件 —— Qt 事件简介 | 如何重写相关的 Event 函数】

阅读导航 引言一、事件介绍二、如何重写相关的 Event 函数1. 事件的处理简介2. 示例重写鼠标相关的 Event 函数&#xff08;1&#xff09;新建Qt项目&#xff0c;设计UI文件&#xff08;2&#xff09;新添加MyLabel类&#xff08;3&#xff09;重写enterEvent()方法和leaveEven…

分享一个爬虫数据挖掘 农村产权交易数据可视化平台 数据分析大数据 Java、python双版(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

形式向好、成本较低、可拓展性较高的名厨亮灶开源了。

简介 AI视频监控平台, 是一款功能强大且简单易用的实时算法视频监控系统。愿景在最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;减少企业级应用约 95%的开发成本&#xff0c;在强大视频算法加…

建筑业首个通过算法备案的大模型发布

建筑业首个通过算法备案的大模型发布 9月10日上午&#xff0c;上海建工四建集团与中国建筑出版传媒有限公司携手推出了Construction-GPT PRO版&#xff0c;这是一款专为建筑行业设计的施工知识大模型。该模型能够理解和生成长达8000字符的内容&#xff0c;其回答速度达到毫秒级…

LLM大模型学习:NLP三大特征抽取器(CNN/RNN/TF)

NLP三大特征抽取器&#xff08;CNN/RNN/TF&#xff09; 结论&#xff1a;RNN已经基本完成它的历史使命&#xff0c;将来会逐步退出历史舞台&#xff1b;CNN如果改造得当&#xff0c;将来还是有希望有自己在NLP领域的一席之地&#xff1b;而Transformer明显会很快成为NLP里担当…

Linux 信息安全:构建坚固的防御体系

摘要&#xff1a; 本文围绕 Linux 信息安全展开。阐述了 Linux 在信息技术中的重要地位&#xff0c;强调信息安全的重要性以及 Linux 信息安全面临复杂网络环境、演变攻击手段与内部威胁等挑战。详细介绍了 Linux 系统的安全架构与机制&#xff0c;包括用户与权限管理、文件系统…

Hexo框架学习——从安装到配置

第一章 Hexo入门 Hexo 是一个快速、简洁且高效的博客框架。 1.1 Hexo的下载与安装 1.1.1 Hexo下载 在下载Hexo之前&#xff0c;我们需要确保电脑上已经安装好以下软件&#xff1a; Node.js (Node.js 版本需不低于 10.13&#xff0c;建议使用 Node.js 12.0 及以上版本) Git…

你真的懂吗系列——串口通信

你真的懂吗 文章目录 你真的懂吗前言二、什么是串口通信二、STM32的串口三、什么是数据通信 前言 串口通信是一种设备间常用的串行通信方式&#xff0c;串口按位&#xff08;bit&#xff09;发送和接收字节。尽管比字节&#xff08;byte&#xff09;的串行通信慢&#xff0c;但…

机器学习算法-决策树算法

文章目录 什么是决策树&#xff1f;决策树的基本概念决策树的构建过程决策树的优缺点优点&#xff1a;缺点&#xff1a; 决策树的优化决策树的应用决策树的实现工具 特征选择准则1. 信息增益&#xff08;Information Gain&#xff09;计算公式&#xff1a;熵&#xff08;Entrop…

ubuntu20.4安装Qt5.15.2

ubantu20.4镜像下载地址&#xff1a; https://releases.ubuntu.com/focal/ubuntu-20.04.6-desktop-amd64.iso Qt5.15.2下载地址&#xff1a; https://download.qt.io/official_releases/online_installers/ 安装步骤 1、进入地址后选择对应安装包&#xff0c;我这是ubuntu…

Redis进阶(二)--Redis高级特性和应用

文章目录 第二章、Redis高级特性和应用一、Redis的慢查询1、慢查询配置2、慢查询操作命令3、慢查询建议 二、Pipeline三、事务1、Redis的事务原理2、Redis的watch命令3、Pipeline和事务的区别 四、Lua1、Lua入门&#xff08;1&#xff09;安装Lua&#xff08;2&#xff09;Lua基…

虚幻引擎 | (类恐鬼症)玩家和NPC语音聊天

SETUP&#xff1a;工具和插件 工具&#xff1a;elevenlabs或者讯飞&#xff0c;用于Speech Synthesis&#xff08;语音合成&#xff0c;text to speech&#xff09;。 https://elevenlabs.io/app/speech-synthesis/text-to-speechhttps://elevenlabs.io/app/speech-synthesis…

海外云手机——跨国业务的高效工具

海外云手机是一种基于云计算的虚拟手机服务&#xff0c;依托海外服务器实现跨国网络访问。这项服务不仅具备传统智能手机的所有功能&#xff0c;还突破了地域限制&#xff0c;为跨国业务提供更加便捷、高效、安全的解决方案。 随着全球化的加速和互联网的快速普及&#xff0c;跨…

C语言深入理解指针五(18)

文章目录 前言一、回调函数是什么&#xff1f;二、qsort使用举例使用qsort函数排序整型数据使用qsort函数排序结构数据 三、qsort的模拟实现总结 前言 本篇将会很有意思&#xff01; 一、回调函数是什么&#xff1f; 回调函数就是一个通过函数指针调用的函数。   如果你把函数…

代码随想录27期|Python|Day52|​动态规划|​647. 回文子串|516. 最长回文子序列

本文是动态规划的回文字符串部分。 647. 回文子串 本题需要搞清楚dp的定义、遍历顺序和递推公式。 1、dp数组的定义 由图片可知&#xff0c;不同于之前的dp数组直接定义为当前遍历到的位置处题目所要求得值&#xff0c;而是应该定义为i为开始&#xff0c;j为结束的子串是否是…

探索音视频SDK的双重核心:客户端与服务端的协同作用

在当今的数字化时代&#xff0c;音视频技术已成为连接人与人、人与世界的重要桥梁。从社交娱乐到在线教育&#xff0c;从远程医疗到视频会议&#xff0c;音视频技术的应用无处不在&#xff0c;极大地丰富了我们的生活方式和工作模式。本文将深入探讨音视频SDK的两大核心类别——…

横版闯关手游【全明星时空阿拉德】Linux手工服务端+运营后台+双app端

横版闯关手游【时空阿拉德】&#xff08;【全明星阿拉德】&#xff09;阿拉德系列2022整理Linux手工服务端余额充值后台安卓苹果双端。 运营后台看目录结构是thinkphp开发的。 代码免费下载&#xff1a;百度网盘