Vue3 Element Plus Upload 组件封装实践

531 阅读6分钟

在 Vue3 + Element Plus 项目中,文件上传是常见的业务需求。然而原生的 ElUpload 组件虽然功能强大,但在实际项目中往往需要大量的重复配置和样式调整。本文将分享如何基于 Element Plus 的 Upload 组件进行深度封装,打造一个开箱即用、高可配置的上传组件。

屏幕录制-2025-09-05-161803.gif

一、封装背景与痛点

在实际项目开发中,我们经常遇到以下痛点:

  1. 重复配置:每个上传场景都需要重复配置 headers、action 等基础参数
  2. 样式统一困难:不同页面的上传样式难以保持一致
  3. 文件类型校验复杂:需要手动编写各种文件类型和大小的校验逻辑
  4. 用户体验不佳:缺少统一的加载、错误提示和预览功能

二、组件设计思路

2.1 核心设计原则

  • 高度可配置:通过 props 暴露常用配置项
  • 样式统一:内置多种预设样式,支持自定义
  • 业务解耦:将上传逻辑封装在组件内部
  • 类型安全:充分利用 TypeScript 的类型推断

2.2 功能特性

我们的封装组件 LeUpload 具备以下特性:

文件类型校验:支持图片、指定扩展名、所有文件类型
大小限制:可配置单个文件大小限制
数量限制:支持最大上传数量限制
自定义图标:根据文件类型自动显示对应图标
预览功能:支持图片预览和文件下载
尺寸:支持 small、default、large 三种尺寸

三、技术实现详解

3.1 组件 Props 设计

const props = defineProps({  
    value: {  
        type: Array as PropType<UploadFile[]>  
    },  
    accept: {  
        type: String,  
        description: '接受的文件类型,支持 MIME 类型和扩展名'  
    },  
    fileType: {  
        type: String as PropType<'all' | 'image' | 'fileExt'>,  
        default: 'all',  
        description: '文件类型:all-所有、image-图片、fileExt-指定扩展名'  
    },  
    fileLimit: {  
        type: Number,  
        default: 10,  
        description: '文件上传大小限制(MB)'  
    },  
    limit: {  
        type: Number,  
        description: '最大上传数量'  
    },  
    text: {  
        type: String,  
        default: '上传'  
    },  
    uploadUrl: {  
        type: String,  
        default: () => `${import.meta.env.VITE_APP_BASE_API}/file/upload`  
    },  
    tips: {  
        type: String,  
        description: '上传提示文字'  
    },  
    multiple: {  
        type: Boolean,  
        default: true  
    },  
    disabled: {  
        type: Boolean,  
        default: false  
    },  
    removeConfirm: {  
        type: Boolean,  
        default: false,  
        description: '删除时是否显示确认框'  
    },  
    size: {  
        type: String as PropType<'small' | 'default' | 'large'>,  
        default: 'large'  
    }  
})  

3.2 智能文件类型识别

组件内置了智能的文件类型识别功能,根据文件扩展名自动显示对应的图标:

const bindProps = computed(() => {  
    const bind: any = Object.assign({ name: 'file' }, props, attrs)  

    // 自动设置图片模式的 accept  
    if (bind.fileType === 'image' && !bind.listType) {  
        bind.listType = 'picture-card'  
    }  
    if (bind.listType === 'picture-card' && !bind.accept) bind.accept = 'image/*'  

    // 自定义图标渲染  
    if (!bind.iconRender) {  
        bind.iconRender = ({ file }) => {  
            const isLoading = file.status === 'uploading'  
            const ext = getFileExt(file.url)  
            let type = 'ep:document'  

            if (isLoading) type = 'eos-icons:bubble-loading'  
            else if (isImageByExt(file.url)) return <img class="local_icon-image" src={file.url} />  
            else if (['.pdf', '.ppt', '.pptx'].includes(ext)) type = 'vscode-icons:file-type-powerpoint2'  
            else if (['.doc', '.docx'].includes(ext)) type = 'vscode-icons:file-type-word'  
            else if (['.xls', '.xlsx'].includes(ext)) type = 'vscode-icons:file-type-excel'  

            return <LeIcon icon={type} />  
        }  
    }  
    return bind  
})  

