diff --git a/src/api/types.ts b/src/api/types.ts index f6e4987e..b58251d5 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1352,7 +1352,7 @@ export interface TransferForm { // 历史ID logid: number // 目标存储 - target_storage: string + target_storage: string | null // 目标路径 target_path: string | null // TMDB ID @@ -1364,7 +1364,7 @@ export interface TransferForm { // 类型 type_name?: string // 整理方式 - transfer_type: string + transfer_type: string | null // 自定义格式 episode_format?: string // 指定集数 @@ -1376,13 +1376,13 @@ export interface TransferForm { // 最小文件大小 min_filesize: number // 刮削 - scrape: boolean + scrape: boolean | null // 复用历史识别信息 from_history: boolean // 媒体库类型子目录 - library_type_folder?: boolean + library_type_folder?: boolean | null // 媒体库类别子目录 - library_category_folder?: boolean + library_category_folder?: boolean | null // 剧集组编号 episode_group?: string | null // 预览模式 @@ -1406,11 +1406,11 @@ export interface ManualTransferTargetPathData { // 整理方式 transfer_type?: string | null // 刮削 - scrape?: boolean + scrape?: boolean | null // 媒体库类型子目录 - library_type_folder?: boolean + library_type_folder?: boolean | null // 媒体库类别子目录 - library_category_folder?: boolean + library_category_folder?: boolean | null } // 手动整理预览统计 diff --git a/src/components/dialog/ReorganizeDialog.vue b/src/components/dialog/ReorganizeDialog.vue index 669bb7ef..4186de80 100644 --- a/src/components/dialog/ReorganizeDialog.vue +++ b/src/components/dialog/ReorganizeDialog.vue @@ -126,6 +126,13 @@ interface ManualTransferTargetPathRequest { target_storage?: string | null } +interface TargetDirectoryOption { + title: string + value: string +} + +const AUTO_TARGET_PATH_VALUE = '__moviepilot_auto_target_path__' + // 生成文件项稳定键,用于去重和状态同步。 function getFileItemKey(item?: FileItem) { return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|') @@ -185,10 +192,27 @@ async function loadStorages() { // 存储字典 const storageOptions = computed(() => { - return storages.value.map(item => ({ - title: item.name, - value: item.type, - })) + return [ + { + title: t('dialog.reorganize.auto'), + value: null, + }, + ...storages.value.map(item => ({ + title: item.name, + value: item.type, + })), + ] +}) + +// 整理方式选项,包含可提交 null 的自动项。 +const manualTransferTypeOptions = computed(() => { + return [ + { + title: t('dialog.reorganize.auto'), + value: null, + }, + ...transferTypeOptions, + ] }) // 剧集组选项属性 @@ -279,7 +303,7 @@ const transferForm = reactive({ logid: 0, target_storage: props.target_storage ?? 'local', target_path: normalizeTargetPath(props.target_path), - transfer_type: '', + transfer_type: null, min_filesize: 0, scrape: false, from_history: false, @@ -299,10 +323,35 @@ async function loadDirectories() { } } -// 目的目录下拉框 -const targetDirectories = computed(() => { - const libraryDirectories = directories.value.map(item => item.library_path) - return [...new Set(libraryDirectories)] +// 目的目录下拉框,第一项用于把目标路径显式重置为后端自动匹配。 +const targetDirectoryOptions = computed(() => { + const libraryDirectories = directories.value.map(item => item.library_path).filter(Boolean) as string[] + return [ + { + title: t('dialog.reorganize.auto'), + value: AUTO_TARGET_PATH_VALUE, + }, + ...[...new Set(libraryDirectories)].map(path => ({ + title: path, + value: path, + })), + ] +}) + +// 目标路径选择值,用哨兵值把界面上的“自动”和接口里的 null 解耦。 +const targetPathSelection = computed({ + get() { + return transferForm.target_path ?? AUTO_TARGET_PATH_VALUE + }, + set(value: string | null) { + const targetPath = normalizeTargetPath(value) + if (!targetPath || targetPath === AUTO_TARGET_PATH_VALUE) { + resetAutomaticTargetConfig() + return + } + + transferForm.target_path = targetPath + }, }) // 构造目的路径自动匹配请求,只传用户真实上下文,避免用默认存储误导后端匹配。 @@ -338,7 +387,7 @@ function createTargetPathMatchRequest(): ManualTransferTargetPathRequest | undef function applyMatchedTargetPath(data?: ManualTransferTargetPathData) { const matchedTargetPath = normalizeTargetPath(data?.target_path) if (!matchedTargetPath) { - transferForm.target_path = null + resetAutomaticTargetConfig() return } @@ -350,13 +399,23 @@ function applyMatchedTargetPath(data?: ManualTransferTargetPathData) { transferForm.target_path = matchedTargetPath } +// 重置为完全自动匹配状态,提交时不携带目标路径及其派生配置。 +function resetAutomaticTargetConfig() { + transferForm.target_storage = null + transferForm.target_path = null + transferForm.transfer_type = null + transferForm.scrape = null + transferForm.library_type_folder = null + transferForm.library_category_folder = null +} + // 请求后端按源目录匹配最合适的手动整理目的路径。 async function autoSelectTargetPath() { if (normalizeTargetPath(props.target_path) || transferForm.target_path) return const payload = createTargetPathMatchRequest() if (!payload) { - transferForm.target_path = null + resetAutomaticTargetConfig() return } @@ -367,14 +426,14 @@ async function autoSelectTargetPath() { ) if (!result.success) { - transferForm.target_path = null + resetAutomaticTargetConfig() return } applyMatchedTargetPath(result.data) } catch (error) { console.log(error) - transferForm.target_path = null + resetAutomaticTargetConfig() } } @@ -391,6 +450,7 @@ watch( transferForm.library_category_folder = directory.library_category_folder ?? false transferForm.library_type_folder = directory.library_type_folder ?? false } else { + transferForm.target_storage = transferForm.target_storage || 'local' transferForm.transfer_type = transferForm.transfer_type || 'copy' transferForm.scrape = false transferForm.library_category_folder = false @@ -398,9 +458,9 @@ watch( } } else { // 路径为空时, 恢复到`自动`条件 - transferForm.transfer_type = '' - transferForm.library_type_folder = undefined - transferForm.library_category_folder = undefined + transferForm.transfer_type = null + transferForm.library_type_folder = null + transferForm.library_category_folder = null } }, ) @@ -496,6 +556,12 @@ function normalizeTargetPath(path?: string | null) { return normalizedPath || null } +// 归一化可选文本参数,保证自动项提交 null 而不是空字符串。 +function normalizeOptionalText(value?: string | null) { + const normalizedValue = value?.trim() + return normalizedValue || null +} + // 归一化剧集组值,兼容历史对象态值。 function normalizeEpisodeGroup(episodeGroup?: string | { value?: string | null } | null) { if (!episodeGroup) return null @@ -822,7 +888,9 @@ function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; l ...transferForm, fileitem: sourceItem, logid: options.logid ?? 0, + target_storage: normalizeOptionalText(transferForm.target_storage), target_path: normalizeTargetPath(transferForm.target_path), + transfer_type: normalizeOptionalText(transferForm.transfer_type), episode_group: normalizeEpisodeGroup(transferForm.episode_group), } @@ -1356,20 +1424,19 @@ onUnmounted(() => { - - + /> { - + { persistent-hint /> - +