Vue+Ant: 自定义上传文件时机以实现导入(简易通用组件封装)

MarkTime: 2024-12-16 08:47:00

LogTime: 2024-12-16 07:00:00

LastModifiedTime: 2025-01-31 00:01:06


导入相关的组件, 觉得有一些用到的东西挺具有通用性, 蛮值得记录一下的

知识点:

  1. 利用axios结合表单传输文件;
  2. ant-desgisn-vue的原生a-upload基础上, 使用自定义上传文件方式 达到 延时触发文件上传的目的;
  3. 是一个导入的封装简易组件(提供①下载导入模板; ②上传装载着导入数据的文件;)
    • ①和②涉及到的后端代码部分不进行展示, 利用EasyExcel完成, 下载模板的时候接收

效果

逻辑

  1. 点击 蓝字 触发导入模板下载 并自定义保存位置;
  2. 在此导入模板基础上进行数据增加/修改;
  3. 点击"选取文件", 选择刚刚操作过的文件;
  4. 点击"导入", 等待后端校验完毕返回导入结果.

附图








源码

自身组成

ls-import.vue

<template>
  <div class="ls-import">
    <!-- 遮罩 -->
    <a-spin 
      :spinning="isPageLoading" 
      :indicator="h(LoadingOutlined, { style: { fontSize: '24px' }, spin: false})"
    >
      <div class="ls-box redure50">
        <a-form style="padding: 0 20px 10px;" name="formRef" ref="formRef" v-model:model="form">
          <a-row>
            <a-col :span="24">
              <div class="remark">
                <exclamation-circle-outlined style="padding-right: 5px;"/>
                <span>备注:请使用模板进行导入!</span>
              </div>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="24">
              <a-form-item label="模板下载:" style="font-size: 16px; display: flex; flex-direction: row; align-items: baseline;">
                <a @click="toDownloadTmpl" href="javascript: void(0)" style="text-decoration: underline;">{{ props.tmplName }}</a>
              </a-form-item>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="1" :xl="{span: 24}" :xs="24" style="width: 110px; display: flex; flex-direction: column; align-items: center;">
              <div :class="['ls-upload',fileList.length>0?'ls-upload-fill':'']" >
                <!-- <a-upload
                  v-model:file-list="fileList"
                  :accept="'.xls,.xlsx'"
                  :action="actionUrl"
                  :before-upload="beforeUpload"
                  :custom-request="customRequest"
                  @change="handleChange"
                  @download="handleDownload"
                  @reject="handleReject"
                  @drop="handleDrop"
                  @preview="handlePreview"
                > -->
                <a-upload
                  v-model:file-list="fileList"
                  :max-count="1"
                  :accept="'.xls,.xlsx'"
                  :before-upload="onBeforeUpload"
                  @change="onChangeUpload"
                >
                  <a-button :style="{ border:'none' }"><upload-outlined></upload-outlined>选取文件</a-button>
                  <template #previewIcon>
                    <link-outlined :style="{width: '14px',color:'#BFBFBF'}"/>
                  </template>

                  <template #removeIcon>
                    <close-circle-filled :style="{width: '14px',color:'#BFBFBF'}"/>
                  </template>
                </a-upload>
              </div>
            </a-col>
          </a-row>
        </a-form>
      </div>

      <div class="ls-buttom-btn center">
        <a-button @click="toImport" type="primary" style="margin-right: 10px;">导入</a-button>
        <a-button @click="toCancel">取消</a-button>
      </div>
    </a-spin>
  </div>
</template>

<script>
  export default {
    name: 'ls-import'
  }
</script>

