Skip to content

Commit dc8b35d

Browse files
authored
feat(system/file): 新增上传前重复文件检测,避免同一目录下重复上传相同文件
1 parent 9610843 commit dc8b35d

2 files changed

Lines changed: 130 additions & 34 deletions

File tree

src/components/FilePreview/index.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ const filePreview = reactive<FilePreview>({
7070
// 弹框标题
7171
const modalTitle = computed(() => {
7272
const { fileName, fileType } = filePreview.fileInfo || {}
73-
return fileName && fileType ? `${fileName}.${fileType}` : '文件预览'
73+
// fileName 已经包含扩展名,直接显示
74+
return fileName || '文件预览'
7475
})
7576
7677
// 预览

src/hooks/modules/useMultipartUploader.ts

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// 分片上传通用 hooks,支持多文件/多分片并发、暂停、恢复、取消、重试等
22
import { computed, onUnmounted, ref } from 'vue'
33
import { throttle } from 'lodash-es'
4+
import { Message } from '@arco-design/web-vue'
45
import {
56
cancelUpload,
67
completeMultipartUpload,
@@ -417,42 +418,62 @@ export function useMultipartUploader(props: {
417418
// 确保parentPath不是空字符串,如果是则使用"/"
418419
const parentPath = task.parentPath && task.parentPath !== '' ? task.parentPath : '/'
419420

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+
})
437432

438-
// 处理断点续传:如果后端返回了已上传的分片编号
439-
if (res.data.uploadedPartNumbers && res.data.uploadedPartNumbers.length > 0) {
433+
if (res && res.data) {
440434
// 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
444439

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]
448446

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('初始化分片上传失败:服务器返回数据为空')
451456
}
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()
456477
return
457478
}
458479
}
@@ -528,8 +549,18 @@ export function useMultipartUploader(props: {
528549
cancelUpload({ uploadId: task.uploadId })
529550
}
530551
}
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
533564
startNextTasks()
534565
}
535566
}
@@ -595,6 +626,70 @@ export function useMultipartUploader(props: {
595626
return
596627
}
597628

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+
598693
for (const file of validFiles) {
599694
const relativePath = (file as any).webkitRelativePath || '/'
600695
let parent = ''

0 commit comments

Comments
 (0)