Vue3.0实现图片预览组件(媒体查看器)

这篇具有很好参考价值的文章主要介绍了Vue3.0实现图片预览组件(媒体查看器)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:最近项目中有个场景,一组图片、视频、音频、文件数据,要求点击图片可以放大预览,左右可以切换音视频、文件,支持鼠标及各种键控制 缩放,左右旋转,移动等功能,整理了一下,封了个组件,注释很全面,每块地方都有讲解,可以直接拿到项目中使用

先看下效果:

clg

关于传值:

(必传)传入url数组urlList,传入图片所处index,也就是在数组中的索引

(非必传)是否支持无限滚动?是否支持ESC键退出?是否支持点击遮罩层退出?是否需要工具栏?

关于图片的相关特效:

定义一个transform样式对象,包含缩放scale、旋转deg、移动offsetX|offsetY、动画enableTransition,可在computed计算中返回由此对象组成的css样式对象,在模板中对图片绑定,当去触发特效相关各类事件时,改变对象里某个值,则会重新捕获对象的改变执行computed,去实时更新图片样式

  1. 缩放操作: 可以通过鼠标滚动上下滑动、键盘上下键up/down、组件内部工具栏按钮这三种方式去控制此特效,定义一个缩放比,即每次缩放的程度,可以根据项目场景自行定义,我这里键盘控制、按钮点击为1.4,鼠标滚轮偏小为1.2,  每次缩放让初始化的样式对象的scale每次乘或除以这个值即可,当然无限缩小无限放大肯定不行,需要定义一个最小最大值控制

   2. 旋转操作: 定义一个旋转常量为90度,顺时针旋转让初始化样式对象deg累加这个值,逆时针相反即可

   3. 移动操作: 顾名思义,也就是在遮罩层内可以通过鼠标对图片进行移动,在鼠标按下事件内,监听鼠标移动事件,每次移动记录下当前鼠标位置,计算offsetX也就是图片要移动的translateX为:移动前的距页面左侧距离offsetX加上当前的鼠标位置event.pageX - 移动前的鼠标位置; 上下移动同理

   4. 动画过渡: 根据特效的触发方式决定是否需要过渡,我这里对通过鼠标操作缩放时没有定义动画,其余操作方式建议都要加上

关于图片初始化展示:

如果想要图片能够自适应在遮罩层的容器内,并且保证图片不变形且宽或高不溢出容器,那么就不应该定死宽高或者是不去定义宽高,我这里解决办法是对图片进行等比例的缩放,具体算法就不在这里过多讲解了,详情:https://blog.csdn.net/dabaooooq/article/details/128852363

关于音视频展示:

视频我这里用的是 vue3-video-play 这个插件,ui和功能各方面整体感觉很棒,算是对Vue 3.0支持比较好的一个吧,详情可以参考:https://codelife.cc/vue3-video-play/

音频用的是原生audio,用法很方便,没什么可讲的,具体看代码

关于抛出数据:

顶部中间一般为图片当前索引index/总长度,当然默认的为这样,抛出index,可以自定义这块地方插槽的使用,顶部左侧插槽也暴露当前index,其实当前组件最需要的数据也莫过于index,暴露方法中也基本都有抛出

附上完整代码文章来源地址https://www.toymoban.com/news/detail-408920.html

