sidebar.jsx
4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// 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;