sidebar.jsx 4.22 KB
// Sidebar — search + collapsible tree, dense Windows-explorer style.

const Sidebar = ({ tree, activeNodeId, expanded, setExpanded, onNodeClick, query, setQuery }) => {
  return (
    <div style={{
      width: "100%", height: "100%", display: "flex", flexDirection: "column",
      background: "#fff", borderRight: "1px solid var(--border)",
      fontSize: 12, color: "var(--text)",
    }}>
      {/* Search bar */}
      <div style={{
        padding: 6, borderBottom: "1px solid var(--border)",
        display: "flex", alignItems: "center", background: "#fff", flex: "none",
      }}>
        <div style={{ position: "relative", flex: 1 }}>
          <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="请输入您想要搜索的关键字"
            style={{
              width: "100%", height: 26, paddingLeft: 8, paddingRight: 26,
              border: "1px solid var(--border-input)", background: "var(--bg-input)",
              fontSize: 12, color: "var(--text)",
            }}
          />
          <div style={{ position: "absolute", right: 6, top: "50%", transform: "translateY(-50%)", color: "var(--text-faint)" }}>
            <Ic.search size={12} />
          </div>
        </div>
      </div>
      {/* Tree */}
      <div style={{ flex: 1, overflow: "auto", padding: "4px 0" }}>
        {tree.map((node) => (
          <TreeNode
            key={node.id}
            node={node}
            depth={0}
            activeNodeId={activeNodeId}
            expanded={expanded}
            setExpanded={setExpanded}
            onNodeClick={onNodeClick}
            query={query}
          />
        ))}
      </div>
    </div>
  );
};

const matches = (node, q) => {
  if (!q) return true;
  const t = q.toLowerCase();
  if ((node.label || "").toLowerCase().includes(t)) return true;
  if (node.children) return node.children.some((c) => matches(c, q));
  return false;
};

const TreeNode = ({ node, depth, activeNodeId, expanded, setExpanded, onNodeClick, query }) => {
  const hasChildren = node.children && node.children.length > 0;
  const isExpanded = expanded[node.id] || (query && matches(node, query) && hasChildren);
  const isActive = activeNodeId === node.id;
  const visible = matches(node, query);
  if (!visible) return null;

  const click = () => {
    if (hasChildren) {
      setExpanded((s) => ({ ...s, [node.id]: !isExpanded }));
    }
    if (node.leaf || !hasChildren) {
      onNodeClick(node);
    }
  };

  return (
    <div>
      <div
        onClick={click}
        style={{
          display: "flex", alignItems: "center",
          height: 24, paddingLeft: 6 + depth * 14, paddingRight: 8,
          cursor: "pointer",
          background: isActive ? "var(--selected)" : "transparent",
          color: isActive ? "var(--accent-strong)" : node.badge ? "var(--accent-strong)" : "var(--text)",
          fontWeight: isActive || node.badge ? 500 : 400,
          whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
        }}
        onMouseEnter={(e) => { if (!isActive) e.currentTarget.style.background = "var(--bg-row-hover)"; }}
        onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.background = "transparent"; }}
      >
        {hasChildren ? (
          <span style={{ width: 12, color: "var(--text-faint)", display: "inline-flex" }}>
            {isExpanded ? <Ic.triangle size={9} /> : <Ic.triangleR size={9} />}
          </span>
        ) : (
          <span style={{ width: 12, display: "inline-flex", justifyContent: "center", color: "var(--text-faint)" }}>
            <Ic.dot size={6} />
          </span>
        )}
        <span style={{ marginLeft: 4, overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>
          {node.label}
        </span>
      </div>
      {isExpanded && hasChildren ? (
        <div>
          {node.children.map((child) => (
            <TreeNode
              key={child.id}
              node={child}
              depth={depth + 1}
              activeNodeId={activeNodeId}
              expanded={expanded}
              setExpanded={setExpanded}
              onNodeClick={onNodeClick}
              query={query}
            />
          ))}
        </div>
      ) : null}
    </div>
  );
};

window.Sidebar = Sidebar;