<script setup>
  import { ref, reactive , computed, defineProps, defineEmits, h } from 'vue';
  import { Upload, message } from 'ant-design-vue';
  import axios from "axios";

  const props = defineProps({
    // 模板名称
    tmplName: {
      type: String,
      default: ''
    },
    // 模板下载地址
    tmplUrl: {
      type: String,
      default: '',
    },
    // 文件上传地址
    uploadUrl: {
      type: String,
      default: '',
    }
  })

  const form = reactive({})
  const iFile = ref({});
  const fileList = ref([]);
  const isPageLoading = ref(false); // 遮罩
  const tmplUrlCmpt = computed(() => {
    let url = window.server.baseURL + props.tmplUrl
    // console.log("tmplUrlCmpt"+url)
    return url;
  })
  const uploadUrlCmpt = computed(() => {
    let url = window.server.baseURL + props.uploadUrl;
    // console.log("uploadUrlCmpt"+url)

    return url;
  })

  /**
   * 模板下载
   */
  const toDownloadTmpl = () => {
    window.open(tmplUrlCmpt.value, '_blank') // 触发新的标签页闪出并弹出模板下载保存窗口
  }

  /**
   * 文件上传 前置校验
   */
  const onBeforeUpload = (file) => {
    let acceptTypeList = ['xls', 'xlsx'];
    let fileSuffix = file.name.substring(file.name.lastIndexOf('.') + 1);
    let fileMaxSize = 10*1024*1024; // M

    let isAcceptType = acceptTypeList.indexOf(fileSuffix) !== -1

    if (!isAcceptType) {
      // message.destroy(); // 全局清除
      message.warning('上传文件格式不符合规范, 支持格式为: .xls,.xlsx');
      return Upload.LIST_IGNORE;
    }

    if (file.size > fileMaxSize) {
      message.warning('选择的文件大小超过限制, 大小不得超过: 10M');
      return Upload.LIST_IGNORE;
    }

    iFile.value = file;

    return false; // 阻止默认上传事件
  }

  /**
   * a-upload 组件没有针对上传的成功失败有专门的回调函数
   */
  const onChangeUpload = (info) => {
    if (info.fileList[0].uid===iFile.value.uid) {
      iFile.value = info.fileList[0].originFileObj;
    }
    // console.log('onChangeUpload: ' + e)
  }

  /**
   * 自定义上传
   */
  const toCustomRequest = (e) => {
    if (!e?.file) {
      message.warning('请先选择导入文件! ', 'warning', );
      return;
    }

    isPageLoading.value = true;

    let formData = new FormData();
    // console.log(e.file); // 确保它是 File 对象
    // console.log(Object.prototype.toString.call(e.file)); // [object File]
    formData.append("file", e.file);

    axios({
      url: uploadUrlCmpt.value,
      method: 'POST',
      data: formData,
      transformRequest: [function(data, headers) {
        // 去除post请求默认的Content-Type
        delete headers.post['Content-Type']
        return data
      }],
      headers: {
        // 'Content-type': 'multipart/form-data',
        'tk': localStorage.getItem("tk") // 这里只是自定义携带一下需要的当前登录用户信息
      },
    }).then(res => { // 成功
      // 成功之后就可以把表单关调了
      if (res.data.success) {
        message.success(res.data.resultObject, 1);
        emit('onSuccess')
      } else {
        message.error(res.data.resultObject, 1);
      }
    }).catch(err => { // 失败
      fileList.value = [];
      message.error(`文件上传失败! `, 1);
    }).finally(() => { isPageLoading.value = false; })
  }

  /**
   * 行为 导入
   */
  const toImport = () => {
    toCustomRequest({ file: iFile.value });
  }

  /**
   * 取消
   */
  const toCancel = () => {
    emit('onClose')
  }

  // 回调
  const emit = defineEmits(['onClose', 'onSuccess'])

</script>

<style lang="less" scoped>
  .ls-import {
    position: relative;
    height: 100%;
    width: 100%;
    .remark {
      color: #ff3118;
      font-size: 14px;
      height: 30px;
    }
  }
</style>

<style lang="less">
  .ant-spin-nested-loading {
    height: 100% !important;
  }
  .ant-spin-container {
    height: 100% !important;
  }

  .ls-upload {
    border-radius: 4px;
    .ant-upload-select {
      width: 100%;
      border-radius: 4px;
      border: 1px solid #D9D9D9;
    }
    .toRight {
      border-left: 1px solid #BFBFBF !important;
      float: right;
      background: #F7F5F7;
    }
    .ant-upload-list-picture-container {
      padding: 0 20px;
    }
    .ant-upload-list-text-container {
      padding: 0 20px;
    }
  }

  .ls-upload-fill {
    padding-bottom: 10px;
    border: 1px solid #D9D9D9;
    .ant-upload-select {
      border: none;
      border-bottom: 1px solid #D9D9D9;

    }
  }
    
