elementPlus 封装 了查看 图片的组件 el-image-viewer 。现有需求 要求可以查看视频。
el-image-viewer 组件在 项目中 已经是被打包过了 非常不利于修改。
去官网拿到 el-image-viewer 组件 稍作修改。( 末尾有我自己修改好的 )
<div class="el-image-viewer__canvas">
<template v-for="(file, i) in urlList" :key="i">
<img
v-if="i == index && file.type == 'image'"
ref="media"
:src="file.url"
:style="mediaStyle"
class="el-image-viewer__img"
@load="handleMediaLoad"
@error="handleMediaError"
@mousedown="handleMouseDown"
/>
<video
controls="controls"
v-if="i == index && file.type == 'video'"
ref="media"
:poster="file.url"
:style="mediaStyle"
class="el-image-viewer__img"
@load="handleMediaLoad"
@error="handleMediaError"
@mousedown="handleMouseDown"
>
<source :src="file.videoUrl" type="video/mp4" />
</video>
</template>
</div>
隐藏 video 时 旋转操作
<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">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"
></path>
</svg>
</i>
</span>
<span
class="el-image-viewer__btn el-image-viewer__next"
:class="{ 'is-disabled': !infinite && isLast }"
@click="next"
>
<i class="el-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"
></path>
</svg>
</i>
</span>
</template>
判断一下
const isSingle = computed(() => {
const { urlList } = props
console.log(urlList, 'urlList')
urlList.forEach((item) => {
if (!item.type) {
item.type = item.response.type
if (item.response.thumbnailUrl) {
item.videoUrl = item.response.thumbnailUrl
}
}
})
return urlList.length <= 1
})
最后就组件就修改好了 (我封装的名称为 MediaViewer)
以下为完整代码 里面的icon 我已经替换成 svg 可直接复制,文件名为MediaViewer.vue 你也可根据最新的vue3.4 语法进行修改。
<template>
<transition name="viewer-fade">
<div
ref="wrapper"
:tabindex="-1"
class="el-image-viewer__wrapper"
:style="{ zIndex }"
>
<div class="mask" @click.self="hideOnClickModal && hide()"></div>
<!-- CLOSE -->
<span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
<i class="el-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"
></path>
</svg>
</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">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"
></path>
</svg>
</i>
</span>
<span
class="el-image-viewer__btn el-image-viewer__next"
:class="{ 'is-disabled': !infinite && isLast }"
@click="next"
>
<i class="el-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"
></path>
</svg>
</i>
</span>
</template>
<!-- ACTIONS -->
<div
class="el-image-viewer__btn el-image-viewer__actions"
v-if="urlList[index].type != 'video'"
>
<div class="el-image-viewer__actions__inner">
<i class="el-icon" @click="handleActions('zoomOut')"
><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704M352 448h256a32 32 0 0 1 0 64H352a32 32 0 0 1 0-64"
></path></svg
></i>
<i class="el-icon" @click="handleActions('zoomIn')"
><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704m-32-384v-96a32 32 0 0 1 64 0v96h96a32 32 0 0 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64z"
></path></svg
></i>
<i class="el-image-viewer__actions__divider"></i>
<i class="el-icon" @click="toggleMode"
><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="m160 96.064 192 .192a32 32 0 0 1 0 64l-192-.192V352a32 32 0 0 1-64 0V96h64zm0 831.872V928H96V672a32 32 0 1 1 64 0v191.936l192-.192a32 32 0 1 1 0 64zM864 96.064V96h64v256a32 32 0 1 1-64 0V160.064l-192 .192a32 32 0 1 1 0-64l192-.192zm0 831.872-192-.192a32 32 0 0 1 0-64l192 .192V672a32 32 0 1 1 64 0v256h-64z"
></path></svg
></i>
<i class="el-image-viewer__actions__divider"></i>
<i class="el-icon" @click="handleActions('anticlocelise')"
><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M289.088 296.704h92.992a32 32 0 0 1 0 64H232.96a32 32 0 0 1-32-32V179.712a32 32 0 0 1 64 0v50.56a384 384 0 0 1 643.84 282.88 384 384 0 0 1-383.936 384 384 384 0 0 1-384-384h64a320 320 0 1 0 640 0 320 320 0 0 0-555.712-216.448z"
></path></svg
></i>
<i class="el-icon" @click="handleActions('clocelise')"
><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M784.512 230.272v-50.56a32 32 0 1 1 64 0v149.056a32 32 0 0 1-32 32H667.52a32 32 0 1 1 0-64h92.992A320 320 0 1 0 524.8 833.152a320 320 0 0 0 320-320h64a384 384 0 0 1-384 384 384 384 0 0 1-384-384 384 384 0 0 1 643.712-282.88z"
></path></svg
></i>
</div>
</div>
<!-- CANVAS -->
<div class="el-image-viewer__canvas">
<template v-for="(file, i) in urlList" :key="i">
<img
v-if="i == index && file.type == 'image'"
ref="media"
:src="file.url"
:style="mediaStyle"
class="el-image-viewer__img"
@load="handleMediaLoad"
@error="handleMediaError"
@mousedown="handleMouseDown"
/>
<video
controls="controls"
v-if="i == index && file.type == 'video'"
ref="media"
:poster="file.url"
:style="mediaStyle"
class="el-image-viewer__img"
@load="handleMediaLoad"
@error="handleMediaError"
@mousedown="handleMouseDown"
>
<source :src="file.videoUrl" type="video/mp4" />
</video>
</template>
</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: 'ElMediaViewer',
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
})
// 处理 video 有video 时 字段
const isSingle = computed(() => {
const { urlList } = props
console.log(urlList, 'urlList')
urlList.forEach((item) => {
if (!item.type) {
item.type = item.response.type
if (item.response.thumbnailUrl) {
item.videoUrl = item.response.thumbnailUrl
}
}
})
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>
<style lang="scss" scoped>
.mask {
background-color: rgba($color: #1a1c16, $alpha: 0.5);
height: 100%;
left: 0;
opacity: 0.5;
position: absolute;
top: 0;
width: 100%;
}
.el-icon {
z-index: 200;
}
.el-image-viewer__btn {
overflow: hidden;
border-radius: 100px;
opacity: 1;
text-align: center;
line-height: 44px;
background-color: rgba($color: #0d0d0d, $alpha: 0.5);
}
</style>
再次封装 : 再 components 文件中新建 一个 upload 文件
子文件夹 :MediaViewer.vue (刚刚保存的文件) 和 index.vue
index.vue 代码:
<template>
<div>
<el-upload
class="avatar-uploader"
:show-file-list="true"
:action="action"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:accept="accept"
v-model:file-list="fileList"
:list-type="type"
:on-preview="handlePictureCardPreview"
>
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="demo-image__preview">
<el-media-viewer
v-if="dialogVisible"
ref="viewer"
:url-list="fileList"
:initial-index="imageViewIndex"
:hide-on-click-modal="true"
@close="close"
:teleported="true"
/>
</div>
<!-- <el-dialog v-model="dialogVisible">
<img style="" :src="imageUrl" alt="Preview Image" />
</el-dialog> -->
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { netConfig } from '@/config/net.config'
import ElMediaViewer from './MediaViewer.vue'
const { baseURL } = netConfig
const action = 'api/upload'
const imageUrl = ref('')
const props = defineProps({
//传入的预加载图片地址
modelValue: {
type: [Array, String, Object]
},
//三种模式 'list' | 'path' | 'path_name'
mode: {
type: String,
default: 'list'
},
//mode='list'时的属性
fileList: {
type: [Array, String]
},
//mode='path'或'path_name'时的属性
fileUrl: {
type: String,
default: ''
},
showUrl: {
type: String,
default: ''
},
fileName: {
type: String,
default: '附件'
},
// 文件列表的类型 'text' | 'picture' | 'picture-card'
type: {
type: String,
default: 'picture-card'
},
//接受文件类型
accept: {
type: String,
default:
'.jpeg,.jpg,.png,.bmp,.gif,.doc,.docx,.xls,.xlsx,.pdf,.zip,.mp4,.avi,.wmv,.mov'
},
//是否可以多选
multiple: {
type: Boolean,
default: true
},
//允许上传的数量
limit: {
type: Number,
default: 2
},
//是否只读
readonly: {
type: Boolean,
default: false
},
btnType: {
type: String,
default: ''
},
size: {
type: String,
default: 'default'
},
showTip: {
type: Boolean,
default: false
},
seal: {
type: Boolean,
default: false
},
showVideoTip: {
type: Boolean,
default: false
},
video: {
type: Boolean,
default: false
},
imageType: {
type: Boolean,
default: false
}
})
const fileList = ref([
{
type: 'image',
name: 'food.jpeg',
url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'
},
{
type: 'image',
name: 'foods.jpeg',
url: 'https://so.gitee.com/static/logo.svg'
}
])
watch(
() => fileList.value,
() => {
fileList.value.forEach((item) => {
if (item.response) {
item.url = item.response.url
}
})
},
{
deep: true
}
)
// 上传的方法
const handleAvatarSuccess = (data) => {
console.log(data)
}
const beforeAvatarUpload = (data) => {
console.log(data)
}
// 点击图片 开启预览 默认第几个
//
const imageViewIndex = ref(1)
const dialogVisible = ref(false)
const handlePictureCardPreview = (file) => {
console.log(file)
imageViewIndex.value = fileList.value?.findIndex(({ url }) => file.url == url)
imageUrl.value = file.url
dialogVisible.value = true
}
// 关闭预览
const close = () => {
dialogVisible.value = false
}
</script>
last 在文件中使用:
<template>
<div>
<upload />
<!--
<upload v-model="list" /> 具体看自己怎么封装 index.vue 中的参数
比如 readonly or type类型是 附件 还是 媒体。 以及上传的数量,这都不懂得话请留言。。
-->
</div>
</template>
<script setup>
import upload from './components/upload/index.vue'
// const list = ref([]);
// or
const datas = reactive({
list:[],
type:'image',
limt:10,
})
</script>
效果图
这个缩略图是我用go gin 写的 想要直接 clone ( 可自行下载 go 环境 )前端现在就完成了。 go 语法和js 部分是相似的 我自己弄起来比较好弄一点。
https://gitee.com/yueqibin/gingorm.git
只想了解上传可 将红色全部 注掉 参数删掉
将 有关数据库 的(圈红部分全部删掉) 保存后 运营 go run mian.go
以上技术为 vue3 elementPlus go语言 gin 框架 gorm数据管理
若想研究其他语法 比如连接数据库 返回实例 sql 怎么写? 欢迎留言 // 你看不吧??嗯?