1. 流程图

2. 文件切片
  // 生成文件切片
  createFileChunk = (file: File, size = SIZE) => {
    const chunkList = []
    let cur = 0
    while (cur < file.size) {
      chunkList.push(file.slice(cur, cur + size))
      cur += size
    }
    return chunkList
  }
3. 计算hash
3.1. requestIdleCallback执行,不占用主线程
class RequestIdle {
  deadlineTime = 0
  callback: ((params: any) => void) | null = null
  channel: MessageChannel | null = null
  port1: MessagePort | null = null
  port2: MessagePort | null = null
  isWaitingAvailableFrame = true

  constructor() {
    this.channel = new MessageChannel()
    this.port1 = this.channel.port1
    this.port2 = this.channel.port2
    this.port2.onmessage = () => {
      const timeRemaining = () => this.deadlineTime - performance.now()
      const _timeRemain = timeRemaining()
      if (_timeRemain > 0 && this.callback && this.isWaitingAvailableFrame) {
        const deadline = {
          timeRemaining,
          didTimeout: _timeRemain < 0,
        }
        this.callback(deadline)
        this.isWaitingAvailableFrame = false
      } else if (this.isWaitingAvailableFrame) {
        if (this.callback) this._requestIdleCallback(this.callback)
      }
    }
  }

  _requestIdleCallback = (cb: { (params: any): void; (params: any): void }) => {
    const SECONDE_DURATION = 1000
    const FRAME_DURATION = SECONDE_DURATION / 60
    this.callback = cb
    this.isWaitingAvailableFrame = true
    if (!document.hidden) {
      requestAnimationFrame((rafTime) => {
        this.deadlineTime = rafTime + FRAME_DURATION
        this.port1?.postMessage(null)
      })
    } else {
      this.deadlineTime = performance.now() + SECONDE_DURATION
      this.port1?.postMessage(null)
    }
  }
}

const { _requestIdleCallback } = new RequestIdle()

const requestIdleCallback = window.requestIdleCallback || _requestIdleCallback

export default requestIdleCallback
3.2. spark-md5计算内容hash
calculateHash = (chunks: Blob[]): Promise<string> => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer()
    let count = 0
    // 根据文件内容追加计算
    const appendToSpark = (file: File) => {
      return new Promise((_resolve) => {
        const reader = new FileReader()
        reader.readAsArrayBuffer(file)
        reader.onload = ({ target }) => {
          spark.append(target?.result as ArrayBuffer)
          _resolve('')
        }
      })
    }
    const workLoop = async (deadline: { timeRemaining: () => number }) => {
      // 有任务,且当前帧还未结束
      while (count < chunks.length && deadline.timeRemaining() > 1) {
        await appendToSpark(chunks[count] as File)
        count++
      }
      resolve(spark.end())
      requestIdleCallback(workLoop)
    }
    requestIdleCallback(workLoop)
  })
}
calculateHashSample = (file: File) => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer()
    const reader = new FileReader()
    // 文件大小
    const { size } = file
    const offset = 2 * 1024 * 1024
    const chunks = [file.slice(0, offset)]
    // 前面100K
    let cur = offset
    while (cur < size) {
      // 最后一块全部加进来
      if (cur + offset >= size) {
        chunks.push(file.slice(cur, cur + offset))
      } else {
        // 中间的前中后去两个字节
        const mid = cur + offset / 2
        const end = cur + offset
        chunks.push(file.slice(cur, cur + 2))
        chunks.push(file.slice(mid, mid + 2))
        chunks.push(file.slice(end - 2, end))
      }
      // 取前两个字节
      cur += offset
    }
    // 拼接
    reader.readAsArrayBuffer(new Blob(chunks))
    reader.onload = ({ target }) => {
      spark.append(target?.result as ArrayBuffer)
      resolve(spark.end())
    }
  })
}
4. 检查文件是否已经存在
4.1. 文件已存在,更新秒传状态
4.2. 文件不存在,检查切片
5. 检查切片
5.1. 已存在的切片上传进度直接设为100
5.2. 若所有切片都已存在,直接合并(秒传)
5.3. 过滤出未上传的切片,进行续传
// 开始上传
handleUpload = async () => {
  const { file } = this
  if (!file) {
    return
  }
  // 生成切片
  const chunks = this.createFileChunk(file)
  // 计算hash
  this.hash = await this.calculateHash(chunks)
  console.log(this.hash)
  const exist = await this.verifyFile(this.hash)
  if (exist === -1) {
    return
  }
  if (exist) {
    this.setState({
      ready: true,
      chunks: chunks.map((chunk, index) => {
        const hash = `${this.hash}-${index + 1}`
        return {
          chunk,
          index: index + 1,
          hash,
          size: chunk.size,
          fileHash: this.hash,
          progress: 100,
        }
      }),
    })
    return
  }
  // 判断文件是否存在,如果不存在,获取已经上传的切片
  const uploadedList = await this.verifyChunks(this.hash)
  if (uploadedList.includes('-1')) {
    return
  }
  this.setState({
    ready: true,
    chunks: chunks.map((chunk, index) => {
      const hash = `${this.hash}-${index + 1}`
      return {
        chunk,
        index: index + 1,
        hash,
        size: chunk.size,
        fileHash: this.hash,
        progress: uploadedList.includes(hash) ? 100 : 0,
      }
    }),
  })
  // 已存在切片数等于总切片数,秒传
  if (uploadedList.length === chunks.length) {
    console.log('秒传成功')
    if (chunks.length > 1) {
      this.mergeRequest()
    } else {
      this.props.onSuccess?.()
    }
    return
  }
  // 获取uploadId
  this.uploadId = await this.getUploadId(file.name)
  this.uploadChunks(uploadedList)
}

