golang实现无限级菜单

查数据库

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
type Menu struct {
Id int `json:"id"`
Pid int `json:"pid"`
Name string `json:"name"`
Sort int `json:"sort"`
Route string `json:"route"`
}

type TreeList struct {
Id int `json:"id"`
Name string `json:"name"`
Pid int `json:"pid"`
Sort int `json:"sort"`
Route string `json:"route"`
Children []*TreeList `json:"children"`
}

func init() {
// 注册模型
orm.RegisterModel(new(Menu))
}

func (m *Menu)TableName() string {
return "auth_menu"
}

/**
菜单列表
*/
func (m *Menu)MenuList() []*TreeList{
return m.getMenu(0)
}

/**
递归获取树形菜单
*/
func (m *Menu)getMenu(pid int) []*TreeList {
o := orm.NewOrm()
var menu []Menu
_,_ = o.QueryTable("auth_menu").Filter("pid", pid).OrderBy("sort").All(&menu)
treeList := []*TreeList{}
for _, v := range menu{
child := v.getMenu(v.Id)
node := &TreeList{
Id:v.Id,
Name:v.Name,
Sort:v.Sort,
Route:v.Route,
Pid:v.Pid,
}
node.Children = child
treeList = append(treeList, node)
}
return treeList
}

只查一次

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package tree

import "sort"

// Tree 统一定义菜单树的数据结构,也可以自定义添加其他字段
type Tree struct {
Title string `json:"title"` //节点名字
Data interface{} `json:"data"` //自定义对象
Leaf bool `json:"leaf"` //叶子节点
Selected bool `json:"checked"` //选中
PartialSelected bool `json:"partial_selected"` //部分选中
Children []Tree `json:"children"` //子节点
}

// ConvertToINodeArray 其他的结构体想要生成菜单树,直接实现这个接口
type INode interface {
// GetTitle 获取显示名字
GetTitle() string
// GetId获取id
GetId() int
// GetFatherId 获取父id
GetFatherId() int
// GetData 获取附加数据
GetData() interface{}
// IsRoot 判断当前节点是否是顶层根节点
IsRoot() bool
}
type INodes []INode

func (nodes INodes) Len() int {
return len(nodes)
}
func (nodes INodes) Swap(i, j int) {
nodes[i], nodes[j] = nodes[j], nodes[i]
}
func (nodes INodes) Less(i, j int) bool {
return nodes[i].GetId() < nodes[j].GetId()
}

// GenerateTree 自定义的结构体实现 INode 接口后调用此方法生成树结构
// nodes 需要生成树的节点
// selectedNode 生成树后选中的节点
// menuTrees 生成成功后的树结构对象
func GenerateTree(nodes, selectedNodes []INode) (trees []Tree) {
trees = []Tree{}
// 定义顶层根和子节点
var roots, childs []INode
for _, v := range nodes {
if v.IsRoot() {
// 判断顶层根节点
roots = append(roots, v)
}
childs = append(childs, v)
}

for _, v := range roots {
childTree := &Tree{
Title: v.GetTitle(),
Data: v.GetData(),
}
// 递归之前,根据父节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
// 递归
recursiveTree(childTree, childs, selectedNodes)
// 递归之后,根据子节点确认 childTree 的选中状态
if !childTree.Selected {
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
}
// 递归之后,根据子节点确认 childTree 的半选中状态
childTree.PartialSelected = nodePartialSelected(childTree.Children)
// 递归之后,根据子节确认是否是叶子节点
childTree.Leaf = len(childTree.Children) == 0
trees = append(trees, *childTree)
}
return
}

// recursiveTree 递归生成树结构
// tree 递归的树对象
// nodes 递归的节点
// selectedNodes 选中的节点
func recursiveTree(tree *Tree, nodes, selectedNodes []INode) {
data := tree.Data.(INode)

for _, v := range nodes {
if v.IsRoot() {
// 如果当前节点是顶层根节点就跳过
continue
}
if data.GetId() == v.GetFatherId() {
childTree := &Tree{
Title: v.GetTitle(),
Data: v.GetData(),
}
// 递归之前,根据子节点和父节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children) || tree.Selected
recursiveTree(childTree, nodes, selectedNodes)

if !childTree.Selected {
// 递归之后,根据子节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
}
// 递归之后,根据子节点确认 childTree 的半选中状态
childTree.PartialSelected = nodePartialSelected(childTree.Children)
// 递归之后,根据子节确认是否是叶子节点
childTree.Leaf = len(childTree.Children) == 0
tree.Children = append(tree.Children, *childTree)
}
}
}

// FindRelationNode 在 allTree 中查询 nodes 中节点的所有父节点
// nodes 要查询父节点的子节点数组
// allTree 所有节点数组
func FindRelationNode(nodes, allNodes []INode) (respNodes []INode) {
nodeMap := make(map[int]INode)
for _, v := range nodes {
recursiveFindRelationNode(nodeMap, allNodes, v, 0)
}

for _, v := range nodeMap {
respNodes = append(respNodes, v)
}
sort.Sort(INodes(respNodes))
return
}

