Vue3+Ts封装类似于Element-plus的图片预览组件

组件目录结构如下:

options.ts文件用来存储配置文件, 代码如下:

import {isFirefox} from './tools';

export type ImageViewerAction = 'zoomIn' | 'zoomOut' | 'clocelise' | 'anticlocelise';

export const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel';

// 键盘按键值
export const EVENT_CODE = {
  space: "Space",
  left: "ArrowLeft", // 37
  up: "ArrowUp", // 38
  right: "ArrowRight", // 39
  down: "ArrowDown", // 40
  esc: "Escape",
};

// icon对应的svg
export const ICON_SVG = {
  close: `<svg t="1628759850204" viewBox="0 0 1024 1024" width="24" height="24"><path d="M764 215.008L512 467.008 260 215.008q-10.016-8.992-22.496-8.992t-22.016 9.504-9.504 22.016 8.992 22.496l252 252-252 252q-12.992 12.992-8.512 31.008t22.016 22.496 31.488-8.512l252-252 252 252q10.016 8.992 22.496 8.992t22.016-9.504 9.504-22.016-8.992-22.496L556.992 512l252-252q12.992-12.992 8.512-31.008t-22.496-22.496-31.008 8.512z" p-id="3923" fill="#ffffff"></path></svg>`,
  arrowLeft: '<svg t="1628762138411" viewBox="0 0 1024 1024" width="24" height="24"><path d="M608.992 148.992L277.984 489.984q-8.992 8.992-8.992 21.504t8.992 22.496l331.008 340.992q8.992 8 20.992 8t20.992-8.992 8.992-20.992-8.992-20.992l-312-320 312-320q8.992-8.992 8.992-20.992t-8.992-20.992-20.992-8.992-20.992 8z" p-id="5372" fill="#ffffff"></path></svg>',
  arrowRight: '<svg t="1628762150860" viewBox="0 0 1024 1024" width="24" height="24"><path d="M340.992 148.992q-8.992 10.016-8.992 22.016t8.992 20.992l312 320-312 320q-8.992 8.992-8.992 20.992t8.992 20.992 20.992 8.992 20.992-8l331.008-340.992q8.992-8.992 8.992-22.016t-8.992-22.016L382.976 148.96q-8.992-8-20.992-8t-20.992 8z" p-id="5663" fill="#ffffff"></path></svg>',
  zoomOut: '<svg t="1628761969296" viewBox="0 0 1024 1024" width="24" height="24"><path d="M796 751.008l124.992 124.992q8.992 10.016 8.992 22.496t-9.504 22.016-22.016 9.504-22.496-8.992l-124.992-124.992q-132.992 108.992-295.008 99.488t-280.992-132.512q-114.016-128.992-111.008-291.008t122.016-286.016q124-119.008 286.016-122.016t291.008 111.008q123.008 119.008 132.512 280.992t-99.488 295.008zM480 832q150.016-4 248.992-103.008T832 480q-4-150.016-103.008-248.992T480 128q-150.016 4-248.992 103.008T128 480q4 150.016 103.008 248.992T480 832z m-128-384h256q14.016 0 23.008 8.992T640 480t-8.992 23.008T608 512h-256q-14.016 0-23.008-8.992T320 480t8.992-23.008T352 448z" p-id="4889" fill="#ffffff"></path></svg>',
  zoomIn: '<svg t="1628761724109" viewBox="0 0 1024 1024" width="24" height="24"><path d="M796 751.008l124.992 124.992q8.992 10.016 8.992 22.496t-9.504 22.016-22.016 9.504-22.496-8.992l-124.992-124.992q-132.992 108.992-295.008 99.488t-280.992-132.512q-114.016-128.992-111.008-291.008t122.016-286.016q124-119.008 286.016-122.016t291.008 111.008q123.008 119.008 132.512 280.992t-99.488 295.008zM480 832q150.016-4 248.992-103.008T832 480q-4-150.016-103.008-248.992T480 128q-150.016 4-248.992 103.008T128 480q4 150.016 103.008 248.992T480 832z m-32-384v-96q0-14.016 8.992-23.008T480 320t23.008 8.992T512 352v96h96q14.016 0 23.008 8.992T640 480t-8.992 23.008T608 512h-96v96q0 14.016-8.992 23.008T480 640t-23.008-8.992T448 608v-96h-96q-14.016 0-23.008-8.992T320 480t8.992-23.008T352 448h96z" p-id="4358" fill="#ffffff"></path></svg>',
  original: '<svg t="1628762365130" viewBox="0 0 1024 1024" width="24" height="24"><path d="M812.992 180.992q26.016 0 43.008 16.992t16.992 43.008v482.016q0 24.992-17.504 42.496t-42.496 17.504H210.976q-24.992 0-42.496-17.504t-17.504-42.496V240.992q0-26.016 16.992-43.008t43.008-16.992h602.016z m0-60.992H210.976q-24 0.992-46.016 10.016t-39.008 26.016-26.496 39.008-9.504 46.016v482.016q0 24 9.504 46.016t26.496 39.008 39.008 26.016 46.016 8.992h602.016q24 0 46.016-8.992t39.008-26.016 26.496-39.008 9.504-46.016V241.056q0-24-9.504-46.016t-26.496-39.008-39.008-26.016-46.016-10.016z m-120 180.992q-12.992 0-21.504 8.992t-8.512 20.992v300.992q0 12.992 8.512 21.504t21.504 8.512 21.504-8.512 8.512-21.504v-300.992q0-12.992-8.512-21.504t-21.504-8.512z m-361.984 0q-12 0-20.992 8.992t-8.992 20.992v300.992q0 12.992 8.512 21.504t21.504 8.512 21.504-8.512 8.512-21.504v-300.992q0-12-8.512-20.992t-21.504-8.992zM512 360.992q-12.992 0.992-21.504 9.504t-8.512 21.504v30.016q0 12 8.512 20.512t21.504 8.512 21.504-8.512 8.512-20.512v-30.016q0-12.992-8.992-21.504t-20.992-9.504zM512 512q-12.992 0-21.504 8.512t-8.512 21.504v30.016q0 12.992 8.512 21.504t21.504 8.512 21.504-8.512 8.512-21.504v-30.016q0-12-8.992-20.992t-20.992-8.992z" p-id="5954" fill="#ffffff"></path></svg>', 
  fullScreen: '<svg t="1628762716543" viewBox="0 0 1024 1024" width="24" height="24"><path d="M160 96h192q14.016 0.992 23.008 10.016t8.992 22.496-8.992 22.496T352 160H160v192q0 14.016-8.992 23.008T128 384t-23.008-8.992T96 352V96h64z m0 832H96v-256q0-14.016 8.992-23.008T128 640t23.008 8.992T160 672v192h192q14.016 0 23.008 8.992t8.992 22.496-8.992 22.496T352 928H160zM864 96h64v256q0 14.016-8.992 23.008T896 384t-23.008-8.992T864 352V160h-192q-14.016 0-23.008-8.992T640 128.512t8.992-22.496T672 96h192z m0 832h-192q-14.016-0.992-23.008-10.016T640 895.488t8.992-22.496T672 864h192v-192q0-14.016 8.992-23.008T896 640t23.008 8.992T928 672v256h-64z" p-id="6683" fill="#ffffff"></path></svg>',
  refreshLeft: '<svg t="1628762407981" viewBox="0 0 1024 1024" width="24" height="24"><path d="M288.992 296.992h92.992q14.016 0 23.008 8.992t8.992 22.496-8.992 22.496-23.008 10.016H232.992q-14.016-0.992-23.008-10.016t-8.992-22.016V179.968q0-14.016 8.992-23.008t23.008-8.992 23.008 8.992 8.992 23.008v50.016q86.016-76.992 196.512-95.488t217.504 26.496q106.016 48 167.488 142.016t62.496 210.016q-4 163.008-112.512 271.488t-271.488 112.512q-163.008-4-271.488-112.512t-112.512-271.488h64q3.008 136 93.504 226.496t226.496 93.504q136-3.008 226.016-93.504t94.016-226.496q-0.992-100.992-56-180.512t-148-117.504q-94.016-35.008-188.512-13.504T289.024 296.992z" p-id="6197" fill="#ffffff"></path></svg>',
  refreshRight: '<svg t="1628762417349" viewBox="0 0 1024 1024" width="24" height="24"><path d="M784.992 230.016V180q0-14.016 8.992-23.008t22.496-8.992 22.496 8.992 10.016 23.008v148.992q-0.992 12.992-10.016 22.016t-22.016 10.016h-148.992q-14.016-0.992-23.008-10.016t-8.992-22.496 8.992-22.496 23.008-8.992h92.992q-78.016-82.016-183.488-99.488t-204.512 34.496q-98.016 54.016-140.992 152t-16.992 208q28.992 108 113.504 173.504t196.512 67.488q136-3.008 226.016-93.504t94.016-226.496h64q-4 163.008-112.512 271.488t-271.488 112.512q-163.008-4-271.488-112.512t-112.512-271.488q0.992-116 62.016-210.016t167.008-140.992q107.008-46.016 217.504-27.488t197.504 95.488z" p-id="6440" fill="#ffffff"></path></svg>',
};

