vue3+element-plus 基于el-upload二次封装的ProUploadFile文件上传组件
ProUploadFile 是一个基于 Element Plus 的 Upload 组件封装的文件上传组件。该组件支持拖拽上传、文件类型限制、大小限制等功能,并提供了美观的文件列表展示界面。
·
ProUploadFile 文件上传组件
实现效果
组件介绍
ProUploadFile 是一个基于 Element Plus 的 Upload 组件封装的文件上传组件。该组件支持拖拽上传、文件类型限制、大小限制等功能,并提供了美观的文件列表展示界面。
功能特点
- 支持拖拽上传和点击上传
- 支持文件类型限制
- 支持文件大小限制
- 支持多文件上传
- 支持文件数量限制
- 支持自定义文件列表展示
- 支持文件上传进度显示
- 支持文件预览和删除
- 支持自定义提示信息
代码实现
/**
* 文件上传组件
* 基于 Element Plus 的 Upload 组件封装
* 支持拖拽上传、文件类型限制、大小限制等功能
* @component ProUploadFile
*/
<template>
<div class="pro-upload-file-box">
<el-upload
:class="['pro-upload-file', { 'is-disabled': disabled }]"
ref="proUploadFileRef"
v-bind="uploadProps"
:on-success="handleSuccess"
:before-upload="handleBeforeUpload"
:on-exceed="handleExceed"
:on-error="handleError"
>
<slot>
<el-button v-if="!drag" type="primary" :disabled="disabled">
<el-icon class="upload-icon">
<Plus />
</el-icon>文件上传
</el-button>
<div v-else>
<el-image
class="img-upload"
src="https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/upload.png"
/>
<div class="upload-guide">
{{ dragTitle }}
</div>
<div v-if="accept" class="upload-support">
支持扩展名:{{ accept.split(",").join(" ") }}
</div>
</div>
</slot>
<!-- file插槽 -->
<template #file>
<slot name="file" />
</template>
<template #tip>
<div v-if="tip" class="upload-tip">
{{ tip }}
</div>
</template>
</el-upload>
<div v-if="!showFileList" class="pro-upload-file-content">
<div
v-for="(file, index) in fileList"
:key="file.uid"
class="file-list-item"
@mouseenter="hoverItem = file.uid"
@mouseleave="hoverItem = ''"
@click="fileClick(file)"
>
<div class="content">
<div class="file-list-item-left">
<el-image
v-if="fileTypeObj(file.name).showFileIcon"
class="file-icon"
:src="fileTypeObj(file.name).fileIconUrl"
/>
<div>
<div class="file-list-item-name">
{{ file.name }}
</div>
<div class="file-list-item-state">
{{ file.loadingState ? "上传中" : "上传完成" }}
</div>
</div>
</div>
<template v-if="!file.loadingState">
<el-icon
v-if="hoverItem === file.uid && !disabled"
class="file-list-item-icon-del"
@click.stop="handleRemove(file, index)"
>
<Close />
</el-icon>
<el-icon v-else class="file-list-item-icon">
<Check />
</el-icon>
</template>
</div>
<el-progress
v-if="file.percentage < 100"
color="rgba(11, 211, 211, 1)"
:stroke-width="2"
:show-text="false"
:percentage="file.percentage"
/>
</div>
</div>
</div>
</template>
<script setup name="ProUploadFile">
import { ref, computed } from 'vue';
import { Close, Check, Plus } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
const proUploadFileRef = ref(null);
const hoverItem = ref('');
// 计算属性
const fileTypeList = computed(() => {
if (!props.accept) return [];
return props.accept.split(',').map(item => item.replace('.', ''));
});
const maxSizeInKB = computed(() => {
if (!props.maxSize) return 0;
return props.sizeUnit === 'MB' ? props.maxSize * 1024 : props.maxSize;
});
// 组件属性定义
const props = defineProps({
/** 上传地址 */
action: {
type: String,
required: true,
default: '',
},
/** 接受的文件类型 */
accept: {
type: String,
default: '.xls,.xlsx',
},
/** 文件大小限制 */
maxSize: {
type: Number,
default: 0,
},
/** 大小单位(KB/MB) */
sizeUnit: {
type: String,
default: 'MB',
validator: (value) => ['KB', 'MB'].includes(value),
},
/** 是否支持拖拽 */
drag: {
type: Boolean,
default: true,
},
/** 文件数量限制 */
limit: {
type: Number,
default: 0,
},
/** 是否支持多选 */
multiple: {
type: Boolean,
default: false,
},
/** 是否显示文件列表 */
showFileList: {
type: Boolean,
default: false,
},
/** 请求头 */
headers: {
type: Object,
default: () => ({}),
},
/** 是否禁用 */
disabled: {
type: Boolean,
default: false,
},
/** 提示信息 */
tip: {
type: String,
default: '',
},
/** 拖拽提示文字 */
dragTitle: {
type: String,
default: '点击或将文件拖拽到这里上传',
},
});
// 组件事件定义
const emit = defineEmits(['success', 'fileClick', 'remove', 'error', 'exceed']);
// 文件列表双向绑定
const fileList = defineModel('fileList', {
type: Array,
default: () => [],
});
const uploadProps = computed(() => ({
action: props.action,
accept: props.accept,
limit: props.limit,
drag: props.drag,
multiple: props.multiple,
autoUpload: true,
showFileList: props.showFileList,
headers: props.headers,
fileList: fileList.value,
disabled: props.disabled,
}));
// 文件类型映射
const FILE_TYPE_MAP = {
pdf: ['pdf'],
excel: ['xls', 'xlsx', 'et'],
csv: ['csv'],
image: ['png', 'jpg', 'jpeg', 'gif', 'bmp'],
doc: ['doc', 'docx'],
zip: ['zip'],
};
/**
* 通用文件类型判断方法
* @param {string} fileName - 文件名
* @param {string} type - 文件类型
* @returns {boolean} 是否为指定类型
*/
const isFileType = (fileName, type) => {
const extension = fileName.split('.').pop().toLowerCase();
return FILE_TYPE_MAP[type]?.includes(extension) || false;
};
const fileTypeObj = (fileName) => {
const iconMap = {
pdf: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/pdf.png',
excel: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/exc.png',
csv: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/csv.png',
image: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/image.png',
doc: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/doc.png',
zip: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/zip.png',
};
for (const [type, url] of Object.entries(iconMap)) {
if (isFileType(fileName, type)) {
return { showFileIcon: true, fileIconUrl: url };
}
}
return { showFileIcon: false, fileIconUrl: '' };
};
/**
* 移除上传文件
* @param {Object} file - 文件对象
* @param {number} index - 文件索引
*/
const handleRemove = (file, index) => {
fileList.value.splice(index, 1);
proUploadFileRef.value.handleRemove(file);
emit('remove', file);
};
/**
* 上传前校验
* @param {File} file - 文件对象
* @returns {boolean} 是否允许上传
*/
const handleBeforeUpload = (file) => {
if (!validateFileType(file)) return false;
if (!validateFileSize(file)) return false;
addFileToList(file);
return true;
};
/**
* 验证文件类型
* @param {File} file - 文件对象
* @returns {boolean} 是否符合类型要求
*/
const validateFileType = (file) => {
if (!props.accept) return true;
const fileType = file.name.split('.').pop();
if (!fileTypeList.value.includes(fileType)) {
ElMessage({
message: `仅支持 ${fileTypeList.value.join('、')} 格式`,
type: 'warning',
});
return false;
}
return true;
};
/**
* 验证文件大小
* @param {File} file - 文件对象
* @returns {boolean} 是否符合大小要求
*/
const validateFileSize = (file) => {
if (!props.maxSize) return true;
const fileSize = file.size / 1024;
if (fileSize > maxSizeInKB.value) {
ElMessage({
message: `大小不能超过 ${props.maxSize}${props.sizeUnit}!`,
type: 'warning',
});
return false;
}
return true;
};
/**
* 创建文件对象
* @param {File} file - 原始文件对象
* @returns {Object} 处理后的文件对象
*/
const createFileObject = (file) => ({
name: file.name,
url: '',
percentage: 0,
loadingState: 1,
type: file.type,
uid: file.uid,
});
/**
* 添加文件到列表
* @param {File} file - 文件对象
*/
const addFileToList = (file) => {
const obj = createFileObject(file);
// 模拟上传进度
obj.interval = setInterval(() => {
const index = fileList.value.findIndex((i) => i.uid === obj.uid);
const targetFile = fileList.value[index];
if (targetFile?.percentage < 90) {
targetFile.percentage += 10;
}
}, 300);
fileList.value.push(obj);
};
/**
* 处理超出限制
* @param {File[]} files - 文件列表
* @param {File[]} uploadFiles - 已上传文件列表
*/
const handleExceed = (files, uploadFiles) => {
ElMessage({
message: `最多只能上传 ${props.limit} 个文件`,
type: 'warning',
});
emit('exceed', files, uploadFiles);
};
/**
* 处理上传成功
* @param {Object} response - 响应数据
* @param {Object} uploadFile - 上传文件对象
* @param {Object[]} uploadFiles - 上传文件列表
*/
const handleSuccess = (response, uploadFile, uploadFiles) => {
const index = fileList.value.findIndex((item) => item.uid === uploadFile.uid);
const targetFile = fileList.value[index];
if (response.code === 10000) {
const { data } = response;
if (data.fileUrl) targetFile.url = data.fileUrl;
if (data.fileName) targetFile.name = data.fileName;
targetFile.percentage = 100;
targetFile.loadingState = 0;
} else {
fileList.value.splice(index, 1);
proUploadFileRef.value.handleRemove(uploadFile);
ElMessage({
message: response?.msg || response?.message || '上传失败',
type: 'error',
});
}
// 清理定时器
targetFile.interval && clearInterval(targetFile.interval);
emit('success', response, uploadFile, uploadFiles);
};
/**
* 处理上传错误
* @param {Error} error - 错误对象
* @param {Object} uploadFile - 上传文件对象
* @param {Object[]} uploadFiles - 上传文件列表
*/
const handleError = (error, uploadFile, uploadFiles) => {
const index = fileList.value.findIndex((item) => item.uid === uploadFile.uid);
proUploadFileRef.value.handleRemove(uploadFile);
fileList.value[index].interval && clearInterval(fileList.value[index].interval);
fileList.value.splice(index, 1);
ElMessage({
message: '上传失败',
type: 'error',
});
emit('error', error, uploadFile, uploadFiles);
};
/**
* 处理文件点击
* @param {Object} file - 文件对象
*/
const fileClick = (file) => {
if (file.loadingState === 1) {
ElMessage({
message: '文件上传中!',
type: 'warning',
});
return;
}
emit('fileClick', file);
};
</script>
<style lang="less" scoped>
.pro-upload-file-box {
text-align: left;
.is-disabled {
:deep(.el-upload-dragger) {
cursor: not-allowed;
}
}
.pro-upload-file {
position: relative;
.upload-icon {
font-size: 16px;
margin-right: 4px;
}
.img-upload {
width: 48px;
height: 48px;
}
.upload-guide {
color: rgba(0, 5, 27, 0.85);
font-size: 14px;
}
.upload-support {
color: rgba(0, 5, 27, 0.45);
font-size: 12px;
margin-top: 4px;
}
}
.upload-tip {
margin-top: 8px;
line-height: 20px;
text-align: left;
font-size: 12px;
color: #909399;
}
.pro-upload-file-content {
width: 100%;
margin-top: 16px;
.file-list-item {
width: 100%;
cursor: pointer;
margin-bottom: 8px;
background: #f6f7fb;
border-radius: 4px;
.content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background: #f6f7fb;
padding: 12px 20px;
box-sizing: border-box;
border-radius: 4px;
}
}
.file-list-item-left {
width: calc(100% - 32px);
display: flex;
align-items: center;
.file-icon {
width: 40px;
height: 40px;
margin-right: 12px;
flex-shrink: 0;
}
}
.file-list-item-name {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: rgba(0, 5, 27, 0.85);
font-size: 14px;
}
.file-list-item-state {
font-size: 12px;
color: rgba(0, 5, 27, 0.45);
}
.file-list-item-icon {
font-size: 16px;
color: #0bd3d3;
}
.file-list-item-icon-del {
color: rgba(0, 5, 27, 0.65);
font-size: 16px;
}
}
:deep(.el-upload-dragger) {
border: 1px dashed rgba(11, 211, 211, 1);
background: rgba(11, 211, 211, 0.02);
&.is-dragover,
&:hover {
background: rgba(11, 211, 211, 0.1) !important;
border: 1px dashed rgba(11, 211, 211, 1) !important;
}
}
:deep(.el-progress-bar__outer) {
border-radius: 4px;
}
}
</style>
属性说明
属性名 | 说明 | 类型 | 默认值 | 必填 |
---|---|---|---|---|
action | 上传地址 | String | - | 是 |
accept | 接受的文件类型,如:'.xls,.xlsx' | String | '.xls,.xlsx' | 否 |
maxSize | 文件大小限制 | Number | 0 | 否 |
sizeUnit | 大小单位,可选值:'KB'、'MB' | String | 'MB' | 否 |
drag | 是否支持拖拽上传 | Boolean | true | 否 |
limit | 文件数量限制 | Number | 0 | 否 |
multiple | 是否支持多选 | Boolean | false | 否 |
showFileList | 是否显示文件列表 | Boolean | false | 否 |
headers | 请求头信息 | Object | {} | 否 |
disabled | 是否禁用 | Boolean | false | 否 |
tip | 提示信息 | String | - | 否 |
dragTitle | 拖拽提示文字 | String | '点击或将文件拖拽到这里上传' | 否 |
事件
事件名 | 说明 | 回调参数 |
---|---|---|
success | 文件上传成功时触发 | (response, uploadFile, uploadFiles) |
fileClick | 点击文件时触发 | (file) |
remove | 移除文件时触发 | (file) |
error | 上传失败时触发 | (error, uploadFile, uploadFiles) |
exceed | 超出文件数量限制时触发 | (files, uploadFiles) |
插槽
插槽名 | 说明 |
---|---|
default | 自定义上传按钮内容 |
file | 自定义文件列表项内容 |
tip | 自定义提示信息内容 |
使用示例
<template>
<pro-upload-file
action="/api/upload"
accept=".xls,.xlsx"
:max-size="10"
:limit="5"
:multiple="true"
tip="文件大小不超过10M"
@success="handleSuccess"
@error="handleError"
@remove="handleRemove"
/>
</template>
<script setup>
const handleSuccess = (response, uploadFile, uploadFiles) => {
// 在此处写你需要的个性化步骤
console.log('上传成功:', response);
};
const handleError = (error, uploadFile, uploadFiles) => {
// 在此处写你需要的个性化步骤
console.log('上传失败:', error);
};
const handleRemove = (file) => {
// 在此处写你需要的个性化步骤
console.log('移除文件:', file);
};
</script>
文件类型支持
组件内置支持以下文件类型的图标显示:
- PDF文件 (.pdf)
- Excel文件 (.xls, .xlsx, .et)
- CSV文件 (.csv)
- 图片文件 (.png, .jpg, .jpeg, .gif, .bmp)
- Word文档 (.doc, .docx)
- 压缩文件 (.zip)
注意事项
1、上传接口返回数据格式需要符合以下规范(如果不符合规范请调整组件封装的代码):
{
code: 10000, // 成功状态码
data: {
fileUrl: '文件地址',
fileName: '文件名'
},
msg: '提示信息'
}
2、文件大小限制的单位默认为 MB,可通过 sizeUnit 属性修改为 KB。
3、当 showFileList 为 false 时,组件会使用自定义的文件列表展示样式。
4、上传中的文件会显示进度条,进度条颜色为 rgba(11, 211, 211, 1)。
5、组件中使用了defineModel(),defineModel()仅在vue 3.4+ 版本中可用,如果版本较低可以升版本或者用update:fileList进行替代
更多推荐
所有评论(0)