// recursiveFindRelationNode 递归查询关联父子节点
// nodeMap 查询结果搜集到map中
// allNodes 所有节点
// node 递归节点
// t 递归查找类型:0 查找父、子节点;1 只查找父节点;2 只查找子节点
func recursiveFindRelationNode(nodeMap map[int]INode, allNodes []INode, node INode, t int) {
nodeMap[node.GetId()] = node
for _, v := range allNodes {
if _, ok := nodeMap[v.GetId()]; ok {
continue
}
// 查找父节点
if t == 0 || t == 1 {
if node.GetFatherId() == v.GetId() {
nodeMap[v.GetId()] = v
if v.IsRoot() {
// 是顶层根节点时,不再进行递归
continue
}
recursiveFindRelationNode(nodeMap, allNodes, v, 1)
}
}
// 查找子节点
if t == 0 || t == 2 {
if node.GetId() == v.GetFatherId() {
nodeMap[v.GetId()] = v
recursiveFindRelationNode(nodeMap, allNodes, v, 2)
}
}
}
}

// nodeSelected 判断节点的选中状态
// node 进行判断节点
func nodeSelected(node INode, selectedNodes []INode, children []Tree) bool {
for _, v := range selectedNodes {
if node.GetId() == v.GetId() {
// 1. 如果选择节点数组中存在当前节点
return true
}
}

if len(children) == 0 {
// 2. 不满足前置条件1,且没有子节点
return false
}
selectedNum := 0
for _, v := range children {
if v.Selected {
selectedNum++
}
}
if selectedNum == len(children) {
// 不满足前置条件1,2 ,且子节点全部是选中状态
return true
}
return false
}

// nodePartialSelected 判断节点的半选中状态
func nodePartialSelected(trees []Tree) bool {
selectedNum := 0
for _, v := range trees {
if v.Selected {
selectedNum++
}
}
if selectedNum == len(trees) || selectedNum == 0 {
// 子节点全选中,或一个也没有选中
return false
}
return true
}

test

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
package tree

import (
"encoding/json"
"fmt"
"github.com/tidwall/pretty"
"testing"
)

// 定义我们自己的菜单对象
type SystemMenu struct {
Id int `json:"id"` //id
FatherId int `json:"father_id"` //上级菜单id
Name string `json:"name"` //菜单名
Route string `json:"route"` //页面路径
Icon string `json:"icon"` //图标路径
}

// region 实现ITree 所有接口
func (s SystemMenu) GetTitle() string {
return s.Name
}

func (s SystemMenu) GetId() int {
return s.Id
}

func (s SystemMenu) GetFatherId() int {
return s.FatherId
}

func (s SystemMenu) GetData() interface{} {
return s
}

func (s SystemMenu) IsRoot() bool {
// 这里通过FatherId等于0 或者 FatherId等于自身Id表示顶层根节点
return s.FatherId == 0 || s.FatherId == s.Id
}

// endregion

type SystemMenus []SystemMenu

// ConvertToINodeArray 将当前数组转换成父类 INode 接口 数组
func (s SystemMenus) ConvertToINodeArray() (nodes []INode) {
for _, v := range s {
nodes = append(nodes, v)
}
return
}

func TestGenerateTree(t *testing.T) {
// 模拟获取数据库中所有菜单,在其它所有的查询中,也是首先将数据库中所有数据查询出来放到数组中,
// 后面的遍历递归,都在这个 allMenu中进行,而不是在数据库中进行递归查询,减小数据库压力。
allMenu := []SystemMenu{
{Id: 1, FatherId: 0, Name: "系统总览", Route: "/systemOverview", Icon: "icon-system"},
{Id: 2, FatherId: 0, Name: "系统配置", Route: "/systemConfig", Icon: "icon-config"},

{Id: 3, FatherId: 1, Name: "资产", Route: "/asset", Icon: "icon-asset"},
{Id: 4, FatherId: 1, Name: "动环", Route: "/pe", Icon: "icon-pe"},

{Id: 5, FatherId: 2, Name: "菜单配置", Route: "/menuConfig", Icon: "icon-menu-config"},
{Id: 6, FatherId: 3, Name: "设备", Route: "/device", Icon: "icon-device"},
{Id: 7, FatherId: 3, Name: "机柜", Route: "/device", Icon: "icon-device"},
}

// 生成完全树
resp := GenerateTree(SystemMenus.ConvertToINodeArray(allMenu), nil)
bytes, _ := json.MarshalIndent(resp, "", "\t")
//fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil)))

// 模拟选中 '资产' 菜单
selectedNode := []SystemMenu{allMenu[2]}
resp = GenerateTree(SystemMenus.ConvertToINodeArray(allMenu), SystemMenus.ConvertToINodeArray(selectedNode))
bytes, _ = json.Marshal(resp)
//fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil)))

// 模拟从数据库中查询出 '设备'
device := []SystemMenu{allMenu[5]}
// 查询 设备 的所有父节点
respNodes := FindRelationNode(SystemMenus.ConvertToINodeArray(device), SystemMenus.ConvertToINodeArray(allMenu))
resp = GenerateTree(respNodes, nil)
bytes, _ = json.Marshal(resp)
fmt.Println(string(pretty.Color(pretty.PrettyOptions(bytes, pretty.DefaultOptions), nil)))
}

https://github.com/azhengyongqin/golang-tree-menu