// 合并切片
mergeRequest = async () => {
  const { params } = this.props
  if (!params) {
    return
  }
  const res = await FileApi.mergeUploadedChunksApi({
    md5: this.hash,
    chunks: this.state.chunks.length,
    sumPartSize: this.state.fileSize,
    fileName: this.state.fileName,
    uploadId: this.uploadId,
    ...params,
  })
  const { code } = res
  if (code === 200) {
    this.props.onSuccess?.()
    console.log(this.state.chunks)
    return
  }
  this.setState({
    hasError: true,
  })
  this.props.onError?.()
}

// 上传切片
uploadChunks = async (uploadedList: string[]) => {
  if (!this.uploadId) {
    return
  }
  // 过滤掉已存在的
  const list = this.state.chunks
    .filter((chunk) => !uploadedList.includes(chunk.hash))
    .map(({ chunk, index }) => {
      const form = new FormData()
      form.append('md5', this.hash)
      form.append('file', chunk)
      form.append('chunk', `${index}`)
      form.append('uploadId', this.uploadId)
      form.append('chunks', `${this.state.chunks.length}`)
      form.append('partSize', `${chunk.size}`)
      form.append('sumPartSize', `${this.state.fileSize}`)
      form.append('fileName', this.state.fileName)
      const { params } = this.props
      if (params) {
        form.append('userIds', params.userIds)
        form.append('fileClassify', `${params.fileClassify}`)
        form.append('tenantId', `${params.tenantId}`)
      }
      return { form, index, status: Status.wait }
    })
  try {
    this.setState({
      startTime: new Date().getTime(),
      hasError: false,
    })
    // 并发请求,返回成功请求数
    const count = await this.requestControl(list)
    const { length } = this.state.chunks
    if (length === 1 && count === 1) {
      message.success('上传成功')
      this.props.onSuccess?.()
      return
    }
    if (uploadedList.length + count === this.state.chunks.length) {
      // 上传和已经存在之和 等于全部的再合并
      console.log('合并....')
      this.mergeRequest()
    }
  } catch (err) {}
}
6. 上传切片
6.1. 异步并发控制
// 异步并发控制
requestControl = (urls: any[], max = 4) => {
  return new Promise<number>((resolve) => {
    // 总请求数
    const len = urls.length
    // 上传成功的请求数
    let success = 0
    // 上传失败的请求数
    let fail = 0
    // 并发通道数
    let _max = max
    // 记录重传次数
    const retryArr: any[] = []
    const start = async () => {
      // 有请求,有通道
      while (success < len && _max > 0) {
        // 等待上传或者上传失败需要重传的
        const i = urls.findIndex((v) => v.status === Status.wait)
        if (i === -1) {
          break
        }
        // 占用通道
        _max--
        // 开始上传
        urls[i].status = Status.uploading
        // 当前请求应该提交的表单数据
        const { form } = urls[i]
        const index = urls[i].index
        await request({
          url: `${host.env}/filemgr/fileRecord/upload`,
          // url: 'http://localhost:3000/upload',
          data: form,
          onprogress: this.createProgressHandler(index - 1),
          requestList: this.requestList,
        })
          .then(() => {
            // 上传成功
            urls[i].status = Status.done
            // 释放通道
            _max++
            urls[i].done = true
            // 累计已完成请求数
            success++
            console.log(i, success)
            if (success === len) {
              resolve(success)
            } else {
              // 继续上传
              start()
            }
          })
          .catch((err) => {
            console.log('===========', err)
            // 上传出错
            urls[i].status = Status.wait
            // 释放当前占用的通道,但是success不累加
            _max++
            // 还原当前切片请求进度
            // this.setState((prev) => {
            //   prev.chunks[i].progress = 0
            //   return {
            //     chunks: [...prev.chunks],
            //   }
            // })
            if (typeof retryArr[i] !== 'number') {
              retryArr[i] = 0
            }
            // 累计重试次数
            retryArr[i]++
            // 一个请求报错3次就放弃
            if (retryArr[i] > 2) {
              console.log('这个请求出错了', urls[i])
              urls[i].status = Status.error
              // 请求失败数量累加
              fail++
              // return reject(new Error('abort'))
            }
            start()
          })
      }
      console.log(retryArr, fail, success, len)
      if (fail > 0 && fail + success === len) {
        this.setState({
          hasError: true,
        })
        resolve(success)
        this.props.onError?.()
      }
    }
    start()
  })
}
6.2. 监听上传进度
createProgressHandler(index: number) {
  return (e: { loaded: number; total: number }) => {
    const percent = parseInt(String((e.loaded / e.total) * 100), 10)
    this.setState((prev) => {
      prev.chunks[index].progress = percent
      return {
        chunks: [...prev.chunks],
      }
    })
  }
}
7. 断点续传
7.1. 暂停上传,取消未发送完成的请求
handlePause = () => {
  // 取消未发送完成的请求
  this.requestList.forEach((xhr) => xhr.abort())
  this.requestList = []
  cancelRequest('abort')
}
7.2. 恢复上传,重复检查文件、检查切片、上传切片的步骤
handleResume = async () => {
  const exist = await this.verifyFile(this.hash)
  if (exist === -1) {
    return
  }
  if (exist) {
    this.setState((prev) => {
      return {
        chunks: prev.chunks.map((chunk) => ({
          ...chunk,
          progress: 100,
        })),
      }
    })
    return
  }
  const uploadedList = await this.verifyChunks(this.hash)
  if (uploadedList.includes('-1')) {
    return
  }
  this.setState((prev) => {
    return {
      chunks: prev.chunks.map((chunk) => ({
        ...chunk,
        // 已存在的切片上传进度设为100
        progress: uploadedList.includes(chunk.hash) ? 100 : 0,
      })),
    }
  })
  if (uploadedList.length === this.state.chunks.length) {
    console.log('秒传成功')
    if (this.state.chunks.length > 1) {
      this.mergeRequest()
    } else {
      this.props.onSuccess?.()
    }
    return
  }
  // 获取uploadId
  // this.uploadId = await this.getUploadId(this.state.fileName)
  setTimeout(() => {
    this.uploadChunks(uploadedList)
  }, 500)
}
8. 实现进度条
8.1. 每个切片大小*每个切片上传进度,累加得到已上传的切片总大小
8.2. 已上传的切片总大小/整个文件大小
// 文件大小单位格式化
sizeFormat = (num: number) => {
  if (num > 1024 * 1024 * 1024) {
    return (num / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
  }
  if (num > 1024 * 1024) {
    return (num / (1024 * 1024)).toFixed(2) + 'MB'
  }
  if (num > 1024) {
    return (num / 1024).toFixed(2) + 'KB'
  }
  return num + 'B'
}

get loaded() {
  if (!this.file || !this.state.chunks.length) {
    return 0
  }
  return this.state.chunks
    .map((item) => item.progress * item.size)
    .reduce((acc, cur) => acc + cur)
}

get uploadProgress() {
  if (!this.loaded || !this.file) {
    return 0
  }
  const percent = parseInt((this.loaded / this.file.size).toFixed(2), 10)
  setTimeout(() => {
    this.props.onProgress?.(percent)
  }, 300)
  return percent
}

get speed() {
  if (this.state.startTime === 0) {
    return 0
  }
  // 以秒为单位
  const duration = (new Date().getTime() - this.state.startTime) / 1000
  if (duration === 0) {
    return 0
  }
  return this.sizeFormat(this.loaded / 100 / duration)
}
9. 完整示例代码
import React from 'react'
import { Progress, message, Modal } from 'antd'
import SparkMD5 from 'spark-md5'
import requestIdleCallback from '@/utils/ric'
import { request } from '@/utils/xhr'
import cx from 'classnames'
import host from '@/api/host'
import { File as FileApi } from '@/api'
import { fileIconMap, fileItemStatusMap, fileOtherIcon, Status } from './const'
import { TdWithCopy } from '@/components'
// import { cancelRequest } from '@/utils/request'
import styles from './uploader.module.less'
import { cancelRequest } from '@/utils/request'

type ChunkType = {
index: number
fileHash: string
progress: number
chunk: Blob
hash: string
size: number
}

type S = {
// hashProgress: number
chunks: ChunkType[]
ready: boolean
fileSize: number
fileName: string
startTime: number
hasError: boolean
}

type P = {
file: File
status: string
show: boolean
params?: { fileClassify: number; userIds: string; tenantId: number }
onProgress?: (percent: number) => void
onSuccess?: () => void
onError?: () => void
}

const SIZE = 5 * 1024 * 1024

/**
* 思路:
* 1.文件切片
* 2.计算hash
*  2.1.spark-md5计算内容hash
*  2.2.requestIdleCallback执行,不占用主线程
* 3.检查文件是否已经存在(秒传)
* 4.检查切片
*  4.1.已存在的切片上传进度直接设为100
*  4.2.过滤出未上传的切片,续传
*  4.3.所有切片都已存在,直接合并(秒传)
* 5.上传切片
*  5.1.异步并发控制
*    5.1.1.
*  5.2.监听上传进度
* 6.暂停上传
*  6.1.取消未发送完成的请求
* 7.恢复上传
*  7.1.重复3-5
* 8.进度条
*  8.1.每个切片大小*每个切片上传进度,累加得到已上传的切片总大小
*  8.2.已上传的切片总大小/整个文件大小
*/
class UploadProgress extends React.Component<P, S> {
private file: File | null = null
private hash: string = ''
private requestList: XMLHttpRequest[] = []
private uploadId: string = ''

constructor(props: P) {
  super(props)
  this.state = {
    // hashProgress: 0,
    chunks: [],
    ready: false,
    fileSize: props.file.size,
    fileName: props.file.name,
    startTime: 0,
    hasError: false,
  }
  this.file = props.file
}
componentDidMount() {
  if (this.props.status === Status.uploading) {
    this.handleUpload()
  }
}
componentDidUpdate(prevProps: { status: string }) {
  if (prevProps.status === this.props.status) {
    return
  }
  switch (this.props.status) {
    case Status.uploading:
      if (prevProps.status === Status.pause) {
        this.handleResume()
        return
      }
      this.handleUpload()
      break
    case Status.pause:
      this.handlePause()
      break
    case Status.cancel:
      this.handleCancel()
      break
    default:
      break
  }
}
// 校验文件是否已上传至磁盘
verifyFile = async (hash: string) => {
  // 这里能不能修改文件名
  const res = await FileApi.getCompleteFile({
    md5: hash,
    tenantId: this.props.params?.tenantId,
  })
  if (!res) {
    return -1
  }
  const { code, data } = res
  if (code === 200 && data?.fileUrl) {
    Modal.warning({
      title: `您上传的文件【${this.state.fileName}】已存在,原文件名为【${data.fileName}】,请在列表中查看`,
    })
    return 1
  }
  return 0
}
// 校验是否已有切片存在
verifyChunks = async (hash: string) => {
  const res = await FileApi.getUploadedChunksApi({
    md5: hash,
    tenantId: this.props.params?.tenantId,
  })
  if (!res) {
    return ['-1']
  }
  const { code, data } = res
  if (code !== 200 || !data) {
    return []
  }
  return [...new Set(data.split(',').map((item) => `${hash}-${item}`))]
}
// 获取uploadId
getUploadId = async (fileName: string) => {
  const res = await FileApi.getUploadIdApi({ fileName })
  if (!res) {
    return ''
  }
  const { code, data } = res
  if (code !== 200) {
    return ''
  }
  return data
}
// 生成文件切片
createFileChunk = (file: File, size = SIZE) => {
  const chunkList = []
  let cur = 0
  while (cur < file.size) {
    chunkList.push(file.slice(cur, cur + size))
    cur += size
  }
  return chunkList
}
// 计算hash-全量
calculateHash = (chunks: Blob[]): Promise<string> => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer()
    let count = 0
    // 根据文件内容追加计算
    const appendToSpark = (file: File) => {
      return new Promise((_resolve) => {
        const reader = new FileReader()
        reader.readAsArrayBuffer(file)
        reader.onload = ({ target }) => {
          spark.append(target?.result as ArrayBuffer)
          _resolve('')
        }
      })
    }
    const workLoop = async (deadline: { timeRemaining: () => number }) => {
      // 有任务,且当前帧还未结束
      while (count < chunks.length && deadline.timeRemaining() > 1) {
        await appendToSpark(chunks[count] as File)
        count++
        // 没有了,计算完毕
        // if (count < chunks.length) {
        //   // 计算中
        //   this.setState({
        //     hashProgress: Number(((100 * count) / chunks.length).toFixed(2)),
        //   })
        // } else {
        //   this.setState({
        //     hashProgress: 100,
        //   })
        //   resolve(spark.end())
        // }
      }
      resolve(spark.end())
      requestIdleCallback(workLoop)
    }
    requestIdleCallback(workLoop)
  })
}
// 抽样hash
calculateHashSample = (file: File) => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer()
    const reader = new FileReader()
    // 文件大小
    const { size } = file
    const offset = 2 * 1024 * 1024
    const chunks = [file.slice(0, offset)]
    // 前面100K
    let cur = offset
    while (cur < size) {
      // 最后一块全部加进来
      if (cur + offset >= size) {
        chunks.push(file.slice(cur, cur + offset))
      } else {
        // 中间的前中后去两个字节
        const mid = cur + offset / 2
        const end = cur + offset
        chunks.push(file.slice(cur, cur + 2))
        chunks.push(file.slice(mid, mid + 2))
        chunks.push(file.slice(end - 2, end))
      }
      // 取前两个字节
      cur += offset
    }
    // 拼接
    reader.readAsArrayBuffer(new Blob(chunks))
    reader.onload = ({ target }) => {
      spark.append(target?.result as ArrayBuffer)
      resolve(spark.end())
    }
  })
}
// 上传切片
uploadChunks = async (uploadedList: string[]) => {
  if (!this.uploadId) {
    return
  }
  // 过滤掉已存在的
  const list = this.state.chunks
    .filter((chunk) => !uploadedList.includes(chunk.hash))
    .map(({ chunk, index }) => {
      const form = new FormData()
      form.append('md5', this.hash)
      form.append('file', chunk)
      form.append('chunk', `${index}`)
      form.append('uploadId', this.uploadId)
      form.append('chunks', `${this.state.chunks.length}`)
      form.append('partSize', `${chunk.size}`)
      form.append('sumPartSize', `${this.state.fileSize}`)
      form.append('fileName', this.state.fileName)
      const { params } = this.props
      if (params) {
        form.append('userIds', params.userIds)
        form.append('fileClassify', `${params.fileClassify}`)
        form.append('tenantId', `${params.tenantId}`)
      }
      return { form, index, status: Status.wait }
    })
  try {
    this.setState({
      startTime: new Date().getTime(),
      hasError: false,
    })
    // 并发请求,返回成功请求数
    const count = await this.requestControl(list)
    const { length } = this.state.chunks
    if (length === 1 && count === 1) {
      message.success('上传成功')
      this.props.onSuccess?.()
      return
    }
    if (uploadedList.length + count === this.state.chunks.length) {
      // 上传和已经存在之和 等于全部的再合并
      console.log('合并....')
      this.mergeRequest()
    }
  } catch (err) {}
}
// 监听上传进度
createProgresshandler(index: number) {
  return (e: { loaded: number; total: number }) => {
    const percent = parseInt(String((e.loaded / e.total) * 100), 10)
    this.setState((prev) => {
      prev.chunks[index].progress = percent
      return {
        chunks: [...prev.chunks],
      }
    })
  }
}
// 异步并发控制
requestControl = (urls: any[], max = 4) => {
  return new Promise<number>((resolve) => {
    // 总请求数
    const len = urls.length
    // 上传成功的请求数
    let success = 0
    // 上传失败的请求数
    let fail = 0
    // 并发通道数
    let _max = max
    // 记录重传次数
    const retryArr: any[] = []
    const start = async () => {
      // 有请求,有通道
      while (success < len && _max > 0) {
        // 等待上传或者上传失败需要重传的
        const i = urls.findIndex((v) => v.status === Status.wait)
        if (i === -1) {
          break
        }
        // 占用通道
        _max--
        // 开始上传
        urls[i].status = Status.uploading
        // 当前请求应该提交的表单数据
        const { form } = urls[i]
        const index = urls[i].index
        await request({
          url: `${host.env}/filemgr/fileRecord/upload`,
          // url: 'http://localhost:3000/upload',
          data: form,
          onprogress: this.createProgresshandler(index - 1),
          requestList: this.requestList,
        })
          .then(() => {
            // 上传成功
            urls[i].status = Status.done
            // 释放通道
            _max++
            urls[i].done = true
            // 累计已完成请求数
            success++
            console.log(i, success)
            if (success === len) {
              resolve(success)
            } else {
              // 继续上传
              start()
            }
          })
          .catch((err) => {
            console.log('===========', err)
            // 上传出错
            urls[i].status = Status.wait
            // 释放当前占用的通道,但是success不累加
            _max++
            // 还原当前切片请求进度
            // this.setState((prev) => {
            //   prev.chunks[i].progress = 0
            //   return {
            //     chunks: [...prev.chunks],
            //   }
            // })
            if (typeof retryArr[i] !== 'number') {
              retryArr[i] = 0
            }
            // 累计重试次数
            retryArr[i]++
            // 一个请求报错3次就放弃
            if (retryArr[i] > 2) {
              console.log('这个请求出错了', urls[i])
              urls[i].status = Status.error
              // 请求失败数量累加
              fail++
              // return reject(new Error('abort'))
            }
            start()
          })
      }
      console.log(retryArr, fail, success, len)
      if (fail > 0 && fail + success === len) {
        this.setState({
          hasError: true,
        })
        resolve(success)
        this.props.onError?.()
      }
    }
    start()
  })
}

