From 3f3a3b2b1e5e62fa0f7d8427e6b095b0b592b60e Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sun, 24 Sep 2023 12:50:08 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E9=A1=B5=E5=8F=B3=E9=94=AE=E5=BF=AB=E6=8D=B7=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=88fac>=3D0.2.10rc18=EF=BC=89=20#I838PN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/layout_c/head_c.py | 2 +- .../callbacks/layout_c/index_c.py | 313 ++++++++++++++++-- .../views/layout/components/content.py | 19 +- 3 files changed, 298 insertions(+), 36 deletions(-) diff --git a/dash-fastapi-frontend/callbacks/layout_c/head_c.py b/dash-fastapi-frontend/callbacks/layout_c/head_c.py index e0e4353..f462f7c 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/head_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/head_c.py @@ -72,7 +72,7 @@ app.clientside_callback( return true; } ''', - Output('trigger-reload-output', 'reload'), + Output('trigger-reload-output', 'reload', allow_duplicate=True), Input('index-reload', 'nClicks'), prevent_initial_call=True ) diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 3241995..376a569 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -1,5 +1,6 @@ import dash from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate import feffery_antd_components as fac from jsonpath_ng import parse from flask import json @@ -11,8 +12,8 @@ from utils.tree_tool import find_title_by_key, find_modules_by_key, find_href_by @app.callback( - [Output('tabs-container', 'items'), - Output('tabs-container', 'activeKey')], + [Output('tabs-container', 'items', allow_duplicate=True), + Output('tabs-container', 'activeKey', allow_duplicate=True)], [Input('index-side-menu', 'currentKey'), Input('tabs-container', 'latestDeletePane')], [State('tabs-container', 'items'), @@ -37,6 +38,7 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act # 先前已更新为非None的按钮的nClicks误触发通知弹出回调 parser = parse('$..nClicks') origin_items = parser.update(origin_items, None) + new_items = dash.Patch() if trigger_id == 'index-side-menu': @@ -57,27 +59,58 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 menu_modules = find_modules_by_key(menu_info.get('menu_info'), currentKey) + for index, item in enumerate(origin_items): + if {'key': '关闭右侧', 'label': '关闭右侧', 'icon': 'antd-arrow-right'} not in item['contextMenu']: + item['contextMenu'].insert(-1, { + 'key': '关闭右侧', + 'label': '关闭右侧', + 'icon': 'antd-arrow-right' + }) + new_items[index]['contextMenu'] = item['contextMenu'] + context_menu = [ + { + 'key': '刷新页面', + 'label': '刷新页面', + 'icon': 'antd-reload' + }, + { + 'key': '关闭当前', + 'label': '关闭当前', + 'icon': 'antd-close' + }, + { + 'key': '关闭其他', + 'label': '关闭其他', + 'icon': 'antd-close-circle' + }, + { + 'key': '全部关闭', + 'label': '全部关闭', + 'icon': 'antd-close-circle' + } + ] + if len(origin_items) != 1: + context_menu.insert(-1, { + 'key': '关闭左侧', + 'label': '关闭左侧', + 'icon': 'antd-arrow-left' + }) if menu_modules: if menu_modules == 'link': - return [dash.no_update] * 2 + raise PreventUpdate else: # 否则追加子项返回 # 其中若各标签页内元素类似,则推荐配合模式匹配构建交互逻辑 - return [ - [ - *origin_items, - { - 'label': menu_title, - 'key': currentKey, - 'children': eval('views.' + menu_modules + '.render(button_perms)'), - } - ], - currentKey - ] - - return [ - [ - *origin_items, + new_items.append( + { + 'label': menu_title, + 'key': currentKey, + 'children': eval('views.' + menu_modules + '.render(button_perms)'), + 'contextMenu': context_menu + } + ) + else: + new_items.append( { 'label': menu_title, 'key': currentKey, @@ -90,34 +123,246 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act 'paddingTop': 0 } ), + 'contextMenu': context_menu } - ], + ) + + return [ + new_items, currentKey ] elif trigger_id == 'tabs-container': - # 若要删除的是当前正激活的标签页 - if latestDeletePane == activeKey: + # 如果删除的是当前标签页则回到最后新增的标签页,否则保持当前标签页不变 + for index, item in enumerate(origin_items): + if item['key'] == latestDeletePane: + context_menu = [ + { + 'key': '刷新页面', + 'label': '刷新页面', + 'icon': 'antd-reload' + }, + { + 'key': '关闭其他', + 'label': '关闭其他', + 'icon': 'antd-close-circle' + }, + { + 'key': '全部关闭', + 'label': '全部关闭', + 'icon': 'antd-close-circle' + } + ] + if index == 1 and len(origin_items) == 2: + new_items[0]['contextMenu'] = context_menu + elif len(origin_items) == 3: + context_menu.insert(1, { + 'key': '关闭当前', + 'label': '关闭当前', + 'icon': 'antd-close' + }) + if index == 1: + new_items[2]['contextMenu'] = context_menu + if index == 2: + new_items[1]['contextMenu'] = context_menu + else: + if index == len(origin_items) - 1: + new_items[index - 1]['contextMenu'] = item['contextMenu'] + new_items.remove(item) + break + new_origin_items = [ + item for item in + origin_items if item['key'] != latestDeletePane + ] + + return [ + new_items, + new_origin_items[-1]['key'] if activeKey == latestDeletePane else activeKey + ] + + raise PreventUpdate + + +@app.callback( + [Output('tabs-container', 'items', allow_duplicate=True), + Output('tabs-container', 'activeKey', allow_duplicate=True), + Output('trigger-reload-output', 'reload', allow_duplicate=True)], + Input('tabs-container', 'clickedContextMenu'), + [State('tabs-container', 'items'), + State('tabs-container', 'activeKey')], + prevent_initial_call=True +) +def handle_via_context_menu(clickedContextMenu, origin_items, activeKey): + """ + 基于标签页标题右键菜单的额外标签页控制 + """ + if clickedContextMenu['menuKey'] == '刷新页面': + + return [ + dash.no_update, + dash.no_update, + True + ] + + if '关闭' in clickedContextMenu['menuKey']: + new_items = dash.Patch() + if clickedContextMenu['menuKey'] == '关闭当前': + for index, item in enumerate(origin_items): + if item['key'] == clickedContextMenu['tabKey']: + context_menu = [ + { + 'key': '刷新页面', + 'label': '刷新页面', + 'icon': 'antd-reload' + }, + { + 'key': '关闭其他', + 'label': '关闭其他', + 'icon': 'antd-close-circle' + }, + { + 'key': '全部关闭', + 'label': '全部关闭', + 'icon': 'antd-close-circle' + } + ] + if index == 1 and len(origin_items) == 2: + new_items[0]['contextMenu'] = context_menu + elif len(origin_items) == 3: + context_menu.insert(1, { + 'key': '关闭当前', + 'label': '关闭当前', + 'icon': 'antd-close' + }) + if index == 1: + new_items[2]['contextMenu'] = context_menu + if index == 2: + new_items[1]['contextMenu'] = context_menu + else: + if index == len(origin_items) - 1: + new_items[index - 1]['contextMenu'] = item['contextMenu'] + new_items.remove(item) + break + new_origin_items = [ + item for item in + origin_items if item['key'] != clickedContextMenu['tabKey'] + ] + return [ - [ - item - for item in origin_items - if item['key'] != latestDeletePane - ], - '首页' + new_items, + new_origin_items[-1]['key'] if activeKey == clickedContextMenu['tabKey'] else activeKey, + dash.no_update ] - # 否则保持当前激活的标签页子项不变,删去目标子项 + elif clickedContextMenu['menuKey'] == '关闭其他': + for item in origin_items: + if item['key'] != clickedContextMenu['tabKey'] and item['key'] != '首页': + new_items.remove(item) + context_menu = [ + { + 'key': '刷新页面', + 'label': '刷新页面', + 'icon': 'antd-reload' + }, + { + 'key': '关闭其他', + 'label': '关闭其他', + 'icon': 'antd-close-circle' + }, + { + 'key': '全部关闭', + 'label': '全部关闭', + 'icon': 'antd-close-circle' + } + ] + if clickedContextMenu['tabKey'] == '首页': + new_items[0]['contextMenu'] = context_menu + else: + context_menu.insert(1, { + 'key': '关闭当前', + 'label': '关闭当前', + 'icon': 'antd-close' + }) + new_items[1]['contextMenu'] = context_menu + + return [ + new_items, + clickedContextMenu['tabKey'], + dash.no_update + ] + + elif clickedContextMenu['menuKey'] == '关闭左侧': + current_index = 0 + for index, item in enumerate(origin_items): + if item['key'] == clickedContextMenu['tabKey']: + current_index = index + item['contextMenu'].remove({ + 'key': '关闭左侧', + 'label': '关闭左侧', + 'icon': 'antd-arrow-left' + }) + new_items[index]['contextMenu'] = item['contextMenu'] + break + for item in origin_items[1:current_index]: + new_items.remove(item) + + return [ + new_items, + clickedContextMenu['tabKey'], + dash.no_update + ] + + elif clickedContextMenu['menuKey'] == '关闭右侧': + current_index = 0 + for index, item in enumerate(origin_items): + if item['key'] == clickedContextMenu['tabKey']: + current_index = index + item['contextMenu'].remove({ + 'key': '关闭右侧', + 'label': '关闭右侧', + 'icon': 'antd-arrow-right' + }) + new_items[index]['contextMenu'] = item['contextMenu'] + break + for item in origin_items[current_index+1:]: + new_items.remove(item) + + return [ + new_items, + clickedContextMenu['tabKey'], + dash.no_update + ] + + for item in origin_items: + if item['key'] != '首页': + new_items.remove(item) + new_items[0]['contextMenu'] = [ + { + 'key': '刷新页面', + 'label': '刷新页面', + 'icon': 'antd-reload' + }, + { + 'key': '关闭其他', + 'label': '关闭其他', + 'icon': 'antd-close-circle' + }, + { + 'key': '全部关闭', + 'label': '全部关闭', + 'icon': 'antd-close-circle' + } + ] + # 否则则为全部关闭 return [ - [ - item - for item in origin_items - if item['key'] != latestDeletePane - ], + new_items, + '首页', dash.no_update ] + raise PreventUpdate + # 页首面包屑和hash回调 @app.callback( @@ -175,4 +420,4 @@ def get_current_breadcrumbs(active_key, menu_info): current_href ] - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/views/layout/components/content.py b/dash-fastapi-frontend/views/layout/components/content.py index 454dbd8..c0f577d 100644 --- a/dash-fastapi-frontend/views/layout/components/content.py +++ b/dash-fastapi-frontend/views/layout/components/content.py @@ -16,7 +16,24 @@ def render_main_content(): 'label': '首页', 'key': '首页', 'closable': False, - 'children': render_dashboard() + 'children': render_dashboard(), + 'contextMenu': [ + { + 'key': '刷新页面', + 'label': '刷新页面', + 'icon': 'antd-reload' + }, + { + 'key': '关闭其他', + 'label': '关闭其他', + 'icon': 'antd-close-circle' + }, + { + 'key': '全部关闭', + 'label': '全部关闭', + 'icon': 'antd-close-circle' + } + ] } ], id='tabs-container', -- Gitee From d71f10b71c058686eef7d4cb2deca3360ed83452 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sun, 24 Sep 2023 22:30:55 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E9=A1=B5=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/layout_c/index_c.py | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 376a569..cfcb6d9 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -28,7 +28,7 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act 具体策略: 1.当左侧某个菜单项被新选中,且右侧标签页子项尚未包含此项时,新建并切换 2.当左侧某个菜单项被新选中,且右侧标签页子项已包含此项时,切换 - 3.当右侧标签页子项某项被删除时,销毁对应标签页的同时切换回主标签页 + 3.当右侧标签页子项某项被删除时,销毁对应标签页的同时切换回最后新增的标签页 """ trigger_id = dash.ctx.triggered_id @@ -154,18 +154,24 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act 'icon': 'antd-close-circle' } ] - if index == 1 and len(origin_items) == 2: - new_items[0]['contextMenu'] = context_menu - elif len(origin_items) == 3: - context_menu.insert(1, { - 'key': '关闭当前', - 'label': '关闭当前', - 'icon': 'antd-close' - }) - if index == 1: - new_items[2]['contextMenu'] = context_menu - if index == 2: - new_items[1]['contextMenu'] = context_menu + if index == 1: + if len(origin_items) == 2: + new_items[0]['contextMenu'] = context_menu + else: + origin_items[2]['contextMenu'].remove({ + 'key': '关闭左侧', + 'label': '关闭左侧', + 'icon': 'antd-arrow-left' + }) + new_items[2]['contextMenu'] = origin_items[2]['contextMenu'] + elif index == 2: + if len(origin_items) == 3: + origin_items[1]['contextMenu'].remove({ + 'key': '关闭右侧', + 'label': '关闭右侧', + 'icon': 'antd-arrow-right' + }) + new_items[1]['contextMenu'] = origin_items[1]['contextMenu'] else: if index == len(origin_items) - 1: new_items[index - 1]['contextMenu'] = item['contextMenu'] @@ -227,18 +233,24 @@ def handle_via_context_menu(clickedContextMenu, origin_items, activeKey): 'icon': 'antd-close-circle' } ] - if index == 1 and len(origin_items) == 2: - new_items[0]['contextMenu'] = context_menu - elif len(origin_items) == 3: - context_menu.insert(1, { - 'key': '关闭当前', - 'label': '关闭当前', - 'icon': 'antd-close' - }) - if index == 1: - new_items[2]['contextMenu'] = context_menu - if index == 2: - new_items[1]['contextMenu'] = context_menu + if index == 1: + if len(origin_items) == 2: + new_items[0]['contextMenu'] = context_menu + else: + origin_items[2]['contextMenu'].remove({ + 'key': '关闭左侧', + 'label': '关闭左侧', + 'icon': 'antd-arrow-left' + }) + new_items[2]['contextMenu'] = origin_items[2]['contextMenu'] + elif index == 2: + if len(origin_items) == 3: + origin_items[1]['contextMenu'].remove({ + 'key': '关闭右侧', + 'label': '关闭右侧', + 'icon': 'antd-arrow-right' + }) + new_items[1]['contextMenu'] = origin_items[1]['contextMenu'] else: if index == len(origin_items) - 1: new_items[index - 1]['contextMenu'] = item['contextMenu'] -- Gitee From e790b03d0d7f7c378988c781178e37b2159c64a4 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 4 Oct 2023 10:53:18 +0800 Subject: [PATCH 3/5] =?UTF-8?q?chore:=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96f?= =?UTF-8?q?effery-utils-components=E7=89=88=E6=9C=AC=E4=B8=BA0.2.0b1?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=BA=95=E5=B1=82bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6cbbfe..362cfa2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ fastapi==0.95.1 feffery-antd-charts==0.0.1rc17 feffery-antd-components==0.2.10rc17 feffery-markdown-components==0.2.10 -feffery-utils-components==0.1.28 +feffery-utils-components==0.2.0b1 Flask==2.2.5 Flask-Compress==1.13 greenlet==2.0.2 -- Gitee From 7e8e35878f7409a604e74e2748ee8fbd90c47bc8 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 4 Oct 2023 10:53:46 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs:=E6=9B=B4=E6=96=B0README=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4420359..1771e4c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@

