共计 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,就没弄
正文完