vue3+Ant Design Vue Pro +typeScript 封装上传组件以及上传hooks (useImport)

  1. 后台管理常见的导入导出功能页面展示

后台管理基本上都会有导入导出功能,由于每个页面基本上都会有,而且写的时候都会写很多重复的代码,所以我们需要将导入导出按钮封装成一个组件去使用

  1. 使用a-upload注意事项

2.1:用过上传组件的基本都知道,上传无非就是包括了直接使用action上传或者使用自定义函数上传,比如后端需要formData格式,我们此时需要new fromData去传参,由于业务需求我们公司使用的直接用action上传,也就意味着直接传URL给a-upload的action属性即可

2.2:需要注意的是,上传时必须在请求头里携带上token,同时必须要在上传过程中做一些必要的校验(例如:只能上传office等),类似导入之类的业务需求一般都是上传excel,所以我们必须要限制文件上传的类型以及文件的大小(大小一般由产品决定)

2.3:导入需求一般需要点击导入按钮,弹出弹窗,然后由用户下载模板,然后填写完成后点击上传,然后会触发上传的before-upload钩子,如果通过校验,则为done状态,如果未通过,则为error状态,这时我们需要toast提示用户格式不正确XXX之类的提示语

2.4:用户在上传后会有成功失败2种状态,如果失败需要查看失败原因等需求(视自己需求决定)

3.子组件代码演示

<template>
  <!-- 导入弹框 -->
  <tempateModal :modalOption="modalOption" title="导入" @modalCloseHandle="addUserModalClose">
    <template #modalContent>
      <div class="import-template">
        导入模板:
        <a-button ghost type="primary" @click="download"> 下载</a-button>
      </div>
      <div class="upload-result" v-if="uploadSuccess">
        共上传{{ importNum }}条,成功{{ successUploadData }}条,
        <span style="color: red">失败{{ failUploadData }}条</span>
        <span style="color: #1890ff; cursor: pointer" @click="downloadFile"> 下载文件</span>
      </div>
    </template>
    <template #footer>
      <div class="upload-footer">
        <a-upload v-model:file-list="fileList" name="file" :data="uploadData.data" :action="upLoadUrl" :headers="{ Authorization: token }" @change="handleChange" :before-upload="beforeUpload">
          <a-button type="primary"> 上传 </a-button>
        </a-upload>
        <a-button type="addUserModalClose" class="cancel-btn" @click="addUserModalClose"> 取消 </a-button>
      </div>
    </template>
  </tempateModal>
</template>

<script setup lang="ts">
import { ref, reactive, getCurrentInstance, watch, computed } from 'vue'
import tempateModal from '@/components/template-modal/index.vue'
import type { UploadChangeParam, UploadProps } from 'ant-design-vue'
import { useStore } from 'vuex'
import * as TYPES from '@/type/mutation-types'
import storage from '@/utlis/storage'

const getTopMenu = computed(() => store.state.app.activeTopMenu === TYPES['JSLX_01']) // 企业级
const activeWareHouse = computed(() => store.state.app.activeWareHouse) // 当前活跃仓库
// const token = computed(() => store.state.app.token)
const token = storage.get('session', TYPES['ACCESS_TOKEN'])
const store = useStore()
/**额外携带参数 */
const uploadData = reactive({
  data: {
    warehouseId: getTopMenu.value ? '' : activeWareHouse.value.warehouseId,
    warehouseCode: getTopMenu.value ? '' : activeWareHouse.value.warehouseCode,
    roleTypeCode: store.state.app.activeTopMenu,
    systemType: 'WMS',
    parkCode: '',
    id: activeWareHouse.value.warehouseId,
  },
})
/**图片上传接口 */
const upLoadUrl = ref('')
/**上传数量 */
const importNum = ref()
/**下载文件路径 */
const importUrl = ref()
/**上传结果 */
const uploadSuccess = ref<boolean>(false)
/**上传成功数据 */
const successUploadData = ref<any[]>([])
/**上传失败数据 */
const failUploadData = ref<any[]>([])
const fileList = ref([])
const _props = defineProps(['importData'])
const emit = defineEmits(['closeImport', 'download', 'downloadFile', 'uploadSuccess'])
const {
  appContext: {
    config: { globalProperties },
  },
}: any = getCurrentInstance()
let uploadList = reactive({
  default: [],
})
// 导入弹窗对象
const modalOption = reactive({
  data: {
    id: 0,
    visible: false,
    width: 650,
    title: '弹窗',
    confirmLoading: false,
    footer: true,
    wrapClassName: 'test3', //  存在多级必传
  },
})

watch(
  /**
   * @method 初始化
   */
  () => _props.importData,
  (now) => {
    modalOption.data.visible = now.data.importShow
    upLoadUrl.value = now.data.upLoadUrl
  },
  { deep: true }
)

const addUserModalClose = () => {
  emit('closeImport', false)
  uploadSuccess.value = false
  fileList.value = []
}

