fix: prevent duplicate manual reorganize requests in filtered directories (#469)

This commit is contained in:
Album
2026-05-14 21:13:46 +08:00
committed by GitHub
parent f3ab2a8eff
commit 6d89dad8de
2 changed files with 99 additions and 30 deletions
+49 -21
View File
@@ -89,6 +89,27 @@ const previewLoaded = ref(false)
// 预览数据 // 预览数据
const previewData = ref<ManualTransferPreviewData>() const previewData = ref<ManualTransferPreviewData>()
function getFileItemKey(item?: FileItem) {
return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
}
function dedupeFileItems(fileItems?: FileItem[]) {
if (!fileItems?.length) return []
const uniqueItems = new Map<string, FileItem>()
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 previewPage = ref(1)
const previewPageSize = ref(10) const previewPageSize = ref(10)
@@ -128,18 +149,21 @@ const dialogTitle = computed(() => {
// 副标题 // 副标题
const dialogSubtitle = computed(() => { const dialogSubtitle = computed(() => {
if (props.items) { if (normalizedItems.value.length) {
if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length }) if (normalizedItems.value.length > 1) {
return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path }) return t('dialog.reorganize.multipleItemsTitle', { count: normalizedItems.value.length })
}
return t('dialog.reorganize.singleItemTitle', { path: normalizedItems.value[0].path })
} else if (props.logids) { } else if (props.logids) {
return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length }) return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length })
} }
}) })
// 禁用指定集数 // 禁用指定集数
const disableEpisodeDetail = computed(() => { const disableEpisodeDetail = computed(() => {
if (props.items) { if (normalizedItems.value.length) {
if (transferForm.episode_format) return false 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) { function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTransferPreviewData) {
if (!incoming) return if (!incoming) return
const incomingItems = incoming.items ?? [] const mergedItems = [...(target.items ?? [])]
const incomingSummary = incoming.summary ?? { const existingItemKeys = new Set(mergedItems.map(item => getPreviewItemKey(item)))
total: incomingItems.length,
success: incomingItems.filter(item => item.success).length,
failed: incomingItems.filter(item => item.success === false).length,
}
target.summary.total += incomingSummary.total ?? 0 ;(incoming.items ?? []).forEach(item => {
target.summary.success += incomingSummary.success ?? 0 const itemKey = getPreviewItemKey(item)
target.summary.failed += incomingSummary.failed ?? 0 if (existingItemKeys.has(itemKey)) return
target.items.push(...incomingItems)
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) { if (incoming.message) {
target.message = [target.message, incoming.message].filter(Boolean).join('') target.message = [target.message, incoming.message].filter(Boolean).join('')
@@ -466,7 +494,7 @@ function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTr
// 预览整理结果 // 预览整理结果
async function previewTransfer() { async function previewTransfer() {
if (!props.logids && !props.items) return if (!props.logids && !normalizedItems.value.length) return
previewLoading.value = true previewLoading.value = true
resetPreviewState() resetPreviewState()
@@ -476,9 +504,9 @@ async function previewTransfer() {
try { try {
const tasks: Promise<void>[] = [] const tasks: Promise<void>[] = []
if (props.items) { if (normalizedItems.value.length) {
tasks.push( tasks.push(
...props.items.map(async item => { ...normalizedItems.value.map(async item => {
try { try {
const result = await requestManualTransfer<ManualTransferPreviewData>( const result = await requestManualTransfer<ManualTransferPreviewData>(
createTransferPayload({ item, preview: true }), createTransferPayload({ item, preview: true }),
@@ -642,14 +670,14 @@ function stopLoadingProgress() {
// 整理文件 // 整理文件
async function transfer(background: boolean = false) { async function transfer(background: boolean = false) {
if (!props.logids && !props.items) return if (!props.logids && !normalizedItems.value.length) return
// 显示进度条 // 显示进度条
progressDialog.value = true progressDialog.value = true
// 文件整理 // 文件整理
if (props.items) { if (normalizedItems.value.length) {
for (const item of props.items) { for (const item of normalizedItems.value) {
if (!background) { if (!background) {
// 如果是文件,计算MD5 // 如果是文件,计算MD5
const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString() const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString()
+50 -9
View File
@@ -107,6 +107,47 @@ const currentItem = ref<FileItem>()
// 选中的项目 // 选中的项目
const selected = ref<FileItem[]>([]) const selected = ref<FileItem[]>([])
function getFileItemKey(item?: FileItem) {
return [item?.storage ?? inProps.item.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
}
function dedupeFileItems(fileItems: FileItem[]) {
const uniqueItems = new Map<string, FileItem>()
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<Context>() const nameTestResult = ref<Context>()
@@ -210,6 +251,7 @@ async function list_files() {
return; return;
} }
items.value = data items.value = data
syncSelectedItems(data)
emit('loading', false) emit('loading', false)
loading.value = false loading.value = false
@@ -285,13 +327,7 @@ function changePath(item: FileItem) {
// 点击列表项 // 点击列表项
function listItemClick(item: FileItem) { function listItemClick(item: FileItem) {
if (selectMode.value) { if (selectMode.value) {
if (selected.value.includes(item)) { setItemSelected(item, !isSelected(item))
selected.value = selected.value.filter(i => i !== item)
} else {
selected.value.push(item)
}
// 去重
selected.value = Array.from(new Set(selected.value))
return false return false
} }
changePath(item) changePath(item)
@@ -436,7 +472,7 @@ function showTransfer(item: FileItem) {
// 显示批量整理对话框 // 显示批量整理对话框
function showBatchTransfer() { function showBatchTransfer() {
transferItems.value = selected.value transferItems.value = dedupeFileItems(selected.value)
transferPopper.value = true transferPopper.value = true
} }
@@ -473,6 +509,7 @@ watch(
async () => { async () => {
// 清空列表 // 清空列表
items.value = [] items.value = []
selected.value = []
// 关闭弹窗 // 关闭弹窗
nameTestResult.value = undefined nameTestResult.value = undefined
nameTestDialog.value = false nameTestDialog.value = false
@@ -726,7 +763,11 @@ onUnmounted(() => {
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="listItemClick(item)"> <VListItem v-bind="hover.props" class="px-3 pe-1" @click="listItemClick(item)">
<template #prepend> <template #prepend>
<VListItemAction v-if="selectMode"> <VListItemAction v-if="selectMode">
<VCheckbox v-model="selected" :value="item" /> <VCheckbox
:model-value="isSelected(item)"
@update:model-value="setItemSelected(item, !!$event)"
@click.stop
/>
</VListItemAction> </VListItemAction>
<template v-else> <template v-else>
<VIcon <VIcon