<template>
    <teleport to='body'>
        <transition>
            <div class="el-image-viewer__wrapper" :tabindex="-1" ref="imageViewer" :style="{ zIndex: computedZIndex }">
                <!-- MASK -->
                <div class="el-image-viewer__mask" @click.self="isClickToDisappear && hide"></div>
                <!-- CLOSE -->
                <span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
                    <el-icon>
                        <Close />
                    </el-icon>
                </span>
                <!-- HEADER -->
                <div class="image-viewer-header" v-if="isTools">
                    <div class="header-left">
                        <span class="header-file-name">
                            <slot name="title" :index="index"></slot>
                        </span>
                        <slot name="left" :index="index"></slot>
                    </div>
                    <div class="header-center">
                        <slot name="content" :index="index">
                            {{ index + 1 }} / {{ urlList.length }}
                        </slot>
                    </div>
                </div>
                <!-- ACTIONS -->
                <div class="el-image-viewer__btn el-image-viewer__actions"
                    v-if="isFileType(urlList[index]) === 'img' && isTools">
                    <div class="el-image-viewer__actions__inner">
                        <el-icon @click="handleActions('zoomOut')">
                            <ZoomOut />
                        </el-icon>
                        <el-icon @click="handleActions('zoomIn')">
                            <ZoomIn />
                        </el-icon>
                        <el-icon @click="toggleMode">
                            <component :is="icons.default[mode.icon]"></component>
                        </el-icon>
                        <el-icon @click="handleActions('anticlockwise')">
                            <RefreshLeft />
                        </el-icon>
                        <el-icon @click="handleActions('clockwise')">
                            <RefreshRight />
                        </el-icon>
                    </div>
                </div>
                <!-- ARROW -->
                <template v-if="!isSingle">
                    <span class="el-image-viewer__btn el-image-viewer__prev" @click="prev">
                        <el-icon>
                            <ArrowLeftBold />
                        </el-icon>
                    </span>
                    <span class="el-image-viewer__btn el-image-viewer__next" @click="next">
                        <el-icon>
                            <ArrowRightBold />
                        </el-icon>
                    </span>
                </template>
                <!-- CANVAS -->
                <div id="image-viewer-canvas" class="el-image-viewer__canvas">
                    <div v-for="(url, i) in urlList" :key="url" style="display: flex" :data-id="url">
                        <!-- 图片显示 -->
                        <img v-if="i === index && isFileType(url) === 'img'" v-loading="loading" :src="url"
                            :style="[mediaStyle, { width: 'auto', height: imgHeight }]" class="el-image-viewer__img"
                            @load="handleImgLoad" @error="handleMediaError" @mousedown="handleMouseDown" />
                        <!-- 视频显示 -->
                        <videoPlay v-if="(i === index && isFileType(url) === 'video')" :src="url"
                            class="el-image-viewer__img" @load="handleMediaLoad" @error="handlePlayError(i)"
                            @mousedown="handleMouseDown">
                        </videoPlay>
                        <!-- 音频显示 -->
                        <audio controls="controls" v-if="i === index && isFileType(url) === 'audio'" :src="url"
                            class="el-image-viewer__img" @load="handleMediaLoad" @error="handleMediaError"></audio>
                        <!-- 文本文件显示 -->
                        <div v-if="i === index && ['file', 'text'].includes(isFileType(url))" class="image-viewer-tips">
                            <span class="image-unknown-file-type-view"></span>
                            <p>
                                我们不能预览该文件。<br>
                                您要先下载文件以查看。
                            </p>
                            <div class="image-viewer-download" @click="download(url)">
                                <span class="icon-download"></span>
                                下载
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </transition>
    </teleport>
</template>
<script lang="ts">
import 'element-plus/es/components/image-viewer/style/css'
import "vue3-video-play/dist/style.css";
import * as icons from './mediaIcons'
import { videoPlay } from "vue3-video-play/dist/index.es";
import { fileType, transformImgRatio } from "@/utils/dataUtils"; // fileType 判断文件后缀方法; transformImgRatio 等比例计算图片宽高方法
import { isNumber, useEventListener } from '@vueuse/core'
import { PropType } from 'vue';
import { useZIndex } from 'element-plus';