tools.ts主要来书写相应的工具类,代码如下:

// 监听元素事件
export function on(
  element: HTMLElement | Document | Window,
  event: string,
  handler: EventListenerOrEventListenerObject,
  useCapture = false,
): void {
  if (element && event && handler) {
    element.addEventListener(event, handler, useCapture);
  }
}

// 解绑元素事件
export function off(
  element: HTMLElement | Document | Window,
  event: string,
  handler: EventListenerOrEventListenerObject,
  useCapture = false,
): void {
  if (element && event && handler) {
    element.removeEventListener(event, handler, useCapture);
  }
}

// 判断是否是火狐浏览器
export function isFirefox(): boolean {
  return !!window.navigator.userAgent.match(/firefox/i);
}

// 定义一个参数为任何类型和数量且返回类型为 T 的函数声明
export type AnyFunction<T> = (...args: any[]) => T;

export function rafThrottle<T extends AnyFunction<any>>(fn: T): AnyFunction<void> {
  let locked = false; // 定义一个锁变量来跟踪函数是否被锁定
  return function (...args: any[]) { // 返回一个新的函数,该函数会被调用时执行 throttled 逻辑
    if (locked) return; // 如果函数被锁定,直接返回,不执行
    locked = true; // 锁定函数
    window.requestAnimationFrame(() => { // 使用 requestAnimationFrame 来调度函数执行
      // fn.apply(this, args); // 如果你希望函数在调用时保持上下文,取消注释这行
      fn.apply(null, args); // 调用传入的函数 fn,this 参数设置为 null(可以根据需要调整)
      locked = false; // 解锁函数,使其可以再次被调用
    });
  };
}

