mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
fix manual transfer auto target options
This commit is contained in:
+8
-8
@@ -1352,7 +1352,7 @@ export interface TransferForm {
|
|||||||
// 历史ID
|
// 历史ID
|
||||||
logid: number
|
logid: number
|
||||||
// 目标存储
|
// 目标存储
|
||||||
target_storage: string
|
target_storage: string | null
|
||||||
// 目标路径
|
// 目标路径
|
||||||
target_path: string | null
|
target_path: string | null
|
||||||
// TMDB ID
|
// TMDB ID
|
||||||
@@ -1364,7 +1364,7 @@ export interface TransferForm {
|
|||||||
// 类型
|
// 类型
|
||||||
type_name?: string
|
type_name?: string
|
||||||
// 整理方式
|
// 整理方式
|
||||||
transfer_type: string
|
transfer_type: string | null
|
||||||
// 自定义格式
|
// 自定义格式
|
||||||
episode_format?: string
|
episode_format?: string
|
||||||
// 指定集数
|
// 指定集数
|
||||||
@@ -1376,13 +1376,13 @@ export interface TransferForm {
|
|||||||
// 最小文件大小
|
// 最小文件大小
|
||||||
min_filesize: number
|
min_filesize: number
|
||||||
// 刮削
|
// 刮削
|
||||||
scrape: boolean
|
scrape: boolean | null
|
||||||
// 复用历史识别信息
|
// 复用历史识别信息
|
||||||
from_history: boolean
|
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
|
episode_group?: string | null
|
||||||
// 预览模式
|
// 预览模式
|
||||||
@@ -1406,11 +1406,11 @@ export interface ManualTransferTargetPathData {
|
|||||||
// 整理方式
|
// 整理方式
|
||||||
transfer_type?: string | null
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动整理预览统计
|
// 手动整理预览统计
|
||||||
|
|||||||
@@ -126,6 +126,13 @@ interface ManualTransferTargetPathRequest {
|
|||||||
target_storage?: string | null
|
target_storage?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TargetDirectoryOption {
|
||||||
|
title: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTO_TARGET_PATH_VALUE = '__moviepilot_auto_target_path__'
|
||||||
|
|
||||||
// 生成文件项稳定键,用于去重和状态同步。
|
// 生成文件项稳定键,用于去重和状态同步。
|
||||||
function getFileItemKey(item?: FileItem) {
|
function getFileItemKey(item?: FileItem) {
|
||||||
return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
|
return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
|
||||||
@@ -185,10 +192,27 @@ async function loadStorages() {
|
|||||||
|
|
||||||
// 存储字典
|
// 存储字典
|
||||||
const storageOptions = computed(() => {
|
const storageOptions = computed(() => {
|
||||||
return storages.value.map(item => ({
|
return [
|
||||||
title: item.name,
|
{
|
||||||
value: item.type,
|
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,
|
logid: 0,
|
||||||
target_storage: props.target_storage ?? 'local',
|
target_storage: props.target_storage ?? 'local',
|
||||||
target_path: normalizeTargetPath(props.target_path),
|
target_path: normalizeTargetPath(props.target_path),
|
||||||
transfer_type: '',
|
transfer_type: null,
|
||||||
min_filesize: 0,
|
min_filesize: 0,
|
||||||
scrape: false,
|
scrape: false,
|
||||||
from_history: false,
|
from_history: false,
|
||||||
@@ -299,10 +323,35 @@ async function loadDirectories() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目的目录下拉框
|
// 目的目录下拉框,第一项用于把目标路径显式重置为后端自动匹配。
|
||||||
const targetDirectories = computed(() => {
|
const targetDirectoryOptions = computed<TargetDirectoryOption[]>(() => {
|
||||||
const libraryDirectories = directories.value.map(item => item.library_path)
|
const libraryDirectories = directories.value.map(item => item.library_path).filter(Boolean) as string[]
|
||||||
return [...new Set(libraryDirectories)]
|
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) {
|
function applyMatchedTargetPath(data?: ManualTransferTargetPathData) {
|
||||||
const matchedTargetPath = normalizeTargetPath(data?.target_path)
|
const matchedTargetPath = normalizeTargetPath(data?.target_path)
|
||||||
if (!matchedTargetPath) {
|
if (!matchedTargetPath) {
|
||||||
transferForm.target_path = null
|
resetAutomaticTargetConfig()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,13 +399,23 @@ function applyMatchedTargetPath(data?: ManualTransferTargetPathData) {
|
|||||||
transferForm.target_path = matchedTargetPath
|
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() {
|
async function autoSelectTargetPath() {
|
||||||
if (normalizeTargetPath(props.target_path) || transferForm.target_path) return
|
if (normalizeTargetPath(props.target_path) || transferForm.target_path) return
|
||||||
|
|
||||||
const payload = createTargetPathMatchRequest()
|
const payload = createTargetPathMatchRequest()
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
transferForm.target_path = null
|
resetAutomaticTargetConfig()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,14 +426,14 @@ async function autoSelectTargetPath() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
transferForm.target_path = null
|
resetAutomaticTargetConfig()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
applyMatchedTargetPath(result.data)
|
applyMatchedTargetPath(result.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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_category_folder = directory.library_category_folder ?? false
|
||||||
transferForm.library_type_folder = directory.library_type_folder ?? false
|
transferForm.library_type_folder = directory.library_type_folder ?? false
|
||||||
} else {
|
} else {
|
||||||
|
transferForm.target_storage = transferForm.target_storage || 'local'
|
||||||
transferForm.transfer_type = transferForm.transfer_type || 'copy'
|
transferForm.transfer_type = transferForm.transfer_type || 'copy'
|
||||||
transferForm.scrape = false
|
transferForm.scrape = false
|
||||||
transferForm.library_category_folder = false
|
transferForm.library_category_folder = false
|
||||||
@@ -398,9 +458,9 @@ watch(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 路径为空时, 恢复到`自动`条件
|
// 路径为空时, 恢复到`自动`条件
|
||||||
transferForm.transfer_type = ''
|
transferForm.transfer_type = null
|
||||||
transferForm.library_type_folder = undefined
|
transferForm.library_type_folder = null
|
||||||
transferForm.library_category_folder = undefined
|
transferForm.library_category_folder = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -496,6 +556,12 @@ function normalizeTargetPath(path?: string | null) {
|
|||||||
return normalizedPath || 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) {
|
function normalizeEpisodeGroup(episodeGroup?: string | { value?: string | null } | null) {
|
||||||
if (!episodeGroup) return null
|
if (!episodeGroup) return null
|
||||||
@@ -822,7 +888,9 @@ function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; l
|
|||||||
...transferForm,
|
...transferForm,
|
||||||
fileitem: sourceItem,
|
fileitem: sourceItem,
|
||||||
logid: options.logid ?? 0,
|
logid: options.logid ?? 0,
|
||||||
|
target_storage: normalizeOptionalText(transferForm.target_storage),
|
||||||
target_path: normalizeTargetPath(transferForm.target_path),
|
target_path: normalizeTargetPath(transferForm.target_path),
|
||||||
|
transfer_type: normalizeOptionalText(transferForm.transfer_type),
|
||||||
episode_group: normalizeEpisodeGroup(transferForm.episode_group),
|
episode_group: normalizeEpisodeGroup(transferForm.episode_group),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1356,20 +1424,19 @@ onUnmounted(() => {
|
|||||||
<VSelect
|
<VSelect
|
||||||
v-model="transferForm.transfer_type"
|
v-model="transferForm.transfer_type"
|
||||||
:label="t('dialog.reorganize.transferType')"
|
:label="t('dialog.reorganize.transferType')"
|
||||||
:items="transferTypeOptions"
|
:items="manualTransferTypeOptions"
|
||||||
:hint="t('dialog.reorganize.transferTypeHint')"
|
:hint="t('dialog.reorganize.transferTypeHint')"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
prepend-inner-icon="mdi-swap-horizontal"
|
prepend-inner-icon="mdi-swap-horizontal"
|
||||||
>
|
/>
|
||||||
<template v-slot:selection="{ item }">
|
|
||||||
{{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }}
|
|
||||||
</template>
|
|
||||||
</VSelect>
|
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCombobox
|
<VCombobox
|
||||||
v-model="transferForm.target_path"
|
v-model="targetPathSelection"
|
||||||
:items="targetDirectories"
|
:items="targetDirectoryOptions"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
:return-object="false"
|
||||||
:label="t('dialog.reorganize.targetPath')"
|
:label="t('dialog.reorganize.targetPath')"
|
||||||
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
|
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
|
||||||
:hint="t('dialog.reorganize.targetPathHint')"
|
:hint="t('dialog.reorganize.targetPathHint')"
|
||||||
@@ -1528,7 +1595,7 @@ onUnmounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6" v-if="transferForm.target_path">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="transferForm.library_type_folder"
|
v-model="transferForm.library_type_folder"
|
||||||
:label="t('dialog.reorganize.typeFolderOption')"
|
:label="t('dialog.reorganize.typeFolderOption')"
|
||||||
@@ -1536,7 +1603,7 @@ onUnmounted(() => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="6" v-if="transferForm.target_path">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="transferForm.library_category_folder"
|
v-model="transferForm.library_category_folder"
|
||||||
:label="t('dialog.reorganize.categoryFolderOption')"
|
:label="t('dialog.reorganize.categoryFolderOption')"
|
||||||
|
|||||||
@@ -2664,12 +2664,12 @@ export default {
|
|||||||
multipleItemsTitle: '{count} Items',
|
multipleItemsTitle: '{count} Items',
|
||||||
singleItemTitle: '{path}',
|
singleItemTitle: '{path}',
|
||||||
targetStorage: 'Target Storage',
|
targetStorage: 'Target Storage',
|
||||||
targetStorageHint: 'Organization target storage',
|
targetStorageHint: 'Organization target storage. Choose Auto to let the backend match it.',
|
||||||
transferType: 'Organization Method',
|
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',
|
targetPath: 'Target Path',
|
||||||
targetPathHint: 'Organization target path, leave empty for auto-match',
|
targetPathHint: 'Organization target path. Choose Auto to match by source path.',
|
||||||
targetPathPlaceholder: 'Leave empty for auto',
|
targetPathPlaceholder: 'Choose Auto or enter a path',
|
||||||
mediaType: 'Type',
|
mediaType: 'Type',
|
||||||
mediaTypeHint: 'File media type',
|
mediaTypeHint: 'File media type',
|
||||||
tmdbId: 'TheMovieDb ID',
|
tmdbId: 'TheMovieDb ID',
|
||||||
|
|||||||
@@ -2616,12 +2616,12 @@ export default {
|
|||||||
multipleItemsTitle: '共 {count} 项',
|
multipleItemsTitle: '共 {count} 项',
|
||||||
singleItemTitle: '{path}',
|
singleItemTitle: '{path}',
|
||||||
targetStorage: '目的存储',
|
targetStorage: '目的存储',
|
||||||
targetStorageHint: '整理目的存储',
|
targetStorageHint: '整理目的存储,选择自动时由后端匹配',
|
||||||
transferType: '整理方式',
|
transferType: '整理方式',
|
||||||
transferTypeHint: '文件操作整理方式',
|
transferTypeHint: '文件操作整理方式,选择自动时使用后端匹配结果或默认规则',
|
||||||
targetPath: '目的路径',
|
targetPath: '目的路径',
|
||||||
targetPathHint: '整理目的路径,留空将自动匹配',
|
targetPathHint: '整理目的路径,选择自动将由后端按源路径匹配',
|
||||||
targetPathPlaceholder: '留空自动',
|
targetPathPlaceholder: '选择自动或输入路径',
|
||||||
mediaType: '类型',
|
mediaType: '类型',
|
||||||
mediaTypeHint: '文件的媒体类型',
|
mediaTypeHint: '文件的媒体类型',
|
||||||
tmdbId: 'TheMovieDb编号',
|
tmdbId: 'TheMovieDb编号',
|
||||||
|
|||||||
@@ -2617,12 +2617,12 @@ export default {
|
|||||||
multipleItemsTitle: '共 {count} 項',
|
multipleItemsTitle: '共 {count} 項',
|
||||||
singleItemTitle: '{path}',
|
singleItemTitle: '{path}',
|
||||||
targetStorage: '目的存儲',
|
targetStorage: '目的存儲',
|
||||||
targetStorageHint: '整理目的存儲',
|
targetStorageHint: '整理目的存儲,選擇自動時由後端匹配',
|
||||||
transferType: '整理方式',
|
transferType: '整理方式',
|
||||||
transferTypeHint: '文件操作整理方式',
|
transferTypeHint: '文件操作整理方式,選擇自動時使用後端匹配結果或預設規則',
|
||||||
targetPath: '目的路徑',
|
targetPath: '目的路徑',
|
||||||
targetPathHint: '整理目的路徑,留空將自動匹配',
|
targetPathHint: '整理目的路徑,選擇自動將由後端按源路徑匹配',
|
||||||
targetPathPlaceholder: '留空自動',
|
targetPathPlaceholder: '選擇自動或輸入路徑',
|
||||||
mediaType: '類型',
|
mediaType: '類型',
|
||||||
mediaTypeHint: '文件的媒體類型',
|
mediaTypeHint: '文件的媒體類型',
|
||||||
tmdbId: 'TheMovieDb編號',
|
tmdbId: 'TheMovieDb編號',
|
||||||
|
|||||||
Reference in New Issue
Block a user