3.3 文件上传前置校验

在上传前进行全面的文件校验,包括类型、大小等:

function defaultBeforeUpload(file: UploadRawFile) {  
    let bool = true  
    const fileType = props.fileType  

    // 图片类型校验  
    if (bindProps.value.accept === 'image/*') {  
        if (!file.type!.includes('image')) {  
            ElMessage.warning(t('le.el.upload.acceptImage'))  
            bool = false  
        }  
    }  
    // 扩展名校验  
    else if (fileType === 'fileExt') {  
        const fileSuffix = getFileExt(file.name)  
        if (props.accept) {  
            const accepts = getAccepts(props.accept)  
            if (!accepts.includes(fileSuffix)) {  
                ElMessage.warning(t('le.el.upload.acceptUpload', [accepts.join(',')]))  
                bool = false  
            }  
        }  
    }  
  
    // 文件大小校验  
    if (bool && props.fileLimit && file.size / 1024 / 1024 > props.fileLimit) {  
        ElMessage.warning(t('le.el.upload.maxSize', [props.fileLimit]))  
        bool = false  
    }  

    return bool  
}  

3.4 统一的事件处理

组件统一处理了上传过程中的各种状态变化:

function handleChange(file: UploadFile, fileList: UploadFile[]) {  
    const status = file.status  

    if (status === 'ready') emit('fileChange', file, 'uploading')

    if (['success', 'fail'].includes(status as string)) {
        const idx = getFileIdx(file, fileList)  
        if (idx === -1) return  

        if (file.status === 'success') {
            // 接口上传返回结果
            const { code, data, msg } = file.response  
            let emitStatus: EmitStatus = 'success'
            if (code !== 200) {
                emitStatus = 'fail'  
                ElMessage.error(msg || t('le.el.upload.uploadErrorTip'))  
                fileList.splice(idx, 1)  
            } else {  
                fileList[idx].url = data  
            }
            emit('fileChange', file, emitStatus)
        } else {  
            ElMessage.error(t('le.el.upload.uploadErrorTip', [file.name]))  
            fileList.splice(idx, 1)  
            emit('fileChange', file, 'fail')  
        }  
    }  

    emitValue(fileList)  
}  

3.5 默认文件上传超出限制

const handleExceed: UploadProps['onExceed'] = (files: File[], uploadFiles: UploadUserFile[]) => {  
    // 选中的文件数  
    const selectedNum = files.length  
    // 可以新增的最大文件数  
    const addNum = props.limit - selectedNum  
    if (addNum <= 0) {  
        // 超过最大上传数量 清空已上传文件  
        uploadRef.value.clearFiles()  
        // 保留 后面选中的数据  
        files.splice(0, Math.abs(addNum))  
    } else {  
        // 可以保留的 已上传文件  
        const leftNum = uploadFiles.length - addNum  
        if (leftNum > 0) uploadFiles.splice(0, leftNum)  
    }  
    files.forEach(_file => {  
        const file = _file as UploadRawFile  
        file.uid = getUid()  
        uploadRef.value!.handleStart(file)  
    })  
    // 如果有文件 则提交上传  
    if (files.length) uploadRef.value!.submit()  
}

3.6 文件预览与下载

集成了图片预览和文件下载功能:

// 文件预览  
function handlePreview(file) {  
    // 如果是图片类型
    if (file.type?.indexOf('image') >= 0 || isImageByExt(file.url)) {  
        createImgPreview({ imageList: [file.url], maskClosable: true })  
    } else {  
        window.open(file.url)  
    }  
}  
  
// 文件下载  
function handleDownload(file: UploadFile) {  
    commonDownload(file.url as string, file.name)  
}  

四、使用示例

4.1 基础用法

<template>  
<LeUpload v-model:value="fileList" />  
</template>  
  
<script setup>  
import { ref } from 'vue'  
  
const fileList = ref([])  
</script>  

4.2 图片上传模式

<LeUpload  
    v-model:value="fileList"  
    fileType="image"  
    :fileLimit="2"  
    tips="只能上传jpg/png格式图片,且不超过2MB"  
/>  

4.3 指定文件类型