export default defineComponent({
    name: 'imageViewerUtil',
    props: {
        urlList: { // url数组
            type: Array as PropType<string[]>,
            default: () => []
        },
        imgIndex: { // 当前文件所处位置,也是在数组中的索引
            type: Number,
            default: 0
        },
        isTools: { // 是否需要工具栏
            type: Boolean,
            default: true
        },
        isInfinite: { // 是否支持无限循环滚动
            type: Boolean,
            default: true
        },
        zIndex: { // 层级
            type: Number
        },
        closeOnPressEscape: { // 是否支持ESC键退出
            type: Boolean,
            default: true
        },
        isClickToDisappear: { // 是否支持通过点击遮罩层关闭
            type: Boolean,
            default: false
        }
    },
    emits: ['close', 'download', 'prevIndex', 'nextIndex'],
    setup(props, { emit }) {
        const global = getCurrentInstance().appContext.config.globalProperties
        const { nextZIndex } = useZIndex()
        const modes = { // 模式对象
            CONTAIN: {
                name: 'contain',
                icon: 'IconEpFullScreen',
            },
            ORIGINAL: {
                name: 'original',
                icon: 'IconEpScaleToOriginal',
            },
        }
        const mode = shallowRef(modes.ORIGINAL) // 模式
        const EVENT_CODE = { // 按钮对象
            left: 'ArrowLeft', // 37
            up: 'ArrowUp', // 38
            right: 'ArrowRight', // 39
            down: 'ArrowDown', // 40
            esc: 'Escape',
            space: 'Backspace'
        }
        const imageViewer = ref<HTMLDivElement>()
        const data = reactive({
            index: 0, // 图片索引,也是在数组中的位置
            loading: true, // 处理加载
            imgHeight: '', // 处理图片高
            transform: {
                scale: 1, // 缩放比
                deg: 0, // 旋转角度
                offsetX: 0,
                offsetY: 0,
                enableTransition: false // 是否需要过渡
            }
        })

        // 是否是火狐
        const isFirefox = (): boolean => /firefox/i.test(window.navigator.userAgent)
        // 鼠标滚轮事件,火狐的不同
        const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'


        const computedZIndex = computed(() => { // 计算z-index值
            return isNumber(props.zIndex) ? props.zIndex : nextZIndex()
        })
        const isSingle = computed(() => { // 是否是单张
            return props.urlList.length <= 1
        })
        const isFirst = computed(() => { // 是否是第一张
            return data.index === 0
        })
        const isLast = computed(() => { // 是否是最后一张
            return data.index === props.urlList.length - 1
        })
        const isFileType = computed(() => { // 判断文件类型
            return (url: string) => {
                return fileType(url.split('?')[0])
            }
        })
        const mediaStyle = computed(() => { // 图片css特效对象
            const { scale, deg, offsetX, offsetY, enableTransition } = data.transform
            let translateX = offsetX / scale
            let translateY = offsetY / scale

            switch (deg % 360) {
                case 90:
                case -270:
                    ;[translateX, translateY] = [translateY, -translateX]
                    break
                case 180:
                case -180:
                    ;[translateX, translateY] = [-translateX, -translateY]
                    break
                case 270:
                case -90:
                    ;[translateX, translateY] = [-translateY, translateX]
                    break
            }
            return {
                transform: `scale(${scale}) rotate(${deg}deg) translate(${translateX}px, ${translateY}px)`,
                transition: enableTransition ? 'transform .3s' : ''
            }
        })


        const handleImgLoad = (e) => { // 处理加载图片后的操作
            // 计算图片等比例缩放后的宽高
            const { clientWidth, clientHeight } = document.querySelector('#image-viewer-canvas') // 当前遮罩容器
            const { width, height } = transformImgRatio(e.target.width, e.target.height, clientWidth, clientHeight - 40) // 计算等比例缩放后的图片宽高
            data.imgHeight = height + 'px'
            data.loading = false
        }

        const hide = () => { // 关闭
            emit('close')
        }
        const prev = () => { // 上一张
            if (!props.isInfinite && !data.index) return ElMessage({ type: 'info', message: '已经是第一张了!' })
            data.index = (data.index - 1 + props.urlList.length) % props.urlList.length
            resetStyle()
            emit('prevIndex', data.index)
        }
        const next = () => { // 下一张
            if (!props.isInfinite && data.index === props.urlList.length - 1) return ElMessage({ type: 'info', message: '已经是最后一张了!' })
            data.index = (data.index + 1 + props.urlList.length) % props.urlList.length
            resetStyle()
            emit('nextIndex', data.index)
        }
        const keydownHandler = (e: event) => { // 键盘事件
            switch (e.code) {
                case EVENT_CODE.esc: // Escape
                    props.closeOnPressEscape && hide()
                    break;
                case EVENT_CODE.left: // ArrowLeft
                    prev()
                    break;
                case EVENT_CODE.right: // ArrowRight
                    next()
                    break;
                case EVENT_CODE.up: // ArrowUp
                    handleActions('zoomIn')
                    break;
                case EVENT_CODE.down: // ArrowDown
                    handleActions('zoomOut')
                    break;
                case EVENT_CODE.space: // Backspace
                    toggleMode()
                    break
            }
            e.preventDefault()
        }
        const mousewheelHandler = (e: WheelEvent | any /* TODO: wheelDelta is deprecated */) => { // 鼠标滚轮事件
            const delta = e.wheelDelta ? e.wheelDelta : -e.detail // 考虑Firefox
            if (delta > 0) { // 向上
                handleActions('zoomIn', { zooRate: 1.2, enableTransition: false })
            } else { // 向下
                handleActions('zoomOut', { zooRate: 1.2, enableTransition: false })
            }
        }
        const handleActions = (action: any, option = {}) => { // 各类指令操作
            const { zoomRate, rotateDeg, enableTransition } = { // 定义常规特效
                zoomRate: 1.4,
                rotateDeg: 90,
                enableTransition: true,
                ...option,
            }
            switch (action) {
                case 'zoomOut': // 缩小
                    if (data.transform.scale > 0.2) data.transform.scale = parseFloat((data.transform.scale / zoomRate).toFixed(3))
                    break;
                case 'zoomIn': // 放大
                    if (data.transform.scale < 6) data.transform.scale = parseFloat((data.transform.scale * zoomRate).toFixed(3))
                    break;
                case 'anticlockwise': // 逆时针旋转
                    data.transform.deg -= rotateDeg
                    break;
                case 'clockwise': // 顺时针旋转
                    data.transform.deg += rotateDeg
                    break;
            }
            data.transform.enableTransition = enableTransition
        }

        const resetStyle = () => { // 左右切换重置transform对象
            data.transform = { scale: 1, deg: 0, enableTransition: false }
        }
        const handleMediaLoad = (e: event) => { // 加载处理
            data.loading = false
        }
        const handleMediaError = () => { // 图片 音频失败处理
            data.loading = false
        }
        const handlePlayError = (index: number) => { // 视频失败处理
            props.urlList[index] = props.urlList[index].split('?')[0] + '?v=' + new Date().getTime()
        }
        const download = (url: string) => { // 文件类型抛出url
            emit('download', url)
        }
        const toggleMode = () => {
            if (data.loading) return
            const modeNames = Object.keys(modes)
            const modeValues = Object.values(modes)
            const currentMode = mode.value.name
            const index = modeValues.findIndex((i) => i.name === currentMode)
            const nextIndex = (index + 1) % modeNames.length
            mode.value = modes[modeNames[nextIndex]]
            resetStyle()
        }
        const handleMouseDown = (e: MouseEvent) => { // 处理鼠标按下事件
            data.transform.enableTransition = false
            const { offsetX, offsetY } = data.transform
            const startX = e.pageX
            const startY = e.pageY

            // 拖拽事件
            const dragHandler = (ev: MouseEvent) => {
                data.transform = {
                    ...data.transform,
                    offsetX: offsetX + ev.pageX - startX,
                    offsetY: offsetY + ev.pageY - startY,
                }
            }
            // 添加鼠标移动事件监听
            const removeMousemove = useEventListener(document, 'mousemove', dragHandler)
            useEventListener(document, 'mouseup', () => {
                removeMousemove()
            })
            e.preventDefault()
        }

        watch(() => props.imgIndex, () => {
            data.index = props.imgIndex
        }, { immediate: true })

        onMounted(() => {
            // 优化注册事件监听
            useEventListener(document, 'keydown', keydownHandler)
            useEventListener(document, mousewheelEventName, mousewheelHandler)

            imageViewer.value?.focus?.()
        })

        return {
            ...toRefs(data),
            computedZIndex,
            mediaStyle,
            isFileType,
            isSingle,
            isFirst,
            isLast,
            mode,
            icons,
            hide,
            prev,
            next,
            handleImgLoad,
            handleActions,
            handleMediaLoad,
            handlePlayError,
            handleMediaError,
            toggleMode,
            download,
            handleMouseDown
        }
    }
})
</script>
<style lang="scss" scoped>
:deep(.el-image-viewer__canvas) {
    //height: calc(100% - 180px);
    align-items: center;
    height: calc(100% - 40px);
}

