写在前面:文章内容为本人的学习过程,路过大神不喜勿喷!
一、具体思路
既然是权限分配,根据RBAC原则,只需要给角色分配菜单权限,给人分配角色即可。
选择使用Python的Pyqt5绘制页面。
1.基础使用的参数变量
ownMenuDf = pd.Dataframe # 以拥有的权限
haveNotMenuDf = pd.Dataframe # 未拥有的权限
chooseMenuIds = [] # 选择需要添加的菜单ID列表
removeMenuIds = [] # 选择需要移除的菜单ID列表
2.构造权限的基础数据
def fillLimit(self, groupId):
self.chooseMenuIds = []
self.removeMenuIds = []
self.ownMenuDf =
# 此处是获取以拥有的权限列表,数据来源自定
# 此Dataframe中应包含一些关键列:
# MENUID 菜单ID
# MENU_ZN 菜单中文名称
# PARENTID 父级节点ID
# MENUNODE 是节点还是菜单的标识(此处0表示为菜单节点,1表示为菜单项)
# MENUSORT 菜单顺序
self.initTreeView(self.ownMenuDf, self.ownModel, self.ownLimitTreeView)
# 筛选出已拥有的所有菜单节点
self.directoryIds = self.ownMenuDf.loc[self.ownMenuDf['MENUNODE'] == '0', 'MENUID'].to_list()
# 筛选出已拥有的所有菜单项
pageIds = self.ownMenuDf.loc[self.ownMenuDf['MENUNODE'] == '1', 'MENUID'].to_list()
# 从全部菜单中过滤掉已拥有的剩下的就是未拥有的菜单项
self.haveNotMenuDf = self.menuDf[~self.menuDf['MENUID'].isin(pageIds)]
# 提取出筛选完未拥有的菜单项的菜单节点的ID
parent_ids = self.haveNotMenuDf.loc[self.haveNotMenuDf['MENUNODE'] == '0', 'MENUID'].to_list()
# 通过递归方法判断这些未拥有的菜单节点下是否包含子菜单项,筛选出菜单节点下没有子菜单的菜单节点ID
filterIds = [parent_id for parent_id in parent_ids if self.has_sub_menu(self.haveNotMenuDf, parent_id)]
# 将这些节点从未拥有的菜单数据集中删除
self.haveNotMenuDf = self.haveNotMenuDf[~self.haveNotMenuDf['MENUID'].isin(filterIds)]
self.haveNotMenuDf.reset_index(inplace=True, drop=True)
self.initTreeView(self.haveNotMenuDf, self.haveNotModel, self.allLimitTreeView)
#递归判断所有目录菜单下是否有子菜单项
def has_sub_menu(self, df, menuid):
children = df[(df['PARENTID'] == menuid) & (df['MENUNODE'] == '1')]
if len(children) == 0:
return True
for child_menuid in children['MENUID'].tolist():
if self.has_sub_menu(df, child_menuid):
return False
return True
3.构造树形结构的菜单列表并开启复选框
# df为要形成树形结构的数据集
# model 为 QStandardItemModel 对象
# treeView 为 QTreeView 对象
def initTreeView(self, df, model, treeView):
model.clear()
# 设置表头信息
model.setHorizontalHeaderLabels(['菜单列表', 'ID'])
treeView.setModel(model)
# 调整第一列的宽度
treeView.header().resizeSection(0, 160)
# 设置节点不可编辑
treeView.setEditTriggers(QAbstractItemView.NoEditTriggers)
# 设置第二列隐藏
treeView.setColumnHidden(1, True)
# 设置成有虚线连接的方式
treeView.setStyle(QStyleFactory.create('windows'))
# 开始构建菜单树
self.build_menu(df, model, treeView)
# 递归构造菜单节点列表
def build_menu(self, df, model, treeView):
# 筛选出所有的菜单节点
root_nodes = df[df['PARENTID'] == '0'].sort_values("MENUSORT")
root_nodes.reset_index(drop=True, inplace=True)
for i, node in root_nodes.iterrows():
# 创建QStandardItem菜单节点对象并添加到QStandardItemModel对象中
item = QStandardItem(node['MENU_ZN'])
item.setCheckable(True) # 开启复选框
model.appendRow(item)
model.setItem(i, 1, QStandardItem(node['MENUID']))
self.add_children(node['MENUID'], df, item)
# 将QStandardItemModel设置到相对应的QTreeView对象中
treeView.setModel(model)
# 递归向菜单节点中添加菜单项
def add_children(self, parent_id, df, parent_item):
# 根据父菜单ID,找出子菜单项
children = df[df['PARENTID'] == parent_id].sort_values("MENUSORT")
children.reset_index(drop=True, inplace=True)
for i, child in children.iterrows():
# 创建QStandardItem菜单项对象并添加到QStandardItemModel对象中
child_item = QStandardItem(child['MENU_ZN'])
child_item.setCheckable(True) # 开启复选框
parent_item.appendRow(child_item)
parent_item.setChild(child_item.index().row(), 1, QStandardItem(children.loc[i, 'MENUID']))
self.add_children(child['MENUID'], df, child_item)
4.绑定属性结构复选框点击事件
self.未拥有的QTreeView.clicked.connect(self.addLimit)
self.已拥有的QTreeView.clicked.connect(self.removeLimit)
5.事件触发方法
# 添加权限
# index 为当前点击的 QModelIndex 对象
def addLimit(self, index):
if index.column() == 0: # 表示只点击复选框有效
item = self.haveNotModel.itemFromIndex(index)
item.setCheckState(Qt.Checked if item.checkState() == Qt.Checked else Qt.Unchecked)
# 递归处理子节点
self.recursivelyHandleChildren(item, item.checkState())
# 处理父节点状态
self.handleParentStatus(item)
# 触发视图更新
self.haveNotModel.dataChanged.emit(index, index)
# 向上递归收集选中的父菜单ID
self.filterMenuIdsPre(item, item.checkState(), self.haveNotModel, self.chooseMenuIds, 'add')
# 向下递归收集选中的子菜单ID
self.filterMenuIdsSub(item, item.checkState(), self.haveNotModel, self.chooseMenuIds)
print('chooseMenuIds', self.chooseMenuIds)
# 移除权限
# index 为当前点击的 QModelIndex 对象
def removeLimit(self, index):
if index.column() == 0: # 表示只点击复选框有效
item = self.ownModel.itemFromIndex(index)
item.setCheckState(Qt.Checked if item.checkState() == Qt.Checked else Qt.Unchecked)
# 递归处理子节点
self.recursivelyHandleChildren(item, item.checkState())
# 处理父节点状态
self.handleParentStatus(item)
# 触发视图更新
self.ownModel.dataChanged.emit(index, index)
# 向上递归收集选中的父菜单ID
self.filterMenuIdsPre(item, item.checkState(), self.ownModel, self.removeMenuIds, 'del')
# 向下递归收集选中的子菜单ID
self.filterMenuIdsSub(item, item.checkState(), self.ownModel, self.removeMenuIds)
print('removeMenuIds', self.removeMenuIds)
# 递归修改子节点选中状态
def recursivelyHandleChildren(self, item, state):
if item.hasChildren():
for i in range(item.rowCount()):
child = item.child(i)
child.setCheckState(state)
self.recursivelyHandleChildren(child, state)
# 递归修改父级节点选中状态
def handleParentStatus(self, item):
parent = item.parent()
if parent:
childStates = [parent.child(i).checkState() for i in range(parent.rowCount())]
if all(state == Qt.Checked for state in childStates):
parent.setCheckState(Qt.Checked)
elif all(state == Qt.Unchecked for state in childStates):
parent.setCheckState(Qt.Unchecked)
else:
parent.setCheckState(Qt.PartiallyChecked)
# 递归收集子菜单对应的菜单ID
def filterMenuIdsSub(self, item, state, model, menuIdList):
# 通过点击的对象寻找在QStandardItemModel中的QModelIndex对象
index = model.indexFromItem(item)
# 通过QModelIndex对象获取到第1列的菜单ID
click_menuId = str(index.sibling(index.row(), 1).data())
# 如果当前点击是选中状态,则递归将选中的菜单ID收集到选中菜单列表中
if state == Qt.Checked:
if click_menuId not in menuIdList:
menuIdList.append(click_menuId)
if item.hasChildren():
for i in range(item.rowCount()):
self.filterMenuIdsSub(item.child(i), state, model, menuIdList)
# 如果当前点击时取消选中,则递归将取消勾选的菜单ID从选中菜单列表中删除
elif state == Qt.Unchecked:
if click_menuId in menuIdList:
menuIdList.remove(click_menuId)
if item.hasChildren():
for i in range(item.rowCount()):
self.filterMenuIdsSub(item.child(i), state, model, menuIdList)
# 递归收集父级菜单的菜单ID
def filterMenuIdsPre(self, item, state, model, menuIdList, which):
# 找到当前点击项的父级
parent = item.parent()
if not parent:
return
# 从对应的QStandardItemModel中寻找QModelIndex对象
index = model.indexFromItem(parent)
# 通过QModelIndex对象获取到第1列的菜单ID
click_menuId = str(index.sibling(index.row(), 1).data())
# 收集父级菜单节点下的所有子菜单的选择状态
childStates = [parent.child(i).checkState() for i in range(parent.rowCount())]
# 添加权限
# ✅勾选状态
# 所有子菜单中有选中的状态
# 当前点击的菜单ID没有存在于添加权限的收集列表中
# 当前点击的菜单ID没有存在于已拥有权限菜单中
# 将父级菜单ID添加到添加权限的收集列表中
if which == 'add' and state == Qt.Checked and (any(qtstate == Qt.Checked for qtstate in childStates) or any(qtstate == Qt.PartiallyChecked for qtstate in childStates)) and click_menuId not in menuIdList and click_menuId not in self.directoryIds:
menuIdList.append(click_menuId)
# 添加权限
# ❎取消勾选
# 所有子菜单都是未勾选状态
# 当前点击的菜单ID存在于添加权限的收集列表中
# 将父级菜单ID从添加权限的收集列表中移除
elif which == 'add' and state == Qt.Unchecked and all(qtstate == Qt.Unchecked for qtstate in childStates) and click_menuId in menuIdList:
menuIdList.remove(click_menuId)
# 移除权限
# ✅勾选状态
# 所有子菜单都是勾选状态
# 当前点击的菜单ID没有存在于删除权限的收集列表中
# 将父级菜单ID添加到删除权限的收集列表中
elif which == 'del' and state == Qt.Checked and all(qtstate == Qt.Checked for qtstate in childStates) and click_menuId not in menuIdList:
menuIdList.append(click_menuId)
# 移除权限
# ❎取消勾选
# 所有子菜单中存在任意一个为选中的状态
# 将父级菜单ID从删除权限的收集列表中移除
elif which == 'del' and state == Qt.Unchecked and (any(qtstate == Qt.Checked for qtstate in childStates) or any(qtstate == Qt.PartiallyChecked for qtstate in childStates)) and click_menuId in menuIdList:
menuIdList.remove(click_menuId)
self.filterMenuIdsPre(parent, state, model, menuIdList, which)
6.保存记录
根据需要将对应列表已固定形式记录下来,根据实际情报保存到数据库、文件或其他地方。
以便后续的查询和操作。
写在后面:
当前写完这个权限分配使用还没出现异常情况。若有逻辑错误还请大家及时指正。
由于本人使用的是企业内数据,菜单名称及列表包含企业数据,存在敏感数据。
核心思路和一些封装方法就是以上所述,欢迎评论或私信讨论优化方案。
最后的最后:希望文章可以帮到大家,路过大神不喜勿喷,我们共同学习,加油吧!!!!