import { type MouseEvent as ReactMouseEvent, useEffect } from "react"; import Icon from "../../components/common/Icon"; import type { ScriptItem, WorkspaceDirectory, } from "~/services/api"; export type WorkspaceTreeTarget = { kind: "root" | "directory" | "file"; path: string; script?: ScriptItem; }; type WorkspaceTreeProps = { title: string; scripts: ScriptItem[]; directories: WorkspaceDirectory[]; selectedId: string | null; onSelect: (id: string) => void; onContextMenu?: ( event: ReactMouseEvent, target: WorkspaceTreeTarget, ) => void; readOnly?: boolean; // 唯一标识此 group(通常 `__group__`),让多个 owner 的 group 各自独立展开。 groupKey: string; expandedPaths: Set; onToggle: (path: string, loadPath?: string) => void; loadingChildrenPaths: Set; }; type WorkspaceTreeItemsProps = Omit & { path: string; depth: number; }; function formatTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }).format(new Date(value)); } export function scriptIcon(item: ScriptItem) { return item.script_type === "notebook" ? "notebook" : "python"; } function ownedScriptPath(item: ScriptItem) { return item.relative_path.replaceAll("\\", "/").split("/").slice(2).join("/"); } function parentOf(path: string) { const parts = path.split("/"); parts.pop(); return parts.join("/"); } export function WorkspaceTreeGroup({ title, scripts, directories, selectedId, onSelect, onContextMenu, readOnly = false, groupKey, expandedPaths, onToggle, loadingChildrenPaths, }: WorkspaceTreeProps) { const open = expandedPaths.has(groupKey); // 首次挂载自动展开(保留原本 useState(true) 的默认展开行为)。 // toggleExpanded 内识别 `__group__` 前缀,不会触发 loadChildren。 useEffect(() => { if (!expandedPaths.has(groupKey)) { void onToggle(groupKey); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [groupKey]); return (
{open && (
{scripts.length === 0 && directories.length === 0 && (

{readOnly ? "暂无文件" : "右键此目录即可新建或上传"}

)}
)}
); } function WorkspaceTreeItems({ groupKey, path, depth, scripts, directories, selectedId, onSelect, onContextMenu, expandedPaths, onToggle, loadingChildrenPaths, }: WorkspaceTreeItemsProps) { const childDirectories = directories.filter( (item) => item.parent_path === path, ); const childScripts = scripts.filter( (item) => parentOf(ownedScriptPath(item)) === path, ); return ( <> {childDirectories.map((directory) => ( ))} {childScripts.map((item) => ( ))} ); } function DirectoryBranch({ groupKey, directory, depth, scripts, directories, selectedId, onSelect, onContextMenu, expandedPaths, onToggle, loadingChildrenPaths, }: Omit & { directory: WorkspaceDirectory; }) { const expandKey = `${groupKey}/${directory.path}`; const open = expandedPaths.has(expandKey); const childrenLoading = loadingChildrenPaths.has(directory.path); return (
{open && ( )}
); }