Bootstrap

【vue3组件封装】Upload上传组件

文章涉及的其他组件可从专栏查询

功能简介

  • 可传入Limit对象对文件上传做限制:文件大小、文件数量
  • 可通过accept限制文件类型
  • 可通过beforeUpload自定义上传前的验证,返回布尔值
  • 支持多文件上传
  • 可自定义上传控件
  • 可以显示上传成功的文件列表

使用

<s-upload @onSuccess="onSucessUpload" :showFiles="true" action=" ">
	<s-button :disabled="version.isUpdate">浏览</s-button>
</s-upload>

效果

在这里插入图片描述

Types

// 文件上传限制,可自由扩展
type Limit={
    size?:number, 		// 文件大小  单位M
    maxFiles?:number,	 // 文件数量
    [index:string]:string|number|undefined
}

// 文件状态
enum FILE_STATUS{
    EMPTY=0, // 空
    SUCCESS=1, // 上传成功
    ERROR=2, // 上传失败
    UPLOADING=3 // 上传中
}
    
// 组件状态
type State={
    fileData:any[]|object,// 当前文件
    fileStatus:FILE_STATUS, // 文件上传状态
    fileList:FileList|[], // 文件列表
    fileIndex:number  	// 文件列表的处理索引
}

// Upload属性
type UploadProp={
    action?: string, // 上传链接
    initFile?: Array<any> | Object,// 初始文件
    accept?: string | Array<string>,// 允许上传的格式
    limit?: Limit, // 上传限制
    multiple?:boolean,// 是否允许多选,
    beforeUpload: (files:FileList)=>boolean,// 上传前处理函数
    showFiles: boolean, // 是否显示文件信息
    help: string// 辅助信息
}

封装

<template>
  <div class="upload-container">
    <!-- 上传控件 -->
    <div class="trigger-container" @click="onUpload">
      <input
        class="hidden"
        ref="fileUploader"
        type="file"
        :multiple="multiple"
        :accept="acceptType"
        @change="fileChange"
      />
      <slot></slot>
    </div>
      
    <!-- 提示信息 -->
    <div v-if="help" class="file-help">
      {{help}}
    </div>
      
    <!-- 文件信息 -->
    <ul class="files-container" v-if="showFiles">
      <li v-for="file in fileList" :key="file.name" class="sspace-vertical">
        <s-icon icon="icon-file" type="symbol"/>
        <span class="sspace-horizon">{{file.name}}</span>
      </li>
    </ul>
  </div>
</template>

<script lang='ts'>
import $axios from "@/request"
import { PropType, computed, ref, watch, reactive, toRefs } from "vue"
import { Message, SIcon } from "@/components"
import { Limit, FILE_STATUS, State} from "@types"

