Bootstrap

Ant Design Pro of Vue2 文件上传组件 Upload 自定义上传实战笔记


最终效果

确认 Ant Design Vue 版本

  • 首先要清楚 Ant Design Vue 和 Ant Design Pro of Vue 的区别:
    可以理解为 Ant Design Vue 是一套 Vue 组件库,而 Pro 是使用了这套组件库的完整前端脚手架。

  • 从 Ant Design Pro of Vue 构建的项目的 package-lock.json 文件中确认
    查看ant-design-vue的版本

    截止本文发布时间(2022 年 9 月 16 日)按照 Pro 官方文档的安装方式默认安装都是 1.7.8 的版本。

  • 明确了框架的版本,那么我们在阅读官方文档使用 upload 或者其他组件时,也一定要确认文档版本,否则会因为版本差异,会影响您的工作。
    切换ant-design-vue版本

    上图中选择 1.x 就是 1.7.8 的版本,选择之后就会显示完整的版本号 1.7.8

【特别说明】为什么要讲上面这些呢,因为我在实际工作中就因为这个问题,官方文档默认打开的版本是 v3 或者 v2,再加上当时项目比较赶,就掉到这坑里了!!!

Upload 组件 API 关键参数和事件介绍

参数

  • action:设置上传地址,可以设置两种类型的值:

    • 字符串:就是一个有效的上传 URL 地址;
    • 回调函数:(file) => Promise这个后续探索后,再给大家分享^_^
  • method:设置上传请求方式,一般为 POST

  • beforeUpload:上传文件之前的钩子,参数为上传的文件

  • customRequest:通过覆盖默认的上传行为,可以自定义自己的上传实现,该参数的值类型为Function

  • data:设置上传所需参数,如:{type: 'article'}

  • fileList:已经上传的文件列表(受控)

  • listType:上传列表的内建样式,支持三种基本样式 text, picturepicture-card

  • multiple:是否支持多选文件,ie10+ 支持。开启后按住 ctrl 可选择多个文件。

  • name:设置发到后台的文件参数名,默认 file

  • remove:点击移除文件时的回调,返回值为 false 时不移除。Function(file): boolean

事件

  • change:上传文件状态改变时的回调,上传中、完成、失败都会调用这个函数。返回三个参数:

    • file 当前操作的文件对象,数据结构为:

      {
          uid: 'uid',      // 文件唯一标识,建议设置为负数,防止和内部产生的 id 冲突
          name: 'xx.png',   // 文件名
          status: 'done', // 状态有:uploading done error removed
          response: '{"status": "success"}', // 服务端响应内容
          linkProps: '{"download": "image"}', // 下载链接额外的 HTML 属性
          xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header
      }
      
    • fileList 当前的文件列表。

    • event 上传中的服务端响应内容,包含了上传进度等信息,高级浏览器支持。

  • preview:点击文件链接或预览图标时的回调,返回参数点击的文件对象。

  • reject:拖拽文件不符合 accept 类型时的回调

Upload 组件代码实战

基础代码构建

<template>
  <div>
    <a-card title="多图上传">
      <!-- 引入 Upload 上传组件 -->
      <a-upload list-type="picture-card">
        <div>
          <a-icon type="plus" />
        </div>
      </a-upload>
    </a-card>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
  methods: {},
};
</script>

此时效果图如下,但是还不能成功上传文件,因为我们还没有做服务端上传相关配置。
基础代码构建效果

自定义上传实现(customRequest)

通过给 upload 组件设置 customRequest 参数来实现自定义上传,设置该从参数会覆盖组件的默认上传行为。

此时 upload 组件代码如下:

<a-upload
  list-type="picture-card"
  :custom-request="customUploadHandle"  // 设置 custom-request 参数
></a-upload>

JS 部分增加自定义上传的实现,代码如下:

// 引用定义好的上传API
import { uploadFile } from '@/api/common';

// 在 methods 增加自定义的实现,就是一个函数回调
// 函数有一个参数 data ,就是打开文件选择器选择的文件对象
methods: {
  customUploadHandle(data) {
    // 构建表单上传对象
    const formData = new FormData();
    // 上传的文件,就是我们熟悉的HTML表单的input[type=file]的值
    formData.append("file", data.file);
    // 设置其他(表单)参数,你可以添加多个该行代码表示增加多个参数,本例参数type=covers代表我上传的是封面图片
    formData.append("type", "covers");
    // 请求上传API
    uploadFile(formData)
      .then((res) => {
        this.uploadLoading = false;
        if (res.code * 1 === 0) {
          // 上传成功的回调
          data.onSuccess(res.data);
        } else {
          // 上传失败的回调
          data.onError(res.msg);
          this.$message.error(res.msg);
        }
      })
      .catch((error) => {
        this.uploadLoading = false;
        this.$message.error(error.message);
      });
  }
}