:deep(.el-image-viewer__actions) {
    height: 40px;
    right: 50px;
    top: 0;
    left: auto;
    z-index: inherit;
    transform: none;
    padding: 0;
    background: none;
    border-radius: 0;
}

:deep(.el-image-viewer__actions__divider) {
    display: none;
}

:deep(.el-image-viewer__next) {
    width: 64px;
    height: 100px;
    background: transparent;
    border-radius: 6px 0px 0px 6px;
    opacity: 0.8;
    right: 0;
    top: 45%;

    .el-icon {
        color: #999999;
        font-size: 50px;
    }
}

:deep(.el-image-viewer__prev) {
    width: 64px;
    height: 100px;
    background: transparent;
    border-radius: 0px 6px 6px 0px;
    opacity: 0.8;
    left: 0;
    top: 45%;

    .el-icon {
        color: #999999;
        font-size: 50px;
    }
}

.el-image-viewer__next:hover,
.el-image-viewer__prev:hover {
    background: #000000;
    opacity: 1;
}

:deep(.el-image-viewer__mask) {
    opacity: 0.8;
}

:deep(.el-image-viewer__close) {
    height: 40px;
    z-index: 56;
    top: 0;
    right: 10px;
    background: transparent;
}

.image-viewer-carousel {
    height: 140px;
    background: #000000;
    z-index: 50;
    padding: 20px;
    position: relative;
    display: flex;
    flex-wrap: wrap;
}

