项目中遇到一组数据既有可能是图片,也有可能是视频,需要同时预览的情况,搜了一下,找到了vue-gallery,试了一下之后发现没法在VUE3下没法用,不知道是真的完全没法用,还是因为我用的Composition API才没法用,没去纠结。

没找到其他的,只好自力更生,但是也没有完全自力更生。我留意到了Element Plus的Image组件是可以大图预览的,毕竟Element Plus是开源的,只要稍微改一下,对图片和视频资源做一个判断,然后分别显示img和video不就可以了。于是我找到了Element Plus的image-viewer的源码,做了一下修改,核心的修改地方如上面所说的,加了判断和video

<div class="el-image-viewer__canvas">
	<img
		v-for="(url, i) in urlList"
		v-show="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-for="(url, i) in urlList"
		v-show="i === index && isVideo"
		ref="media"
		:key="url"
		:src="url"
		:style="mediaStyle"
		class="el-image-viewer__img"
		@load="handleMediaLoad"
		@error="handleMediaError"
		@mousedown="handleMouseDown"
	></video>
</div>

然后把图片预览的相关操作比如放大缩小旋转等工具条在视频的时候给隐藏,把Element Plus的部分ts语法改成js,部分工具函数给拿出来,事件函数on和off给重写下,就完事了,完整代码如下

<template>
    <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"
            >
                <i class="el-icon-close"></i>
            </span>
            <!-- ARROW -->
            <template v-if="!isSingle">
                <span
                    class="el-image-viewer__btn el-image-viewer__prev"
                    :class="{ 'is-disabled': !infinite && isFirst }"
                    @click="prev"
                >
                    <i class="el-icon-arrow-left"></i>
                </span>
                <span
                    class="el-image-viewer__btn el-image-viewer__next"
                    :class="{ 'is-disabled': !infinite && isLast }"
                    @click="next"
                >
                    <i class="el-icon-arrow-right"></i>
                </span>
            </template>
            <!-- ACTIONS -->
            <div
                v-if="isImage"
                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="el-image-viewer__canvas">
                <img
                    v-for="(url, i) in urlList"
                    v-show="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-for="(url, i) in urlList"
                    v-show="i === index && isVideo"
                    ref="media"
                    :key="url"
                    :src="url"
                    :style="mediaStyle"
                    class="el-image-viewer__img"
                    @load="handleMediaLoad"
                    @error="handleMediaError"
                    @mousedown="handleMouseDown"
                ></video>
            </div>
        </div>
    </transition>
</template>

<script>
import { computed, ref, onMounted, watch, nextTick } from 'vue'

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: 'el-icon-full-screen',
    },
    ORIGINAL: {
        name: 'original',
        icon: 'el-icon-c-scale-to-original',
    },
}

const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
const CLOSE_EVENT = 'close'
const SWITCH_EVENT = 'switch'

export default {
    name: 'MediaViewer',
    props: {
        urlList: {
            type: Array,
            default: () => [],
        },
        zIndex: {
            type: Number,
            default: 2000,
        },
        initialIndex: {
            type: Number,
            default: 0,
        },
        infinite: {
            type: Boolean,
            default: true,
        },
        hideOnClickModal: {
            type: Boolean,
            default: false,
        },
    },
    emits: [CLOSE_EVENT, SWITCH_EVENT],
    setup(props, { emit }) {
        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]
            return currentUrl.endsWith('.mp4')
        })

        const isImage = computed(() => {
            const currentUrl = props.urlList[index.value]
            return currentUrl.endsWith('.jpg') || currentUrl.endsWith('.png')
        })

        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()
            emit(CLOSE_EVENT)
        }

        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()
            emit(SWITCH_EVENT, 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?.()
        })

        return {
            index,
            wrapper,
            media,
            isSingle,
            isFirst,
            isLast,
            currentMedia,
            isImage,
            isVideo,
            mediaStyle,
            mode,
            handleActions,
            prev,
            next,
            hide,
            toggleMode,
            handleMediaLoad,
            handleMediaError,
            handleMouseDown,
        }
    },
}
</script>

使用

<teleport to="body">
	<MediaViewer
		v-if="previewState.isShow"
		:z-index="1000"
		:initial-index="previewState.index"
		:url-list="previewState.srcList"
		:hide-on-click-modal="true"
		@close="closeViewer"
	/>
</teleport>

大功告成
展示图片
展示视频

注意:我在里面直接用了Elment Plus的样式,如果要单独使用还得把这些样式也给提取出来,因为是scss我的项目没有用,要提取有点麻烦而且我本来就用的Element Plus,就没弄

Logo

前往低代码交流专区

更多推荐