<LeUpload  
    v-model:value="fileList"  
    fileType="fileExt"  
    accept=".pdf,.doc,.docx"  
    :fileLimit="10"  
    tips="请上传PDF、Word文档"  
/>  

4.4 自定义尺寸和样式

<LeUpload  
    v-model:value="fileList"  
    size="small"  
    listType="picture-card"  
/>  

五、完整代码

<script lang="tsx" setup>  
import type { UploadFile, UploadRawFile, UploadInstance, UploadProps } from 'element-plus'  
import { ElIcon, ElMessageBox, ElProgress, ElUpload, UploadUserFile } from 'element-plus'  
import { computed, ref, useAttrs } from 'vue'  
import type { EmitStatus } from './index'  
import { commonDownload, t } from '@/utils'  
import LeIcon from '@/components/Icon.vue'  
import { ElMessage } from 'element-plus'  
import { createImgPreview } from '@/components/Preview/index'  
import { getHeaders } from '@/utils/request'  
import { getFileExt, getUid, isImageByExt } from '@/utils/file'  
import { useNamespace } from '@/hooks/useNameSpace'  
  
defineOptions({ name: 'LeUpload' })  
  
const props = defineProps({  
    value: {  
        type: Array as PropType<UploadFile[]>  
    },  
    // 接受的文件类型  
    accept: {  
        /**  
        * 通过,拼接的 文件类型  
        * eg:  
        * 1. 以.开头的合法文件名扩展名 如: .jpg、.pdf 或 .doc  
        * 2. MIME 类型字符串 如: image/png、audio/webm (https://developer.mozilla.org/zh-CN/docs/Web/HTTP/MIME_types)  
        * 3. 字符串 audio/*,表示“任何音频文件”  
        * 4. 字符串 video/*,表示“任何视频文件”  
        * 5. 字符串 video/*,表示“任何视频文件”  
        * 6.字符串 image/*,表示“任何图片文件”  
        * 7. 通配符 * 匹配所有类型  
        *  
        */  
        type: String  
    },  
    fileType: {  
        /**  
        * image: 默认所有图片  
        * fileExt: 传递具体.xxx(.文件扩展名) 校验需要配置 `accept` 配合使用 如: .jpg,.pdf 或 .doc  
        * all: 所有  
        */  
        type: String as PropType<'all' | 'image' | 'fileExt'>,  
        default: 'all'  
    },  
    // 文件上传大小限制(MB)  
    fileLimit: {  
        type: Number,  
        default: 10  
    },  
    text: {  
        type: String,  
        // default: '上传'  
        default: t('le.el.upload.upload')  
    },  
    uploadUrl: {  
        type: String,  
        default: `${import.meta.env.VITE_APP_BASE_API}/file/upload`  
    },  
    // 提示  
    tips: {  
        type: String  
    },  
    // 最大上传数量  
    limit: {  
        type: Number  
    },  
    multiple: {  
        type: Boolean,  
        default: true  
    },  
    disabled: {  
        type: Boolean,  
        default: false  
    },  
    // 删除时是否显示确认框  
    removeConfirm: {  
        type: Boolean,  
        default: false  
    },  
    size: {  
        type: String as PropType<'small' | 'default' | 'large'>,  
        default: 'large'  
    }  
})  
const emit = defineEmits<{  
    change: [value: UploadFile[]]  
    'update:value': [value: UploadFile[]]  
    fileChange: [file: UploadFile, status: EmitStatus]  
}>()  
const { prefixCls } = useNamespace('upload')  
console.error(prefixCls, 'prefixCls')  
  
