From 44d23480a380b649a8f44740b6029dc2b7c7c487 Mon Sep 17 00:00:00 2001 From: Album <51018113+Mister-album@users.noreply.github.com> Date: Sat, 23 May 2026 09:24:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=9A=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=95=B4=E7=90=86=E9=A2=84=E8=A7=88=E4=B8=8E=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E6=99=BA=E8=83=BD=E7=94=9F=E6=88=90=20(#476)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/api/types.ts | 7 +- src/components/dialog/ReorganizeDialog.vue | 191 ++++++++++++++++----- src/locales/en-US.ts | 3 +- src/locales/zh-CN.ts | 3 +- src/locales/zh-TW.ts | 3 +- 6 files changed, 164 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 879aa1eb..bbfffac3 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ package-lock.json # iconify dist files src/@iconify/*.js public/plugin_icon/** +docs-lock/ +.trae/ diff --git a/src/api/types.ts b/src/api/types.ts index 94bd176a..0d19b5bf 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1328,7 +1328,12 @@ export interface TransferForm { } // 手动整理请求 -export interface ManualTransferPayload extends TransferForm {} +export interface ManualTransferPayload extends Omit { + // 文件项 + fileitem?: FileItem + // 多选文件批量请求 + fileitems?: FileItem[] +} // 手动整理预览统计 export interface ManualTransferPreviewSummary { diff --git a/src/components/dialog/ReorganizeDialog.vue b/src/components/dialog/ReorganizeDialog.vue index 0904cce7..e6fd5bab 100644 --- a/src/components/dialog/ReorganizeDialog.vue +++ b/src/components/dialog/ReorganizeDialog.vue @@ -438,15 +438,37 @@ const previewToggleIcon = computed(() => { return previewVisible.value ? 'mdi-eye-off-outline' : 'mdi-eye-outline' }) +function getFileParentKey(item?: FileItem) { + if (!item?.path) return '' + const storage = item.storage ?? 'local' + const pathParts = item.path.split('/') + pathParts.pop() + const parentPath = pathParts.join('/') || '/' + return `${storage}|${parentPath}` +} + +const episodeFormatRecommendSelectedFileItems = computed(() => { + return shouldUseBatchFileItems(normalizedItems.value) ? normalizedItems.value : [] +}) + +const episodeFormatRecommendHasValidSelectedFiles = computed(() => { + if (episodeFormatRecommendSelectedFileItems.value.length <= 1) return false + + const directoryKeys = new Set( + episodeFormatRecommendSelectedFileItems.value.map(item => getFileParentKey(item)), + ) + return directoryKeys.size === 1 +}) + const episodeFormatRecommendSourceItem = computed(() => { - if (transferForm.fileitem?.path) return transferForm.fileitem if (normalizedItems.value.length !== 1) return undefined return normalizedItems.value[0] }) const canRecommendEpisodeFormat = computed(() => { return ( - Boolean(episodeFormatRecommendSourceItem.value?.path) && + (Boolean(episodeFormatRecommendSourceItem.value?.path) || + episodeFormatRecommendHasValidSelectedFiles.value) && !progressDialog.value && !episodeFormatRecommendState.loading ) @@ -454,7 +476,15 @@ const canRecommendEpisodeFormat = computed(() => { const episodeFormatRecommendTooltip = computed(() => { if (episodeFormatRecommendState.loading) return t('dialog.reorganize.episodeFormatRecommendLoading') - if (!episodeFormatRecommendSourceItem.value?.path) return t('dialog.reorganize.episodeFormatRecommendSelectFile') + if ( + normalizedItems.value.length > 1 && + !episodeFormatRecommendHasValidSelectedFiles.value + ) { + return t('dialog.reorganize.episodeFormatRecommendInvalidSelection') + } + if (!episodeFormatRecommendSourceItem.value?.path && !episodeFormatRecommendHasValidSelectedFiles.value) { + return t('dialog.reorganize.episodeFormatRecommendSelectFile') + } if (episodeFormatRuleConfigured.value === false) return t('dialog.reorganize.episodeFormatRecommendNeedWords') return t('dialog.reorganize.episodeFormatRecommendAction') }) @@ -472,14 +502,35 @@ watch( { immediate: true }, ) +function shouldUseBatchFileItems(items: FileItem[]) { + return items.length > 0 && items.every(item => item.type === 'file') +} + +function getBatchItemsLabel(items: FileItem[]) { + if (items.length === 1) return items[0].path || items[0].name + return t('dialog.reorganize.multipleItemsTitle', { count: items.length }) +} + // 构造整理请求 -function createTransferPayload(options: { item?: FileItem; logid?: number; preview?: boolean }) { +function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; logid?: number; preview?: boolean }) { + const sourceItem = + options.item ?? + (options.items?.length + ? options.items[0] + : ({} as FileItem)) const payload: ManualTransferPayload = { ...transferForm, - fileitem: options.item ?? ({} as FileItem), + fileitem: sourceItem, logid: options.logid ?? 0, } + if (options.items?.length) { + payload.fileitems = options.items + if (!options.item) { + // 文件集合请求以 fileitems 为准,避免残留 fileitem 状态把请求误导成目录语义。 + delete payload.fileitem + } + } if (options.preview) payload.preview = true return payload } @@ -504,8 +555,14 @@ async function loadEpisodeFormatRuleConfiguration() { async function handleRecommendEpisodeFormat() { const sourceItem = episodeFormatRecommendSourceItem.value - if (!sourceItem?.path) { - $toast.warning(t('dialog.reorganize.episodeFormatRecommendSelectFile')) + const selectedFileItems = episodeFormatRecommendSelectedFileItems.value + const hasValidSelectedFiles = episodeFormatRecommendHasValidSelectedFiles.value + if (!sourceItem?.path && !hasValidSelectedFiles) { + $toast.warning( + normalizedItems.value.length > 1 + ? t('dialog.reorganize.episodeFormatRecommendInvalidSelection') + : t('dialog.reorganize.episodeFormatRecommendSelectFile'), + ) return } @@ -518,9 +575,16 @@ async function handleRecommendEpisodeFormat() { try { const hasExistingEpisodeFormat = Boolean(transferForm.episode_format?.trim()) - const result = await api.post('transfer/episode-format/recommend', { - fileitem: sourceItem, - }) + const result = await api.post( + 'transfer/episode-format/recommend', + hasValidSelectedFiles + ? { + fileitems: selectedFileItems, + } + : { + fileitem: sourceItem, + }, + ) if (!result.success) { $toast.error(result.message || t('dialog.reorganize.episodeFormatRecommendFailed')) @@ -645,40 +709,68 @@ async function previewTransfer() { const tasks: Promise[] = [] if (normalizedItems.value.length) { - tasks.push( - ...normalizedItems.value.map(async item => { - try { - const result = await requestManualTransfer( - createTransferPayload({ item, preview: true }), + if (shouldUseBatchFileItems(normalizedItems.value)) { + try { + const result = await requestManualTransfer( + createTransferPayload({ items: normalizedItems.value, preview: true }), + ) + if (!result.success) { + mergePreviewData( + mergedPreviewData, + createFailedPreviewData({ + source: getBatchItemsLabel(normalizedItems.value), + message: result.message || t('dialog.reorganize.previewRequestFailed'), + }), ) - if (!result.success) { + } else { + mergePreviewData(mergedPreviewData, result.data) + } + } catch (err: any) { + console.warn(`预览请求异常: ${err?.message}`) + mergePreviewData( + mergedPreviewData, + createFailedPreviewData({ + source: getBatchItemsLabel(normalizedItems.value), + message: `${getBatchItemsLabel(normalizedItems.value)}: ${err?.message || t('dialog.reorganize.previewRequestFailed')}`, + }), + ) + } + } else { + tasks.push( + ...normalizedItems.value.map(async item => { + try { + const result = await requestManualTransfer( + createTransferPayload({ item, preview: true }), + ) + if (!result.success) { + mergePreviewData( + mergedPreviewData, + createFailedPreviewData({ + source: item.path || item.name, + type: item.type, + title: item.name, + message: result.message || t('dialog.reorganize.previewRequestFailed'), + }), + ) + return + } + + mergePreviewData(mergedPreviewData, result.data) + } catch (err: any) { + console.warn(`预览请求异常: ${err?.message}`) mergePreviewData( mergedPreviewData, createFailedPreviewData({ source: item.path || item.name, type: item.type, title: item.name, - message: result.message || t('dialog.reorganize.previewRequestFailed'), + message: `${item.name || item.path}: ${err?.message || t('dialog.reorganize.previewRequestFailed')}`, }), ) - return } - - mergePreviewData(mergedPreviewData, result.data) - } catch (err: any) { - console.warn(`预览请求异常: ${err?.message}`) - mergePreviewData( - mergedPreviewData, - createFailedPreviewData({ - source: item.path || item.name, - type: item.type, - title: item.name, - message: `${item.name || item.path}: ${err?.message || t('dialog.reorganize.previewRequestFailed')}`, - }), - ) - } - }), - ) + }), + ) + } } if (props.logids) { @@ -794,6 +886,16 @@ async function handleTransfer(item: FileItem, background: boolean = false) { } } +async function handleTransferBatch(items: FileItem[], background: boolean = false) { + try { + const result: { [key: string]: any } = await requestManualTransfer(createTransferPayload({ items }), background) + if (!result.success) $toast.error(result.message) + else if (background) $toast.success(t('dialog.reorganize.successMessage', { name: getBatchItemsLabel(items) })) + } catch (e) { + console.log(e) + } +} + // 整理日志 async function handleTransferLog(logid: number, background: boolean = false) { try { @@ -850,15 +952,22 @@ async function transfer(background: boolean = false) { // 文件整理 if (normalizedItems.value.length) { - for (const item of normalizedItems.value) { + if (shouldUseBatchFileItems(normalizedItems.value)) { if (!background) { - // 如果是文件,计算MD5 - const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString() - - // 开始监听进度 - startLoadingProgress(key) + startLoadingProgress('filetransfer') + } + await handleTransferBatch(normalizedItems.value, background) + } else { + for (const item of normalizedItems.value) { + if (!background) { + // 如果是文件,计算MD5 + const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString() + + // 开始监听进度 + startLoadingProgress(key) + } + await handleTransfer(item, background) } - await handleTransfer(item, background) } } diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index b3464008..bd48fbde 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -2554,7 +2554,8 @@ export default { episodeFormatPlaceholder: 'Use {ep} to position episode', episodeFormatRecommendAction: 'Generate', episodeFormatRecommendLoading: 'Generating...', - episodeFormatRecommendSelectFile: 'Please select a single file or directory first', + episodeFormatRecommendSelectFile: 'Please select a single file, a single directory, or multiple files first', + episodeFormatRecommendInvalidSelection: 'Current selection cannot be used for template generation', episodeFormatRecommendNeedWords: 'Manual episode positioning rules are empty, please fill them in on the words page first', episodeFormatRecommendSuccess: 'Episode format template generated', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index f305d590..13444903 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -2508,7 +2508,8 @@ export default { episodeFormatPlaceholder: '使用{ep}定位集数', episodeFormatRecommendAction: '智能生成', episodeFormatRecommendLoading: '生成中...', - episodeFormatRecommendSelectFile: '请先选择单个文件或目录', + episodeFormatRecommendSelectFile: '请先选择单个文件、单个目录,或多个文件', + episodeFormatRecommendInvalidSelection: '当前选择不满足智能生成条件', episodeFormatRecommendNeedWords: '手动整理集数定位规则为空,请先前往词表填写', episodeFormatRecommendSuccess: '已生成集数定位模板', episodeFormatRecommendOverwriteSuccess: '已用智能生成结果覆盖当前集数定位', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index db7ec506..f3eb7679 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -2509,7 +2509,8 @@ export default { episodeFormatPlaceholder: '使用{ep}定位集數', episodeFormatRecommendAction: '智能生成', episodeFormatRecommendLoading: '生成中...', - episodeFormatRecommendSelectFile: '請先選擇單個文件或目錄', + episodeFormatRecommendSelectFile: '請先選擇單個文件、單個目錄,或多個文件', + episodeFormatRecommendInvalidSelection: '當前選擇不滿足智能生成條件', episodeFormatRecommendNeedWords: '手動整理集數定位規則為空,請先前往詞表填寫', episodeFormatRecommendSuccess: '已生成集數定位模板', episodeFormatRecommendOverwriteSuccess: '已用智能生成結果覆蓋當前集數定位',