业务场景
项目中不需要点击小图然后展示大图,类似于elementui中的Image图片组件。适用于直接展示大图,支持拖拽和缩放的场景,比如:用户需要比对两种数据的图片展示,左右两侧进行展示。
效果图
使用方式
-
在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) ); };
-
在main.js文件加入如下代码
import Image from "@/components/image-view/index.vue"; Vue.component("ImageView", Image);
-
使用
<template> <div class="image-demo"> <ImageView url="https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg" /> </div> </template>