logo

-

Dash-FastAPI-Admin v1.0.5

+

Dash-FastAPI-Admin v1.0.6

基于Dash+FastAPI前后端分离的纯Python快速开发框架

- + - +

@@ -24,7 +24,7 @@ Dash-FastAPI-Admin是一套全部开源的快速开发平台,毫无保留给 ## 内置功能 1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 -2. 角色管理:角色菜单权限分配。 +2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 4. 部门管理:配置系统组织机构(公司、部门、小组)。 5. 岗位管理:配置系统用户所属担任职务。 -- Gitee From c37d86e5ad0f180c9ff38835d309ef30bb6fe20e Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 4 Oct 2023 10:54:48 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E4=BA=94=E7=A7=8D=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=20#I848B?= =?UTF-8?q?G?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/aspect/data_scope.py | 35 +++ .../controller/role_controller.py | 18 ++ .../controller/user_controller.py | 5 +- .../module_admin/dao/role_dao.py | 34 ++- .../module_admin/dao/user_dao.py | 8 +- .../module_admin/entity/vo/role_vo.py | 20 ++ .../module_admin/service/role_service.py | 36 +++ .../module_admin/service/user_service.py | 5 +- dash-fastapi-frontend/api/role.py | 5 + .../callbacks/system_c/role_c/data_scope_c.py | 251 ++++++++++++++++++ .../callbacks/system_c/role_c/role_c.py | 2 +- .../views/system/role/__init__.py | 18 +- .../views/system/role/data_scope.py | 175 ++++++++++++ 13 files changed, 600 insertions(+), 12 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/aspect/data_scope.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/role_c/data_scope_c.py create mode 100644 dash-fastapi-frontend/views/system/role/data_scope.py diff --git a/dash-fastapi-backend/module_admin/aspect/data_scope.py b/dash-fastapi-backend/module_admin/aspect/data_scope.py new file mode 100644 index 0000000..8afe135 --- /dev/null +++ b/dash-fastapi-backend/module_admin/aspect/data_scope.py @@ -0,0 +1,35 @@ +from fastapi import Depends +from module_admin.entity.vo.user_vo import CurrentUserInfoServiceResponse +from module_admin.service.login_service import get_current_user +from typing import Optional + + +class GetDataScope: + """ + 获取当前用户数据权限对应的查询sql语句 + """ + def __init__(self, query_alias: Optional[str] = '', db_alias: Optional[str] = 'db'): + self.query_alias = query_alias + self.db_alias = db_alias + + def __call__(self, current_user: CurrentUserInfoServiceResponse = Depends(get_current_user)): + user_id = current_user.user.user_id + dept_id = current_user.user.dept_id + role_datascope_list = [dict(role_id=item.role_id, data_scope=int(item.data_scope)) for item in current_user.role] + max_data_scope_dict = min(role_datascope_list, key=lambda x: x['data_scope']) + max_role_id = max_data_scope_dict['role_id'] + max_data_scope = max_data_scope_dict['data_scope'] + if self.query_alias == '' or max_data_scope == 1 or user_id == 1: + param_sql = '1 == 1' + elif max_data_scope == 2: + param_sql = f'{self.query_alias}.dept_id.in_({self.db_alias}.query(SysRoleDept.dept_id).filter(SysRoleDept.role_id == {max_role_id}))' + elif max_data_scope == 3: + param_sql = f'{self.query_alias}.dept_id == {dept_id}' + elif max_data_scope == 4: + param_sql = f'{self.query_alias}.dept_id.in_({self.db_alias}.query(SysDept.dept_id).filter(or_(SysDept.dept_id == {dept_id}, func.find_in_set({dept_id}, SysDept.ancestors))))' + elif max_data_scope == 5: + param_sql = f'{self.query_alias}.user_id == {user_id}' + else: + param_sql = '1 == 0' + + return param_sql diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 9f0658d..55e68fd 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -75,6 +75,24 @@ async def edit_system_role(request: Request, edit_role: AddRoleModel, query_db: except Exception as e: logger.exception(e) return response_500(data="", message=str(e)) + + +@roleController.patch("/role/dataScope", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@log_decorator(title='角色管理', business_type=4) +async def edit_system_role_datascope(request: Request, role_data_scope: RoleDataScopeModel, query_db: Session = Depends(get_db), current_user: CurrentUserInfoServiceResponse = Depends(get_current_user)): + try: + role_data_scope.update_by = current_user.user.user_name + role_data_scope.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + role_data_scope_result = RoleService.role_datascope_services(query_db, role_data_scope) + if role_data_scope_result.is_success: + logger.info(role_data_scope_result.message) + return response_200(data=role_data_scope_result, message=role_data_scope_result.message) + else: + logger.warning(role_data_scope_result.message) + return response_400(data="", message=role_data_scope_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message=str(e)) @roleController.post("/role/delete", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:remove'))]) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index a4c9b75..c7f9a30 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -12,6 +12,7 @@ from utils.response_util import * from utils.log_util import * from utils.common_util import bytes2file_response from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.aspect.data_scope import GetDataScope from module_admin.annotation.log_annotation import log_decorator @@ -19,11 +20,11 @@ userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) -async def get_system_user_list(request: Request, user_page_query: UserPageObject, query_db: Session = Depends(get_db)): +async def get_system_user_list(request: Request, user_page_query: UserPageObject, query_db: Session = Depends(get_db), data_scope_sql: str = Depends(GetDataScope('SysUser'))): try: user_query = UserQueryModel(**user_page_query.dict()) # 获取全量数据 - user_query_result = UserService.get_user_list_services(query_db, user_query) + user_query_result = UserService.get_user_list_services(query_db, user_query, data_scope_sql) # 分页操作 user_page_query_result = get_page_obj(user_query_result, user_page_query.page_num, user_page_query.page_size) logger.info('获取成功') diff --git a/dash-fastapi-backend/module_admin/dao/role_dao.py b/dash-fastapi-backend/module_admin/dao/role_dao.py index 52f13f6..8ed51b4 100644 --- a/dash-fastapi-backend/module_admin/dao/role_dao.py +++ b/dash-fastapi-backend/module_admin/dao/role_dao.py @@ -1,8 +1,9 @@ from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from module_admin.entity.do.role_do import SysRole, SysRoleMenu +from module_admin.entity.do.role_do import SysRole, SysRoleMenu, SysRoleDept +from module_admin.entity.do.dept_do import SysDept from module_admin.entity.do.menu_do import SysMenu -from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RoleQueryModel, RoleDetailModel +from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RoleDeptModel, RoleQueryModel, RoleDetailModel from utils.time_format_util import list_format_datetime, object_format_datetime from datetime import datetime, time @@ -74,9 +75,15 @@ class RoleDao: .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ .distinct().all() + query_role_dept_info = db.query(SysDept).select_from(SysRole) \ + .filter(SysRole.del_flag == 0, SysRole.role_id == role_id) \ + .outerjoin(SysRoleDept, SysRole.role_id == SysRoleDept.role_id) \ + .outerjoin(SysDept, and_(SysRoleDept.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ + .distinct().all() results = dict( role=object_format_datetime(query_role_basic_info), menu=list_format_datetime(query_role_menu_info), + dept=list_format_datetime(query_role_dept_info), ) return RoleDetailModel(**results) @@ -177,3 +184,26 @@ class RoleDao: db.query(SysRoleMenu) \ .filter(SysRoleMenu.role_id == role_menu.role_id) \ .delete() + + @classmethod + def add_role_dept_dao(cls, db: Session, role_dept: RoleDeptModel): + """ + 新增角色部门关联信息数据库操作 + :param db: orm对象 + :param role_dept: 用户角色部门关联对象 + :return: + """ + db_role_dept = SysRoleDept(**role_dept.dict()) + db.add(db_role_dept) + + @classmethod + def delete_role_dept_dao(cls, db: Session, role_dept: RoleDeptModel): + """ + 删除角色部门关联信息数据库操作 + :param db: orm对象 + :param role_dept: 角色部门关联对象 + :return: + """ + db.query(SysRoleDept) \ + .filter(SysRoleDept.role_id == role_dept.role_id) \ + .delete() diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index 19ea73a..74ee023 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -1,7 +1,7 @@ from sqlalchemy import and_, or_, desc, func from sqlalchemy.orm import Session from module_admin.entity.do.user_do import SysUser, SysUserRole, SysUserPost -from module_admin.entity.do.role_do import SysRole, SysRoleMenu +from module_admin.entity.do.role_do import SysRole, SysRoleMenu, SysRoleDept from module_admin.entity.do.dept_do import SysDept from module_admin.entity.do.post_do import SysPost from module_admin.entity.do.menu_do import SysMenu @@ -139,11 +139,12 @@ class UserDao: return CurrentUserInfo(**results) @classmethod - def get_user_list(cls, db: Session, query_object: UserQueryModel): + def get_user_list(cls, db: Session, query_object: UserQueryModel, data_scope_sql: str): """ 根据查询参数获取用户列表信息 :param db: orm对象 :param query_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 :return: 用户列表信息对象 """ user_list = db.query(SysUser, SysDept) \ @@ -160,7 +161,8 @@ class UserDao: SysUser.create_time.between( datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if query_object.create_time_start and query_object.create_time_end else True + if query_object.create_time_start and query_object.create_time_end else True, + eval(data_scope_sql) ) \ .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ .distinct().all() diff --git a/dash-fastapi-backend/module_admin/entity/vo/role_vo.py b/dash-fastapi-backend/module_admin/entity/vo/role_vo.py index 6532c58..0f94d43 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/role_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/role_vo.py @@ -1,6 +1,7 @@ from pydantic import BaseModel from typing import Union, Optional, List from module_admin.entity.vo.user_vo import RoleModel +from module_admin.entity.vo.dept_vo import DeptModel from module_admin.entity.vo.menu_vo import MenuModel @@ -15,6 +16,17 @@ class RoleMenuModel(BaseModel): orm_mode = True +class RoleDeptModel(BaseModel): + """ + 角色和部门关联表对应pydantic模型 + """ + role_id: Optional[int] + dept_id: Optional[int] + + class Config: + orm_mode = True + + class RoleQueryModel(RoleModel): """ 角色管理不分页查询模型 @@ -65,6 +77,13 @@ class AddRoleModel(RoleModel): type: Optional[str] +class RoleDataScopeModel(RoleModel): + """ + 角色数据权限模型 + """ + dept_id: Optional[str] + + class DeleteRoleModel(BaseModel): """ 删除角色模型 @@ -80,3 +99,4 @@ class RoleDetailModel(BaseModel): """ role: Union[RoleModel, None] menu: List[Union[MenuModel, None]] + dept: List[Union[DeptModel, None]] diff --git a/dash-fastapi-backend/module_admin/service/role_service.py b/dash-fastapi-backend/module_admin/service/role_service.py index 06a9867..7690255 100644 --- a/dash-fastapi-backend/module_admin/service/role_service.py +++ b/dash-fastapi-backend/module_admin/service/role_service.py @@ -100,6 +100,42 @@ class RoleService: return CrudRoleResponse(**result) + @classmethod + def role_datascope_services(cls, result_db: Session, page_object: RoleDataScopeModel): + """ + 分配角色数据权限service + :param result_db: orm对象 + :param page_object: 角色数据权限对象 + :return: 分配角色数据权限结果 + """ + edit_role = page_object.dict(exclude_unset=True) + del edit_role['dept_id'] + role_info = cls.detail_role_services(result_db, edit_role.get('role_id')) + if role_info: + if role_info.role.role_name != page_object.role_name: + role = RoleDao.get_role_by_info(result_db, RoleModel(**dict(role_name=page_object.role_name))) + if role: + result = dict(is_success=False, message='角色名称已存在') + return CrudRoleResponse(**result) + try: + RoleDao.edit_role_dao(result_db, edit_role) + role_id_dict = dict(role_id=page_object.role_id) + RoleDao.delete_role_dept_dao(result_db, RoleDeptModel(**role_id_dict)) + if page_object.dept_id and page_object.data_scope == '2': + dept_id_list = page_object.dept_id.split(',') + for dept in dept_id_list: + dept_dict = dict(role_id=page_object.role_id, dept_id=dept) + RoleDao.add_role_dept_dao(result_db, RoleDeptModel(**dept_dict)) + result_db.commit() + result = dict(is_success=True, message='分配成功') + except Exception as e: + result_db.rollback() + result = dict(is_success=False, message=str(e)) + else: + result = dict(is_success=False, message='角色不存在') + + return CrudRoleResponse(**result) + @classmethod def delete_role_services(cls, result_db: Session, page_object: DeleteRoleModel): """ diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index 4f45111..1d8260e 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -10,14 +10,15 @@ class UserService: """ @classmethod - def get_user_list_services(cls, result_db: Session, query_object: UserQueryModel): + def get_user_list_services(cls, result_db: Session, query_object: UserQueryModel, data_scope_sql: str): """ 获取用户列表信息service :param result_db: orm对象 :param query_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 :return: 用户列表信息对象 """ - user_list_result = UserDao.get_user_list(result_db, query_object) + user_list_result = UserDao.get_user_list(result_db, query_object, data_scope_sql) return user_list_result diff --git a/dash-fastapi-frontend/api/role.py b/dash-fastapi-frontend/api/role.py index 97f9fc3..2ddbeba 100644 --- a/dash-fastapi-frontend/api/role.py +++ b/dash-fastapi-frontend/api/role.py @@ -21,6 +21,11 @@ def edit_role_api(page_obj: dict): return api_request(method='patch', url='/system/role/edit', is_headers=True, json=page_obj) +def role_datascope_api(page_obj: dict): + + return api_request(method='patch', url='/system/role/dataScope', is_headers=True, json=page_obj) + + def delete_role_api(page_obj: dict): return api_request(method='post', url='/system/role/delete', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c/data_scope_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c/data_scope_c.py new file mode 100644 index 0000000..808dab9 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/role_c/data_scope_c.py @@ -0,0 +1,251 @@ +import dash +import time +from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate +import feffery_utils_components as fuc + +from server import app +from api.role import get_role_detail_api, role_datascope_api +from api.dept import get_dept_tree_api, get_dept_list_api + + +@app.callback( + Output('role-dept-perms', 'expandedKeys'), + Input('role-dept-perms-radio-fold-unfold', 'checked'), + State('role-dept-store', 'data'), + prevent_initial_call=True +) +def fold_unfold_role_dept(fold_unfold, dept_info): + """ + 数据权限表单中展开/折叠checkbox回调 + """ + if dept_info: + default_expanded_keys = [] + for item in dept_info: + if fold_unfold: + default_expanded_keys.append(str(item.get('dept_id'))) + else: + if item.get('parent_id') == 0: + default_expanded_keys.append(str(item.get('dept_id'))) + + return default_expanded_keys + + return dash.no_update + + +@app.callback( + Output('role-dept-perms', 'checkedKeys', allow_duplicate=True), + Input('role-dept-perms-radio-all-none', 'checked'), + State('role-dept-store', 'data'), + prevent_initial_call=True +) +def all_none_role_dept_mode(all_none, dept_info): + """ + 数据权限表单中全选/全不选checkbox回调 + """ + if dept_info: + default_expanded_keys = [] + for item in dept_info: + if item.get('parent_id') == 0: + default_expanded_keys.append(str(item.get('dept_id'))) + + if all_none: + return [str(item.get('dept_id')) for item in dept_info] + else: + return [] + + return dash.no_update + + +@app.callback( + [Output('role-dept-perms', 'checkStrictly'), + Output('role-dept-perms', 'checkedKeys', allow_duplicate=True)], + Input('role-dept-perms-radio-parent-children', 'checked'), + State('current-role-dept-store', 'data'), + prevent_initial_call=True +) +def change_role_dept_mode(parent_children, current_role_dept): + """ + 数据权限表单中父子联动checkbox回调 + """ + checked_dept = [] + if parent_children: + if current_role_dept: + for item in current_role_dept: + has_children = False + for other_item in current_role_dept: + if other_item['parent_id'] == item['dept_id']: + has_children = True + break + if not has_children: + checked_dept.append(str(item.get('dept_id'))) + return [False, checked_dept] + else: + if current_role_dept: + checked_dept = [str(item.get('dept_id')) for item in current_role_dept if item] or [] + return [True, checked_dept] + + +@app.callback( + output=dict( + dept_div=Output('role-dept-perms-div', 'hidden'), + dept_perms_tree=Output('role-dept-perms', 'treeData'), + dept_perms_expanded_check=Output('role-dept-perms-radio-fold-unfold', 'checked'), + dept_perms_checkedkeys=Output('role-dept-perms', 'checkedKeys', allow_duplicate=True), + dept_perms_halfcheckedkeys=Output('role-dept-perms', 'halfCheckedKeys', allow_duplicate=True), + role_dept=Output('role-dept-store', 'data'), + current_role_dept=Output('current-role-dept-store', 'data') + ), + inputs=dict( + data_scope=Input({'type': 'datascope-form-value', 'index': 'data_scope'}, 'value'), + ), + state=dict( + role_info=State('role-edit-id-store', 'data') + ), + prevent_initial_call=True +) +def get_role_dept_info(data_scope, role_info): + if data_scope == '2': + tree_info = get_dept_tree_api({}) + dept_list_info = get_dept_list_api({}) + if tree_info.get('code') == 200 and dept_list_info.get('code') == 200: + tree_data = tree_info['data'] + dept_list = [item for item in dept_list_info['data']['rows'] if item.get('status') == '0'] + checked_dept = [] + checked_dept_all = [] + if role_info.get('dept')[0]: + for item in role_info.get('dept'): + checked_dept_all.append(str(item.get('dept_id'))) + has_children = False + for other_item in role_info.get('dept'): + if other_item['parent_id'] == item['dept_id']: + has_children = True + break + if not has_children: + checked_dept.append(str(item.get('dept_id'))) + half_checked_dept = [x for x in checked_dept_all if x not in checked_dept] + + return dict( + dept_div=False, + dept_perms_tree=tree_data, + dept_perms_expanded_check=True, + dept_perms_checkedkeys=checked_dept, + dept_perms_halfcheckedkeys=half_checked_dept, + role_dept=dept_list, + current_role_dept=role_info.get('dept') + ) + + return dict( + dept_div=False, + dept_perms_tree=dash.no_update, + dept_perms_expanded_check=dash.no_update, + dept_perms_checkedkeys=dash.no_update, + dept_perms_halfcheckedkeys=dash.no_update, + role_dept=dash.no_update, + current_role_dept=dash.no_update + ) + + return dict( + dept_div=True, + dept_perms_tree=dash.no_update, + dept_perms_expanded_check=dash.no_update, + dept_perms_checkedkeys=dash.no_update, + dept_perms_halfcheckedkeys=dash.no_update, + role_dept=dash.no_update, + current_role_dept=dash.no_update + ) + + +@app.callback( + output=dict( + modal_visible=Output('role-datascope-modal', 'visible', allow_duplicate=True), + form_value=Output({'type': 'datascope-form-value', 'index': ALL}, 'value'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('role-edit-id-store', 'data', allow_duplicate=True) + ), + inputs=dict( + button_click=Input({'type': 'role-operation-table', 'operation': ALL, 'index': ALL}, 'nClicks') + ), + prevent_initial_call=True +) +def edit_role_datascope_modal(button_click): + """ + 显示角色数据权限弹窗回调 + """ + trigger_id = dash.ctx.triggered_id + if trigger_id.operation == 'datascope': + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[1]] + role_id = int(trigger_id.index) + role_info_res = get_role_detail_api(role_id=role_id) + if role_info_res['code'] == 200: + role_info = role_info_res['data'] + return dict( + modal_visible=True, + form_value=[role_info.get('role').get(k) for k in form_value_list], + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=role_info + ) + + return dict( + modal_visible=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None + ) + + raise PreventUpdate + + +@app.callback( + output=dict( + modal_visible=Output('role-datascope-modal', 'visible'), + operations=Output('role-operations-store', 'data', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + global_message_container=Output('global-message-container', 'children', allow_duplicate=True) + ), + inputs=dict( + confirm_trigger=Input('role-datascope-modal', 'okCounts') + ), + state=dict( + edit_row_info=State('role-edit-id-store', 'data'), + form_value=State({'type': 'datascope-form-value', 'index': ALL}, 'value'), + dept_checked_keys=State('role-dept-perms', 'checkedKeys'), + dept_half_checked_keys=State('role-dept-perms', 'halfCheckedKeys'), + parent_checked=State('role-dept-perms-radio-parent-children', 'checked') + ), + prevent_initial_call=True +) +def role_datascope_confirm(confirm_trigger, edit_row_info, form_value, dept_checked_keys, dept_half_checked_keys, parent_checked): + """ + 角色数据权限弹窗确认回调,实现分配数据权限的操作 + """ + if confirm_trigger: + # 获取所有输入表单项对应的value + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[1]} + dept_half_checked_keys = dept_half_checked_keys if dept_half_checked_keys else [] + dept_checked_keys = dept_checked_keys if dept_checked_keys else [] + if parent_checked: + dept_perms = dept_half_checked_keys + dept_checked_keys + else: + dept_perms = dept_checked_keys + params_datascope = form_value_state + params_datascope['dept_id'] = ','.join(dept_perms) if dept_perms else None + params_datascope['role_id'] = edit_row_info.get('role').get('role_id') if edit_row_info else None + api_res = role_datascope_api(params_datascope) + if api_res.get('code') == 200: + return dict( + modal_visible=False, + operations={'type': 'datascope'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('分配成功', type='success') + ) + + return dict( + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('分配失败', type='error') + ) + + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c/role_c.py index de81624..e382657 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c/role_c.py @@ -167,7 +167,7 @@ def get_role_table_data(search_click, refresh_click, pagination, operations, rol direction='vertical' ), placement='bottomRight' - ) + ) if 'system:role:edit' in button_perms else [] ] ) diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index 212fce1..27fba42 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -2,7 +2,7 @@ from dash import dcc, html import feffery_antd_components as fac import callbacks.system_c.role_c.role_c -from . import allocate_user +from . import data_scope, allocate_user from api.role import get_role_list_api @@ -109,7 +109,7 @@ def render(button_perms): direction='vertical' ), placement='bottomRight' - ) + ) if 'system:role:edit' in button_perms else [] ] ) @@ -129,6 +129,9 @@ def render(button_perms): # 角色管理模块菜单权限存储容器 dcc.Store(id='role-menu-store'), dcc.Store(id='current-role-menu-store'), + # 角色管理模块数据权限存储容器 + dcc.Store(id='role-dept-store'), + dcc.Store(id='current-role-dept-store'), fac.AntdRow( [ fac.AntdCol( @@ -685,6 +688,17 @@ def render(button_perms): centered=True ), + # 数据权限modal + fac.AntdModal( + data_scope.render(), + id='role-datascope-modal', + title='数据权限', + mask=False, + width=600, + renderFooter=True, + okClickClose=False + ), + # 分配用户modal fac.AntdModal( allocate_user.render(button_perms), diff --git a/dash-fastapi-frontend/views/system/role/data_scope.py b/dash-fastapi-frontend/views/system/role/data_scope.py new file mode 100644 index 0000000..7322e79 --- /dev/null +++ b/dash-fastapi-frontend/views/system/role/data_scope.py @@ -0,0 +1,175 @@ +from dash import html +import feffery_antd_components as fac + +import callbacks.system_c.role_c.data_scope_c + + +def render(): + return [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id={ + 'type': 'datascope-form-value', + 'index': 'role_name' + }, + placeholder='请输入角色名称', + allowClear=True, + disabled=True, + style={ + 'width': 350 + } + ), + label='角色名称', + id={ + 'type': 'datascope-form-label', + 'index': 'role_name' + }, + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + fac.AntdFormItem( + fac.AntdInput( + id={ + 'type': 'datascope-form-value', + 'index': 'role_key' + }, + placeholder='请输入权限字符', + allowClear=True, + disabled=True, + style={ + 'width': 350 + } + ), + label='权限字符', + id={ + 'type': 'datascope-form-label', + 'index': 'role_key' + }, + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + fac.AntdFormItem( + fac.AntdSelect( + id={ + 'type': 'datascope-form-value', + 'index': 'data_scope' + }, + options=[ + { + 'label': '全部数据权限', + 'value': '1' + }, + { + 'label': '自定义数据权限', + 'value': '2' + }, + { + 'label': '本部门数据权限', + 'value': '3' + },{ + 'label': '本部门及以下数据权限', + 'value': '4' + }, + { + 'label': '仅本人数据权限', + 'value': '5' + } + ], + placeholder='请选择权限范围', + style={ + 'width': 350 + } + ), + label='权限范围', + id={ + 'type': 'datascope-form-label', + 'index': 'data_scope' + }, + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + html.Div( + fac.AntdFormItem( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdCheckbox( + id='role-dept-perms-radio-fold-unfold', + label='展开/折叠' + ), + span=7, + ), + fac.AntdCol( + fac.AntdCheckbox( + id='role-dept-perms-radio-all-none', + label='全选/全不选' + ), + span=8, + ), + fac.AntdCol( + fac.AntdCheckbox( + id='role-dept-perms-radio-parent-children', + label='父子联动', + checked=True + ), + span=6, + ), + ], + style={ + 'paddingTop': '6px' + } + ), + fac.AntdRow( + fac.AntdCol( + html.Div( + [ + fac.AntdTree( + id='role-dept-perms', + treeData=[], + multiple=True, + checkable=True, + showLine=False, + selectable=False + ) + ], + style={ + 'border': 'solid 1px rgba(0, 0, 0, 0.2)', + 'border-radius': '5px', + 'width': 350 + } + ) + ), + style={ + 'paddingTop': '6px' + } + ), + ], + label='数据权限', + id='role-dept-perms-form-item', + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + id='role-dept-perms-div' + ) + ] + ) + ] -- Gitee