const beforeUpload: UploadProps['beforeUpload'] = async (file: any) => {
  /**
   * @method 上传前操作
   */

  if (file.type.indexOf('application/') >= 0) {
    const isJpgOrPng = file.type === 'application/vnd.ms-excel' || file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    if (!isJpgOrPng) {
      file.status = 'error'
      globalProperties.$message.error('请上传正确的文件')
      return Promise.reject()
    }
  }
  // const isLtSize = file.size / 1024 < 2000
  // if (!isLtSize) {
  //   file.status = 'error'
  //   globalProperties.$message.error(`上传限制2000KB`)
  //   return Promise.reject()
  // }
}

const download = () => {
  /**
   * @method 下载模板
   */
  emit('download')
}

const handleChange = (info: UploadChangeParam) => {
  /**
   * @method 上传中、完成、失败都会调用这个函数
   */
  if (info.file.status === 'done') {
    if (info.file.response.ResultCode === 200) {
      uploadSuccess.value = true
      successUploadData.value = info.file.response.Tag.successNum
      failUploadData.value = info.file.response.Tag.errorNum
      importNum.value = info.file.response.Tag.importNum
      importUrl.value = info.file.response.Tag.url
      emit('uploadSuccess', info.file.response)
    } else {
      globalProperties.$message.error(info.file.response.Tag)
    }
  }
}

const downloadFile = () => {
  /**
   * @method 下载文件
   */
  emit('downloadFile', importUrl.value)
}

/**
 * 设置上传数量
 * @param importDataNums 上传数量/成功数量/失败数量
 */
const setImportNums = (importDataNums: any) => {
  uploadSuccess.value = true
  importNum.value = importDataNums.importNum
  successUploadData.value = importDataNums.successNum
  failUploadData.value = importDataNums.errorNum
  importUrl.value = importDataNums.url
}

/**
 * 设置上传参数
 * @param data 上传参数
 */
const setUploadData = (data: any) => {
  uploadData.data = data
}
defineExpose({ setImportNums, setUploadData })
</script>

<style scoped lang="less">
.import-template {
  padding: 15px;
  height: 60px;
}
.upload-result {
  border-top: 1px solid #eee;
  padding: 15px;
  height: 60px;
  line-height: 30px;
}
.upload-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
.upload-footer span:first-child {
  width: 100%;
  margin-right: 80px;
}
.cancel-btn {
  align-self: baseline;
  margin-left: 10px;
  position: absolute;
}
</style>

3.1:上述代码可以看出,在上传过程中可能会有额外的参数传给a-upload组件的data属性即可,如果需要修改,可以对外暴漏函数去调用传值修改即可。

3.2: 上传文件路径以及下载模板等一般需要后端配合,我们这里的做法是路径会传一个字符串到子组件里,下载模板需要后端返回URL,前端直接下载文件即可,所以父组件需要传文件上传路径以及下载文件的请求到子组件

3.3: 上传结束后,如果有失败状态的文件,也需要调用后端接口下载excel文件

4.父组件使用

4.1:父组件代码

import templateImport from '@/components/template-import/index.vue'

<templateImport 
:importData="importData" 
@closeImport="importClosed" 
@download="onDownload" 
@downloadFile="onDownloadFile"
>
</templateImport>

const goodsImportData = () => init.getConfig().api + 'baseservice/goods/importData' // 商品文件上传地址

const importData = reactive({
  data: {
    // 控制导入弹窗的打开
    importShow: false,
    // 上传文件地址
    upLoadUrl: '',
  },
})

  /**
   * @method 导入
   */
const handleImport = () => {
  // 给子组件传递上传地址
  importData.data.upLoadUrl = goodsImportData()
  //打开导入弹窗
  importData.data.importShow = true
}

  /**
   * @method 导入关闭
   */
const importClosed = (is: boolean) => {
  importData.data.importShow = is
  // 刷新列表数据
  setHttpTableData()
}

 /**
   * @method 下载模板
   */
const onDownload = () => {
   // 后端接口返回模板URL,前端直接下载excel
  goodsImportTemplate()
    .then((res) => {
      if (res.ResultCode === 200 && res.Success) {
        uploadFile(res.Tag)
      }
    })
    .catch((err) => {
      console.log(err)
    })
}

  /**
   * @method 上传成功后下载的失败/成功的文件
   */
const onDownloadFile = (url: string) => {
  // 子组件发射事件,传出文件URL,上传失败后下载文件
  uploadFile(url)
}

/**
 * @method 下载文件
 */
const uploadFile = (file: string) => {
  window.open(encodeURI(file), 'foo', 'noopener=yes,noreferrer=yes')
}

4.2: 父组件为每一个页面,但是后台管理中父组件有无数个页面,就意味着上述重复的代码要写无数次,那我们直接封装成hook调用即可

5.hook封装使用