// 开始上传
handleUpload = async () => {
  const { file } = this
  if (!file) {
    return
  }
  // 生成切片
  const chunks = this.createFileChunk(file)
  // 计算hash
  this.hash = await this.calculateHash(chunks)
  console.log(this.hash)
  const exist = await this.verifyFile(this.hash)
  if (exist === -1) {
    return
  }
  if (exist) {
    this.setState({
      ready: true,
      chunks: chunks.map((chunk, index) => {
        const hash = `${this.hash}-${index + 1}`
        return {
          chunk,
          index: index + 1,
          hash,
          size: chunk.size,
          fileHash: this.hash,
          progress: 100,
        }
      }),
    })
    return
  }
  // 判断文件是否存在,如果不存在,获取已经上传的切片
  const uploadedList = await this.verifyChunks(this.hash)
  if (uploadedList.includes('-1')) {
    return
  }
  this.setState({
    ready: true,
    chunks: chunks.map((chunk, index) => {
      const hash = `${this.hash}-${index + 1}`
      return {
        chunk,
        index: index + 1,
        hash,
        size: chunk.size,
        fileHash: this.hash,
        progress: uploadedList.includes(hash) ? 100 : 0,
      }
    }),
  })
  // 已存在切片数等于总切片数,秒传
  if (uploadedList.length === chunks.length) {
    console.log('秒传成功')
    if (chunks.length > 1) {
      this.mergeRequest()
    } else {
      this.props.onSuccess?.()
    }
    return
  }
  // 获取uploadId
  this.uploadId = await this.getUploadId(file.name)
  this.uploadChunks(uploadedList)
}

