修复策划工作区文件树显示
Project CI / Repository checks (pull_request) Successful in 2m44s
Project CI / Frontend tests (pull_request) Successful in 3m32s
Project CI / Backend tests (pull_request) Successful in 6m21s
Project CI / Native shell tests (pull_request) Successful in 18m20s

改用可展开的层级目录树显示文件

保留相对路径读取并修复目录内产物不可见问题
This commit is contained in:
2026-09-11 05:16:39 +00:00
parent 87711e4034
commit 26169102a0
2 changed files with 190 additions and 42 deletions
@@ -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<string, DesignTreeNode>([['', 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<string>;
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 ? (
<ChevronDown size={14} aria-hidden="true" />
) : (
<ChevronRight size={14} aria-hidden="true" />
)
) : (
<span
className="design-workspace-tree__branch-spacer"
aria-hidden="true"
/>
)}
{isDirectory ? (
<Folder size={15} aria-hidden="true" />
) : (
<File size={15} aria-hidden="true" />
)}
<span title={node.path}>{node.name}</span>
</>
);
return (
<>
{isDirectory ? (
<button
type="button"
className="design-workspace-tree__entry is-directory"
style={{ paddingLeft }}
aria-expanded={expanded}
onClick={() => onToggle(node.path)}
>
{label}
</button>
) : (
<button
type="button"
className={`design-workspace-tree__entry${
selectedPath === node.path ? ' is-selected' : ''
}`}
style={{ paddingLeft }}
title={node.path}
onClick={() => void onOpenFile(node.path)}
>
{label}
</button>
)}
{isDirectory && expanded
? node.children.map((child) => (
<DesignTreeBranch
key={child.path}
node={child}
depth={depth + 1}
expandedPaths={expandedPaths}
selectedPath={selectedPath}
onToggle={onToggle}
onOpenFile={onOpenFile}
/>
))
: null}
</>
);
}
export function DesignWorkspacePanel({
@@ -35,6 +178,9 @@ export function DesignWorkspacePanel({
const [loading, setLoading] = useState(true);
const [previewLoading, setPreviewLoading] = useState(false);
const [error, setError] = useState('');
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(
() => new Set(),
);
const loadWorkspace = useCallback(async () => {
const invoke = resolveTauriInvoke();
@@ -55,6 +201,21 @@ export function DesignWorkspacePanel({
]);
setView(nextView);
setFiles(nextFiles);
setExpandedPaths((current) => {
const next = new Set(current);
if (current.size === 0) {
nextFiles
.filter((entry) => entry.kind === 'directory')
.forEach((entry) => next.add(entry.path));
} else {
nextFiles
.filter(
(entry) => entry.kind === 'directory' && next.has(entry.path),
)
.forEach((entry) => next.add(entry.path));
}
return next;
});
setSelectedPath((current) =>
current &&
nextFiles.some(
@@ -106,16 +267,7 @@ export function DesignWorkspacePanel({
};
}, [loadWorkspace, projectPath]);
const sortedFiles = useMemo(
() =>
[...files].sort((left, right) => {
if (left.kind !== right.kind) {
return left.kind === 'directory' ? -1 : 1;
}
return left.path.localeCompare(right.path, 'zh-CN');
}),
[files],
);
const sortedFiles = useMemo(() => buildDesignTree(files), [files]);
const openFile = useCallback(
async (path: string) => {
@@ -210,7 +362,7 @@ export function DesignWorkspacePanel({
</header>
{loading ? (
<p className="design-workspace-empty"></p>
) : sortedFiles.length === 0 ? (
) : sortedFiles.children.length === 0 ? (
<div className="design-workspace-empty">
<Folder size={24} aria-hidden="true" />
<strong></strong>
@@ -218,34 +370,24 @@ export function DesignWorkspacePanel({
</div>
) : (
<div className="design-workspace-tree__list">
{sortedFiles.map((entry) => {
const isDirectory = entry.kind === 'directory';
return isDirectory ? (
<div
key={entry.path}
className="design-workspace-tree__entry is-directory"
style={{
paddingLeft: `${12 + pathDepth(entry.path) * 12}px`,
}}
>
<Folder size={15} aria-hidden="true" />
<span>{entry.path}</span>
</div>
) : (
<button
key={entry.path}
type="button"
className={`design-workspace-tree__entry${selectedPath === entry.path ? ' is-selected' : ''}`}
style={{
paddingLeft: `${12 + pathDepth(entry.path) * 12}px`,
}}
onClick={() => void openFile(entry.path)}
>
<File size={15} aria-hidden="true" />
<span>{entry.path}</span>
</button>
);
})}
{sortedFiles.children.map((node) => (
<DesignTreeBranch
key={node.path}
node={node}
depth={0}
expandedPaths={expandedPaths}
selectedPath={selectedPath}
onToggle={(path) =>
setExpandedPaths((current) => {
const next = new Set(current);
if (next.has(path)) next.delete(path);
else next.add(path);
return next;
})
}
onOpenFile={openFile}
/>
))}
</div>
)}
</aside>
@@ -8374,6 +8374,12 @@ iframe.preview-frame {
white-space: nowrap;
}
.design-workspace-tree__branch-spacer {
display: block;
width: 14px;
flex: 0 0 14px;
}
button.design-workspace-tree__entry {
cursor: pointer;
}