vue3项目 - 手写可拖拽带进度监控的文件上传组件
1.实现原理:
- 原生的上传文件组件: <input ref="uploadFileRef" style="display: none" type="file"/>
- 自定义上传区域:
![]()
-
拖拽事件添加(dragover,dragenter,drop),点击事件添加(click)
- 调用原生上传组件的click事件:uploadFileRef.value.click()
- 监听元素上传组件的值回传事件:change
- 进度监控利用axios中的回调函数onUploadProgress实时或是上传文件大小
-
![]()
- 最后实现遮罩层
2.源码:
<template>
<el-dialog
v-bind="$attrs"
:title="props.type === '2' ? '分期结算' : '上传售后明细'"
custom-class="dialog-s select-brand"
destroy-on-close
:close-on-click-modal="false"
@open="handleOpen"
@closed="handleClose"
>
<div>
<span v-if="props.type === '2'" class="upload_title">请先在微信支付平台下载需要结算时间段的明细数据并上传</span>
<div class="upload_box">
<input ref="uploadFileRef" style="display: none" type="file" :accept="props.accept" name="file" @change="uploadChange" />
<!-- 上传 -->
<div class="upload_content " ref="dropArea" @click="uploadFileClick" @dragover.prevent="handleDragOver" @dragenter.prevent="handleDragLeave" @drop.prevent="handleDrop" >
<div v-if="props.type === '1'" class="content_box">
<img class="imge" src="@/assets/upload.png" />
<p class="content">将文件拖到此处,或<el-button type="text">点击上传</el-button></p>
<p class="content desc">支持格式:.csv、.xls、.xlsx格式,<el-button type="text" @click.capture.stop="downLoadModule">下载填写模板</el-button></p>
</div>
<div v-if="props.type === '2'" class="content_box">
<img class="imge mb4" src="@/assets/adtop.png" />
<p class="content mb4"><el-button>选择文件</el-button></p>
<p class="content desc">可直接将文件拖拽到此处进行上传,支持格式:.csv、.xls、.xlsx</p>
</div>
</div>
<!-- 遮罩层-->
<div v-if="maskedShow" class="masked_box"></div>
</div>
<!-- 文件 -->
<div class="upload_files" v-for="item in data.files" :key="item.uid">
<!-- 文件名称 -->
<a @click="downLoadItem(item)">{{ item.name }}</a>
<!-- 文件状态 -->
<div class="upload_files_statue">
<svg-icon :icon="statueList[item.status].icon" :class="item.status == 'ready' ? 'loading' : ''" class="mr4"></svg-icon>
<span
>{{ statueList[item.status].msg }}
<span v-if="item.status == 'ready'">{{ item.percentage }}%</span>
<span v-if="item.status == 'exception'">
<el-icon class="ml4" @click="refresh(item)"><Refresh /></el-icon>
</span>
</span>
</div>
<!-- 删除 -->
<div class="ml4" v-if="props.eidtState == 'edit'">
<el-button type="text" @click="deleteItem(item)">删除</el-button>
</div>
</div>
</div>
<template #footer>
<div class="footer-wrap">
<el-button @click="handleClose">取消</el-button>
<el-button :loading="submitLoading" type="primary" @click="handleSubmit()">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="EditUser">
import { A_createOrEditUser } from '@/api/system/sysuser'
import { A_getRoleList } from '@/api/system/sysrole'
import { reactive, watchEffect, onMounted, computed, nextTick, ref } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import cache from '@/utils/cache'
import { downloadExcel, handleUploadFile } from '@/utils/upload'
import { A_getOssFileById } from '@/api/oss'
const props = defineProps({
curInfo: [Object],
accept: {
type: String,
default: '.csv,.xls,.xlsx'
},
type: {
type: String,
default: '2'
},
size: {
type: Number,
default: 5
},
limt: {
type: Number,
default: 1
},
eidtState: {
type: String,
default: 'edit'
}
})
const emits = defineEmits<{
(e: 'update:modelValue', value: any): void
(e: 'update'): void
}>()
const maskedShow = computed(() => {
return props.eidtState !== 'edit' || data.files.length >= props.limt
})
//打开
const handleOpen = () => {
data.files = []
props.curInfo.id && getOssFileById([props.curInfo.id])
}
let submitLoading = ref<Boolean>(false)
//确认上传核销明细
const handleSubmit = () => {
submitLoading.value = true
}
//关闭
const handleClose = () => {
emits('update:modelValue', false)
}
/**
*
* 手撸文件上传功能
* 1.实现拖拽功能
* 2.拖拽区域可以下载上传模板功能
* 3.上传文件进度监控
* 4.上传中的文件也可以直接下载
*
*
*
*/
const uploadFileRef = ref(null)
const dropArea = ref(null)
const statueList = {
ready: {
icon: 'loading',
msg: '上传中'
},
succeed: {
icon: 'succfull',
msg: '上传成功'
},
exception: {
icon: 'fail',
msg: '上传失败'
}
}
const data = reactive({
files: []
})
const handleDragOver = (e: any) => {
e.preventDefault()
dropArea.value.classList.add('dragover')
}
const handleDragLeave = (e: any) => {
e.preventDefault()
dropArea.value.classList.remove('dragover')
}
const handleDrop = (e: any) => {
e.preventDefault()
dropArea.value.classList.remove('dragover')
const files = e.dataTransfer.files
for (let i = 0; i < files.length; i++) {
const curryFile = handleStart(files[i])
addFilde(curryFile)
}
}
//点击上传事件
const uploadFileClick = () => {
uploadFileRef.value.click()
}
const addFilde = (curryFile: any) => {
if (checkFilesSize(curryFile)) {
data.files.push(curryFile)
uploadFiles(curryFile)
}
}
//原生上传事件
const uploadChange = (e: any) => {
const chooseFile = e.target.files[0]
e.target.value = ''
const curryFile = handleStart(chooseFile)
addFilde(curryFile)
}
//检测文件大小
const checkFilesSize = (rawFile: any): boolean => {
if (data.files.length >= props.limt) {
ElMessage.warning(`最多上传${props.limt}个文件`)
return false
}
console.log(rawFile,'acceptList')
if (!['application/vnd.ms-excel','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','text/csv'].includes(rawFile.raw.type)) {
ElMessage.error(`上传文件格式错误,仅支持${props?.accept}`)
return false
}
if (rawFile.size / 1024 / 1024 > props.size) {
ElMessage.error(`文件大小不能超过${props.size}MB!`)
return false
}
return true
}
//上传文件准备
const handleStart = (rawFile: any) => {
rawFile.uid = Date.now()
return {
status: 'ready',
name: rawFile.name,
size: rawFile.size,
percentage: 0,
uid: rawFile.uid,
raw: rawFile,
serviceFilesUrl: '',
serviceFlieName: '',
serviceId: -1
}
}
//上传文件
const uploadFiles = (rawFile: any) => {
const formData = new FormData()
formData.append('file', rawFile.raw)
axios({
method: 'POST',
url: '/backend-platform/sys/oss/upload',
data: formData,
headers: {
token: cache.getToken()
},
onUploadProgress: function (progressEvent) {
let cuurFile = data.files.find(item => item.uid == rawFile.uid)
cuurFile && (cuurFile.percentage = Number(((progressEvent.loaded / progressEvent.total) * 95).toFixed(2)))
}
})
.then(res => {
let cuurFile = data.files.find(item => item.uid == rawFile.uid)
const { data: row } = res
if (row.code === 0 && cuurFile) {
cuurFile.percentage = 100
cuurFile.serviceFilesUrl = row.data[0]?.ossUrl || ''
cuurFile.serviceFlieName = row.data[0]?.fileName || ''
cuurFile.id = row.data[0]?.id
cuurFile.status = 'succeed'
} else {
ElMessage.error(row.msg)
cuurFile.percentage = 100
cuurFile.serviceFilesUrl = ''
cuurFile.id = ''
cuurFile.serviceFlieName = ''
cuurFile.status = 'exception'
}
})
.catch(err => {
rawFile.percentage = 100
rawFile.serviceFilesUrl = ''
rawFile.id = ''
rawFile.serviceFlieName = ''
rawFile.status = 'exception'
})
}
//下载文件
const downLoadItem = (file: any) => {
if (file.serviceFilesUrl) {
downloadExcel(file.serviceFilesUrl, file.serviceFlieName)
} else {
handleUploadFile(file.raw, file.name)
}
}
//删除
const deleteItem = (detail: any) => {
const index = data.files.findIndex(item => item.uui == detail.uui)
if (index > -1) {
data.files.splice(index, 1)
}
}
//重新上传
const refresh = (item: any) => {
uploadFiles(item)
}
//下载模版
const downLoadModule = () => {
downloadExcel('https://ycbsaas-bucket.oss-cn-hangzhou.aliyuncs.com/images/20231101/b87532037f354340bc632e52a348f633.xls', '模版.xls')
}
//查询文件信息
const getOssFileById = (ids: any) => {
ids.length &&
A_getOssFileById({ ids }).then(res => {
const imageDatas = res.data as []
imageDatas.forEach((item: any) => {
let curryFile = {
status: 'succeed',
name: item.fileName,
size: '',
percentage: 0,
uid: item.id,
raw: '',
serviceFilesUrl: item.ossUrl,
serviceFlieName: item.fileName,
serviceId: item.id
}
data.files.push(curryFile)
})
})
}
</script>
<style scoped lang="scss">
.upload_title {
display: inline-block;
padding: 0 8px;
margin-bottom: 8px;
}
.upload_box {
position: relative;
z-index: 99999;
.masked_box {
position: absolute;
width: 100%;
height: 100%;
top: 0;
background-color: rgba(#9999, 0.2);
cursor: no-drop;
}
.upload_content {
border: 1px dashed #cccc;
height: 172px;
cursor: pointer;
.content_box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.imge {
width: 70px;
height: 70px;
}
.desc {
padding: 0 40px;
}
}
.upload_content:hover {
border-color: var(--el-color-primary);
}
.dragover {
border-color: var(--el-color-primary);
border-width: 2px;
}
}
.upload_files {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
padding: 0 16px;
border-bottom: 1px solid rgba(#9999, 0.2);
> a {
color: #02a7f0;
}
.upload_files_statue {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
::v-deep(.el-icon) {
vertical-align: middle;
}
}
.loading {
animation: loading 1s linear infinite;
}
@keyframes loading {
0% {
transform: rotateZ(0deg);
}
8% {
transform: rotateZ(30deg);
}
16% {
transform: rotateZ(60deg);
}
24% {
transform: rotateZ(90deg);
}
32% {
transform: rotateZ(120deg);
}
40% {
transform: rotateZ(150deg);
}
48% {
transform: rotateZ(180deg);
}
56% {
transform: rotateZ(210deg);
}
64% {
transform: rotateZ(240deg);
}
72% {
transform: rotateZ(270deg);
}
81% {
transform: rotateZ(300deg);
}
89% {
transform: rotateZ(330deg);
}
100% {
transform: rotateZ(360deg);
}
}
}
</style>


浙公网安备 33010602011771号