diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx index 55c470eb9..9f4c6b770 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx @@ -1,4 +1,12 @@ -import { ArrowLeft, Check, File, Folder, RefreshCw } from 'lucide-react'; +import { + ArrowLeft, + Check, + ChevronDown, + ChevronRight, + File, + Folder, + RefreshCw, +} from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { resolveTauriInvoke } from '../../app/tauri'; @@ -21,8 +29,143 @@ function phaseLabel(phase: string) { return PHASES.find(([id]) => id === phase)?.[1] ?? phase; } -function pathDepth(path: string) { - return path.split(/[\\/]/u).filter(Boolean).length - 1; +type DesignTreeNode = { + name: string; + path: string; + kind: 'directory' | 'file'; + children: DesignTreeNode[]; +}; + +function buildDesignTree(entries: DesignWorkspaceEntry[]) { + const root: DesignTreeNode = { + name: '工作区', + path: '', + kind: 'directory', + children: [], + }; + const nodes = new Map([['', root]]); + + for (const entry of entries) { + const parts = entry.path.split(/[\\/]/u).filter(Boolean); + let parent = root; + let currentPath = ''; + parts.forEach((part, index) => { + currentPath = currentPath ? `${currentPath}/${part}` : part; + let node = nodes.get(currentPath); + if (!node) { + node = { + name: part, + path: currentPath, + kind: + index === parts.length - 1 && entry.kind !== 'directory' + ? 'file' + : 'directory', + children: [], + }; + nodes.set(currentPath, node); + parent.children.push(node); + } + if (index === parts.length - 1) { + node.kind = entry.kind === 'directory' ? 'directory' : 'file'; + } + parent = node; + }); + } + + function sort(nodesToSort: DesignTreeNode[]) { + nodesToSort.sort((left, right) => { + if (left.kind !== right.kind) { + return left.kind === 'directory' ? -1 : 1; + } + return left.name.localeCompare(right.name, 'zh-CN'); + }); + nodesToSort.forEach((node) => sort(node.children)); + } + sort(root.children); + return root; +} + +function DesignTreeBranch({ + node, + depth, + expandedPaths, + selectedPath, + onToggle, + onOpenFile, +}: { + node: DesignTreeNode; + depth: number; + expandedPaths: Set; + selectedPath: string | null; + onToggle: (path: string) => void; + onOpenFile: (path: string) => void; +}) { + const isDirectory = node.kind === 'directory'; + const expanded = isDirectory && expandedPaths.has(node.path); + const paddingLeft = `${12 + depth * 14}px`; + const label = ( + <> + {isDirectory ? ( + expanded ? ( +