</style>

ls-global.css

  .ls-box {
    position: relative;
    height: 100%;
    overflow-y: auto;
  }
  
  .redure50 {
    height: calc(100% - 50px);
  }
  
  .ls-buttom-btn {
    background-color: #fff; 
    display: flex; 
    justify-content: right; 
    align-items: center;
  
    position: absolute;
    width: 100%;
    height: 50px;
    bottom: 0;
  
    &.center {
      justify-content: center; 
    }
  }
  
  .ls-select-disabled-allow-copy {
      &.ant-select-disabled {
        cursor: default;   /* 去掉悬浮时出现的小手 */
  
        .ant-select-selector {
            pointer-events: none; /* 你可以看到元素,但你操作不到,操作会穿透触发到下层的元素 */
    
            .ant-select-selection-item {
                user-select: auto; /* 允许选择文本 */
            }
        }     
      }
  }
  
  .ls-input-disabled-allow-copy {
    &.ant-input-disabled {
      cursor: default;
      user-select: auto;
    }
  }

main.js

暴露样式文件ls-global.css到全局

import '../public/ls/ls-global.css';

ls-import.vue的使用

<template>
  <div class="lsl-test">
  </div>
</template>

<script>
  export default {
    name: 'lsl-test',
  }
</script>

<script setup>

// 导入 ------------------------------------------------------
  const LsImport = defineAsyncComponent(() => import('@views/business/base/import-export/ls-import.vue')); // 导入
  const lsImportRef = ref();
  const isShowImport = ref(false);

  const impAssisted = reactive({
    tmplName: 'lsl测试导入模板.xlsx',
    tmplUrl: '/html/forest/substanceDeviceAction.do?action=downloadSubstDevExcelImpTmpl', // 模板下载地址
    uploadUrl: '/html/forest/substanceDeviceAction/importSubstDevFromExcel', // 文件上传地址
  })

  // 在页面内设置一个按钮或者其他什么, 触发的时候调用这个 toImport() 就行
  const toImport = () => {
    isShowImport.value = true;
  }

// -----------------------------------------------------------
</script>

<style lang="less" scoped>
  .lsl-test {
    position: relative;
    height: 100%;
    width: 100%;
    
  }
</style>

补充

导入模板下载(后端实现简述)

导入模板下载的后端实现: 逻辑:

① 将以项目resource文件夹下的lsl测试导入模板.xlsx为基底的文件, 进行一些模板占位值替换行为后, 得到可供用户下载的excel模板文件;

Controller层接口接收HttpServletResponse作为入参, 调用封装好的工具类下载方法, 将准备好的excel模板文件 复制到 HttpServletResponse提供的字节流以输出;

③ 后端主要还是使用了EasyExcel达成目的.


以下为主实现代码


触发导入行为(后端接口层入参)