.image-viewer-header {
    background-color: #000000;
    box-sizing: border-box;
    border-spacing: 0;
    width: 100%;
    height: 40px;
    z-index: 55;
    position: relative;
    z-index: 55;
    color: #FFFFFF;
    text-align: center;
    line-height: 40px;

    .header-left {
        position: absolute;
    }
}

.header-file-icon {
    width: 24px;
    height: 50px;
    display: inline-block;
    margin-left: 20px;
    background-image: url(../../../../assets/svg/image-icon.svg);
    background-repeat: no-repeat;
    background-position: 50% 50%;
    /*这个是按从左往右,从上往下的百分比位置进行调整*/
    background-size: 100% 60%;
    /*按比例缩放*/
}

.header-file-name {
    overflow: hidden;
    text-overflow: ellipsis;
    display: inline-block;
    vertical-align: top;
    margin: 0 20px;
}

.image-viewer-tips {
    background-color: rgba(0, 0, 0, 0.8);
    color: #fff;
    box-sizing: border-box;
    display: inline-block;
    vertical-align: middle;
    text-align: center;
    min-width: 490px;
    padding: 35px 100px;
    line-height: 2em;
    border-radius: 5px;
    z-index: 56;
}

.image-unknown-file-type-view {
    display: inline-block;
    width: 96px;
    height: 96px;
    background-size: contain;
    background: url(../../../../assets/svg/file-icon.svg);
    background-repeat: no-repeat;
    background-position: center center;
    background-size: contain;
}

.image-viewer-download {
    box-sizing: border-box;
    background: #f5f5f5;
    border: 1px solid #ccc;
    border-radius: 3.01px;
    color: #333;
    cursor: pointer;
    display: inline-block;
    font-family: inherit;
    font-size: 14px;
    font-variant: normal;
    font-weight: 400;
    height: 2.14285714em;
    line-height: 1.42857143;
    margin: 0;
    padding: 4px 10px;
    vertical-align: baseline;
    white-space: nowrap;
    text-decoration: none;
    margin: 20px 10px 0 10px;

    .icon-download {
        position: relative;
        top: 4px;
        background: url(../../../../assets/svg/download.svg) no-repeat 0 0;
        border: none;
        margin: 0;
        padding: 0;
        text-indent: -999em;
        vertical-align: text-bottom;
        display: inline-block;
        text-align: left;
        line-height: 0;
        position: relative;
        vertical-align: text-top;
        height: 16px;
        width: 16px;
    }
}
</style>

