implement path mapping UI

This commit is contained in:
stkevintan
2025-12-08 14:01:42 +08:00
parent 3d622d2efe
commit 8add4e6b46
4 changed files with 110 additions and 54 deletions
+107 -48
View File
@@ -55,19 +55,50 @@ const downloaderInfoDialog = ref(false)
// 表单 // 表单
const downloaderForm = ref() const downloaderForm = ref()
// 路径校验规则 // 路径前缀选项
const remotePathRules = [ const prefixOptions = computed(() => {
(v: string) => !!v || t('downloader.pathMappingRequired'), return storageAttributes.map(item => ({
(v: string) => { title: t(`storage.${item.type}`),
const prefixes = storageAttributes.map(s => (s.type === 'local' ? '/' : `${s.type}:/`)) value: `${item.type}:`,
const pattern = new RegExp(`^(${prefixes.join('|')})`) }))
return pattern.test(v) || t('downloader.pathMappingFormatError') })
},
]
const localPathRules = [ // 获取远程路径前缀
function getRemotePrefix(path: string) {
if (!path) return 'local:'
const match = prefixOptions.value
.find(p => path.startsWith(p.value))
return match ? match.value : 'local:'
}
// 获取远程路径后缀
function getRemotePath(path: string) {
if (!path) return ''
const prefix = getRemotePrefix(path)
return path.replace(new RegExp(`^${prefix}`), '')
}
// 更新远程路径前缀
function updateRemotePrefix(index: number, prefix: string) {
if (!downloaderInfo.value.path_mapping) return
const currentPath = downloaderInfo.value.path_mapping[index][0] || ''
const currentSuffix = getRemotePath(currentPath)
prefix = prefix === 'local:' ? '' : prefix // 本地路径前缀为空
downloaderInfo.value.path_mapping[index][0] = prefix + currentSuffix
}
// 更新远程路径后缀
function updateRemotePath(index: number, suffix: string) {
if (!downloaderInfo.value.path_mapping) return
const currentPath = downloaderInfo.value.path_mapping[index][0] || ''
let currentPrefix = getRemotePrefix(currentPath)
currentPrefix = currentPrefix === 'local:' ? '' : currentPrefix // 本地路径前缀为空
downloaderInfo.value.path_mapping[index][0] = currentPrefix + suffix
}
const downloadPathRules = [
(v: string) => !!v || t('downloader.pathMappingRequired'), (v: string) => !!v || t('downloader.pathMappingRequired'),
(v: string) => v.startsWith('/') || t('downloader.pathMappingLocalError'), (v: string) => v.startsWith('/') || t('downloader.pathMappingError'),
] ]
// 下载器详情 // 下载器详情
@@ -189,19 +220,32 @@ onUnmounted(() => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.path-icon { .pm-row {
line-height: 40px; padding-inline-start: 12px;
height: 40px; padding-inline-end: 12px;
} :deep(.v-input__append) {
.path-input { margin-inline-start: 8px;
flex: 0 0 100%; margin-inline-end: 8px;
}
@media (min-width: 960px) {
.path-input {
flex: 1 1 auto;
max-width: 50%;
} }
} }
.rp-select {
flex: 0 0 95px;
margin-right: -1px;
:deep(.v-field) {
padding-inline-end: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
--v-field-padding-end: 0;
}
}
.rp-input {
flex: 1 1 auto;
:deep(.v-field) {
padding-inline-start: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
</style> </style>
<template> <template>
<div> <div>
@@ -427,35 +471,50 @@ onUnmounted(() => {
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VLabel class="mb-2">{{ t('downloader.pathMapping') }}</VLabel> <VLabel class="mb-2">{{ t('downloader.pathMapping') }}</VLabel>
<div <VRow
v-for="(mapping, index) in downloaderInfo.path_mapping" v-for="(mapping, index) in downloaderInfo.path_mapping"
:key="index" :key="index"
class="d-flex gap-2 mb-2 align-start flex-wrap" class="align-start flex-wrap pm-row"
> >
<VTextField <VCol cols="12" md="6" class="pl-0 pr-0">
v-model="mapping[0]" <div class="d-flex flex-nowrap align-start">
placeholder="/remote/path" <VSelect
density="compact" class="rp-select"
hide-details="auto" :model-value="getRemotePrefix(mapping[0])"
:rules="remotePathRules" :items="prefixOptions"
:appendIcon="'mdi-arrow-right'" density="compact"
class="path-input" hide-details
/> @update:model-value="v => updateRemotePrefix(index, v)"
<VTextField />
v-model="mapping[1]" <VTextField
placeholder="/download/path" class="rp-input"
density="compact" :model-value="getRemotePath(mapping[0])"
hide-details="auto" placeholder="/remote/path"
:rules="localPathRules" density="compact"
:appendIcon="'mdi-close'" hide-details="auto"
@click:append="removePathMapping(index)" :rules="downloadPathRules"
/> @update:model-value="v => updateRemotePath(index, v)"
</div> append-icon="mdi-arrow-right"
<div> />
<VBtn variant="tonal" size="small" prepend-icon="mdi-plus" @click="addPathMapping"> </div>
{{ t('common.add') }} </VCol>
</VBtn> <VCol cols="12" md="6" class="pl-0 pr-0">
</div> <VTextField
v-model="mapping[1]"
placeholder="/download/path"
density="compact"
hide-details="auto"
:rules="downloadPathRules"
append-icon="mdi-close"
@click:append="removePathMapping(index)"
/>
</VCol>
</VRow>
</VCol>
<VCol>
<VBtn variant="tonal" size="small" prepend-icon="mdi-plus" @click="addPathMapping">
{{ t('common.add') }}
</VBtn>
</VCol> </VCol>
</VRow> </VRow>
</VForm> </VForm>
+1 -2
View File
@@ -2679,8 +2679,7 @@ export default {
pathMapping: 'Path Mapping', pathMapping: 'Path Mapping',
pathMappingHint: 'Map download paths between downloader and client', pathMappingHint: 'Map download paths between downloader and client',
pathMappingRequired: 'Path cannot be empty', pathMappingRequired: 'Path cannot be empty',
pathMappingFormatError: 'Invalid storage type prefix', pathMappingError: 'Must start with /',
pathMappingLocalError: 'Must start with /',
}, },
filterRule: { filterRule: {
title: 'Filter Rule', title: 'Filter Rule',
+1 -2
View File
@@ -2647,8 +2647,7 @@ export default {
pathMapping: '路径映射', pathMapping: '路径映射',
pathMappingHint: '将下载器中的路径映射为媒体服务器可访问的路径,格式:源路径=>目标路径,多个映射使用换行分隔', pathMappingHint: '将下载器中的路径映射为媒体服务器可访问的路径,格式:源路径=>目标路径,多个映射使用换行分隔',
pathMappingRequired: '路径不能为空', pathMappingRequired: '路径不能为空',
pathMappingFormatError: '存储类型前缀错误', pathMappingError: '必须以 / 开头',
pathMappingLocalError: '必须以 / 开头',
}, },
filterRule: { filterRule: {
title: '过滤规则', title: '过滤规则',
+1 -2
View File
@@ -2633,8 +2633,7 @@ export default {
pathMapping: '路徑映射', pathMapping: '路徑映射',
pathMappingHint: '下載器服務端路徑映射為客戶端路徑,用於整理媒體文件', pathMappingHint: '下載器服務端路徑映射為客戶端路徑,用於整理媒體文件',
pathMappingRequired: '路徑不能為空', pathMappingRequired: '路徑不能為空',
pathMappingFormatError: '存儲類型前綴錯誤', pathMappingError: '必須以 / 開頭',
pathMappingLocalError: '必須以 / 開頭',
}, },
filterRule: { filterRule: {
title: '過濾規則', title: '過濾規則',