// 暂停上传
handlePause = () => {
  // 取消未发送完成的请求
  this.requestList.forEach((xhr) => xhr.abort())
  this.requestList = []
  cancelRequest('abort')
}

// 恢复上传
handleResume = async () => {
  const exist = await this.verifyFile(this.hash)
  if (exist === -1) {
    return
  }
  if (exist) {
    this.setState((prev) => {
      return {
        chunks: prev.chunks.map((chunk) => ({
          ...chunk,
          progress: 100,
        })),
      }
    })
    return
  }
  const uploadedList = await this.verifyChunks(this.hash)
  if (uploadedList.includes('-1')) {
    return
  }
  this.setState((prev) => {
    return {
      chunks: prev.chunks.map((chunk) => ({
        ...chunk,
        // 已存在的切片上传进度设为100
        progress: uploadedList.includes(chunk.hash) ? 100 : 0,
      })),
    }
  })
  if (uploadedList.length === this.state.chunks.length) {
    console.log('秒传成功')
    if (this.state.chunks.length > 1) {
      this.mergeRequest()
    } else {
      this.props.onSuccess?.()
    }
    return
  }
  // 获取uploadId
  // this.uploadId = await this.getUploadId(this.state.fileName)
  setTimeout(() => {
    this.uploadChunks(uploadedList)
  }, 500)
}
// 取消上传
handleCancel = async () => {
  const uploadedList = await this.verifyChunks(this.hash)
  if (uploadedList.length > 0) {
    FileApi.cancelUploadedChunksApi({
      md5: this.hash,
      tenantId: this.props.params?.tenantId,
    })
  }
}
// 合并切片
mergeRequest = async () => {
  const { params } = this.props
  if (!params) {
    return
  }
  const res = await FileApi.mergeUploadedChunksApi({
    md5: this.hash,
    chunks: this.state.chunks.length,
    sumPartSize: this.state.fileSize,
    fileName: this.state.fileName,
    uploadId: this.uploadId,
    ...params,
  })
  const { code } = res
  if (code === 200) {
    this.props.onSuccess?.()
    console.log(this.state.chunks)
    return
  }
  this.setState({
    hasError: true,
  })
  this.props.onError?.()
}
// 文件大小单位格式化
sizeFormat = (num: number) => {
  if (num > 1024 * 1024 * 1024) {
    return (num / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
  }
  if (num > 1024 * 1024) {
    return (num / (1024 * 1024)).toFixed(2) + 'MB'
  }
  if (num > 1024) {
    return (num / 1024).toFixed(2) + 'KB'
  }
  return num + 'B'
}

get loaded() {
  if (!this.file || !this.state.chunks.length) {
    return 0
  }
  return this.state.chunks
    .map((item) => item.progress * item.size)
    .reduce((acc, cur) => acc + cur)
}

get uploadProgress() {
  if (!this.loaded || !this.file) {
    return 0
  }
  const percent = parseInt((this.loaded / this.file.size).toFixed(2), 10)
  setTimeout(() => {
    this.props.onProgress?.(percent)
  }, 300)
  return percent
}

get speed() {
  if (this.state.startTime === 0) {
    return 0
  }
  // 以秒为单位
  const duration = (new Date().getTime() - this.state.startTime) / 1000
  if (duration === 0) {
    return 0
  }
  return this.sizeFormat(this.loaded / 100 / duration)
}

get fileIcon() {
  const suffix = this.state.fileName.split('.')[1]
  return fileIconMap.filter((items) => items.key.includes(suffix)).length > 0
    ? fileIconMap.filter((items) => items.key.includes(suffix))[0].icon
    : fileOtherIcon
}

render() {
  return (
    <div
      className={cx(
        styles.item,
        { [styles.completed]: this.props.status === Status.done },
        { [styles.error]: this.state.hasError },
      )}
    >
      <div className={styles.fileInfo}>
        <div className={styles.fileIcon} style={{ backgroundImage: `url(${this.fileIcon})` }} />
        <div>
          <div className={styles.name}>
            <TdWithCopy manual={this.props.show}>{this.state.fileName}</TdWithCopy>
          </div>
          <div className={styles.load}>
            {this.state.hasError
              ? '发生了一些未知错误,请重新上传'
              : [Status.pause, Status.uploading].includes(this.props.status)
              ? `${this.sizeFormat(this.loaded / 100)}/${this.sizeFormat(this.state.fileSize)}`
              : this.sizeFormat(this.state.fileSize)}
          </div>
        </div>
      </div>

      <div className={styles.status}>
        {this.props.status === Status.done ? (
          <div className={styles.completed} />
        ) : this.props.status === Status.uploading && this.state.ready ? (
          `${this.speed}/s`
        ) : (
          // @ts-ignore
          fileItemStatusMap[this.props.status]
        )}
      </div>
      {this.props.status !== Status.done && (
        <div className={styles.progress}>
          <Progress
            percent={this.uploadProgress}
            size="small"
            strokeColor="#F46143"
            showInfo={false}
          />
        </div>
      )}
    </div>
  )
}
}

export default UploadProgress

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