@PostMapping("/importSubstDevFromExcel")
@ResponseBody
public String importSubstDevFromExcel(@RequestParam(value = "file") MultipartFile file{
    // ...相关导入校验逻辑
}

导出的前端代码

导出的封装的前端组件, 其实和导入的差别不大, 甚至更简单, 所以只附上最主要的触发导出的方法.

以下代码的逻辑是: 携带当前页面的搜索条件, 以便于触发导出时, 也使用此搜索条件进行数据过滤, 并导成excel文件后返回给前端供用户下载.

<template>
  <div class="ls-export">
    <a-spin 
      :spinning="isPageLoading" 
      :indicator="h(LoadingOutlined, { style: { fontSize: '24px' }, spin: false})"
    >
      <div class="ls-box redure50">
        <l-form style="padding: 0 20px 10px;" name="formRef" ref="formRef" v-model:model="form">
          <l-row>
            <a-col :span="24">
              <a-form-item  
                style="font-size: 16px; display: flex; flex-direction: row; align-items: baseline;"
                label="导出页类型"
                name="expPageType"
                :rules="[ {required: true, message: '请选择导出页类型', trigger: ['change', 'blur'] },]"
              >
                <a-select 
                  style="padding-left: 10px;"
                  v-model:value.trim="form.expPageType" 
                  placeholder="请选择导出页类型" 
                  allow-clear
                  :options="expPageTypeList"
                >
                </a-select>
              </a-form-item>
            </a-col>
          </l-row>
        </l-form>
      </div>

      <div class="ls-buttom-btn center">
        <a-button type="primary" style="margin-right: 10px;" @click="toExport">导出</a-button>
        <a-button @click="toCancel">取消</a-button>
      </div>
    </a-spin>
  </div>
</template>

<script>
  export default {
    name: 'ls-export'
  }
</script>

<script setup>
  import { ref, reactive , h, defineProps, defineEmits, isProxy, toRaw } from 'vue';
  import axios from "axios";
  
  const formRef = ref();

  const props = defineProps({
    // 导出文件名称
    fileName: {
      type: String,
      default: '导出',
    },  
    // 导出调用接口
    url: {
      type: String,
      default: '',
    },
    // 相关参数
    param: {
      type: Object,
      default: () => {},
    }
  })
  // 回调
  const emit = defineEmits(['onClose', 'onSuccess', 'onFailure'])
  
  const form = reactive({ // 表单数据
    expPageType: '1', // 默认 导出本页
  });
  const expPageTypeList = reactive([
    { label: '导出本页', value: '1'},
    { label: '导出全部', value: '2'},
  ]);
  const isPageLoading = ref(false);

  /**
   * 触发导出
   */
  const toExport = () => {
    formRef.value.validate({
      success: () => {
        axios({
          url: window.server.baseURL + props.url,
          method: 'POST',
          headers: {
            tk: localStorage.getItem('tk') // 当前登录用户的信息, 可根据实际调整, 即不传
          },
          data: packageParam(), // 自定义的封装参数的方法, 返回一个对象(不是重点, 根据项目实际调整)
          responseType: 'blob'
        }).then(res => { // 成功
          let url = window.URL.createObjectURL(new Blob([res.data]));
          let link = document.createElement('a');
          link.style.display = 'none';
          link.href = url;
          let fileName = props.fileName + new Date().toLocaleString().replaceAll('/', '').replaceAll(':', '').replaceAll(' ', '');
          link.setAttribute('download', fileName + '.xlsx');
          document.body.appendChild(link);
          link.click();

          emit('onSuccess');
        }).catch(err => { // 失败
          emit('onFailure', err.data);
        }).finally(() => { isPageLoading.value = false; })
      },
      error: () => {
        isPageLoading.value = false;
      }
    });
  }

  /**
   * 封装参数(根据实际调整或删除该方法)
   */
  const packageParam = () => {
    let data = props.param;
    data.condition = Object.assign(props.param.condition, form);
    // 为一致, 使用 lh.core.js 的 fetch() 中逻辑
    let paraMap = {};
    for (let d in data) {
        if (data[d]) {
            if (d==='condition' && isProxy(data[d])) { // 判断 data[d] 是否是 Proxy 类型
                data[d] = JSON.stringify(toRaw(data[d]));
            }
            paraMap["map['" + d + "']"] = data[d];
            paraMap[d] = data[d];
        }
    }
    paraMap.currPage = data.currPage?data.currPage:1;
    paraMap.pageSize = data.pageSize?data.pageSize:10;
    return paraMap;
  }

    /**
   * 取消
   */
   const toCancel = () => {
    emit('onClose')
  }


</script>

<style lang="less" scoped>
  .ls-export {
    position: relative;
    height: 100%;
    width: 100%;
    :deep(.ant-spin-nested-loading)  {
      height: 100% ;
      .ant-spin-container  {
        height: 100%;
      }
    }
  }
</style>

参考资料

问题 <= 参考

  1. 上传文件form表单没进去 <= 解决前端上传Formdata中的file为[object Object]的问题

  2. [formData打印是空对象 <= 遇到一个问题,formData 对象的 append() 函数增加值,无效](https://segmentfault.com/q/1010000010087308)

  3. 后端获取不到file对象 <= 项目中使用antd中的upload组件file对象到底是info.file还是info.file.originFileObj_坑

  4. Blob、File与Antd Upload


posted @   LinForest_zZ  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示