import { replayHistory } from "../send"; /** * 左侧集合树:搜索 / 目录 / 请求 / 右键菜单 / 拖拽移动 / 历史记录。 */ import type { Collection, CollectionNode } from "../core/index"; import { findNode, searchTree } from "../core/index"; import { useMemo, useState } from "react"; import { useWorkspace } from "../workspaceStore"; import { ContextMenu, cx, formatTime, type MenuItem } from "./common"; import { Icon } from "./icons"; import { KindIcon } from "./KindIcon"; import { Logo } from "./Logo"; import { WorkspaceSidebar } from "./WorkspaceSidebar"; export function Sidebar() { const collections = useWorkspace((s) => s.collections); const search = useWorkspace((s) => s.search); const setSearch = useWorkspace((s) => s.setSearch); const history = useWorkspace((s) => s.history); const clearHistory = useWorkspace((s) => s.clearHistory); const openTab = useWorkspace((s) => s.openTab); // 接口树 / 历史在侧栏分段切换;活动栏只保留「接口」入口,避免语义叠层 const side = useWorkspace((s) => s.sidebarSide); const setSide = useWorkspace((s) => s.setSidebarSide); const [menu, setMenu] = useState<{ x: number; y: number; items: MenuItem[] } | null>(null); const hitIds = useMemo(() => searchTree(collections, search), [collections, search]); return (
{side === "api" && ( setSearch(e.target.value)} /> )}
{side === "history" ? ( <> {history.length === 0 && (
暂无请求记录
发送请求后自动归档
)} {history.slice(0, 50).map((h) => (
{h.nodeId && ( )}
))} {history.length > 0 && ( )} ) : ( <> {collections.length === 0 && (
还没有集合
点击顶部「导入」或「新建」开始
)} {collections.map((collection) => (
{collection.children .filter((n) => search === "" || hitIds.has(n.id)) .map((node) => ( ))}
))} )}
setMenu(null)} />
); } function TreeChildren({ nodes, depth, hitIds, collectionId, onMenu, }: { nodes: CollectionNode[]; depth: number; hitIds: Set; collectionId: string; onMenu: (m: { x: number; y: number; items: MenuItem[] } | null) => void; }) { const expanded = useWorkspace((s) => s.expanded); const toggleExpanded = useWorkspace((s) => s.toggleExpanded); const search = useWorkspace((s) => s.search); return ( <> {nodes .filter((n) => search === "" || hitIds.has(n.id)) .map((node) => (
{node.type === "folder" && expanded[node.id] && ( )}
))} ); } function TreeRow({ node, depth, hitIds, collectionId, isCollection = false, onMenu, }: { node: CollectionNode | Collection; depth: number; hitIds: Set; collectionId: string; isCollection?: boolean; onMenu: (m: { x: number; y: number; items: MenuItem[] } | null) => void; }) { const activeTabId = useWorkspace((s) => s.activeTabId); const dirtyIds = useWorkspace((s) => s.dirtyIds); const expanded = useWorkspace((s) => s.expanded); const toggleExpanded = useWorkspace((s) => s.toggleExpanded); const openTab = useWorkspace((s) => s.openTab); const removeNodeById = useWorkspace((s) => s.removeNodeById); const duplicateNodeById = useWorkspace((s) => s.duplicateNodeById); const addRequest = useWorkspace((s) => s.addRequest); const addFolder = useWorkspace((s) => s.addFolder); const addCollection = useWorkspace((s) => s.addCollection); const setDialog = useWorkspace((s) => s.setDialog); const moveNodeTo = useWorkspace((s) => s.moveNodeTo); const [dragOver, setDragOver] = useState(false); const isFolder = node.type === "folder" || node.type === "collection"; const requestNode = node.type === "request" ? node : null; const isOpen = isFolder && expanded[node.id]; const isActive = requestNode !== null && node.id === activeTabId; const buildMenu = (): MenuItem[] => { const items: MenuItem[] = []; if (isCollection) { items.push( { label: "新建请求", icon: , onClick: () => setDialog({ type: "new", collectionId, parentId: null }) }, { label: "新建目录", icon: , onClick: () => addFolder(collectionId, null) }, { type: "sep" }, { label: "重命名", icon: , onClick: () => setDialog({ type: "rename", id: node.id }) }, { label: "导出该集合…", icon: , onClick: () => setDialog({ type: "export", collectionId }) }, { type: "sep" }, { label: "新建集合", icon: , onClick: () => addCollection() }, ); return items; } if (isFolder) { items.push( { label: "新建请求", icon: , onClick: () => setDialog({ type: "new", collectionId, parentId: node.id }) }, { label: "新建子目录", icon: , onClick: () => addFolder(collectionId, node.id) }, { type: "sep" }, { label: "重命名", icon: "✎", onClick: () => setDialog({ type: "rename", id: node.id }) }, { label: "复制目录", icon: , onClick: () => duplicateNodeById(node.id), }, { type: "sep" }, { label: "删除目录", icon: , danger: true, onClick: () => removeNodeById(node.id) }, ); return items; } // request if (node.kind === "http") { items.unshift({ label: "打开", icon: , onClick: () => openTab(node.id) }); items.push( { label: "重命名", icon: , onClick: () => setDialog({ type: "rename", id: node.id }) }, { label: "复制接口", icon: , onClick: () => duplicateNodeById(node.id) }, { type: "sep" }, { label: "删除", icon: , danger: true, onClick: () => removeNodeById(node.id) }, ); } else { items.push( { label: "打开", icon: , onClick: () => openTab(node.id) }, { label: "重命名", icon: , onClick: () => setDialog({ type: "rename", id: node.id }) }, { label: "复制", icon: , onClick: () => duplicateNodeById(node.id) }, { type: "sep" }, { label: "删除", icon: , danger: true, onClick: () => removeNodeById(node.id) }, ); } return items; }; const label = ( {node.name} {requestNode && dirtyIds[node.id] ? ( ) : null} ); return (
{ e.dataTransfer.setData("text/opsxapi-node", node.id); e.dataTransfer.effectAllowed = "move"; }} onDragOver={(e) => { if (!isFolder) return; e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); if (!isFolder) return; const dragId = e.dataTransfer.getData("text/opsxapi-node"); if (dragId && dragId !== node.id) { const parentId = isCollection ? null : node.id; moveNodeTo(dragId, collectionId, parentId); if (parentId) toggleExpanded(parentId, true); } }} onClick={() => { if (isFolder) toggleExpanded(node.id); else openTab(node.id); }} onContextMenu={(e) => { e.preventDefault(); onMenu({ x: e.clientX, y: e.clientY, items: buildMenu() }); }} > {isFolder ? ( ) : ( )} {node.type === "collection" ? ( ) : node.type === "folder" ? ( ) : requestNode && requestNode.kind === "http" ? ( {requestNode.data.method} ) : requestNode ? ( ) : null} {label}
); }