关于vue.js:Vue3-预览图片和视频

1次阅读

共计 8660 个字符,预计需要花费 22 分钟才能阅读完成。

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

正文完
 0