基于vue-simple-uploader的上传组件

上传组件简述

组件提供了通用能力,整合到平台中,需要根据需求做定制和集成。
在本平台中,我使用该上传组件,主要解决的是文件库上传文件, 需要支持文件、多文件上传,直接上传文件夹。用户明了的看到有多少文件,成功和失败的数量

特性

  • 支持文件、多文件、文件夹上传

  • 支持拖拽文件、文件夹上传

  • 统一对待文件和文件夹,方便操作管理

  • 可暂停、继续上传

  • 错误处理

  • 支持“快传”,通过文件判断服务端是否已存在从而实现“快传”

  • 上传队列管理,支持最大并发上传

  • 分块上传

  • 支持进度、预估剩余时间、出错自动重试、重传等操作

安装

1
npm install vue-simple-uploader --save

使用

初始化

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import uploader from 'vue-simple-uploader'
import App from './App.vue'

Vue.use(uploader)

/* eslint-disable no-new */
new Vue({
render(createElement) {
return createElement(App)
}
}).$mount('#app')

dialogUpload.vue

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
<template>
<el-dialog :close-on-click-modal="false" title="文件上传" :visible.sync="dialogVisible" width="800px" :before-close="handleClose" append-to-body>
<div id="upload">
<!-- 上传 -->
<uploader
ref="uploader"
:options="initOptions"
:fileStatusText="fileStatusText"
:auto-start="autoStart"
@file-added="onFileAdded"
@files-added="onFilesAdded"
@file-success="onFileSuccess"
@file-progress="onFileProgress"
@file-error="onFileError"
class="uploader-app"
>
<uploader-unsupport></uploader-unsupport>
<uploader-btn id="global-uploader-btn" ref="uploadBtn">选择文件</uploader-btn>
<uploader-btn id="global-uploader-btn" ref="uploadFolderBtn" :directory="true">选择文件夹</uploader-btn>
<uploader-drop>
<i class="el-icon-upload" style="font-size: 70px"></i>
<div class="el-upload__text">将文件拖到此处,或<em style="color: #409eff">点击上传</em></div>
<div style="padding-top: 20px;">
<el-button type="primary" size="mini" @click="upload(true)" v-if="auth.authNewFile == 1">
选择文件
</el-button>
<el-button type="primary" size="mini" @click="upload(false)" v-if="auth.authNewDir == 1">
选择文件夹
</el-button>
</div>
</uploader-drop>

<!-- <uploader-list v-show="panelShow">-->
<div class="file-panel" :class="{ collapse: collapse }" v-show="panelShow">
<div class="file-title">
<div class="title">文件列表 {{fileList.length>0 ? successFile+'/':''}}{{fileList.length>0 ? fileList.length:''}}</div>
<div class="operate">
<el-button @click="collapse = !collapse" type="text" :title="collapse ? '展开' : '折叠'">
<i class="iconfont" :class="collapse ? 'el-icon-full-screen' : 'el-icon-minus'"></i>
</el-button>
</div>
</div>

<ul class="file-list">
<li class="file-item" v-for="file in fileList" :key="file.id">
<uploader-file :class="['file_' + file.id, customStatus]" ref="files" :file="file" :list="false">
</uploader-file>
</li>
<div class="no-file" v-if="!fileList.length">
<i class="iconfont icon-empty-file"></i> 暂无待上传文件
</div>
</ul>
</div>
<!-- </uploader-list>-->
</uploader>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="small" type="primary" @click="handleSave">上 传</el-button>
<el-button size="small" @click="handleClose">关 闭</el-button>
</span>
</el-dialog>
</template>