5.1: 新建useImport.ts文件

5.2:代码演示

import { reactive, getCurrentInstance, type ComponentInternalInstance } from 'vue'
import { axiosResponse } from '@/type/interface'
type CallBackType = ((...args: any[]) => string) | string
export default function useImport() {
  const { proxy } = getCurrentInstance() as ComponentInternalInstance
  /**导入参数 */
  const importData = reactive({
    data: {
      importShow: false,
      upLoadUrl: '',
    },
  })

  /**
   * @method 打开导入弹窗
   * @param callBack 获取导入地址函数 / 导入地址
   */
  function handleImport<T>(callBack: T extends CallBackType ? T : never) {
    importData.data.upLoadUrl = typeof callBack === 'function' ? callBack() : callBack
    importData.data.importShow = true
  }

  /**
   * @method 下载模板
   */
  async function onDownload(callBack: () => Promise<axiosResponse>) {
    try {
      const { Success, Tag, ResultCode } = await callBack()
      if (ResultCode === 200 && Success) {
        proxy?.$_u.uploadFile(Tag)
      }
    } catch (error) {
      console.log('下载模板error', error)
    }
  }

  /**
   * @method 导入弹窗关闭事件
   * @param is 是否关闭
   * @param callBack 关闭后回调(一般为重新请求)
   */
  function importClosed(is: boolean, callBack: (...args: any[]) => void) {
    importData.data.importShow = is
    callBack()
  }

  return {
    importData,
    importClosed,
    onDownload,
    handleImport,
  }
}

5.3:父组件使用

 <a-button :style="{ marginLeft: '10px' }" @click="handleOpera('import')" v-permission="'CD00102'"> 导入</a-button>

<templateImport 
:importData="importData" 
@closeImport="(is:boolean)=>importClosed(is,setHttpTableData)" 
@download="onDownload(workAreaImportTemplate)" 
@downloadFile="(url:string)=>proxy?.$_u.uploadFile(url)"
>
</templateImport>

import useImport from '@/hooks/useImport'

const { importData, importClosed, onDownload, handleImport } = useImport()

/**
 * 操作按钮
 * @param type 操作类型 add:新增 on:启用 off:禁用 import:导入 export:导出 print:打印
 */
const handleOpera = (type: operaType) => {
  switch (type) {
    case 'add':
      router.push({ name: 'workSpaceAdd' })
      break
    case 'on':
    case 'off':
      handleEnable(type)
      break
    case 'import':
      handleImport(proxy!.$api.workSpaceList_api.workAreaImport)
      break
    case 'export':
      handleExport('工作区', workAreaExportWorkArea, queryInfo)
      break

    default:
      break
  }
}

6.导出hooks封装

6.1:导出功能较为简单,一般是根据条件筛选导出,前端只需要把条件传给后端以及调用导出接口,后端返回URL后前端下载excel即可

6.2:导出hook代码演示



import { getCurrentInstance, type ComponentInternalInstance } from 'vue'
import { axiosResponse } from '@/type/interface'
export default function useExport() {
  const { proxy } = getCurrentInstance() as ComponentInternalInstance

  /**
   * @method 导出
   * @param from 单据来源
   * @param callBack 请求回调
   * @param exportInfo 导出参数
   */
  async function handleExport(from: string, callBack: (exportInfo: Record<string, any>) => Promise<axiosResponse>, exportInfo: Record<string, any>) {
    try {
      const { Success, Tag, ResultCode } = await callBack(exportInfo)
      if (ResultCode === 200 && Success) {
        Tag ? proxy!.$_u.uploadFile(Tag) : proxy!.$message.error(`暂无${from}信息导出数据`)
      }
    } catch (error) {
      console.log(`${from}导出error`, error)
    }
  }

  return {
    handleExport,
  }
}

6.2:代码演示

<a-button :style="{ marginLeft: '10px' }" @click="handleOpera('export')" v-permission="'CD00103'"> 导出</a-button>

import useExport from '@/hooks/useExport'
const { handleExport } = useExport()

const queryInfo = {
  code: '',
  name: '',
  state: '',
  warehouseId: getTopMenu.value ? '' : activeWareHouse.value.warehouseId,
  warehouseRegionId: '',
  warehouseCodeOrName: '',
  workCodeOrName: '',
}

/**
 * 操作按钮
 * @param type 操作类型 add:新增 on:启用 off:禁用 import:导入 export:导出 print:打印
 */
const handleOpera = (type: operaType) => {
  switch (type) {
    case 'add':
      router.push({ name: 'workSpaceAdd' })
      break
    case 'on':
    case 'off':
      handleEnable(type)
      break
    case 'import':
      handleImport(proxy!.$api.workSpaceList_api.workAreaImport)
      break
    case 'export':
      handleExport('工作区', workAreaExportWorkArea, queryInfo)
      break

    default:
      break
  }
}

Logo

前往低代码交流专区

更多推荐