当前位置: 首页 > news >正文

单个及批量上传文件和文件夹思路——AntDesign

一、问题

antDesign的Upload组件设置了direcotry属性,允许上传文件夹后;点击上传时无法 选择单个文件。刚开始的做法是直接重新写了upload组件上传框的点击事件,手动调用 window.showOpenFilePicker允许点击上传单个文件;但是需求又要求点击的时候既支持 选择文件和文件夹。由于文件夹和文件选择框的弹出API不同,必须分解为两个交互。

一个框支持上传单个文件,一个支持批量上传。

二、解决方法

1.点击上传:弹出文件夹选择框和文件选择框因选择的 文件类型完全不同,交互也不相同,所以对应不同的API;选择文件和文件夹的交互必须分开

文件夹选择框:window.showDirectoryPicker()

文件选择框:window.showOpenFilePicker()

2.拖拽上传:拖拽文件夹和文件的本质是相同的,可以一次性实现拖拽文件夹和文件,交互无须分开

因为浏览器http协议限制不允许一次性访问本地的文件夹,所以无论是 点击还是拖拽的批量上传文件或文件夹,本质上都是一个一个的上传文件

axios直接传文件夹会报错: net::ERR_FILE_NOT_FOUND,所以只能一个一个的遍历上传

3.拖拽时文件夹上传框,不允许上传单个文件

drop回调 判断如果是单个文件element.webkitGetAsEntry().isFile,则从文件列表中删除

4.拖拽时文件上传框,不允许上传文件夹

不开启dircotry选项

5.完整代码

