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

View File

@@ -89,6 +89,27 @@ const previewLoaded = ref(false)
// 预览数据
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 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<void>[] = []
if (props.items) {
if (normalizedItems.value.length) {
tasks.push(
...props.items.map(async item => {
...normalizedItems.value.map(async item => {
try {
const result = await requestManualTransfer<ManualTransferPreviewData>(
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()

View File

@@ -107,6 +107,47 @@ const currentItem = 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>()
@@ -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(() => {
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="listItemClick(item)">
<template #prepend>
<VListItemAction v-if="selectMode">
<VCheckbox v-model="selected" :value="item" />
<VCheckbox
:model-value="isSelected(item)"
@update:model-value="setItemSelected(item, !!$event)"
@click.stop
/>
</VListItemAction>
<template v-else>
<VIcon