Preview.vue主要来写组件的页面布局和组件的逻辑处理,代码如下:

<template>
	<teleport to="body" :disabled="!appendToBody">
		<transition name="preview-fade">
			<div ref="wrapper" :tabindex="-1" class="preview" :style="{ zIndex }" v-if="visible">
				<!-- 蒙层 -->
				<div class="preview__mask" @click.self="hideOnClickModal && hide()"></div>
				<!-- 关闭按钮 -->
				<span v-html="ICON_SVG.close" class="preview__btn preview__close" @click="hide" title="关闭"></span>
				<!-- 左右箭头 -->
				<template v-if="!isSingle">
					<span
						@click="prev"
						:class="{ 'is-disabled': !infinite && isFirst }"
						class="preview__btn preview__prev"
						v-html="ICON_SVG.arrowLeft"
					>
					</span>
					<span
						@click="next"
						:class="{ 'is-disabled': !infinite && isLast }"
						class="preview__btn preview__next"
						v-html="ICON_SVG.arrowRight"
					>
					</span>
				</template>
				<!-- 操作区 -->
				<div class="preview__actions">
					<span v-html="ICON_SVG.zoomOut" class="preview__icon" title="缩小" @click="handleActions('zoomOut')"></span>
					<span v-html="ICON_SVG.zoomIn" class="preview__icon" title="放大" @click="handleActions('zoomIn')"></span>
					<span
						v-html="ICON_SVG.fullScreen"
						class="preview__icon"
						v-show="mode === 'original'"
						title="原图"
						@click="toggleMode('fullscreen')"
					></span>
					<span
						v-html="ICON_SVG.original"
						class="preview__icon"
						v-show="mode === 'fullscreen'"
						title="1:1"
						@click="toggleMode('original')"
					></span>
					<span v-html="ICON_SVG.refreshLeft" class="preview__icon" title="左旋转" @click="handleActions('anticlocelise')"></span>
					<span v-html="ICON_SVG.refreshRight" class="preview__icon" title="右旋转" @click="handleActions('clocelise')"></span>
				</div>
				<!-- 图片展示 -->
				<div class="preview__canvas">
					<img
						v-for="(url, i) in imgPaths"
						v-show="i === index"
						ref="img"
						class="preview__img"
						:key="url"
						:src="url"
						:style="imgStyle"
						@load="handleImgLoad"
						@error="handleImgError"
						@mousedown="handleMouseDown"
					/>
					<svg v-if="loading" viewBox="25 25 50 50" class="infinite-scroll__svg">
						<circle cx="50" cy="50" r="20" class="infinite-scroll__circle"></circle>
					</svg>
				</div>
			</div>
		</transition>
	</teleport>
