vue3项目 - 手写可拖拽带进度监控的文件上传组件

发布时间 2023-11-02 14:14:41作者: 激动1223

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>