refactor:重构路径输入组件

This commit is contained in:
jxxghp
2025-01-17 13:28:31 +08:00
parent 1fe8aeb9e1
commit 822711a530
4 changed files with 147 additions and 30 deletions
+12 -27
View File
@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TransferDirectoryConf } from '@/api/types' import type { TransferDirectoryConf } from '@/api/types'
import PathInput from '@/components/input/PathInput.vue'
import api from '@/api' import api from '@/api'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import { storageOptions } from '@/api/constants' import { storageOptions } from '@/api/constants'
@@ -20,12 +19,6 @@ const props = defineProps({
height: String, height: String,
}) })
// 下载路径
const downloadPath = ref<string>('')
// 媒体库路径
const libraryPath = ref<string>('')
// 卡版是否折叠状态 // 卡版是否折叠状态
const isCollapsed = ref(true) const isCollapsed = ref(true)
@@ -210,16 +203,12 @@ watch(
/> />
</VCol> </VCol>
<VCol cols="8"> <VCol cols="8">
<PathInput v-model="props.directory.download_path" :storage="props.directory.storage"> <VPathField
<template #activator="{ menuprops }"> v-model="props.directory.download_path"
<VTextField :storage="props.directory.storage"
:model-value="props.directory.download_path" variant="underlined"
v-bind="menuprops" label="下载目录/源目录"
variant="underlined" />
label="下载目录/源目录"
/>
</template>
</PathInput>
</VCol> </VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''"> <VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
<VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch> <VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch>
@@ -257,16 +246,12 @@ watch(
/> />
</VCol> </VCol>
<VCol cols="8"> <VCol cols="8">
<PathInput v-model="props.directory.library_path" :storage="props.directory.library_storage"> <VPathField
<template #activator="{ menuprops }"> v-model="props.directory.library_path"
<VTextField :storage="props.directory.library_storage"
:modelValue="props.directory.library_path" variant="underlined"
v-bind="menuprops" label="媒体库目录"
variant="underlined" />
label="媒体库目录"
/>
</template>
</PathInput>
</VCol> </VCol>
<VCol cols="4"> <VCol cols="4">
<VSelect <VSelect
+49
View File
@@ -0,0 +1,49 @@
<script setup lang="ts">
import PathInput from '@/components/input/PathInput.vue'
const attrs = useAttrs()
const props = defineProps({
modelValue: {
type: String,
default: '* * * * *',
},
storage: {
type: String,
default: 'local',
},
})
const emit = defineEmits(['update:modelValue'])
const innerValue = ref(props.modelValue)
watch(
() => props.modelValue,
value => {
innerValue.value = value
},
)
const propsWithoutModelValue = computed(() => {
const { modelValue, ...rest } = props
return { ...rest, ...attrs }
})
function updateModelValue(value: string) {
innerValue.value = value
emit('update:modelValue', value)
}
</script>
<template>
<PathInput v-model="innerValue" :storage="props.storage" @update:modelValue="updateModelValue">
<template #activator="{ menuprops }">
<VTextField
:modelValue="innerValue"
@update:modelValue="updateModelValue"
v-bind="{ ...menuprops, ...propsWithoutModelValue }"
/>
</template>
</PathInput>
</template>
+84 -3
View File
@@ -3,6 +3,10 @@ import api from '@/api'
import { FileItem } from '@/api/types' import { FileItem } from '@/api/types'
const props = defineProps({ const props = defineProps({
modelValue: {
type: String,
default: '/',
},
root: { root: {
type: String, type: String,
default: '/', default: '/',
@@ -16,7 +20,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const activedDirs = ref<Record<string, any>[]>([]) const menuVisible = ref(false)
const treeItems = ref<FileItem[]>([ const treeItems = ref<FileItem[]>([
{ {
@@ -29,16 +33,70 @@ const treeItems = ref<FileItem[]>([
}, },
]) ])
const activedDirs = ref<FileItem[]>([])
const openedDirs = ref<FileItem[]>([])
// 调用API查询子目录
async function fetchDirs(item: any) { async function fetchDirs(item: any) {
return api return api
.post('/storage/list', item) .post('/storage/list', item)
.then((data: any) => { .then((data: any) => {
data = data.filter((i: any) => i.type === 'dir') data = data.filter((i: any) => i.type === 'dir')
item.children.push(...data) item.children?.push(...data)
}) })
.catch(err => console.warn(err)) .catch(err => console.warn(err))
} }
// 递归查询路径
function findPath(item: FileItem, path: string): FileItem | null {
if (item.path === path) {
return item
}
if (item.children) {
for (const child of item.children) {
const res: FileItem | null = findPath(child, path)
if (res) {
return res
}
}
}
return null
}
// 根据路径展开所有子目录
async function expandDirs(path: string) {
// 分割路径
const paths = path.split('/').filter(i => i)
// 展开根目录
const root_item = treeItems.value[0]
await fetchDirs(root_item)
openedDirs.value.push(root_item)
// 逐级展开
let currentPath = '/'
for (const p of paths) {
currentPath += `${p}/`
// 查询当前目录
const item = findPath(root_item, currentPath)
if (!item) {
break
}
// 加载子目录
if (item.children?.length === 0) {
await fetchDirs(item)
}
// 打开当前目录
if (!openedDirs.value.includes(item) && path != currentPath) {
openedDirs.value.push(item)
}
// 选中当前目录
if (path == currentPath) {
activedDirs.value = [item]
}
}
}
// 当前选中项
const selectedPath = computed(() => { const selectedPath = computed(() => {
if (activedDirs.value.length > 0) { if (activedDirs.value.length > 0) {
return activedDirs.value[0].path return activedDirs.value[0].path
@@ -51,6 +109,27 @@ watch(activedDirs, newVal => {
emit('update:modelValue', selectedPath.value) emit('update:modelValue', selectedPath.value)
}) })
watch(
() => menuVisible.value,
async visible => {
if (visible) {
treeItems.value = [
{
name: '/',
path: props.root,
children: [],
type: 'dir',
basename: props.root,
storage: props.storage,
},
]
openedDirs.value = []
activedDirs.value = []
await expandDirs(props.modelValue)
}
},
)
watch( watch(
() => props.storage, () => props.storage,
async newVal => { async newVal => {
@@ -65,18 +144,20 @@ watch(
}, },
] ]
activedDirs.value = [] activedDirs.value = []
openedDirs.value = []
}, },
) )
</script> </script>
<template> <template>
<div> <div>
<VMenu :close-on-content-click="false" content-class="cursor-default"> <VMenu v-model="menuVisible" :close-on-content-click="false" content-class="cursor-default">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<slot name="activator" :menuprops="props" /> <slot name="activator" :menuprops="props" />
</template> </template>
<VTreeview <VTreeview
v-model:activated="activedDirs" v-model:activated="activedDirs"
v-model:opened="openedDirs"
:items="treeItems" :items="treeItems"
:load-children="fetchDirs" :load-children="fetchDirs"
item-key="path" item-key="path"
+2
View File
@@ -36,6 +36,7 @@ import MediaInfoCard from './components/cards/MediaInfoCard.vue'
import TorrentCard from './components/cards/TorrentCard.vue' import TorrentCard from './components/cards/TorrentCard.vue'
import MediaIdSelector from './components/misc/MediaIdSelector.vue' import MediaIdSelector from './components/misc/MediaIdSelector.vue'
import CronField from './components/field/CronField.vue' import CronField from './components/field/CronField.vue'
import PathField from './components/field/PathField.vue'
// 7. 样式文件 // 7. 样式文件
import '@core/scss/template/libs/vuetify/index.scss' import '@core/scss/template/libs/vuetify/index.scss'
@@ -82,6 +83,7 @@ initializeApp().then(() => {
.component('VTorrentCard', TorrentCard) .component('VTorrentCard', TorrentCard)
.component('VMediaIdSelector', MediaIdSelector) .component('VMediaIdSelector', MediaIdSelector)
.component('VCronField', CronField) .component('VCronField', CronField)
.component('VPathField', PathField)
// 注册插件 // 注册插件
app app