组件代码内容 MediaViewer.vue
<template>
<teleport to="body">
<transition name="viewer-fade">
<div ref="wrapper" :tabindex="-1" class="el-image-viewer__wrapper" :style="{ zIndex }">
<div class="el-image-viewer__mask" @click.self="hideOnClickModal && hide()"></div>
<!-- CLOSE -->
<span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
<el-icon>
<Close />
</el-icon>
</span>
<!-- ARROW -->
<template v-if="!isSingle">
<span class="el-image-viewer__btn el-image-viewer__prev" :class="{ 'is-disabled': !infinite && isFirst }"
@click="prev">
<el-icon>
<ArrowLeft />
</el-icon>
</span>
<span class="el-image-viewer__btn el-image-viewer__next" :class="{ 'is-disabled': !infinite && isLast }"
@click="next">
<el-icon>
<ArrowRight />
</el-icon>
</span>
</template>
<!-- ACTIONS -->
<div v-if="isImage" class="el-image-viewer__btn el-image-viewer__actions">
<div class="el-image-viewer__actions__inner">
<el-icon @click="handleActions('zoomOut')">
<ZoomOut />
</el-icon>
<el-icon @click="handleActions('zoomIn')">
<ZoomIn />
</el-icon>
<i class="el-image-viewer__actions__divider"></i>
<el-icon @click="toggleMode">
<component :is="mode.icon"></component>
</el-icon>
<i class="el-image-viewer__actions__divider"></i>
<el-icon @click="handleActions('anticlocelise')">
<RefreshLeft />
</el-icon>
<el-icon @click="handleActions('clocelise')">
<RefreshRight />
</el-icon>
</div>
</div>
<!-- CANVAS -->
<div class="el-image-viewer__canvas">
<template v-for="(url, i) in urlList">
<img v-if="i === index && isImage" ref="media" :key="url" :src="url" :style="mediaStyle"
class="el-image-viewer__img" @load="handleMediaLoad" @error="handleMediaError"
@mousedown="handleMouseDown" />
<video controls="controls" v-if="i === index && isVideo" ref="media" :key="url" :src="url" :style="mediaStyle"
class="el-image-viewer__img" @load="handleMediaLoad" @error="handleMediaError"
@mousedown="handleMouseDown"></video>
</template>
</div>
</div>
</transition>
</teleport>
</template>
<script setup>
import { computed, ref, onMounted, watch, nextTick } from 'vue'
const props = defineProps({
urlList: {
type: Array,
default: () => [],
},
zIndex: {
type: Number,
default: 9999,
},
initialIndex: {
type: Number,
default: 0,
},
infinite: {
type: Boolean,
default: true,
},
hideOnClickModal: {
type: Boolean,
default: false,
}, // 视频格式
videoType: {
type: Array,
},
//图片格式
imgType: {
type: Array,
}
},)
const emits = defineEmits(['close', "switch"])
const EVENT_CODE = {
tab: 'Tab',
enter: 'Enter',
space: 'Space',
left: 'ArrowLeft', // 37
up: 'ArrowUp', // 38
right: 'ArrowRight', // 39
down: 'ArrowDown', // 40
esc: 'Escape',
delete: 'Delete',
backspace: 'Backspace',
}
const isFirefox = function () {
return !!window.navigator.userAgent.match(/firefox/i)
}
const rafThrottle = function (fn) {
let locked = false
return function (...args) {
if (locked) return
locked = true
window.requestAnimationFrame(() => {
fn.apply(this, args)
locked = false
})
}
}
const Mode = {
CONTAIN: {
name: 'contain',
icon: 'FullScreen',
},
ORIGINAL: {
name: 'original',
icon: 'ScaleToOriginal',
},
}
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
let _keyDownHandler = null
let _mouseWheelHandler = null
let _dragHandler = null
const loading = ref(true)
const index = ref(props.initialIndex)
const wrapper = ref(null)
const media = ref(null)
const mode = ref(Mode.CONTAIN)
const transform = ref({
scale: 1,
deg: 0,
offsetX: 0,
offsetY: 0,
enableTransition: false,
})
const isSingle = computed(() => {
const { urlList } = props
return urlList.length <= 1
})
const isFirst = computed(() => {
return index.value === 0
})
const isLast = computed(() => {
return index.value === props.urlList.length - 1
})
const currentMedia = computed(() => {
return props.urlList[index.value]
})
const isVideo = computed(() => {
const currentUrl = props.urlList[index.value]
const name = currentUrl.split('.').slice(-1)[0].toLocaleLowerCase()
const isVideo = props.videoType.find(itemVideo => itemVideo == name);
if (isVideo) {
return true
} else {
return false
}
})
const isImage = computed(() => {
const currentUrl = props.urlList[index.value]
const name = currentUrl.split('.').slice(-1)[0].toLocaleLowerCase()
const isImg = props.imgType.find(itemVideo => itemVideo == name);
if (isImg) {
return true
} else {
return false
}
})
const mediaStyle = 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`,
}
if (mode.value.name === Mode.CONTAIN.name) {
style.maxWidth = style.maxHeight = '100%'
}
return style
})
function hide() {
deviceSupportUninstall()
emits('close')
}
function deviceSupportInstall() {
_keyDownHandler = rafThrottle((e) => {
switch (e.code) {
// ESC
case EVENT_CODE.esc:
hide()
break
// SPACE
case EVENT_CODE.space:
toggleMode()
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
}
})
_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,
})
}
})
document.addEventListener('keydown', _keyDownHandler, false)
document.addEventListener(
mousewheelEventName,
_mouseWheelHandler,
false
)
}
function deviceSupportUninstall() {
document.removeEventListener('keydown', _keyDownHandler, false)
document.removeEventListener(
mousewheelEventName,
_mouseWheelHandler,
false
)
_keyDownHandler = null
_mouseWheelHandler = null
}
function handleMediaLoad() {
loading.value = false
}
function handleMediaError(e) {
loading.value = false
}
function handleMouseDown(e) {
if (loading.value || e.button !== 0) return
const { offsetX, offsetY } = transform.value
const startX = e.pageX
const startY = e.pageY
const divLeft = wrapper.value.clientLeft
const divRight =
wrapper.value.clientLeft + wrapper.value.clientWidth
const divTop = wrapper.value.clientTop
const divBottom =
wrapper.value.clientTop + wrapper.value.clientHeight
_dragHandler = rafThrottle((ev) => {
transform.value = {
...transform.value,
offsetX: offsetX + ev.pageX - startX,
offsetY: offsetY + ev.pageY - startY,
}
})
document.addEventListener('mousemove', _dragHandler, false)
document.addEventListener(
'mouseup',
(e) => {
const mouseX = e.pageX
const mouseY = e.pageY
if (
mouseX < divLeft ||
mouseX > divRight ||
mouseY < divTop ||
mouseY > divBottom
) {
reset()
}
document.removeEventListener(
'mousemove',
_dragHandler,
false
)
},
false
)
e.preventDefault()
}
function reset() {
transform.value = {
scale: 1,
deg: 0,
offsetX: 0,
offsetY: 0,
enableTransition: false,
}
}
function toggleMode() {
if (loading.value) return
const modeNames = Object.keys(Mode)
const modeValues = Object.values(Mode)
const currentMode = mode.value.name
const index = modeValues.findIndex((i) => i.name === currentMode)
const nextIndex = (index + 1) % modeNames.length
mode.value = Mode[modeNames[nextIndex]]
reset()
}
function prev() {
if (isFirst.value && !props.infinite) return
const len = props.urlList.length
index.value = (index.value - 1 + len) % len
}
function next() {
if (isLast.value && !props.infinite) return
const len = props.urlList.length
index.value = (index.value + 1) % len
}
function 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.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
}
transform.value.enableTransition = enableTransition
}
watch(currentMedia, () => {
nextTick(() => {
const $media = media.value
if (!$media.complete) {
loading.value = true
}
})
})
watch(index, (val) => {
reset()
emits('switch', val)
})
onMounted(() => {
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?.()
})
</script>
引用 VideoOrImagePreview.vue
<template>
<!-- 参数已element-plus图片预览组件相似 -->
<MediaViewer v-if="isShow" :url-list="realSrcList" :videoType="videoType" :imgType="imgType" @close="closeViewer" />
<div @click="openViewer" :style="`width:${realWidth};height:${realHeight};`" class="viewer">
<el-image v-if="isImage" class="viewer-img" fit="cover" :src="realSrc" />
<div v-if="isVideo" style="z-index: 0;position: relative;">
<video :src="realSrc" :width="width" style="z-index: -1;position: relative;" :height="height" controls
disablePictureInPicture="true" controlslist="nodownload noremoteplayback"></video>
</div>
</div>
</template>
<script setup>
import { isExternal } from "@/utils/validate";
import MediaViewer from "../MediaViewer"
const props = defineProps({
src: {
type: String,
default: ""
},
width: {
type: [Number, String],
default: ""
},
height: {
type: [Number, String],
default: ""
},
// 视频格式
videoType: {
type: Array,
default: ["avi", "wmv", "mpg", "mpeg", "mov", "rm", "ram", "swf", "flv", "mp4", "mp3", "wma", "avi", "rm", "rmvb", "flv", "mpg", "mkv"]
},
//图片格式
imgType: {
type: Array,
default: ['svgz', 'pjp', 'png', 'ico', 'avif', 'tiff', 'tif', 'jfif', 'svg', 'xbm', 'pjpeg', 'webp', 'jpg', 'jpeg', 'bmp', 'gif']
}
});
const isShow = ref(false)
// 默认显示第一张处理
const realSrc = computed(() => {
if (!props.src) {
return;
}
let real_src = props.src.split(",")[0];
if (isExternal(real_src)) {
return real_src;
}
return import.meta.env.VITE_APP_BASE_API + real_src;
});
// 判断第一张是否为图片
const isImage = computed(() => {
if (!realSrc.value) {
return;
}
const name = realSrc.value.split('.').slice(-1)[0].toLocaleLowerCase()
const isImg = props.imgType.find(itemVideo => itemVideo == name);
if (isImg) {
return true
} else {
return false
}
})
// 判断第一张是否为视频
const isVideo = computed(() => {
if (!realSrc.value) {
return;
}
const name = realSrc.value.split('.').slice(-1)[0].toLocaleLowerCase()
const isVideo = props.videoType.find(itemVideo => itemVideo == name);
if (isVideo) {
return true
} else {
return false
}
})
const realSrcList = computed(() => {
if (!props.src) {
return;
}
let real_src_list = props.src.split(",");
let srcList = [];
real_src_list.forEach(item => {
if (isExternal(item)) {
return srcList.push(item);
}
return srcList.push(import.meta.env.VITE_APP_BASE_API + item);
});
return srcList;
});
const realWidth = computed(() =>
typeof props.width == "string" ? props.width : `${props.width}px`
);
const realHeight = computed(() =>
typeof props.height == "string" ? props.height : `${props.height}px`
);
// 关闭预览
function closeViewer() {
isShow.value = false
}
//打开预览
function openViewer() {
isShow.value = true
}
</script>
<style lang="scss" scoped>
.viewer {
cursor: pointer;
border-radius: 4px;
overflow: hidden;
.viewer-img {
width: 100%;
height: 100%;
transition: all 0.3s;
}
.viewer-img:hover {
transform: scale(1.2);
}
}
</style>
/**
* 判断path是否为外链
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
使用文章来源:https://www.toymoban.com/news/detail-763063.html
<template>
<videoorimage-preview v-if="arr" :src="arr" :width="50" :height="50" />
</template>
注:element-plus,vue3文章来源地址https://www.toymoban.com/news/detail-763063.html
到了这里,关于vue实现预览图片及视频组件的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!