feat: 支持多文件整理预览与模板智能生成 (#476)

This commit is contained in:
Album
2026-05-23 09:24:49 +08:00
committed by GitHub
parent 01796b3dc5
commit 44d23480a3
6 changed files with 164 additions and 45 deletions

View File

@@ -1328,7 +1328,12 @@ export interface TransferForm {
}
// 手动整理请求
export interface ManualTransferPayload extends TransferForm {}
export interface ManualTransferPayload extends Omit<TransferForm, 'fileitem'> {
// 文件项
fileitem?: FileItem
// 多选文件批量请求
fileitems?: FileItem[]
}
// 手动整理预览统计
export interface ManualTransferPreviewSummary {

View File

@@ -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<FileItem | undefined>(() => {
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<void>[] = []
if (normalizedItems.value.length) {
tasks.push(
...normalizedItems.value.map(async item => {
try {
const result = await requestManualTransfer<ManualTransferPreviewData>(
createTransferPayload({ item, preview: true }),
if (shouldUseBatchFileItems(normalizedItems.value)) {
try {
const result = await requestManualTransfer<ManualTransferPreviewData>(
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<ManualTransferPreviewData>(
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)
}
}

View File

@@ -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',

View File

@@ -2508,7 +2508,8 @@ export default {
episodeFormatPlaceholder: '使用{ep}定位集数',
episodeFormatRecommendAction: '智能生成',
episodeFormatRecommendLoading: '生成中...',
episodeFormatRecommendSelectFile: '请先选择单个文件或目录',
episodeFormatRecommendSelectFile: '请先选择单个文件、单个目录,或多个文件',
episodeFormatRecommendInvalidSelection: '当前选择不满足智能生成条件',
episodeFormatRecommendNeedWords: '手动整理集数定位规则为空,请先前往词表填写',
episodeFormatRecommendSuccess: '已生成集数定位模板',
episodeFormatRecommendOverwriteSuccess: '已用智能生成结果覆盖当前集数定位',

View File

@@ -2509,7 +2509,8 @@ export default {
episodeFormatPlaceholder: '使用{ep}定位集數',
episodeFormatRecommendAction: '智能生成',
episodeFormatRecommendLoading: '生成中...',
episodeFormatRecommendSelectFile: '請先選擇單個文件或目錄',
episodeFormatRecommendSelectFile: '請先選擇單個文件、單個目錄,或多個文件',
episodeFormatRecommendInvalidSelection: '當前選擇不滿足智能生成條件',
episodeFormatRecommendNeedWords: '手動整理集數定位規則為空,請先前往詞表填寫',
episodeFormatRecommendSuccess: '已生成集數定位模板',
episodeFormatRecommendOverwriteSuccess: '已用智能生成結果覆蓋當前集數定位',