<script>
export default {
props: {
// 发送给服务器的额外参数
params: {
type: Object
},
options: {
type: Object
},
autoStart: {
type: Boolean,
default: false
},
auth: {type: Object}
},
data () {
return {
dialogVisible: false,
uploader:'',
initOptions: {
target: this.options.target,
chunkSize: 1024 * 1024 * (this.options.chunkSize ? this.options.chunkSize : 1),
fileParameterName: 'file',
maxChunkRetries: this.options.maxChunkRetries ? this.options.maxChunkRetries : 3,
simultaneousUploads:1,
// 是否开启服务器分片校验
testChunks: false,
autoUpload: this.autoStart,
headers: {
'X-Access-Token': this.$cookies.get('token'),
},
// 用于格式化你想要剩余时间,一般可以用来做多语言
parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
// timeRemaining{Number}, 剩余时间,秒为单位
// parsedTimeRemaining{String}, 默认展示的剩余时间内容
return parsedTimeRemaining
.replace(/\syears?/, '年')
.replace(/\days?/, '天')
.replace(/\shours?/, '小时')
.replace(/\sminutes?/, '分钟')
.replace(/\sseconds?/, '秒')
}
},
fileStatusText: {
success: '上传成功',
error: '上传失败',
uploading: '上传中',
paused: '已暂停',
waiting: '等待上传'
},
panelShow: true, //选择文件后,展示上传panel
attrs: [],//文件上传类型
collapse: false,
customParams: {},
customStatus: '',
dropActive: false,
fileList: [],
successFile : 0,
erroeFile : 0,
}
},
methods:{
upload(b) {
if (b) {
if (this.$refs.uploadBtn) {
this.$refs.uploadBtn.$el.click()
}
} else {
if (this.$refs.uploadFolderBtn) {
this.$refs.uploadFolderBtn.$el.click()
}
}

},
deepCopy() {
let obj = JSON.stringify(this.customParams);
return JSON.parse(obj)
},
onFilesAdded: function (files, fileList, event) {
this.panelShow = true
},
onFileAdded(file) {
// this.emit('fileAdded')
file.params = this.deepCopy()
this.fileList.push(file)
// 计算MD5
this.computeMD5(file).then((result) => this.startUpload(result))
},

/**
* 计算md5值,以实现断点续传及秒传
* @param file
* @returns Promise
*/
computeMD5(file) {

//单个文件的大小限制100m
let fileSizeLimit = 100 * 1024 * 1024;
// console.log("文件大小:"+file.size);
// console.log("限制大小:"+fileSizeLimit);
if (file.size > fileSizeLimit) {
this.$message({
showClose: true,
message: '文件大小不能超过100MB'
});
file.cancel();
} else {
let fileReader = new FileReader()
let time = new Date().getTime()
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
let currentChunk = 0
const chunkSize = 10 * 1024 * 1000
let chunks = Math.ceil(file.size / chunkSize)
let spark = new SparkMD5.ArrayBuffer()
// file.pause()
// file.resume()
loadNext()

return new Promise((resolve, reject) => {
fileReader.onload = (e) => {
spark.append(e.target.result)
// spark.append(file.name)
if (currentChunk < chunks) {
currentChunk++
loadNext()
} else {
let md5 = spark.end()
md5 = SparkMD5.hash(md5 + file.name + chunks + time)
// console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`)
file.uniqueIdentifier = md5

resolve({file})
}
}

fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`)
file.cancel()
}
})

function loadNext() {
let start = currentChunk * chunkSize
let end = start + chunkSize >= file.size ? file.size : start + chunkSize

fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
}
}
},
startUpload({file}) {
// console.log(file)
if (this.autoStart) {
file.resume()
}else {
this.statusSet(file.id,"waiting")
}
},
/**
* 一个根文件(文件夹)成功上传完成。
*/
onFileComplete: function (rootFile) {
this.emit('fileSuccess')
},
//文件上传成功
onFileSuccess(rootFile, file, response, chunk) {
let res = JSON.parse(response)
// 服务端自定义的错误(即http状态码为200,但是是错误的情况),这种错误是Uploader无法拦截的
if (res.code != 200) {
this.error('上传失败')
this.statusSet(file.id, 'error')
return
}
// 如果服务端返回了需要合并的参数 !file.params.isFolder &&
if (res.result.needMerge) {
// 文件状态设为“上传中”
this.statusSet(file.id, 'uploading')
let data = {
identifier: file.uniqueIdentifier,
total: res.result.total,
totalSize: file.size,
relativePath: file.relativePath,
fileName: file.name,
...file.params
}
// 发起请求
PostAction("*******", data).then((res) => {
if (res.code == 200) {
this.statusSet(file.id, 'success')
this.successFile++;
} else {
this.erroeFile++;
this.error(res.message)
this.statusSet(file.id, 'error')
}
})
// 不需要合并
} else {
// this.emit('fileSuccess')
// console.log('上传成功')
}


},

onFileProgress(rootFile, file, chunk) {
// console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},

onFileError(rootFile, file, response, chunk) {
this.error(response)
},

close() {
this.uploader.cancel()
},

statusSet(id, status) {
this.customStatus = status
this.$nextTick(() => {
const statusTag = document.querySelector(`.file_${id} .uploader-file-status`)
if (!statusTag){
return
}
let first = statusTag.firstElementChild;
first.innerText = this.fileStatusText[status]
if (status == 'error') {
let bd = document.querySelector(`.file_${id} .uploader-file-progress`)
bd.style.background = "rgb(255 169 178)";

let remove = document.querySelector(`.file_${id} .uploader-file-actions .uploader-file-remove`)
remove.style.display = 'block'
}
})
},

statusRemove(id) {
this.customStatus = ''
this.$nextTick(() => {
const statusTag = document.querySelector(`.custom-status-${id}`)
statusTag.remove()
})
},

emit(e) {
Bus.$emit(e)
this.$emit(e)
},
error(msg) {
this.$notify({
title: '错误',
message: msg,
type: 'error',
duration: 2000
})
},
switchChange() {
this.dialogVisible = true;
let time = setTimeout(()=>{
this.uploader = this.$refs.uploader.uploader
this.close();
this.fileList = [];
this.successFile = this.erroeFile = 0
this.startTime()
clearTimeout(time)
},500)
},
handleSave() {
this.fileList.map((item) => {
item.resume()
});
},
handleClose() {
this.dialogVisible = false;
},
}
}
</script>

自己的小优化

  1. 显示文件总数和成功的数量
1
<div class="title">文件列表 {{fileList.length>0 ? successFile+'/':''}}{{fileList.length>0 ? fileList.length:''}}</div>
  1. 上传文件夹时,列表中显示的是该文件夹下的所有文件

  2. 不在组件中使用uploader-list标签。

  3. 在onFileAdded方法中把文件对象存到自己的变量中。

    1
    2
    3
    onFileAdded(file) {
    this.fileList.push(file)
    }
  4. 循环渲染

    1
    2
    3
    4
    <li class="file-item" v-for="file in fileList" :key="file.id">
    <uploader-file :class="['file_' + file.id, customStatus]" ref="files" :file="file" :list="false">
    </uploader-file>
    </li>
  5. uploader-file标签上的 :list=false

    1
    <uploader-file :class="['file_' + file.id, customStatus]" ref="files" :file="file" :list="false"></uploader-file


vue-simple-uploader 文档

一个基于 simple-uploader.js 的 Vue 上传组件

组件

Uploader

上传根组件,可理解为一个上传器。

Props

  • options {Object}

    参考 simple-uploader.js 配置

    此外,你可以有如下配置项可选:

    • parseTimeRemaining(timeRemaining, parsedTimeRemaining) {Function}

      用于格式化你想要剩余时间,一般可以用来做多语言。参数:

      • timeRemaining{Number}, 剩余时间,秒为单位

      • parsedTimeRemaining{String}, 默认展示的剩余时间内容,你也可以这样做替换使用:

        1
        2
        3
        4
        5
        6
        7
        8
        parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
        return parsedTimeRemaining
        .replace(/\syears?/, '年')
        .replace(/\days?/, '天')
        .replace(/\shours?/, '小时')
        .replace(/\sminutes?/, '分钟')
        .replace(/\sseconds?/, '秒')
        }
    • categoryMap {Object}

      文件类型 map,默认:

      1
      2
      3
      4
      5
      6
      {
      image: ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp'],
      video: ['mp4', 'm3u8', 'rmvb', 'avi', 'swf', '3gp', 'mkv', 'flv'],
      audio: ['mp3', 'wav', 'wma', 'ogg', 'aac', 'flac'],
      document: ['doc', 'txt', 'docx', 'pages', 'epub', 'pdf', 'numbers', 'csv', 'xls', 'xlsx', 'keynote', 'ppt', 'pptx']
      }
  • autoStart {Boolean}

    默认 true, 是否选择文件后自动开始上传。

  • fileStatusText {Object}

    默认:

    1
    2
    3
    4
    5
    6
    7
    {
    success: 'success',
    error: 'error',
    uploading: 'uploading',
    paused: 'paused',
    waiting: 'waiting'
    }

    用于转换文件上传状态文本映射对象。

    0.6.0 版本之后,fileStatusText 可以设置为一个函数,参数为 (status, response = null), 第一个 status 为状态,第二个为响应内容,默认 null,示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fileStatusText(status, response) {
    const statusTextMap = {
    uploading: 'uploading',
    paused: 'paused',
    waiting: 'waiting'
    }
    if (status === 'success' || status === 'error') {
    // 只有status为success或者error的时候可以使用 response

    // eg:
    // return response data ?
    return response.data
    } else {
    return statusTextMap[status]
    }
    }

事件

参见 simple-uploader.js uploader 事件

注意:

  • 所有的事件都会通过 lodash.kebabCase 做转换,例如 fileSuccess 就会变成 file-success

  • catch-all 这个事件是不会触发的。

  • file-added(file), 添加了一个文件事件,一般用做文件校验,如果设置 file.ignored = true 的话这个文件就会被过滤掉。

  • files-added(files, fileList), 添加了一批文件事件,一般用做一次选择的多个文件进行校验,如果设置 files.ignored = true 或者 fileList.ignored = true 的话本次选择的文件就会被过滤掉。

作用域插槽

  • files {Array}

    纯文件列表,没有文件夹概念。

  • fileList {Array}

    统一对待文件、文件夹列表。

  • started

    是否开始上传了。

得到 Uploader 实例

可以通过如下方式获得:

1
2
3
4
5
// 在 uploader 组件上会有 uploader 属性 指向的就是 Uploader 实例
const uploaderInstance = this.$refs.uploader.uploader
// 这里可以调用实例方法
// https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md#方法
uploaderInstance.cancel()

UploaderBtn

点选上传文件按钮。

Props

  • directory {Boolean}

    默认 false, 是否是文件夹上传。

  • single {Boolean}

    默认 false, 如果设为 true,则代表一次只能选择一个文件。

  • attrs {Object}

    默认 {}, 添加到 input 元素上的额外属性。

UploaderDrop

拖拽上传区域。

UploaderList

文件、文件夹列表,同等对待。

作用域插槽

  • fileList {Array}

    文件、文件夹组成数组。

UploaderFiles

文件列表,没有文件夹概念,纯文件列表。

作用域插槽

  • files {Array}

    文件列表。

UploaderUnsupport

不支持 HTML5 File API 的时候会显示。

UploaderFile

文件、文件夹单个组件。

Props

  • file {Uploader.File}

    封装的文件实例。

  • list {Boolean}

    如果是在 UploaderList 组件中使用的话,请设置为 true

作用域插槽

  • file {Uploader.File}

    文件实例。

  • list {Boolean}

    是否在 UploaderList 组件中使用。

  • status {String}

    当前状态,可能是:success, error, uploading, paused, waiting

  • paused {Boolean}

    是否暂停了。

  • error {Boolean}

    是否出错了。

  • averageSpeed {Number}

    平均上传速度,单位字节每秒。

  • formatedAverageSpeed {String}

    格式化后的平均上传速度,类似:3 KB / S

  • currentSpeed {Number}

    当前上传速度,单位字节每秒。

  • isComplete {Boolean}

    是否已经上传完成。

  • isUploading {Boolean}

    是否在上传中。

  • size {Number}

    文件或者文件夹大小。

  • formatedSize {Number}

    格式化后文件或者文件夹大小,类似:10 KB.

  • uploadedSize {Number}

    已经上传大小,单位字节。

  • progress {Number}

    介于 0 到 1 之间的小数,上传进度。

  • progressStyle {String}

    进度样式,transform 属性,类似:{transform: '-50%'}.

  • progressingClass {String}

    正在上传中的时候值为:uploader-file-progressing

  • timeRemaining {Number}

    预估剩余时间,单位秒。

  • formatedTimeRemaining {String}

    格式化后剩余时间,类似:3 miniutes.

  • type {String}

    文件类型。

  • extension {String}

    文件名后缀,小写。

  • fileCategory {String}

    文件分类,其中之一:folder, document, video, audio, image, unknown

Development

1
2
3
4
5
6
7
8
9
10
11
# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report