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 163 additions and 44 deletions
+2
View File
@@ -35,3 +35,5 @@ package-lock.json
# iconify dist files # iconify dist files
src/@iconify/*.js src/@iconify/*.js
public/plugin_icon/** public/plugin_icon/**
docs-lock/
.trae/
+6 -1
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 { export interface ManualTransferPreviewSummary {
+149 -40
View File
@@ -438,15 +438,37 @@ const previewToggleIcon = computed(() => {
return previewVisible.value ? 'mdi-eye-off-outline' : 'mdi-eye-outline' 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>(() => { const episodeFormatRecommendSourceItem = computed<FileItem | undefined>(() => {
if (transferForm.fileitem?.path) return transferForm.fileitem
if (normalizedItems.value.length !== 1) return undefined if (normalizedItems.value.length !== 1) return undefined
return normalizedItems.value[0] return normalizedItems.value[0]
}) })
const canRecommendEpisodeFormat = computed(() => { const canRecommendEpisodeFormat = computed(() => {
return ( return (
Boolean(episodeFormatRecommendSourceItem.value?.path) && (Boolean(episodeFormatRecommendSourceItem.value?.path) ||
episodeFormatRecommendHasValidSelectedFiles.value) &&
!progressDialog.value && !progressDialog.value &&
!episodeFormatRecommendState.loading !episodeFormatRecommendState.loading
) )
@@ -454,7 +476,15 @@ const canRecommendEpisodeFormat = computed(() => {
const episodeFormatRecommendTooltip = computed(() => { const episodeFormatRecommendTooltip = computed(() => {
if (episodeFormatRecommendState.loading) return t('dialog.reorganize.episodeFormatRecommendLoading') 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') if (episodeFormatRuleConfigured.value === false) return t('dialog.reorganize.episodeFormatRecommendNeedWords')
return t('dialog.reorganize.episodeFormatRecommendAction') return t('dialog.reorganize.episodeFormatRecommendAction')
}) })
@@ -472,14 +502,35 @@ watch(
{ immediate: true }, { 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 = { const payload: ManualTransferPayload = {
...transferForm, ...transferForm,
fileitem: options.item ?? ({} as FileItem), fileitem: sourceItem,
logid: options.logid ?? 0, 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 if (options.preview) payload.preview = true
return payload return payload
} }
@@ -504,8 +555,14 @@ async function loadEpisodeFormatRuleConfiguration() {
async function handleRecommendEpisodeFormat() { async function handleRecommendEpisodeFormat() {
const sourceItem = episodeFormatRecommendSourceItem.value const sourceItem = episodeFormatRecommendSourceItem.value
if (!sourceItem?.path) { const selectedFileItems = episodeFormatRecommendSelectedFileItems.value
$toast.warning(t('dialog.reorganize.episodeFormatRecommendSelectFile')) const hasValidSelectedFiles = episodeFormatRecommendHasValidSelectedFiles.value
if (!sourceItem?.path && !hasValidSelectedFiles) {
$toast.warning(
normalizedItems.value.length > 1
? t('dialog.reorganize.episodeFormatRecommendInvalidSelection')
: t('dialog.reorganize.episodeFormatRecommendSelectFile'),
)
return return
} }
@@ -518,9 +575,16 @@ async function handleRecommendEpisodeFormat() {
try { try {
const hasExistingEpisodeFormat = Boolean(transferForm.episode_format?.trim()) const hasExistingEpisodeFormat = Boolean(transferForm.episode_format?.trim())
const result = await api.post('transfer/episode-format/recommend', { const result = await api.post(
fileitem: sourceItem, 'transfer/episode-format/recommend',
}) hasValidSelectedFiles
? {
fileitems: selectedFileItems,
}
: {
fileitem: sourceItem,
},
)
if (!result.success) { if (!result.success) {
$toast.error(result.message || t('dialog.reorganize.episodeFormatRecommendFailed')) $toast.error(result.message || t('dialog.reorganize.episodeFormatRecommendFailed'))
@@ -645,40 +709,68 @@ async function previewTransfer() {
const tasks: Promise<void>[] = [] const tasks: Promise<void>[] = []
if (normalizedItems.value.length) { if (normalizedItems.value.length) {
tasks.push( if (shouldUseBatchFileItems(normalizedItems.value)) {
...normalizedItems.value.map(async item => { try {
try { const result = await requestManualTransfer<ManualTransferPreviewData>(
const result = await requestManualTransfer<ManualTransferPreviewData>( createTransferPayload({ items: normalizedItems.value, preview: true }),
createTransferPayload({ item, 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( mergePreviewData(
mergedPreviewData, mergedPreviewData,
createFailedPreviewData({ createFailedPreviewData({
source: item.path || item.name, source: item.path || item.name,
type: item.type, type: item.type,
title: item.name, 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) { 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) { async function handleTransferLog(logid: number, background: boolean = false) {
try { try {
@@ -850,15 +952,22 @@ async function transfer(background: boolean = false) {
// 文件整理 // 文件整理
if (normalizedItems.value.length) { if (normalizedItems.value.length) {
for (const item of normalizedItems.value) { if (shouldUseBatchFileItems(normalizedItems.value)) {
if (!background) { if (!background) {
// 如果是文件,计算MD5 startLoadingProgress('filetransfer')
const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString() }
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) startLoadingProgress(key)
}
await handleTransfer(item, background)
} }
await handleTransfer(item, background)
} }
} }
+2 -1
View File
@@ -2554,7 +2554,8 @@ export default {
episodeFormatPlaceholder: 'Use {ep} to position episode', episodeFormatPlaceholder: 'Use {ep} to position episode',
episodeFormatRecommendAction: 'Generate', episodeFormatRecommendAction: 'Generate',
episodeFormatRecommendLoading: 'Generating...', 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: episodeFormatRecommendNeedWords:
'Manual episode positioning rules are empty, please fill them in on the words page first', 'Manual episode positioning rules are empty, please fill them in on the words page first',
episodeFormatRecommendSuccess: 'Episode format template generated', episodeFormatRecommendSuccess: 'Episode format template generated',
+2 -1
View File
@@ -2508,7 +2508,8 @@ export default {
episodeFormatPlaceholder: '使用{ep}定位集数', episodeFormatPlaceholder: '使用{ep}定位集数',
episodeFormatRecommendAction: '智能生成', episodeFormatRecommendAction: '智能生成',
episodeFormatRecommendLoading: '生成中...', episodeFormatRecommendLoading: '生成中...',
episodeFormatRecommendSelectFile: '请先选择单个文件或目录', episodeFormatRecommendSelectFile: '请先选择单个文件、单个目录,或多个文件',
episodeFormatRecommendInvalidSelection: '当前选择不满足智能生成条件',
episodeFormatRecommendNeedWords: '手动整理集数定位规则为空,请先前往词表填写', episodeFormatRecommendNeedWords: '手动整理集数定位规则为空,请先前往词表填写',
episodeFormatRecommendSuccess: '已生成集数定位模板', episodeFormatRecommendSuccess: '已生成集数定位模板',
episodeFormatRecommendOverwriteSuccess: '已用智能生成结果覆盖当前集数定位', episodeFormatRecommendOverwriteSuccess: '已用智能生成结果覆盖当前集数定位',
+2 -1
View File
@@ -2509,7 +2509,8 @@ export default {
episodeFormatPlaceholder: '使用{ep}定位集數', episodeFormatPlaceholder: '使用{ep}定位集數',
episodeFormatRecommendAction: '智能生成', episodeFormatRecommendAction: '智能生成',
episodeFormatRecommendLoading: '生成中...', episodeFormatRecommendLoading: '生成中...',
episodeFormatRecommendSelectFile: '請先選擇單個文件或目錄', episodeFormatRecommendSelectFile: '請先選擇單個文件、單個目錄,或多個文件',
episodeFormatRecommendInvalidSelection: '當前選擇不滿足智能生成條件',
episodeFormatRecommendNeedWords: '手動整理集數定位規則為空,請先前往詞表填寫', episodeFormatRecommendNeedWords: '手動整理集數定位規則為空,請先前往詞表填寫',
episodeFormatRecommendSuccess: '已生成集數定位模板', episodeFormatRecommendSuccess: '已生成集數定位模板',
episodeFormatRecommendOverwriteSuccess: '已用智能生成結果覆蓋當前集數定位', episodeFormatRecommendOverwriteSuccess: '已用智能生成結果覆蓋當前集數定位',