<template>
  <!-- 批量上传电子发票 -->
  <AModal visible title="批量上传电子发票" :maskClosable="false" centered :width="720" @cancel="handleClose">
    <ASteps :current="current" :items="stepArray" size="small" class="steps-wrap">
      <AStep
        v-for="(item, itemKey) of stepArray"
        :key="itemKey"
        :class="[{ 'font-semibold text-cb06 ': itemKey === current }]"
        :title="item.title"
      ></AStep
    ></ASteps>
    <div v-loading="loading" class="modal-area mt-6">
      <AForm>
        <!-- 上传文件 -->
        <template v-if="isUpload">
          <div>
            <div class="tip-area flex justify-between">
              <div class="limit text-ctext02">
                仅支持<span>pdf</span>格式,<span>单个文件最大1M,最多可传50个</span>,文件名称需为<span>订单编号</span>
              </div>
              <div class="text-center">
                <ATooltip overlayClassName="white-tooltip file-tooltip" placement="bottom">
                  <AButton size="small" type="link" @click.stop>查看示例图 </AButton>
                  <template #title>
                    <div class="text-center">
                      <div class="text-[#000]">
                        <SvgIcon name="pdf"></SvgIcon> <span class="bg-cr06 bg-opacity-30">订单ID</span> .pdf
                      </div>
                      <div class="tip">
                        <div class="text-cr06">需为订单编号</div>
                        <div class="text-ctext01">方便上传时快速识别订单</div>
                      </div>
                    </div>
                  </template>
                </ATooltip>
              </div>
            </div>
            <div class="upload-area flex gap-6">
              <div class="flex-1">
                <AUploadDragger
                  v-model:fileList="fileList"
                  name="file"
                  accept=".pdf"
                  :maxCount="50"
                  :show-upload-list="false"
                  :before-upload="beforeUpload"
                  @change="handleChangeFile"
                >
                  <div class="p-4">
                    <p>
                      <SvgIconfont
                        symbol-name="icon_a-download_linexiazai"
                        :class="[' text-[40px] text-cbd02']"
                      ></SvgIconfont>
                    </p>
                    <p class="flex justify-center items-center font-semibold !mt-4 !mb-2 text-ctext02">
                      <SvgIconfont
                        symbol-name="icon_a-file_2_linewendang2"
                        color="#B3B3B3"
                        class="text-[16px]"
                      ></SvgIconfont>
                      <span class="ml-1">单个文件上传</span>
                    </p>
                    <p class="text-ctext02 text-[12px]">点击或将【文件】拖拽到这里上传</p>
                  </div>
                </AUploadDragger>
              </div>
              <div class="flex-1">
                <AUploadDragger
                  v-model:fileList="fileList"
                  name="file"
                  multiple
                  directory
                  accept=".pdf"
                  :maxCount="50"
                  :show-upload-list="false"
                  :before-upload="beforeUpload"
                  @change="handleChangeFile"
                  @drop="handleDropFile"
                >
                  <div class="p-4">
                    <p>
                      <SvgIconfont
                        symbol-name="icon_a-download_linexiazai"
                        :class="[' text-[40px] text-cbd02']"
                      ></SvgIconfont>
                    </p>
                    <p class="flex justify-center items-center font-semibold !mt-4 !mb-2 text-ctext02">
                      <SvgIconfont
                        symbol-name="icon_a-folder_linewenjianjia"
                        color="#B3B3B3"
                        class="text-[16px]"
                      ></SvgIconfont>
                      <span class="ml-1">文件夹上传</span>
                    </p>
                    <p class="text-ctext02 text-[12px]">点击或将【文件夹】拖拽到这里上传</p>
                  </div>
                </AUploadDragger>
              </div>
            </div>
          </div>

          <!-- 文件列表 -->
          <template v-if="fileList?.length">
            <ul class="file-list-area">
              <li v-for="(file, fileKey) of fileList" :key="fileKey">
                <div class="flex justify-between">
                  <div>
                    <ASpin :indicator="indicator" :class="['!mr-1', { '!opacity-0': !file.loading }]" />
                    <SvgIcon name="pdf"></SvgIcon>
                    <span :class="['ml-1', { 'text-cr06': file.errMsg }]">{{ file.name }}</span>
                    <template v-if="file.errMsg">
                      <div class="text-sm text-cr06 ml-4">
                        {{ file.errMsg }}
                      </div>
                    </template>
                  </div>
                  <div :class="['delete', { '!block': file.errMsg }]" title="删除文件" @click="handleDelete(fileKey)">
                    <SvgIconfont
                      symbol-name="icon_a-delect_lineshanchu"
                      :class="['text-[16px] text-ctext02', { '!text-cr06': file.errMsg }]"
                    ></SvgIconfont>
                  </div>
                </div>
              </li>
            </ul>
          </template>
        </template>

        <!-- 关联订单 -->
        <template v-else-if="isRelatedOrder">
          <ATable :class="['mt-[22px]']" :columns="columns" :dataSource="tableData" bordered :pagination="false">
            <template #bodyCell="{ column, record }">
              <template v-if="column.key === 'filename'">
                <div class="h-8 leading-[32px]">
                  <SvgIcon name="pdf"></SvgIcon> <span>{{ record.filename }}</span>
                </div>
              </template>
              <template v-if="column.key === 'orderId'">
                <template v-if="record.isEdit.value">
                  <!-- <AInput v-model:value="record.orderId.value" class="!w-max !rounded-[4px]"> </AInput> -->
                  <DigitInput v-model:value="record.orderId.value" class="!w-max !rounded-[4px]"> </DigitInput>
                </template>
                <template v-else>
                  <div class="h-8 leading-[32px]">
                    <span v-if="record.orderId.value">{{ record.orderId.value }} </span>
                    <template v-else>
                      <SvgIconfont symbol-name="icon_a-error_warning_filljinggao" class="text-cr06 text-base">
                      </SvgIconfont>
                      <span> 未识别到订单ID </span>
                    </template>
                    <EditOutlined :style="{ color: '#1890FF' }" @click="record.isEdit.value = true" />
                  </div>
                </template>
              </template>
            </template>
          </ATable>
        </template>

        <!-- 上传完成 -->
        <template v-else-if="isFinish">
          <div class="finish-wrap">
            <div class="finish-item mb-4">
              <SvgIconfont
                symbol-name="icon_a-checkbox_circle_fillxuanzegouxuankuang"
                :class="['text-cg06 text-[19px]']"
              ></SvgIconfont>
              上传成功 <span> {{ finishResult.successCount }}</span
              >个
            </div>
            <div class="flex justify-between finish-item">
              <div>
                <SvgIconfont
                  symbol-name="icon_a-error_warning_filljinggao"
                  :class="['text-cr06 text-[19px]']"
                ></SvgIconfont>
                上传失败
                <span class="text-cr06"> {{ finishResult.failCount }}</span
                >个
              </div>
              <AButton v-if="finishResult.failCount" type="link" class="!text-base" @click="handleDownloadFailData">
                下载失败原因
              </AButton>
            </div>
          </div>
        </template>
      </AForm>
    </div>
    <template #footer>
      <AButton v-if="isUpload" @click="handleClick('cancel')">取消</AButton>
      <AButton v-if="isRelatedOrder" @click="handleClick('cancel')">上一步</AButton>
      <AButton v-if="isUpload" type="primary" @click="handleClick('relatedOrder')">去关联订单</AButton>
      <AButton v-if="isRelatedOrder" type="primary" @click="handleClick('confirmUpload')">确认上传</AButton>
      <AButton v-if="isFinish" type="primary" @click="handleSure">确认</AButton>
    </template>
  </AModal>
</template>
<script lang="tsx" setup>
import DigitInput from '@/components/DigitInput.vue'

