diff --git a/internal/controllers/file_controller.go b/internal/controllers/file_controller.go index 18cb52b..2db740f 100644 --- a/internal/controllers/file_controller.go +++ b/internal/controllers/file_controller.go @@ -35,6 +35,7 @@ type FileNode struct { Name string `json:"name"` Path string `json:"path"` IsDir bool `json:"isDir"` + ModTime int64 `json:"modTime"` Children []*FileNode `json:"children,omitempty"` } @@ -84,6 +85,12 @@ func (fc *FileController) GetFileTree(c *gin.Context) { relPath, _ := filepath.Rel(fc.workDir, path) parts := strings.Split(relPath, string(filepath.Separator)) + info, err := d.Info() + var modTime int64 + if err == nil { + modTime = info.ModTime().UnixMilli() + } + current := root for i, part := range parts { found := false @@ -98,9 +105,10 @@ func (fc *FileController) GetFileTree(c *gin.Context) { isLast := i == len(parts)-1 isDir := !isLast || d.IsDir() node := &FileNode{ - Name: part, - Path: strings.Join(parts[:i+1], "/"), - IsDir: isDir, + Name: part, + Path: strings.Join(parts[:i+1], "/"), + IsDir: isDir, + ModTime: modTime, } if isDir { node.Children = []*FileNode{} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index c1449d2..547e4f3 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -377,6 +377,7 @@ export interface FileNode { name: string path: string isDir: boolean + modTime: number children?: FileNode[] } diff --git a/web/src/components/DirTreeSelect.vue b/web/src/components/DirTreeSelect.vue index 82e604b..18a4fdb 100644 --- a/web/src/components/DirTreeSelect.vue +++ b/web/src/components/DirTreeSelect.vue @@ -6,8 +6,11 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { api, type FileNode } from '@/api' const props = defineProps<{ - modelValue?: string + modelValue?: string | null placeholder?: string + fileTree?: FileNode[] + defaultExpand?: string + rootLabel?: string }>() const emit = defineEmits<{ @@ -16,9 +19,21 @@ const emit = defineEmits<{ const open = ref(false) const loading = ref(false) -const fileTree = ref([]) +const internalFileTree = ref([]) +const fileTree = computed(() => props.fileTree || internalFileTree.value) const expandedDirs = ref>(new Set()) +watch(() => props.defaultExpand, (newVal) => { + if (newVal) { + const parts = newVal.split('/') + let current = '' + for (const part of parts) { + current = current ? `${current}/${part}` : part + expandedDirs.value.add(current) + } + } +}, { immediate: true }) + interface FlatDir { path: string name: string @@ -48,12 +63,13 @@ function flattenDirs(nodes: FileNode[], depth = 0): FlatDir[] { const flatDirs = computed(() => flattenDirs(fileTree.value)) async function loadTree() { - if (fileTree.value.length > 0) return + if (props.fileTree) return + if (internalFileTree.value.length > 0) return loading.value = true try { - fileTree.value = await api.files.tree() + internalFileTree.value = await api.files.tree() } catch { - fileTree.value = [] + internalFileTree.value = [] } finally { loading.value = false } @@ -88,9 +104,13 @@ function isSelected(dirPath: string): boolean { return false } +function getRootLabel() { + return props.rootLabel || 'scripts (默认)' +} + // 检查是否是默认目录 function isDefaultSelected(): boolean { - if (!props.modelValue) return true + if (!props.modelValue || props.modelValue === '/') return true // 绝对路径以 /scripts 结尾且没有子目录 if (props.modelValue.endsWith('/scripts') || props.modelValue.endsWith('/data/scripts')) return true return false @@ -101,21 +121,17 @@ watch(open, (val) => { }) const displayValue = computed(() => { - if (!props.modelValue) return props.placeholder || 'scripts (默认)' - // 如果是绝对路径,只显示最后部分 - const parts = props.modelValue.split('/') - const lastPart = parts[parts.length - 1] - // 如果是 scripts 目录本身 - if (lastPart === 'scripts') return 'scripts (默认)' - return lastPart || props.modelValue + if (!props.modelValue || props.modelValue === '/') return props.placeholder || getRootLabel() + if (isDefaultSelected() && !props.rootLabel) return 'scripts (默认)' + return props.modelValue })