vue3-chunked-upload 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1312 @@
1
+ # vue3-chunked-upload
2
+
3
+ 基于 Vue 3 的分片上传组件,支持断点续传、并发上传、失败重试等功能。
4
+
5
+ **组件特点**:
6
+ - 完全通用,不依赖任何 UI 组件库
7
+ - 支持通过 `notice` 事件自定义提示方式
8
+ - 可集成 Arco Design、Element Plus 等主流 UI 库
9
+ - 支持拖拽上传和按钮上传两种模式
10
+ - 支持自定义上传区域插槽
11
+ - 支持文件数量限制和类型限制
12
+
13
+ ## 功能特性
14
+
15
+ - **分片上传**:大文件自动分片上传,默认分片大小为 10MB
16
+ - **断点续传**:支持暂停/继续上传,刷新页面后可继续上传
17
+ - **并发控制**:支持配置最大并发数,默认 3 个并发
18
+ - **失败重试**:单个分片上传失败自动重试,默认重试 3 次
19
+ - **拖拽上传**:支持拖拽和点击选择文件
20
+ - **按钮上传**:可通过配置切换为纯按钮上传模式
21
+ - **表单集成**:支持 v-model 双向绑定,可直接集成到表单中
22
+ - **文件类型限制**:支持通过 `accept` 配置限制可上传的文件类型
23
+ - **文件数量限制**:支持通过 `limit` 配置限制最大上传数量
24
+ - **自定义请求头**:支持通过 `headers` 配置自定义请求头
25
+ - **上传前校验**:支持通过 `onBeforeUpload` 回调进行上传前校验
26
+ - **文件类型识别**:自动识别视频、音频文件并显示对应图标
27
+ - **通用提示**:通过事件回调处理提示,不依赖特定 UI 库
28
+ - **禁用功能**:支持 `disabled` 属性禁用上传功能
29
+
30
+ ## 安装
31
+
32
+ ```bash
33
+ npm install vue3-chunked-upload
34
+ # 或
35
+ yarn add vue3-chunked-upload
36
+ # 或
37
+ pnpm add vue3-chunked-upload
38
+ ```
39
+
40
+ ## 快速开始
41
+
42
+ ### 步骤 1:引入组件
43
+
44
+ 在 Vue 组件中引入 `Vue3ChunkedUpload` 组件:
45
+
46
+ ```vue
47
+ <script setup lang="ts">
48
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
49
+ import type { NoticeType } from 'vue3-chunked-upload'
50
+ </script>
51
+ ```
52
+
53
+ ### 步骤 2:在模板中使用
54
+
55
+ 使用 `v-model` 绑定已上传文件列表:
56
+
57
+ ```vue
58
+ <template>
59
+ <Vue3ChunkedUpload v-model="uploadedFiles" @notice="handleNotice" />
60
+ </template>
61
+ ```
62
+
63
+ ### 步骤 3:配置后端接口
64
+
65
+ 在 `options` 中配置你的后端上传接口:
66
+
67
+ ```vue
68
+ <template>
69
+ <Vue3ChunkedUpload
70
+ v-model="uploadedFiles"
71
+ :options="uploadOptions"
72
+ @notice="handleNotice"
73
+ />
74
+ </template>
75
+
76
+ <script setup lang="ts">
77
+ import { ref } from 'vue'
78
+ import { Vue3ChunkedUpload, type ChunkUploadOptions } from 'vue3-chunked-upload'
79
+
80
+ const uploadedFiles = ref([])
81
+
82
+ // 配置上传接口(必填)
83
+ const uploadOptions: ChunkUploadOptions = {
84
+ uploadUrl: '/api/chunkUpload/chunk', // 分片上传接口
85
+ mergeUrl: '/api/chunkUpload/complete', // 文件合并接口
86
+ chunkSize: 10 * 1024 * 1024, // 分片大小,默认 10MB
87
+ maxConcurrent: 3, // 最大并发数
88
+ autoStart: true, // 选择文件后自动开始上传
89
+ }
90
+ </script>
91
+ ```
92
+
93
+ ### 步骤 4:处理提示消息
94
+
95
+ 实现 `handleNotice` 回调,使用你喜欢的 UI 库显示提示:
96
+
97
+ ```vue
98
+ <script setup lang="ts">
99
+ // Arco Design
100
+ import { Message } from '@arco-design/web-vue'
101
+
102
+ const handleNotice = (type: NoticeType, message: string) => {
103
+ const typeMap = {
104
+ success: 'success',
105
+ error: 'error',
106
+ warning: 'warning',
107
+ info: 'info'
108
+ }
109
+ Message[typeMap[type]](message)
110
+ }
111
+ </script>
112
+ ```
113
+
114
+ ## 使用方式
115
+
116
+ ### 基础用法
117
+
118
+ ```vue
119
+ <template>
120
+ <Vue3ChunkedUpload v-model="uploadedFiles" @notice="handleNotice" />
121
+ </template>
122
+
123
+ <script setup lang="ts">
124
+ import { ref } from 'vue'
125
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
126
+ import type { NoticeType, UploadFile } from 'vue3-chunked-upload'
127
+
128
+ const uploadedFiles = ref<UploadFile[]>([])
129
+
130
+ // 处理提示消息(根据使用的 UI 库选择提示方式)
131
+ const handleNotice = (type: NoticeType, message: string) => {
132
+ // Arco Design
133
+ Message[type === 'error' ? 'error' : type === 'warning' ? 'warning' : type === 'info' ? 'info' : 'success'](message)
134
+
135
+ // 或 Element Plus
136
+ // ElMessage[type](message)
137
+
138
+ // 或原生提示
139
+ // alert(message)
140
+ }
141
+ </script>
142
+ ```
143
+
144
+ ### 自定义配置
145
+
146
+ ```vue
147
+ <template>
148
+ <Vue3ChunkedUpload
149
+ v-model="uploadedFiles"
150
+ :options="uploadOptions"
151
+ @success="handleSuccess"
152
+ @error="handleError"
153
+ @progress="handleProgress"
154
+ @notice="handleNotice"
155
+ />
156
+ </template>
157
+
158
+ <script setup lang="ts">
159
+ import { ref } from 'vue'
160
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
161
+ import type { ChunkUploadOptions, UploadTask, NoticeType } from 'vue3-chunked-upload'
162
+
163
+ const uploadedFiles = ref<UploadFile[]>([])
164
+
165
+ const uploadOptions: ChunkUploadOptions = {
166
+ chunkSize: 5 * 1024 * 1024, // 分片大小 5MB
167
+ maxConcurrent: 3, // 最大并发数 3
168
+ uploadUrl: '/api/chunkUpload/chunk',
169
+ mergeUrl: '/api/chunkUpload/complete',
170
+ maxFileSize: 500 * 1024 * 1024, // 最大文件 500MB
171
+ autoStart: true, // 自动开始上传
172
+ retryTimes: 3, // 重试次数
173
+ retryDelay: 1000, // 重试延迟
174
+ headers: { // 自定义请求头
175
+ 'Authorization': 'Bearer token',
176
+ },
177
+ }
178
+
179
+ const handleSuccess = (task: UploadTask) => {
180
+ console.log('文件上传成功:', task.file.name, task.path)
181
+ }
182
+
183
+ const handleError = (task: UploadTask, error: Error) => {
184
+ console.error('文件上传失败:', task.file.name, error.message)
185
+ }
186
+
187
+ const handleProgress = (task: UploadTask) => {
188
+ console.log('上传进度:', task.progress + '%', '速度:', task.speed + 'KB/s')
189
+ }
190
+
191
+ const handleNotice = (type: NoticeType, message: string) => {
192
+ console.log(`[${type}] ${message}`)
193
+ }
194
+ </script>
195
+ ```
196
+
197
+ ### 在表单中使用
198
+
199
+ 在表单中使用时,`v-model` 绑定的数据会自动更新为已上传完成的文件的列表:
200
+
201
+ ```vue
202
+ <template>
203
+ <form @submit.prevent="handleSubmit">
204
+ <div class="mb-4">
205
+ <label class="block text-sm font-medium text-gray-700 mb-2">文件上传</label>
206
+ <Vue3ChunkedUpload
207
+ v-model="formData.files"
208
+ :options="uploadOptions"
209
+ @notice="handleNotice"
210
+ @success="handleSuccess"
211
+ @error="handleError"
212
+ />
213
+ </div>
214
+ <!-- 预览已上传的文件 -->
215
+ <div v-if="formData.files.length > 0" class="mb-4">
216
+ <h4 class="text-sm font-medium text-gray-700">已上传文件:</h4>
217
+ <ul class="list-disc list-inside text-sm text-gray-600">
218
+ <li v-for="file in formData.files" :key="file.id">
219
+ {{ file.name }} ({{ formatSize(file.size) }})
220
+ </li>
221
+ </ul>
222
+ </div>
223
+ <button
224
+ type="submit"
225
+ :disabled="formData.files.length === 0"
226
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
227
+ >
228
+ 提交
229
+ </button>
230
+ </form>
231
+ </template>
232
+
233
+ <script setup lang="ts">
234
+ import { reactive } from 'vue'
235
+ import { Vue3ChunkedUpload, formatFileSize, type UploadFile, type NoticeType, type UploadTask } from 'vue3-chunked-upload'
236
+
237
+ const formData = reactive({
238
+ files: [] as UploadFile[],
239
+ })
240
+
241
+ const uploadOptions = {
242
+ uploadUrl: '/api/chunkUpload/chunk',
243
+ mergeUrl: '/api/chunkUpload/complete',
244
+ }
245
+
246
+ // 格式化文件大小
247
+ const formatSize = (size: number) => formatFileSize(size)
248
+
249
+ // 处理提示消息
250
+ const handleNotice = (type: NoticeType, message: string) => {
251
+ console.log(`[${type}] ${message}`)
252
+ }
253
+
254
+ // 处理上传成功
255
+ const handleSuccess = (task: UploadTask) => {
256
+ console.log('文件上传成功:', task.file.name)
257
+ }
258
+
259
+ // 处理上传失败
260
+ const handleError = (task: UploadTask, error: Error) => {
261
+ console.error('文件上传失败:', task.file.name, error.message)
262
+ }
263
+
264
+ // 提交表单
265
+ const handleSubmit = () => {
266
+ console.log('提交的文件:', formData.files)
267
+ // 在这里调用你的 API 提交表单数据
268
+ }
269
+ </script>
270
+ ```
271
+
272
+ ### 拖拽上传模式(默认)
273
+
274
+ 默认启用拖拽上传,用户可以将文件拖拽到上传区域或点击选择文件:
275
+
276
+ ```vue
277
+ <Vue3ChunkedUpload v-model="files" />
278
+ ```
279
+
280
+ ### 按钮上传模式
281
+
282
+ 设置 `draggable: false` 切换为纯按钮上传模式:
283
+
284
+ ```vue
285
+ <template>
286
+ <Vue3ChunkedUpload
287
+ v-model="files"
288
+ :options="{ draggable: false }"
289
+ @notice="handleNotice"
290
+ />
291
+ </template>
292
+ ```
293
+
294
+ ### 限制文件类型
295
+
296
+ 使用 `accept` 配置限制可上传的文件类型:
297
+
298
+ ```vue
299
+ <template>
300
+ <!-- 只接受图片 -->
301
+ <Vue3ChunkedUpload
302
+ v-model="imageFiles"
303
+ :options="{ accept: 'image/*' }"
304
+ @notice="handleNotice"
305
+ />
306
+
307
+ <!-- 只接受 PDF 和 Word 文档 -->
308
+ <Vue3ChunkedUpload
309
+ v-model="docFiles"
310
+ :options="{ accept: '.pdf,.doc,.docx' }"
311
+ @notice="handleNotice"
312
+ />
313
+
314
+ <!-- 只接受视频 -->
315
+ <Vue3ChunkedUpload
316
+ v-model="videoFiles"
317
+ :options="{ accept: 'video/*' }"
318
+ @notice="handleNotice"
319
+ />
320
+
321
+ <!-- 接受多种类型 -->
322
+ <Vue3ChunkedUpload
323
+ v-model="mixedFiles"
324
+ :options="{ accept: 'image/*,video/*,.pdf,.doc,.docx' }"
325
+ @notice="handleNotice"
326
+ />
327
+ </template>
328
+ ```
329
+
330
+ ### 限制文件数量
331
+
332
+ 使用 `limit` 配置限制最大上传数量:
333
+
334
+ ```vue
335
+ <template>
336
+ <!-- 限制最多上传 5 个文件 -->
337
+ <Vue3ChunkedUpload
338
+ v-model="files"
339
+ :options="{ limit: 5 }"
340
+ @notice="handleNotice"
341
+ />
342
+
343
+ <!-- 限制只能上传 1 个文件 -->
344
+ <Vue3ChunkedUpload
345
+ v-model="singleFile"
346
+ :options="{ limit: 1 }"
347
+ @notice="handleNotice"
348
+ />
349
+
350
+ <!-- 不限制文件数量(默认) -->
351
+ <Vue3ChunkedUpload v-model="files" />
352
+ </template>
353
+ ```
354
+
355
+ ### 上传前校验
356
+
357
+ 使用 `onBeforeUpload` 回调进行上传前校验,返回 `true` 允许上传,`false` 拒绝:
358
+
359
+ ```vue
360
+ <template>
361
+ <Vue3ChunkedUpload
362
+ v-model="files"
363
+ :options="uploadOptions"
364
+ @notice="handleNotice"
365
+ />
366
+ </template>
367
+
368
+ <script setup lang="ts">
369
+ import { ref, reactive } from 'vue'
370
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
371
+ import type { ChunkUploadOptions, NoticeType } from 'vue3-chunked-upload'
372
+
373
+ const files = ref([])
374
+
375
+ const uploadOptions = reactive<ChunkUploadOptions>({
376
+ uploadUrl: '/api/chunkUpload/chunk',
377
+ mergeUrl: '/api/chunkUpload/complete',
378
+
379
+ // 同步校验:检查文件名
380
+ onBeforeUpload: (files: File[]) => {
381
+ const hasInvalidName = files.some(f => f.name.includes('test'))
382
+ if (hasInvalidName) {
383
+ return false // 拒绝上传
384
+ }
385
+ return true // 允许上传
386
+ },
387
+ })
388
+
389
+ // 异步校验:检查文件是否已存在于服务器
390
+ const uploadOptionsAsync = reactive<ChunkUploadOptions>({
391
+ uploadUrl: '/api/chunkUpload/chunk',
392
+ mergeUrl: '/api/chunkUpload/complete',
393
+
394
+ onBeforeUpload: async (files: File[]) => {
395
+ try {
396
+ const response = await fetch('/api/checkFilesExist', {
397
+ method: 'POST',
398
+ headers: { 'Content-Type': 'application/json' },
399
+ body: JSON.stringify({ fileNames: files.map(f => f.name) }),
400
+ })
401
+ const result = await response.json()
402
+
403
+ if (result.hasConflict) {
404
+ return false // 文件已存在,拒绝上传
405
+ }
406
+ return true // 允许上传
407
+ } catch (error) {
408
+ console.error('校验失败:', error)
409
+ return false
410
+ }
411
+ },
412
+ })
413
+ </script>
414
+ ```
415
+
416
+ ### 自定义上传区域
417
+
418
+ 使用 `#trigger` 插槽自定义上传区域:
419
+
420
+ ```vue
421
+ <template>
422
+ <Vue3ChunkedUpload v-model="files" @notice="handleNotice">
423
+ <template #trigger>
424
+ <div class="custom-upload-area">
425
+ <a-button type="primary">
426
+ <upload-outlined /> 点击上传文件
427
+ </a-button>
428
+ <p class="text-gray-500 mt-2">支持 PDF、Word、图片等文件</p>
429
+ </div>
430
+ </template>
431
+ </Vue3ChunkedUpload>
432
+ </template>
433
+ ```
434
+
435
+ ### 禁用上传功能
436
+
437
+ 设置 `disabled` 属性禁用上传功能:
438
+
439
+ ```vue
440
+ <template>
441
+ <!-- 禁用上传功能 -->
442
+ <Vue3ChunkedUpload v-model="files" disabled @notice="handleNotice" />
443
+
444
+ <!-- 动态控制禁用状态 -->
445
+ <Vue3ChunkedUpload
446
+ v-model="files"
447
+ :disabled="isUploading"
448
+ @notice="handleNotice"
449
+ />
450
+ </template>
451
+ ```
452
+
453
+ ### 监听所有上传完成
454
+
455
+ 使用 `@all-complete` 事件监听所有文件上传完成:
456
+
457
+ ```vue
458
+ <template>
459
+ <Vue3ChunkedUpload
460
+ v-model="files"
461
+ @all-complete="handleAllComplete"
462
+ @notice="handleNotice"
463
+ />
464
+ </template>
465
+
466
+ <script setup lang="ts">
467
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
468
+ import type { UploadTask } from 'vue3-chunked-upload'
469
+
470
+ const handleAllComplete = (completedTasks: UploadTask[]) => {
471
+ console.log('所有文件上传完成!')
472
+ console.log('已完成的任务:', completedTasks)
473
+ console.log('上传成功的文件路径:', completedTasks.map(t => t.path))
474
+
475
+ // 可以在这里执行提交表单等操作
476
+ }
477
+ </script>
478
+ ```
479
+
480
+ ### 监听任务添加和删除
481
+
482
+ 使用 `@task-add` 和 `@task-remove` 事件监听任务变化:
483
+
484
+ ```vue
485
+ <template>
486
+ <Vue3ChunkedUpload
487
+ v-model="files"
488
+ @task-add="handleTaskAdd"
489
+ @task-remove="handleTaskRemove"
490
+ @notice="handleNotice"
491
+ />
492
+ </template>
493
+
494
+ <script setup lang="ts">
495
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
496
+ import type { UploadTask } from 'vue3-chunked-upload'
497
+
498
+ const handleTaskAdd = (task: UploadTask) => {
499
+ console.log('添加任务:', task.file.name, `大小: ${(task.file.size / 1024 / 1024).toFixed(2)}MB`)
500
+ }
501
+
502
+ const handleTaskRemove = (task: UploadTask) => {
503
+ console.log('删除任务:', task.file.name)
504
+ }
505
+ </script>
506
+ ```
507
+
508
+ ### 初始文件回显
509
+
510
+ 通过 `v-model` 传入已上传的文件列表,组件会自动回显:
511
+
512
+ ```vue
513
+ <template>
514
+ <Vue3ChunkedUpload v-model="uploadedFiles" @notice="handleNotice" />
515
+ </template>
516
+
517
+ <script setup lang="ts">
518
+ import { ref } from 'vue'
519
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
520
+ import type { UploadFile } from 'vue3-chunked-upload'
521
+
522
+ // 从服务器加载已上传的文件列表
523
+ const uploadedFiles = ref<UploadFile[]>([
524
+ {
525
+ id: 'file1',
526
+ name: 'document.pdf',
527
+ size: 1024000,
528
+ path: '/uploads/2024/document.pdf',
529
+ status: 'completed',
530
+ },
531
+ {
532
+ id: 'file2',
533
+ name: 'video.mp4',
534
+ size: 52428800,
535
+ path: '/uploads/2024/video.mp4',
536
+ status: 'completed',
537
+ },
538
+ ])
539
+ </script>
540
+ ```
541
+
542
+ ### 手动控制上传任务
543
+
544
+ 使用组件暴露的方法手动控制上传任务:
545
+
546
+ ```vue
547
+ <template>
548
+ <div>
549
+ <Vue3ChunkedUpload ref="uploaderRef" v-model="uploadedFiles" :options="uploadOptions" />
550
+
551
+ <!-- 自定义控制按钮 -->
552
+ <div class="mt-4 flex gap-2">
553
+ <button @click="startAll" class="px-3 py-1 bg-green-500 text-white rounded">
554
+ 全部开始
555
+ </button>
556
+ <button @click="pauseAll" class="px-3 py-1 bg-yellow-500 text-white rounded">
557
+ 全部暂停
558
+ </button>
559
+ <button @click="clearAll" class="px-3 py-1 bg-red-500 text-white rounded">
560
+ 清空列表
561
+ </button>
562
+ </div>
563
+ </div>
564
+ </template>
565
+
566
+ <script setup lang="ts">
567
+ import { ref } from 'vue'
568
+ import { Vue3ChunkedUpload, type UploadFile, type ChunkUploadOptions } from 'vue3-chunked-upload'
569
+
570
+ const uploaderRef = ref()
571
+ const uploadedFiles = ref<UploadFile[]>([])
572
+
573
+ const uploadOptions: ChunkUploadOptions = {
574
+ uploadUrl: '/api/chunkUpload/chunk',
575
+ mergeUrl: '/api/chunkUpload/complete',
576
+ autoStart: false, // 关闭自动开始
577
+ }
578
+
579
+ // 开始所有上传
580
+ const startAll = () => {
581
+ uploaderRef.value?.tasks?.forEach(task => {
582
+ if (task.status === 'pending') {
583
+ // 调用内部方法开始上传
584
+ }
585
+ })
586
+ }
587
+
588
+ // 暂停所有上传
589
+ const pauseAll = () => {
590
+ uploaderRef.value?.pauseAll?.()
591
+ }
592
+
593
+ // 清空列表
594
+ const clearAll = () => {
595
+ uploaderRef.value?.clearAllTasks?.()
596
+ }
597
+ </script>
598
+ ```
599
+
600
+ ### 与 Element Plus 集成
601
+
602
+ ```vue
603
+ <template>
604
+ <el-card>
605
+ <template #header>
606
+ <div class="card-header">
607
+ <span>文件上传</span>
608
+ </div>
609
+ </template>
610
+ <Vue3ChunkedUpload
611
+ v-model="uploadedFiles"
612
+ :options="uploadOptions"
613
+ @notice="handleNotice"
614
+ />
615
+ </el-card>
616
+ </template>
617
+
618
+ <script setup lang="ts">
619
+ import { ref } from 'vue'
620
+ import { ElMessage } from 'element-plus'
621
+ import { Vue3ChunkedUpload, type UploadFile, type NoticeType, type ChunkUploadOptions } from 'vue3-chunked-upload'
622
+
623
+ const uploadedFiles = ref<UploadFile[]>([])
624
+
625
+ const uploadOptions: ChunkUploadOptions = {
626
+ uploadUrl: '/api/chunkUpload/chunk',
627
+ mergeUrl: '/api/chunkUpload/complete',
628
+ }
629
+
630
+ // Element Plus 提示
631
+ const handleNotice = (type: NoticeType, message: string) => {
632
+ ElMessage({
633
+ message,
634
+ type: type === 'error' ? 'error' : type === 'warning' ? 'warning' : type === 'info' ? 'info' : 'success',
635
+ })
636
+ }
637
+ </script>
638
+
639
+ <style scoped>
640
+ .card-header {
641
+ display: flex;
642
+ justify-content: space-between;
643
+ align-items: center;
644
+ }
645
+ </style>
646
+ ```
647
+
648
+ ## Props
649
+
650
+ | 属性 | 类型 | 默认值 | 说明 |
651
+ | ------------ | -------------------- | ------ | ------------------------------------- |
652
+ | `modelValue` | `UploadFile[]` | `[]` | 已上传文件列表,支持 v-model 双向绑定 |
653
+ | `options` | `ChunkUploadOptions` | `{}` | 上传配置选项 |
654
+ | `disabled` | `boolean` | `false` | 是否禁用上传功能 |
655
+
656
+ ## Options 配置
657
+
658
+ | 属性 | 类型 | 默认值 | 说明 |
659
+ | --------------- | ------------------------------------------------------------------ | --------------------------- | ------------------------------------------------- |
660
+ | `chunkSize` | `number` | `10 * 1024 * 1024` | 分片大小(字节),默认 10MB |
661
+ | `maxConcurrent` | `number` | `3` | 最大并发上传数 |
662
+ | `uploadUrl` | `string` | `/api/chunkUpload/chunk` | 分片上传接口地址 |
663
+ | `mergeUrl` | `string` | `/api/chunkUpload/complete` | 文件合并接口地址 |
664
+ | `maxFileSize` | `number` | `10 * 1024 * 1024 * 1024` | 最大文件大小(字节),默认 10GB |
665
+ | `autoStart` | `boolean` | `true` | 是否自动开始上传 |
666
+ | `retryTimes` | `number` | `3` | 分片失败重试次数 |
667
+ | `retryDelay` | `number` | `1000` | 重试延迟(毫秒) |
668
+ | `draggable` | `boolean` | `true` | 是否支持拖拽上传,设为 `false` 则使用按钮上传 |
669
+ | `accept` | `string` | `''` | 接受的文件类型,如 `image/*`、`.pdf,.doc,.docx` |
670
+ | `limit` | `number` | `0` | 最大文件数量,`0` 表示不限制 |
671
+ | `headers` | `Record<string, string>` | `{}` | 自定义请求头,用于携带 Token 等认证信息 |
672
+ | `onBeforeUpload`| `(files: File[]) => boolean \| Promise<boolean>` | `() => true` | 上传前校验回调,返回 `true` 允许上传,`false` 拒绝 |
673
+ | `onProgress` | `(task: UploadTask) => void` | `() => {}` | 进度更新回调 |
674
+ | `onSuccess` | `(task: UploadTask) => void` | `() => {}` | 单个文件上传成功回调 |
675
+ | `onError` | `(task: UploadTask, error: Error) => void` | `() => {}` | 单个文件上传失败回调 |
676
+ | `onTaskAdd` | `(task: UploadTask) => void` | `() => {}` | 添加任务回调 |
677
+ | `onTaskRemove` | `(task: UploadTask) => void` | `() => {}` | 删除任务回调 |
678
+ | `onNotice` | `(type: NoticeType, message: string) => void` | `() => {}` | 提示消息回调 |
679
+ | `onAllComplete` | `(completedTasks: UploadTask[]) => void` | `() => {}` | 所有文件上传完成回调 |
680
+
681
+ ## Events
682
+
683
+ | 事件 | 参数 | 说明 |
684
+ | ------------------- | -------------------------------------------- | ----------------------------- |
685
+ | `update:modelValue` | `UploadFile[]` | 文件列表变化时触发 |
686
+ | `success` | `task: UploadTask` | 单个文件上传成功时触发 |
687
+ | `error` | `task: UploadTask, error: Error` | 单个文件上传失败时触发 |
688
+ | `progress` | `task: UploadTask` | 上传进度更新时触发 |
689
+ | `taskAdd` | `task: UploadTask` | 添加任务时触发 |
690
+ | `taskRemove` | `task: UploadTask` | 删除任务时触发 |
691
+ | `notice` | `(type: NoticeType, message: string)` | 提示消息事件,需父组件处理 |
692
+ | `allComplete` | `completedTasks: UploadTask[]` | 所有文件上传完成时触发 |
693
+
694
+ ## Slots
695
+
696
+ | 插槽名 | 说明 | 用法示例 |
697
+ | ------- | ------------ | ---------------------------------------------- |
698
+ | `trigger` | 自定义上传区域 | `<template #trigger>自定义上传区域</template>` |
699
+
700
+ ## NoticeType 类型
701
+
702
+ ```typescript
703
+ type NoticeType = 'success' | 'error' | 'warning' | 'info'
704
+ ```
705
+
706
+ ## 工具函数
707
+
708
+ ### formatFileSize
709
+
710
+ 格式化文件大小为可读字符串。
711
+
712
+ ```typescript
713
+ import { formatFileSize } from 'vue3-chunked-upload'
714
+
715
+ console.log(formatFileSize(1024)) // "1.00 KB"
716
+ console.log(formatFileSize(1048576)) // "1.00 MB"
717
+ ```
718
+
719
+ **参数**:
720
+ - `bytes: number` - 文件大小(字节)
721
+
722
+ **返回值**: `string` - 格式化后的文件大小字符串
723
+
724
+ ## 类型定义
725
+
726
+ ### UploadFile
727
+
728
+ 组件使用的文件类型,用于 v-model 双向绑定:
729
+
730
+ ```typescript
731
+ interface UploadFile {
732
+ id: string // 文件唯一标识
733
+ name: string // 文件名
734
+ size: number // 文件大小(字节)
735
+ path?: string // 上传成功后服务器返回的文件路径
736
+ status: string // 上传状态,通常为 'completed'
737
+ }
738
+ ```
739
+
740
+ ### UploadTask
741
+
742
+ 上传任务对象,包含详细的进度和状态信息:
743
+
744
+ ```typescript
745
+ interface UploadTask {
746
+ id: string // 任务唯一标识
747
+ file: File // 文件对象
748
+ status: 'pending' | 'uploading' | 'paused' | 'completed' | 'error'
749
+ // 任务状态
750
+ progress: number // 上传进度(0-100)
751
+ chunkSize: number // 分片大小
752
+ totalChunks: number // 总分片数
753
+ uploadedChunks: Set<number> // 已上传的分片索引集合
754
+ path?: string // 上传成功后服务器返回的文件路径
755
+ speed?: number // 上传速度(KB/s)
756
+ errorMessage?: string // 错误信息
757
+ }
758
+ ```
759
+
760
+ ### ChunkUploadRequest
761
+
762
+ 分片上传请求参数类型:
763
+
764
+ ```typescript
765
+ interface ChunkUploadRequest {
766
+ fileId: string // 文件唯一标识
767
+ fileName: string // 文件名
768
+ chunkIndex: number // 当前分片索引
769
+ totalChunks: number // 总分片数
770
+ chunk: Blob // 当前分片数据
771
+ }
772
+ ```
773
+
774
+ ### ChunkUploadResponse
775
+
776
+ 分片上传响应格式:
777
+
778
+ ```typescript
779
+ interface ChunkUploadResponse {
780
+ success: boolean
781
+ message?: string
782
+ data?: {
783
+ fileId: string
784
+ chunkIndex: number
785
+ totalChunks: number
786
+ }
787
+ }
788
+ ```
789
+
790
+ ### MergeUploadRequest
791
+
792
+ 合并文件请求参数类型:
793
+
794
+ ```typescript
795
+ interface MergeUploadRequest {
796
+ fileId: string // 文件唯一标识
797
+ fileName: string // 文件名
798
+ totalChunks: number // 总分片数
799
+ fileSize: number // 文件大小
800
+ }
801
+ ```
802
+
803
+ ### MergeUploadResponse
804
+
805
+ 合并文件响应格式:
806
+
807
+ ```typescript
808
+ interface MergeUploadResponse {
809
+ success: boolean
810
+ message?: string
811
+ data?: {
812
+ path: string // 上传成功后服务器返回的文件路径
813
+ fileName: string
814
+ fileSize: number
815
+ }
816
+ }
817
+ ```
818
+
819
+ ### ExistingFile
820
+
821
+ 已存在文件类型,用于初始化已上传的文件列表:
822
+
823
+ ```typescript
824
+ interface ExistingFile {
825
+ id: string // 文件唯一标识
826
+ name: string // 文件名
827
+ size: number // 文件大小(字节)
828
+ path?: string // 文件路径
829
+ status: string // 状态,通常为 'completed'
830
+ }
831
+ ```
832
+
833
+ ## 暴露的方法
834
+
835
+ 通过 ref 可以调用以下方法:
836
+
837
+ ```vue
838
+ <template>
839
+ <Vue3ChunkedUpload ref="uploaderRef" v-model="uploadedFiles" />
840
+ <button @click="handleAddFile">手动添加文件</button>
841
+ <button @click="handlePauseAll">暂停所有</button>
842
+ <button @click="handleResumeAll">继续所有</button>
843
+ <button @click="handleRetryAll">重试失败</button>
844
+ </template>
845
+
846
+ <script setup lang="ts">
847
+ import { ref } from 'vue'
848
+ import { Vue3ChunkedUpload } from 'vue3-chunked-upload'
849
+
850
+ const uploaderRef = ref()
851
+
852
+ // 添加文件
853
+ const handleAddFile = () => {
854
+ const fileInput = document.createElement('input')
855
+ fileInput.type = 'file'
856
+ fileInput.multiple = true
857
+ fileInput.onchange = () => {
858
+ if (fileInput.files) {
859
+ uploaderRef.value?.addFiles(fileInput.files)
860
+ }
861
+ }
862
+ fileInput.click()
863
+ }
864
+
865
+ // 暂停指定上传
866
+ const handlePauseUpload = (taskId: string) => {
867
+ uploaderRef.value?.pauseUpload(taskId)
868
+ }
869
+
870
+ // 恢复指定上传
871
+ const handleResumeUpload = (taskId: string) => {
872
+ uploaderRef.value?.resumeUpload(taskId)
873
+ }
874
+
875
+ // 删除指定任务
876
+ const handleRemoveTask = (taskId: string) => {
877
+ uploaderRef.value?.removeTask(taskId)
878
+ }
879
+
880
+ // 重试指定任务
881
+ const handleRetryTask = (taskId: string) => {
882
+ uploaderRef.value?.retryTask(taskId)
883
+ }
884
+
885
+ // 暂停所有上传
886
+ const handlePauseAll = () => {
887
+ uploaderRef.value?.tasks.forEach((task: any) => {
888
+ if (task.status === 'uploading') {
889
+ uploaderRef.value?.pauseUpload(task.id)
890
+ }
891
+ })
892
+ }
893
+
894
+ // 继续所有上传
895
+ const handleResumeAll = () => {
896
+ uploaderRef.value?.tasks.forEach((task: any) => {
897
+ if (task.status === 'paused' || task.status === 'pending') {
898
+ uploaderRef.value?.resumeUpload(task.id)
899
+ }
900
+ })
901
+ }
902
+
903
+ // 重试所有失败的任务
904
+ const handleRetryAll = () => {
905
+ uploaderRef.value?.tasks.forEach((task: any) => {
906
+ if (task.status === 'error') {
907
+ uploaderRef.value?.retryTask(task.id)
908
+ }
909
+ })
910
+ }
911
+ </script>
912
+ ```
913
+
914
+ | 方法 | 参数 | 说明 |
915
+ | ---------------- | ------------------- | ------------------------ |
916
+ | `addFiles` | `FileList \| File[]` | 添加文件到上传队列 |
917
+ | `pauseUpload` | `taskId: string` | 暂停指定上传任务 |
918
+ | `resumeUpload` | `taskId: string` | 恢复指定上传任务 |
919
+ | `removeTask` | `taskId: string` | 删除指定上传任务 |
920
+ | `retryTask` | `taskId: string` | 重试指定失败任务 |
921
+ | `getUploadedChunks` | `taskId: string` | 获取已上传的分片列表 |
922
+
923
+ ## 后端接口要求
924
+
925
+ ### 分片上传接口
926
+
927
+ **URL**: `POST /api/chunkUpload/chunk`
928
+
929
+ **请求头**:
930
+ ```
931
+ Content-Type: multipart/form-data
932
+ Authorization: Bearer <token> // 可选,通过 headers 配置
933
+ X-Custom-Header: <value> // 可选,通过 headers 配置
934
+ ```
935
+
936
+ **请求体** (FormData):
937
+
938
+ | 字段 | 类型 | 说明 |
939
+ | ------------- | -------- | ------------ |
940
+ | `fileId` | `string` | 文件唯一标识 |
941
+ | `fileName` | `string` | 文件名 |
942
+ | `chunkIndex` | `string` | 分片索引 |
943
+ | `totalChunks` | `string` | 总分片数 |
944
+ | `chunk` | `File` | 分片文件 |
945
+
946
+ **响应**:
947
+
948
+ ```json
949
+ {
950
+ "success": true,
951
+ "message": "上传成功"
952
+ }
953
+ ```
954
+
955
+ ### 合并文件接口
956
+
957
+ **URL**: `POST /api/chunkUpload/complete`
958
+
959
+ **请求头**:
960
+ ```
961
+ Content-Type: application/json
962
+ Authorization: Bearer <token> // 可选,通过 headers 配置
963
+ ```
964
+
965
+ **请求体** (JSON):
966
+
967
+ ```json
968
+ {
969
+ "fileId": "string",
970
+ "fileName": "string",
971
+ "totalChunks": 10,
972
+ "fileSize": 10485760
973
+ }
974
+ ```
975
+
976
+ **响应**:
977
+
978
+ ```json
979
+ {
980
+ "success": true,
981
+ "data": {
982
+ "path": "/uploads/2024/07/file-name.mp4"
983
+ },
984
+ "message": "合并成功"
985
+ }
986
+ ```
987
+
988
+
989
+ ## Node.js 后端示例
990
+
991
+ 以下是使用 Express + Multer 实现的完整 Node.js 后端代码:
992
+
993
+ ```javascript
994
+ // server.js
995
+ const express = require('express')
996
+ const multer = require('multer')
997
+ const fs = require('fs')
998
+ const path = require('path')
999
+
1000
+ const app = express()
1001
+
1002
+ // 配置存储
1003
+ const TEMP_DIR = path.join(__dirname, 'temp')
1004
+ const UPLOAD_DIR = path.join(__dirname, 'uploads')
1005
+
1006
+ // 确保目录存在
1007
+ if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true })
1008
+ if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
1009
+
1010
+ // 配置 multer
1011
+ const storage = multer.memoryStorage()
1012
+ const upload = multer({
1013
+ storage: storage,
1014
+ limits: {
1015
+ fileSize: 50 * 1024 * 1024, // 限制单个分片 50MB
1016
+ },
1017
+ })
1018
+
1019
+ // ============= 核心功能:分片上传 =============
1020
+
1021
+ /**
1022
+ * 1. 上传分片接口
1023
+ */
1024
+ app.post('/chunkUpload/chunk', upload.single('chunk'), (req, res) => {
1025
+ try {
1026
+ const { fileId, chunkIndex, totalChunks, fileName } = req.body
1027
+
1028
+ // 验证必要参数
1029
+ if (!fileId || chunkIndex === undefined || !totalChunks || !req.file) {
1030
+ return res.status(400).json({
1031
+ success: false,
1032
+ message: '缺少必要参数',
1033
+ })
1034
+ }
1035
+
1036
+ // 创建文件专属临时目录
1037
+ const fileTempDir = path.join(TEMP_DIR, fileId)
1038
+ if (!fs.existsSync(fileTempDir)) {
1039
+ fs.mkdirSync(fileTempDir, { recursive: true })
1040
+ }
1041
+
1042
+ // 保存分片文件(使用 chunkIndex 命名,保证顺序)
1043
+ const chunkPath = path.join(fileTempDir, `chunk_${chunkIndex}`)
1044
+ fs.writeFileSync(chunkPath, req.file.buffer)
1045
+
1046
+ // 可选:保存分片元数据
1047
+ const metadataPath = path.join(fileTempDir, 'metadata.json')
1048
+ let metadata = {}
1049
+ if (fs.existsSync(metadataPath)) {
1050
+ metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
1051
+ }
1052
+
1053
+ metadata[chunkIndex] = {
1054
+ size: req.file.size,
1055
+ uploadedAt: new Date().toISOString(),
1056
+ chunkIndex: parseInt(chunkIndex),
1057
+ }
1058
+
1059
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2))
1060
+
1061
+ console.log(`分片 ${chunkIndex}/${totalChunks} 上传成功,文件ID: ${fileId}`)
1062
+
1063
+ res.json({
1064
+ success: true,
1065
+ message: `分片 ${chunkIndex} 上传成功`,
1066
+ data: {
1067
+ fileId,
1068
+ chunkIndex: parseInt(chunkIndex),
1069
+ totalChunks: parseInt(totalChunks),
1070
+ },
1071
+ })
1072
+ } catch (error) {
1073
+ console.error('上传分片失败:', error)
1074
+ res.status(500).json({
1075
+ success: false,
1076
+ message: error.message || '上传分片失败',
1077
+ })
1078
+ }
1079
+ })
1080
+
1081
+ /**
1082
+ * 2. 合并文件接口(核心逻辑)
1083
+ */
1084
+ app.post('/chunkUpload/complete', (req, res) => {
1085
+ try {
1086
+ const { fileId, fileName, totalChunks, fileSize } = req.body
1087
+
1088
+ if (!fileId || !fileName || !totalChunks) {
1089
+ return res.status(400).json({
1090
+ success: false,
1091
+ message: '缺少必要参数',
1092
+ })
1093
+ }
1094
+
1095
+ const fileTempDir = path.join(TEMP_DIR, fileId)
1096
+
1097
+ // 检查临时目录是否存在
1098
+ if (!fs.existsSync(fileTempDir)) {
1099
+ return res.status(404).json({
1100
+ success: false,
1101
+ message: '文件分片不存在',
1102
+ })
1103
+ }
1104
+
1105
+ // 按索引顺序合并
1106
+ const outputPath = path.join(UPLOAD_DIR, fileName)
1107
+ const writeStream = fs.createWriteStream(outputPath)
1108
+
1109
+ // 按顺序读取并写入每个分片
1110
+ for (let i = 0; i < parseInt(totalChunks); i++) {
1111
+ const chunkPath = path.join(fileTempDir, `chunk_${i}`)
1112
+
1113
+ // 检查分片是否存在
1114
+ if (!fs.existsSync(chunkPath)) {
1115
+ writeStream.close()
1116
+ fs.unlinkSync(outputPath)
1117
+
1118
+ return res.status(400).json({
1119
+ success: false,
1120
+ message: `分片 ${i} 缺失,请重新上传`,
1121
+ })
1122
+ }
1123
+
1124
+ // 读取分片数据并写入
1125
+ const chunkData = fs.readFileSync(chunkPath)
1126
+ writeStream.write(chunkData)
1127
+
1128
+ // 删除分片文件(节省空间)
1129
+ fs.unlinkSync(chunkPath)
1130
+ }
1131
+
1132
+ // 删除元数据文件
1133
+ const metadataPath = path.join(fileTempDir, 'metadata.json')
1134
+ if (fs.existsSync(metadataPath)) {
1135
+ fs.unlinkSync(metadataPath)
1136
+ }
1137
+
1138
+ // 等待流写入完成后再继续
1139
+ writeStream.on('finish', () => {
1140
+ // 删除临时目录
1141
+ fs.rmdirSync(fileTempDir)
1142
+
1143
+ // 验证文件大小
1144
+ const stats = fs.statSync(outputPath)
1145
+ if (fileSize && stats.size !== parseInt(fileSize)) {
1146
+ fs.unlinkSync(outputPath)
1147
+ return res.status(400).json({
1148
+ success: false,
1149
+ message: `文件大小不匹配,期望 ${fileSize},实际 ${stats.size}`,
1150
+ })
1151
+ }
1152
+
1153
+ console.log(`文件合并成功: ${fileName}, 大小: ${stats.size} bytes`)
1154
+
1155
+ res.json({
1156
+ success: true,
1157
+ message: '文件合并成功',
1158
+ data: {
1159
+ fileName,
1160
+ fileSize: stats.size,
1161
+ path: `/uploads/${fileName}`, // 返回相对路径,供前端使用
1162
+ },
1163
+ })
1164
+ })
1165
+
1166
+ writeStream.end()
1167
+ } catch (error) {
1168
+ console.error('合并文件失败:', error)
1169
+ res.status(500).json({
1170
+ success: false,
1171
+ message: error.message || '合并文件失败',
1172
+ })
1173
+ }
1174
+ })
1175
+
1176
+
1177
+ ```
1178
+
1179
+ ## 常见问题
1180
+
1181
+ ### 1. 上传接口需要认证 token 怎么办?
1182
+
1183
+ 你可以在 `options` 中添加自定义请求头:
1184
+
1185
+ ```typescript
1186
+ const uploadOptions: ChunkUploadOptions = {
1187
+ uploadUrl: '/api/chunkUpload/chunk',
1188
+ mergeUrl: '/api/chunkUpload/complete',
1189
+ headers: {
1190
+ Authorization: `Bearer ${getToken()}`,
1191
+ },
1192
+ }
1193
+ ```
1194
+
1195
+ ### 2. 如何限制上传文件的类型?
1196
+
1197
+ 可以使用 `accept` 配置限制:
1198
+
1199
+ ```vue
1200
+ <!-- 只接受图片 -->
1201
+ <Vue3ChunkedUpload :options="{ accept: 'image/*' }" />
1202
+
1203
+ <!-- 只接受 PDF 和 Word -->
1204
+ <Vue3ChunkedUpload :options="{ accept: '.pdf,.doc,.docx' }" />
1205
+ ```
1206
+
1207
+ ### 3. 如何显示上传速度?
1208
+
1209
+ `UploadTask` 中包含 `speed` 属性,单位为 KB/s:
1210
+
1211
+ ```typescript
1212
+ const handleProgress = (task: UploadTask) => {
1213
+ if (task.speed) {
1214
+ console.log(`上传速度: ${task.speed} KB/s`)
1215
+ }
1216
+ }
1217
+ ```
1218
+
1219
+ ### 4. 上传中断后如何恢复?
1220
+
1221
+ 组件会自动保存已上传的分片信息,通过 `resumeUpload` 方法可以继续上传:
1222
+
1223
+ ```typescript
1224
+ // 继续上传
1225
+ resumeUpload(taskId: string)
1226
+
1227
+ // 继续所有暂停的任务
1228
+ resumeAll()
1229
+ ```
1230
+
1231
+ ### 5. 如何处理大文件(超过 2GB)?
1232
+
1233
+ 确保以下几点:
1234
+
1235
+ 1. 服务器配置允许大文件上传
1236
+ 2. 分片大小适中(建议 5-10MB)
1237
+ 3. 服务器端正确处理大文件的合并和存储
1238
+
1239
+ ```typescript
1240
+ const uploadOptions: ChunkUploadOptions = {
1241
+ chunkSize: 5 * 1024 * 1024, // 5MB 分片
1242
+ maxFileSize: 5 * 1024 * 1024 * 1024, // 5GB 最大文件
1243
+ }
1244
+ ```
1245
+
1246
+ ### 6. 多个文件同时上传的顺序如何控制?
1247
+
1248
+ 默认情况下,多个文件会并发上传(由 `maxConcurrent` 控制)。如果需要串行上传:
1249
+
1250
+ ```typescript
1251
+ const uploadOptions: ChunkUploadOptions = {
1252
+ maxConcurrent: 1, // 一次只上传一个文件
1253
+ }
1254
+ ```
1255
+
1256
+ ## 最佳实践
1257
+
1258
+ ### 1. 合理设置分片大小
1259
+
1260
+ - **小文件**(< 50MB):使用默认 10MB 分片即可
1261
+ - **大文件**(50MB - 1GB):建议 5MB 分片,平衡并发效率和失败重试成本
1262
+ - **超大文件**(> 1GB):建议 2-5MB 分片,减少单次失败的影响范围
1263
+
1264
+ ### 2. 错误处理
1265
+
1266
+ 务必实现错误回调,以便及时发现问题:
1267
+
1268
+ ```typescript
1269
+ const uploadOptions: ChunkUploadOptions = {
1270
+ // ...其他配置
1271
+ onError: (task, error) => {
1272
+ // 上报到监控系统
1273
+ reportError({
1274
+ fileName: task.file.name,
1275
+ error: error.message,
1276
+ chunkIndex: task.currentChunk,
1277
+ })
1278
+ },
1279
+ }
1280
+ ```
1281
+
1282
+ ### 3. 用户体验优化
1283
+
1284
+ 1. **禁用按钮**:上传过程中禁用提交按钮,防止用户重复点击
1285
+ 2. **进度提示**:展示总体进度和单个文件进度
1286
+ 3. **取消确认**:提供确认对话框,防止误操作取消上传
1287
+ 4. **自动清理**:定期清理未完成的上传任务,避免占用临时存储
1288
+
1289
+ ### 4. 安全建议
1290
+
1291
+ 1. **文件类型验证**:后端必须验证文件类型,不能仅依赖前端
1292
+ 2. **文件名处理**:使用唯一 ID 存储,避免文件名冲突和安全问题
1293
+ 3. **大小限制**:后端同时限制单文件和总上传大小
1294
+ 4. **临时文件清理**:定期清理临时目录,防止磁盘空间耗尽
1295
+
1296
+
1297
+ ## 更新日志
1298
+
1299
+ ### v1.0.0 (2026-07)
1300
+ - 初始版本发布
1301
+ - 支持分片上传、断点续传、并发控制
1302
+ - 支持拖拽上传和按钮上传
1303
+ - 支持自定义上传区域插槽
1304
+ - 支持文件类型和数量限制
1305
+ - 支持上传前校验
1306
+ - 支持自定义请求头
1307
+ - 支持禁用功能
1308
+ - 支持所有文件上传完成事件
1309
+
1310
+ ## License
1311
+
1312
+ MIT