用vue3 写了个上传组件列表,支持取消上传,展示进度条
最近写了个上传附件组件,回想起来之前单位有个功能模块,也是差不多文件上传功能(支持取消、支持删除、支持显示进度条),然后被同事写的是乱七八糟。维护起来非常费劲,简直就是我的眼中钉,肉中刺,一直想把这段代码给拔掉。这次刚好遇到了相似的功能,咱们就从组件设计以及实现一起来看看,怎么实现一个文件上传附件的功能!
- 支持文件上传 (这个肯定要支持某些文件根据自己的业务需求限制)
- 支持文件列表删除
- 支持显示附件上传进度条
- 支持上传中的附件取消
组件设计
咱们先来划分组件设计,以及职责划分。如下图咱们共划分为3个模块:交互层、操作层、数据层。
模块划分示意图:
模块交互示意图:
其实这么划分组件已经能很清晰看明白每个组件要做什么了!那么咱们的文件目录划分如下:
bash
├── index.vue
├── list.vue
├── operator.vue
├── options.js
└── running
├── index.vue
├── item.vue
└── progress.vue
index.vue
负责载各个组件list.vue
负责展示上传的文件列表operator.vue
负责展示操作按钮(上传、删除)options.js
就放了个uuid
和convertFileSize
转换大小的函数running
文件夹中主要负责上传文件,取消文件,展示进度条
相关实现
咱们就来分析下具体实现细节:
- 显示进度条以及支持取消相关功能具体如下:
进度条以及支持取消 running下item.vue代码
:
js
import { ref } from 'vue'
import axios from 'axios'
const percent = ref(0) // 记录百分比
const CancelToken = axios.CancelToken // 取消上传
const axiosCancel = ref(null)
const uploadFile = () => {
const formData = new FormData()
formData.append('file', props.file)
axios
.post(`你自己的API`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
cancelToken: new CancelToken(function executor(c) {
axiosCancel.value = c
}),
onUploadProgress: progressEvent => { // 这里能计算出来上传的百分比
if (progressEvent.lengthComputable) {
percent.value = Math.round((progressEvent.loaded / progressEvent.total) * 100)
}
}
})
.then(() => {
percent.value = 100
setTimeout(() => { // 完成
emits('success', props.uuid)
}, 300)
})
.catch(error => {
if (axios.isCancel(error)) {
console.log('Request canceled', error.message)
} else {
console.log(error)
}
})
}
const oncancel = () => {
axiosCancel.value?.()
isCancel.value = true
setTimeout(() => { // 取消
emits('cancel', props.uuid)
}, 300)
}
- 上传中数据删除、新增逻辑
runningUploadFiles
- 上传完成后删除
- 取消后删除
- 新选文件后新增
新增 index.vue代码
:
vue
<template>
<div class="work-flow-attachment">
<WorkFlowAttachmentRunning v-model="runningUploadFiles" :formId="formId" @complate="onComplate" />
<WorkFlowAttachmentOperator :uploadFiles="uploadFiles" :deleteFiles="deleteFiles" />
<WorkFlowAttachmentList ref="listRef" :formId="formId" v-model="checkData" />
</div>
</template>
<script setup>
import { defineProps, ref } from 'vue'
import { Confirm } from '@/components/pro-confirm/index.js'
import WorkFlowAttachmentOperator from './operator.vue'
import WorkFlowAttachmentRunning from './running/index.vue'
import WorkFlowAttachmentList from './list.vue'
import { deleteAttachmentApi } from '@/api/attachment.js'
import { generateUUID } from './options'
const ATTACHMENT_TYPE = {
VIEW: 'view',
EDIT: 'edit'
}
defineProps({
formId: {
type: String,
default: ''
},
size: {
type: String,
default: 'middle' // middle、small
},
type: {
type: String,
default: 'edit' // edit、view
}
})
const listRef = ref(null)
const runningUploadFiles = ref([])
const checkData = ref([])
const uploadFiles = files => {
files.forEach(file => {
runningUploadFiles.value.push({ // 新增任务
file,
uuid: generateUUID()
})
})
}
const deleteFiles = () => {
if (!checkData.value.length) {
Confirm.error('请至少选中一项数据')
return
}
Confirm.warning({
content: '是否删除选中数据?',
async confirm() {
await deleteAttachmentApi({
ids: checkData.value.join()
})
onComplate()
}
})
}
const onComplate = () => {
listRef.value?.reload?.()
}
</script>
<style lang="less" scoped>
.work-flow-attachment {
padding: 12px 24px;
}
</style>
上传完成和上传取消(利用v-model=""
语法糖更改runningUploadFiles
数据):
running 下 index.vue
代码
vue
<template>
<div class="work-flow-attachment-running">
<AttachmentRunningItem
v-for="item in modelValue"
:key="item.uuid"
v-bind="item"
:formId="formId"
@cancel="onCancel"
@success="onSuccess"
/>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import AttachmentRunningItem from './item.vue'
const emit = defineEmits(['update:modelValue', 'complate'])
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
formId: {
type: String,
default: ''
}
})
// 清除数据
const onCancel = uuid => {
emit(
'update:modelValue',
props.modelValue.filter(item => item.uuid !== uuid)
)
}
// 完成
const onSuccess = uuid => {
onCancel(uuid)
emit('complate')
}
</script>
<style lang="less" scoped>
.work-flow-attachment-running {
> div:last-child {
margin-bottom: 16px;
}
}
</style>
- 删除列表相关实现也是利用**
v-model="" 语法糖更新checkData数据
:**
list.vue
代码:
vue
<template>
<div class="work-flow-attachment-list">
<div v-for="(item, idx) in listData" :key="idx" class="attachment-list__item">
<ProCheckbox v-model="listData[idx].check" @change="onCheckboxChange">
<span class="attachment-list__item--content" @click.stop="onDownload(item.id)">
<span class="attachment-list__item--title">
<span>{{ item.name }}</span>
<span>[{{ item.size }}]</span>
</span>
<span class="attachment-list__item--operator" @click.stop="onDownload(item.id)">
<a-tooltip placement="top">
<template #title>
<span>下载</span>
</template>
<download-outlined />
</a-tooltip>
</span>
<span
class="attachment-list__item--operator"
v-if="isVerifySupportPreview(item.name)"
@click.stop="onPreview(item.id)"
>
<a-tooltip placement="top">
<template #title>
<span>预览</span>
</template>
<folder-view-outlined />
</a-tooltip>
</span>
</span>
</ProCheckbox>
</div>
</div>
</template>
<script setup>
import { defineProps, onMounted, ref, defineEmits, defineExpose, watch } from 'vue'
import ProCheckbox from '@/components/pro-checkbox/index.vue'
import { getAttachmentListApi } from '@/api/attachment.js'
import { convertFileSize } from './options'
const isSupportPreview = ['.png', '.ofd', '.xls', '.xlsx', '.jpg', '.doc', '.docx', '.pdf']
const emit = defineEmits(['update:modelValue', 'complate'])
const props = defineProps({
formId: {
type: String,
default: ''
},
modelValue: {
type: Array,
default: () => []
}
})
const listData = ref([])
const isVerifySupportPreview = (name = '') => {
return isSupportPreview.some(item => name.endsWith(item))
}
watch(
() => props.formId,
() => {
getData()
}
)
const getData = async () => {
if (!props.formId) return
const response = await getAttachmentListApi({ formId: props.formId })
listData.value = (response?.result || []).map(item => {
return {
name: item.fileName,
suffix: item.fileSuffix,
id: item.id,
size: convertFileSize(item.totalSpace),
check: false
}
})
}
const onDownload = id => {}
const onPreview = () => {}
const reload = getData
const onCheckboxChange = () => {
emit(
'update:modelValue',
listData.value.filter(item => item.check).map(item => item.id)
)
}
onMounted(getData)
defineExpose({
reload
})
</script>
<style lang="less" scoped>
.work-flow-attachment-list {
.attachment-list__item {
margin-top: 8px;
}
.attachment-list__item--content {
.attachment-list__item--title {
&:hover {
color: #3080f8;
}
}
.attachment-list__item--operator {
font-size: 16px;
&:hover {
color: #3080f8;
}
}
span {
margin-right: 4px;
}
}
}
</style>
总结
组件设计好,职责划分明确后在进行编码能使代码更好维护,更利于迭代。 咱们在划分完成后只关心数据
runningUploadFiles
以及checkData
处理就行了。希望这个文章对你来说能学习到:
- 文件进度条获取
- 文件上传取消
- 利用
v-model
语法糖使代码数据流转更便捷清晰 - 数据模块的设计划分