const attrs = useAttrs()  
const headers = getHeaders()  
// refs  
const uploadRef = ref<UploadInstance>()  
// 是否达到了最大上传数量  
const isLimit = computed(() => props.limit > 0 && props.value?.length >= props.limit)  
// 合并 props 和 attrs  
const bindProps = computed(() => {  
    const bind: any = Object.assign({ name: 'file' }, props, attrs)  
    bind.data = { biz: 'temp', ...bind.data }  
    if (!bind.listType) {  
        if (bind.fileType === 'image') {  
            // picture text picture-card  
            bind.listType = 'picture-card'  
        } else {  
            bind.listType = 'text'  
        }  
    }  
    if (bind.listType === 'picture-card' && !bind.accept) bind.accept = 'image/*'  
    // 用于 渲染自定义图标  
    if (!bind.iconRender) {  
        bind.iconRender = ({ file }) => {  
            const isLoading = file.status === 'uploading'  
            const ext = getFileExt(file.url)  
            let type = 'ep:document'  
            if (isLoading) type = 'eos-icons:bubble-loading'  
            else if (isImageByExt(file.url)) return <img class="local_icon-image" src={file.url} />  
            else if (['.pdf', '.ppt', '.pptx'].includes(ext)) type = 'vscode-icons:file-type-powerpoint2'  
            else if (['.doc', '.docx'].includes(ext)) type = 'vscode-icons:file-type-word'  
            else if (['.xls', '.xlsx'].includes(ext)) type = 'vscode-icons:file-type-excel'  
            return <LeIcon icon={type} />  
        }  
    }  
    return bind  
})  
// 当前是否是上传图片模式  
const isPictureCard = computed(() => bindProps.value.listType === 'picture-card')  
    function getAccepts(accept: string) {  
    return accept.replace(/\s/g, '').split(',')  
}  
  
// 默认 文件上传之前的操作 若有 其他判断 建议自行传beforeUpload 自定义  
function defaultBeforeUpload(file: UploadRawFile) {  
    let bool = true  
    // console.error('defaultBeforeUpload', file, JSON.stringify(fileList))  
    const fileType = props.fileType  
    if (bindProps.value.accept === 'image/*') {  
        // 当前是否是上传图片模式  
        if (!file.type!.includes('image')) {  
            ElMessage.warning(t('le.el.upload.acceptImage'))  
            bool = false  
        }  
    } else if (fileType === 'fileExt') {  
        const fileSuffix = getFileExt(file.name)  
        if (props.accept) {  
            const accepts = getAccepts(props.accept)  
            if (!accepts.includes(fileSuffix)) {  
                // 只能上传 {.ppt,.pptx,.doc,.docx,.xls,.xlsx,.pdf,.txt,.jpg,.jpeg,.png,.gif,.bmp}格式文件  
                ElMessage.warning(t('le.el.upload.acceptUpload', [accepts.join(',')]))  
                bool = false  
            }  
        }  
    }  
    if (bool && props.fileLimit && file.size / 1024 / 1024 > props.fileLimit) {  
        // 单个文件大小不超过{0}MB  
        ElMessage.warning(t('le.el.upload.maxSize', [props.fileLimit]))  
        bool = false  
    }  
    return bool  
}  

// 用于 file slot 中的删除  
async function handleRemove(file: UploadFile) {  
    const before = await beforeRemove() // fileList  
    if (before) {  
    const fileList = props.value || []  
    const idx = fileList.findIndex(f => f.uid === file.uid)  
    idx >= 0 && fileList.splice(idx, 1)  
    }  
}  
  
// 删除处理事件  
function beforeRemove(/* file: UploadFile, uploadFiles: UploadFile[] */) {  
    if (props.removeConfirm) {  
        return ElMessageBox.confirm(t('le.el.upload.delConfirm'), t('le.el.upload.del'), {  
        type: 'warning'  
    })  
        .then(() => {  
            return true  
        })  
        .catch(() => {  
            return false  
        })  
    }  
    return true  
}  
  
function getFileIdx(file: UploadFile, fileList: UploadFile[]): number {  
    const uid = file.uid  
    return fileList.findIndex(f => f.uid === uid)  
}  
  
