fix manual transfer auto target options

This commit is contained in:
jxxghp
2026-06-13 10:42:34 +08:00
parent d7562ea506
commit 4413fedec5
5 changed files with 113 additions and 46 deletions

View File

@@ -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
}
// 手动整理预览统计

View File

@@ -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<TransferForm>({
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<TargetDirectoryOption[]>(() => {
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(() => {
<VSelect
v-model="transferForm.transfer_type"
:label="t('dialog.reorganize.transferType')"
:items="transferTypeOptions"
:items="manualTransferTypeOptions"
:hint="t('dialog.reorganize.transferTypeHint')"
persistent-hint
prepend-inner-icon="mdi-swap-horizontal"
>
<template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }}
</template>
</VSelect>
/>
</VCol>
<VCol cols="12">
<VCombobox
v-model="transferForm.target_path"
:items="targetDirectories"
v-model="targetPathSelection"
:items="targetDirectoryOptions"
item-title="title"
item-value="value"
:return-object="false"
:label="t('dialog.reorganize.targetPath')"
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetPathHint')"
@@ -1528,7 +1595,7 @@ onUnmounted(() => {
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VCol cols="12" md="6">
<VSwitch
v-model="transferForm.library_type_folder"
:label="t('dialog.reorganize.typeFolderOption')"
@@ -1536,7 +1603,7 @@ onUnmounted(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VCol cols="12" md="6">
<VSwitch
v-model="transferForm.library_category_folder"
:label="t('dialog.reorganize.categoryFolderOption')"

View File

@@ -2664,12 +2664,12 @@ export default {
multipleItemsTitle: '{count} Items',
singleItemTitle: '{path}',
targetStorage: 'Target Storage',
targetStorageHint: 'Organization target storage',
targetStorageHint: 'Organization target storage. Choose Auto to let the backend match it.',
transferType: 'Organization Method',
transferTypeHint: 'File operation organization method',
transferTypeHint: 'File operation method. Choose Auto to use the backend match or default rule.',
targetPath: 'Target Path',
targetPathHint: 'Organization target path, leave empty for auto-match',
targetPathPlaceholder: 'Leave empty for auto',
targetPathHint: 'Organization target path. Choose Auto to match by source path.',
targetPathPlaceholder: 'Choose Auto or enter a path',
mediaType: 'Type',
mediaTypeHint: 'File media type',
tmdbId: 'TheMovieDb ID',

View File

@@ -2616,12 +2616,12 @@ export default {
multipleItemsTitle: '共 {count} 项',
singleItemTitle: '{path}',
targetStorage: '目的存储',
targetStorageHint: '整理目的存储',
targetStorageHint: '整理目的存储,选择自动时由后端匹配',
transferType: '整理方式',
transferTypeHint: '文件操作整理方式',
transferTypeHint: '文件操作整理方式,选择自动时使用后端匹配结果或默认规则',
targetPath: '目的路径',
targetPathHint: '整理目的路径,留空将自动匹配',
targetPathPlaceholder: '留空自动',
targetPathHint: '整理目的路径,选择自动将由后端按源路径匹配',
targetPathPlaceholder: '选择自动或输入路径',
mediaType: '类型',
mediaTypeHint: '文件的媒体类型',
tmdbId: 'TheMovieDb编号',

View File

@@ -2617,12 +2617,12 @@ export default {
multipleItemsTitle: '共 {count} 項',
singleItemTitle: '{path}',
targetStorage: '目的存儲',
targetStorageHint: '整理目的存儲',
targetStorageHint: '整理目的存儲,選擇自動時由後端匹配',
transferType: '整理方式',
transferTypeHint: '文件操作整理方式',
transferTypeHint: '文件操作整理方式,選擇自動時使用後端匹配結果或預設規則',
targetPath: '目的路徑',
targetPathHint: '整理目的路徑,留空將自動匹配',
targetPathPlaceholder: '留空自動',
targetPathHint: '整理目的路徑,選擇自動將由後端按源路徑匹配',
targetPathPlaceholder: '選擇自動或輸入路徑',
mediaType: '類型',
mediaTypeHint: '文件的媒體類型',
tmdbId: 'TheMovieDb編號',