修复策划工作区文件树显示
改用可展开的层级目录树显示文件 保留相对路径读取并修复目录内产物不可见问题
This commit is contained in:
+184
-42
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user