// upload组件change事件  
function handleChange(file: UploadFile, fileList: UploadFile[]) {  
    const status = file.status  
    // 文件刚上传时 emit('uploading')  
    if (status === 'ready') emit('fileChange', file, 'uploading')  

    if (['success', 'fail'].includes(status as string)) {  
        const idx = getFileIdx(file, fileList)  
        if (idx === -1) return  
        // 成功处理  
        if (status === 'success') {  
            const { code, data, msg } = file.response  
            let emitStatus: EmitStatus = 'success'  
            // 错误处理 code !== 200: 有错误  
            if (code !== 200) {  
                ElMessage.error(msg || t('le.el.upload.uploadErrorTip'))  
                emitStatus = 'fail'  
                fileList.splice(idx, 1)  
            } else {  
                fileList[idx].url = data  
            }  
            emit('fileChange', file, emitStatus)  
        } else {  
            // 失败  
            ElMessage.error(t('le.el.upload.uploadErrorTip', [file.name]))  
            emit('fileChange', file, 'fail')  
            fileList.splice(idx, 1)  
        }  
    }  
    emitValue(fileList)  
}  
// 超出限制时 钩子函数  
const handleExceed: UploadProps['onExceed'] = (files: File[], uploadFiles: UploadUserFile[]) => {  
    // 选中的文件数  
    const selectedNum = files.length  
    // 可以新增的最大文件数  
    const addNum = props.limit - selectedNum  
    if (addNum <= 0) {  
        // 超过最大上传数量 清空已上传文件  
        uploadRef.value.clearFiles()  
        // 保留 后面选中的数据  
        files.splice(0, Math.abs(addNum))  
    } else {  
        // 可以保留的 已上传文件  
        const leftNum = uploadFiles.length - addNum  
        if (leftNum > 0) uploadFiles.splice(0, leftNum)  
    }  
    files.forEach(_file => {  
        const file = _file as UploadRawFile  
        file.uid = getUid()  
        uploadRef.value!.handleStart(file)  
    })  
    // 如果有文件 则提交上传  
    if (files.length) uploadRef.value!.submit()  
}  
// 预览文件、图片  
function handlePreview(file) {  
    if (file.type?.indexOf('image') >= 0 || isImageByExt(file.url)) {  
        createImgPreview({ urlList: [file.url] })  
    } else {  
        window.open(file.url)  
    }  
}  
function handleDownload(file: UploadFile) {  
    commonDownload(file.url as string, file.name)  
}  
function emitValue(value: UploadFile[]) {  
    emit('change', value)  
    emit('update:value', value)  
}  
  
defineExpose({  
    uploadRef  
})  
</script>  
  
<template>  
<div :class="`${prefixCls}-container ${prefixCls}-container--${isLimit ? 'limit' : 'normal'} ${prefixCls}-container--${size}`">  
    <slot name="tips">  
    <div v-if="tips" class="tip my-7px text-12px text-#8a8886">{{ tips }}</div>  
    </slot>  
    <ElUpload  
        ref="uploadRef"  
        :headers="headers"  
        :action="uploadUrl"  
        v-bind="bindProps"  
        :before-upload="bindProps.beforeUpload || defaultBeforeUpload"  
        :file-list="value"  
        :before-remove="beforeRemove"  
        :on-exceed="handleExceed"  
        @change="handleChange"  
    >  
    <!-- @preview="handlePreview" -->  
    <slot>  
        <template v-if="isPictureCard">  
            <div class="text-center">  
                <LeIcon size="18" icon="ant-design:plus-outlined" />  
                <div class="ant-upload-text">  
                {{ text }}  
                </div>  
            </div>  
        </template>  
        <el-button v-else :disabled="isLimit || disabled">  
            <LeIcon size="18" icon="ant-design:upload-outlined" />  
            <span>{{ text }}</span>  
        </el-button>  
    </slot>  
    <!-- 自定义file 渲染: 目前仅针对 picture-card 做自定义渲染 -->  
    <template #file="{ file, index }">  
        <slot name="file" :file="file" :index="index">  
            <div v-if="bindProps.listType === 'picture' || bindProps.listType === 'picture-card'" class="el-upload-list__item-thumbnail-wrap">  
                <component :is="bindProps.iconRender({ file })" />  
            </div>  
            <!-- 常规文件渲染 -->  
            <div v-if="file.status === 'uploading' || bindProps.listType !== 'picture-card'" class="el-upload-list__item-info">  
                <a class="el-upload-list__item-name" @click.prevent="handlePreview(file)">  
                    <div v-if="bindProps.listType === 'text'" class="el-upload-upload-text-icon">  
                    <component :is="bindProps.iconRender({ file })" />  
                    </div>  
                    <span class="el-upload-list__item-file-name" :title="file.name">  
                        {{ file.name }}  
                    </span>  
                </a>  
                <el-progress v-if="file.status === 'uploading'" type="line" :stroke-width="2" :percentage="+file.percentage" />  
            </div>  
            <!-- 文件-状态 -->  
            <label class="el-upload-list__item-status-label">  
                <el-icon v-if="bindProps.listType === 'text'" class="el-icon--upload-success el-icon--circle-check">  
                    <LeIcon icon="ep:circle-check" />  
                </el-icon>  
                <el-icon v-else-if="['picture-card', 'picture'].includes(bindProps.listType)" class="el-icon--upload-success el-icon--check">  
                    <LeIcon icon="ep:check" />  
                </el-icon>  
            </label>  
            <div class="el-upload-list__item-actions">  
                <!-- action.下载 -->  
                <LeIcon v-if="file.status === 'success'" class="action action--download" icon="ep:download" @click="handleDownload(file)" />  
                <!-- action.删除 -->  
                <LeIcon v-if="!disabled" class="action action--close" icon="ep:close" @click="handleRemove(file)" />  
                </div>  
                <i v-if="!disabled" class="el-icon--close-tip">按 delete 键可删除</i>  
                <!-- picture-card actions -->  
                <div v-if="bindProps.listType === 'picture-card'" class="el-upload-list__item-actions actions--picture-card">  
                <!-- 查看 -->  
                <LeIcon class="action action--view" icon="ep:view" @click="handlePreview(file)" />  
                <!-- 下载 -->  
                <LeIcon v-if="file.status === 'success'" class="action action--download" icon="ep:download" @click="handleDownload(file)" />  
                <!-- 删除 -->  
                <LeIcon v-if="!disabled" class="action action--delete" icon="ep:delete" @click="handleRemove(file)" />  
        </div>  
    </slot>  
