|
1 | 1 | // 分片上传通用 hooks,支持多文件/多分片并发、暂停、恢复、取消、重试等 |
2 | 2 | import { computed, onUnmounted, ref } from 'vue' |
3 | 3 | import { throttle } from 'lodash-es' |
| 4 | +import { Message } from '@arco-design/web-vue' |
4 | 5 | import { |
5 | 6 | cancelUpload, |
6 | 7 | completeMultipartUpload, |
@@ -417,42 +418,62 @@ export function useMultipartUploader(props: { |
417 | 418 | // 确保parentPath不是空字符串,如果是则使用"/" |
418 | 419 | const parentPath = task.parentPath && task.parentPath !== '' ? task.parentPath : '/' |
419 | 420 |
|
420 | | - const res = await initMultipartUpload({ |
421 | | - fileName: task.fileName, |
422 | | - fileSize: task.fileSize, |
423 | | - fileMd5: task.fileMd5, |
424 | | - parentPath, |
425 | | - metaData: { |
426 | | - contentType: task.fileType, |
427 | | - originalName: task.fileName, |
428 | | - }, |
429 | | - }) |
430 | | - |
431 | | - if (res && res.data) { |
432 | | - // eslint-disable-next-line no-console |
433 | | - console.log(`[Hooks] initMultipartUpload 成功: ${task.fileName}, uploadId: ${res.data.uploadId}`) |
434 | | - task.uploadId = res.data.uploadId |
435 | | - task.chunkSize = res.data.partSize |
436 | | - task.path = res.data.path |
| 421 | + try { |
| 422 | + const res = await initMultipartUpload({ |
| 423 | + fileName: task.fileName, |
| 424 | + fileSize: task.fileSize, |
| 425 | + fileMd5: task.fileMd5, |
| 426 | + parentPath, |
| 427 | + metaData: { |
| 428 | + contentType: task.fileType, |
| 429 | + originalName: task.fileName, |
| 430 | + }, |
| 431 | + }) |
437 | 432 |
|
438 | | - // 处理断点续传:如果后端返回了已上传的分片编号 |
439 | | - if (res.data.uploadedPartNumbers && res.data.uploadedPartNumbers.length > 0) { |
| 433 | + if (res && res.data) { |
440 | 434 | // eslint-disable-next-line no-console |
441 | | - console.log(`[Hooks] 发现已上传分片: ${task.fileName}, 已上传分片: ${res.data.uploadedPartNumbers.join(',')}`) |
442 | | - // 将已上传的分片编号添加到任务中 |
443 | | - task.uploadedChunks = [...res.data.uploadedPartNumbers] |
| 435 | + console.log(`[Hooks] initMultipartUpload 成功: ${task.fileName}, uploadId: ${res.data.uploadId}`) |
| 436 | + task.uploadId = res.data.uploadId |
| 437 | + task.chunkSize = res.data.partSize |
| 438 | + task.path = res.data.path |
444 | 439 |
|
445 | | - // 计算当前进度 |
446 | | - const totalChunks = Math.ceil(task.fileSize / task.chunkSize) |
447 | | - updateTaskProgress(task, totalChunks) |
| 440 | + // 处理断点续传:如果后端返回了已上传的分片编号 |
| 441 | + if (res.data.uploadedPartNumbers && res.data.uploadedPartNumbers.length > 0) { |
| 442 | + // eslint-disable-next-line no-console |
| 443 | + console.log(`[Hooks] 发现已上传分片: ${task.fileName}, 已上传分片: ${res.data.uploadedPartNumbers.join(',')}`) |
| 444 | + // 将已上传的分片编号添加到任务中 |
| 445 | + task.uploadedChunks = [...res.data.uploadedPartNumbers] |
448 | 446 |
|
449 | | - // eslint-disable-next-line no-console |
450 | | - console.log(`[Hooks] 断点续传进度: ${task.fileName}, 进度: ${(task.progress * 100).toFixed(1)}%`) |
| 447 | + // 计算当前进度 |
| 448 | + const totalChunks = Math.ceil(task.fileSize / task.chunkSize) |
| 449 | + updateTaskProgress(task, totalChunks) |
| 450 | + |
| 451 | + // eslint-disable-next-line no-console |
| 452 | + console.log(`[Hooks] 断点续传进度: ${task.fileName}, 进度: ${(task.progress * 100).toFixed(1)}%`) |
| 453 | + } |
| 454 | + } else { |
| 455 | + throw new Error('初始化分片上传失败:服务器返回数据为空') |
451 | 456 | } |
452 | | - } else { |
453 | | - // eslint-disable-next-line no-console |
454 | | - console.log(`[Hooks] initMultipartUpload 失败: ${task.fileName}`) |
455 | | - task.status = 'failed' |
| 457 | + } catch (error: any) { |
| 458 | + // 处理特定错误类型 |
| 459 | + const errorMsg = error?.response?.data?.message || error?.message || '未知错误' |
| 460 | + |
| 461 | + if (errorMsg.includes('文件名已存在')) { |
| 462 | + task.status = 'failed' |
| 463 | + task.errorMessage = '文件名已存在' |
| 464 | + Message.error(`文件 "${task.fileName}" 已存在,请勿重复上传`) |
| 465 | + } else if (errorMsg.includes('不支持的文件类型')) { |
| 466 | + task.status = 'failed' |
| 467 | + task.errorMessage = '不支持的文件类型' |
| 468 | + Message.error(`文件 "${task.fileName}" 类型不支持`) |
| 469 | + } else { |
| 470 | + task.status = 'failed' |
| 471 | + task.errorMessage = `初始化失败: ${errorMsg}` |
| 472 | + Message.error(`文件 "${task.fileName}" 上传失败:${errorMsg}`) |
| 473 | + } |
| 474 | + |
| 475 | + task._uploading = false |
| 476 | + startNextTasks() |
456 | 477 | return |
457 | 478 | } |
458 | 479 | } |
@@ -528,8 +549,18 @@ export function useMultipartUploader(props: { |
528 | 549 | cancelUpload({ uploadId: task.uploadId }) |
529 | 550 | } |
530 | 551 | } |
531 | | - } catch (e) { |
532 | | - task.status = 'failed' |
| 552 | + } catch (error: any) { |
| 553 | + // 捕获并处理所有未处理的异常 |
| 554 | + const errorMsg = error?.message || '未知错误' |
| 555 | + // eslint-disable-next-line no-console |
| 556 | + console.error(`[Hooks] 上传任务异常: ${task.fileName}, 错误: ${errorMsg}`, error) |
| 557 | + |
| 558 | + if (task.status !== 'failed') { |
| 559 | + task.status = 'failed' |
| 560 | + task.errorMessage = `上传失败: ${errorMsg}` |
| 561 | + } |
| 562 | + |
| 563 | + task._uploading = false |
533 | 564 | startNextTasks() |
534 | 565 | } |
535 | 566 | } |
@@ -595,6 +626,70 @@ export function useMultipartUploader(props: { |
595 | 626 | return |
596 | 627 | } |
597 | 628 |
|
| 629 | + // 检测文件名重复(同一目录下文件名不能重复) |
| 630 | + const duplicateFileKeys = new Set<string>() |
| 631 | + const newFileKeys = new Set<string>() |
| 632 | + const fileKeyMap = new Map<File, string>() |
| 633 | + |
| 634 | + for (const file of validFiles) { |
| 635 | + const relativePath = (file as any).webkitRelativePath || '/' |
| 636 | + let parent = '' |
| 637 | + |
| 638 | + if (isFolder) { |
| 639 | + if (relativePath && relativePath !== '/') { |
| 640 | + parent = props.rootPath || parentPath || '/' |
| 641 | + if (parent.length > 1 && parent.endsWith('/')) { |
| 642 | + parent = parent.slice(0, -1) |
| 643 | + } |
| 644 | + const pathParts = relativePath.split('/') |
| 645 | + pathParts.pop() |
| 646 | + const folderPath = pathParts.join('/') |
| 647 | + if (folderPath) { |
| 648 | + parent = `${parent}/${folderPath}` |
| 649 | + } |
| 650 | + } else { |
| 651 | + parent = props.rootPath || parentPath || '/' |
| 652 | + } |
| 653 | + } else { |
| 654 | + parent = props.rootPath || parentPath || '/' |
| 655 | + } |
| 656 | + |
| 657 | + if (parent.length > 1 && parent.endsWith('/')) { |
| 658 | + parent = parent.slice(0, -1) |
| 659 | + } |
| 660 | + |
| 661 | + const fileKey = `${parent}/${file.name}` |
| 662 | + fileKeyMap.set(file, fileKey) |
| 663 | + |
| 664 | + // 检查是否与已有任务重复 |
| 665 | + const existsInTasks = fileTasks.value.some( |
| 666 | + (task) => task.parentPath === parent && task.fileName === file.name, |
| 667 | + ) |
| 668 | + |
| 669 | + // 检查是否与本次添加的文件重复 |
| 670 | + const existsInNewFiles = newFileKeys.has(fileKey) |
| 671 | + |
| 672 | + if (existsInTasks || existsInNewFiles) { |
| 673 | + duplicateFileKeys.add(fileKey) |
| 674 | + } else { |
| 675 | + newFileKeys.add(fileKey) |
| 676 | + } |
| 677 | + } |
| 678 | + |
| 679 | + // 如果有重复文件,提示用户并跳过 |
| 680 | + if (duplicateFileKeys.size > 0) { |
| 681 | + const duplicateNames = [...duplicateFileKeys].map((k) => k.split('/').pop()).join(', ') |
| 682 | + Message.warning(`以下文件已存在,已跳过:${duplicateNames}`) |
| 683 | + // 过滤掉重复的文件(基于完整路径) |
| 684 | + const uniqueFiles = validFiles.filter((file) => !duplicateFileKeys.has(fileKeyMap.get(file)!)) |
| 685 | + if (uniqueFiles.length === 0) { |
| 686 | + return |
| 687 | + } |
| 688 | + // 继续处理不重复的文件 |
| 689 | + validFiles.length = 0 |
| 690 | + validFiles.push(...uniqueFiles) |
| 691 | + } |
| 692 | + |
598 | 693 | for (const file of validFiles) { |
599 | 694 | const relativePath = (file as any).webkitRelativePath || '/' |
600 | 695 | let parent = '' |
|
0 commit comments