文件验证(类型、大小)

beforeUpload 参数(上传前的钩子)中进行文件类型、大小进行判断,如果不满足条件,返回 false 则停止上传。

<a-upload
  list-type="picture-card"
  :custom-request="customUploadHandle"
  :before-upload="beforeUploadHandle"  // 设置 before-upload 参数
></a-upload>

JS 部分增加 beforeUpload 的实现,代码如下:


// 在 methods 增加 beforeUpload 的实现,就是一个函数回调
// 函数有两个参数:file为当前已选择待上传的文件, fileList为已经选择上传的文件列表
methods: {
  beforeUploadHandle (file, fileList) {
    // 检查文件 file 的大小和类型,
    // result 为验证果 true or false,messages 为验证失败时的错误消息
    const { result, messages } = fileCheckForImage(file)
    if (!result) { // 验证失败的处理
      fileList.pop() // 从已选择的文件列表移除最后一个文件,也就是当前验证的文件,因为默认会显示
      this.$message.error(messages) // 弹出验证失败的提示信息
      return false // 返回 false 阻止上传
    }
  }
}

// 文件大小和类型验证的实现,我把该函数放到了公共函数文件,你可以根据实际情况来处理
export function fileCheckForImage (file) {
  const messages = []

  // 文件类型验证
  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
  if (!isJpgOrPng) {
    messages.push('上传文件格式不正确!只允许上传图片!')
  }

  // 文件大小验证(2M以内)
  const isLt2M = file.size / 1024 / 1024 < 2
  if (!isLt2M) {
    messages.push('上传文件大小超出限制!不能超过2MB!')
  }

  return {
    result: isJpgOrPng && isLt2M,
    messages
  }
}

模拟实现上传进度条效果

由于采用了自定义上传方式,上传进度条显示异常,使用显示为一个白条,正确的应该是白条是进度条的总长度,实时的进度用蓝色来动态显示。所以模拟实现上传进度条效果是为了更好的体验,只是由于目前的我还不能获取的真正的上传进度的数据,实现效果不太理想,勉强可以使用。

代码实现其实很简单,通过 setInterval 来更新组件的上传进度状态,代码如下:

methods: {
  customUploadHandle(data) {
    const formData = new FormData();
    formData.append("file", data.file);
    formData.append("type", "covers");

    // 模拟上传进度条
    const uploadProgress = { percent: 0 }
    const intervalId = setInterval(() => {
      if (uploadProgress.percent < 100) {
        uploadProgress.percent += 0.00001
      }
      data.onProgress(uploadProgress) // 更新组件的上传进度
      if (uploadProgress.percent >= 100) {
        clearInterval(intervalId)
      }
    })

    uploadFile(formData)
      .then((res) => {
        this.uploadLoading = false;
        if (res.code * 1 === 0) {
          uploadProgress.percent = 100 // 上传完成
          data.onSuccess(res.data);
        } else {
          data.onError(res.msg);
          this.$message.error(res.msg);
        }
      })
      .catch((error) => {
        this.uploadLoading = false;
        this.$message.error(error.message);
      });
  }
}

上传图片预览

通过 preview 事件来实现文件预览,preview 事件是点击文件链接或预览图标时的回调。

UI 组件代码变动如下:

<!-- 此时 upload 组件代码如下: -->
<a-upload
  list-type="picture-card"
  :custom-request="customUploadHandle"
  :before-upload="beforeUploadHandle"
  @preview="previewHandle" // 设置 preview 事件
>
  <div>
    <a-icon type='plus' />
  </div>
</a-upload>

<!-- 增加图片预览弹窗 modal 组件 -->
<a-modal
  :visible="previewVisible"
  :footer="null"
  @cancel="closePreviewHandle"
  :bodyStyle="{ padding: '41px 10px 20px' }"
>
  <img alt='' style='width: 100%' :src='previewImage' />
  <div style='padding: 30px 0;text-align: center' v-show='previewLoading'>
    <a-spin :spinning='previewLoading'></a-spin>
  </div>