import { requestUploadApplyInvoice, requestBatchInvoiceAssociatedOrder } from '@/api/order-management/apply-invoice'
import { Modal } from 'ant-design-vue'
import { EditOutlined, Loading3QuartersOutlined } from '@ant-design/icons-vue'
import { aoaToSheetXlsx } from '@/utils/excel'
import { h } from 'vue'
import type { BatchInvoiceResult } from '@/api/order-management/apply-invoice/model'

const emit = defineEmits(['close', 'sure'])

/**
 * 步骤条
 **/
const current = ref<number>(0)
const stepArray = [
  { title: '上传发票' },
  {
    title: '关联未开发票的订单'
  },
  {
    title: '上传完成'
  }
]
function next() {
  current.value++
}
function previous() {
  current.value--
}

const fileList = ref<FileList & { errMsg?: string; loading?: boolean; url?: string; uid?: string; name?: string }[]>([])
function handleClose() {
  emit('close')
}

/**
 * 1.上传文件
 */
const isUpload = computed(() => current.value === 0)
function beforeUpload() {
  return false
}
const maxSize = 1

async function handleChangeFile({ file }) {
  fileList.value.splice(fileList.value.length - singleFileArray.length)
  singleFileArray = []
  let targetIndex = fileList.value.findIndex((element) => element.uid === file.uid)
  if (targetIndex === -1) {
    return
  }
  let targetFile = fileList.value[targetIndex]

  targetFile.loading = true
  dealFile(file, targetIndex)
}

async function dealFile(file: File, targetIndex: number) {
  let { errMsg, url } = (await requestUploadApplyInvoice({ fileList: file }))[0]
  let targetFile = fileList.value[targetIndex]

  targetFile.errMsg = errMsg
  targetFile.url = url
  targetFile.loading = false

  if (!(file.size / 1024 / 1024 <= maxSize)) {
    targetFile.errMsg = '单个文件最大1M'
  }
}

let singleFileArray: any[] = []
async function handleDropFile(event: { dataTransfer: { items: any[] } }) {
  event.dataTransfer.items.forEach((element) => {
    /** 文件夹上传框不允许上传单个文件 */
    if (element.webkitGetAsEntry().isFile || element.type) {
      // console.log('单个文件', element, element.getAsFile())
      singleFileArray.push(element.getAsFile())
    } else {
      // console.log('文件夹', element, element.getAsFile())
    }
  })
}
/**
 * 删除
 */
function handleDelete(fileIndex: number) {
  fileList.value = fileList.value.filter((element, elementIndex) => elementIndex !== fileIndex)
}

//     <SvgIconfont symbol-name="icon_a-loading_linejiazai" class="text-base"></SvgIconfont>
const indicator = h(Loading3QuartersOutlined, {
  style: {
    fontSize: '12px',
    fontWeight: 'bold',
    color: '#B3B3B3'
  },
  spin: true
})

/**
 * 操作
 */
const loading = ref(false)
function handleClick(type: string) {
  switch (type) {
    case 'cancel':
      handleCancel()
      break
    case 'relatedOrder':
      handleRelatedOrder()
      break
    case 'confirmUpload':
      handleConfirmUpload()
      break
    default:
      break
  }
}

/**
 * 取消
 */
function handleCancel() {
  if (isUpload.value) {
    handleClose()
  } else if (isRelatedOrder.value) {
    previous()
  }
}

/**
 * 2.去关联订单
 */
const columns = [
  {
    title: '电子发票pdf文件',
    key: 'filename'
  },
  {
    title: '关联订单',
    key: 'orderId',
    width: 400
  }
]

const tableData = computed(() => {
  return fileList.value.map((element) => {
    return {
      filename: element.name,
      orderId: ref(getOrderId(element.name)),
      isEdit: ref(false),
      url: element.url
    }
  })
})
function getOrderId(filename: string) {
  let temp = filename.replace('.pdf', '')
  if (/^\d+$/.test(temp)) {
    return temp
  } else {
    return ''
  }
}
const isRelatedOrder = computed(() => current.value === 1)
function handleRelatedOrder() {
  if (fileList.value.length) {
    let hasError = fileList.value.find((element) => element.errMsg)
    if (hasError) {
      confirmModal('监测到您上传的部分文件不是PDF格式,请确认所有文件均为PDF格式且上传成功,再进行订单关联!')
      return
    }
    next()
  } else {
    confirmModal('请上传电子发票pdf文件后,再进行订单关联!')
  }
}

function confirmModal(text: string) {
  Modal.confirm({
    content: text,
    icon: '',
    cancelButtonProps: {
      style: 'display:none'
    }
  })
}

/**
 * 3.上传完成
 */
