1
+ use std:: collections:: HashMap ;
1
2
use yew:: prelude:: * ;
2
3
3
4
#[ derive( Clone , PartialEq , Properties ) ]
@@ -7,55 +8,133 @@ pub struct FileNode {
7
8
pub children : Option < Vec < FileNode > > ,
8
9
}
9
10
11
+ fn load_data ( ) -> Vec < FileNode > {
12
+ let file_tree = vec ! [ FileNode {
13
+ name: "Yew 篇" . to_string( ) ,
14
+ is_folder: true ,
15
+ children: Some ( vec![
16
+ FileNode {
17
+ name: "环境配置" . to_string( ) ,
18
+ is_folder: false ,
19
+ children: None ,
20
+ } ,
21
+ FileNode {
22
+ name: "第一个静态页面" . to_string( ) ,
23
+ is_folder: false ,
24
+ children: None ,
25
+ } ,
26
+ FileNode {
27
+ name: "HTML" . to_string( ) ,
28
+ is_folder: true ,
29
+ children: Some ( vec![
30
+ FileNode {
31
+ name: "html! 宏" . to_string( ) ,
32
+ is_folder: false ,
33
+ children: None ,
34
+ } ,
35
+ FileNode {
36
+ name: "事件" . to_string( ) ,
37
+ is_folder: true ,
38
+ children: Some ( vec![
39
+ FileNode {
40
+ name: "事件监听器" . to_string( ) ,
41
+ is_folder: false ,
42
+ children: None ,
43
+ } ,
44
+ FileNode {
45
+ name: "事件冒泡" . to_string( ) ,
46
+ is_folder: false ,
47
+ children: None ,
48
+ } ,
49
+ ] ) ,
50
+ } ,
51
+ ] ) ,
52
+ } ,
53
+ ] ) ,
54
+ } ] ;
55
+ file_tree
56
+ }
57
+
58
+ #[ function_component( SearchIcon ) ]
59
+ fn search_icon ( ) -> Html {
60
+ html ! {
61
+ <svg
62
+ xmlns="http://www.w3.org/2000/svg"
63
+ class="search-icon"
64
+ fill="none"
65
+ viewBox="0 0 24 24"
66
+ stroke="currentColor"
67
+ >
68
+ <path
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ stroke-width="2"
72
+ d="M15.75 15.75L19.5 19.5M10.5 18.75a8.25 8.25 0 1 1 0-16.5 8.25 8.25 0 0 1 0 16.5z"
73
+ />
74
+ </svg>
75
+ }
76
+ }
77
+
78
+
79
+ #[ function_component( SearchInput ) ]
80
+ fn search_input ( ) -> Html {
81
+ html ! {
82
+ <input
83
+ type ="text"
84
+ class="search-input"
85
+ placeholder="查找"
86
+ />
87
+ }
88
+ }
89
+
90
+
91
+ #[ derive( Properties , PartialEq ) ]
92
+ pub struct FolderIconProps {
93
+ pub used : bool ,
94
+ }
95
+ // 定义 FolderIcon 组件
96
+ #[ function_component( FolderIcon ) ]
97
+ pub fn folder_icon ( props : & FolderIconProps ) -> Html {
98
+ // 根据 `used` 状态生成类名
99
+ let class_name = if props. used {
100
+ "folder-icon open"
101
+ } else {
102
+ "folder-icon"
103
+ } ;
104
+
105
+ html ! {
106
+ <svg class={ class_name} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
107
+ {
108
+ if props. used {
109
+ html! {
110
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" /> // 向下箭头
111
+ }
112
+ } else {
113
+ html! {
114
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 6l6 6-6 6" /> // 向右箭头
115
+ }
116
+ }
117
+ }
118
+ </svg>
119
+ }
120
+ }
121
+
122
+
123
+ #[ function_component( FileIcon ) ]
124
+ pub fn file_icon ( ) -> Html {
125
+ html ! {
126
+ <svg class="file-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
127
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
128
+ </svg>
129
+ }
130
+ }
131
+
10
132
#[ function_component( Sidebar ) ]
11
133
pub fn sidebar ( ) -> Html {
12
- let file_tree = vec ! [
13
- FileNode {
14
- name: "Yew 篇" . to_string( ) ,
15
- is_folder: true ,
16
- children: Some ( vec![
17
- FileNode {
18
- name: "环境配置" . to_string( ) ,
19
- is_folder: false ,
20
- children: None ,
21
- } ,
22
- FileNode {
23
- name: "第一个静态页面" . to_string( ) ,
24
- is_folder: false ,
25
- children: None ,
26
- } ,
27
- FileNode {
28
- name: "HTML" . to_string( ) ,
29
- is_folder: true ,
30
- children: Some ( vec![
31
- FileNode {
32
- name: "html! 宏" . to_string( ) ,
33
- is_folder: false ,
34
- children: None ,
35
- } ,
36
- FileNode {
37
- name: "事件" . to_string( ) ,
38
- is_folder: true ,
39
- children: Some ( vec![
40
- FileNode {
41
- name: "事件监听器" . to_string( ) ,
42
- is_folder: false ,
43
- children: None ,
44
- } ,
45
- FileNode {
46
- name: "事件冒泡" . to_string( ) ,
47
- is_folder: false ,
48
- children: None ,
49
- } ,
50
- ] ) ,
51
- } ,
52
- ] ) ,
53
- } ,
54
- ] ) ,
55
- } ,
56
- ] ;
57
-
58
- let is_expanded = use_state ( || true ) ;
134
+ let file_tree = load_data ( ) ;
135
+
136
+ // 初始化文件夹的展开状态
137
+ let expanded_state = use_state ( || HashMap :: < String , bool > :: new ( ) ) ;
59
138
60
139
html ! {
61
140
<div class="sidebar" >
@@ -65,68 +144,60 @@ pub fn sidebar() -> Html {
65
144
</div>
66
145
<div class="sidebar-search" >
67
146
<div class="search-container" >
68
- <input
69
- type ="text"
70
- class="search-input"
71
- placeholder="查找"
72
- />
73
- <svg
74
- xmlns="http://www.w3.org/2000/svg"
75
- class="search-icon"
76
- fill="none"
77
- viewBox="0 0 24 24"
78
- stroke="currentColor"
79
- >
80
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.75 15.75L19.5 19.5M10.5 18.75a8.25 8.25 0 1 1 0-16.5 8.25 8.25 0 0 1 0 16.5z" />
81
- </svg>
147
+ <SearchInput />
148
+ <SearchIcon />
82
149
</div>
83
150
</div>
84
151
<div class="sidebar-content" >
85
- { file_tree. iter( ) . map( |node| render_file_node( node, is_expanded . clone( ) ) ) . collect:: <Html >( ) }
152
+ { file_tree. iter( ) . map( |node| render_file_node( node, expanded_state . clone( ) ) ) . collect:: <Html >( ) }
86
153
</div>
87
154
</div>
88
155
}
89
156
}
90
157
91
- fn render_file_node ( file_node : & FileNode , is_expanded : UseStateHandle < bool > ) -> Html {
158
+ fn render_file_node ( file_node : & FileNode , expanded_state : UseStateHandle < HashMap < String , bool > > ) -> Html {
159
+ let file_name = file_node. name . clone ( ) ;
160
+
161
+ // 获取当前文件夹的展开状态
162
+ let is_expanded = expanded_state
163
+ . get ( & file_name)
164
+ . cloned ( )
165
+ . unwrap_or ( false ) ;
166
+
167
+ // 点击事件:更新当前文件夹的展开状态
92
168
let toggle = {
93
- let is_expanded = is_expanded. clone ( ) ;
94
- Callback :: from ( move |_| is_expanded. set ( !* is_expanded) )
169
+ let expanded_state = expanded_state. clone ( ) ;
170
+ let file_name = file_name. clone ( ) ;
171
+ Callback :: from ( move |_| {
172
+ let mut state = ( * expanded_state) . clone ( ) ;
173
+ let is_expanded = state. entry ( file_name. clone ( ) ) . or_insert ( false ) ;
174
+ * is_expanded = !* is_expanded;
175
+ expanded_state. set ( state) ;
176
+ } )
95
177
} ;
96
178
97
179
html ! {
98
180
<div class="file-node" >
99
181
<div class="file-node-header" onclick={ toggle} >
100
182
{
101
183
if file_node. is_folder {
102
- if * is_expanded {
103
- html! {
104
- <svg class="folder-icon open" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
105
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" />
106
- </svg>
107
- }
184
+ if is_expanded {
185
+ html! { <FolderIcon used={ true } />}
108
186
} else {
109
- html! {
110
- <svg class="folder-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
111
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 6l6 6-6 6" />
112
- </svg>
113
- }
187
+
188
+ html! { <FolderIcon used={ false } />}
114
189
}
115
190
} else {
116
- html! {
117
- <svg class="file-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
118
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
119
- </svg>
120
- }
191
+ html! { <FileIcon />}
121
192
}
122
193
}
123
194
<span class="file-node-name" >{ & file_node. name } </span>
124
195
</div>
125
196
{
126
- if * is_expanded && file_node. is_folder {
197
+ if is_expanded && file_node. is_folder {
127
198
html! {
128
199
<div class="file-node-children" >
129
- { file_node. children. as_ref( ) . unwrap_or( & vec![ ] ) . iter( ) . map( |child| render_file_node( child, is_expanded . clone( ) ) ) . collect:: <Html >( ) }
200
+ { file_node. children. as_ref( ) . unwrap_or( & vec![ ] ) . iter( ) . map( |child| render_file_node( child, expanded_state . clone( ) ) ) . collect:: <Html >( ) }
130
201
</div>
131
202
}
132
203
} else {
0 commit comments