├── .gitignore ├── tree_test.go ├── tree.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | .idea -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tidwall/pretty" 7 | "testing" 8 | ) 9 | 10 | // 定义我们自己的菜单对象 11 | type SystemMenu struct { 12 | Id int `json:"id"` //id 13 | FatherId int `json:"father_id"` //上级菜单id 14 | Name string `json:"name"` //菜单名 15 | Route string `json:"route"` //页面路径 16 | Icon string `json:"icon"` //图标路径 17 | } 18 | 19 | // region 实现ITree 所有接口 20 | func (s SystemMenu) GetTitle() string { 21 | return s.Name 22 | } 23 | 24 | func (s SystemMenu) GetId() int { 25 | return s.Id 26 | } 27 | 28 | func (s SystemMenu) GetFatherId() int { 29 | return s.FatherId 30 | } 31 | 32 | func (s SystemMenu) GetData() interface{} { 33 | return s 34 | } 35 | 36 | func (s SystemMenu) IsRoot() bool { 37 | // 这里通过FatherId等于0 或者 FatherId等于自身Id表示顶层根节点 38 | return s.FatherId == 0 || s.FatherId == s.Id 39 | } 40 | 41 | // endregion 42 | 43 | type SystemMenus []SystemMenu 44 | 45 | // ConvertToINodeArray 将当前数组转换成父类 INode 接口 数组 46 | func (s SystemMenus) ConvertToINodeArray() (nodes []INode) { 47 | for _, v := range s { 48 | nodes = append(nodes, v) 49 | } 50 | return 51 | } 52 | 53 | func TestGenerateTree(t *testing.T) { 54 | // 模拟获取数据库中所有菜单,在其它所有的查询中,也是首先将数据库中所有数据查询出来放到数组中, 55 | // 后面的遍历递归,都在这个 allMenu中进行,而不是在数据库中进行递归查询,减小数据库压力。 56 | allMenu := []SystemMenu{ 57 | {Id: 1, FatherId: 0, Name: "系统总览", Route: "/systemOverview", Icon: "icon-system"}, 58 | {Id: 2, FatherId: 0, Name: "系统配置", Route: "/systemConfig", Icon: "icon-config"}, 59 | 60 | {Id: 3, FatherId: 1, Name: "资产", Route: "/asset", Icon: "icon-asset"}, 61 | {Id: 4, FatherId: 1, Name: "动环", Route: "/pe", Icon: "icon-pe"}, 62 | 63 | {Id: 5, FatherId: 2, Name: "菜单配置", Route: "/menuConfig", Icon: "icon-menu-config"}, 64 | {Id: 6, FatherId: 3, Name: "设备", Route: "/device", Icon: "icon-device"}, 65 | {Id: 7, FatherId: 3, Name: "机柜", Route: "/device", Icon: "icon-device"}, 66 | } 67 | 68 | // 生成完全树 69 | resp := GenerateTree(SystemMenus.ConvertToINodeArray(allMenu), nil) 70 | bytes, _ := json.MarshalIndent(resp, "", "\t") 71 | //fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil))) 72 | 73 | // 模拟选中 '资产' 菜单 74 | selectedNode := []SystemMenu{allMenu[2]} 75 | resp = GenerateTree(SystemMenus.ConvertToINodeArray(allMenu), SystemMenus.ConvertToINodeArray(selectedNode)) 76 | bytes, _ = json.Marshal(resp) 77 | //fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil))) 78 | 79 | // 模拟从数据库中查询出 '设备' 80 | device := []SystemMenu{allMenu[5]} 81 | // 查询 设备 的所有父节点 82 | respNodes := FindRelationNode(SystemMenus.ConvertToINodeArray(device), SystemMenus.ConvertToINodeArray(allMenu)) 83 | resp = GenerateTree(respNodes, nil) 84 | bytes, _ = json.Marshal(resp) 85 | fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil))) 86 | } 87 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import "sort" 4 | 5 | // Tree 统一定义菜单树的数据结构,也可以自定义添加其他字段 6 | type Tree struct { 7 | Title string `json:"title"` //节点名字 8 | Data interface{} `json:"data"` //自定义对象 9 | Leaf bool `json:"leaf"` //叶子节点 10 | Selected bool `json:"checked"` //选中 11 | PartialSelected bool `json:"partial_selected"` //部分选中 12 | Children []Tree `json:"children"` //子节点 13 | } 14 | 15 | // ConvertToINodeArray 其他的结构体想要生成菜单树,直接实现这个接口 16 | type INode interface { 17 | // GetTitle 获取显示名字 18 | GetTitle() string 19 | // GetId获取id 20 | GetId() int 21 | // GetFatherId 获取父id 22 | GetFatherId() int 23 | // GetData 获取附加数据 24 | GetData() interface{} 25 | // IsRoot 判断当前节点是否是顶层根节点 26 | IsRoot() bool 27 | } 28 | type INodes []INode 29 | 30 | func (nodes INodes) Len() int { 31 | return len(nodes) 32 | } 33 | func (nodes INodes) Swap(i, j int) { 34 | nodes[i], nodes[j] = nodes[j], nodes[i] 35 | } 36 | func (nodes INodes) Less(i, j int) bool { 37 | return nodes[i].GetId() < nodes[j].GetId() 38 | } 39 | 40 | // GenerateTree 自定义的结构体实现 INode 接口后调用此方法生成树结构 41 | // nodes 需要生成树的节点 42 | // selectedNode 生成树后选中的节点 43 | // menuTrees 生成成功后的树结构对象 44 | func GenerateTree(nodes, selectedNodes []INode) (trees []Tree) { 45 | trees = []Tree{} 46 | // 定义顶层根和子节点 47 | var roots, childs []INode 48 | for _, v := range nodes { 49 | if v.IsRoot() { 50 | // 判断顶层根节点 51 | roots = append(roots, v) 52 | } 53 | childs = append(childs, v) 54 | } 55 | 56 | for _, v := range roots { 57 | childTree := &Tree{ 58 | Title: v.GetTitle(), 59 | Data: v.GetData(), 60 | } 61 | // 递归之前,根据父节点确认 childTree 的选中状态 62 | childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children) 63 | // 递归 64 | recursiveTree(childTree, childs, selectedNodes) 65 | // 递归之后,根据子节点确认 childTree 的选中状态 66 | if !childTree.Selected { 67 | childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children) 68 | } 69 | // 递归之后,根据子节点确认 childTree 的半选中状态 70 | childTree.PartialSelected = nodePartialSelected(childTree.Children) 71 | // 递归之后,根据子节确认是否是叶子节点 72 | childTree.Leaf = len(childTree.Children) == 0 73 | trees = append(trees, *childTree) 74 | } 75 | return 76 | } 77 | 78 | // recursiveTree 递归生成树结构 79 | // tree 递归的树对象 80 | // nodes 递归的节点 81 | // selectedNodes 选中的节点 82 | func recursiveTree(tree *Tree, nodes, selectedNodes []INode) { 83 | data := tree.Data.(INode) 84 | 85 | for _, v := range nodes { 86 | if v.IsRoot() { 87 | // 如果当前节点是顶层根节点就跳过 88 | continue 89 | } 90 | if data.GetId() == v.GetFatherId() { 91 | childTree := &Tree{ 92 | Title: v.GetTitle(), 93 | Data: v.GetData(), 94 | } 95 | // 递归之前,根据子节点和父节点确认 childTree 的选中状态 96 | childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children) || tree.Selected 97 | recursiveTree(childTree, nodes, selectedNodes) 98 | 99 | if !childTree.Selected { 100 | // 递归之后,根据子节点确认 childTree 的选中状态 101 | childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children) 102 | } 103 | // 递归之后,根据子节点确认 childTree 的半选中状态 104 | childTree.PartialSelected = nodePartialSelected(childTree.Children) 105 | // 递归之后,根据子节确认是否是叶子节点 106 | childTree.Leaf = len(childTree.Children) == 0 107 | tree.Children = append(tree.Children, *childTree) 108 | } 109 | } 110 | } 111 | 112 | // FindRelationNode 在 allTree 中查询 nodes 中节点的所有父节点 113 | // nodes 要查询父节点的子节点数组 114 | // allTree 所有节点数组 115 | func FindRelationNode(nodes, allNodes []INode) (respNodes []INode) { 116 | nodeMap := make(map[int]INode) 117 | for _, v := range nodes { 118 | recursiveFindRelationNode(nodeMap, allNodes, v, 0) 119 | } 120 | 121 | for _, v := range nodeMap { 122 | respNodes = append(respNodes, v) 123 | } 124 | sort.Sort(INodes(respNodes)) 125 | return 126 | } 127 | 128 | // recursiveFindRelationNode 递归查询关联父子节点 129 | // nodeMap 查询结果搜集到map中 130 | // allNodes 所有节点 131 | // node 递归节点 132 | // t 递归查找类型:0 查找父、子节点;1 只查找父节点;2 只查找子节点 133 | func recursiveFindRelationNode(nodeMap map[int]INode, allNodes []INode, node INode, t int) { 134 | nodeMap[node.GetId()] = node 135 | for _, v := range allNodes { 136 | if _, ok := nodeMap[v.GetId()]; ok { 137 | continue 138 | } 139 | // 查找父节点 140 | if t == 0 || t == 1 { 141 | if node.GetFatherId() == v.GetId() { 142 | nodeMap[v.GetId()] = v 143 | if v.IsRoot() { 144 | // 是顶层根节点时,不再进行递归 145 | continue 146 | } 147 | recursiveFindRelationNode(nodeMap, allNodes, v, 1) 148 | } 149 | } 150 | // 查找子节点 151 | if t == 0 || t == 2 { 152 | if node.GetId() == v.GetFatherId() { 153 | nodeMap[v.GetId()] = v 154 | recursiveFindRelationNode(nodeMap, allNodes, v, 2) 155 | } 156 | } 157 | } 158 | } 159 | 160 | // nodeSelected 判断节点的选中状态 161 | // node 进行判断节点 162 | func nodeSelected(node INode, selectedNodes []INode, children []Tree) bool { 163 | for _, v := range selectedNodes { 164 | if node.GetId() == v.GetId() { 165 | // 1. 如果选择节点数组中存在当前节点 166 | return true 167 | } 168 | } 169 | 170 | if len(children) == 0 { 171 | // 2. 不满足前置条件1,且没有子节点 172 | return false 173 | } 174 | selectedNum := 0 175 | for _, v := range children { 176 | if v.Selected { 177 | selectedNum++ 178 | } 179 | } 180 | if selectedNum == len(children) { 181 | // 不满足前置条件1,2 ,且子节点全部是选中状态 182 | return true 183 | } 184 | return false 185 | } 186 | 187 | // nodePartialSelected 判断节点的半选中状态 188 | func nodePartialSelected(trees []Tree) bool { 189 | selectedNum := 0 190 | for _, v := range trees { 191 | if v.Selected { 192 | selectedNum++ 193 | } 194 | } 195 | if selectedNum == len(trees) || selectedNum == 0 { 196 | // 子节点全选中,或一个也没有选中 197 | return false 198 | } 199 | return true 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-tree-menu 2 | `golang`中实现快速菜单树的生成,包括菜单节点的选中状态、半选中状态,菜单的搜索。 3 | @[toc] 4 | ## 1 该包提供两个方法根接口 5 | ### 1.1 GenerateTree(nodes, selectedNodes []INode) (trees []Tree) 6 | `GenerateTree` 自定义的结构体实现 `INode` 接口后调用此方法生成树结构。 7 | ### 1.2 FindRelationNode(nodes, allNodes []INode) (respNodes []INode) 8 | `FindRelationNode` 在 `allNodes` 中查询 `nodes` 中节点的所有父子节点 返回 `respNodes`(包含 `nodes` , 跟其所有父子节点) 9 | 10 | ### 1.3 接口 INode 11 | ```go 12 | // ConvertToINodeArray 其他的结构体想要生成菜单树,直接实现这个接口 13 | type INode interface { 14 | // GetTitle 获取显示名字 15 | GetTitle() string 16 | // GetId获取id 17 | GetId() int 18 | // GetFatherId 获取父id 19 | GetFatherId() int 20 | // GetData 获取附加数据 21 | GetData() interface{} 22 | // IsRoot 判断当前节点是否是顶层根节点 23 | IsRoot() bool 24 | } 25 | ``` 26 | 27 | ## 2 使用 28 | ``` 29 | go get github.com/azhengyongqin/golang-tree-menu 30 | ``` 31 | 32 | ### 2.1 定义自己的菜单结构体并且实现接口 `INode` 33 | ```go 34 | // 定义我们自己的菜单对象 35 | type SystemMenu struct { 36 | Id int `json:"id"` //id 37 | FatherId int `json:"father_id"` //上级菜单id 38 | Name string `json:"name"` //菜单名 39 | Route string `json:"route"` //页面路径 40 | Icon string `json:"icon"` //图标路径 41 | } 42 | 43 | func (s SystemMenu) GetTitle() string { 44 | return s.Name 45 | } 46 | func (s SystemMenu) GetId() int { 47 | return s.Id 48 | } 49 | func (s SystemMenu) GetFatherId() int { 50 | return s.FatherId 51 | } 52 | func (s SystemMenu) GetData() interface{} { 53 | return s 54 | } 55 | func (s SystemMenu) IsRoot() bool { 56 | // 这里通过FatherId等于0 或者 FatherId等于自身Id表示顶层根节点 57 | return s.FatherId == 0 || s.FatherId == s.Id 58 | } 59 | ``` 60 | ### 2.2 实现一个将自定义结构体`SystemMenu` 数组转换成 `INode` 数组的方法 61 | ```go 62 | type SystemMenus []SystemMenu 63 | // ConvertToINodeArray 将当前数组转换成父类 INode 接口 数组 64 | func (s SystemMenus) ConvertToINodeArray() (nodes []INode) { 65 | for _, v := range s { 66 | nodes = append(nodes, v) 67 | } 68 | return 69 | } 70 | ``` 71 | 72 | ## 3 测试效果 73 | ### 3.1 添加测试数据 74 | ```go 75 | // 模拟获取数据库中所有菜单,在其它所有的查询中,也是首先将数据库中所有数据查询出来放到数组中, 76 | // 后面的遍历递归,都在这个 allMenu中进行,而不是在数据库中进行递归查询,减小数据库压力。 77 | allMenu := []SystemMenu{ 78 | {Id: 1, FatherId: 0, Name: "系统总览", Route: "/systemOverview", Icon: "icon-system"}, 79 | {Id: 2, FatherId: 0, Name: "系统配置", Route: "/systemConfig", Icon: "icon-config"}, 80 | 81 | {Id: 3, FatherId: 1, Name: "资产", Route: "/asset", Icon: "icon-asset"}, 82 | {Id: 4, FatherId: 1, Name: "动环", Route: "/pe", Icon: "icon-pe"}, 83 | 84 | {Id: 5, FatherId: 2, Name: "菜单配置", Route: "/menuConfig", Icon: "icon-menu-config"}, 85 | {Id: 6, FatherId: 3, Name: "设备", Route: "/device", Icon: "icon-device"}, 86 | {Id: 7, FatherId: 3, Name: "机柜", Route: "/device", Icon: "icon-device"}, 87 | } 88 | ``` 89 | ### 3.2 生成完全树 90 | ```go 91 | // 生成完全树 92 | resp := GenerateTree(SystemMenus.ConvertToINodeArray(allMenu), nil) 93 | bytes, _ := json.MarshalIndent(resp, "", "\t") 94 | fmt.Println(string(bytes)) 95 | ``` 96 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191010190933163.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzMTc5MDc1,size_16,color_FFFFFF,t_70) 97 | ```json 98 | [ 99 | { 100 | "title": "系统总览", 101 | "leaf": false, 102 | "checked": false, 103 | "partial_selected": false, 104 | "children": [ 105 | { 106 | "title": "资产", 107 | "leaf": false, 108 | "checked": false, 109 | "partial_selected": false, 110 | "children": [ 111 | { 112 | "title": "设备", 113 | "leaf": true, 114 | "checked": false, 115 | "partial_selected": false, 116 | "children": null 117 | }, 118 | { 119 | "title": "机柜", 120 | "leaf": true, 121 | "checked": false, 122 | "partial_selected": false, 123 | "children": null 124 | } 125 | ] 126 | }, 127 | { 128 | "title": "动环", 129 | "leaf": true, 130 | "checked": false, 131 | "partial_selected": false, 132 | "children": null 133 | } 134 | ] 135 | }, 136 | { 137 | "title": "系统配置", 138 | "leaf": false, 139 | "checked": false, 140 | "partial_selected": false, 141 | "children": [ 142 | { 143 | "title": "菜单配置", 144 | "leaf": true, 145 | "checked": false, 146 | "partial_selected": false, 147 | "children": null 148 | } 149 | ] 150 | } 151 | ] 152 | ``` 153 | 154 | ### 3.3 带选中状态和半选中状态的树 155 | ```go 156 | // 模拟选中 '资产' 菜单 157 | selectedNode := []SystemMenu{allMenu[2]} 158 | resp = GenerateTree(SystemMenus.ConvertToINodeArray(allMenu), SystemMenus.ConvertToINodeArray(selectedNode)) 159 | bytes, _ = json.Marshal(resp) 160 | fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil))) 161 | ``` 162 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2019101019133053.png) 163 | ```json 164 | [ 165 | { 166 | "title": "系统总览", 167 | "leaf": false, 168 | "checked": false, 169 | "partial_selected": true, 170 | "children": [ 171 | { 172 | "title": "资产", 173 | "leaf": false, 174 | "checked": true, 175 | "partial_selected": false, 176 | "children": [ 177 | { 178 | "title": "设备", 179 | "leaf": true, 180 | "checked": true, 181 | "partial_selected": false, 182 | "children": null 183 | }, 184 | { 185 | "title": "机柜", 186 | "leaf": true, 187 | "checked": true, 188 | "partial_selected": false, 189 | "children": null 190 | } 191 | ] 192 | }, 193 | { 194 | "title": "动环", 195 | "leaf": true, 196 | "checked": false, 197 | "partial_selected": false, 198 | "children": null 199 | } 200 | ] 201 | }, 202 | { 203 | "title": "系统配置", 204 | "leaf": false, 205 | "checked": false, 206 | "partial_selected": false, 207 | "children": [ 208 | { 209 | "title": "菜单配置", 210 | "leaf": true, 211 | "checked": false, 212 | "partial_selected": false, 213 | "children": null 214 | } 215 | ] 216 | } 217 | ] 218 | ``` 219 | 220 | ### 3.4 模拟查询某个节点,然后生成树 221 | ```go 222 | // 模拟从数据库中查询出 '设备' 223 | device := []SystemMenu{allMenu[5]} 224 | // 查询 `设备` 的所有父节点 225 | respNodes := FindRelationNode(SystemMenus.ConvertToINodeArray(device), SystemMenus.ConvertToINodeArray(allMenu)) 226 | resp = GenerateTree(respNodes, nil) 227 | bytes, _ = json.Marshal(resp) 228 | fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil))) 229 | ``` 230 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191010191711638.png) 231 | ```json 232 | [ 233 | { 234 | "title": "系统总览", 235 | "leaf": false, 236 | "checked": false, 237 | "partial_selected": false, 238 | "children": [ 239 | { 240 | "title": "资产", 241 | "leaf": false, 242 | "checked": false, 243 | "partial_selected": false, 244 | "children": [ 245 | { 246 | "title": "设备", 247 | "leaf": true, 248 | "checked": false, 249 | "partial_selected": false, 250 | "children": null 251 | } 252 | ] 253 | } 254 | ] 255 | } 256 | ] 257 | ``` 258 | > 源码地址:[https://github.com/azhengyongqin/golang-tree-menu](https://github.com/azhengyongqin/golang-tree-menu) 259 | --------------------------------------------------------------------------------