diff --git a/src/components/dialog/ReorganizeDialog.vue b/src/components/dialog/ReorganizeDialog.vue index ddedc070..b93834d3 100644 --- a/src/components/dialog/ReorganizeDialog.vue +++ b/src/components/dialog/ReorganizeDialog.vue @@ -89,6 +89,27 @@ const previewLoaded = ref(false) // 预览数据 const previewData = ref() +function getFileItemKey(item?: FileItem) { + return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|') +} + +function dedupeFileItems(fileItems?: FileItem[]) { + if (!fileItems?.length) return [] + + const uniqueItems = new Map() + fileItems.forEach(item => { + uniqueItems.set(getFileItemKey(item), item) + }) + + return Array.from(uniqueItems.values()) +} + +function getPreviewItemKey(item: ManualTransferPreviewItem) { + return [item.source ?? '', item.target ?? '', item.success === false ? 'failed' : 'success'].join('|') +} + +const normalizedItems = computed(() => dedupeFileItems(props.items)) + // 分页 const previewPage = ref(1) const previewPageSize = ref(10) @@ -128,18 +149,21 @@ const dialogTitle = computed(() => { // 副标题 const dialogSubtitle = computed(() => { - if (props.items) { - if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length }) - return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path }) + if (normalizedItems.value.length) { + if (normalizedItems.value.length > 1) { + return t('dialog.reorganize.multipleItemsTitle', { count: normalizedItems.value.length }) + } + + return t('dialog.reorganize.singleItemTitle', { path: normalizedItems.value[0].path }) } else if (props.logids) { return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length }) } }) // 禁用指定集数 const disableEpisodeDetail = computed(() => { - if (props.items) { + if (normalizedItems.value.length) { if (transferForm.episode_format) return false - return !(props.items.length === 1 && props.items[0].type !== 'dir') + return !(normalizedItems.value.length === 1 && normalizedItems.value[0].type !== 'dir') } }) @@ -447,17 +471,21 @@ function getPreviewFailureMessage(data?: ManualTransferPreviewData) { function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTransferPreviewData) { if (!incoming) return - const incomingItems = incoming.items ?? [] - const incomingSummary = incoming.summary ?? { - total: incomingItems.length, - success: incomingItems.filter(item => item.success).length, - failed: incomingItems.filter(item => item.success === false).length, - } + const mergedItems = [...(target.items ?? [])] + const existingItemKeys = new Set(mergedItems.map(item => getPreviewItemKey(item))) - target.summary.total += incomingSummary.total ?? 0 - target.summary.success += incomingSummary.success ?? 0 - target.summary.failed += incomingSummary.failed ?? 0 - target.items.push(...incomingItems) + ;(incoming.items ?? []).forEach(item => { + const itemKey = getPreviewItemKey(item) + if (existingItemKeys.has(itemKey)) return + + existingItemKeys.add(itemKey) + mergedItems.push(item) + }) + + target.items = mergedItems + target.summary.total = mergedItems.length + target.summary.success = mergedItems.filter(item => item.success !== false).length + target.summary.failed = mergedItems.filter(item => item.success === false).length if (incoming.message) { target.message = [target.message, incoming.message].filter(Boolean).join(';') @@ -466,7 +494,7 @@ function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTr // 预览整理结果 async function previewTransfer() { - if (!props.logids && !props.items) return + if (!props.logids && !normalizedItems.value.length) return previewLoading.value = true resetPreviewState() @@ -476,9 +504,9 @@ async function previewTransfer() { try { const tasks: Promise[] = [] - if (props.items) { + if (normalizedItems.value.length) { tasks.push( - ...props.items.map(async item => { + ...normalizedItems.value.map(async item => { try { const result = await requestManualTransfer( createTransferPayload({ item, preview: true }), @@ -642,14 +670,14 @@ function stopLoadingProgress() { // 整理文件 async function transfer(background: boolean = false) { - if (!props.logids && !props.items) return + if (!props.logids && !normalizedItems.value.length) return // 显示进度条 progressDialog.value = true // 文件整理 - if (props.items) { - for (const item of props.items) { + if (normalizedItems.value.length) { + for (const item of normalizedItems.value) { if (!background) { // 如果是文件,计算MD5 const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString() diff --git a/src/components/filebrowser/FileList.vue b/src/components/filebrowser/FileList.vue index 659adae5..7a5cb4bf 100644 --- a/src/components/filebrowser/FileList.vue +++ b/src/components/filebrowser/FileList.vue @@ -107,6 +107,47 @@ const currentItem = ref() // 选中的项目 const selected = ref([]) +function getFileItemKey(item?: FileItem) { + return [item?.storage ?? inProps.item.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|') +} + +function dedupeFileItems(fileItems: FileItem[]) { + const uniqueItems = new Map() + fileItems.forEach(item => { + uniqueItems.set(getFileItemKey(item), item) + }) + + return Array.from(uniqueItems.values()) +} + +function syncSelectedItems(nextItems: FileItem[] = items.value) { + if (!selected.value.length) return + + const currentItemMap = new Map(nextItems.map(item => [getFileItemKey(item), item])) + selected.value = dedupeFileItems(selected.value) + .map(item => currentItemMap.get(getFileItemKey(item))) + .filter((item): item is FileItem => !!item) +} + +const selectedKeys = computed(() => new Set(selected.value.map(item => getFileItemKey(item)))) + +function isSelected(item: FileItem) { + return selectedKeys.value.has(getFileItemKey(item)) +} + +function setItemSelected(item: FileItem, checked: boolean) { + const itemKey = getFileItemKey(item) + + if (checked) { + if (!selectedKeys.value.has(itemKey)) { + selected.value = [...selected.value, item] + } + return + } + + selected.value = selected.value.filter(selectedItem => getFileItemKey(selectedItem) !== itemKey) +} + // 识别结果 const nameTestResult = ref() @@ -210,6 +251,7 @@ async function list_files() { return; } items.value = data + syncSelectedItems(data) emit('loading', false) loading.value = false @@ -285,13 +327,7 @@ function changePath(item: FileItem) { // 点击列表项 function listItemClick(item: FileItem) { if (selectMode.value) { - if (selected.value.includes(item)) { - selected.value = selected.value.filter(i => i !== item) - } else { - selected.value.push(item) - } - // 去重 - selected.value = Array.from(new Set(selected.value)) + setItemSelected(item, !isSelected(item)) return false } changePath(item) @@ -436,7 +472,7 @@ function showTransfer(item: FileItem) { // 显示批量整理对话框 function showBatchTransfer() { - transferItems.value = selected.value + transferItems.value = dedupeFileItems(selected.value) transferPopper.value = true } @@ -473,6 +509,7 @@ watch( async () => { // 清空列表 items.value = [] + selected.value = [] // 关闭弹窗 nameTestResult.value = undefined nameTestDialog.value = false @@ -726,7 +763,11 @@ onUnmounted(() => {