</a-modal>

JS 部分增加的代码变动如下:

export default {
  data() {
    return {
      previewVisible: false, // 预览弹窗的显示状态
      previewImage: '', // 当前预览的图片(临时动态存储)
      previewLoading: true, // 预览弹窗的加载状态
    };
  },
  methods: {
    /**
     * 图片预览的实现,实质就是打开一个 modal 弹窗显示图片
     */
    previewHandle (file) {
      this.previewVisible = true
      this.previewLoading = true

      // 此处根据自己的数据结构来处理,file.response 的值为自定义上传 API 返回的数据结构
      const previewImage = file.url || file.response.path
      const imageObj = new Image()
      imageObj.src = previewImage
      imageObj.onload = () => {
        this.previewImage = imageObj.src
        this.previewLoading = false
      }
    },
    /**
     * 关闭预览 modal 的回调
     */
    closePreviewHandle () {
      this.previewVisible = false
      setTimeout(() => {
        this.previewImage = ''
        this.previewLoading = true
      }, 300)
    }
  },
};

完全控制上传列表 && 限制上传列表数量

通过参数 fileList 和 事件 change 对列表进行完全控制,以实现某些自定义功能。

UI 组件代码变动如下:

<a-upload
  list-type="picture-card"
  :custom-request="customUploadHandle"
  :before-upload="beforeUploadHandle"
  :file-list="uploadFileList" // 设置 fileList 参数
  @preview="previewHandle"
  @change="uploadChangeHandle" // 设置 change 事件
>
  <div>
    <a-icon type='plus' />
  </div>
</a-upload>

<!-- 点击下面的按钮,控制台会打印 fileList  -->
<a-button type='primary' @click='fileListShowHandle'>控制台打印上传列表</a-button>

JS 部分增加的代码变动如下:

export default {
  data() {
    return {
      previewVisible: false, // 预览弹窗的显示状态
      previewImage: '', // 当前预览的图片(临时动态存储)
      previewLoading: true, // 预览弹窗的加载状态

      uploadFileList: [],
    };
  },
  methods: {
    /**
     * 图片预览的实现,实质就是打开一个 modal 弹窗显示图片
     */
    uploadChangeHandle (info) {
      let fileList = [...info.fileList]

      // TODO:此处可以对 fileList 进行处理,比如下面的代码来限制上传数量
      if (fileList.length >= 3) {
        this.$message.error('已达最多允许上传数量(3个)')
        fileList = fileList.slice(0, 3)
      }

      this.uploadFileList = fileList
    },
    fileListShowHandle () {
      console.log(this.uploadFileList)
    }
  },
};

已上传图片回显

通过初始化参数 fileList 的值,来回显已上传的图片。

JS 部分增加的代码变动如下:

/**
 * 导入获取已上传图片的API,该接口为简单演示接口,数据结构简单如下:
 * {code: 0, message: 'success', data: [{id: 100, path: '/path/xxx1.png'}, {id: 101, path: '/path/xxx2.png'}]}
 */
import { photos } from '@/api/common'
export default {
  data() {
    return {
      // ...其他参数
      uploadFileList: [], // 必须
    };
  },
  created () {
    this.uploadListRender()
  },
  methods: {
    uploadListRender () {
      photos({}).then(res => {
        this.uploadFileList = res.data.map(file => {
          const filename = file.path.split('.').pop()

          // fileList 数据结构如下
          return {
            uid: '-' + String(file.id), // uid为其在上传列表中的文件唯一标识,官方建议设置为负数,防止和内部产生的 id 冲突
            name: filename,
            status: 'done',
            url: file.path
          }
        })
      }).catch(error => {
        this.$message.error(JSON.stringify(error))
      })
    }
  },
};

完整代码

api 文件 common.js

import request from '@/utils/request'

const apiMap = {
  FileUpload: '/upload',
  PhotoList: '/photos'
}

// 文件上传接口
export function uploadFile (data) {
  return request({
    url: apiMap.FileUpload,
    method: 'post',
    data
  })
}

// 已上传文件接口
export function photos (data) {
  return request({
    url: apiMap.PhotoList,
    data
  })
}

公共函数文件 util.js

/**
 * 文件上传验证
 * @param file
 * @returns {{result: boolean, messages: *[]}}
 */
