Element Plus Upload 上传组件实战指南:从基础用法到源码级原理
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
本篇指南以 Element Plus 官方文档 upload.md 为主线,完整讲解<el-upload>上传组件的全部能力:基础点击上传、拖拽上传、目录上传、照片墙、手动上传,以及limit、before-upload、before-remove、on-exceed等核心配置与钩子函数。全文结合仓库内packages/components/upload的真实实现源码与docs/examples/upload下的全部官方示例,帮助读者在掌握可复制的实战代码的同时,理解上传状态机、文件 ID 生成、进度上报等底层机制。
组件能力总览
Upload是 Element Plus 中负责文件上传的组件,核心能力包括:
- 通过点击或拖拽两种方式选择文件;
- 选择文件后默认自动上传到指定
action地址,也支持关闭自动上传、手动触发提交; - 通过
multiple支持多文件上传; - 通过
list-type切换文件列表样式(文本列表 / 图片列表 / 照片墙卡片); - 通过一系列
before-*与on-*钩子,在“上传前校验、上传成功、上传失败、进度、移除、超限”等关键节点插入自定义逻辑; - 支持
directory目录上传,选择文件夹后自动将其中文件展平加入上传队列(自 v2.13.1 起)。
从源码结构看,Upload由多个子模块协作完成:upload.vue 负责整体编排,upload-content.vue 负责选择文件与发起请求,upload-dragger.vue 负责拖拽区域,use-handlers.ts 集中管理文件列表与所有事件回调,ajax.ts 提供默认的 XMLHttpRequest 上传实现。
基础用法:点击上传与三个关键钩子
最基础的用法是通过slot自定义上传按钮,并设置limit(允许的最大文件数)、on-exceed(超出限制时的回调)和before-remove(移除前拦截)。完整示例见 basic.vue:
<template> <el-upload v-model:file-list="fileList" class="upload-demo" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" multiple :on-preview="handlePreview" :on-remove="handleRemove" :before-remove="beforeRemove" :limit="3" :on-exceed="handleExceed" > <el-button type="primary">Click to upload</el-button> <template #tip> <div class="el-upload__tip"> jpg/png files with a size less than 500KB. </div> </template> </el-upload> </template> <script lang="ts" setup> import { ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import type { UploadProps, UploadUserFile } from 'element-plus' const fileList = ref<UploadUserFile[]>([ { name: 'element-plus-logo.svg', url: 'https://element-plus.org/images/element-plus-logo.svg', }, { name: 'element-plus-logo2.svg', url: 'https://element-plus.org/images/element-plus-logo.svg', }, ]) const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => { console.log(file, uploadFiles) } const handlePreview: UploadProps['onPreview'] = (uploadFile) => { console.log(uploadFile) } const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => { ElMessage.warning( `The limit is 3, you selected ${files.length} files this time, add up to ${ files.length + uploadFiles.length } totally` ) } const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => { return ElMessageBox.confirm( `Cancel the transfer of ${uploadFile.name} ?` ).then( () => true, () => false ) } </script>要点拆解:
v-model:file-list:双向绑定当前文件列表(类型为UploadUserFile[]),可以直接用已有对象初始化,展示历史已上传文件;multiple:允许多选;limit+on-exceed:文件数达到limit后继续选择会触发on-exceed,回调参数为「本次新选的文件files」和「当前列表uploadFiles」,常用于弹出提示或执行覆盖逻辑;before-remove:移除文件前的拦截钩子。返回false(或返回被 reject 的Promise)则取消移除。示例中通过ElMessageBox.confirm弹出确认框,确认则 resolvetrue继续删除,取消则 resolvefalse中止删除。
在源码层,移除流程实现在 use-handlers.ts 的handleRemove中:先取出对应的UploadFile,执行abort取消正在进行的请求,从列表中过滤掉该文件,触发onRemove回调,并调用revokeFileObjectURL释放blob:类型的对象 URL(见 use-handlers.ts),避免内存泄漏。beforeRemove的判定逻辑是:只有当before !== false时才真正执行删除。
覆盖旧文件:limit+on-exceed的经典组合
当业务要求「最多只能有 1 个文件,选择新文件时自动替换旧文件」时,官方提供了limit-cover方案,见 limit-cover.vue:
<template> <el-upload ref="upload" class="upload-demo" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" :limit="1" :on-exceed="handleExceed" :auto-upload="false" > <template #trigger> <el-button type="primary">select file</el-button> </template> <el-button class="ml-3" type="success" @click="submitUpload"> upload to server </el-button> <template #tip> <div class="el-upload__tip text-red"> limit 1 file, new file will cover the old file </div> </template> </el-upload> </template> <script setup lang="ts"> import { ref } from 'vue' import { genFileId } from 'element-plus' import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus' const upload = ref<UploadInstance>() const handleExceed: UploadProps['onExceed'] = (files) => { upload.value!.clearFiles() const file = files[0] as UploadRawFile file.uid = genFileId() upload.value!.handleStart(file) } const submitUpload = () => { upload.value!.submit() } </script>这套组合拳的实现思路是:
upload.value!.clearFiles():清空当前列表,为覆盖做准备;genFileId():为新文件生成一个新的唯一 ID。genFileId实现在 upload.ts,其实现为Date.now() + fileId++,其中fileId是模块级自增计数器。由于本次的file与上一次是同一个 DOMFile引用(浏览器对同一文件选择框的引用可能复用),不重新生成 ID 会导致 Vue 无法区分新旧文件,列表不会更新;upload.value!.handleStart(file):将文件手动加入上传队列(内部会走onStart→ 组装UploadFile→ 追加到uploadFiles);submit():配合:auto-upload="false"手动触发真正上传。
这个模式是理解Exposes(组件暴露的方法)价值的入口:clearFiles、handleStart、submit、genFileId四者组合,让你可以完全接管上传流程。
用户头像上传:before-upload前置校验
头像上传是典型的「格式 + 大小」双重校验场景。官方示例 avatar.vue 展示了如何用before-upload钩子在上传前拦截非法文件:
<template> <el-upload class="avatar-uploader" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload" > <img v-if="imageUrl" :src="imageUrl" class="avatar" /> <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon> </el-upload> </template> <script lang="ts" setup> import { ref } from 'vue' import { ElMessage } from 'element-plus' import { Plus } from '@element-plus/icons-vue' import type { UploadProps } from 'element-plus' const imageUrl = ref('') const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => { imageUrl.value = URL.createObjectURL(uploadFile.raw!) } const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => { if (rawFile.type !== 'image/jpeg') { ElMessage.error('Avatar picture must be JPG format!') return false } else if (rawFile.size / 1024 / 1024 > 2) { ElMessage.error('Avatar picture size can not exceed 2MB!') return false } return true } </script>关键点:
before-upload接收原始文件rawFile(UploadRawFile),返回值语义:返回false或返回被 reject 的Promise时中止上传;返回true或undefined时继续;也可以返回File/Blob来替换要上传的文件(例如前端压缩后再上传);:show-file-list="false":隐藏默认文件列表,界面完全由自定义内容(头像图片)呈现;on-success:上传成功后拿到uploadFile.raw,用URL.createObjectURL生成本地预览地址并回显到<img>;- 校验代码中
rawFile.type(MIME 类型)与rawFile.size(字节数)都是浏览器File对象的标准属性,size / 1024 / 1024 > 2即判断是否超过 2MB。
照片墙:list-type="picture-card"与预览对话框
将list-type设为picture-card即可得到经典的九宫格照片墙样式,配合on-preview可实现点击放大预览。官方示例 photo-wall.vue:
<template> <el-upload v-model:file-list="fileList" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" list-type="picture-card" :on-preview="handlePictureCardPreview" :on-remove="handleRemove" > <el-icon><Plus /></el-icon> </el-upload> <el-dialog v-model="dialogVisible"> <img w-full :src="dialogImageUrl" alt="Preview Image" /> </el-dialog> </template> <script lang="ts" setup> import { ref } from 'vue' import { Plus } from '@element-plus/icons-vue' import type { UploadProps, UploadUserFile } from 'element-plus' const fileList = ref<UploadUserFile[]>([]) const dialogImageUrl = ref('') const dialogVisible = ref(false) const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => { dialogImageUrl.value = uploadFile.url! dialogVisible.value = true } </script>从源码看,list-type="picture-card"(以及picture)状态下,use-handlers.ts 的handleStart会在文件加入列表时自动执行URL.createObjectURL(file)生成预览 URL 存入uploadFile.url;并且 use-handlers.ts 中还有一段watch:当运行中把listType切换为图片类时,会为所有缺失url的文件补建对象 URL。因此照片墙模式下无需手动拼接预览地址,直接读取uploadFile.url即可。
list-type的可选值(定义在 upload.ts):'text'(默认,纯文本列表)、'picture'(缩略图 + 文件名)、'picture-card'(卡片式照片墙)。
自定义缩略图模板:file插槽
当默认的缩略图行为不满足需求时,可通过名为file的作用域插槽完全重写每个文件项的渲染内容。官方示例 custom-thumbnail.vue:
<el-upload action="#" list-type="picture-card" :auto-upload="false"> <el-icon><Plus /></el-icon> <template #file="{ file }"> <div> <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" /> <span class="el-upload-list__item-actions"> <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)"> <el-icon><zoom-in /></el-icon> </span> <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleDownload(file)"> <el-icon><Download /></el-icon> </span> <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)"> <el-icon><Delete /></el-icon> </span> </span> </div> </template> </el-upload>file插槽的插槽作用域类型为{ file: UploadFile, index: number },其中file即 upload.ts 中定义的UploadFile对象,包含name、status、percentage、uid、url、raw等字段。你可以借助这些字段实现自定义预览按钮、下载按钮、删除按钮乃至完全不同的文件卡片布局。
带缩略图的文件列表:list-type="picture"
如果只想在常规列表里给图片加缩略图,用list-type="picture"即可。官方示例 file-list-with-thumbnail.vue:
<template> <el-upload v-model:file-list="fileList" class="upload-demo" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" :on-preview="handlePreview" :on-remove="handleRemove" list-type="picture" > <el-button type="primary">Click to upload</el-button> <template #tip> <div class="el-upload__tip"> jpg/png files with a size less than 500kb </div> </template> </el-upload> </template> <script lang="ts" setup> import { ref } from 'vue' import type { UploadProps, UploadUserFile } from 'element-plus' const fileList = ref<UploadUserFile[]>([ { name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/...jpeg', }, // ... ]) </script>与picture-card一样,picture模式也会为本地新选文件自动生成blob:预览 URL。file-list中已有的对象如果提供了url字段,会直接作为缩略图来源渲染。
文件列表控制:on-change钩子
on-change在「选择文件」「上传成功」「上传失败」三种时机都会触发,是控制列表形态的万能入口。官方示例 file-list.vue 用它把列表裁剪为最近 3 条:
<script lang="ts" setup> import { ref } from 'vue' import type { UploadProps, UploadUserFile } from 'element-plus' const fileList = ref<UploadUserFile[]>([/* 初始文件 */]) const handleChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => { fileList.value = fileList.value.slice(-3) } </script>在源码中,on-change通过 use-handlers.ts 的emitChange触发:它在nextTick中调用props.onChange(file, uploadFiles.value),保证回调拿到的uploadFiles是最新列表。handleSuccess、handleError、handleStart内部都会调用emitChange,这正好对应文档中“select file 或 upload success 或 upload fail 时触发”的描述。
拖拽上传:drag属性
给组件加上drag属性即可开启拖拽区域。官方示例 drag-and-drop.vue:
<el-upload class="upload-demo" drag action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" multiple > <el-icon class="el-icon--upload"><upload-filled /></el-icon> <div class="el-upload__text"> Drop file here or <em>click to upload</em> </div> <template #tip> <div class="el-upload__tip"> jpg/png files with a size less than 500kb </div> </template> </el-upload>拖拽区域由独立的 upload-dragger.vue 实现,它监听dragover/drop等事件,在drop时取出e.dataTransfer.files派发给上层处理。drag与directory可组合使用,实现“拖入整个文件夹”。
目录上传:directory属性(v2.13.1+)
自 v2.13.1 起,Upload支持目录上传。开启directory后,文件选择框只能选择文件夹,选中后文件夹内的文件会被展平后加入上传队列。官方示例 directory.vue:
<template> <el-upload class="upload-demo" drag action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" directory multiple :on-change="handleChange" > <el-icon class="el-icon--upload"><upload-filled /></el-icon> <div class="el-upload__text"> Drop directory here or <em>click to upload</em> </div> </el-upload> </template> <script setup lang="ts"> import { UploadFilled } from '@element-plus/icons-vue' import type { UploadFile, UploadFiles } from 'element-plus' const handleChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => { console.log(uploadFile, uploadFiles) } </script>目录展平逻辑位于 upload-dragger.vue:拖入或选择目录时,组件读取每个目录条目(entry),为文件夹递归遍历其内部文件,并把子文件标记为entry.isDirectory === false;展平后的每个文件会打上isDirectory标记(对应UploadRawFile类型中的isDirectory?: boolean字段,见 upload.ts),随后作为普通文件逐条进入上传队列。可以借助该标记在before-upload中区分文件来源。
手动上传:auto-upload="false"+submit()
关闭自动上传后,选中的文件会停留在ready状态等待提交。官方示例 manual.vue:
<template> <el-upload ref="uploadRef" class="upload-demo" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" :auto-upload="false" > <template #trigger> <el-button type="primary">select file</el-button> </template> <el-button class="ml-3" type="success" @click="submitUpload"> upload to server </el-button> <template #tip> <div class="el-upload__tip"> jpg/png files with a size less than 500kb </div> </template> </el-upload> </template> <script lang="ts" setup> import { ref } from 'vue' import type { UploadInstance } from 'element-plus' const uploadRef = ref<UploadInstance>() const submitUpload = () => { uploadRef.value!.submit() } </script>注意trigger插槽的用途:默认插槽里所有内容都会触发文件选择,而trigger插槽可以精确控制“哪部分点击才弹文件选择框”。手动模式的核心是组件暴露的submit()方法。看源码 use-handlers.ts,submit会过滤出所有status === 'ready'的文件并逐个调用upload(raw),因此已经上传成功或失败的文件不会被重复提交。
API 完整参考
Attributes(属性)
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| action ^(必填) | 上传请求的 URL | string | — |
| headers | 请求头 | Headers \| Record<string, any> | — |
| method | 上传请求方法 | string | post |
| multiple | 是否支持多文件上传 | boolean | false |
| data | 请求附加数据;自 v2.3.13 起支持Awaitable数据与函数形式 | Record<string, any> \| Awaitable<Record<string, any>>/(rawFile: UploadRawFile) => Awaitable<Record<string, any>> | {} |
| name | 上传文件字段名(对应 form-data 的 key) | string | file |
| with-credentials | 是否携带 Cookie | boolean | false |
| show-file-list | 是否显示已上传文件列表 | boolean | true |
| drag | 是否开启拖拽上传模式 | boolean | false |
| accept | 接受的文件类型(原生accept属性,参考 MDN input accept);thumbnail-mode === true时不生效 | string | '' |
| crossorigin | 原生crossorigin属性 | '' \| 'anonymous' \| 'use-credentials' | — |
| on-preview | 点击已上传文件时的钩子 | (uploadFile: UploadFile) => void | — |
| on-remove | 移除文件时的钩子 | (uploadFile: UploadFile, uploadFiles: UploadFiles) => void | — |
| on-success | 上传成功时的钩子 | (response: any, uploadFile: UploadFile, uploadFiles: UploadFiles) => void | — |
| on-error | 上传出错时的钩子 | (error: Error, uploadFile: UploadFile, uploadFiles: UploadFiles) => void | — |
| on-progress | 上传进度时的钩子 | (evt: UploadProgressEvent, uploadFile: UploadFile, uploadFiles: UploadFiles) => void | — |
| on-change | 选择文件、上传成功或失败时触发 | (uploadFile: UploadFile, uploadFiles: UploadFiles) => void | — |
| on-exceed | 超出limit限制时触发 | (files: File[], uploadFiles: UploadUserFile[]) => void | — |
| before-upload | 上传前的钩子,参数为待上传文件;返回false或返回被 reject 的Promise将中止上传 | (rawFile: UploadRawFile) => Awaitable<void \| undefined \| null \| boolean \| File \| Blob> | — |
| before-remove | 移除前的钩子,参数为文件与文件列表;返回false或返回被 reject 的Promise将中止移除 | (uploadFile: UploadFile, uploadFiles: UploadFiles) => Awaitable<boolean> | — |
| file-list / v-model:file-list | 默认已上传文件列表 | UploadUserFile[] | [] |
| list-type | 文件列表类型 | 'text' \| 'picture' \| 'picture-card' | text |
| auto-upload | 是否自动上传 | boolean | true |
| http-request | 覆盖默认 XHR 行为,自定义上传请求实现 | (options: UploadRequestOptions) => XMLHttpRequest \| Promise<unknown> | ajaxUpload(见 ajax.ts) |
| disabled | 是否禁用上传 | boolean | false |
| limit | 允许上传的最大文件数 | number | — |
| directory ^(2.13.1) | 是否支持目录上传;开启后只能选择文件夹,选中后文件夹内文件会被展平 | boolean | false |
关于http-request默认值:源码 upload.ts 中该 prop 的默认值即ajaxUpload。ajaxUpload(见 ajax.ts)内部基于XMLHttpRequest构造FormData,把action、method、headers、data、filename、withCredentials等选项逐一应用到请求上,并通过onProgress/onSuccess/onError三个回调把进度、成功、失败事件上报给组件。业务上需要对接自定义上传通道(如分片、七牛、OSS 直传签名)时,只需实现一个签名相同的函数覆盖该 prop 即可。
Slots(插槽)
| 名称 | 说明 | 插槽作用域类型 |
|---|---|---|
| default | 自定义默认内容 | — |
| trigger | 触发文件选择框的内容 | — |
| tip | 提示内容 | — |
| file | 缩略图模板内容 | { file: UploadFile, index: number } |
Exposes(组件暴露的方法)
| 名称 | 说明 | 类型 |
|---|---|---|
| abort | 取消上传请求;传入file时取消对应文件的待处理请求,不传则取消所有待处理请求 | (file?: UploadFile) => void |
| submit | 手动上传文件列表(仅提交ready状态的文件) | () => void |
| clearFiles | 清空文件列表(在before-upload钩子中不支持调用) | (status?: UploadStatus[]) => void |
| handleStart | 手动将文件加入上传队列 | (rawFile: UploadRawFile) => void |
| handleRemove | 手动移除文件;file与rawFile已合并,rawFile参数将于 v2.2.0 移除 | (file: UploadFile \| UploadRawFile, rawFile?: UploadRawFile) => void |
clearFiles支持按状态过滤:默认清空['ready', 'uploading', 'success', 'fail']全部状态(见 use-handlers.ts),你也可以传入['ready']只清空待上传的文件。
类型声明速查
组件涉及的核心类型定义在 upload.ts,与官方文档完全一致:
type UploadFiles = UploadFile[] type UploadUserFile = Omit<UploadFile, 'status' | 'uid'> & Partial<Pick<UploadFile, 'status' | 'uid'>> type UploadStatus = 'ready' | 'uploading' | 'success' | 'fail' type Awaitable<T> = Promise<T> | T type Mutable<T> = { -readonly [P in keyof T]: T[P] } interface UploadFile { name: string percentage?: number status: UploadStatus size?: number response?: unknown uid: number url?: string raw?: UploadRawFile } interface UploadProgressEvent extends ProgressEvent { percent: number } interface UploadRawFile extends File { uid: number isDirectory?: boolean } interface UploadRequestOptions { action: string method: string data: Record<string, string | Blob | [string | Blob, string] | string[]> filename: string file: UploadRawFile headers: Headers | Record<string, string | number | null | undefined> onError: (evt: UploadAjaxError) => void onProgress: (evt: UploadProgressEvent) => void onSuccess: (response: any) => void withCredentials: boolean }理解UploadStatus四态对调试至关重要:文件加入队列时为ready,请求进行中为uploading,成功后为success(response字段保存服务端返回),失败后为fail并触发on-error。submit()只处理ready状态的文件,而clearFiles(status)可以按状态精确清理,这两个方法配合即可覆盖绝大多数手动控制的场景。
状态机与事件流:源码视角的完整链路
综合 use-handlers.ts 的实现,一个文件的完整生命周期如下:
- 选择文件:
upload-content触发onStart→handleStart为新文件分配genFileId()(若为空),组装UploadFile(percentage: 0、status: 'ready'),图片类list-type下自动URL.createObjectURL生成预览,追加进列表并触发on-change; - 发起上传:若
auto-upload为true或手动调用submit(),对ready文件执行upload(rawFile);上传前若有before-upload则先执行校验; - 请求过程:
on-progress更新file.percentage = Math.round(evt.percent),状态置为uploading; - 请求结束:成功则
file.status = 'success'并保存file.response,触发on-success与on-change;失败则打印错误、状态置为fail、移除文件并触发on-error与on-change; - 移除文件:
handleRemove先走before-remove拦截,通过后abort取消请求、从列表删除、释放blob:对象 URL,并触发on-remove。
此外 use-handlers.ts 中还有一段对uploadFiles的深监听:为外部传入但缺失uid的文件自动补发 ID,为缺失status的自动补为success。这意味着用v-model:file-list传入“只包含name和url”的历史文件时无需手动补齐字段,组件会自动规范化。
实战选型建议
- 普通业务文件上传:默认配置 +
limit/on-exceed+before-remove确认弹窗即可覆盖 90% 场景,参考 basic.vue; - 单文件覆盖场景(如替换文档、更换配置):采用
clearFiles + genFileId + handleStart的组合,参考 limit-cover.vue; - 需要格式/大小强校验:在
before-upload中做同步校验并返回false,需要异步校验(如调用后端 API 查重)时返回 Promise,参考 avatar.vue; - 图片类业务:
list-type="picture-card"配合on-preview做预览弹窗;要彻底定制卡片交互时使用#file插槽; - 需要先选后传 / 批量二次确认:
auto-upload="false"+ 暴露的submit()方法; - 对接自定义存储通道:覆盖
http-requestprop,实现签名与UploadRequestOptions一致的自定义请求函数(注意需自行调用onProgress/onSuccess/onError回报状态,否则组件无法感知上传结果); - 需要整目录备份/迁移:使用
directory属性,并可通过UploadRawFile.isDirectory区分扁平化后的文件来源。
组件其余源码细节可继续阅读 upload.vue、upload-content.vue、upload-list.vue,以及对应的测试用例 upload.test.tsx、upload-dragger.test.tsx,官方示例则全部位于 docs/examples/upload 目录下,可直接复制运行。
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考