到了这里,关于Vue3.0实现图片预览组件(媒体查看器)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • vue3 element-plus 实现图片预览

    element-plus下有这么一个组件 el-image-viewer /,但是这个组件是没写在文档上面的,像普通组件一样使用即可 可以通过点击按钮实现图片预览,而非el-image组件只能通过点击图片实现预览 2.1封装组件 2.3组件使用 在需要使用的地方引入,然后使用即可,这不是重点,每个人使用的

    2024年02月15日
    浏览(58)
  • 记录--Vue3问题:如何实现组件拖拽实时预览功能?

    实现一个CMS内容管理系统,在后台进行内容编辑,在官网更新展示内容。 关于后台的编辑功能,大致分为两部分: 组件拖拽预览 、 组件内容编辑实时预览 。 对于组件拖拽预览,用户可以在含有各种功能组件的列表中,选择需要的组件进行拖拽。将组件拖拽到预览画布中后

    2024年02月04日
    浏览(43)
  • Vue3 实现文件预览 Word Excel pdf 图片 视频等格式 大全!!!!

    先上效果图    插件安装 先说 word 文件是docx-preview插件           excel文件是用 xlsx 插件     介绍后端返回的数据 因为在拦截器处 做了对数据的处理 最后你调接口拿到的数据是 一个对象 里面包含: url :  blob对象转换的用于访问 Blob 数据的临时链接。这个链接可以被用于

    2024年02月07日
    浏览(84)
  • vue2自定义封装图片预览组件

    前言:预览图片现在已经有成熟的组件了,比如element ui的图片预览功能,但是现实开发过程中,element ui图片预览已经不满足需求了,比如涉及预览时删除图片以及下载图片 自定义封装图片预览组件 功能:滚轮滚动图片放大、还原图片、左旋转、右旋转、上一张、下一张、删

    2024年01月18日
    浏览(59)
  • 在Vue3项目中使用VueCropper裁剪组件(裁剪及预览效果)

    某次看到后台系统中使用到了裁剪组件,感觉挺好玩的并且最近也在学Vue3和Ts,所以就研究了VueCropper组件,封装了裁剪组件,效果如下图。 npm i vue-cropper --save 代码如下(示例): import ‘vue-cropper/dist/index.css’ import { VueCropper } from ‘vue-cropper’ TipsDialog是我自己封装的dialog组

    2023年04月24日
    浏览(60)
  • vue3+element-plus点击列表中的图片预览时,图片被表格覆盖

    视觉 点击图片进行预览,但还能继续选中其他的图片进行预览,鼠标放在表格上,那一行表格也会选中,如图所示第一行的效果。 代码 官网文档有这么一个属性,官网是这个描述的Image属性。 在 el-image 中加入属性 preview-teleported=\\\"true\\\" 即可。

    2024年02月13日
    浏览(43)
  • 前端Vue图片上传组件支持单个文件多个文件上传 自定义上传数量 预览删除图片 图片压缩

    前端Vue图片上传组件支持单个文件多个文件上传 自定义上传数量 预览删除图片 图片压缩, 下载完整代码请访问uni-app插件市场址:https://ext.dcloud.net.cn/plugin?id=13099 效果图如下: 组件初始化 使用方法 HTML代码部分

    2024年02月09日
    浏览(69)
  • element ui vue 附件预览组件、可预览图片、excel 、pdf.word等文件(浏览器打开文件的方式)

    目录 1.组件源码  2.html 代码 3.组件源码  此组件就是一个单纯的预览图片、浏览器打开文件的形式简单的组合了下而成的,word、excel是直接下载、pdf浏览器打开的形式,如果想本地打开直接预览的话就直接不用看了。word、excel、pdf 的图片是我放到服务器上的图片地址。 1.组

    2024年02月11日
    浏览(71)
  • vue3 element组件上传图片

    在 Vue 3 中使用 Element 组件库进行图片上传,您需要使用 Element 的 Upload 组件。这个组件可以方便地实现文件上传功能,包括图片上传。 以下是一个简单的示例,演示如何在 Vue 3 中使用 Element 的 Upload 组件进行图片上传: 首先,确保您已经安装并配置了 Element 组件库。您可以

    2024年02月15日
    浏览(43)
  • vue3 el-upload 上传附件及预览 限制只能上传一个图片或者pdf格式的文件

    效果如图 直接看代码吧 template部分 css部分 js部分

    2024年02月11日
    浏览(80)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包