</template>  
</ElUpload>  
</div>  
</template>  
  
<style lang="scss">  
$prefix-cls: '#{$prefix}upload';  
  
.#{$prefix-cls}-container {  
    position: relative;  

    .el-upload-list {  
        // listType: text  
        &--text {  
        // 文件 自定义小图标  
        .el-upload-upload-text-icon {  
            color: var(--el-text-color-secondary);  
            margin-right: 6px;  
            .local_icon-image {  
                display: block;  
                width: 1.2em;  
                height: 1.2em;  
                overflow: hidden;  
                object-fit: contain;  
            }  
    }  
    .el-upload-list__item {  
        //background-color: var(--el-fill-color-lighter);  
        background-color: var(--el-fill-color-extra-light);  
        &:hover {  
            background-color: var(--el-fill-color-light);  
        }  
        &-info {  
            flex: 1;  
            width: 0;  
            margin-left: 0;  
        }  
        &:hover {  
            .el-upload-list__item-actions {  
                opacity: 1;  
            }  
      }  
        .el-upload-list__item-actions {  
            opacity: 0;  
            //padding-right: 16px;  
            padding-right: 2px;  
        }  
    }  
}  
// listType: picture  
&--picture {  
.el-upload-list__item {  
    &-thumbnail {  
        &-wrap,  
        &-wrap img {  
        width: 70px;  
        height: 70px;  
        //position: static;  
        //display: block;  
        //width: 100%;  
        //height: 1;  
        }  
        &-wrap {  
        // 自定义小图标  
            .le-icon {  
                font-size: 70px;  
            }  
        }  
    }  
    &-info {  
        flex: 1;  
        width: 0;  
        margin-left: 0;  
    }  
    .el-progress {  
        bottom: unset;  
    }  
}  
// 文件 自定义小图标  
.el-upload-upload-text-icon {  
        color: var(--el-text-color-secondary);  
        margin-right: 6px;  
        .local_icon-image {  
            display: block;  
            //width: 1.2em;  
            //height: 1.2em;  
            width: 70px;  
            height: 70px;  
            overflow: hidden;  
            object-fit: contain;  
        }  
    }  
}  
// listType: picture-card  
&--picture-card {  
    gap: 6px;  
    .el-upload-list__item {  
        margin: 0;  
        flex-direction: column;  
        &-status-label {  
            .le-icon {  
                font-size: 12px;  
                //margin-top: 11px;  
                //transform: rotate(-45deg);  
            }  
        }  
        &-actions {  
            span + span {  
                margin-left: 2px;  
            }  
        }  
        .el-progress {  
            //width: 126px;  
            //top: 86%;  
            top: unset;  
            //bottom: 0;  
            bottom: 2px;  
            width: 100%;  
            .el-progress__text {  
                top: -13px;  
            }  
        }  
    }  
    // 上传按钮 禁用 隐藏  
    .el-upload.el-upload--picture-card {  
        &.is-disabled {  
            display: none;  
        }  
    }  
}  
  
&__item {  
    display: flex;  
    align-items: center;  
    &-thumbnail {  
        &-wrap,  
        &-wrap img {  
        position: static;  
        display: block;  
        width: 100%;  
        height: 100%;  
        object-fit: contain;  
    }  
    &-wrap {  
        display: flex;  
        align-items: center;  
        justify-content: center;  
        // 自定义小图标  
        .le-icon {  
            font-size: 60px;  
        }  
    }  
}  
    .el-progress {  
        top: unset;  
        bottom: -1px;  
    }  
}  
&__item-actions {  
    display: inline-flex;  
        .action {  
            width: 16px;  
            font-size: 16px;  
            margin: 0 2px;  
            cursor: pointer;  
            transition: all 0.3s;  
            &:hover {  
            color: var(--el-color-primary);  
            opacity: 1;  
        }  
    }  
    .el-icon {  
        width: 16px;  
        //margin: 0 4px;  
        font-size: 16px;  
        //padding: 0 6px;  
        margin: 0 2px;  
        cursor: pointer;  
        transition: all 0.3s;  
            &:hover {  
                color: var(--el-color-primary);  
                opacity: 1;  
            }  
        }  
    }  
}  
  
