Vue3 预览图片和视频

项目中遇到一组数据既有可能是图片,也有可能是视频,需要同时预览的情况,搜了一下,找到了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,就没弄

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
引用\[1\]提到了一个场景,即在一个项目需要实现一个图片预览组件,支持放大预览、切换音视频和文件、缩放、旋转、移动等功能。该组件已经封装好,注释详细,可以直接拿来使用。引用\[2\]提到了一个适用于Vue 3.0的视频播放插件vue3-video-play,该插件的UI和功能都很好,可以作为实现视频播放功能的参考。引用\[3\]提到了定义一个transform样式对象,包含缩放、旋转、移动等属性,可以在computed计算属性返回一个由该对象组成的CSS样式对象,然后在模板将该样式对象绑定到图片上,当触发特效相关的事件时,改变对象的某个值,就会重新计算computed属性,从而实时更新图片的样式。 综合以上引用内容,可以根据项目需求使用已封装好的图片预览组件,并结合vue3-video-play插件实现视频播放功能。同时,可以定义一个transform样式对象,通过computed属性实时更新图片的样式,以实现缩放、旋转、移动等效果。 #### 引用[.reference_title] - *1* *2* *3* [Vue3.0实现图片预览组件(媒体查看器)](https://blog.csdn.net/dabaooooq/article/details/128841487)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值