0%

Upload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<template>
<div id="app">
<!-- 上传组件 -->
<el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div>
</el-upload>

<!-- 进度显示 -->
<div class="progress-box">
<span>上传进度:{{ percent.toFixed() }}%</span>
<el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button>
</div>

<!-- 展示上传成功的视频 -->
<div v-if="videoUrl">
<video :src="videoUrl" controls />
</div>
</div>
</template>

<script>
import SparkMD5 from "spark-md5"
import axios from "axios"

export default {
name: 'App3',
filters: {
btnTextFilter(val) {
return val ? '暂停' : '继续'
}
},
data() {
return {
percent: 0,
videoUrl: '',
upload: true,
percentCount: 0
}
},
methods: {
async handleChange(file) {
if (!file) return
this.percent = 0
this.videoUrl = ''
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw
let buffer
try {
buffer = await this.fileToBuffer(fileObj)
} catch (e) {
console.log(e)
}

// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
const chunkSize = 2097152,
chunkList = [], // 保存所有切片的数组
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名

// 根据文件内容生成 hash 值
const spark = new SparkMD5.ArrayBuffer()
spark.append(buffer)
const hash = spark.end()

// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0 // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
chunk: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名
}
curChunk += chunkSize
chunkList.push(item)
}
this.chunkList = chunkList // sendRequest 要用到
this.hash = hash // sendRequest 要用到
this.sendRequest()
},

// 发送请求
sendRequest() {
const requestList = [] // 请求集合
this.chunkList.forEach((item, index) => {
const fn = () => {
const formData = new FormData()
formData.append('chunk', item.chunk)
formData.append('filename', item.fileName)
return axios({
url: '/single3',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
data: formData
}).then(res => {
if (res.data.code === 0) { // 成功
if (this.percentCount === 0) { // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
this.percentCount = 100 / this.chunkList.length
}
this.percent += this.percentCount // 改变进度
this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
}
})
}
requestList.push(fn)
})

let i = 0 // 记录发送的请求个数
// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
const complete = () => {
axios({
url: '/merge',
method: 'get',
params: { hash: this.hash }
}).then(res => {
if (res.data.code === 0) { // 请求发送成功
this.videoUrl = res.data.path
}
})
}
const send = async () => {
if (!this.upload) return
if (i >= requestList.length) {
// 发送完毕
complete()
return
}
await requestList[i]()
i++
send()
}
send() // 发送请求
},

// 按下暂停按钮
handleClickBtn() {
this.upload = !this.upload
// 如果不暂停则继续上传
if (this.upload) this.sendRequest()
},

// 将 File 对象转为 ArrayBuffer
fileToBuffer(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader()
fr.onload = e => {
resolve(e.target.result)
}
fr.readAsArrayBuffer(file)
fr.onerror = () => {
reject(new Error('转换文件格式发生错误'))
}
})
}
}
}
</script>

<style scoped>
.progress-box {
box-sizing: border-box;
width: 360px;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding: 8px 10px;
background-color: #ecf5ff;
font-size: 14px;
border-radius: 4px;
}
</style>