const isFinish = computed(() => current.value === 2)
const finishResult = reactive<BatchInvoiceResult>({})
let loadingTimeout: ReturnType<typeof setTimeout> | null = null
async function handleConfirmUpload() {
  loading.value = true
  try {
    let result = await requestBatchInvoiceAssociatedOrder({
      invoiceList: tableData.value.map((element) => {
        return {
          filename: element.filename,
          orderId: element.orderId.value,
          url: element.url
        }
      })
    })
    Object.assign(finishResult, result)
    if (loadingTimeout) {
      clearTimeout(loadingTimeout)
      loadingTimeout = null
    }
    loadingTimeout = setTimeout(() => {
      loading.value = false
      next()
    }, 1000)
  } catch (error) {
    loading.value = false
    console.log('error', error)
  }
}

function handleDownloadFailData() {
  aoaToSheetXlsx({ header: finishResult.header, data: finishResult.errList, filename: finishResult.filename })
}

function handleSure() {
  emit('sure', { type: 'batchUploadInvoice' })
  handleClose()
}
</script>
<style lang="less" scoped>
@import url('@/styles/modal.less');

:deep(.anticon-check) {
  font-size: 13px;
}
.steps-wrap {
  padding: 0 24px;

  .ant-steps-item {
    flex: none;

    &:nth-child(1) {
      width: 216px;
    }
    &:nth-child(2) {
      width: 296px;
    }
    .ant-steps-item-title,
    .ant-steps-icon {
      font-size: 16px;
    }
  }
  &.ant-steps-small.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item {
    padding-left: 8px;
    &:first-child {
      padding-left: 0;
    }
  }
}
.modal-area {
  .tip-area {
    margin-bottom: 16px;
    padding: 10px 16px;
    background: #e6f7ff;
    border: 1px solid #1890ff;
    border-radius: 4px;
    .limit {
      span {
        color: #f53f3f;
      }
    }
  }

  .file-list-area {
    li {
      padding: 10px 4px 10px 12px;
      border-bottom: 1px solid #f0f0f0;
      .delete {
        cursor: pointer;
        display: none;
      }
      &:hover {
        background: #f0f0f0;
        .delete {
          display: block;
        }
      }
    }
  }

  .finish-wrap {
    margin-top: 12px;
    font-size: 16px;
    .finish-item {
      padding: 16px;
      background: #fafafa;
      border-radius: 4px;
    }
  }
}
</style>
<style lang="less">
@import '@/styles/tooltip.less';
.file-tooltip {
  .tip {
    background: url('@/assets/fileTip.png');
    width: 230px;
    padding: 9px 0 6px;
    margin-top: 2px;
  }
}
</style>

三、总结

1.文件选择弹框和文件夹选择弹框api不一样,必须分开

2.拖拽上传可以同时支持文件和文件夹上传

3.批量上传的时候如果全部不符合想要的类型如何处理?

目前是发现一个不符合类型,就提示

没有判断是否全部不符合:需求上其实是希望如果全部不符合才提示(减少提示次数)—— 目前没有空处理(因为里面涉及到 文件夹中嵌套文件夹等递归问题,比较复杂,待完善)

4.欸,以前都没有做过,所以实现的时候没有思路,特此记录。

相关文章:

  • LeetCode 124.二叉树中的最大路径和
  • 深度学习与传统算法在人脸识别领域的演进:从Eigenfaces到ArcFace
  • 线性表的顺序表示
  • QuecPython + MQTT:物联网设备通信实战指南
  • 解决前端文字超高度有滚动条的情况下padding失效(el-scrollbar)使用
  • 鸿蒙跳转到系统设置app界面
  • 虚幻基础:GAS
  • ngx_http_module_t
  • Java调用Oss JDk删除指定目录下的所有文件
  • 【最大异或和——可持久化Trie】
  • 设计模式-桥接模式
  • C语言文件管理详解(上)
  • 下拉菜单+DoTween插件
  • 基于ssm图文印务交互系统小程序(源码+lw+部署文档+讲解),源码可白嫖!
  • Docker 使用指南
  • Django Rest Framework 创建纯净版Django项目部署DRF
  • 每日一题——二叉树的三种中序遍历方法
  • C语言基础要素(017):退出条件循环:do-while
  • Qt 实现波浪填充的圆形进度显示
  • 谈谈 undefined 和 null
  • 欧盟和英国对俄新一轮制裁将中国公司也列入名单,外交部回应
  • 外交部:中方高度重视同太平洋岛国的关系,双方友好合作关系正不断深化和发展
  • 重庆黔江一足疗养生馆负责人涉嫌违法犯罪被移送检察机关
  • 马上评|当众猥亵女演员,没有任何开脱理由
  • 演员朱媛媛去世,其丈夫辛柏青发讣告
  • 每日475.52元!最高检公布最新侵犯公民人身自由的赔偿金标准