export function fileCheckForImage (file) {
  const messages = []
  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
  if (!isJpgOrPng) {
    messages.push('上传文件格式不正确!只允许上传图片!')
  }

  const isLt2M = file.size / 1024 / 1024 < 2
  if (!isLt2M) {
    messages.push('上传文件大小超出限制!不能超过2MB!')
  }

  return {
    result: isJpgOrPng && isLt2M,
    messages
  }
}

主文件 Upload.vue

<template>
  <div>
    <a-card title='多图上传'>
      <a-upload
        list-type='picture-card'
        :custom-request='customUploadHandle'
        :before-upload='beforeUploadHandle'
        :file-list='uploadFileList'
        @preview='previewHandle'
        @change='uploadChangeHandle'>
        <div>
          <a-icon type='plus' />
        </div>
      </a-upload>
      <a-button type='primary' @click='fileListShowHandle'>控制台打印上传列表</a-button>
      <a-modal
        :visible='previewVisible'
        :footer='null'
        @cancel='closePreviewHandle'
        :bodyStyle="{ padding: '41px 10px 20px' }">
        <img alt='' style='width: 100%' :src='previewImage' />
        <div style='padding: 30px 0;text-align: center' v-show='previewLoading'>
          <a-spin :spinning='previewLoading'></a-spin>
        </div>
      </a-modal>
    </a-card>
  </div>
</template>

<script>
import { fileCheckForImage } from '@/utils/util'
import { uploadFile, photos } from '@/api/common'

export default {
  name: 'Basic',
  data () {
    return {
      previewVisible: false,
      previewImage: '',
      previewLoading: true,
      uploadFileList: [],
      uploadLoading: false
    }
  },
  created () {
    this.uploadListRender()
  },
  methods: {
    uploadListRender () {
      photos({}).then(res => {
        this.uploadFileList = res.data.map(file => {
          const filename = file.path.split('.').pop()
          return {
            uid: '-' + String(file.id),
            name: filename,
            status: 'done',
            url: file.path
          }
        })
      }).catch(error => {
        this.$message.error(JSON.stringify(error))
      })
    },
    customUploadHandle (data) {
      // 构建表单上传对象
      const formData = new FormData()
      // 上传的文件,就是我们熟悉的HTML表单的input[type=file]的值
      formData.append('file', data.file)
      // 设置其他(表单)参数,你可以添加多个该行代码表示增加多个参数,本例参数type=covers代表我上传的是封面图片
      formData.append('type', 'covers')

      // 模拟上传进度条
      const uploadProgress = { percent: 0 }
      const intervalId = setInterval(() => {
        if (uploadProgress.percent < 100) {
          uploadProgress.percent += 0.00001
        }
        data.onProgress(uploadProgress)
        if (uploadProgress.percent >= 100) {
          clearInterval(intervalId)
        }
      })

      // 请求上传API
      uploadFile(formData).then(res => {
        this.uploadLoading = false
        if (res.code * 1 === 0) {
          uploadProgress.percent = 100
          // 上传成功的回调
          setTimeout(() => {
            data.onSuccess(res.data)
          }, 50)
        } else {
          // 上传失败的回调
          data.onError(res.msg)
          this.$message.error(res.msg)
        }
      }).catch(error => {
        this.uploadLoading = false
        this.$message.error(error.message)
      })
    },
    beforeUploadHandle (file, fileList) {
      const { result, messages } = fileCheckForImage(file)
      if (!result) {
        fileList.pop()
        this.$message.error(messages)
        return false
      }
    },
    fileListShowHandle () {
      console.log(this.uploadFileList)
    },
    uploadChangeHandle (info) {
      let fileList = [...info.fileList]
      fileList = fileList.map(file => {
        if (file.response) {
          file.url = file.response.path
        }
        return file
      })

      if (fileList.length > 3) {
        this.$message.error('已达最多允许上传数量(3个)')
        fileList = fileList.slice(0, 3)
      }

      this.uploadFileList = fileList
    },
    previewHandle (file) {
      this.previewVisible = true
      this.previewLoading = true

      const previewImage = file.url || file.response.path
      const imageObj = new Image()
      imageObj.src = previewImage
      imageObj.onload = () => {
        this.previewImage = imageObj.src
        this.previewLoading = false
      }
    },
    closePreviewHandle () {
      this.previewVisible = false
      setTimeout(() => {
        this.previewImage = ''
        this.previewLoading = true
      }, 300)
    }
  }
}
</script>

<style scoped>

</style>

完结。。。。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;