</template>

<script lang="ts" setup>
import { onMounted, computed, PropType, ref, watch, nextTick } from "vue";
import { on, off, rafThrottle } from "./tools";
import { EVENT_CODE, ICON_SVG, ImageViewerAction, mousewheelEventName } from "./options";

let prevOverflow = "";
const props = defineProps({
	modelValue: {
		type: Boolean,
		default: false
	},
	// 展示的图片路径列表
	urlList: {
		type: Array as PropType<string[]>,
		default: () => [] as string[]
	},
	// 组件所处层级
	zIndex: {
		type: Number,
		default: 2000
	},
	// 点击蒙层是否关闭
	hideOnClickModal: {
		type: Boolean,
		default: false
	},
	// 预览的首张图片的位置
	initialIndex: {
		type: Number,
		default: 0
	},
	// 是否无限循环预览
	infinite: {
		type: Boolean,
		default: true
	},
	// 是否将组件插入至 body 元素上
	appendToBody: {
		type: Boolean,
		default: true
	}
});

const emit = defineEmits(["close", "switch", "update:modelValue"]);

const visible = ref(false);
const wrapper = ref(null);
const img = ref(null);
const index = ref(props.initialIndex);
const loading = ref(true);
// 展示的图片数组
const imgPaths = ref(props.urlList);
// 按键按下的处理函数, 但要节流处理一下
let keyDownHandler: (() => void) | null;
// 处理鼠标滚动, 但要节流处理一下
let mouseWheelHandler: (() => void) | null;
// 拖动事件, 但要节流处理一下
let dragHandler: () => void;
// 是否存在箭头
const isSingle = computed(() => imgPaths.value.length <= 1);
// 是否是第一张
const isFirst = computed(() => index.value === 0);
// 是否是最后一张
const isLast = computed(() => index.value === imgPaths.value.length - 1);
// 图片模式, fullscreen: 当前屏幕的宽高比例  original: 原图
const mode = ref("original");
// 当前图片路径
const currentImg = computed(() => imgPaths.value[index.value]);
// 图片样式
const transform = ref({
	scale: 1,
	deg: 0,
	offsetX: 0,
	offsetY: 0,
	enableTransition: false
});
const imgStyle = computed(() => {
	const { scale, deg, offsetX, offsetY, enableTransition } = transform.value;
	const style = {
		transform: `scale(${scale}) rotate(${deg}deg)`,
		transition: enableTransition ? "transform .3s" : "",
		marginLeft: `${offsetX}px`,
		marginTop: `${offsetY}px`,
		maxWidth: "none",
		maxHeight: "none"
	};
	if (mode.value === "original") {
		style.maxWidth = "100%";
		style.maxHeight = "100%";
	}
	return style;
});
// 显示组件
const open = (imgUrls: string | string[]) => {
	if (imgUrls) {
		imgPaths.value = Array.isArray(imgUrls) ? imgUrls : [imgUrls];
	}
	show();
};
// 显示组件
const show = () => {
	deviceSupportInstall();
	prevOverflow = document.body.style.overflow;
	document.body.style.overflow = "hidden";
	reset();
	visible.value = true;
};
// 隐藏组件
const hide = () => {
	deviceSupportUninstall();
	document.body.style.overflow = prevOverflow;
	visible.value = false;
	emit("close");
	emit("update:modelValue", false);
};
// 组件初始化, 绑定各种事件
const deviceSupportInstall = () => {
	keyDownHandler = rafThrottle((e: KeyboardEvent) => {
		switch (e.code) {
			// ESC
			case EVENT_CODE.esc:
				hide();
				break;
			// SPACE
			case EVENT_CODE.space:
				toggleMode(mode.value === "original" ? "fullscreen" : "original");
				break;
			// LEFT_ARROW
			case EVENT_CODE.left:
				prev();
				break;
			// UP_ARROW
			case EVENT_CODE.up:
				handleActions("zoomIn");
				break;
			// RIGHT_ARROW
			case EVENT_CODE.right:
				next();
				break;
			// DOWN_ARROW
			case EVENT_CODE.down:
				handleActions("zoomOut");
				break;
			// no default
		}
	});
	mouseWheelHandler = rafThrottle(e => {
		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(document, "keydown", keyDownHandler);
	on(document, mousewheelEventName, mouseWheelHandler);
};
// 组件销毁, 解绑各种事件
const deviceSupportUninstall = () => {
	off(document, "keydown", keyDownHandler!);
	off(document, mousewheelEventName, mouseWheelHandler!);
	keyDownHandler = null;
	mouseWheelHandler = null;
};
// 上一张
const prev = () => {
	if (isFirst.value && !props.infinite) return;
	const len = imgPaths.value.length;
	index.value = (index.value - 1 + len) % len;
};
// 下一张
const next = () => {
	if (isLast.value && !props.infinite) return;
	const len = imgPaths.value.length;
	index.value = (index.value + 1) % len;
};
// 设置图片的index值
const setImageIndex = (val: number) => {
	if (val < 0 || val >= imgPaths.value.length) {
		index.value = 0;
	} else {
		index.value = val;
	}
};
// 鼠标在图片上按下事件
const handleMouseDown = (e: MouseEvent) => {
	if (loading.value || e.button !== 0) return;

	const { offsetX, offsetY } = transform.value;
	const startX = e.pageX;
	const startY = e.pageY;
	dragHandler = rafThrottle(ev => {
		transform.value = {
			...transform.value,
			offsetX: offsetX + ev.pageX - startX,
			offsetY: offsetY + ev.pageY - startY
		};
	});
	on(document, "mousemove", dragHandler);
	on(document, "mouseup", () => {
		off(document, "mousemove", dragHandler);
	});

	e.preventDefault();
};
// 原图与1:1切换
const toggleMode = (type: string) => {
	if (loading.value) return;
	mode.value = type;
	reset();
};
// 放大/缩小/左旋转/右旋转
const handleActions = (action: ImageViewerAction, options = {}) => {
	if (loading.value) return;
	const { zoomRate, rotateDeg, enableTransition } = {
		zoomRate: 0.2,
		rotateDeg: 90,
		enableTransition: true,
		...options
	};
	switch (action) {
		case "zoomOut":
			if (transform.value.scale > 0.2) {
				transform.value.scale = parseFloat((transform.value.scale - zoomRate).toFixed(3));
			}
			break;
		case "zoomIn":
			transform.value.scale = parseFloat((transform.value.scale + zoomRate).toFixed(3));
			break;
		case "clocelise":
			transform.value.deg += rotateDeg;
			break;
		case "anticlocelise":
			transform.value.deg -= rotateDeg;
			break;
		// no default
	}
	transform.value.enableTransition = enableTransition;
};
// 图片加载完毕
const handleImgLoad = () => {
	loading.value && (loading.value = false);
};
// 图片加载失败
const handleImgError = () => {
	loading.value && (loading.value = false);
};
// 重置样式
const reset = () => {
	transform.value = {
		scale: 1,
		deg: 0,
		offsetX: 0,
		offsetY: 0,
		enableTransition: false
	};
};
onMounted(() => {
	if (props.modelValue) {
		show();
	}
	// deviceSupportInstall();
	// add tabindex then wrapper can be focusable via Javascript
	// focus wrapper so arrow key can't cause inner scroll behavior underneath
	// wrapper.value?.focus?.();
});
// 监听除第一张图片外, 每张图片是否加载完毕了
watch(currentImg, () => {
	nextTick(() => {
		const $img: HTMLImageElement = img.value!;
		if (!$img.complete) {
			loading.value = true;
		}
	});
});
// 监听每次切换
watch(index, val => {
	reset();
	emit("switch", val);
});
// 监听v-model
watch(
	() => props.modelValue,
	val => {
		if (val) {
			show();
		} else if (visible.value) {
			hide();
		}
	}
);
defineExpose({
	hide,
	open,
	prev,
	next,
	setImageIndex
});
</script>

<style scoped>
.preview {
	position: fixed;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
}
.preview__mask {
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	opacity: 0.5;
	background: #000;
}
.preview__btn {
	width: 44px;
	height: 44px;
	position: absolute;
	z-index: 10;
	display: flex;
	justify-content: center;
	align-items: center;
	border-radius: 50%;
	opacity: 0.8;
	cursor: pointer;
	box-sizing: border-box;
	user-select: none;
	background-color: rgb(96 98 102);
}
.preview__close {
	top: 40px;
	right: 40px;
}
.preview__prev {
	top: 50%;
	transform: translateY(-50%);
	left: 40px;
}
.preview__next {
	top: 50%;
	transform: translateY(-50%);
	right: 40px;
}
.preview__img {
	cursor: move;
	z-index: 1;
}
.is-disabled {
	cursor: no-drop !important;
}
.preview__actions {
	background-color: rgb(96 98 102);
	position: absolute;
	z-index: 10;
	left: 50%;
	bottom: 30px;
	transform: translateX(-50%);
	display: flex;
	align-items: center;
	justify-content: center;
	opacity: 0.8;
	cursor: pointer;
	box-sizing: border-box;
	user-select: none;
	border-radius: 22px;
	width: 282px;
	height: 44px;
	padding: 0 23px;
}
.preview__icon {
	width: 24px;
	height: 24px;
	margin: 0 12px;
}
.preview__canvas {
	width: 100%;
	height: 100%;
	display: flex;
	justify-content: center;
	align-items: center;
}
@keyframes preview-fade-in {
	0% {
		-webkit-transform: translate3d(0, -20px, 0);
		transform: translate3d(0, -20px, 0);
		opacity: 0;
	}
	100% {
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
		opacity: 1;
	}
}
@keyframes preview-fade-out {
	0% {
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
		opacity: 1;
	}
	100% {
		-webkit-transform: translate3d(0, -20px, 0);
		transform: translate3d(0, -20px, 0);
		opacity: 0;
	}
}
.infinite-scroll__svg {
	transform-origin: center;
	animation: rotate 2s linear infinite;
	width: 50px;
	position: absolute;
	z-index: 0;
}
.infinite-scroll__circle {
	fill: none;
	stroke-width: 3;
	stroke-dasharray: 1, 200;
	stroke-dashoffset: 0;
	stroke-linecap: round;
	animation: dash 1.5s ease-in-out infinite;
	stroke: #a5a5a5;
}
@keyframes rotate {
	100% {
		transform: rotate(360deg);
	}
}
@keyframes dash {
	0% {
		stroke-dasharray: 1, 200;
		stroke-dashoffset: 0;
	}
	50% {
		stroke-dasharray: 90, 200;
		stroke-dashoffset: -35px;
	}
	100% {
		stroke-dashoffset: -125px;
	}
}
</style>

使用时:

<template>
	<div>
		<ImagePreview ref="imagePreviewRef"></ImagePreview>
	</div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import ImagePreview from "@/components/ImagePreview/Preview.vue";

const imagePreviewRef = ref();

const showImage = (urlList: string[], index: number) => {
    // 用来打开图片预览组件,传递一个图片组成的url数组,或者string类型的url
	imagePreviewRef.value.open(urlList);
    // 用来设置当前图片展示的下标
	imagePreviewRef.value.setImageIndex(index);
};
</script>

<style lang="scss" scoped></style>

好的,我可以为你提供一个基本的ElDialog封装,供你参考: ```vue <template> <el-dialog :title="title" :visible.sync="dialogVisible" :before-close="handleClose" :close-on-click-modal="false" :custom-class="customClass" :width="width" :lock-scroll="lockScroll" :modal-append-to-body="modalAppendToBody" :destroy-on-close="destroyOnClose" :center="center" @opened="handleOpen" @closed="handleClosed" v-bind="$attrs" v-on="$listeners" > <slot></slot> </el-dialog> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { ElDialog } from 'element-plus'; export default defineComponent({ name: 'MyDialog', props: { title: { type: String, default: '', }, dialogVisible: { type: Boolean, default: false, }, customClass: { type: String, default: '', }, width: { type: String, default: '50%', }, lockScroll: { type: Boolean, default: true, }, modalAppendToBody: { type: Boolean, default: true, }, destroyOnClose: { type: Boolean, default: false, }, center: { type: Boolean, default: true, }, }, emits: ['update:dialogVisible', 'opened', 'closed'], methods: { handleClose(done: () => void) { // 自定义关闭操作 done(); }, handleOpen() { this.$emit('opened'); }, handleClosed() { this.$emit('closed'); }, }, components: { ElDialog, }, }); </script> ``` 这里我们使用了Vue3的Composition API,使用`defineComponent`定义了一个组件,并引入了Element Plus的ElDialog组件。 我们将ElDialog组件的属性和事件通过props和emits暴露出来,并在组件内部进行了一些自定义操作,如自定义关闭操作和自定义事件触发。 你可以根据自己的需求对组件进行进一步封装和定制化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值