基于elementui开发上传组件,实现文件上传loading列表
基于elementui开发上传组件,实现文件上传loading列表
·
一、效果图
实现上传按钮或者其他元素的popover显示上传文件的状态,速度,大小等信息
优点
- 能够直观看到文件上传的进度和状态
- 优化了大文件上传的时候,系统大面积loading空白页过长,没有用户反馈
- 可扩展性强,可以增加失败后重新上传,自定义上传方法等
- 可以传参限制文件大小,文件类型,不用自行写校验规则,依旧可以继续使用自定义方法
二、使用
直接使用
此时使用,会默认使用一个el-button作为上传触发
<ProgUpPopover
v-if="!setReadOnly"
:uploadAction="uploadAction"
multiple
:data="uploadData"
accept=".png, .jpg"
btnText="上传图片"
:onSuccess="handleUploadSuccess"
></ProgUpPopover>
插槽使用
使用customTrigger插槽来自定义触发的组件,以便来满足其他业务需求
<ProgUpPopover
v-if="!setReadOnly"
:uploadAction="uploadAction"
multiple
:data="uploadData"
accept=".png, .jpg"
btnText="上传图片"
:onSuccess="handleUploadSuccess"
>
<template #customTrigger>
<el-button type="primary" size="small" icon="el-icon-upload"> test </el-button>
</template>
</ProgUpPopover>
三、源码
上传组件ProgUpPopover.vue
<template>
<div class="ax-upload-container">
<el-upload
class="upload_btn"
:action="uploadActionComputed"
:data="data"
:headers="headersComputed"
:on-success="handleUploadSuccess"
:show-file-list="false"
:multiple="multiple"
:on-error="handleUploadError"
:on-change="handleInputChange"
:on-progress="handleProgress"
:accept="accept"
:before-upload="handleBeforeUpload"
>
<el-popover placement="bottom" trigger="hover">
<UploadCard :uploadFileList="uploadFileList" :hasWaitingOrUploading="hasWaitingOrUploading"></UploadCard>
<slot name="customTrigger" slot="reference" v-if="customTriggerVisiable"></slot>
<el-button v-else type="primary" slot="reference" size="small" :icon="BtnIcon"> {{ btnText }}</el-button>
</el-popover>
</el-upload>
</div>
</template>
<script>
import { FileState, TableUtils } from './TableUtil';
import UploadCard from './UploadCard.vue';
export default {
components: {
UploadCard,
},
props: {
uploadAction: {
type: String,
default: '',
},
uploadBtnIcon: {
type: String,
default: 'el-icon-upload',
},
fileSizeLimit: {
type: String,
default: '150MB',
},
headers: {
type: Object,
default: null,
},
accept: {
type: String,
default: null,
},
data: {
type: Object,
default: null,
},
btnText: {
type: String,
default: '上传',
},
onSuccess: {
type: Function,
default: null,
},
onError: {
type: Function,
default: null,
},
beforeUpload: {
type: Function,
default: null,
},
onChange: {
type: Function,
default: null,
},
onProgress: {
type: Function,
default: null,
},
multiple: {
type: Boolean,
default: false,
},
},
computed: {
uploadActionComputed() {
return this.uploadAction
? `${window.__HOST__URL__ + window.__PREFIX__URL__}${this.uploadAction}`
: `${
window.__HOST__URL__ + window.__PREFIX__URL__
}sjwflowCommProd/cap/projectImport/importIqpFlowZipToOneQueryForm`;
},
headersComputed() {
return this.headers ? this.headers : { auth: this.$store.getters.token };
},
},
mounted() {
this.customTriggerVisiable = this.$scopedSlots.customTrigger;
this.BtnIcon = this.uploadBtnIcon;
},
data() {
return {
customTriggerVisiable: false,
// 上传文件列表
uploadFileList: [],
BtnIcon: '',
hasWaitingOrUploading: false,
};
},
methods: {
getBtnIcon(uploadFileList) {
this.hasWaitingOrUploading = false;
let hasError = false;
for (let i = 0; i < uploadFileList.length; i += 1) {
const file = uploadFileList[i];
if (file.state === FileState.Waiting || file.state === FileState.Uploading) {
this.hasWaitingOrUploading = true;
}
if (file.state === FileState.Error) {
hasError = true;
break;
}
}
if (this.hasWaitingOrUploading) {
this.$emit('setIcon', 'el-icon-loading');
return 'el-icon-loading';
}
this.$emit('setIcon', this.uploadBtnIcon);
return this.uploadBtnIcon;
},
handleUploadSuccess(response, file, fileList) {
if (this.onSuccess) {
this.onSuccess(response, file, fileList);
}
const index = this.uploadFileList.findIndex(item => {
return file.uid === item.id;
});
if (response.code === 200) {
if (index === -1) {
// this.uploadFileList.push({
// file,
// name: file.name,
// state: FileState.Waiting,
// progress: 0,
// size: TableUtils.formatFileSize(file.size),
// speed: '速度计算中...',
// id: file.uid,
// });
} else {
this.uploadFileList[index].state = FileState.Success;
}
if (!this.onSuccess) {
this.$message({
type: 'success',
message: '上传成功',
});
}
} else {
if (!this.onSuccess) {
this.$message({
type: 'warning',
message: response.message,
});
}
if (index !== -1) {
this.uploadFileList[index].state = FileState.Error;
}
}
},
handleUploadError(err, file, fileList) {
// 上传失败的回调
const index = this.uploadFileList.findIndex(item => {
return file.uid === item.id;
});
if (index !== -1) {
this.uploadFileList[index].state = FileState.Error;
}
if (this.onError) {
this.onError(err, file, fileList);
} else {
this.$message({
type: 'Error',
message: err,
});
}
},
// 检查文件格式
checkAccept(file) {
// 获取文件名
const fileName = file.name;
// 判断是否有接受参数
if (this.accept) {
// 将接受参数转换为数组
const arr = this.accept.split(",");
const lowerCaseArray = arr.map(element => element.toLowerCase());
// 获取文件后缀
const index = fileName.lastIndexOf(".");
// 判断文件后缀是否在接受参数中
const type = lowerCaseArray.includes(fileName.slice(index).toLowerCase());
// 如果不存在,则报错
if (!type) {
this.$message.error(`请上传${this.accept}文件`);
return false;
}
// 如果存在,则返回true
return true;
}
// 如果没有接受参数,则直接返回true
return true;
},
// 检查文件尺寸
checkFileSize(file) {
const sizeLimit = this.parseSize(this.fileSizeLimit);
if (file.size > sizeLimit) {
this.$message.error(`上传文件大小请小于${this.fileSizeLimit}`);
return false;
}
return true;
},
changeFileError(file) {
const index = this.uploadFileList.findIndex(item => {
return file.uid === item.id;
});
if (index !== -1) {
this.uploadFileList[index].state = FileState.Error;
}
},
handleBeforeUpload(file) {
if (this.beforeUpload) {
// 以自定义的上传前操作优先级最高
const flag = this.beforeUpload(file);
if (!flag) {
this.changeFileError(file);
}
return flag;
}
const flagArr = [];
if (this.accept) {
// 如果有accept,默认拿取accept作为文件格式校验
const flag = this.checkAccept(file);
if (!flag) {
this.changeFileError(file);
}
flagArr.push(flag);
// return flag;
}
if (this.fileSizeLimit) {
const flag = this.checkFileSize(file);
if (!flag) {
this.changeFileError(file);
}
flagArr.push(flag);
}
return !flagArr.includes(false);
},
//校验文件大小
parseSize(sizeStr) {
const units = {
kb: 1024,
mb: 1024 * 1024,
gb: 1024 * 1024 * 1024,
tb: 1024 * 1024 * 1024 * 1024,
};
const regex = /(\d+)(kb|mb|gb|tb|m|g|t|k)/i;
const matches = sizeStr.toLowerCase().match(regex);
if (matches && matches[1] && matches[2]) {
const value = parseInt(matches[1], 10);
const unit = matches[2].length === 1 ? `${matches[2]}b` : matches[2];
return value * (units[unit] || 1);
}
return 0;
},
handleInputChange(file, fileList) {
if (this.onChange) {
this.onChange(file, fileList);
}
const index = this.uploadFileList.findIndex(item => {
return file.uid === item.id;
});
if (index === -1) {
this.uploadFileList.push({
file,
name: file.name,
state: FileState.Waiting,
progress: 0,
size: TableUtils.formatFileSize(file.size),
speed: '速度计算中...',
id: file.uid,
startTime: new Date().getTime(),
});
}
},
handleProgress(event, file, fileList) {
if (this.onProgress) {
this.onProgress(event, file, fileList);
}
const index = this.uploadFileList.findIndex(item => {
return file.uid === item.id;
});
if (index !== -1) {
let timer;
// 用于计算上传速度
this.uploadFileList[index].state = FileState.Uploading;
// 计算上传速度
const currentTime = new Date().getTime();
// const timeInterval = event.timeStamp / 1000;
const timeInterval = (currentTime - this.uploadFileList[index].startTime) / 1000;
const speed = event.loaded / timeInterval;
this.uploadFileList[index].speed = `${TableUtils.formatFileSize(speed)}/秒`;
// 计算上传进度
const complete = Math.round((event.loaded * 100) / event.total);
// 上传进度超过80%时,模拟进度条
if (complete >= 70) {
if (timer) return;
timer = setInterval(() => {
this.uploadFileList[index].progress += Math.round((100 - this.uploadFileList[index].progress) * 0.1);
if (this.uploadFileList[index].progress > 99 && timer) clearInterval(timer);
}, 1000);
}
this.uploadFileList[index].progress = complete;
}
},
},
watch: {
uploadFileList: {
handler(val) {
this.BtnIcon = this.getBtnIcon(val);
},
deep: true,
},
},
};
</script>
<style scoped></style>
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
uploadAction | 必选参数,上传的地址 | string | - | - |
uploadBtnIcon | 上传按钮的前置图标,一定是elementui的icon字符串 | string | elementui的icon | ‘el-icon-upload’ |
fileSizeLimit | 限制文件大小 | string | eq:‘150MB’、‘20KB’等,大小写不区分 | ‘150MB’ |
accept | 接受上传的文件类型(thumbnail-mode 模式下此参数无效),并且对文件类型上传做限制 | string | - | - |
btnText | 上传按钮文字 | string | - | ‘上传’ |
文件列表UploadCard.vue
文件列表的一些内容,包括文件名称,文件大小,上传速度,上床状态等
其中getIconByFileName方法要注意,设置自己的全局svg
<SvgIcon :icon-class="getIconByFileName(item.file)"></SvgIcon>
<template>
<div class="ax-loading-container">
<template v-if="uploadFileList.length > 0">
<div class="ax-top-label">
<i :style="'color:' + headerIconColor" :class="headerIcon"></i>
{{ getHeaderText }}
</div>
<div class="ax-file-container">
<div class="ax-file-item" v-for="(item, index) in uploadFileList" :key="index">
<div class="ax-file-type-icon">
<SvgIcon :icon-class="getIconByFileName(item.file)"></SvgIcon>
</div>
<div class="ax-file-info">
<div class="ax-file-filename">{{ item.name }}</div>
<div class="ax-file-loadinfo">{{ item.size }} {{ getuploadStatus(item.state) }} {{ getSpeed(item) }}</div>
</div>
<div class="ax-file-prograss">
<!-- 待上传 -->
<i v-if="item.state == 0" class="el-icon-upload2" style="color: #909399"></i>
<!-- 上传中 -->
<el-progress
v-else-if="item.state == 1"
type="circle"
:percentage="item.progress"
:width="30"
:show-text="false"
:stroke-width="3"
></el-progress>
<!-- 已完成 -->
<i v-else-if="item.state == 2" class="el-icon-circle-check" style="color: #67c23a"></i>
<i v-else-if="item.state == 3" class="el-icon-warning" style="color: #f56c6c"></i>
</div>
</div>
</div>
</template>
<template v-else>
<div class="ax-top-label">暂无上传记录</div>
</template>
</div>
</template>
<script>
import lodash from 'lodash';
import { FileState, TableUtils } from './TableUtil';
export default {
props: {
uploadFileList: {
type: Array,
default: () => [],
},
hasWaitingOrUploading: {
type: Boolean,
default: false,
},
},
data() {
return {
errorCount: 0,
waitingOrUploadingCount: 0,
};
},
computed: {
headerIcon() {
const state = lodash(this.uploadFileList).map('state').uniq().value();
// 优先检查是否存在错误状态 (3)
if (state.includes(3)) {
return 'el-icon-warning';
}
// 检查是否有正在上传或准备上传的文件 (0 或 1)
if (state.includes(0) || state.includes(1)) {
return 'el-icon-loading';
}
// 如果以上条件都不满足,则返回成功状态的图标
return 'el-icon-circle-check';
},
headerIconColor() {
if (this.headerIcon === 'el-icon-circle-check') {
return '#67C23A';
}
if (this.headerIcon === 'el-icon-loading') {
return '#409EFF';
}
if (this.headerIcon === 'el-icon-warning') {
return '#f56c6c';
}
return '#409EFF';
},
getHeaderText() {
if (this.waitingOrUploadingCount > 0 || this.errorCount > 0) {
if (this.waitingOrUploadingCount > 0) {
return `正在上传,剩余
${this.waitingOrUploadingCount}
个文件,其中(有${this.errorCount}个失败)`;
}
return `上传任务完成,有
${this.errorCount}个失败`;
}
return '上传任务完成';
},
},
mounted() {},
methods: {
getuploadStatus(state) {
const mapping = ['等待上传', '上传中', '上传成功', '上传失败'];
return mapping[state];
},
getSpeed(item) {
if (item.state === 2 || item.state === 3) {
return '';
}
return item.speed;
},
getIconByFileName(file) {
// 文件扩展名
const ext = file.name.split('.').pop()?.toLowerCase();
// 文件扩展名和图标的映射关系
const mapping = {
audio: 'mp3,wav,aac,flac,ogg,wma,m4a',
doc: 'doc,docx',
pdf: 'pdf',
ppt: 'ppt,pptx',
txt: 'txt',
video: 'mp4,avi,wmv,rmvb,mkv,mov,flv,f4v,m4v,rm,3gp,dat,ts,mts,vob',
xls: 'xls,xlsx',
zip: 'zip,rar,7z',
pic: 'jpg,jpeg,png,gif,bmp,webp',
};
// 根据文件扩展名获取对应的图标
let icon = 'file';
Object.keys(mapping).forEach(key => {
const exts = mapping[key].split(',');
if (exts.includes(ext)) {
icon = key;
}
});
return `icon-${icon}-m`;
},
getFileStatus() {
// 计算state等于FileState.Waiting或FileState.Uploading的元素数量
this.waitingOrUploadingCount = this.uploadFileList.filter(
item => item.state === FileState.Waiting || item.state === FileState.Uploading
).length;
// 计算state等于FileState.Error的元素数量
this.errorCount = this.uploadFileList.filter(item => item.state === FileState.Error).length;
},
},
watch: {
uploadFileList: {
handler(val) {
this.getFileStatus();
},
deep: true,
},
},
};
</script>
<style lang="scss" scoped>
.ax-loading-container {
min-width: 300px;
.ax-top-label {
width: 100%;
min-height: 40px;
// background-color: red;
line-height: 40px;
font-size: 18px;
border-bottom: 1px solid #f7f7f8;
.el-icon-loading {
margin-right: 10px;
}
}
.ax-file-container {
max-height: 300px;
overflow: auto;
width: 100%;
.ax-file-item {
width: 400px;
height: 90px;
display: flex;
align-items: center;
justify-content: space-evenly;
// background-color: red;
.ax-file-type-icon {
width: 60px;
height: 60px;
.SvgIcon {
width: 100%;
height: 100%;
}
}
.ax-file-info {
width: 250px;
height: 60px;
display: flex;
flex-direction: column;
justify-content: center;
.ax-file-filename {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 16px;
font-weight: 600;
color: black;
margin-bottom: 5px;
}
.ax-file-loadinfo {
width: 100%;
font-weight: 400;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #8e8e8e;
}
}
.ax-file-prograss {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
}
}
}
</style>
工具类TableUtil.js
export const FileState = {
// 等待上传
Waiting: 0,
// 上传中
Uploading: 1,
// 上传成功
Success: 2,
// 上传失败
Error: 3,
};
export class TableUtils {
static formatFileSize(fileSize) {
if (fileSize < 1024) {
return `${fileSize.toFixed(2)}B`;
}
if (fileSize < 1024 * 1024) {
let temp = fileSize / 1024;
temp = +temp.toFixed(2);
return `${temp}KB`;
}
if (fileSize < 1024 * 1024 * 1024) {
let temp = fileSize / (1024 * 1024);
temp = +temp.toFixed(2);
return `${temp}MB`;
}
let temp = fileSize / (1024 * 1024 * 1024);
temp = +temp.toFixed(2);
return `${temp}GB`;
}
}
更多推荐
已为社区贡献2条内容
所有评论(0)