mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
feat: 支持多文件整理预览与模板智能生成 (#476)
This commit is contained in:
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -2508,7 +2508,8 @@ export default {
|
|||||||
episodeFormatPlaceholder: '使用{ep}定位集数',
|
episodeFormatPlaceholder: '使用{ep}定位集数',
|
||||||
episodeFormatRecommendAction: '智能生成',
|
episodeFormatRecommendAction: '智能生成',
|
||||||
episodeFormatRecommendLoading: '生成中...',
|
episodeFormatRecommendLoading: '生成中...',
|
||||||
episodeFormatRecommendSelectFile: '请先选择单个文件或目录',
|
episodeFormatRecommendSelectFile: '请先选择单个文件、单个目录,或多个文件',
|
||||||
|
episodeFormatRecommendInvalidSelection: '当前选择不满足智能生成条件',
|
||||||
episodeFormatRecommendNeedWords: '手动整理集数定位规则为空,请先前往词表填写',
|
episodeFormatRecommendNeedWords: '手动整理集数定位规则为空,请先前往词表填写',
|
||||||
episodeFormatRecommendSuccess: '已生成集数定位模板',
|
episodeFormatRecommendSuccess: '已生成集数定位模板',
|
||||||
episodeFormatRecommendOverwriteSuccess: '已用智能生成结果覆盖当前集数定位',
|
episodeFormatRecommendOverwriteSuccess: '已用智能生成结果覆盖当前集数定位',
|
||||||
|
|||||||
@@ -2509,7 +2509,8 @@ export default {
|
|||||||
episodeFormatPlaceholder: '使用{ep}定位集數',
|
episodeFormatPlaceholder: '使用{ep}定位集數',
|
||||||
episodeFormatRecommendAction: '智能生成',
|
episodeFormatRecommendAction: '智能生成',
|
||||||
episodeFormatRecommendLoading: '生成中...',
|
episodeFormatRecommendLoading: '生成中...',
|
||||||
episodeFormatRecommendSelectFile: '請先選擇單個文件或目錄',
|
episodeFormatRecommendSelectFile: '請先選擇單個文件、單個目錄,或多個文件',
|
||||||
|
episodeFormatRecommendInvalidSelection: '當前選擇不滿足智能生成條件',
|
||||||
episodeFormatRecommendNeedWords: '手動整理集數定位規則為空,請先前往詞表填寫',
|
episodeFormatRecommendNeedWords: '手動整理集數定位規則為空,請先前往詞表填寫',
|
||||||
episodeFormatRecommendSuccess: '已生成集數定位模板',
|
episodeFormatRecommendSuccess: '已生成集數定位模板',
|
||||||
episodeFormatRecommendOverwriteSuccess: '已用智能生成結果覆蓋當前集數定位',
|
episodeFormatRecommendOverwriteSuccess: '已用智能生成結果覆蓋當前集數定位',
|
||||||
|
|||||||
Reference in New Issue
Block a user