Vue3 預(yù)覽圖片和視頻

項(xiàng)目中遇到一組數(shù)據(jù)既有可能是圖片,也有可能是視頻违柏,需要同時(shí)預(yù)覽的情況为肮,搜了一下,找到了vue-gallery涡真,試了一下之后發(fā)現(xiàn)沒法在VUE3下沒法用,不知道是真的完全沒法用肾筐,還是因?yàn)槲矣玫腃omposition API才沒法用哆料,沒去糾結(jié)。

沒找到其他的局齿,只好自力更生剧劝,但是也沒有完全自力更生橄登。我留意到了Element Plus的Image組件是可以大圖預(yù)覽的抓歼,畢竟Element Plus是開源的,只要稍微改一下拢锹,對(duì)圖片和視頻資源做一個(gè)判斷谣妻,然后分別顯示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>

然后把圖片預(yù)覽的相關(guān)操作比如放大縮小旋轉(zhuǎn)等工具條在視頻的時(shí)候給隱藏充坑,把Element Plus的部分ts語法改成js减江,部分工具函數(shù)給拿出來染突,事件函數(shù)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的樣式份企,如果要單獨(dú)使用還得把這些樣式也給提取出來,因?yàn)槭莝css我的項(xiàng)目沒有用巡莹,要提取有點(diǎn)麻煩而且我本來就用的Element Plus司志,就沒弄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市降宅,隨后出現(xiàn)的幾起案子骂远,更是在濱河造成了極大的恐慌,老刑警劉巖腰根,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件激才,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡额嘿,警方通過查閱死者的電腦和手機(jī)贸营,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來岩睁,“玉大人钞脂,你說我怎么就攤上這事〔度澹” “怎么了冰啃?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)刘莹。 經(jīng)常有香客問我阎毅,道長(zhǎng),這世上最難降的妖魔是什么点弯? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任扇调,我火速辦了婚禮,結(jié)果婚禮上抢肛,老公的妹妹穿的比我還像新娘狼钮。我一直安慰自己,他們只是感情好捡絮,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布熬芜。 她就那樣靜靜地躺著,像睡著了一般福稳。 火紅的嫁衣襯著肌膚如雪涎拉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音鼓拧,去河邊找鬼半火。 笑死,一個(gè)胖子當(dāng)著我的面吹牛季俩,可吹牛的內(nèi)容都是我干的慈缔。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼种玛,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼藐鹤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赂韵,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤娱节,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后祭示,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肄满,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年质涛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了稠歉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汇陆,死狀恐怖怒炸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情毡代,我是刑警寧澤阅羹,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站教寂,受9級(jí)特大地震影響捏鱼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酪耕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一导梆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧迂烁,春花似錦看尼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至址芯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谷炸。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工北专, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旬陡。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓拓颓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親描孟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子驶睦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351