&--default {  
    .el-upload-list {  
        --el-upload-list-picture-card-size: 80px;  
            &--picture-card {  
            }  
        .el-upload--picture-card {  
            --el-upload-picture-card-size: 80px;  
        }  
        &--picture {  
            .el-upload-list__item {  
                padding: 6px;  
                margin-top: 6px;  
                &-thumbnail {  
                    &-wrap,  
                    &-wrap img {  
                        width: 50px;  
                        height: 50px;  
                    }  
                    &-wrap {  
                        // 自定义小图标  
                        .le-icon {  
                            font-size: 50px;  
                        }  
                    }  
                }  
            }  
        }  
    }  
}  
&--small {  
    .el-upload-list {  
        --el-upload-list-picture-card-size: 52px;  
        &--picture-card {  
            gap: 4px;  
            .el-upload-list__item {  
                //margin: 0 4px 4px 0;  
                //padding: 0;  
                /*overflow: hidden;  
                &-name {  
                    padding: 0 2px !important;  
                    bottom: 2px !important;  
                }*/  
                &-thumbnail {  
                    &-wrap {  
                        .le-icon {  
                            font-size: 30px;  
                        }  
                    }  
                //line-height: 52px !important;  
                //.anticon {}  
                }  
            }  
            .el-upload--picture-card {  
                --el-upload-picture-card-size: 52px;  
            }  
        }  
        &--picture {  
            .el-upload-list__item {  
                padding: 2px;  
                margin-top: 4px;  
                margin-bottom: 2px;  
                &-thumbnail {  
                    &-wrap,  
                    &-wrap img {  
                    width: 36px;  
                    height: 36px;  
                    }  
                    &-wrap {  
                    // 自定义小图标  
                        .le-icon {  
                            font-size: 36px;  
                        }  
                    }  
                }  
            }  
        }  
    }  
}  
    // 超出limit 限制  
    &--limit {  
        .el-upload-list {  
            // listType: picture-card  
            &--picture-card {  
                // 上传按钮 禁用 隐藏  
                .el-upload.el-upload--picture-card {  
                display: none;  
                }  
            }  
        }  
    }  
}  
</style>

六、预览/源码

预览 lancejiang.github.io/Lance-Eleme…
源码 github.com/LanceJiang/…