export default {
  name: "s-upload",
  components: { SIcon },
  props: {
    // 上传连接
    action: String,
    // 初始文件
    initFile: {
      type: [Array, Object],
      default: null
    },
    // 允许上传的格式,数组会被转为字符串,支持的字符串参照mdn文档
    accept: {
      type: [String, Array],
      default: "image/*"
    },
    // 上传限制
    limit: Object as PropType<Limit>,
    // 是否允许多选
    multiple: {
      type: Boolean,
      default: false
    },
    // 上传前处理函数
    beforeUpload: Function as PropType<(files:FileList)=>boolean>,
    // 是否显示文件信息
    showFiles: {
      type: Boolean,
      default: false
    },
    // 辅助信息
    help: String
  },

  emits: ["onSuccess", "onError"],

  setup (props, context) {
    // 原生的input dom
    const fileUploader = ref<null | HTMLInputElement>(null)

	// 校验accept格式
    const acceptType = computed(() => {
      if (typeof props.accept !== "string") {
        if (Array.isArray(props.accept)) {
          return props.accept.join()
        } else {
          console.error("accept接收字符串或数组,请输入正确的格式")
        }
      }
      return props.accept
    })

	// 组件状态
    const state = reactive<State>({
      fileData: props.initFile, // 初始文件
      fileStatus: FILE_STATUS.ERROR, // 文件上传状态
      fileList: [], // 当前文件列表
      fileIndex: 0 // 文件处理索引(处理进度索引)
    })

    // 监听是否有初始文件,有则重新赋值
    watch(() => props.initFile, (val) => {
      if (val) {
        state.fileStatus = FILE_STATUS.SUCCESS
        state.fileData = val
      }
    })

    // 调用原生的upload
    const onUpload = (e:Event) => {
      if (fileUploader.value) {
        fileUploader.value.click()
      }
    }

    // 自定义验证 处理beforeUploade。
    const customCheck = async (files:FileList) => {
      return new Promise((resolve, reject) => {
        // 判断有无改属性
        if (props.beforeUpload) {
           // 自定义验证的结果
          const result = props.beforeUpload(files)
          if (typeof result !== "boolean") {
            reject(new Error("beforeUploadu应该返回一个布尔值"))
          }
          resolve(result)
        } else {
          resolve(true)
        }
      })
    }

    // 文件大小验证(组件自带的 通过limit.size定义的)
    const sizeCheck = (files:FileList) => {
      return new Promise((resolve, reject) => {
        const { size } = props.limit
        if (size) {
          let index = 0
          while (index < files.length) {
            const file = files[index]
            const fileSize = file.size / 1024
            if (fileSize > size) {
              const msg = `${file.name}文件大小超出${size}K,请重新调整!`
              Message.error(msg)
              reject(new Error(msg))
            }
            index++
          }
          resolve(true)
        }
        resolve(true)
      })
    }

    // 文件数量验证(组件自带的 通过limit.maxFiles定义的)
    const lengthCheck = (files:FileList) => {
      return new Promise((resolve, reject) => {
        const { maxFiles } = props.limit
        if (maxFiles) {
          console.log(files.length, maxFiles)
          if (files.length > maxFiles) {
            const msg = `文件数量不得超过${maxFiles}`
            Message.error(msg)
            reject(new Error(msg))
          }
          resolve(true)
        }
        resolve(true)
      })
    }

    
    // 上传前处理
    const fileChange = async (e:Event) => {
      const target = e.target as HTMLInputElement
      const files = target.files
      if (files && file.length) {
        // 上传前验证
        await customCheck(files)
        if (props.limit) {
          await sizeCheck(files)
          await lengthCheck(files)
        }

        // 本地 不上传到服务器时,验证完直接传回
        if (!props.action) {
          context.emit("onSuccess", files)
          state.fileList = files
          state.fileStatus = FILE_STATUS.SUCCESS
        } else {
        // 否则依次开始上传
          state.fileStatus = FILE_STATUS.UPLOADING
          state.fileList = files
          state.fileIndex = 0
          uploadFile(state.fileList[state.fileIndex])
        }
      }
    }

    // 上传文件
    const uploadFile = async (file:File) => {
      try {
        const fd = new FormData()
        fd.append("file", file)
         const data = await $axios.upload(props.action, fd)
         // 判断接口返回的数据决定是否继续上传,成功时:
        if (data) { 
           await isFinish()
         } else {
           throw new Error(`${file.name}在上传过程中发生错误,上传中止`)
         }
      } catch (err) {
        state.fileStatus = FILE_STATUS.ERROR
        state.fileList = []
        context.emit("onError",`${file.name}在上传过程中发生错误,上传中止`)
      } finally {
        // 重置索引
        state.fileIndex = 0
      }
      state.fileStatus = FILE_STATUS.SUCCESS
      context.emit("onSuccess", state.fileList)
      state.fileList = []
    }

    // 遍历所有文件
    const isFinish = () => {
      return new Promise((resolve, reject) => {
        // 如果有多个文件
        if (props.multiple && state.fileList.length > 1) {
          // 判断当前文件索引和文件列表长度
          if (state.fileIndex < state.fileList.length - 1) {
            state.fileIndex++
            uploadFile(state.fileList[state.fileIndex])
          } else {
            resolve(FILE_STATUS.SUCCESS)
          }
        } else {
          resolve(FILE_STATUS.SUCCESS)
        }
      })
    }

    return {
      acceptType,
      fileChange,
      onUpload,
      fileUploader,
      ...toRefs(state),
      ...toRefs(props)
    }
  }
}
</script>

<style scoped lang="less">
.upload-container{
  display: flex;
  flex-direction: column;
  .file-help{
    color:@help;
    margin: 10px 0;
    font-size: 0.85em;
  }
  .files-container{
    cursor: default;
  }
}

</style>
;