文件切片上传流程(前端)
【代码】文件切片上传流程(前端)
·
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
更多推荐
所有评论(0)