From 75f8d8616c01693ee31e6a6063eb52d5c4bdaf38 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Thu, 21 Sep 2023 10:24:05 +0800 Subject: [PATCH 1/5] =?UTF-8?q?chore:=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96f?= =?UTF-8?q?effery-antd-components=E7=89=88=E6=9C=AC=E4=B8=BA0.2.10rc17?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8DAntdTable=E5=BA=95=E5=B1=82bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7674277..f6cbbfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ email-validator==2.0.0.post2 et-xmlfile==1.1.0 fastapi==0.95.1 feffery-antd-charts==0.0.1rc17 -feffery-antd-components==0.2.9 +feffery-antd-components==0.2.10rc17 feffery-markdown-components==0.2.10 feffery-utils-components==0.1.28 Flask==2.2.5 @@ -50,7 +50,6 @@ Pillow==10.0.0 plotly==5.14.1 ply==3.11 psutil==5.9.5 -psycopg2==2.9.6 pyasn1==0.5.0 pycparser==2.21 pydantic==1.10.7 -- Gitee From 9ea238534e60d2128c82688927b97b147c78d151 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Thu, 21 Sep 2023 11:36:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=83=A8=E9=97=A8=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/module_admin/dao/dept_dao.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-fastapi-backend/module_admin/dao/dept_dao.py b/dash-fastapi-backend/module_admin/dao/dept_dao.py index e08f859..dc5098d 100644 --- a/dash-fastapi-backend/module_admin/dao/dept_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dept_dao.py @@ -197,7 +197,7 @@ class DeptDao: """ db_dept = SysDept(**dept.dict()) db.add(db_dept) - db.refresh(db_dept) # 刷新 + db.flush() return db_dept -- Gitee From 1c3bca8d1430f3415039441dd6518617312d6641 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Thu, 21 Sep 2023 11:52:55 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=AF=BC=E5=87=BAservice=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/module_admin/service/job_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-fastapi-backend/module_admin/service/job_service.py b/dash-fastapi-backend/module_admin/service/job_service.py index 571c18f..5f50e8c 100644 --- a/dash-fastapi-backend/module_admin/service/job_service.py +++ b/dash-fastapi-backend/module_admin/service/job_service.py @@ -167,7 +167,7 @@ class JobService: data = [JobModel(**vars(row)).dict() for row in job_list] job_group_list = await DictDataService.query_dict_data_list_from_cache_services(request.app.state.redis, dict_type='sys_job_group') - job_group_option = [dict(label=item.dict_label, value=item.dict_value) for item in job_group_list] + job_group_option = [dict(label=item.get('dict_label'), value=item.get('dict_value')) for item in job_group_list] job_group_option_dict = {item.get('value'): item for item in job_group_option} for item in data: -- Gitee From 3dcb5bdae64e9745916ee6fcd4c4cc94ea398a9c Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Thu, 21 Sep 2023 14:48:47 +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 | 10 +++++++--- demo-pictures/dashzsxq.jpg | Bin 0 -> 71242 bytes demo-pictures/wxcode.jpg | Bin 0 -> 89252 bytes 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 demo-pictures/dashzsxq.jpg create mode 100644 demo-pictures/wxcode.jpg diff --git a/README.md b/README.md index 04caeaf..4420359 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@

logo

-

Dash-FastAPI-Admin v1.0.4

+

Dash-FastAPI-Admin v1.0.5

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

- + @@ -130,10 +130,14 @@ python3 app.py ``` ## 交流与赞助 -如果有对本项目及FastAPI感兴趣的朋友,欢迎加入知识星球一起交流学习,让我们一起变得更强。如果你觉得这个项目帮助到了你,你可以请作者喝杯咖啡表示鼓励☕。 +如果有对本项目及FastAPI感兴趣的朋友,欢迎加入知识星球一起交流学习,让我们一起变得更强。如果你觉得这个项目帮助到了你,你可以请作者喝杯咖啡表示鼓励☕。扫描下面微信二维码添加微信备注DF-Admin即可进群,也欢迎大家加入dash大神费弗里的知识星球学习更多dash开发知识。 + + + +
zsxq zanzhu
wxcodedashzsxq
\ No newline at end of file diff --git a/demo-pictures/dashzsxq.jpg b/demo-pictures/dashzsxq.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2f7be556491f9c54ea01353dbf805d6a61d14fab GIT binary patch literal 71242 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 literal 0 HcmV?d00001 diff --git a/demo-pictures/wxcode.jpg b/demo-pictures/wxcode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d9a0b1d589e5c4dab0715704cb3f943d553def0 GIT binary patch literal 89252 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�ro9;YzqzbX}`hwS^gHk8;qR?Abz@n^Eg=q;8 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 ORZWhv&fZ|#_J05_Rvmi) literal 0 HcmV?d00001 -- Gitee From 93f8982e19223afd0d09719b772fad7140c1aabb Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Thu, 21 Sep 2023 14:49:51 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=E5=BC=95=E5=85=A5Dash=E7=89=B9?= =?UTF-8?q?=E6=80=A7flexible=20callback=20signatures=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=A4=8D=E6=9D=82=E7=9A=84=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=89=80=E6=9C=89=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=EF=BC=8C=E6=8F=90=E9=AB=98=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=8F=AF=E8=AF=BB=E6=80=A7=20#I825WG=20feat:?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=A1=A8=E5=8D=95=E5=8A=A8=E6=80=81=E5=9B=9E?= =?UTF-8?q?=E5=86=99=E5=8F=8A=E6=A0=A1=E9=AA=8C=EF=BC=8C=E5=B0=86=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E5=AD=97=E6=AE=B5id=E4=B8=8E=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=AD=97=E6=AE=B5=E5=90=8D=E4=B8=80=E4=B8=80=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E5=B9=B6=E8=AE=BE=E7=BD=AE=E6=A0=A1=E9=AA=8C=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=8D=B3=E5=8F=AF=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-frontend/app.py | 239 +++---- dash-fastapi-frontend/callbacks/forget_c.py | 182 +++--- .../callbacks/layout_c/fold_side_menu.py | 1 + dash-fastapi-frontend/callbacks/login_c.py | 168 ++--- .../callbacks/monitor_c/cache_c/control_c.py | 3 + .../callbacks/monitor_c/job_c/job_c.py | 571 +++++++++-------- .../callbacks/monitor_c/job_c/job_log_c.py | 149 +++-- .../callbacks/monitor_c/logininfor_c.py | 95 ++- .../callbacks/monitor_c/online_c.py | 69 +- .../callbacks/monitor_c/operlog_c.py | 155 +++-- .../callbacks/system_c/config_c.py | 347 ++++++----- .../callbacks/system_c/dept_c.py | 404 ++++++------ .../callbacks/system_c/dict_c/dict_c.py | 394 +++++++----- .../callbacks/system_c/dict_c/dict_data_c.py | 360 ++++++----- .../menu_c/components_c/button_type_c.py | 158 ++--- .../menu_c/components_c/content_type_c.py | 178 +++--- .../menu_c/components_c/menu_type_c.py | 186 +++--- .../callbacks/system_c/menu_c/menu_c.py | 249 +++++--- .../callbacks/system_c/notice_c.py | 331 ++++++---- .../callbacks/system_c/post_c.py | 343 +++++----- .../system_c/role_c/allocate_user_c.py | 33 +- .../callbacks/system_c/role_c/role_c.py | 427 +++++++------ .../system_c/user_c/allocate_role_c.py | 33 +- .../system_c/user_c/profile_c/avatar_c.py | 19 +- .../system_c/user_c/profile_c/reset_pwd_c.py | 11 +- .../system_c/user_c/profile_c/user_info_c.py | 11 +- .../callbacks/system_c/user_c/user_c.py | 588 +++++++++++------- .../views/monitor/job/__init__.py | 231 +++++-- .../views/monitor/job/job_log.py | 135 +++- .../views/monitor/operlog/__init__.py | 120 +++- .../views/system/config/__init__.py | 55 +- .../views/system/dept/__init__.py | 77 ++- .../views/system/dict/__init__.py | 44 +- .../views/system/dict/dict_data.py | 88 ++- .../views/system/post/__init__.py | 55 +- .../views/system/role/__init__.py | 65 +- .../views/system/user/__init__.py | 176 +++++- 37 files changed, 4123 insertions(+), 2627 deletions(-) diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index dd9d815..cf8cd50 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -76,20 +76,27 @@ app.layout = html.Div( @app.callback( - [Output('app-mount', 'children'), - Output('redirect-container', 'children', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('current-key-container', 'data'), - Output('menu-info-store-container', 'data'), - Output('menu-list-store-container', 'data'), - Output('search-panel', 'data')], - Input('url-container', 'pathname'), - [State('url-container', 'trigger'), - State('token-container', 'data')], + output=dict( + app_mount=Output('app-mount', 'children'), + redirect_container=Output('redirect-container', 'children', allow_duplicate=True), + global_message_container=Output('global-message-container', 'children', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + menu_current_key=Output('current-key-container', 'data'), + menu_info=Output('menu-info-store-container', 'data'), + menu_list=Output('menu-list-store-container', 'data'), + search_panel_data=Output('search-panel', 'data') + ), + inputs=dict(pathname=Input('url-container', 'pathname')), + state=dict( + url_trigger=State('url-container', 'trigger'), + session_token=State('token-container', 'data') + ), prevent_initial_call=True ) -def router(pathname, trigger, session_token): +def router(pathname, url_trigger, session_token): + """ + 全局路由回调 + """ # 检查当前会话是否已经登录 token_result = session.get('Authorization') # 若已登录 @@ -115,142 +122,136 @@ def router(pathname, trigger, session_token): current_key = '首页' if pathname == '/user/profile': current_key = '个人资料' - if trigger == 'load': + if url_trigger == 'load': # 根据pathname控制渲染行为 if pathname == '/login' or pathname == '/forget': # 重定向到主页面 - return [ - dash.no_update, - dcc.Location( - pathname='/', - id='router-redirect' - ), - None, - {'timestamp': time.time()}, - {'current_key': current_key}, - {'menu_info': menu_info}, - {'menu_list': menu_list}, - search_panel_data - ] + return dict( + app_mount=dash.no_update, + redirect_container=dcc.Location(pathname='/', id='router-redirect'), + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key={'current_key': current_key}, + menu_info={'menu_info': menu_info}, + menu_list={'menu_list': menu_list}, + search_panel_data=search_panel_data + ) # 否则正常渲染主页面 - return [ - views.layout.render_content(user_menu_info), - None, - fuc.FefferyFancyNotification('进入主页面', type='success', autoClose=2000), - {'timestamp': time.time()}, - {'current_key': current_key}, - {'menu_info': menu_info}, - {'menu_list': menu_list}, - search_panel_data - ] + return dict( + app_mount=views.layout.render_content(user_menu_info), + redirect_container=None, + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key={'current_key': current_key}, + menu_info={'menu_info': menu_info}, + menu_list={'menu_list': menu_list}, + search_panel_data=search_panel_data + ) else: - return [ - dash.no_update, - None, - None, - {'timestamp': time.time()}, - {'current_key': current_key}, - {'menu_info': menu_info}, - {'menu_list': menu_list}, - search_panel_data - ] + return dict( + app_mount=dash.no_update, + redirect_container=None, + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key={'current_key': current_key}, + menu_info={'menu_info': menu_info}, + menu_list={'menu_list': menu_list}, + search_panel_data=search_panel_data + ) else: # 渲染404状态页 - return [ - views.page_404.render_content(), - None, - None, - {'timestamp': time.time()}, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update - ] + return dict( + app_mount=views.page_404.render_content(), + redirect_container=None, + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key=dash.no_update, + menu_info=dash.no_update, + menu_list=dash.no_update, + search_panel_data=dash.no_update + ) else: - return [ - dash.no_update, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update - ] + return dict( + app_mount=dash.no_update, + redirect_container=dash.no_update, + global_message_container=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key=dash.no_update, + menu_info=dash.no_update, + menu_list=dash.no_update, + search_panel_data=dash.no_update + ) except Exception as e: print(e) - return [ - dash.no_update, - None, - fuc.FefferyFancyNotification('接口异常', type='error', autoClose=2000), - {'timestamp': time.time()}, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update - ] + return dict( + app_mount=dash.no_update, + redirect_container=None, + global_message_container=fuc.FefferyFancyNotification('接口异常', type='error', autoClose=2000), + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key=dash.no_update, + menu_info=dash.no_update, + menu_list=dash.no_update, + search_panel_data=dash.no_update + ) else: # 若未登录 # 根据pathname控制渲染行为 # 检验pathname合法性 if pathname not in RouterConfig.BASIC_VALID_PATHNAME: # 渲染404状态页 - return [ - views.page_404.render_content(), - None, - None, - {'timestamp': time.time()}, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update - ] + return dict( + app_mount=views.page_404.render_content(), + redirect_container=None, + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key=dash.no_update, + menu_info=dash.no_update, + menu_list=dash.no_update, + search_panel_data=dash.no_update + ) if pathname == '/login': - return [ - views.login.render_content(), - None, - None, - {'timestamp': time.time()}, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update - ] + return dict( + app_mount=views.login.render_content(), + redirect_container=None, + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key=dash.no_update, + menu_info=dash.no_update, + menu_list=dash.no_update, + search_panel_data=dash.no_update + ) if pathname == '/forget': - return [ - views.forget.render_forget_content(), - None, - None, - {'timestamp': time.time()}, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update - ] + return dict( + app_mount=views.forget.render_forget_content(), + redirect_container=None, + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key=dash.no_update, + menu_info=dash.no_update, + menu_list=dash.no_update, + search_panel_data=dash.no_update + ) # 否则重定向到登录页 - return [ - dash.no_update, - dcc.Location( - pathname='/login', - id='router-redirect' - ), - None, - {'timestamp': time.time()}, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update - ] + return dict( + app_mount=dash.no_update, + redirect_container=dcc.Location(pathname='/login', id='router-redirect'), + global_message_container=None, + api_check_token_trigger={'timestamp': time.time()}, + menu_current_key=dash.no_update, + menu_info=dash.no_update, + menu_list=dash.no_update, + search_panel_data=dash.no_update + ) if __name__ == '__main__': diff --git a/dash-fastapi-frontend/callbacks/forget_c.py b/dash-fastapi-frontend/callbacks/forget_c.py index 9527ce3..7b12a2b 100644 --- a/dash-fastapi-frontend/callbacks/forget_c.py +++ b/dash-fastapi-frontend/callbacks/forget_c.py @@ -2,6 +2,7 @@ import dash from dash import dcc import feffery_utils_components as fuc from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate from server import app from api.user import forget_user_pwd_api @@ -9,26 +10,32 @@ from api.message import send_message_api @app.callback( - [Output('forget-username-form-item', 'validateStatus'), - Output('forget-password-form-item', 'validateStatus'), - Output('forget-password-again-form-item', 'validateStatus'), - Output('forget-captcha-form-item', 'validateStatus'), - Output('forget-username-form-item', 'help'), - Output('forget-password-form-item', 'help'), - Output('forget-password-again-form-item', 'help'), - Output('forget-captcha-form-item', 'help'), - Output('forget-submit', 'loading'), - Output('redirect-container', 'children', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('forget-submit', 'nClicks'), - [State('forget-username', 'value'), - State('forget-password', 'value'), - State('forget-password-again', 'value'), - State('forget-input-captcha', 'value'), - State('sms_code-session_id-container', 'data')], + output=dict( + username_form_status=Output('forget-username-form-item', 'validateStatus'), + password_form_status=Output('forget-password-form-item', 'validateStatus'), + password_again_form_status=Output('forget-password-again-form-item', 'validateStatus'), + captcha_form_status=Output('forget-captcha-form-item', 'validateStatus'), + username_form_help=Output('forget-username-form-item', 'help'), + password_form_help=Output('forget-password-form-item', 'help'), + password_again_form_help=Output('forget-password-again-form-item', 'help'), + captcha_form_help=Output('forget-captcha-form-item', 'help'), + submit_loading=Output('forget-submit', 'loading'), + redirect_container=Output('redirect-container', 'children', allow_duplicate=True), + global_message_container=Output('global-message-container', 'children', allow_duplicate=True) + ), + inputs=dict( + nClicks=Input('forget-submit', 'nClicks') + ), + state=dict( + username=State('forget-username', 'value'), + password=State('forget-password', 'value'), + password_again=State('forget-password-again', 'value'), + input_captcha=State('forget-input-captcha', 'value'), + session_id=State('sms_code-session_id-container', 'data') + ), prevent_initial_call=True ) -def login_auth(nClicks, username, password, password_again, input_captcha, session_id): +def forget_auth(nClicks, username, password, password_again, input_captcha, session_id): if nClicks: # 校验全部输入值是否不为空 if all([username, password, password_again, input_captcha]): @@ -39,84 +46,81 @@ def login_auth(nClicks, username, password, password_again, input_captcha, sessi change_result = forget_user_pwd_api(forget_params) if change_result.get('code') == 200: - return [ - None, - None, - None, - None, - None, - None, - None, - None, - True, - dcc.Location( - pathname='/login', - id='forget-redirect' - ), - fuc.FefferyFancyMessage(change_result.get('message'), type='success') - ] + return dict( + username_form_status=None, + password_form_status=None, + password_again_form_status=None, + captcha_form_status=None, + username_form_help=None, + password_form_help=None, + password_again_form_help=None, + captcha_form_help=None, + submit_loading=False, + redirect_container=dcc.Location(pathname='/login', id='forget-redirect'), + global_message_container=fuc.FefferyFancyMessage(change_result.get('message'), type='success') + ) else: - return [ - None, - None, - None, - None, - None, - None, - None, - None, - False, - None, - fuc.FefferyFancyMessage(change_result.get('message'), type='error') - ] + return dict( + username_form_status=None, + password_form_status=None, + password_again_form_status=None, + captcha_form_status=None, + username_form_help=None, + password_form_help=None, + password_again_form_help=None, + captcha_form_help=None, + submit_loading=False, + redirect_container=None, + global_message_container=fuc.FefferyFancyMessage(change_result.get('message'), type='error') + ) except Exception as e: - return [ - None, - None, - None, - None, - None, - None, - None, - None, - False, - None, - fuc.FefferyFancyMessage(str(e), type='error') - ] + return dict( + username_form_status=None, + password_form_status=None, + password_again_form_status=None, + captcha_form_status=None, + username_form_help=None, + password_form_help=None, + password_again_form_help=None, + captcha_form_help=None, + submit_loading=False, + redirect_container=None, + global_message_container=fuc.FefferyFancyMessage(str(e), type='error') + ) else: - return [ - None, - 'error', - 'error', - None, - None, - '两次密码不一致', - '两次密码不一致', - None, - False, - None, - None - ] - - return [ - None if username else 'error', - None if password else 'error', - None if password_again else 'error', - None if input_captcha else 'error', - None if username else '请输入用户名!', - None if password else '请输入新密码!', - None if password_again else '请再次输入新密码!', - None if input_captcha else '请输入短信验证码!', - False, - None, - None - ] - - return [dash.no_update] * 11 + return dict( + username_form_status=None, + password_form_status='error', + password_again_form_status='error', + captcha_form_status=None, + username_form_help=None, + password_form_help='两次密码不一致', + password_again_form_help='两次密码不一致', + captcha_form_help=None, + submit_loading=False, + redirect_container=None, + global_message_container=None + ) + + return dict( + username_form_status=None if username else 'error', + password_form_status=None if password else 'error', + password_again_form_status=None if password_again else 'error', + captcha_form_status=None if input_captcha else 'error', + username_form_help=None if username else '请输入用户名!', + password_form_help=None if password else '请输入新密码!', + password_again_form_help=None if password_again else '请再次输入新密码!', + captcha_form_help=None if input_captcha else '请输入短信验证码!', + submit_loading=False, + redirect_container=None, + global_message_container=None + ) + + raise PreventUpdate @app.callback( diff --git a/dash-fastapi-frontend/callbacks/layout_c/fold_side_menu.py b/dash-fastapi-frontend/callbacks/layout_c/fold_side_menu.py index eed62bb..5096ca7 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/fold_side_menu.py +++ b/dash-fastapi-frontend/callbacks/layout_c/fold_side_menu.py @@ -3,6 +3,7 @@ from dash.dependencies import Input, Output, State from server import app +# 侧边栏折叠回调 app.clientside_callback( ''' (nClicks, collapsed) => { diff --git a/dash-fastapi-frontend/callbacks/login_c.py b/dash-fastapi-frontend/callbacks/login_c.py index 465e1cf..d423bd0 100644 --- a/dash-fastapi-frontend/callbacks/login_c.py +++ b/dash-fastapi-frontend/callbacks/login_c.py @@ -10,24 +10,30 @@ from api.login import login_api, get_captcha_image_api @app.callback( - [Output('login-username-form-item', 'validateStatus'), - Output('login-password-form-item', 'validateStatus'), - Output('login-captcha-form-item', 'validateStatus'), - Output('login-username-form-item', 'help'), - Output('login-password-form-item', 'help'), - Output('login-captcha-form-item', 'help'), - Output('login-captcha-image-container', 'n_clicks'), - Output('login-submit', 'loading'), - Output('token-container', 'data'), - Output('redirect-container', 'children', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('login-submit', 'nClicks'), - [State('login-username', 'value'), - State('login-password', 'value'), - State('login-captcha', 'value'), - State('captcha_image-session_id-container', 'data'), - State('login-captcha-image-container', 'n_clicks'), - State('captcha-row-container', 'hidden')], + output=dict( + username_form_status=Output('login-username-form-item', 'validateStatus'), + password_form_status=Output('login-password-form-item', 'validateStatus'), + captcha_form_status=Output('login-captcha-form-item', 'validateStatus'), + username_form_help=Output('login-username-form-item', 'help'), + password_form_help=Output('login-password-form-item', 'help'), + captcha_form_help=Output('login-captcha-form-item', 'help'), + image_click=Output('login-captcha-image-container', 'n_clicks'), + submit_loading=Output('login-submit', 'loading'), + token=Output('token-container', 'data'), + redirect_container=Output('redirect-container', 'children', allow_duplicate=True), + global_message_container=Output('global-message-container', 'children', allow_duplicate=True) + ), + inputs=dict( + nClicks=Input('login-submit', 'nClicks') + ), + state=dict( + username=State('login-username', 'value'), + password=State('login-password', 'value'), + input_captcha=State('login-captcha', 'value'), + session_id=State('captcha_image-session_id-container', 'data'), + image_click=State('login-captcha-image-container', 'n_clicks'), + captcha_hidden=State('captcha-row-container', 'hidden') + ), prevent_initial_call=True ) def login_auth(nClicks, username, password, input_captcha, session_id, image_click, captcha_hidden): @@ -43,70 +49,78 @@ def login_auth(nClicks, username, password, input_captcha, session_id, image_cli if userinfo_result['code'] == 200: token = userinfo_result['data']['access_token'] session['Authorization'] = token - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - False, - token, - dcc.Location( - pathname='/', - id='login-redirect' - ), - fuc.FefferyFancyMessage('登录成功', type='success'), - ] + return dict( + username_form_status=None, + password_form_status=None, + captcha_form_status=None, + username_form_help=None, + password_form_help=None, + captcha_form_help=None, + image_click=dash.no_update, + submit_loading=False, + token=token, + redirect_container=dcc.Location(pathname='/', id='login-redirect'), + global_message_container=fuc.FefferyFancyMessage('登录成功', type='success') + ) else: - return [ - None, - None, - None, - None, - None, - None, - image_click + 1, - False, - None, - None, - fuc.FefferyFancyMessage(userinfo_result.get('message'), type='error'), - ] + return dict( + username_form_status=None, + password_form_status=None, + captcha_form_status=None, + username_form_help=None, + password_form_help=None, + captcha_form_help=None, + image_click=image_click + 1, + submit_loading=False, + token=None, + redirect_container=None, + global_message_container=fuc.FefferyFancyMessage(userinfo_result.get('message'), type='error') + ) except Exception as e: print(e) - return [ - None, - None, - None, - None, - None, - None, - image_click + 1, - False, - None, - None, - fuc.FefferyFancyMessage('接口异常', type='error'), - ] - - return [ - None if username else 'error', - None if password else 'error', - None if input_captcha else 'error', - None if username else '请输入用户名!', - None if password else '请输入密码!', - None if input_captcha else '请输入验证码!', - dash.no_update, - False, - None, - None, - None - ] - - return [dash.no_update] * 6 + [image_click + 1] + [dash.no_update] * 4 + return dict( + username_form_status=None, + password_form_status=None, + captcha_form_status=None, + username_form_help=None, + password_form_help=None, + captcha_form_help=None, + image_click=image_click + 1, + submit_loading=False, + token=None, + redirect_container=None, + global_message_container=fuc.FefferyFancyMessage('接口异常', type='error') + ) + + return dict( + username_form_status=None if username else 'error', + password_form_status=None if password else 'error', + captcha_form_status=None if input_captcha else 'error', + username_form_help=None if username else '请输入用户名!', + password_form_help=None if password else '请输入密码!', + captcha_form_help=None if input_captcha else '请输入验证码!', + image_click=dash.no_update, + submit_loading=False, + token=None, + redirect_container=None, + global_message_container=None + ) + + return dict( + username_form_status=dash.no_update, + password_form_status=dash.no_update, + captcha_form_status=dash.no_update, + username_form_help=dash.no_update, + password_form_help=dash.no_update, + captcha_form_help=dash.no_update, + image_click=image_click + 1, + submit_loading=dash.no_update, + token=dash.no_update, + redirect_container=dash.no_update, + global_message_container=dash.no_update + ) @app.callback( diff --git a/dash-fastapi-frontend/callbacks/monitor_c/cache_c/control_c.py b/dash-fastapi-frontend/callbacks/monitor_c/cache_c/control_c.py index 16afc77..d735092 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/cache_c/control_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/cache_c/control_c.py @@ -3,6 +3,7 @@ from dash.dependencies import Input, Output, State, ClientsideFunction from server import app +# 初始化echarts图表数据 app.clientside_callback( ''' (n_intervals, data) => { @@ -17,6 +18,7 @@ app.clientside_callback( ) +# 渲染命令统计图表 app.clientside_callback( ClientsideFunction( namespace='clientside_command_stats', @@ -27,6 +29,7 @@ app.clientside_callback( ) +# 渲染内存信息统计图表 app.clientside_callback( ClientsideFunction( namespace='clientside_memory', diff --git a/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_c.py b/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_c.py index 21fbb84..d0eb60a 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_c.py @@ -4,6 +4,7 @@ import uuid import json from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -12,23 +13,32 @@ from api.dict import query_dict_data_list_api @app.callback( - [Output('job-list-table', 'data', allow_duplicate=True), - Output('job-list-table', 'pagination', allow_duplicate=True), - Output('job-list-table', 'key'), - Output('job-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('job-search', 'nClicks'), - Input('job-refresh', 'nClicks'), - Input('job-list-table', 'pagination'), - Input('job-operations-store', 'data')], - [State('job-job_name-input', 'value'), - State('job-job_group-select', 'value'), - State('job-status-select', 'value'), - State('job-button-perms-container', 'data')], + output=dict( + job_table_data=Output('job-list-table', 'data', allow_duplicate=True), + job_table_pagination=Output('job-list-table', 'pagination', allow_duplicate=True), + job_table_key=Output('job-list-table', 'key'), + job_table_selectedrowkeys=Output('job-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('job-search', 'nClicks'), + refresh_click=Input('job-refresh', 'nClicks'), + pagination=Input('job-list-table', 'pagination'), + operations=Input('job-operations-store', 'data') + ), + state=dict( + job_name=State('job-job_name-input', 'value'), + job_group=State('job-job_group-select', 'value'), + status_select=State('job-status-select', 'value'), + button_perms=State('job-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_job_table_data(search_click, refresh_click, pagination, operations, job_name, job_group, status_select, button_perms): + """ + 获取定时任务表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( job_name=job_name, job_group=job_group, @@ -101,13 +111,26 @@ def get_job_table_data(search_click, refresh_click, pagination, operations, job_ } if 'monitor:job:query' in button_perms else None ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + job_table_data=table_data, + job_table_pagination=table_pagination, + job_table_key=str(uuid.uuid4()), + job_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + job_table_data=dash.no_update, + job_table_pagination=dash.no_update, + job_table_key=dash.no_update, + job_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置定时任务搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -126,6 +149,7 @@ app.clientside_callback( ) +# 隐藏/显示定时任务搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -152,6 +176,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_job_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -162,7 +189,7 @@ def change_job_edit_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -171,64 +198,77 @@ def change_job_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_job_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( - [Output('job-modal', 'visible', allow_duplicate=True), - Output('job-modal', 'title'), - Output('job-job_name', 'value'), - Output('job-job_group', 'value'), - Output('job-invoke_target', 'value'), - Output('job-cron_expression', 'value'), - Output('job-job_args', 'value'), - Output('job-job_kwargs', 'value'), - Output('job-misfire_policy', 'value'), - Output('job-concurrent', 'value'), - Output('job-status', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('job-edit-id-store', 'data'), - Output('job-operations-store-bk', 'data')], - [Input({'type': 'job-operation-button', 'index': ALL}, 'nClicks'), - Input('job-list-table', 'nClicksDropdownItem')], - [State('job-list-table', 'selectedRowKeys'), - State('job-list-table', 'recentlyClickedDropdownItemTitle'), - State('job-list-table', 'recentlyDropdownItemClickedRow')], + output=dict( + modal_visible=Output('job-modal', 'visible', allow_duplicate=True), + modal_title=Output('job-modal', 'title'), + form_value=Output({'type': 'job-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'job-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'job-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('job-edit-id-store', 'data'), + modal_type=Output('job-operations-store-bk', 'data') + ), + inputs=dict( + operation_click=Input({'type': 'job-operation-button', 'index': ALL}, 'nClicks'), + dropdown_click=Input('job-list-table', 'nClicksDropdownItem') + ), + state=dict( + selected_row_keys=State('job-list-table', 'selectedRowKeys'), + recently_clicked_dropdown_item_title=State('job-list-table', 'recentlyClickedDropdownItemTitle'), + recently_dropdown_item_clicked_row=State('job-list-table', 'recentlyDropdownItemClickedRow') + ), prevent_initial_call=True ) def add_edit_job_modal(operation_click, dropdown_click, selected_row_keys, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + """ + 显示新增或编辑定时任务弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'add', 'type': 'job-operation-button'} \ or trigger_id == {'index': 'edit', 'type': 'job-operation-button'} \ or (trigger_id == 'job-list-table' and recently_clicked_dropdown_item_title == '修改'): + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] if trigger_id == {'index': 'add', 'type': 'job-operation-button'}: - return [ - True, - '新增任务', - None, - None, - None, - None, - None, - None, - '1', - '1', - '0', - dash.no_update, - None, - {'type': 'add'} - ] + job_info = dict( + job_name=None, + job_group=None, + invoke_target=None, + cron_expression=None, + job_args=None, + job_kwargs=None, + misfire_policy='1', + concurrent='1', + status='0' + ) + return dict( + modal_visible=True, + modal_title='新增任务', + form_value=[job_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger=dash.no_update, + edit_row_info=None, + modal_type={'type': 'add'} + ) elif trigger_id == {'index': 'edit', 'type': 'job-operation-button'} or (trigger_id == 'job-list-table' and recently_clicked_dropdown_item_title == '修改'): if trigger_id == {'index': 'edit', 'type': 'job-operation-button'}: job_id = int(','.join(selected_row_keys)) @@ -237,125 +277,112 @@ def add_edit_job_modal(operation_click, dropdown_click, selected_row_keys, recen job_info_res = get_job_detail_api(job_id=job_id) if job_info_res['code'] == 200: job_info = job_info_res['data'] - return [ - True, - '编辑任务', - job_info.get('job_name'), - job_info.get('job_group'), - job_info.get('invoke_target'), - job_info.get('cron_expression'), - job_info.get('job_args'), - job_info.get('job_kwargs'), - job_info.get('misfire_policy'), - job_info.get('concurrent'), - job_info.get('status'), - {'timestamp': time.time()}, - job_info if job_info else None, - {'type': 'edit'} - ] - - return [dash.no_update] * 11 + [{'timestamp': time.time()}, None, None] + return dict( + modal_visible=True, + modal_title='编辑任务', + form_value=[job_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=job_info if job_info else None, + modal_type={'type': 'edit'} + ) + + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) - return [dash.no_update] * 12 + [None, None] + raise PreventUpdate @app.callback( - [Output('job-job_name-form-item', 'validateStatus'), - Output('job-invoke_target-form-item', 'validateStatus'), - Output('job-cron_expression-form-item', 'validateStatus'), - Output('job-job_name-form-item', 'help'), - Output('job-invoke_target-form-item', 'help'), - Output('job-cron_expression-form-item', 'help'), - Output('job-modal', 'visible'), - Output('job-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('job-modal', 'okCounts'), - [State('job-operations-store-bk', 'data'), - State('job-edit-id-store', 'data'), - State('job-job_name', 'value'), - State('job-job_group', 'value'), - State('job-invoke_target', 'value'), - State('job-cron_expression', 'value'), - State('job-job_args', 'value'), - State('job-job_kwargs', 'value'), - State('job-misfire_policy', 'value'), - State('job-concurrent', 'value'), - State('job-status', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'job-form-label', 'index': ALL, 'required': True}, 'validateStatus', + allow_duplicate=True), + form_label_validate_info=Output({'type': 'job-form-label', 'index': ALL, 'required': True}, 'help', + allow_duplicate=True), + modal_visible=Output('job-modal', 'visible'), + operations=Output('job-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('job-modal', 'okCounts') + ), + state=dict( + modal_type=State('job-operations-store-bk', 'data'), + edit_row_info=State('job-edit-id-store', 'data'), + form_value=State({'type': 'job-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'job-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def job_confirm(confirm_trigger, operation_type, cur_job_info, job_name, job_group, invoke_target, cron_expression, - job_args, job_kwargs, misfire_policy, concurrent, status): +def job_confirm(confirm_trigger, modal_type, edit_row_info, form_value, form_label): + """ + 新增或编定时任务弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: - if all([job_name, invoke_target, cron_expression]): - params_add = dict(job_name=job_name, job_group=job_group, invoke_target=invoke_target, - cron_expression=cron_expression, job_args=job_args, job_kwargs=job_kwargs, - misfire_policy=misfire_policy, concurrent=concurrent, status=status) - params_edit = dict(job_id=cur_job_info.get('job_id') if cur_job_info else None, job_name=job_name, - job_group=job_group, invoke_target=invoke_target, cron_expression=cron_expression, - job_args=job_args, job_kwargs=job_kwargs, - misfire_policy=misfire_policy, concurrent=concurrent, status=status) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + if all([form_value_state.get(k) for k in form_label_output_list]): + params_add = form_value_state + params_edit = params_add.copy() + params_edit['job_id'] = edit_row_info.get('job_id') if edit_row_info else None api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_job_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_job_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] + if modal_type == 'add': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [ - None if job_name else 'error', - None if invoke_target else 'error', - None if cron_expression else 'error', - None if job_name else '请输入任务名称!', - None if invoke_target else '请输入调用目标字符串!', - None if cron_expression else '请输入cron执行表达式!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 10 + raise PreventUpdate @app.callback( @@ -368,6 +395,9 @@ def job_confirm(confirm_trigger, operation_type, cur_job_info, job_name, job_gro prevent_initial_call=True ) def table_switch_job_status(recently_switch_data_index, recently_switch_status, recently_switch_row): + """ + 表格内切换定时任务状态回调 + """ if recently_switch_data_index: if recently_switch_status: params = dict(job_id=int(recently_switch_row['key']), status='0', type='status') @@ -383,109 +413,89 @@ def table_switch_job_status(recently_switch_data_index, recently_switch_status, ] return [ - dash.no_update, + {'type': 'switch-status'}, {'timestamp': time.time()}, fuc.FefferyFancyMessage('修改失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( - [Output('job_detail-modal', 'visible', allow_duplicate=True), - Output('job_detail-modal', 'title'), - Output('job_detail-job_name-text', 'children'), - Output('job_detail-job_group-text', 'children'), - Output('job_detail-job_executor-text', 'children'), - Output('job_detail-invoke_target-text', 'children'), - Output('job_detail-job_args-text', 'children'), - Output('job_detail-job_kwargs-text', 'children'), - Output('job_detail-cron_expression-text', 'children'), - Output('job_detail-misfire_policy-text', 'children'), - Output('job_detail-concurrent-text', 'children'), - Output('job_detail-status-text', 'children'), - Output('job_detail-create_time-text', 'children'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('job-list-table', 'nClicksDropdownItem'), - [State('job-list-table', 'recentlyClickedDropdownItemTitle'), - State('job-list-table', 'recentlyDropdownItemClickedRow')], + output=dict( + modal_visible=Output('job_detail-modal', 'visible', allow_duplicate=True), + modal_title=Output('job_detail-modal', 'title'), + form_value=Output({'type': 'job_detail-form-value', 'index': ALL}, 'children'), + 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( + dropdown_click=Input('job-list-table', 'nClicksDropdownItem') + ), + state=dict( + recently_clicked_dropdown_item_title=State('job-list-table', 'recentlyClickedDropdownItemTitle'), + recently_dropdown_item_clicked_row=State('job-list-table', 'recentlyDropdownItemClickedRow') + ), prevent_initial_call=True ) def get_job_detail_modal(dropdown_click, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + """ + 显示定时任务详情弹窗回调及执行一次定时任务回调 + """ + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[-3]] + # 显示定时任务详情弹窗 if dropdown_click and recently_clicked_dropdown_item_title == '任务详细': job_id = int(recently_dropdown_item_clicked_row['key']) job_info_res = get_job_detail_api(job_id=job_id) if job_info_res['code'] == 200: job_info = job_info_res['data'] if job_info.get('misfire_policy') == '1': - misfire_policy = '立即执行' + job_info['misfire_policy'] = '立即执行' elif job_info.get('misfire_policy') == '2': - misfire_policy = '执行一次' + job_info['misfire_policy'] = '执行一次' else: - misfire_policy = '放弃执行' - return [ - True, - '任务详情', - job_info.get('job_name'), - job_info.get('job_group'), - job_info.get('job_executor'), - job_info.get('invoke_target'), - job_info.get('job_args'), - job_info.get('job_kwargs'), - job_info.get('cron_expression'), - misfire_policy, - '是' if job_info.get('concurrent') == '0' else '否', - '正常' if job_info.get('status') == '0' else '停用', - job_info.get('create_time'), - {'timestamp': time.time()}, - None - ] + job_info['misfire_policy'] = '放弃执行' + job_info['concurrent'] = '是' if job_info.get('concurrent') == '0' else '否' + job_info['status'] = '正常' if job_info.get('status') == '0' else '停用' + return dict( + modal_visible=True, + modal_title='任务详情', + form_value=[job_info.get(k) for k in form_value_list], + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=None + ) - return [dash.no_update] * 13 + [{'timestamp': time.time()}, None] + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=None + ) + # 执行一次定时任务 if dropdown_click and recently_clicked_dropdown_item_title == '执行一次': job_id = int(recently_dropdown_item_clicked_row['key']) job_info_res = execute_job_api(dict(job_id=job_id)) if job_info_res['code'] == 200: + return dict( + modal_visible=False, + modal_title=None, + form_value=[None] * len(form_value_list), + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('执行成功', type='success') + ) - return [ - False, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('执行成功', type='success') - ] - - return [ - False, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('执行失败', type='success') - ] + return dict( + modal_visible=False, + modal_title=None, + form_value=[None] * len(form_value_list), + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('执行失败', type='error') + ) - return [dash.no_update] * 15 + raise PreventUpdate @app.callback( @@ -501,6 +511,9 @@ def get_job_detail_modal(dropdown_click, recently_clicked_dropdown_item_title, r ) def job_delete_modal(operation_click, dropdown_click, selected_row_keys, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + """ + 显示删除定时任务二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'job-operation-button'} or ( trigger_id == 'job-list-table' and recently_clicked_dropdown_item_title == '删除'): @@ -519,7 +532,7 @@ def job_delete_modal(operation_click, dropdown_click, {'job_ids': job_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -531,6 +544,9 @@ def job_delete_modal(operation_click, dropdown_click, prevent_initial_call=True ) def job_delete_confirm(delete_confirm, job_ids_data): + """ + 删除定时任务弹窗确认回调,实现删除操作 + """ if delete_confirm: params = job_ids_data @@ -548,24 +564,33 @@ def job_delete_confirm(delete_confirm, job_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( - [Output('job_to_job_log-modal', 'visible'), - Output('job_to_job_log-modal', 'title'), - Output('job_log-job_name-input', 'value', allow_duplicate=True), - Output('job_log-job_group-select', 'options'), - Output('job_log-search', 'nClicks'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input({'type': 'job-operation-log', 'index': ALL}, 'nClicks'), - Input('job-list-table', 'nClicksDropdownItem')], - [State('job-list-table', 'recentlyClickedDropdownItemTitle'), - State('job-list-table', 'recentlyDropdownItemClickedRow'), - State('job_log-search', 'nClicks')], + output=dict( + job_log_modal_visible=Output('job_to_job_log-modal', 'visible'), + job_log_modal_title=Output('job_to_job_log-modal', 'title'), + job_log_job_name=Output('job_log-job_name-input', 'value', allow_duplicate=True), + job_log_job_group_options=Output('job_log-job_group-select', 'options'), + job_log_search_nclick=Output('job_log-search', 'nClicks'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + operation_click=Input({'type': 'job-operation-log', 'index': ALL}, 'nClicks'), + dropdown_click=Input('job-list-table', 'nClicksDropdownItem') + ), + state=dict( + recently_clicked_dropdown_item_title=State('job-list-table', 'recentlyClickedDropdownItemTitle'), + recently_dropdown_item_clicked_row=State('job-list-table', 'recentlyDropdownItemClickedRow'), + job_log_search_nclick=State('job_log-search', 'nClicks') + ), prevent_initial_call=True ) def job_to_job_log_modal(operation_click, dropdown_click, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row, job_log_search_nclick): + """ + 显示定时任务对应调度日志表格弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'log', 'type': 'job-operation-log'} or (trigger_id == 'job-list-table' and recently_clicked_dropdown_item_title == '调度日志'): @@ -576,25 +601,25 @@ def job_to_job_log_modal(operation_click, dropdown_click, recently_clicked_dropd option_table = [dict(label=item.get('dict_label'), value=item.get('dict_value')) for item in data] if trigger_id == 'job-list-table' and recently_clicked_dropdown_item_title == '调度日志': - return [ - True, - '任务调度日志', - recently_dropdown_item_clicked_row.get('job_name'), - option_table, - job_log_search_nclick + 1 if job_log_search_nclick else 1, - {'timestamp': time.time()}, - ] - - return [ - True, - '任务调度日志', - None, - option_table, - job_log_search_nclick + 1 if job_log_search_nclick else 1, - {'timestamp': time.time()}, - ] + return dict( + job_log_modal_visible=True, + job_log_modal_title='任务调度日志', + job_log_job_name=recently_dropdown_item_clicked_row.get('job_name'), + job_log_job_group_options=option_table, + job_log_search_nclick=job_log_search_nclick + 1 if job_log_search_nclick else 1, + api_check_token_trigger={'timestamp': time.time()} + ) + + return dict( + job_log_modal_visible=True, + job_log_modal_title='任务调度日志', + job_log_job_name=None, + job_log_job_group_options=option_table, + job_log_search_nclick=job_log_search_nclick + 1 if job_log_search_nclick else 1, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 6 + raise PreventUpdate @app.callback( @@ -606,6 +631,9 @@ def job_to_job_log_modal(operation_click, dropdown_click, recently_clicked_dropd prevent_initial_call=True ) def export_job_list(export_click): + """ + 导出定时任务信息回调 + """ if export_click: export_job_res = export_job_list_api({}) if export_job_res.status_code == 200: @@ -625,7 +653,7 @@ def export_job_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -634,8 +662,11 @@ def export_job_list(export_click): prevent_initial_call=True ) def reset_job_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_log_c.py b/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_log_c.py index 61e0540..7b827ac 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_log_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/job_c/job_log_c.py @@ -4,6 +4,7 @@ import uuid import json from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -12,23 +13,32 @@ from api.dict import query_dict_data_list_api @app.callback( - [Output('job_log-list-table', 'data', allow_duplicate=True), - Output('job_log-list-table', 'pagination', allow_duplicate=True), - Output('job_log-list-table', 'key'), - Output('job_log-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('job_log-search', 'nClicks'), - Input('job_log-refresh', 'nClicks'), - Input('job_log-list-table', 'pagination'), - Input('job_log-operations-store', 'data')], - [State('job_log-job_name-input', 'value'), - State('job_log-job_group-select', 'value'), - State('job_log-status-select', 'value'), - State('job_log-create_time-range', 'value'), - State('job_log-button-perms-container', 'data')], + output=dict( + job_log_table_data=Output('job_log-list-table', 'data', allow_duplicate=True), + job_log_table_pagination=Output('job_log-list-table', 'pagination', allow_duplicate=True), + job_log_table_key=Output('job_log-list-table', 'key'), + job_log_table_selectedrowkeys=Output('job_log-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('job_log-search', 'nClicks'), + refresh_click=Input('job_log-refresh', 'nClicks'), + pagination=Input('job_log-list-table', 'pagination'), + operations=Input('job_log-operations-store', 'data') + ), + state=dict( + job_name=State('job_log-job_name-input', 'value'), + job_group=State('job_log-job_group-select', 'value'), + status_select=State('job_log-status-select', 'value'), + create_time_range=State('job_log-create_time-range', 'value'), + button_perms=State('job_log-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_job_log_table_data(search_click, refresh_click, pagination, operations, job_name, job_group, status_select, create_time_range, button_perms): + """ + 获取定时任务对应调度日志表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ create_time_start = None create_time_end = None @@ -93,13 +103,26 @@ def get_job_log_table_data(search_click, refresh_click, pagination, operations, } if 'monitor:job:query' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + job_log_table_data=table_data, + job_log_table_pagination=table_pagination, + job_log_table_key=str(uuid.uuid4()), + job_log_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + job_log_table_data=dash.no_update, + job_log_table_pagination=dash.no_update, + job_log_table_key=dash.no_update, + job_log_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置定时任务调度日志搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -119,6 +142,7 @@ app.clientside_callback( ) +# 隐藏/显示定时任务调度日志搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -140,51 +164,45 @@ app.clientside_callback( @app.callback( - [Output('job_log-modal', 'visible', allow_duplicate=True), - Output('job_log-modal', 'title'), - Output('job_log-job_name-text', 'children'), - Output('job_log-job_group-text', 'children'), - Output('job_log-job_executor-text', 'children'), - Output('job_log-invoke_target-text', 'children'), - Output('job_log-job_args-text', 'children'), - Output('job_log-job_kwargs-text', 'children'), - Output('job_log-job_trigger-text', 'children'), - Output('job_log-job_message-text', 'children'), - Output('job_log-status-text', 'children'), - Output('job_log-create_time-text', 'children'), - Output('job_log-exception_info-text', 'children'), - Output('api-check-token', 'data', allow_duplicate=True)], - Input('job_log-list-table', 'nClicksButton'), - [State('job_log-list-table', 'clickedContent'), - State('job_log-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal_visible=Output('job_log-modal', 'visible', allow_duplicate=True), + modal_title=Output('job_log-modal', 'title'), + form_value=Output({'type': 'job_log-form-value', 'index': ALL}, 'children'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + button_click=Input('job_log-list-table', 'nClicksButton') + ), + state=dict( + clicked_content=State('job_log-list-table', 'clickedContent'), + recently_button_clicked_row=State('job_log-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_job_log_modal(button_click, clicked_content, recently_button_clicked_row): if button_click: + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[-2]] job_log_id = int(recently_button_clicked_row['key']) job_log_info_res = get_job_log_detail_api(job_log_id=job_log_id) if job_log_info_res['code'] == 200: job_log_info = job_log_info_res['data'] - return [ - True, - '任务执行日志详情', - job_log_info.get('job_name'), - job_log_info.get('job_group'), - job_log_info.get('job_executor'), - job_log_info.get('invoke_target'), - job_log_info.get('job_args'), - job_log_info.get('job_kwargs'), - job_log_info.get('job_trigger'), - job_log_info.get('job_message'), - '成功' if job_log_info.get('status') == '0' else '失败', - job_log_info.get('create_time'), - job_log_info.get('exception_info'), - {'timestamp': time.time()}, - ] + job_log_info['status'] = '成功' if job_log_info.get('status') == '0' else '失败' + return dict( + modal_visible=True, + modal_title='任务执行日志详情', + form_value=[job_log_info.get(k) for k in form_value_list], + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 13 + [{'timestamp': time.time()}] + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 14 + raise PreventUpdate @app.callback( @@ -193,17 +211,18 @@ def add_edit_job_log_modal(button_click, clicked_content, recently_button_clicke prevent_initial_call=True ) def change_job_log_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -215,6 +234,9 @@ def change_job_log_delete_button_status(table_rows_selected): prevent_initial_call=True ) def job_log_delete_modal(operation_click, selected_row_keys): + """ + 显示删除或清空定时任务调度日志二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.index in ['delete', 'clear']: if trigger_id.index == 'delete': @@ -233,7 +255,7 @@ def job_log_delete_modal(operation_click, selected_row_keys): {'oper_type': 'clear', 'job_log_ids': ''} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -245,6 +267,9 @@ def job_log_delete_modal(operation_click, selected_row_keys): prevent_initial_call=True ) def job_log_delete_confirm(delete_confirm, job_log_ids_data): + """ + 删除或清空定时任务调度日志弹窗确认回调,实现删除或清空操作 + """ if delete_confirm: oper_type = job_log_ids_data.get('oper_type') @@ -279,7 +304,7 @@ def job_log_delete_confirm(delete_confirm, job_log_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -291,6 +316,9 @@ def job_log_delete_confirm(delete_confirm, job_log_ids_data): prevent_initial_call=True ) def export_job_log_list(export_click): + """ + 导出定时任务调度日志信息回调 + """ if export_click: export_job_log_res = export_job_log_list_api({}) if export_job_log_res.status_code == 200: @@ -310,7 +338,7 @@ def export_job_log_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -319,9 +347,12 @@ def export_job_log_list(export_click): prevent_initial_call=True ) def reset_job_log_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py index 754ed98..1971c5b 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py @@ -3,6 +3,7 @@ import time import uuid from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -10,23 +11,32 @@ from api.log import get_login_log_list_api, delete_login_log_api, clear_login_lo @app.callback( - [Output('login_log-list-table', 'data', allow_duplicate=True), - Output('login_log-list-table', 'pagination', allow_duplicate=True), - Output('login_log-list-table', 'key'), - Output('login_log-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('login_log-search', 'nClicks'), - Input('login_log-refresh', 'nClicks'), - Input('login_log-list-table', 'pagination'), - Input('login_log-operations-store', 'data')], - [State('login_log-ipaddr-input', 'value'), - State('login_log-user_name-input', 'value'), - State('login_log-status-select', 'value'), - State('login_log-login_time-range', 'value'), - State('login_log-button-perms-container', 'data')], + output=dict( + login_log_table_data=Output('login_log-list-table', 'data', allow_duplicate=True), + login_log_table_pagination=Output('login_log-list-table', 'pagination', allow_duplicate=True), + login_log_table_key=Output('login_log-list-table', 'key'), + login_log_table_selectedrowkeys=Output('login_log-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('login_log-search', 'nClicks'), + refresh_click=Input('login_log-refresh', 'nClicks'), + pagination=Input('login_log-list-table', 'pagination'), + operations=Input('login_log-operations-store', 'data') + ), + state=dict( + ipaddr=State('login_log-ipaddr-input', 'value'), + user_name=State('login_log-user_name-input', 'value'), + status_select=State('login_log-status-select', 'value'), + login_time_range=State('login_log-login_time-range', 'value'), + button_perms=State('login_log-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_login_log_table_data(search_click, refresh_click, pagination, operations, ipaddr, user_name, status_select, login_time_range, button_perms): + """ + 获取登录日志表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ login_time_start = None login_time_end = None @@ -72,13 +82,26 @@ def get_login_log_table_data(search_click, refresh_click, pagination, operations item['status'] = dict(tag='失败', color='volcano') item['key'] = str(item['info_id']) - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + login_log_table_data=table_data, + login_log_table_pagination=table_pagination, + login_log_table_key=str(uuid.uuid4()), + login_log_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + login_log_table_data=dash.no_update, + login_log_table_pagination=dash.no_update, + login_log_table_key=dash.no_update, + login_log_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置登录日志搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -98,6 +121,7 @@ app.clientside_callback( ) +# 隐藏/显示登录日志搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -124,17 +148,18 @@ app.clientside_callback( prevent_initial_call=True ) def change_login_log_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -143,6 +168,9 @@ def change_login_log_delete_button_status(table_rows_selected): prevent_initial_call=True ) def change_login_log_unlock_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制解锁按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -153,7 +181,7 @@ def change_login_log_unlock_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -165,6 +193,9 @@ def change_login_log_unlock_button_status(table_rows_selected): prevent_initial_call=True ) def login_log_delete_modal(operation_click, selected_row_keys): + """ + 显示删除或清空登录日志二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.index in ['delete', 'clear']: if trigger_id.index == 'delete': @@ -183,7 +214,7 @@ def login_log_delete_modal(operation_click, selected_row_keys): {'oper_type': 'clear', 'info_ids': ''} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -195,6 +226,9 @@ def login_log_delete_modal(operation_click, selected_row_keys): prevent_initial_call=True ) def login_log_delete_confirm(delete_confirm, info_ids_data): + """ + 删除或清空登录日志弹窗确认回调,实现删除或清空操作 + """ if delete_confirm: oper_type = info_ids_data.get('oper_type') @@ -229,7 +263,7 @@ def login_log_delete_confirm(delete_confirm, info_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -241,6 +275,9 @@ def login_log_delete_confirm(delete_confirm, info_ids_data): prevent_initial_call=True ) def export_login_log_list(export_click): + """ + 导出登录日志信息回调 + """ if export_click: export_login_log_res = export_login_log_list_api({}) if export_login_log_res.status_code == 200: @@ -260,7 +297,7 @@ def export_login_log_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -269,12 +306,15 @@ def export_login_log_list(export_click): prevent_initial_call=True ) def reset_login_log_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate @app.callback( @@ -285,6 +325,9 @@ def reset_login_log_export_status(data): prevent_initial_call=True ) def unlock_user(unlock_click, selected_rows): + """ + 解锁用户回调 + """ if unlock_click: user_name = selected_rows[0].get('user_name') unlock_info_res = unlock_user_api(dict(user_name=user_name)) @@ -300,4 +343,4 @@ def unlock_user(unlock_click, selected_rows): fuc.FefferyFancyMessage('解锁失败', type='error') ] - return [dash.no_update] * 2 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/monitor_c/online_c.py b/dash-fastapi-frontend/callbacks/monitor_c/online_c.py index eacbc75..8a63b45 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/online_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/online_c.py @@ -2,6 +2,7 @@ import dash import time import uuid from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -9,21 +10,30 @@ from api.online import get_online_list_api, force_logout_online_api, batch_logou @app.callback( - [Output('online-list-table', 'data', allow_duplicate=True), - Output('online-list-table', 'pagination', allow_duplicate=True), - Output('online-list-table', 'key'), - Output('online-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('online-search', 'nClicks'), - Input('online-refresh', 'nClicks'), - Input('online-list-table', 'pagination'), - Input('online-operations-store', 'data')], - [State('online-ipaddr-input', 'value'), - State('online-user_name-input', 'value'), - State('online-button-perms-container', 'data')], + output=dict( + online_table_data=Output('online-list-table', 'data', allow_duplicate=True), + online_table_pagination=Output('online-list-table', 'pagination', allow_duplicate=True), + online_table_key=Output('online-list-table', 'key'), + online_table_selectedrowkeys=Output('online-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('online-search', 'nClicks'), + refresh_click=Input('online-refresh', 'nClicks'), + pagination=Input('online-list-table', 'pagination'), + operations=Input('online-operations-store', 'data') + ), + state=dict( + ipaddr=State('online-ipaddr-input', 'value'), + user_name=State('online-user_name-input', 'value'), + button_perms=State('online-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_online_table_data(search_click, refresh_click, pagination, operations, ipaddr, user_name, button_perms): + """ + 获取在线用户表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( ipaddr=ipaddr, user_name=user_name, @@ -60,13 +70,26 @@ def get_online_table_data(search_click, refresh_click, pagination, operations, i } if 'monitor:online:forceLogout' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + online_table_data=table_data, + online_table_pagination=table_pagination, + online_table_key=str(uuid.uuid4()), + online_table_selectedrowkeys=None, + api_check_token_trigger= {'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + online_table_data=dash.no_update, + online_table_pagination=dash.no_update, + online_table_key=dash.no_update, + online_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置在线用户搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -84,6 +107,7 @@ app.clientside_callback( ) +# 隐藏/显示在线用户搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -110,6 +134,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_online_edit_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制批量强退按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -118,7 +145,7 @@ def change_online_edit_delete_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -134,6 +161,9 @@ def change_online_edit_delete_button_status(table_rows_selected): ) def online_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示强退在线用户二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'online-operation-button'} or ( trigger_id == 'online-list-table' and clicked_content == '强退'): @@ -154,7 +184,7 @@ def online_delete_modal(operation_click, button_click, {'session_ids': session_ids, 'logout_type': logout_type} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -166,6 +196,9 @@ def online_delete_modal(operation_click, button_click, prevent_initial_call=True ) def online_delete_confirm(delete_confirm, session_ids_data): + """ + 强退在线用户弹窗确认回调,实现强退操作 + """ if delete_confirm: params = dict(session_ids=session_ids_data.get('session_ids')) @@ -187,4 +220,4 @@ def online_delete_confirm(delete_confirm, session_ids_data): fuc.FefferyFancyMessage('强退失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py index e45503f..4bd6aa3 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py @@ -4,6 +4,7 @@ import uuid import json from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -12,24 +13,33 @@ from api.dict import query_dict_data_list_api @app.callback( - [Output('operation_log-list-table', 'data', allow_duplicate=True), - Output('operation_log-list-table', 'pagination', allow_duplicate=True), - Output('operation_log-list-table', 'key'), - Output('operation_log-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('operation_log-search', 'nClicks'), - Input('operation_log-refresh', 'nClicks'), - Input('operation_log-list-table', 'pagination'), - Input('operation_log-operations-store', 'data')], - [State('operation_log-title-input', 'value'), - State('operation_log-oper_name-input', 'value'), - State('operation_log-business_type-select', 'value'), - State('operation_log-status-select', 'value'), - State('operation_log-oper_time-range', 'value'), - State('operation_log-button-perms-container', 'data')], + output=dict( + operation_log_table_data=Output('operation_log-list-table', 'data', allow_duplicate=True), + operation_log_table_pagination=Output('operation_log-list-table', 'pagination', allow_duplicate=True), + operation_log_table_key=Output('operation_log-list-table', 'key'), + operation_log_table_selectedrowkeys=Output('operation_log-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('operation_log-search', 'nClicks'), + refresh_click=Input('operation_log-refresh', 'nClicks'), + pagination=Input('operation_log-list-table', 'pagination'), + operations=Input('operation_log-operations-store', 'data') + ), + state=dict( + title=State('operation_log-title-input', 'value'), + oper_name=State('operation_log-oper_name-input', 'value'), + business_type=State('operation_log-business_type-select', 'value'), + status_select=State('operation_log-status-select', 'value'), + oper_time_range=State('operation_log-oper_time-range', 'value'), + button_perms=State('operation_log-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_operation_log_table_data(search_click, refresh_click, pagination, operations, title, oper_name, business_type, status_select, oper_time_range, button_perms): + """ + 获取操作日志表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ oper_time_start = None oper_time_end = None @@ -97,13 +107,26 @@ def get_operation_log_table_data(search_click, refresh_click, pagination, operat } if 'monitor:operlog:query' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + operation_log_table_data=table_data, + operation_log_table_pagination=table_pagination, + operation_log_table_key=str(uuid.uuid4()), + operation_log_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + operation_log_table_data=dash.no_update, + operation_log_table_pagination=dash.no_update, + operation_log_table_key=dash.no_update, + operation_log_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置操作日志搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -124,6 +147,7 @@ app.clientside_callback( ) +# 隐藏/显示操作日志搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -145,26 +169,28 @@ app.clientside_callback( @app.callback( - [Output('operation_log-modal', 'visible', allow_duplicate=True), - Output('operation_log-modal', 'title'), - Output('operation_log-title-text', 'children'), - Output('operation_log-oper_url-text', 'children'), - Output('operation_log-login_info-text', 'children'), - Output('operation_log-request_method-text', 'children'), - Output('operation_log-method-text', 'children'), - Output('operation_log-oper_param-text', 'children'), - Output('operation_log-json_result-text', 'children'), - Output('operation_log-status-text', 'children'), - Output('operation_log-cost_time-text', 'children'), - Output('operation_log-oper_time-text', 'children'), - Output('api-check-token', 'data', allow_duplicate=True)], - Input('operation_log-list-table', 'nClicksButton'), - [State('operation_log-list-table', 'clickedContent'), - State('operation_log-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal_visible=Output('operation_log-modal', 'visible', allow_duplicate=True), + modal_title=Output('operation_log-modal', 'title'), + form_value=Output({'type': 'operation_log-form-value', 'index': ALL}, 'children'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + button_click=Input('operation_log-list-table', 'nClicksButton') + ), + state=dict( + clicked_content=State('operation_log-list-table', 'clickedContent'), + recently_button_clicked_row=State('operation_log-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_operation_log_modal(button_click, clicked_content, recently_button_clicked_row): + """ + 显示操作日志详情弹窗回调 + """ if button_click: + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[-2]] oper_id = int(recently_button_clicked_row['key']) operation_log_info_res = get_operation_log_detail_api(oper_id=oper_id) if operation_log_info_res['code'] == 200: @@ -172,26 +198,24 @@ def add_edit_operation_log_modal(button_click, clicked_content, recently_button_ oper_name = operation_log_info.get('oper_name') if operation_log_info.get('oper_name') else '' oper_ip = operation_log_info.get('oper_ip') if operation_log_info.get('oper_ip') else '' oper_location = operation_log_info.get('oper_location') if operation_log_info.get('oper_location') else '' - login_info = f'{oper_name} / {oper_ip} / {oper_location}' - return [ - True, - '操作日志详情', - operation_log_info.get('title'), - operation_log_info.get('oper_url'), - login_info, - operation_log_info.get('request_method'), - operation_log_info.get('method'), - operation_log_info.get('oper_param'), - operation_log_info.get('json_result'), - '正常' if operation_log_info.get('status') == 0 else '失败', - f"{operation_log_info.get('cost_time')}毫秒", - operation_log_info.get('oper_time'), - {'timestamp': time.time()}, - ] + operation_log_info['login_info'] = f'{oper_name} / {oper_ip} / {oper_location}' + operation_log_info['status'] = '正常' if operation_log_info.get('status') == 0 else '失败' + operation_log_info['cost_time'] = f"{operation_log_info.get('cost_time')}毫秒" + return dict( + modal_visible=True, + modal_title='操作日志详情', + form_value=[operation_log_info.get(k) for k in form_value_list], + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 12 + [{'timestamp': time.time()}] + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 13 + raise PreventUpdate @app.callback( @@ -200,17 +224,18 @@ def add_edit_operation_log_modal(button_click, clicked_content, recently_button_ prevent_initial_call=True ) def change_operation_log_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -222,6 +247,9 @@ def change_operation_log_delete_button_status(table_rows_selected): prevent_initial_call=True ) def operation_log_delete_modal(operation_click, selected_row_keys): + """ + 显示删除或清空操作日志二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.index in ['delete', 'clear']: if trigger_id.index == 'delete': @@ -240,7 +268,7 @@ def operation_log_delete_modal(operation_click, selected_row_keys): {'oper_type': 'clear', 'oper_ids': ''} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -252,6 +280,9 @@ def operation_log_delete_modal(operation_click, selected_row_keys): prevent_initial_call=True ) def operation_log_delete_confirm(delete_confirm, oper_ids_data): + """ + 删除或清空操作日志弹窗确认回调,实现删除或清空操作 + """ if delete_confirm: oper_type = oper_ids_data.get('oper_type') @@ -286,7 +317,7 @@ def operation_log_delete_confirm(delete_confirm, oper_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -298,6 +329,9 @@ def operation_log_delete_confirm(delete_confirm, oper_ids_data): prevent_initial_call=True ) def export_operation_log_list(export_click): + """ + 导出操作日志信息回调 + """ if export_click: export_operation_log_res = export_operation_log_list_api({}) if export_operation_log_res.status_code == 200: @@ -317,7 +351,7 @@ def export_operation_log_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -326,9 +360,12 @@ def export_operation_log_list(export_click): prevent_initial_call=True ) def reset_operation_log_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/config_c.py b/dash-fastapi-frontend/callbacks/system_c/config_c.py index b0d2ca4..47bf411 100644 --- a/dash-fastapi-frontend/callbacks/system_c/config_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/config_c.py @@ -3,6 +3,7 @@ import time import uuid from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -10,23 +11,32 @@ from api.config import get_config_list_api, get_config_detail_api, add_config_ap @app.callback( - [Output('config-list-table', 'data', allow_duplicate=True), - Output('config-list-table', 'pagination', allow_duplicate=True), - Output('config-list-table', 'key'), - Output('config-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('config-search', 'nClicks'), - Input('config-refresh', 'nClicks'), - Input('config-list-table', 'pagination'), - Input('config-operations-store', 'data')], - [State('config-config_name-input', 'value'), - State('config-config_key-input', 'value'), - State('config-config_type-select', 'value'), - State('config-create_time-range', 'value'), - State('config-button-perms-container', 'data')], + output=dict( + config_table_data=Output('config-list-table', 'data', allow_duplicate=True), + config_table_pagination=Output('config-list-table', 'pagination', allow_duplicate=True), + config_table_key=Output('config-list-table', 'key'), + config_table_selectedrowkeys=Output('config-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('config-search', 'nClicks'), + refresh_click=Input('config-refresh', 'nClicks'), + pagination=Input('config-list-table', 'pagination'), + operations=Input('config-operations-store', 'data') + ), + state=dict( + config_name=State('config-config_name-input', 'value'), + config_key=State('config-config_key-input', 'value'), + config_type=State('config-config_type-select', 'value'), + create_time_range=State('config-create_time-range', 'value'), + button_perms=State('config-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_config_table_data(search_click, refresh_click, pagination, operations, config_name, config_key, config_type, create_time_range, button_perms): + """ + 获取参数设置表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ create_time_start = None create_time_end = None if create_time_range: @@ -84,13 +94,26 @@ def get_config_table_data(search_click, refresh_click, pagination, operations, c } if 'system:config:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + config_table_data=table_data, + config_table_pagination=table_pagination, + config_table_key=str(uuid.uuid4()), + config_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + config_table_data=dash.no_update, + config_table_pagination=dash.no_update, + config_table_key=dash.no_update, + config_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置参数设置搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -110,6 +133,7 @@ app.clientside_callback( ) +# 隐藏/显示参数设置搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -136,6 +160,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_config_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -146,7 +173,7 @@ def change_config_edit_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -155,55 +182,66 @@ def change_config_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_config_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( - [Output('config-modal', 'visible', allow_duplicate=True), - Output('config-modal', 'title'), - Output('config-config_name', 'value'), - Output('config-config_key', 'value'), - Output('config-config_value', 'value'), - Output('config-config_type', 'value'), - Output('config-remark', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('config-edit-id-store', 'data'), - Output('config-operations-store-bk', 'data')], - [Input({'type': 'config-operation-button', 'index': ALL}, 'nClicks'), - Input('config-list-table', 'nClicksButton')], - [State('config-list-table', 'selectedRowKeys'), - State('config-list-table', 'clickedContent'), - State('config-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal_visible=Output('config-modal', 'visible', allow_duplicate=True), + modal_title=Output('config-modal', 'title'), + form_value=Output({'type': 'config-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'config-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'config-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('config-edit-id-store', 'data'), + modal_type=Output('config-operations-store-bk', 'data') + ), + inputs=dict( + operation_click=Input({'type': 'config-operation-button', 'index': ALL}, 'nClicks'), + button_click=Input('config-list-table', 'nClicksButton') + ), + state=dict( + selected_row_keys=State('config-list-table', 'selectedRowKeys'), + clicked_content=State('config-list-table', 'clickedContent'), + recently_button_clicked_row=State('config-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_config_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示新增或编辑参数设置弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'add', 'type': 'config-operation-button'} \ or trigger_id == {'index': 'edit', 'type': 'config-operation-button'} \ or (trigger_id == 'config-list-table' and clicked_content == '修改'): + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] if trigger_id == {'index': 'add', 'type': 'config-operation-button'}: - return [ - True, - '新增参数', - None, - None, - None, - 'Y', - None, - dash.no_update, - None, - {'type': 'add'} - ] + config_info = dict(config_name=None, config_key=None, config_value=None, config_type='Y', remark=None) + return dict( + modal_visible=True, + modal_title='新增参数', + form_value=[config_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger=dash.no_update, + edit_row_info=None, + modal_type={'type': 'add'} + ) elif trigger_id == {'index': 'edit', 'type': 'config-operation-button'} or (trigger_id == 'config-list-table' and clicked_content == '修改'): if trigger_id == {'index': 'edit', 'type': 'config-operation-button'}: config_id = int(','.join(selected_row_keys)) @@ -212,114 +250,112 @@ def add_edit_config_modal(operation_click, button_click, selected_row_keys, clic config_info_res = get_config_detail_api(config_id=config_id) if config_info_res['code'] == 200: config_info = config_info_res['data'] - return [ - True, - '编辑参数', - config_info.get('config_name'), - config_info.get('config_key'), - config_info.get('config_value'), - config_info.get('config_type'), - config_info.get('remark'), - {'timestamp': time.time()}, - config_info if config_info else None, - {'type': 'edit'} - ] - - return [dash.no_update] * 7 + [{'timestamp': time.time()}, None, None] + return dict( + modal_visible=True, + modal_title='编辑参数', + form_value=[config_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=config_info if config_info else None, + modal_type={'type': 'edit'} + ) + + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) - return [dash.no_update] * 8 + [None, None] + raise PreventUpdate @app.callback( - [Output('config-config_name-form-item', 'validateStatus'), - Output('config-config_key-form-item', 'validateStatus'), - Output('config-config_value-form-item', 'validateStatus'), - Output('config-config_name-form-item', 'help'), - Output('config-config_key-form-item', 'help'), - Output('config-config_value-form-item', 'help'), - Output('config-modal', 'visible'), - Output('config-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('config-modal', 'okCounts'), - [State('config-operations-store-bk', 'data'), - State('config-edit-id-store', 'data'), - State('config-config_name', 'value'), - State('config-config_key', 'value'), - State('config-config_value', 'value'), - State('config-config_type', 'value'), - State('config-remark', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'config-form-label', 'index': ALL, 'required': True}, 'validateStatus', + allow_duplicate=True), + form_label_validate_info=Output({'type': 'config-form-label', 'index': ALL, 'required': True}, 'help', + allow_duplicate=True), + modal_visible=Output('config-modal', 'visible'), + operations=Output('config-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('config-modal', 'okCounts') + ), + state=dict( + modal_type=State('config-operations-store-bk', 'data'), + edit_row_info=State('config-edit-id-store', 'data'), + form_value=State({'type': 'config-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'config-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def dict_type_confirm(confirm_trigger, operation_type, cur_config_info, config_name, config_key, config_value, config_type, remark): +def dict_type_confirm(confirm_trigger, modal_type, edit_row_info, form_value, form_label): + """ + 新增或编辑参数设置弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: - if all([config_name, config_key, config_value]): - params_add = dict(config_name=config_name, config_key=config_key, config_value=config_value, - config_type=config_type, remark=remark) - params_edit = dict(config_id=cur_config_info.get('config_id') if cur_config_info else None, - config_name=config_name, config_key=config_key, config_value=config_value, - config_type=config_type, remark=remark) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + if all([form_value_state.get(k) for k in form_label_output_list]): + params_add = form_value_state + params_edit = params_add.copy() + params_edit['config_id'] = edit_row_info.get('config_id') if edit_row_info else None api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_config_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_config_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [ - None if config_name else 'error', - None if config_key else 'error', - None if config_value else 'error', - None if config_name else '请输入参数名称!', - None if config_key else '请输入参数键名!', - None if config_value else '请输入参数键值!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 10 + raise PreventUpdate @app.callback( @@ -335,6 +371,9 @@ def dict_type_confirm(confirm_trigger, operation_type, cur_config_info, config_n ) def config_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示删除参数设置二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'config-operation-button'} or ( trigger_id == 'config-list-table' and clicked_content == '删除'): @@ -353,7 +392,7 @@ def config_delete_modal(operation_click, button_click, {'config_ids': config_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -365,6 +404,9 @@ def config_delete_modal(operation_click, button_click, prevent_initial_call=True ) def config_delete_confirm(delete_confirm, config_ids_data): + """ + 删除参数设置弹窗确认回调,实现删除操作 + """ if delete_confirm: params = config_ids_data @@ -382,7 +424,7 @@ def config_delete_confirm(delete_confirm, config_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -394,6 +436,9 @@ def config_delete_confirm(delete_confirm, config_ids_data): prevent_initial_call=True ) def export_config_list(export_click): + """ + 导出参数设置信息回调 + """ if export_click: export_config_res = export_config_list_api({}) if export_config_res.status_code == 200: @@ -413,7 +458,7 @@ def export_config_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -422,12 +467,15 @@ def export_config_list(export_click): prevent_initial_call=True ) def reset_config_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate @app.callback( @@ -437,6 +485,9 @@ def reset_config_export_status(data): prevent_initial_call=True ) def refresh_config_cache(refresh_click): + """ + 刷新缓存回调 + """ if refresh_click: refresh_info_res = refresh_config_api({}) if refresh_info_res.get('code') == 200: @@ -450,4 +501,4 @@ def refresh_config_cache(refresh_click): fuc.FefferyFancyMessage('刷新失败', type='error') ] - return [dash.no_update] * 2 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py index 8ce33e5..660cc53 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dept_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -2,6 +2,7 @@ import dash import time import uuid from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -11,23 +12,32 @@ from api.dept import get_dept_tree_api, get_dept_list_api, add_dept_api, edit_de @app.callback( - [Output('dept-list-table', 'data', allow_duplicate=True), - Output('dept-list-table', 'key'), - Output('dept-list-table', 'defaultExpandedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('dept-fold', 'nClicks')], - [Input('dept-search', 'nClicks'), - Input('dept-refresh', 'nClicks'), - Input('dept-operations-store', 'data'), - Input('dept-fold', 'nClicks')], - [State('dept-dept_name-input', 'value'), - State('dept-status-select', 'value'), - State('dept-list-table', 'defaultExpandedRowKeys'), - State('dept-button-perms-container', 'data')], + output=dict( + dept_table_data=Output('dept-list-table', 'data', allow_duplicate=True), + dept_table_key=Output('dept-list-table', 'key'), + dept_table_defaultexpandedrowkeys=Output('dept-list-table', 'defaultExpandedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + fold_click=Output('dept-fold', 'nClicks') + ), + inputs=dict( + search_click=Input('dept-search', 'nClicks'), + refresh_click=Input('dept-refresh', 'nClicks'), + operations=Input('dept-operations-store', 'data'), + fold_click=Input('dept-fold', 'nClicks') + ), + state=dict( + dept_name=State('dept-dept_name-input', 'value'), + status_select=State('dept-status-select', 'value'), + in_default_expanded_row_keys=State('dept-list-table', 'defaultExpandedRowKeys'), + button_perms=State('dept-button-perms-container', 'data') + ), prevent_initial_call=True ) -def get_dept_table_data(search_click, refresh_click, operations, fold_click, dept_name, status_select, in_default_expanded_row_keys, button_perms): - +def get_dept_table_data(search_click, refresh_click, operations, fold_click, dept_name, status_select, + in_default_expanded_row_keys, button_perms): + """ + 获取部门表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( dept_name=dept_name, status=status_select @@ -92,15 +102,40 @@ def get_dept_table_data(search_click, refresh_click, operations, fold_click, dep if fold_click: if in_default_expanded_row_keys: - return [table_data_new, str(uuid.uuid4()), [], {'timestamp': time.time()}, None] + return dict( + dept_table_data=table_data_new, + dept_table_key=str(uuid.uuid4()), + dept_table_defaultexpandedrowkeys=[], + api_check_token_trigger={'timestamp': time.time()}, + fold_click=None + ) - return [table_data_new, str(uuid.uuid4()), default_expanded_row_keys, {'timestamp': time.time()}, None] + return dict( + dept_table_data=table_data_new, + dept_table_key=str(uuid.uuid4()), + dept_table_defaultexpandedrowkeys=default_expanded_row_keys, + api_check_token_trigger={'timestamp': time.time()}, + fold_click=None + ) - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}, None] + return dict( + dept_table_data=dash.no_update, + dept_table_key=dash.no_update, + dept_table_defaultexpandedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + fold_click=None + ) - return [dash.no_update] * 4 + [None] + return dict( + dept_table_data=dash.no_update, + dept_table_key=dash.no_update, + dept_table_defaultexpandedrowkeys=dash.no_update, + api_check_token_trigger=dash.no_update, + fold_click=None + ) +# 重置部门搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -117,7 +152,7 @@ app.clientside_callback( prevent_initial_call=True ) - +# 隐藏/显示部门搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -139,29 +174,39 @@ app.clientside_callback( @app.callback( - [Output('dept-modal', 'visible', allow_duplicate=True), - Output('dept-modal', 'title'), - Output('dept-parent_id-div', 'hidden'), - Output('dept-parent_id', 'treeData'), - Output('dept-parent_id', 'value'), - Output('dept-dept_name', 'value'), - Output('dept-order_num', 'value'), - Output('dept-leader', 'value'), - Output('dept-phone', 'value'), - Output('dept-email', 'value'), - Output('dept-status', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('dept-edit-id-store', 'data'), - Output('dept-operations-store-bk', 'data')], - [Input({'type': 'dept-operation-button', 'index': ALL}, 'nClicks'), - Input('dept-list-table', 'nClicksButton')], - [State('dept-list-table', 'clickedContent'), - State('dept-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal_visible=Output('dept-modal', 'visible', allow_duplicate=True), + modal_title=Output('dept-modal', 'title'), + parent_id_div_ishidden=Output('dept-parent_id-div', 'hidden'), + parent_id_tree=Output({'type': 'dept-form-value', 'index': 'parent_id'}, 'treeData'), + form_value=Output({'type': 'dept-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'dept-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'dept-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('dept-edit-id-store', 'data'), + modal_type=Output('dept-operations-store-bk', 'data') + ), + inputs=dict( + operation_click=Input({'type': 'dept-operation-button', 'index': ALL}, 'nClicks'), + button_click=Input('dept-list-table', 'nClicksButton') + ), + state=dict( + clicked_content=State('dept-list-table', 'clickedContent'), + recently_button_clicked_row=State('dept-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_dept_modal(operation_click, button_click, clicked_content, recently_button_clicked_row): + """ + 显示新增或编辑部门弹窗回调 + """ trigger_id = dash.ctx.triggered_id - if trigger_id == {'index': 'add', 'type': 'dept-operation-button'} or (trigger_id == 'dept-list-table' and clicked_content != '删除'): + if trigger_id == {'index': 'add', 'type': 'dept-operation-button'} or ( + trigger_id == 'dept-list-table' and clicked_content != '删除'): + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[4]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[5]] dept_params = dict(dept_name='') if trigger_id == 'dept-list-table' and clicked_content == '修改': dept_params['dept_id'] = int(recently_button_clicked_row['key']) @@ -171,176 +216,143 @@ def add_edit_dept_modal(operation_click, button_click, clicked_content, recently if tree_info['code'] == 200: tree_data = tree_info['data'] - if trigger_id == {'index': 'add', 'type': 'dept-operation-button'}: - return [ - True, - '新增部门', - False, - tree_data, - None, - None, - None, - None, - None, - None, - '0', - {'timestamp': time.time()}, - None, - {'type': 'add'} - ] - elif trigger_id == 'dept-list-table' and clicked_content == '新增': - return [ - True, - '新增部门', - False, - tree_data, - str(recently_button_clicked_row['key']), - None, - None, - None, - None, - None, - '0', - {'timestamp': time.time()}, - None, - {'type': 'add'} - ] + if trigger_id == {'index': 'add', 'type': 'dept-operation-button'} or (trigger_id == 'dept-list-table' and clicked_content == '新增'): + dept_info = dict( + parent_id=None if trigger_id == {'index': 'add', 'type': 'dept-operation-button'} else str(recently_button_clicked_row['key']), + dept_name=None, + order_num=None, + leader=None, + phone=None, + email=None, + status='0', + ) + return dict( + modal_visible=True, + modal_title='新增部门', + parent_id_div_ishidden=False, + parent_id_tree=tree_data, + form_value=[dept_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type={'type': 'add'} + ) elif trigger_id == 'dept-list-table' and clicked_content == '修改': dept_id = int(recently_button_clicked_row['key']) dept_info_res = get_dept_detail_api(dept_id=dept_id) if dept_info_res['code'] == 200: dept_info = dept_info_res['data'] - if dept_info.get('parent_id') == 0: - return [ - True, - '编辑部门', - True, - tree_data, - str(dept_info.get('parent_id')), - dept_info.get('dept_name'), - dept_info.get('order_num'), - dept_info.get('leader'), - dept_info.get('phone'), - dept_info.get('email'), - dept_info.get('status'), - {'timestamp': time.time()}, - dept_info, - {'type': 'edit'} - ] - else: - return [ - True, - '编辑部门', - False, - tree_data, - str(dept_info.get('parent_id')), - dept_info.get('dept_name'), - dept_info.get('order_num'), - dept_info.get('leader'), - dept_info.get('phone'), - dept_info.get('email'), - dept_info.get('status'), - {'timestamp': time.time()}, - dept_info, - {'type': 'edit'} - ] + return dict( + modal_visible=True, + modal_title='编辑部门', + parent_id_div_ishidden=dept_info.get('parent_id') == 0, + parent_id_tree=tree_data, + form_value=[dept_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=dept_info, + modal_type={'type': 'edit'} + ) - return [dash.no_update] * 11 + [{'timestamp': time.time()}, None, None] + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + parent_id_div_ishidden=dash.no_update, + parent_id_tree=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) - return [dash.no_update] * 12 + [None, None] + raise PreventUpdate @app.callback( - [Output('dept-parent_id-form-item', 'validateStatus'), - Output('dept-dept_name-form-item', 'validateStatus'), - Output('dept-order_num-form-item', 'validateStatus'), - Output('dept-parent_id-form-item', 'help'), - Output('dept-dept_name-form-item', 'help'), - Output('dept-order_num-form-item', 'help'), - Output('dept-modal', 'visible'), - Output('dept-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('dept-modal', 'okCounts'), - [State('dept-operations-store-bk', 'data'), - State('dept-edit-id-store', 'data'), - State('dept-parent_id', 'value'), - State('dept-dept_name', 'value'), - State('dept-order_num', 'value'), - State('dept-leader', 'value'), - State('dept-phone', 'value'), - State('dept-email', 'value'), - State('dept-status', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'dept-form-label', 'index': ALL, 'required': True}, 'validateStatus', + allow_duplicate=True), + form_label_validate_info=Output({'type': 'dept-form-label', 'index': ALL, 'required': True}, 'help', + allow_duplicate=True), + modal_visible=Output('dept-modal', 'visible'), + operations=Output('dept-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('dept-modal', 'okCounts') + ), + state=dict( + modal_type=State('dept-operations-store-bk', 'data'), + edit_row_info=State('dept-edit-id-store', 'data'), + form_value=State({'type': 'dept-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'dept-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def dept_confirm(confirm_trigger, operation_type, cur_dept_info, parent_id, dept_name, order_num, leader, phone, email, status): +def dept_confirm(confirm_trigger, modal_type, edit_row_info, form_value, form_label): + """ + 新增或编辑部门弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: - if all([parent_id, dept_name, order_num]): - params_add = dict(parent_id=parent_id, dept_name=dept_name, order_num=order_num, leader=leader, phone=phone, - email=email, status=status) - params_edit = dict(dept_id=cur_dept_info.get('dept_id') if cur_dept_info else None, parent_id=parent_id, dept_name=dept_name, - order_num=order_num, leader=leader, phone=phone, email=email, status=status) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + if all([form_value_state.get(k) for k in form_label_output_list]): + params_add = form_value_state + params_edit = params_add.copy() + params_edit['dept_id'] = edit_row_info.get('dept_id') if edit_row_info else None api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_dept_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_dept_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] - - return [ - None if parent_id else 'error', - None if dept_name else 'error', - None if order_num else 'error', - None if parent_id else '请选择上级部门!', - None if dept_name else '请输入部门名称!', - None if order_num else '请输入显示排序!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) + + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 10 + raise PreventUpdate @app.callback( @@ -353,6 +365,9 @@ def dept_confirm(confirm_trigger, operation_type, cur_dept_info, parent_id, dept prevent_initial_call=True ) def dept_delete_modal(button_click, clicked_content, recently_button_clicked_row): + """ + 显示删除部门二次确认弹窗回调 + """ if button_click: if clicked_content == '删除': @@ -366,7 +381,7 @@ def dept_delete_modal(button_click, clicked_content, recently_button_clicked_row {'dept_ids': dept_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -378,6 +393,9 @@ def dept_delete_modal(button_click, clicked_content, recently_button_clicked_row prevent_initial_call=True ) def dept_delete_confirm(delete_confirm, dept_ids_data): + """ + 删除部门弹窗确认回调,实现删除操作 + """ if delete_confirm: params = dept_ids_data @@ -395,4 +413,4 @@ def dept_delete_confirm(delete_confirm, dept_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py index 2cb608f..a2b87cc 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py @@ -3,6 +3,7 @@ import time import uuid from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -10,23 +11,32 @@ from api.dict import get_dict_type_list_api, get_all_dict_type_api, get_dict_typ @app.callback( - [Output('dict_type-list-table', 'data', allow_duplicate=True), - Output('dict_type-list-table', 'pagination', allow_duplicate=True), - Output('dict_type-list-table', 'key'), - Output('dict_type-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('dict_type-search', 'nClicks'), - Input('dict_type-refresh', 'nClicks'), - Input('dict_type-list-table', 'pagination'), - Input('dict_type-operations-store', 'data')], - [State('dict_type-dict_name-input', 'value'), - State('dict_type-dict_type-input', 'value'), - State('dict_type-status-select', 'value'), - State('dict_type-create_time-range', 'value'), - State('dict_type-button-perms-container', 'data')], + output=dict( + dict_type_table_data=Output('dict_type-list-table', 'data', allow_duplicate=True), + dict_type_table_pagination=Output('dict_type-list-table', 'pagination', allow_duplicate=True), + dict_type_table_key=Output('dict_type-list-table', 'key'), + dict_type_table_selectedrowkeys=Output('dict_type-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('dict_type-search', 'nClicks'), + refresh_click=Input('dict_type-refresh', 'nClicks'), + pagination=Input('dict_type-list-table', 'pagination'), + operations=Input('dict_type-operations-store', 'data') + ), + state=dict( + dict_name=State('dict_type-dict_name-input', 'value'), + dict_type=State('dict_type-dict_type-input', 'value'), + status_select=State('dict_type-status-select', 'value'), + create_time_range=State('dict_type-create_time-range', 'value'), + button_perms=State('dict_type-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_dict_type_table_data(search_click, refresh_click, pagination, operations, dict_name, dict_type, status_select, create_time_range, button_perms): + """ + 获取字典类型表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ create_time_start = None create_time_end = None if create_time_range: @@ -88,13 +98,26 @@ def get_dict_type_table_data(search_click, refresh_click, pagination, operations } if 'system:dict:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + dict_type_table_data=table_data, + dict_type_table_pagination=table_pagination, + dict_type_table_key=str(uuid.uuid4()), + dict_type_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + dict_type_table_data=dash.no_update, + dict_type_table_pagination=dash.no_update, + dict_type_table_key=dash.no_update, + dict_type_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置字典类型搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -114,6 +137,7 @@ app.clientside_callback( ) +# 隐藏/显示字典类型搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -140,6 +164,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_dict_type_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -150,7 +177,7 @@ def change_dict_type_edit_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -159,53 +186,66 @@ def change_dict_type_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_dict_type_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( - [Output('dict_type-modal', 'visible', allow_duplicate=True), - Output('dict_type-modal', 'title'), - Output('dict_type-dict_name', 'value'), - Output('dict_type-dict_type', 'value'), - Output('dict_type-status', 'value'), - Output('dict_type-remark', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('dict_type-edit-id-store', 'data'), - Output('dict_type-operations-store-bk', 'data')], - [Input({'type': 'dict_type-operation-button', 'index': ALL}, 'nClicks'), - Input('dict_type-list-table', 'nClicksButton')], - [State('dict_type-list-table', 'selectedRowKeys'), - State('dict_type-list-table', 'clickedContent'), - State('dict_type-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal_visible=Output('dict_type-modal', 'visible', allow_duplicate=True), + modal_title=Output('dict_type-modal', 'title'), + form_value=Output({'type': 'dict_type-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'dict_type-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'dict_type-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('dict_type-edit-id-store', 'data'), + modal_type=Output('dict_type-operations-store-bk', 'data') + ), + inputs=dict( + operation_click=Input({'type': 'dict_type-operation-button', 'index': ALL}, 'nClicks'), + button_click=Input('dict_type-list-table', 'nClicksButton') + ), + state=dict( + selected_row_keys=State('dict_type-list-table', 'selectedRowKeys'), + clicked_content=State('dict_type-list-table', 'clickedContent'), + recently_button_clicked_row=State('dict_type-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_dict_type_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示新增或编辑字典类型弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'add', 'type': 'dict_type-operation-button'} \ or trigger_id == {'index': 'edit', 'type': 'dict_type-operation-button'} \ or (trigger_id == 'dict_type-list-table' and clicked_content == '修改'): + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] if trigger_id == {'index': 'add', 'type': 'dict_type-operation-button'}: - return [ - True, - '新增字典类型', - None, - None, - '0', - None, - dash.no_update, - None, - {'type': 'add'} - ] + dict_type_info = dict(dict_name=None, dict_type=None, status='0', remark=None,) + return dict( + modal_visible=True, + modal_title='新增字典类型', + form_value=[dict_type_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger=dash.no_update, + edit_row_info=None, + modal_type={'type': 'add'} + ) elif trigger_id == {'index': 'edit', 'type': 'dict_type-operation-button'} or (trigger_id == 'dict_type-list-table' and clicked_content == '修改'): if trigger_id == {'index': 'edit', 'type': 'dict_type-operation-button'}: dict_id = int(','.join(selected_row_keys)) @@ -214,100 +254,112 @@ def add_edit_dict_type_modal(operation_click, button_click, selected_row_keys, c dict_type_info_res = get_dict_type_detail_api(dict_id=dict_id) if dict_type_info_res['code'] == 200: dict_type_info = dict_type_info_res['data'] - return [ - True, - '编辑字典类型', - dict_type_info.get('dict_name'), - dict_type_info.get('dict_type'), - dict_type_info.get('status'), - dict_type_info.get('remark'), - {'timestamp': time.time()}, - dict_type_info if dict_type_info else None, - {'type': 'edit'} - ] - - return [dash.no_update] * 6 + [{'timestamp': time.time()}, None, None] + return dict( + modal_visible=True, + modal_title='编辑字典类型', + form_value=[dict_type_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=dict_type_info if dict_type_info else None, + modal_type={'type': 'edit'} + ) + + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) - return [dash.no_update] * 7 + [None, None] + raise PreventUpdate @app.callback( - [Output('dict_type-dict_name-form-item', 'validateStatus'), - Output('dict_type-dict_type-form-item', 'validateStatus'), - Output('dict_type-dict_name-form-item', 'help'), - Output('dict_type-dict_type-form-item', 'help'), - Output('dict_type-modal', 'visible'), - Output('dict_type-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('dict_type-modal', 'okCounts'), - [State('dict_type-operations-store-bk', 'data'), - State('dict_type-edit-id-store', 'data'), - State('dict_type-dict_name', 'value'), - State('dict_type-dict_type', 'value'), - State('dict_type-status', 'value'), - State('dict_type-remark', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'dict_type-form-label', 'index': ALL, 'required': True}, 'validateStatus', + allow_duplicate=True), + form_label_validate_info=Output({'type': 'dict_type-form-label', 'index': ALL, 'required': True}, 'help', + allow_duplicate=True), + modal_visible=Output('dict_type-modal', 'visible'), + operations=Output('dict_type-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('dict_type-modal', 'okCounts') + ), + state=dict( + modal_type=State('dict_type-operations-store-bk', 'data'), + edit_row_info=State('dict_type-edit-id-store', 'data'), + form_value=State({'type': 'dict_type-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'dict_type-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def dict_type_confirm(confirm_trigger, operation_type, cur_post_info, dict_name, dict_type, status, remark): +def dict_type_confirm(confirm_trigger, modal_type, edit_row_info, form_value, form_label): + """ + 新增或编字典类型弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: - if all([dict_name, dict_type]): - params_add = dict(dict_name=dict_name, dict_type=dict_type, status=status, remark=remark) - params_edit = dict(dict_id=cur_post_info.get('dict_id') if cur_post_info else None, dict_name=dict_name, - dict_type=dict_type, status=status, remark=remark) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + if all([form_value_state.get(k) for k in form_label_output_list]): + params_add = form_value_state + params_edit = params_add.copy() + params_edit['dict_id'] = edit_row_info.get('dict_id') if edit_row_info else None api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_dict_type_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_dict_type_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [ - None if dict_name else 'error', - None if dict_type else 'error', - None if dict_name else '请输入字典名称!', - None if dict_type else '请输入字典类型!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 8 + raise PreventUpdate @app.callback( @@ -323,6 +375,9 @@ def dict_type_confirm(confirm_trigger, operation_type, cur_post_info, dict_name, ) def dict_type_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示删除字典类型二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'dict_type-operation-button'} or ( trigger_id == 'dict_type-list-table' and clicked_content == '删除'): @@ -341,7 +396,7 @@ def dict_type_delete_modal(operation_click, button_click, {'dict_ids': dict_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -353,6 +408,9 @@ def dict_type_delete_modal(operation_click, button_click, prevent_initial_call=True ) def dict_type_delete_confirm(delete_confirm, dict_ids_data): + """ + 删除字典类型弹窗确认回调,实现删除操作 + """ if delete_confirm: params = dict_ids_data @@ -370,23 +428,32 @@ def dict_type_delete_confirm(delete_confirm, dict_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( - [Output('dict_type_to_dict_data-modal', 'visible'), - Output('dict_type_to_dict_data-modal', 'title'), - Output('dict_data-dict_type-select', 'options'), - Output('dict_data-dict_type-select', 'value', allow_duplicate=True), - Output('dict_data-search', 'nClicks'), - Output('api-check-token', 'data', allow_duplicate=True)], - Input('dict_type-list-table', 'nClicksButton'), - [State('dict_type-list-table', 'clickedContent'), - State('dict_type-list-table', 'recentlyButtonClickedRow'), - State('dict_data-search', 'nClicks')], + output=dict( + dict_data_modal_visible=Output('dict_type_to_dict_data-modal', 'visible'), + dict_data_modal_title=Output('dict_type_to_dict_data-modal', 'title'), + dict_data_select_options=Output('dict_data-dict_type-select', 'options'), + dict_data_select_value=Output('dict_data-dict_type-select', 'value', allow_duplicate=True), + dict_data_search_nclick=Output('dict_data-search', 'nClicks'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + button_click=Input('dict_type-list-table', 'nClicksButton') + ), + state=dict( + clicked_content=State('dict_type-list-table', 'clickedContent'), + recently_button_clicked_row=State('dict_type-list-table', 'recentlyButtonClickedRow'), + dict_data_search_nclick=State('dict_data-search', 'nClicks') + ), prevent_initial_call=True ) def dict_type_to_dict_data_modal(button_click, clicked_content, recently_button_clicked_row, dict_data_search_nclick): + """ + 显示字典类型对应数据表格弹窗回调 + """ if button_click and clicked_content == recently_button_clicked_row.get('dict_type').get('content'): all_dict_type_info = get_all_dict_type_api({}) @@ -394,25 +461,25 @@ def dict_type_to_dict_data_modal(button_click, clicked_content, recently_button_ all_dict_type = all_dict_type_info.get('data') dict_data_options = [dict(label=item.get('dict_name'), value=item.get('dict_type')) for item in all_dict_type] - return [ - True, - '字典数据', - dict_data_options, - recently_button_clicked_row.get('dict_type').get('content'), - dict_data_search_nclick + 1 if dict_data_search_nclick else 1, - {'timestamp': time.time()}, - ] + return dict( + dict_data_modal_visible=True, + dict_data_modal_title='字典数据', + dict_data_select_options=dict_data_options, + dict_data_select_value=recently_button_clicked_row.get('dict_type').get('content'), + dict_data_search_nclick=dict_data_search_nclick + 1 if dict_data_search_nclick else 1, + api_check_token_trigger={'timestamp': time.time()} + ) - return [ - True, - '字典数据', - [], - recently_button_clicked_row.get('dict_type').get('content'), - dict_data_search_nclick + 1 if dict_data_search_nclick else 1, - {'timestamp': time.time()}, - ] + return dict( + dict_data_modal_visible=True, + dict_data_modal_title='字典数据', + dict_data_select_options=[], + dict_data_select_value=recently_button_clicked_row.get('dict_type').get('content'), + dict_data_search_nclick=dict_data_search_nclick + 1 if dict_data_search_nclick else 1, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 6 + raise PreventUpdate @app.callback( @@ -424,6 +491,9 @@ def dict_type_to_dict_data_modal(button_click, clicked_content, recently_button_ prevent_initial_call=True ) def export_dict_type_list(export_click): + """ + 导出字典类型信息回调 + """ if export_click: export_dict_type_res = export_dict_type_list_api({}) if export_dict_type_res.status_code == 200: @@ -443,7 +513,7 @@ def export_dict_type_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -452,12 +522,15 @@ def export_dict_type_list(export_click): prevent_initial_call=True ) def reset_dict_type_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate @app.callback( @@ -467,6 +540,9 @@ def reset_dict_type_export_status(data): prevent_initial_call=True ) def refresh_dict_cache(refresh_click): + """ + 刷新缓存回调 + """ if refresh_click: refresh_info_res = refresh_dict_api({}) if refresh_info_res.get('code') == 200: @@ -480,4 +556,4 @@ def refresh_dict_cache(refresh_click): fuc.FefferyFancyMessage('刷新失败', type='error') ] - return [dash.no_update] * 2 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py index c01bcb6..f5c98bc 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py @@ -3,6 +3,7 @@ import time import uuid from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -10,22 +11,31 @@ from api.dict import get_dict_data_list_api, get_dict_data_detail_api, add_dict_ @app.callback( - [Output('dict_data-list-table', 'data', allow_duplicate=True), - Output('dict_data-list-table', 'pagination', allow_duplicate=True), - Output('dict_data-list-table', 'key'), - Output('dict_data-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('dict_data-search', 'nClicks'), - Input('dict_data-refresh', 'nClicks'), - Input('dict_data-list-table', 'pagination'), - Input('dict_data-operations-store', 'data')], - [State('dict_data-dict_type-select', 'value'), - State('dict_data-dict_label-input', 'value'), - State('dict_data-status-select', 'value'), - State('dict_data-button-perms-container', 'data')], + output=dict( + dict_data_table_data=Output('dict_data-list-table', 'data', allow_duplicate=True), + dict_data_table_pagination=Output('dict_data-list-table', 'pagination', allow_duplicate=True), + dict_data_table_key=Output('dict_data-list-table', 'key'), + dict_data_table_selectedrowkeys=Output('dict_data-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('dict_data-search', 'nClicks'), + refresh_click=Input('dict_data-refresh', 'nClicks'), + pagination=Input('dict_data-list-table', 'pagination'), + operations=Input('dict_data-operations-store', 'data') + ), + state=dict( + dict_type=State('dict_data-dict_type-select', 'value'), + dict_label=State('dict_data-dict_label-input', 'value'), + status_select=State('dict_data-status-select', 'value'), + button_perms=State('dict_data-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_dict_data_table_data(search_click, refresh_click, pagination, operations, dict_type, dict_label, status_select, button_perms): + """ + 获取字典数据表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( dict_type=dict_type, @@ -74,13 +84,26 @@ def get_dict_data_table_data(search_click, refresh_click, pagination, operations } if 'system:dict:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + dict_data_table_data=table_data, + dict_data_table_pagination=table_pagination, + dict_data_table_key=str(uuid.uuid4()), + dict_data_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + dict_data_table_data=dash.no_update, + dict_data_table_pagination=dash.no_update, + dict_data_table_key=dash.no_update, + dict_data_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置字典数据搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -98,6 +121,7 @@ app.clientside_callback( ) +# 隐藏/显示字典数据搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -124,6 +148,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_dict_data_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -134,7 +161,7 @@ def change_dict_data_edit_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -143,63 +170,77 @@ def change_dict_data_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_dict_data_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( - [Output('dict_data-modal', 'visible', allow_duplicate=True), - Output('dict_data-modal', 'title'), - Output('dict_data-dict_type', 'value'), - Output('dict_data-dict_label', 'value'), - Output('dict_data-dict_value', 'value'), - Output('dict_data-css_class', 'value'), - Output('dict_data-dict_sort', 'value'), - Output('dict_data-list_class', 'value'), - Output('dict_data-status', 'value'), - Output('dict_data-remark', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('dict_data-edit-id-store', 'data'), - Output('dict_data-operations-store-bk', 'data')], - [Input({'type': 'dict_data-operation-button', 'index': ALL}, 'nClicks'), - Input('dict_data-list-table', 'nClicksButton')], - [State('dict_data-list-table', 'selectedRowKeys'), - State('dict_data-list-table', 'clickedContent'), - State('dict_data-list-table', 'recentlyButtonClickedRow'), - State('dict_data-dict_type-select', 'value')], + output=dict( + modal_visible=Output('dict_data-modal', 'visible', allow_duplicate=True), + modal_title=Output('dict_data-modal', 'title'), + form_value=Output({'type': 'dict_data-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'dict_data-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'dict_data-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('dict_data-edit-id-store', 'data'), + modal_type=Output('dict_data-operations-store-bk', 'data') + ), + inputs=dict( + operation_click=Input({'type': 'dict_data-operation-button', 'index': ALL}, 'nClicks'), + button_click=Input('dict_data-list-table', 'nClicksButton') + ), + state=dict( + selected_row_keys=State('dict_data-list-table', 'selectedRowKeys'), + clicked_content=State('dict_data-list-table', 'clickedContent'), + recently_button_clicked_row=State('dict_data-list-table', 'recentlyButtonClickedRow'), + dict_type_select=State('dict_data-dict_type-select', 'value') + ), prevent_initial_call=True ) def add_edit_dict_data_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row, dict_type_select): + """ + 显示新增或编辑字典数据弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'add', 'type': 'dict_data-operation-button'} \ or trigger_id == {'index': 'edit', 'type': 'dict_data-operation-button'} \ or (trigger_id == 'dict_data-list-table' and clicked_content == '修改'): + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] if trigger_id == {'index': 'add', 'type': 'dict_data-operation-button'}: - return [ - True, - '新增字典数据', - dict_type_select, - None, - None, - None, - 0, - 'default', - '0', - None, - dash.no_update, - None, - {'type': 'add'} - ] + dict_data_info = dict( + dict_type=dict_type_select, + dict_label=None, + dict_value=None, + css_class=None, + dict_sort=0, + list_class='default', + status='0', + remark=None + ) + return dict( + modal_visible=True, + modal_title='新增字典数据', + form_value=[dict_data_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger=dash.no_update, + edit_row_info=None, + modal_type={'type': 'add'} + ) elif trigger_id == {'index': 'edit', 'type': 'dict_data-operation-button'} or (trigger_id == 'dict_data-list-table' and clicked_content == '修改'): if trigger_id == {'index': 'edit', 'type': 'dict_data-operation-button'}: dict_code = int(','.join(selected_row_keys)) @@ -208,117 +249,112 @@ def add_edit_dict_data_modal(operation_click, button_click, selected_row_keys, c dict_data_info_res = get_dict_data_detail_api(dict_code=dict_code) if dict_data_info_res['code'] == 200: dict_data_info = dict_data_info_res['data'] - return [ - True, - '编辑字典数据', - dict_data_info.get('dict_type'), - dict_data_info.get('dict_label'), - dict_data_info.get('dict_value'), - dict_data_info.get('css_class'), - dict_data_info.get('dict_sort'), - dict_data_info.get('list_class'), - dict_data_info.get('status'), - dict_data_info.get('remark'), - {'timestamp': time.time()}, - dict_data_info if dict_data_info else None, - {'type': 'edit'} - ] - - return [dash.no_update] * 10 + [{'timestamp': time.time()}, None, None] + return dict( + modal_visible=True, + modal_title='编辑字典数据', + form_value=[dict_data_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=dict_data_info if dict_data_info else None, + modal_type={'type': 'edit'} + ) + + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) - return [dash.no_update] * 11 + [None, None] + raise PreventUpdate @app.callback( - [Output('dict_data-dict_label-form-item', 'validateStatus'), - Output('dict_data-dict_value-form-item', 'validateStatus'), - Output('dict_data-dict_sort-form-item', 'validateStatus'), - Output('dict_data-dict_label-form-item', 'help'), - Output('dict_data-dict_value-form-item', 'help'), - Output('dict_data-dict_sort-form-item', 'help'), - Output('dict_data-modal', 'visible'), - Output('dict_data-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('dict_data-modal', 'okCounts'), - [State('dict_data-operations-store-bk', 'data'), - State('dict_data-edit-id-store', 'data'), - State('dict_data-dict_type', 'value'), - State('dict_data-dict_label', 'value'), - State('dict_data-dict_value', 'value'), - State('dict_data-css_class', 'value'), - State('dict_data-dict_sort', 'value'), - State('dict_data-list_class', 'value'), - State('dict_data-status', 'value'), - State('dict_data-remark', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'dict_data-form-label', 'index': ALL, 'required': True}, 'validateStatus', + allow_duplicate=True), + form_label_validate_info=Output({'type': 'dict_data-form-label', 'index': ALL, 'required': True}, 'help', + allow_duplicate=True), + modal_visible=Output('dict_data-modal', 'visible'), + operations=Output('dict_data-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('dict_data-modal', 'okCounts') + ), + state=dict( + modal_type=State('dict_data-operations-store-bk', 'data'), + edit_row_info=State('dict_data-edit-id-store', 'data'), + form_value=State({'type': 'dict_data-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'dict_data-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def dict_data_confirm(confirm_trigger, operation_type, cur_post_info, dict_type, dict_label, dict_value, css_class, dict_sort, list_class, status, remark): +def dict_data_confirm(confirm_trigger, modal_type, edit_row_info, form_value, form_label): + """ + 新增或编字典数据弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: - if all([dict_label, dict_value, dict_sort]): - params_add = dict(dict_type=dict_type, dict_label=dict_label, dict_value=dict_value, css_class=css_class, dict_sort=dict_sort, list_class=list_class, status=status, remark=remark) - params_edit = dict(dict_code=cur_post_info.get('dict_code') if cur_post_info else None, dict_type=dict_type, dict_label=dict_label, dict_value=dict_value, css_class=css_class, dict_sort=dict_sort, list_class=list_class, status=status, remark=remark) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + if all([form_value_state.get(k) for k in form_label_output_list]): + params_add = form_value_state + params_edit = params_add.copy() + params_edit['dict_code'] = edit_row_info.get('dict_code') if edit_row_info else None api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_dict_data_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_dict_data_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [ - None if dict_label else 'error', - None if dict_value else 'error', - None if dict_sort else 'error', - None if dict_label else '请输入数据标签!', - None if dict_value else '请输入数据键值!', - None if dict_sort else '请输入显示排序!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 10 + raise PreventUpdate @app.callback( @@ -334,6 +370,9 @@ def dict_data_confirm(confirm_trigger, operation_type, cur_post_info, dict_type, ) def dict_data_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示删除字典数据二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'dict_data-operation-button'} or ( trigger_id == 'dict_data-list-table' and clicked_content == '删除'): @@ -352,7 +391,7 @@ def dict_data_delete_modal(operation_click, button_click, {'dict_codes': dict_codes} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -364,6 +403,9 @@ def dict_data_delete_modal(operation_click, button_click, prevent_initial_call=True ) def dict_data_delete_confirm(delete_confirm, dict_codes_data): + """ + 删除字典数据弹窗确认回调,实现删除操作 + """ if delete_confirm: params = dict_codes_data @@ -381,7 +423,7 @@ def dict_data_delete_confirm(delete_confirm, dict_codes_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -394,6 +436,9 @@ def dict_data_delete_confirm(delete_confirm, dict_codes_data): prevent_initial_call=True ) def export_dict_data_list(export_click, dict_type): + """ + 导出字典数据信息回调 + """ if export_click: export_dict_data_res = export_dict_data_list_api(dict(dict_type=dict_type)) if export_dict_data_res.status_code == 200: @@ -413,7 +458,7 @@ def export_dict_data_list(export_click, dict_type): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -422,9 +467,12 @@ def export_dict_data_list(export_click, dict_type): prevent_initial_call=True ) def reset_dict_data_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py index 773a70d..53da181 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py @@ -1,6 +1,7 @@ import dash import time from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -8,94 +9,92 @@ from api.menu import add_menu_api, edit_menu_api @app.callback( - [Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-parent_id-form-item', 'help', allow_duplicate=True), - Output('menu-menu_name-form-item', 'help', allow_duplicate=True), - Output('menu-order_num-form-item', 'help', allow_duplicate=True), - Output('menu-modal', 'visible', allow_duplicate=True), - Output('menu-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('menu-modal-F-trigger', 'data'), - [State('menu-operations-store-bk', 'data'), - State('menu-edit-id-store', 'data'), - State('menu-parent_id', 'value'), - State('menu-menu_type', 'value'), - State('menu-icon', 'value'), - State('menu-menu_name', 'value'), - State('menu-order_num', 'value'), - State('button-menu-perms', 'value')], + output=dict( + form_validate=[ + Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-parent_id-form-item', 'help', allow_duplicate=True), + Output('menu-menu_name-form-item', 'help', allow_duplicate=True), + Output('menu-order_num-form-item', 'help', allow_duplicate=True) + ], + modal_visible=Output('menu-modal', 'visible', allow_duplicate=True), + operations=Output('menu-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('menu-modal-F-trigger', 'data'), + ), + state=dict( + modal_type=State('menu-operations-store-bk', 'data'), + edit_row_info=State('menu-edit-id-store', 'data'), + parent_id=State('menu-parent_id', 'value'), + menu_type=State('menu-menu_type', 'value'), + icon=State('menu-icon', 'value'), + menu_name=State('menu-menu_name', 'value'), + order_num=State('menu-order_num', 'value'), + perms=State('button-menu-perms', 'value') + ), prevent_initial_call=True ) -def menu_confirm_button(confirm_trigger, operation_type, cur_menu_info, parent_id, menu_type, icon, menu_name, order_num, perms): +def menu_confirm_button(confirm_trigger, modal_type, edit_row_info, parent_id, menu_type, icon, menu_name, order_num, perms): + """ + 菜单类型为按钮时新增或编辑弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: if all([parent_id, menu_name, order_num]): params_add = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, perms=perms) - params_edit = dict(menu_id=cur_menu_info.get('menu_id') if cur_menu_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, + params_edit = dict(menu_id=edit_row_info.get('menu_id') if edit_row_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, perms=perms) api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_menu_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_menu_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] - - return [ - None if parent_id else 'error', - None if menu_name else 'error', - None if order_num else 'error', - None if parent_id else '请选择上级菜单!', - None if menu_name else '请输入菜单名称!', - None if order_num else '请输入显示排序!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_validate=[None] * 6, + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_validate=[None] * 6, + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) - return [dash.no_update] * 10 + return dict( + form_validate=[None] * 6, + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) + + return dict( + form_validate=[ + None if parent_id else 'error', + None if menu_name else 'error', + None if order_num else 'error', + None if parent_id else '请选择上级菜单!', + None if menu_name else '请输入菜单名称!', + None if order_num else '请输入显示排序!' + ], + 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 @app.callback( @@ -103,7 +102,10 @@ def menu_confirm_button(confirm_trigger, operation_type, cur_menu_info, parent_i Input('menu-edit-id-store', 'data') ) def set_edit_info(edit_info): + """ + 菜单类型为按钮时回显菜单数据回调 + """ if edit_info: return edit_info.get('perms') - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py index 4b69210..d18c716 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py @@ -1,6 +1,7 @@ import dash import time from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -8,108 +9,100 @@ from api.menu import add_menu_api, edit_menu_api @app.callback( - [Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), - Output('content-menu-path-form-item', 'validateStatus'), - Output('menu-parent_id-form-item', 'help', allow_duplicate=True), - Output('menu-menu_name-form-item', 'help', allow_duplicate=True), - Output('menu-order_num-form-item', 'help', allow_duplicate=True), - Output('content-menu-path-form-item', 'help'), - Output('menu-modal', 'visible', allow_duplicate=True), - Output('menu-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('menu-modal-M-trigger', 'data'), - [State('menu-operations-store-bk', 'data'), - State('menu-edit-id-store', 'data'), - State('menu-parent_id', 'value'), - State('menu-menu_type', 'value'), - State('menu-icon', 'value'), - State('menu-menu_name', 'value'), - State('menu-order_num', 'value'), - State('content-menu-is_frame', 'value'), - State('content-menu-path', 'value'), - State('content-menu-visible', 'value'), - State('content-menu-status', 'value')], + output=dict( + form_validate=[ + Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), + Output('content-menu-path-form-item', 'validateStatus'), + Output('menu-parent_id-form-item', 'help', allow_duplicate=True), + Output('menu-menu_name-form-item', 'help', allow_duplicate=True), + Output('menu-order_num-form-item', 'help', allow_duplicate=True), + Output('content-menu-path-form-item', 'help'), + ], + modal_visible=Output('menu-modal', 'visible', allow_duplicate=True), + operations=Output('menu-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('menu-modal-M-trigger', 'data') + ), + state=dict( + modal_type=State('menu-operations-store-bk', 'data'), + edit_row_info=State('menu-edit-id-store', 'data'), + parent_id=State('menu-parent_id', 'value'), + menu_type=State('menu-menu_type', 'value'), + icon=State('menu-icon', 'value'), + menu_name=State('menu-menu_name', 'value'), + order_num=State('menu-order_num', 'value'), + is_frame=State('content-menu-is_frame', 'value'), + path=State('content-menu-path', 'value'), + visible=State('content-menu-visible', 'value'), + status=State('content-menu-status', 'value') + ), prevent_initial_call=True ) -def menu_confirm_content(confirm_trigger, operation_type, cur_menu_info, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, visible, status): +def menu_confirm_content(confirm_trigger, modal_type, edit_row_info, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, visible, status): + """ + 菜单类型为目录时新增或编辑弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: if all([parent_id, menu_name, order_num, path]): params_add = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, is_frame=is_frame, path=path, visible=visible, status=status) - params_edit = dict(menu_id=cur_menu_info.get('menu_id') if cur_menu_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, + params_edit = dict(menu_id=edit_row_info.get('menu_id') if edit_row_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, is_frame=is_frame, path=path, visible=visible, status=status) api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_menu_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_menu_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] - - return [ - None if parent_id else 'error', - None if menu_name else 'error', - None if order_num else 'error', - None if path else 'error', - None if parent_id else '请选择上级菜单!', - None if menu_name else '请输入菜单名称!', - None if order_num else '请输入显示排序!', - None if path else '请输入路由地址!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_validate=[None] * 8, + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_validate=[None] * 8, + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_validate=[None] * 8, + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) + + return dict( + form_validate=[ + None if parent_id else 'error', + None if menu_name else 'error', + None if order_num else 'error', + None if path else 'error', + None if parent_id else '请选择上级菜单!', + None if menu_name else '请输入菜单名称!', + None if order_num else '请输入显示排序!', + None if path else '请输入路由地址!', + ], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 12 + raise PreventUpdate @app.callback( @@ -120,6 +113,9 @@ def menu_confirm_content(confirm_trigger, operation_type, cur_menu_info, parent_ Input('menu-edit-id-store', 'data') ) def set_edit_info(edit_info): + """ + 菜单类型为目录时回显菜单数据回调 + """ if edit_info: return [ edit_info.get('is_frame'), @@ -128,4 +124,4 @@ def set_edit_info(edit_info): edit_info.get('status') ] - return [dash.no_update] * 4 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py index 5ca9d4a..f37b792 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py @@ -1,6 +1,7 @@ import dash import time from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -8,114 +9,106 @@ from api.menu import add_menu_api, edit_menu_api @app.callback( - [Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), - Output('menu-menu-path-form-item', 'validateStatus'), - Output('menu-parent_id-form-item', 'help', allow_duplicate=True), - Output('menu-menu_name-form-item', 'help', allow_duplicate=True), - Output('menu-order_num-form-item', 'help', allow_duplicate=True), - Output('menu-menu-path-form-item', 'help', allow_duplicate=True), - Output('menu-modal', 'visible'), - Output('menu-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('menu-modal-C-trigger', 'data'), - [State('menu-operations-store-bk', 'data'), - State('menu-edit-id-store', 'data'), - State('menu-parent_id', 'value'), - State('menu-menu_type', 'value'), - State('menu-icon', 'value'), - State('menu-menu_name', 'value'), - State('menu-order_num', 'value'), - State('menu-menu-is_frame', 'value'), - State('menu-menu-path', 'value'), - State('menu-menu-component', 'value'), - State('menu-menu-perms', 'value'), - State('menu-menu-query', 'value'), - State('menu-menu-is_cache', 'value'), - State('menu-menu-visible', 'value'), - State('menu-menu-status', 'value')], + output=dict( + form_validate=[ + Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu-path-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-parent_id-form-item', 'help', allow_duplicate=True), + Output('menu-menu_name-form-item', 'help', allow_duplicate=True), + Output('menu-order_num-form-item', 'help', allow_duplicate=True), + Output('menu-menu-path-form-item', 'help', allow_duplicate=True), + ], + modal_visible=Output('menu-modal', 'visible', allow_duplicate=True), + operations=Output('menu-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('menu-modal-C-trigger', 'data') + ), + state=dict( + modal_type=State('menu-operations-store-bk', 'data'), + edit_row_info=State('menu-edit-id-store', 'data'), + parent_id=State('menu-parent_id', 'value'), + menu_type=State('menu-menu_type', 'value'), + icon=State('menu-icon', 'value'), + menu_name=State('menu-menu_name', 'value'), + order_num=State('menu-order_num', 'value'), + is_frame=State('menu-menu-is_frame', 'value'), + path=State('menu-menu-path', 'value'), + component=State('menu-menu-component', 'value'), + perms=State('menu-menu-perms', 'value'), + query=State('menu-menu-query', 'value'), + is_cache=State('menu-menu-is_cache', 'value'), + visible=State('menu-menu-visible', 'value'), + status=State('menu-menu-status', 'value') + ), prevent_initial_call=True ) -def menu_confirm_menu(confirm_trigger, operation_type, cur_menu_info, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, +def menu_confirm_menu(confirm_trigger, modal_type, edit_row_info, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, component, perms, query, is_cache, visible, status): + """ + 菜单类型为菜单时新增或编辑弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: if all([parent_id, menu_name, order_num, path]): params_add = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, is_frame=is_frame, path=path, component=component, perms=perms, query=query, is_cache=is_cache, visible=visible, status=status) - params_edit = dict(menu_id=cur_menu_info.get('menu_id') if cur_menu_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, + params_edit = dict(menu_id=edit_row_info.get('menu_id') if edit_row_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, is_frame=is_frame, path=path, component=component, perms=perms, query=query, is_cache=is_cache, visible=visible, status=status) api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_menu_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_menu_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] - - return [ - None if parent_id else 'error', - None if menu_name else 'error', - None if order_num else 'error', - None if path else 'error', - None if parent_id else '请选择上级菜单!', - None if menu_name else '请输入菜单名称!', - None if order_num else '请输入显示排序!', - None if path else '请输入路由地址!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_validate=[None] * 8, + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_validate=[None] * 8, + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_validate=[None] * 8, + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) + + return dict( + form_validate=[ + None if parent_id else 'error', + None if menu_name else 'error', + None if order_num else 'error', + None if path else 'error', + None if parent_id else '请选择上级菜单!', + None if menu_name else '请输入菜单名称!', + None if order_num else '请输入显示排序!', + None if path else '请输入路由地址!' + ], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 12 + raise PreventUpdate @app.callback( @@ -130,6 +123,9 @@ def menu_confirm_menu(confirm_trigger, operation_type, cur_menu_info, parent_id, Input('menu-edit-id-store', 'data') ) def set_edit_info(edit_info): + """ + 菜单类型为菜单时回显菜单数据回调 + """ if edit_info: return [ edit_info.get('is_frame'), @@ -142,4 +138,4 @@ def set_edit_info(edit_info): edit_info.get('status') ] - return [dash.no_update] * 8 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py index de196db..dfcdedd 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py @@ -2,6 +2,7 @@ import dash import time import uuid from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_antd_components as fac import feffery_utils_components as fuc @@ -12,22 +13,31 @@ from api.menu import get_menu_tree_api, get_menu_tree_for_edit_option_api, get_m @app.callback( - [Output('menu-list-table', 'data', allow_duplicate=True), - Output('menu-list-table', 'key'), - Output('menu-list-table', 'defaultExpandedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('menu-fold', 'nClicks')], - [Input('menu-search', 'nClicks'), - Input('menu-refresh', 'nClicks'), - Input('menu-operations-store', 'data'), - Input('menu-fold', 'nClicks')], - [State('menu-menu_name-input', 'value'), - State('menu-status-select', 'value'), - State('menu-list-table', 'defaultExpandedRowKeys'), - State('menu-button-perms-container', 'data')], + output=dict( + menu_table_data=Output('menu-list-table', 'data', allow_duplicate=True), + menu_table_key=Output('menu-list-table', 'key'), + menu_table_defaultexpandedrowkeys=Output('menu-list-table', 'defaultExpandedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + fold_click=Output('menu-fold', 'nClicks') + ), + inputs=dict( + search_click=Input('menu-search', 'nClicks'), + refresh_click=Input('menu-refresh', 'nClicks'), + operations=Input('menu-operations-store', 'data'), + fold_click=Input('menu-fold', 'nClicks') + ), + state=dict( + menu_name=State('menu-menu_name-input', 'value'), + status_select=State('menu-status-select', 'value'), + in_default_expanded_row_keys=State('menu-list-table', 'defaultExpandedRowKeys'), + button_perms=State('menu-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_menu_table_data(search_click, refresh_click, operations, fold_click, menu_name, status_select, in_default_expanded_row_keys, button_perms): + """ + 获取菜单表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( menu_name=menu_name, @@ -90,15 +100,40 @@ def get_menu_table_data(search_click, refresh_click, operations, fold_click, men if fold_click: if not in_default_expanded_row_keys: - return [table_data_new, str(uuid.uuid4()), default_expanded_row_keys, {'timestamp': time.time()}, None] - - return [table_data_new, str(uuid.uuid4()), [], {'timestamp': time.time()}, None] - - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}, None] - - return [dash.no_update] * 4 + [None] + return dict( + menu_table_data=table_data_new, + menu_table_key=str(uuid.uuid4()), + menu_table_defaultexpandedrowkeys=default_expanded_row_keys, + api_check_token_trigger={'timestamp': time.time()}, + fold_click=None + ) + + return dict( + menu_table_data=table_data_new, + menu_table_key=str(uuid.uuid4()), + menu_table_defaultexpandedrowkeys=[], + api_check_token_trigger={'timestamp': time.time()}, + fold_click=None + ) + + return dict( + menu_table_data=dash.no_update, + menu_table_key=dash.no_update, + menu_table_defaultexpandedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + fold_click=None + ) + + return dict( + menu_table_data=dash.no_update, + menu_table_key=dash.no_update, + menu_table_defaultexpandedrowkeys=dash.no_update, + api_check_token_trigger=dash.no_update, + fold_click=None + ) +# 重置菜单搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -116,6 +151,7 @@ app.clientside_callback( ) +# 隐藏/显示菜单搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -143,36 +179,55 @@ app.clientside_callback( prevent_initial_call=True ) def get_select_icon(icon): + """ + 获取新增或编辑表单中选择的icon回调 + """ if icon: return [ icon, fac.AntdIcon(icon=icon) ] - return [dash.no_update] * 2 - + raise PreventUpdate @app.callback( - [Output('menu-modal', 'visible', allow_duplicate=True), - Output('menu-modal', 'title'), - Output('menu-parent_id', 'treeData'), - Output('menu-parent_id', 'value'), - Output('menu-menu_type', 'value'), - Output('menu-icon', 'value', allow_duplicate=True), - Output('menu-icon', 'prefix', allow_duplicate=True), - Output('menu-menu_name', 'value'), - Output('menu-order_num', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('menu-edit-id-store', 'data'), - Output('menu-operations-store-bk', 'data')], - [Input({'type': 'menu-operation-button', 'index': ALL}, 'nClicks'), - Input('menu-list-table', 'nClicksButton')], - [State('menu-list-table', 'clickedContent'), - State('menu-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal=dict(visible=Output('menu-modal', 'visible', allow_duplicate=True), title=Output('menu-modal', 'title')), + form_value=dict( + parent_tree=Output('menu-parent_id', 'treeData'), parent_id=Output('menu-parent_id', 'value'), + menu_type=Output('menu-menu_type', 'value'), icon=Output('menu-icon', 'value', allow_duplicate=True), + icon_prefix=Output('menu-icon', 'prefix', allow_duplicate=True), icon_category=Output('icon-category', 'value'), + menu_name=Output('menu-menu_name', 'value'), order_num=Output('menu-order_num', 'value') + ), + form_validate=[ + Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-parent_id-form-item', 'help', allow_duplicate=True), + Output('menu-menu_name-form-item', 'help', allow_duplicate=True), + Output('menu-order_num-form-item', 'help', allow_duplicate=True) + ], + other=dict( + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('menu-edit-id-store', 'data'), + modal_type=Output('menu-operations-store-bk', 'data') + ) + ), + inputs=dict( + operation_click=Input({'type': 'menu-operation-button', 'index': ALL}, 'nClicks'), + button_click=Input('menu-list-table', 'nClicksButton') + ), + state=dict( + clicked_content=State('menu-list-table', 'clickedContent'), + recently_button_clicked_row=State('menu-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_menu_modal(operation_click, button_click, clicked_content, recently_button_clicked_row): + """ + 显示新增或编辑菜单弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'add', 'type': 'menu-operation-button'} or (trigger_id == 'menu-list-table' and clicked_content != '删除'): menu_params = dict(menu_name='') @@ -184,58 +239,70 @@ def add_edit_menu_modal(operation_click, button_click, clicked_content, recently tree_data = tree_info['data'] if trigger_id == {'index': 'add', 'type': 'menu-operation-button'}: - return [ - True, - '新增菜单', - tree_data, - '0', - 'M', - None, - None, - None, - None, - {'timestamp': time.time()}, - None, - {'type': 'add'} - ] + return dict( + modal=dict(visible=True, title='新增菜单'), + form_value=dict( + parent_tree=tree_data, parent_id='0', menu_type='M', icon=None, + icon_prefix=None, icon_category=None, menu_name=None, order_num=None + ), + form_validate=[None] * 6, + other=dict( + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type={'type': 'add'} + ) + ) elif trigger_id == 'menu-list-table' and clicked_content == '新增': - return [ - True, - '新增菜单', - tree_data, - str(recently_button_clicked_row['key']), - 'M', - None, - None, - None, - None, - {'timestamp': time.time()}, - None, - {'type': 'add'} - ] + return dict( + modal=dict(visible=True, title='新增菜单'), + form_value=dict( + parent_tree=tree_data, parent_id=str(recently_button_clicked_row['key']), menu_type='M', + icon=None, icon_prefix=None, icon_category=None, menu_name=None, order_num=None + ), + form_validate=[None] * 6, + other=dict( + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type={'type': 'add'} + ) + ) elif trigger_id == 'menu-list-table' and clicked_content == '修改': menu_id = int(recently_button_clicked_row['key']) menu_info_res = get_menu_detail_api(menu_id=menu_id) if menu_info_res['code'] == 200: menu_info = menu_info_res['data'] - return [ - True, - '编辑菜单', - tree_data, - str(menu_info.get('parent_id')), - menu_info.get('menu_type'), - menu_info.get('icon'), - fac.AntdIcon(icon=menu_info.get('icon')), - menu_info.get('menu_name'), - menu_info.get('order_num'), - {'timestamp': time.time()}, - menu_info, - {'type': 'edit'} - ] - - return [dash.no_update] * 9 + [{'timestamp': time.time()}, None, None] - - return [dash.no_update] * 10 + [None, None] + return dict( + modal=dict(visible=True, title='编辑菜单'), + form_value=dict( + parent_tree=tree_data, parent_id=str(menu_info.get('parent_id')), + menu_type=menu_info.get('menu_type'), icon=menu_info.get('icon'), + icon_prefix=fac.AntdIcon(icon=menu_info.get('icon')), icon_category=menu_info.get('icon'), + menu_name=menu_info.get('menu_name'), order_num=menu_info.get('order_num') + ), + form_validate=[None] * 6, + other=dict( + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=menu_info, + modal_type={'type': 'edit'} + ) + ) + + return dict( + modal=dict(visible=dash.no_update, title=dash.no_update), + form_value=dict( + parent_tree=dash.no_update, parent_id=dash.no_update, menu_type=dash.no_update, + icon=dash.no_update, icon_prefix=dash.no_update, icon_category=dash.no_update, + menu_name=dash.no_update, order_num=dash.no_update + ), + form_validate=[dash.no_update] * 6, + other=dict( + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) + ) + + raise PreventUpdate @app.callback( @@ -258,7 +325,7 @@ def get_bottom_content(menu_value): elif menu_value == 'F': return [button_type.render(), str(uuid.uuid4()), {'type': 'F'}] - return dash.no_update + raise PreventUpdate @app.callback( @@ -293,7 +360,7 @@ def modal_confirm_trigger(confirm, menu_type): {'timestamp': time.time()} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -306,6 +373,9 @@ def modal_confirm_trigger(confirm, menu_type): prevent_initial_call=True ) def menu_delete_modal(button_click, clicked_content, recently_button_clicked_row): + """ + 显示删除菜单二次确认弹窗回调 + """ if button_click: if clicked_content == '删除': @@ -319,7 +389,7 @@ def menu_delete_modal(button_click, clicked_content, recently_button_clicked_row {'menu_ids': menu_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -331,6 +401,9 @@ def menu_delete_modal(button_click, clicked_content, recently_button_clicked_row prevent_initial_call=True ) def menu_delete_confirm(delete_confirm, menu_ids_data): + """ + 删除菜单弹窗确认回调,实现删除操作 + """ if delete_confirm: params = menu_ids_data @@ -348,4 +421,4 @@ def menu_delete_confirm(delete_confirm, menu_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/notice_c.py b/dash-fastapi-frontend/callbacks/system_c/notice_c.py index 953deeb..accdfd3 100644 --- a/dash-fastapi-frontend/callbacks/system_c/notice_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/notice_c.py @@ -5,6 +5,7 @@ import re import json from flask import session from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -14,24 +15,33 @@ from api.dict import query_dict_data_list_api @app.callback( - [Output('notice-list-table', 'data', allow_duplicate=True), - Output('notice-list-table', 'pagination', allow_duplicate=True), - Output('notice-list-table', 'key'), - Output('notice-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('notice-search', 'nClicks'), - Input('notice-refresh', 'nClicks'), - Input('notice-list-table', 'pagination'), - Input('notice-operations-store', 'data')], - [State('notice-notice_title-input', 'value'), - State('notice-update_by-input', 'value'), - State('notice-notice_type-select', 'value'), - State('notice-create_time-range', 'value'), - State('notice-button-perms-container', 'data')], + output=dict( + notice_table_data=Output('notice-list-table', 'data', allow_duplicate=True), + notice_table_pagination=Output('notice-list-table', 'pagination', allow_duplicate=True), + notice_table_key=Output('notice-list-table', 'key'), + notice_table_selectedrowkeys=Output('notice-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('notice-search', 'nClicks'), + refresh_click=Input('notice-refresh', 'nClicks'), + pagination=Input('notice-list-table', 'pagination'), + operations=Input('notice-operations-store', 'data') + ), + state=dict( + notice_title=State('notice-notice_title-input', 'value'), + update_by=State('notice-update_by-input', 'value'), + notice_type=State('notice-notice_type-select', 'value'), + create_time_range=State('notice-create_time-range', 'value'), + button_perms=State('notice-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_notice_table_data(search_click, refresh_click, pagination, operations, notice_title, update_by, notice_type, create_time_range, button_perms): + """ + 获取通知公告表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ create_time_start = None create_time_end = None if create_time_range: @@ -102,13 +112,26 @@ def get_notice_table_data(search_click, refresh_click, pagination, operations, n } if 'system:notice:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + notice_table_data=table_data, + notice_table_pagination=table_pagination, + notice_table_key=str(uuid.uuid4()), + notice_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + notice_table_data=dash.no_update, + notice_table_pagination=dash.no_update, + notice_table_key=dash.no_update, + notice_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置通知公告搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -128,6 +151,7 @@ app.clientside_callback( ) +# 隐藏/显示通知公告搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -154,6 +178,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_notice_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -164,7 +191,7 @@ def change_notice_edit_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -173,17 +200,18 @@ def change_notice_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_notice_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -192,6 +220,9 @@ def change_notice_delete_button_status(table_rows_selected): prevent_initial_call=True ) def init_render_editor(html_string): + """ + 初始化富文本编辑器回调 + """ url = f'{ApiBaseUrlConfig.BaseUrl}/common/uploadForEditor' token = 'Bearer ' + session.get('Authorization') @@ -286,40 +317,57 @@ def init_render_editor(html_string): @app.callback( - [Output('notice-modal', 'visible', allow_duplicate=True), - Output('notice-modal', 'title'), - Output('notice-notice_title', 'value'), - Output('notice-notice_type', 'value'), - Output('notice-status', 'value'), - Output('notice-written-editor-store', 'data'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('notice-edit-id-store', 'data'), - Output('notice-operations-store-bk', 'data')], - [Input({'type': 'notice-operation-button', 'index': ALL}, 'nClicks'), - Input('notice-list-table', 'nClicksButton')], - [State('notice-list-table', 'selectedRowKeys'), - State('notice-list-table', 'clickedContent'), - State('notice-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal=dict(visible=Output('notice-modal', 'visible', allow_duplicate=True), title=Output('notice-modal', 'title')), + form_value=dict( + notice_title=Output('notice-notice_title', 'value'), + notice_type=Output('notice-notice_type', 'value'), + status=Output('notice-status', 'value'), + editor_content=Output('notice-written-editor-store', 'data'), + ), + form_validate=[ + Output('notice-notice_title-form-item', 'validateStatus', allow_duplicate=True), + Output('notice-notice_type-form-item', 'validateStatus', allow_duplicate=True), + Output('notice-notice_title-form-item', 'help', allow_duplicate=True), + Output('notice-notice_type-form-item', 'help', allow_duplicate=True) + ], + other=dict( + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('notice-edit-id-store', 'data'), + modal_type=Output('notice-operations-store-bk', 'data') + ) + ), + inputs=dict( + operation_click=Input({'type': 'notice-operation-button', 'index': ALL}, 'nClicks'), + button_click=Input('notice-list-table', 'nClicksButton') + ), + state=dict( + selected_row_keys=State('notice-list-table', 'selectedRowKeys'), + clicked_content=State('notice-list-table', 'clickedContent'), + recently_button_clicked_row=State('notice-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_notice_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示新增或编辑通知公告弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'add', 'type': 'notice-operation-button'} \ or trigger_id == {'index': 'edit', 'type': 'notice-operation-button'} \ or (trigger_id == 'notice-list-table' and clicked_content == '修改'): if trigger_id == {'index': 'add', 'type': 'notice-operation-button'}: - return [ - True, - '新增通知公告', - None, - None, - '0', - '


', - dash.no_update, - None, - {'type': 'add'} - ] + return dict( + modal=dict(visible=True, title='新增通知公告'), + form_value=dict(notice_title=None, notice_type=None, status='0', editor_content='


'), + form_validate=[None] * 4, + other=dict( + api_check_token_trigger=dash.no_update, + edit_row_info=None, + modal_type={'type': 'add'} + ) + ) elif trigger_id == {'index': 'edit', 'type': 'notice-operation-button'} or (trigger_id == 'notice-list-table' and clicked_content == '修改'): if trigger_id == {'index': 'edit', 'type': 'notice-operation-button'}: notice_id = int(','.join(selected_row_keys)) @@ -330,102 +378,129 @@ def add_edit_notice_modal(operation_click, button_click, selected_row_keys, clic notice_info = notice_info_res['data'] notice_content = notice_info.get('notice_content') - return [ - True, - '编辑通知公告', - notice_info.get('notice_title'), - notice_info.get('notice_type'), - notice_info.get('status'), - re.sub(r"\n", "", notice_content), - {'timestamp': time.time()}, - notice_info if notice_info else None, - {'type': 'edit'} - ] + return dict( + modal=dict(visible=True, title='编辑通知公告'), + form_value=dict( + notice_title=notice_info.get('notice_title'), + notice_type=notice_info.get('notice_type'), + status=notice_info.get('status'), + editor_content=re.sub(r"\n", "", notice_content) + ), + form_validate=[None] * 4, + other=dict( + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=notice_info if notice_info else None, + modal_type={'type': 'edit'} + ) + ) - return [dash.no_update] * 6 + [{'timestamp': time.time()}, None, None] + return dict( + modal=dict(visible=dash.no_update, title=dash.no_update), + form_value=dict( + notice_title=dash.no_update, + notice_type=dash.no_update, + status=dash.no_update, + editor_content=dash.no_update + ), + form_validate=[dash.no_update] * 4, + other=dict( + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) + ) - return [dash.no_update] * 7 + [None, None] + raise PreventUpdate @app.callback( - [Output('notice-notice_title-form-item', 'validateStatus'), - Output('notice-notice_type-form-item', 'validateStatus'), - Output('notice-notice_title-form-item', 'help'), - Output('notice-notice_type-form-item', 'help'), - Output('notice-modal', 'visible'), - Output('notice-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('notice-modal', 'okCounts'), - [State('notice-operations-store-bk', 'data'), - State('notice-edit-id-store', 'data'), - State('notice-notice_title', 'value'), - State('notice-notice_type', 'value'), - State('notice-status', 'value'), - State('notice-content', 'data')], + output=dict( + notice_title_form_status=Output('notice-notice_title-form-item', 'validateStatus', allow_duplicate=True), + notice_type_form_status=Output('notice-notice_type-form-item', 'validateStatus', allow_duplicate=True), + notice_title_form_help=Output('notice-notice_title-form-item', 'help', allow_duplicate=True), + notice_type_form_help=Output('notice-notice_type-form-item', 'help', allow_duplicate=True), + modal_visible=Output('notice-modal', 'visible'), + operations=Output('notice-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('notice-modal', 'okCounts') + ), + state=dict( + modal_type=State('notice-operations-store-bk', 'data'), + edit_row_info=State('notice-edit-id-store', 'data'), + notice_title=State('notice-notice_title', 'value'), + notice_type=State('notice-notice_type', 'value'), + status=State('notice-status', 'value'), + notice_content=State('notice-content', 'data') + ), prevent_initial_call=True ) -def notice_confirm(confirm_trigger, operation_type, cur_notice_info, notice_title, notice_type, status, notice_content): +def notice_confirm(confirm_trigger, modal_type, edit_row_info, notice_title, notice_type, status, notice_content): + """ + 新增或编辑通知公告弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: if all([notice_title, notice_type]): params_add = dict(notice_title=notice_title, notice_type=notice_type, status=status, notice_content=notice_content.get('html')) - params_edit = dict(notice_id=cur_notice_info.get('notice_id') if cur_notice_info else None, + params_edit = dict(notice_id=edit_row_info.get('notice_id') if edit_row_info else None, notice_title=notice_title, notice_type=notice_type, status=status, notice_content=notice_content.get('html')) api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_notice_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_notice_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + notice_title_form_status=None, + notice_type_form_status=None, + notice_title_form_help=None, + notice_type_form_help=None, + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + notice_title_form_status=None, + notice_type_form_status=None, + notice_title_form_help=None, + notice_type_form_help=None, + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + notice_title_form_status=None, + notice_type_form_status=None, + notice_title_form_help=None, + notice_type_form_help=None, + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [ - None if notice_title else 'error', - None if notice_type else 'error', - None if notice_title else '请输入公告标题!', - None if notice_type else '请输入公告类型!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + return dict( + notice_title_form_status=None if notice_title else 'error', + notice_type_form_status=None if notice_type else 'error', + notice_title_form_help=None if notice_title else '请输入公告标题!', + notice_type_form_help=None if notice_type else '请输入公告类型!', + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 8 + raise PreventUpdate @app.callback( @@ -441,6 +516,9 @@ def notice_confirm(confirm_trigger, operation_type, cur_notice_info, notice_titl ) def notice_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示删除通知公告二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'notice-operation-button'} or ( trigger_id == 'notice-list-table' and clicked_content == '删除'): @@ -460,7 +538,7 @@ def notice_delete_modal(operation_click, button_click, {'notice_ids': notice_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -472,6 +550,9 @@ def notice_delete_modal(operation_click, button_click, prevent_initial_call=True ) def notice_delete_confirm(delete_confirm, notice_ids_data): + """ + 删除岗通知公告弹窗确认回调,实现删除操作 + """ if delete_confirm: params = notice_ids_data @@ -489,4 +570,4 @@ def notice_delete_confirm(delete_confirm, notice_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index 30c46dc..1f8985a 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -3,6 +3,7 @@ import time import uuid from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -10,22 +11,31 @@ from api.post import get_post_list_api, get_post_detail_api, add_post_api, edit_ @app.callback( - [Output('post-list-table', 'data', allow_duplicate=True), - Output('post-list-table', 'pagination', allow_duplicate=True), - Output('post-list-table', 'key'), - Output('post-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('post-search', 'nClicks'), - Input('post-refresh', 'nClicks'), - Input('post-list-table', 'pagination'), - Input('post-operations-store', 'data')], - [State('post-post_code-input', 'value'), - State('post-post_name-input', 'value'), - State('post-status-select', 'value'), - State('post-button-perms-container', 'data')], + output=dict( + post_table_data=Output('post-list-table', 'data', allow_duplicate=True), + post_table_pagination=Output('post-list-table', 'pagination', allow_duplicate=True), + post_table_key=Output('post-list-table', 'key'), + post_table_selectedrowkeys=Output('post-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('post-search', 'nClicks'), + refresh_click=Input('post-refresh', 'nClicks'), + pagination=Input('post-list-table', 'pagination'), + operations=Input('post-operations-store', 'data') + ), + state=dict( + post_code=State('post-post_code-input', 'value'), + post_name=State('post-post_name-input', 'value'), + status_select=State('post-status-select', 'value'), + button_perms=State('post-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_post_table_data(search_click, refresh_click, pagination, operations, post_code, post_name, status_select, button_perms): + """ + 获取岗位表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( post_code=post_code, @@ -73,14 +83,26 @@ def get_post_table_data(search_click, refresh_click, pagination, operations, pos 'icon': 'antd-delete' } if 'system:post:remove' in button_perms else {}, ] + return dict( + post_table_data=table_data, + post_table_pagination=table_pagination, + post_table_key=str(uuid.uuid4()), + post_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] - - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + post_table_data=dash.no_update, + post_table_pagination=dash.no_update, + post_table_key=dash.no_update, + post_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置岗位搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -99,6 +121,7 @@ app.clientside_callback( ) +# 隐藏/显示岗位搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -125,6 +148,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_post_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -135,7 +161,7 @@ def change_post_edit_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -144,55 +170,66 @@ def change_post_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_post_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( - [Output('post-modal', 'visible', allow_duplicate=True), - Output('post-modal', 'title'), - Output('post-post_name', 'value'), - Output('post-post_code', 'value'), - Output('post-post_sort', 'value'), - Output('post-status', 'value'), - Output('post-remark', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('post-edit-id-store', 'data'), - Output('post-operations-store-bk', 'data')], - [Input({'type': 'post-operation-button', 'index': ALL}, 'nClicks'), - Input('post-list-table', 'nClicksButton')], - [State('post-list-table', 'selectedRowKeys'), - State('post-list-table', 'clickedContent'), - State('post-list-table', 'recentlyButtonClickedRow')], + output=dict( + modal_visible=Output('post-modal', 'visible', allow_duplicate=True), + modal_title=Output('post-modal', 'title'), + form_value=Output({'type': 'post-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'post-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'post-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('post-edit-id-store', 'data'), + modal_type=Output('post-operations-store-bk', 'data') + ), + inputs=dict( + operation_click=Input({'type': 'post-operation-button', 'index': ALL}, 'nClicks'), + button_click=Input('post-list-table', 'nClicksButton') + ), + state=dict( + selected_row_keys=State('post-list-table', 'selectedRowKeys'), + clicked_content=State('post-list-table', 'clickedContent'), + recently_button_clicked_row=State('post-list-table', 'recentlyButtonClickedRow') + ), prevent_initial_call=True ) def add_edit_post_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示新增或编辑岗位弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'add', 'type': 'post-operation-button'} \ or trigger_id == {'index': 'edit', 'type': 'post-operation-button'} \ or (trigger_id == 'post-list-table' and clicked_content == '修改'): + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] if trigger_id == {'index': 'add', 'type': 'post-operation-button'}: - return [ - True, - '新增岗位', - None, - None, - 0, - '0', - None, - dash.no_update, - None, - {'type': 'add'} - ] + post_info = dict(post_name=None, post_code=None, post_sort=0, status='0', remark=None) + return dict( + modal_visible=True, + modal_title='新增岗位', + form_value=[post_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger=dash.no_update, + edit_row_info=None, + modal_type={'type': 'add'} + ) elif trigger_id == {'index': 'edit', 'type': 'post-operation-button'} or (trigger_id == 'post-list-table' and clicked_content == '修改'): if trigger_id == {'index': 'edit', 'type': 'post-operation-button'}: post_id = int(','.join(selected_row_keys)) @@ -201,112 +238,112 @@ def add_edit_post_modal(operation_click, button_click, selected_row_keys, clicke post_info_res = get_post_detail_api(post_id=post_id) if post_info_res['code'] == 200: post_info = post_info_res['data'] - return [ - True, - '编辑岗位', - post_info.get('post_name'), - post_info.get('post_code'), - post_info.get('post_sort'), - post_info.get('status'), - post_info.get('remark'), - {'timestamp': time.time()}, - post_info if post_info else None, - {'type': 'edit'} - ] - - return [dash.no_update] * 7 + [{'timestamp': time.time()}, None, None] + return dict( + modal_visible=True, + modal_title='编辑岗位', + form_value=[post_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=post_info if post_info else None, + modal_type={'type': 'edit'} + ) + + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) - return [dash.no_update] * 8 + [None, None] + raise PreventUpdate @app.callback( - [Output('post-post_name-form-item', 'validateStatus'), - Output('post-post_code-form-item', 'validateStatus'), - Output('post-post_sort-form-item', 'validateStatus'), - Output('post-post_name-form-item', 'help'), - Output('post-post_code-form-item', 'help'), - Output('post-post_sort-form-item', 'help'), - Output('post-modal', 'visible'), - Output('post-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('post-modal', 'okCounts'), - [State('post-operations-store-bk', 'data'), - State('post-edit-id-store', 'data'), - State('post-post_name', 'value'), - State('post-post_code', 'value'), - State('post-post_sort', 'value'), - State('post-status', 'value'), - State('post-remark', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'post-form-label', 'index': ALL, 'required': True}, 'validateStatus', + allow_duplicate=True), + form_label_validate_info=Output({'type': 'post-form-label', 'index': ALL, 'required': True}, 'help', + allow_duplicate=True), + modal_visible=Output('post-modal', 'visible'), + operations=Output('post-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('post-modal', 'okCounts') + ), + state=dict( + modal_type=State('post-operations-store-bk', 'data'), + edit_row_info=State('post-edit-id-store', 'data'), + form_value=State({'type': 'post-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'post-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def post_confirm(confirm_trigger, operation_type, cur_post_info, post_name, post_code, post_sort, status, remark): +def post_confirm(confirm_trigger, modal_type, edit_row_info, form_value, form_label): + """ + 新增或编辑岗位弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: - if all([post_name, post_code, post_sort]): - params_add = dict(post_name=post_name, post_code=post_code, post_sort=post_sort, status=status, remark=remark) - params_edit = dict(post_id=cur_post_info.get('post_id') if cur_post_info else None, post_name=post_name, - post_code=post_code, post_sort=post_sort, status=status, remark=remark) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + if all([form_value_state.get(k) for k in form_label_output_list]): + params_add = form_value_state + params_edit = params_add.copy() + params_edit['post_id'] = edit_row_info.get('post_id') if edit_row_info else None api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_post_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_post_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] - - return [ - None if post_name else 'error', - None if post_code else 'error', - None if post_sort else 'error', - None if post_name else '请输入岗位名称!', - None if post_code else '请输入岗位编码!', - None if post_sort else '请输入岗位顺序!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) + + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) - return [dash.no_update] * 10 + raise PreventUpdate @app.callback( @@ -322,6 +359,9 @@ def post_confirm(confirm_trigger, operation_type, cur_post_info, post_name, post ) def post_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示删除岗位二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'post-operation-button'} or (trigger_id == 'post-list-table' and clicked_content == '删除'): @@ -331,7 +371,7 @@ def post_delete_modal(operation_click, button_click, if clicked_content == '删除': post_ids = recently_button_clicked_row['key'] else: - return dash.no_update + raise PreventUpdate return [ f'是否确认删除岗位编号为{post_ids}的岗位?', @@ -339,7 +379,7 @@ def post_delete_modal(operation_click, button_click, {'post_ids': post_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -351,6 +391,9 @@ def post_delete_modal(operation_click, button_click, prevent_initial_call=True ) def post_delete_confirm(delete_confirm, post_ids_data): + """ + 删除岗位弹窗确认回调,实现删除操作 + """ if delete_confirm: params = post_ids_data @@ -368,7 +411,7 @@ def post_delete_confirm(delete_confirm, post_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -380,6 +423,9 @@ def post_delete_confirm(delete_confirm, post_ids_data): prevent_initial_call=True ) def export_post_list(export_click): + """ + 导出岗位信息回调 + """ if export_click: export_post_res = export_post_list_api({}) if export_post_res.status_code == 200: @@ -399,7 +445,7 @@ def export_post_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -408,9 +454,12 @@ def export_post_list(export_click): prevent_initial_call=True ) def reset_post_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c/allocate_user_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c/allocate_user_c.py index 3268395..9c3a925 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c/allocate_user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c/allocate_user_c.py @@ -2,6 +2,7 @@ import dash import time import uuid from dash.dependencies import Input, Output, State, ALL, MATCH +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -24,6 +25,9 @@ from api.role import get_allocated_user_list_api, get_unallocated_user_list_api, prevent_initial_call=True ) def get_allocate_user_table_data(search_click, refresh_click, pagination, operations, user_name, phonenumber, role_id, button_perms): + """ + 使用模式匹配回调MATCH模式,根据不同类型获取角色已分配用户列表及未分配用户列表(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( role_id=int(role_id), @@ -76,9 +80,10 @@ def get_allocate_user_table_data(search_click, refresh_click, pagination, operat return [dash.no_update, dash.no_update, dash.no_update, dash.no_update] - return [dash.no_update] * 4 + raise PreventUpdate +# 重置分配用户搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -96,6 +101,7 @@ app.clientside_callback( ) +# 隐藏/显示分配用户搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -122,6 +128,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_allocated_user_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制取批量消授权按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -129,7 +138,7 @@ def change_allocated_user_delete_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -140,11 +149,14 @@ def change_allocated_user_delete_button_status(table_rows_selected): prevent_initial_call=True ) def allocate_user_modal(add_click, unallocated_user): + """ + 分配用户弹框中添加用户按钮回调 + """ if add_click: return [True, unallocated_user + 1 if unallocated_user else 1] - return [dash.no_update] * 2 + raise PreventUpdate @app.callback( @@ -158,6 +170,9 @@ def allocate_user_modal(add_click, unallocated_user): prevent_initial_call=True ) def allocate_user_add_confirm(add_confirm, selected_row_keys, role_id): + """ + 添加用户确认回调,实现给角色分配用户操作 + """ if add_confirm: if selected_row_keys: @@ -185,7 +200,7 @@ def allocate_user_add_confirm(add_confirm, selected_row_keys, role_id): fuc.FefferyFancyMessage('请选择用户', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -201,6 +216,9 @@ def allocate_user_add_confirm(add_confirm, selected_row_keys, role_id): ) def allocate_user_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示取消授权二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.type == 'allocate_user-operation-button' or ( trigger_id.type == 'allocate_user-list-table' and clicked_content == '取消授权'): @@ -219,7 +237,7 @@ def allocate_user_delete_modal(operation_click, button_click, user_ids ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -232,6 +250,9 @@ def allocate_user_delete_modal(operation_click, button_click, prevent_initial_call=True ) def allocate_user_delete_confirm(delete_confirm, user_ids_data, role_id): + """ + 取消授权弹窗确认回调,实现取消授权操作 + """ if delete_confirm: params = {'user_ids': user_ids_data, 'role_ids': role_id} @@ -249,4 +270,4 @@ def allocate_user_delete_confirm(delete_confirm, user_ids_data, role_id): fuc.FefferyFancyMessage('取消授权失败', type='error') ] - return [dash.no_update] * 3 + 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 b419d9b..de81624 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 @@ -3,6 +3,7 @@ import time import uuid from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_antd_components as fac import feffery_utils_components as fuc @@ -12,23 +13,32 @@ from api.menu import get_menu_tree_api @app.callback( - [Output('role-list-table', 'data', allow_duplicate=True), - Output('role-list-table', 'pagination', allow_duplicate=True), - Output('role-list-table', 'key'), - Output('role-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('role-search', 'nClicks'), - Input('role-refresh', 'nClicks'), - Input('role-list-table', 'pagination'), - Input('role-operations-store', 'data')], - [State('role-role_name-input', 'value'), - State('role-role_key-input', 'value'), - State('role-status-select', 'value'), - State('role-create_time-range', 'value'), - State('role-button-perms-container', 'data')], + output=dict( + role_table_data=Output('role-list-table', 'data', allow_duplicate=True), + role_table_pagination=Output('role-list-table', 'pagination', allow_duplicate=True), + role_table_key=Output('role-list-table', 'key'), + role_table_selectedrowkeys=Output('role-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + search_click=Input('role-search', 'nClicks'), + refresh_click=Input('role-refresh', 'nClicks'), + pagination=Input('role-list-table', 'pagination'), + operations=Input('role-operations-store', 'data') + ), + state=dict( + role_name=State('role-role_name-input', 'value'), + role_key=State('role-role_key-input', 'value'), + status_select=State('role-status-select', 'value'), + create_time_range=State('role-create_time-range', 'value'), + button_perms=State('role-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_role_table_data(search_click, refresh_click, pagination, operations, role_name, role_key, status_select, create_time_range, button_perms): + """ + 获取角色表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ create_time_start = None create_time_end = None @@ -69,9 +79,9 @@ def get_role_table_data(search_click, refresh_click, pagination, operations, rol ) for item in table_data: if item['status'] == '0': - item['status'] = dict(checked=True) + item['status'] = dict(checked=True, disabled=item['role_id'] == 1) else: - item['status'] = dict(checked=False) + item['status'] = dict(checked=False, disabled=item['role_id'] == 1) item['key'] = str(item['role_id']) if item['role_id'] == 1: item['operation'] = [] @@ -161,13 +171,26 @@ def get_role_table_data(search_click, refresh_click, pagination, operations, rol ] ) - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + role_table_data=table_data, + role_table_pagination=table_pagination, + role_table_key=str(uuid.uuid4()), + role_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + role_table_data=dash.no_update, + role_table_pagination=dash.no_update, + role_table_key=dash.no_update, + role_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置角色搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -187,6 +210,7 @@ app.clientside_callback( ) +# 隐藏/显示角色搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -213,6 +237,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_role_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -232,13 +259,14 @@ def change_role_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_role_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: if '1' in table_rows_selected: return True - if len(table_rows_selected) > 1: - return False return False @@ -254,6 +282,9 @@ def change_role_delete_button_status(table_rows_selected): prevent_initial_call=True ) def fold_unfold_role_menu(fold_unfold, menu_info): + """ + 新增和编辑表单中展开/折叠checkbox回调 + """ if menu_info: default_expanded_keys = [] for item in menu_info: @@ -275,6 +306,9 @@ def fold_unfold_role_menu(fold_unfold, menu_info): prevent_initial_call=True ) def all_none_role_menu_mode(all_none, menu_info): + """ + 新增和编辑表单中全选/全不选checkbox回调 + """ if menu_info: default_expanded_keys = [] for item in menu_info: @@ -297,6 +331,9 @@ def all_none_role_menu_mode(all_none, menu_info): prevent_initial_call=True ) def change_role_menu_mode(parent_children, current_role_menu): + """ + 新增和编辑表单中父子联动checkbox回调 + """ checked_menu = [] if parent_children: if current_role_menu: @@ -316,53 +353,63 @@ def change_role_menu_mode(parent_children, current_role_menu): @app.callback( - [Output('role-modal', 'visible', allow_duplicate=True), - Output('role-modal', 'title'), - Output('role-role_name', 'value'), - Output('role-role_key', 'value'), - Output('role-role_sort', 'value'), - Output('role-status', 'value'), - Output('role-menu-perms', 'treeData'), - Output('role-menu-perms', 'expandedKeys', allow_duplicate=True), - Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), - Output('role-menu-perms', 'halfCheckedKeys', allow_duplicate=True), - Output('role-menu-store', 'data'), - Output('current-role-menu-store', 'data'), - Output('role-remark', 'value'), - Output('api-check-token', 'data', allow_duplicate=True), - Output('role-edit-id-store', 'data'), - Output('role-operations-store-bk', 'data')], - [Input({'type': 'role-operation-button', 'operation': ALL}, 'nClicks'), - Input({'type': 'role-operation-table', 'operation': ALL, 'index': ALL}, 'nClicks')], - State('role-list-table', 'selectedRowKeys'), + output=dict( + modal_visible=Output('role-modal', 'visible', allow_duplicate=True), + modal_title=Output('role-modal', 'title'), + form_value=Output({'type': 'role-form-value', 'index': ALL, 'required': ALL}, 'value'), + form_label_validate_status=Output({'type': 'role-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'role-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + menu_perms_tree=Output('role-menu-perms', 'treeData'), + menu_perms_expandedkeys=Output('role-menu-perms', 'expandedKeys', allow_duplicate=True), + menu_perms_checkedkeys=Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), + menu_perms_halfcheckedkeys=Output('role-menu-perms', 'halfCheckedKeys', allow_duplicate=True), + role_menu=Output('role-menu-store', 'data'), + current_role_menu=Output('current-role-menu-store', 'data'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True), + edit_row_info=Output('role-edit-id-store', 'data'), + modal_type=Output('role-operations-store-bk', 'data') + ), + inputs=dict( + operation_click=Input({'type': 'role-operation-button', 'operation': ALL}, 'nClicks'), + button_click=Input({'type': 'role-operation-table', 'operation': ALL, 'index': ALL}, 'nClicks') + ), + state=dict( + selected_row_keys=State('role-list-table', 'selectedRowKeys') + ), prevent_initial_call=True ) def add_edit_role_modal(operation_click, button_click, selected_row_keys): + """ + 显示新增或编辑角色弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.operation in ['add', 'edit']: + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] menu_params = dict(menu_name='', type='role') tree_info = get_menu_tree_api(menu_params) if tree_info.get('code') == 200: tree_data = tree_info['data'] if trigger_id.type == 'role-operation-button' and trigger_id.operation == 'add': - return [ - True, - '新增角色', - None, - None, - None, - '0', - tree_data[0], - [], - None, - None, - tree_data[1], - None, - None, - {'timestamp': time.time()}, - None, - {'type': 'add'} - ] + role_info = dict(role_name=None, role_key=None, role_sort=None, status='0', remark=None) + return dict( + modal_visible=True, + modal_title='新增角色', + form_value=[role_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + menu_perms_tree=tree_data[0], + menu_perms_expandedkeys=[], + menu_perms_checkedkeys=None, + menu_perms_halfcheckedkeys=None, + role_menu=tree_data[1], + current_role_menu=None, + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type={'type': 'add'} + ) elif trigger_id.operation == 'edit': if trigger_id.type == 'role-operation-button': role_id = int(','.join(selected_row_keys)) @@ -384,125 +431,134 @@ def add_edit_role_modal(operation_click, button_click, selected_row_keys): if not has_children: checked_menu.append(str(item.get('menu_id'))) half_checked_menu = [x for x in checked_menu_all if x not in checked_menu] - return [ - True, - '编辑角色', - role_info.get('role').get('role_name'), - role_info.get('role').get('role_key'), - role_info.get('role').get('role_sort'), - role_info.get('role').get('status'), - tree_data[0], - [], - checked_menu, - half_checked_menu, - tree_data[1], - role_info.get('menu'), - role_info.get('role').get('remark'), - {'timestamp': time.time()}, - role_info.get('role') if role_info else None, - {'type': 'edit'} - ] - - return [dash.no_update] * 13 + [{'timestamp': time.time()}, None, None] - - return [dash.no_update] * 14 + [None, None] + return dict( + modal_visible=True, + modal_title='编辑角色', + form_value=[role_info.get('role').get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + menu_perms_tree=tree_data[0], + menu_perms_expandedkeys=[], + menu_perms_checkedkeys=checked_menu, + menu_perms_halfcheckedkeys=half_checked_menu, + role_menu=tree_data[1], + current_role_menu=role_info.get('menu'), + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=role_info.get('role') if role_info else None, + modal_type={'type': 'edit'} + ) + + return dict( + modal_visible=dash.no_update, + modal_title=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_value_list), + form_label_validate_info=[dash.no_update] * len(form_value_list), + menu_perms_tree=dash.no_update, + menu_perms_expandedkeys=dash.no_update, + menu_perms_checkedkeys=dash.no_update, + menu_perms_halfcheckedkeys=dash.no_update, + role_menu=dash.no_update, + current_role_menu=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + edit_row_info=None, + modal_type=None + ) + + raise PreventUpdate @app.callback( - [Output('role-role_name-form-item', 'validateStatus'), - Output('role-role_Key-form-item', 'validateStatus'), - Output('role-role_sort-form-item', 'validateStatus'), - Output('role-role_name-form-item', 'help'), - Output('role-role_Key-form-item', 'help'), - Output('role-role_sort-form-item', 'help'), - Output('role-modal', 'visible'), - Output('role-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('role-modal', 'okCounts'), - [State('role-operations-store-bk', 'data'), - State('role-edit-id-store', 'data'), - State('role-role_name', 'value'), - State('role-role_key', 'value'), - State('role-role_sort', 'value'), - State('role-status', 'value'), - State('role-menu-perms', 'checkedKeys'), - State('role-menu-perms', 'halfCheckedKeys'), - State('role-menu-perms-radio-parent-children', 'checked'), - State('role-remark', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'role-form-label', 'index': ALL, 'required': True}, 'validateStatus', + allow_duplicate=True), + form_label_validate_info=Output({'type': 'role-form-label', 'index': ALL, 'required': True}, 'help', + allow_duplicate=True), + modal_visible=Output('role-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-modal', 'okCounts') + ), + state=dict( + modal_type=State('role-operations-store-bk', 'data'), + edit_row_info=State('role-edit-id-store', 'data'), + form_value=State({'type': 'role-form-value', 'index': ALL, 'required': ALL}, 'value'), + form_label=State({'type': 'role-form-value', 'index': ALL, 'required': True}, 'placeholder'), + menu_checked_keys=State('role-menu-perms', 'checkedKeys'), + menu_half_checked_keys=State('role-menu-perms', 'halfCheckedKeys'), + parent_checked=State('role-menu-perms-radio-parent-children', 'checked') + ), prevent_initial_call=True ) -def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role_key, role_sort, status, menu_checked_keys, menu_half_checked_keys, parent_checked, remark): +def role_confirm(confirm_trigger, modal_type, edit_row_info, form_value, form_label, menu_checked_keys, menu_half_checked_keys, parent_checked): + """ + 新增或编辑角色弹窗确认回调,实现新增或编辑操作 + """ if confirm_trigger: - if all([role_name, role_key, role_sort]): + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[3]} + if all([form_value_state.get(k) for k in form_label_output_list]): + menu_half_checked_keys = menu_half_checked_keys if menu_half_checked_keys else [] + menu_checked_keys = menu_checked_keys if menu_checked_keys else [] if parent_checked: menu_perms = menu_half_checked_keys + menu_checked_keys else: menu_perms = menu_checked_keys - params_add = dict(role_name=role_name, role_key=role_key, role_sort=role_sort, menu_id=','.join(menu_perms) if menu_perms else None, status=status, remark=remark) - params_edit = dict(role_id=cur_role_info.get('role_id') if cur_role_info else None, role_name=role_name, role_key=role_key, role_sort=role_sort, - menu_id=','.join(menu_perms) if menu_perms else '', status=status, remark=remark) + params_add = form_value_state + params_add['menu_id'] = ','.join(menu_perms) if menu_perms else None + params_edit = params_add.copy() + params_edit['role_id'] = edit_row_info.get('role_id') if edit_row_info else None api_res = {} - operation_type = operation_type.get('type') - if operation_type == 'add': + modal_type = modal_type.get('type') + if modal_type == 'add': api_res = add_role_api(params_add) - if operation_type == 'edit': + if modal_type == 'edit': api_res = edit_role_api(params_edit) if api_res.get('code') == 200: - if operation_type == 'add': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - if operation_type == 'edit': - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] - - return [ - None if role_name else 'error', - None if role_key else 'error', - None if role_sort else 'error', - None if role_name else '请输入角色名称!', - None if role_key else '请输入权限字符!', - None if role_sort else '请输入角色排序!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('处理失败', type='error') - ] + if modal_type == 'add': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + if modal_type == 'edit': + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) - return [dash.no_update] * 10 + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('处理失败', type='error') + ) + + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else form_label_state.get(k) for k in form_label_output_list], + 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 @app.callback( @@ -515,6 +571,9 @@ def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role prevent_initial_call=True ) def table_switch_role_status(recently_switch_data_index, recently_switch_status, recently_switch_row): + """ + 表格内切换角色状态回调 + """ if recently_switch_data_index: if recently_switch_status: params = dict(role_id=int(recently_switch_row['key']), status='0', type='status') @@ -530,12 +589,12 @@ def table_switch_role_status(recently_switch_data_index, recently_switch_status, ] return [ - dash.no_update, + {'type': 'switch-status'}, {'timestamp': time.time()}, fuc.FefferyFancyMessage('修改失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -548,6 +607,9 @@ def table_switch_role_status(recently_switch_data_index, recently_switch_status, prevent_initial_call=True ) def role_delete_modal(operation_click, button_click, selected_row_keys): + """ + 显示删除角色二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.operation == 'delete': @@ -557,7 +619,7 @@ def role_delete_modal(operation_click, button_click, selected_row_keys): if trigger_id.type == 'role-operation-table': role_ids = trigger_id.index else: - return dash.no_update + raise PreventUpdate return [ f'是否确认删除角色编号为{role_ids}的角色?', @@ -565,7 +627,7 @@ def role_delete_modal(operation_click, button_click, selected_row_keys): {'role_ids': role_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -577,6 +639,9 @@ def role_delete_modal(operation_click, button_click, selected_row_keys): prevent_initial_call=True ) def role_delete_confirm(delete_confirm, role_ids_data): + """ + 删除角色弹窗确认回调,实现删除操作 + """ if delete_confirm: params = role_ids_data @@ -594,7 +659,7 @@ def role_delete_confirm(delete_confirm, role_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -606,6 +671,9 @@ def role_delete_confirm(delete_confirm, role_ids_data): prevent_initial_call=True ) def role_to_allocated_user_modal(allocated_click, allocated_user_search_nclick): + """ + 显示角色分配用户弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.operation == 'allocation': return [ @@ -614,7 +682,7 @@ def role_to_allocated_user_modal(allocated_click, allocated_user_search_nclick): trigger_id.index ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -626,6 +694,9 @@ def role_to_allocated_user_modal(allocated_click, allocated_user_search_nclick): prevent_initial_call=True ) def export_role_list(export_click): + """ + 导出角色信息回调 + """ if export_click: export_role_res = export_role_list_api({}) if export_role_res.status_code == 200: @@ -645,7 +716,7 @@ def export_role_list(export_click): fuc.FefferyFancyMessage('导出失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -654,22 +725,12 @@ def export_role_list(export_click): prevent_initial_call=True ) def reset_role_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update - - -# 由于采用了自定义单元格元素,路由变化时需要重置selectedRows,不然会报错 -app.clientside_callback( - ''' - (url) => { - return null; - } - ''', - Output('role-list-table', 'selectedRows'), - Input('url-container', 'pathname'), - prevent_initial_call=True -) + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/allocate_role_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/allocate_role_c.py index a62052b..5ddf787 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/allocate_role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/allocate_role_c.py @@ -2,6 +2,7 @@ import dash import time import uuid from dash.dependencies import Input, Output, State, ALL, MATCH +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -24,6 +25,9 @@ from api.user import get_allocated_role_list_api, get_unallocated_role_list_api, prevent_initial_call=True ) def get_allocate_role_table_data(search_click, refresh_click, pagination, operations, role_name, role_key, user_id, button_perms): + """ + 使用模式匹配回调MATCH模式,根据不同类型获取用户已分配角色列表及未分配角色列表(进行表格相关增删查改操作后均会触发此回调) + """ query_params = dict( user_id=int(user_id), @@ -76,9 +80,10 @@ def get_allocate_role_table_data(search_click, refresh_click, pagination, operat return [dash.no_update, dash.no_update, dash.no_update, dash.no_update] - return [dash.no_update] * 4 + raise PreventUpdate +# 重置分配角色搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -96,6 +101,7 @@ app.clientside_callback( ) +# 隐藏/显示分配角色搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -122,6 +128,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_allocated_role_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制取批量消授权按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -129,7 +138,7 @@ def change_allocated_role_delete_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -140,11 +149,14 @@ def change_allocated_role_delete_button_status(table_rows_selected): prevent_initial_call=True ) def allocate_role_modal(add_click, unallocated_role): + """ + 分配角色弹框中添加角色按钮回调 + """ if add_click: return [True, unallocated_role + 1 if unallocated_role else 1] - return [dash.no_update] * 2 + raise PreventUpdate @app.callback( @@ -158,6 +170,9 @@ def allocate_role_modal(add_click, unallocated_role): prevent_initial_call=True ) def allocate_user_add_confirm(add_confirm, selected_row_keys, user_id): + """ + 添加角色确认回调,实现给用户分配角色操作 + """ if add_confirm: if selected_row_keys: @@ -185,7 +200,7 @@ def allocate_user_add_confirm(add_confirm, selected_row_keys, user_id): fuc.FefferyFancyMessage('请选择角色', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -201,6 +216,9 @@ def allocate_user_add_confirm(add_confirm, selected_row_keys, user_id): ) def allocate_role_delete_modal(operation_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + """ + 显示取消授权二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id.type == 'allocate_role-operation-button' or ( trigger_id.type == 'allocate_role-list-table' and clicked_content == '取消授权'): @@ -219,7 +237,7 @@ def allocate_role_delete_modal(operation_click, button_click, role_ids ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -232,6 +250,9 @@ def allocate_role_delete_modal(operation_click, button_click, prevent_initial_call=True ) def allocate_role_delete_confirm(delete_confirm, role_ids_data, user_id): + """ + 取消授权弹窗确认回调,实现取消授权操作 + """ if delete_confirm: params = {'user_ids': user_id, 'role_ids': role_ids_data} @@ -249,4 +270,4 @@ def allocate_role_delete_confirm(delete_confirm, role_ids_data, user_id): fuc.FefferyFancyMessage('取消授权失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py index 1e2cb44..0930ec4 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py @@ -3,6 +3,7 @@ import feffery_utils_components as fuc import time import uuid from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate from server import app from api.user import change_user_avatar_api @@ -16,10 +17,13 @@ from api.user import change_user_avatar_api prevent_initial_call=True ) def avatar_cropper_modal_visible(n_clicks, user_avatar_image_info): + """ + 显示编辑头像弹窗回调 + """ if n_clicks: return [True, user_avatar_image_info] - return dash.no_update, dash.no_update + raise PreventUpdate @app.callback( @@ -28,11 +32,14 @@ def avatar_cropper_modal_visible(n_clicks, user_avatar_image_info): prevent_initial_call=True ) def upload_user_avatar(list_upload_task_record): + """ + 上传用户头像获取后端url回调 + """ if list_upload_task_record: return list_upload_task_record[-1].get('url') - return dash.no_update + raise PreventUpdate @app.callback( @@ -41,6 +48,9 @@ def upload_user_avatar(list_upload_task_record): prevent_initial_call=True ) def edit_user_avatar(src_data): + """ + 使用cropper.js编辑头像回调 + """ return """ // 创建新图像元素 @@ -127,6 +137,9 @@ def edit_user_avatar(src_data): prevent_initial_call=True ) def change_user_avatar_callback(submit_click, avatar_data): + """ + 提交编辑完成头像数据回调,实现更新头像操作 + """ if submit_click: params = dict(type='avatar', avatar=avatar_data['avatarBase64']) @@ -149,4 +162,4 @@ def change_user_avatar_callback(submit_click, avatar_data): dash.no_update ] - return [dash.no_update] * 5 + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py index a5457d8..ca81b66 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py @@ -2,6 +2,7 @@ import dash import feffery_utils_components as fuc import time from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate from server import app from api.user import reset_user_password_api @@ -23,6 +24,9 @@ from api.user import reset_user_password_api prevent_initial_call=True ) def reset_submit_user_info(reset_click, old_password, new_password, confirm_password): + """ + 重置当前用户密码回调 + """ if reset_click: if all([old_password, new_password, confirm_password]): @@ -76,7 +80,7 @@ def reset_submit_user_info(reset_click, old_password, new_password, confirm_pass fuc.FefferyFancyMessage('修改失败', type='error'), ] - return [dash.no_update] * 8 + raise PreventUpdate @app.callback( @@ -85,7 +89,10 @@ def reset_submit_user_info(reset_click, old_password, new_password, confirm_pass prevent_initial_call=True ) def close_personal_info_modal(close_click): + """ + 关闭当前个人资料标签页回调 + """ if close_click: return '个人资料' - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py index 6cdef31..6b2769d 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py @@ -2,6 +2,7 @@ import dash import feffery_utils_components as fuc import time from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate from server import app from api.user import change_user_info_api @@ -24,6 +25,9 @@ from api.user import change_user_info_api prevent_initial_call=True ) def reset_submit_user_info(reset_click, nick_name, phonenumber, email, sex): + """ + 修改当前用户信息回调 + """ if reset_click: if all([nick_name, phonenumber, email]): @@ -64,7 +68,7 @@ def reset_submit_user_info(reset_click, nick_name, phonenumber, email, sex): fuc.FefferyFancyMessage('修改失败', type='error'), ] - return [dash.no_update] * 8 + raise PreventUpdate @app.callback( @@ -73,7 +77,10 @@ def reset_submit_user_info(reset_click, nick_name, phonenumber, email, sex): prevent_initial_call=True ) def close_personal_info_modal(close_click): + """ + 关闭当前个人资料标签页回调 + """ if close_click: return '个人资料' - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py index 9e3ebbc..b5c03cc 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py @@ -3,6 +3,7 @@ import time import uuid from dash import dcc from dash.dependencies import Input, Output, State, ALL +from dash.exceptions import PreventUpdate import feffery_utils_components as fuc from server import app @@ -30,25 +31,34 @@ def get_search_dept_tree(dept_input): @app.callback( - [Output('user-list-table', 'data', allow_duplicate=True), - Output('user-list-table', 'pagination', allow_duplicate=True), - Output('user-list-table', 'key'), - Output('user-list-table', 'selectedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('dept-tree', 'selectedKeys'), - Input('user-search', 'nClicks'), - Input('user-refresh', 'nClicks'), - Input('user-list-table', 'pagination'), - Input('user-operations-store', 'data')], - [State('user-user_name-input', 'value'), - State('user-phone_number-input', 'value'), - State('user-status-select', 'value'), - State('user-create_time-range', 'value'), - State('user-button-perms-container', 'data')], + output=dict( + user_table_data=Output('user-list-table', 'data', allow_duplicate=True), + user_table_pagination=Output('user-list-table', 'pagination', allow_duplicate=True), + user_table_key=Output('user-list-table', 'key'), + user_table_selectedrowkeys=Output('user-list-table', 'selectedRowKeys'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + selected_dept_tree=Input('dept-tree', 'selectedKeys'), + search_click=Input('user-search', 'nClicks'), + refresh_click=Input('user-refresh', 'nClicks'), + pagination=Input('user-list-table', 'pagination'), + operations=Input('user-operations-store', 'data') + ), + state=dict( + user_name=State('user-user_name-input', 'value'), + phone_number=State('user-phone_number-input', 'value'), + status_select=State('user-status-select', 'value'), + create_time_range=State('user-create_time-range', 'value'), + button_perms=State('user-button-perms-container', 'data') + ), prevent_initial_call=True ) def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, refresh_click, pagination, operations, user_name, phone_number, status_select, create_time_range, button_perms): + """ + 获取用户表格数据回调(进行表格相关增删查改操作后均会触发此回调) + """ dept_id = None create_time_start = None create_time_end = None @@ -119,13 +129,26 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, refresh_c } if 'system:user:edit' in button_perms else None ] - return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + return dict( + user_table_data=table_data, + user_table_pagination=table_pagination, + user_table_key=str(uuid.uuid4()), + user_table_selectedrowkeys=None, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return dict( + user_table_data=dash.no_update, + user_table_pagination=dash.no_update, + user_table_key=dash.no_update, + user_table_selectedrowkeys=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate +# 重置用户搜索表单数据回调 app.clientside_callback( ''' (reset_click) => { @@ -146,6 +169,7 @@ app.clientside_callback( ) +# 隐藏/显示用户搜索表单回调 app.clientside_callback( ''' (hidden_click, hidden_status) => { @@ -172,6 +196,9 @@ app.clientside_callback( prevent_initial_call=True ) def change_user_edit_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制编辑按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: @@ -182,7 +209,7 @@ def change_user_edit_button_status(table_rows_selected): return True - return dash.no_update + raise PreventUpdate @app.callback( @@ -191,32 +218,49 @@ def change_user_edit_button_status(table_rows_selected): prevent_initial_call=True ) def change_user_delete_button_status(table_rows_selected): + """ + 根据选择的表格数据行数控制删除按钮状态回调 + """ outputs_list = dash.ctx.outputs_list if outputs_list: if table_rows_selected: if '1' in table_rows_selected: return True - if len(table_rows_selected) > 1: - return False return False return True - return dash.no_update + raise PreventUpdate @app.callback( - [Output('user-add-modal', 'visible', allow_duplicate=True), - Output('user-add-dept_id', 'treeData'), - Output('user-add-post', 'options'), - Output('user-add-role', 'options'), - Output('api-check-token', 'data', allow_duplicate=True)], - Input('user-add', 'nClicks'), + output=dict( + modal_visible=Output('user-add-modal', 'visible', allow_duplicate=True), + dept_tree=Output({'type': 'user_add-form-value', 'index': 'dept_id'}, 'treeData'), + form_value=Output({'type': 'user_add-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'user_add-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'user_add-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + user_post=Output('user-add-post', 'value'), + user_role=Output('user-add-role', 'value'), + post_option=Output('user-add-post', 'options'), + role_option=Output('user-add-role', 'options'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + add_click=Input('user-add', 'nClicks') + ), prevent_initial_call=True ) def add_user_modal(add_click): + """ + 显示新增用户弹窗回调 + """ if add_click: + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] dept_params = dict(dept_name='') tree_info = get_dept_tree_api(dept_params) post_option_info = get_post_select_option_api() @@ -225,126 +269,138 @@ def add_user_modal(add_click): tree_data = tree_info['data'] post_option = post_option_info['data'] role_option = role_option_info['data'] + user_info = dict(nick_name=None, dept_id=None, phonenumber=None, email=None, user_name=None, password=None, sex=None, status='0', remark=None) + + return dict( + modal_visible=True, + dept_tree=tree_data, + form_value=[user_info.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + user_post=None, + user_role=None, + post_option=[dict(label=item['post_name'], value=item['post_id']) for item in post_option], + role_option=[dict(label=item['role_name'], value=item['role_id']) for item in role_option], + api_check_token_trigger={'timestamp': time.time()} + ) - return [ - True, - tree_data, - [dict(label=item['post_name'], value=item['post_id']) for item in post_option], - [dict(label=item['role_name'], value=item['role_id']) for item in role_option], - {'timestamp': time.time()} - ] - - return [dash.no_update] * 4 + [{'timestamp': time.time()}] + return dict( + modal_visible=dash.no_update, + dept_tree=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + user_post=dash.no_update, + user_role=dash.no_update, + post_option=dash.no_update, + role_option=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 5 + raise PreventUpdate @app.callback( - [Output('user-add-nick_name-form-item', 'validateStatus'), - Output('user-add-user_name-form-item', 'validateStatus'), - Output('user-add-password-form-item', 'validateStatus'), - Output('user-add-nick_name-form-item', 'help'), - Output('user-add-user_name-form-item', 'help'), - Output('user-add-password-form-item', 'help'), - Output('user-add-modal', 'visible', allow_duplicate=True), - Output('user-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('user-add-modal', 'okCounts'), - [State('user-add-nick_name', 'value'), - State('user-add-dept_id', 'value'), - State('user-add-phone_number', 'value'), - State('user-add-email', 'value'), - State('user-add-user_name', 'value'), - State('user-add-password', 'value'), - State('user-add-sex', 'value'), - State('user-add-status', 'value'), - State('user-add-post', 'value'), - State('user-add-role', 'value'), - State('user-add-remark', 'value')], + output=dict( + form_label_validate_status=Output({'type': 'user_add-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'user_add-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + modal_visible=Output('user-add-modal', 'visible', allow_duplicate=True), + operations=Output('user-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( + add_confirm=Input('user-add-modal', 'okCounts') + ), + state=dict( + post=State('user-add-post', 'value'), + role=State('user-add-role', 'value'), + form_value=State({'type': 'user_add-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'user_add-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def usr_add_confirm(add_confirm, nick_name, dept_id, phone_number, email, user_name, password, sex, status, post, role, - remark): +def usr_add_confirm(add_confirm, post, role, form_value, form_label): if add_confirm: - - if all([nick_name, user_name, password]): - params = dict(nick_name=nick_name, dept_id=dept_id, phonenumber=phone_number, - email=email, user_name=user_name, password=password, sex=sex, - status=status, post_id=','.join(map(str, post)) if post else '', - role_id=','.join(map(str, role)) if role else '', remark=remark) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + + if all([form_value_state.get(k) for k in form_label_output_list]): + params = form_value_state + params['post_id'] = ','.join(map(str, post)) if post else '' + params['role_id'] = ','.join(map(str, role)) if role else '' add_button_result = add_user_api(params) if add_button_result['code'] == 200: - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增失败', type='error') - ] + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'add'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增成功', type='success') + ) + + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增失败', type='error') + ) - return [ - None if nick_name else 'error', - None if user_name else 'error', - None if password else 'error', - None if nick_name else '请输入用户昵称!', - None if user_name else '请输入用户名称!', - None if password else '请输入用户密码!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增失败', type='error') - ] + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('新增失败', type='error') + ) - return [dash.no_update] * 10 + raise PreventUpdate @app.callback( - [Output('user-edit-modal', 'visible', allow_duplicate=True), - Output('user-edit-dept_id', 'treeData'), - Output('user-edit-post', 'options'), - Output('user-edit-role', 'options'), - Output('user-edit-nick_name', 'value'), - Output('user-edit-dept_id', 'value'), - Output('user-edit-phone_number', 'value'), - Output('user-edit-email', 'value'), - Output('user-edit-sex', 'value'), - Output('user-edit-status', 'value'), - Output('user-edit-post', 'value'), - Output('user-edit-role', 'value'), - Output('user-edit-remark', 'value'), - Output('user-edit-id-store', 'data'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input({'type': 'user-operation-button', 'index': ALL}, 'nClicks'), - Input('user-list-table', 'nClicksDropdownItem')], - [State('user-list-table', 'selectedRowKeys'), - State('user-list-table', 'recentlyClickedDropdownItemTitle'), - State('user-list-table', 'recentlyDropdownItemClickedRow')], + output=dict( + modal_visible=Output('user-edit-modal', 'visible', allow_duplicate=True), + dept_tree=Output({'type': 'user_edit-form-value', 'index': 'dept_id'}, 'treeData'), + form_value=Output({'type': 'user_edit-form-value', 'index': ALL}, 'value'), + form_label_validate_status=Output({'type': 'user_edit-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'user_edit-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + user_post=Output('user-edit-post', 'value'), + user_role=Output('user-edit-role', 'value'), + post_option=Output('user-edit-post', 'options'), + role_option=Output('user-edit-role', 'options'), + edit_row_info=Output('user-edit-id-store', 'data'), + api_check_token_trigger=Output('api-check-token', 'data', allow_duplicate=True) + ), + inputs=dict( + operation_click=Input({'type': 'user-operation-button', 'index': ALL}, 'nClicks'), + dropdown_click=Input('user-list-table', 'nClicksDropdownItem') + ), + state=dict( + selected_row_keys=State('user-list-table', 'selectedRowKeys'), + recently_clicked_dropdown_item_title=State('user-list-table', 'recentlyClickedDropdownItemTitle'), + recently_dropdown_item_clicked_row=State('user-list-table', 'recentlyDropdownItemClickedRow') + ), prevent_initial_call=True ) def user_edit_modal(operation_click, dropdown_click, selected_row_keys, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + """ + 显示编辑用户弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'edit', 'type': 'user-operation-button'} or (trigger_id == 'user-list-table' and recently_clicked_dropdown_item_title == '修改'): + # 获取所有输出表单项对应value的index + form_value_list = [x['id']['index'] for x in dash.ctx.outputs_list[2]] + # 获取所有输出表单项对应label的index + form_label_list = [x['id']['index'] for x in dash.ctx.outputs_list[3]] dept_params = dict(dept_name='') tree_data = get_dept_tree_api(dept_params)['data'] @@ -357,97 +413,111 @@ def user_edit_modal(operation_click, dropdown_click, if recently_clicked_dropdown_item_title == '修改': user_id = int(recently_dropdown_item_clicked_row['key']) else: - return [dash.no_update] * 15 + raise PreventUpdate edit_button_info = get_user_detail_api(user_id) if edit_button_info['code'] == 200: edit_button_result = edit_button_info['data'] user = edit_button_result['user'] - dept = edit_button_result['dept'] role = edit_button_result['role'] post = edit_button_result['post'] - return [ - True, - tree_data, - [dict(label=item['post_name'], value=item['post_id']) for item in post_option if item] or [], - [dict(label=item['role_name'], value=item['role_id']) for item in role_option if item] or [], - user['nick_name'], - dept['dept_id'] if dept else None, - user['phonenumber'], - user['email'], - user['sex'], - user['status'], - [item['post_id'] for item in post if item] or [], - [item['role_id'] for item in role if item] or [], - user['remark'], - {'user_id': user_id}, - {'timestamp': time.time()} - ] + return dict( + modal_visible=True, + dept_tree=tree_data, + form_value=[user.get(k) for k in form_value_list], + form_label_validate_status=[None] * len(form_label_list), + form_label_validate_info=[None] * len(form_label_list), + user_post=[item['post_id'] for item in post if item] or [], + user_role=[item['role_id'] for item in role if item] or [], + post_option=[dict(label=item['post_name'], value=item['post_id']) for item in post_option if item] or [], + role_option=[dict(label=item['role_name'], value=item['role_id']) for item in role_option if item] or [], + edit_row_info={'user_id': user_id}, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 14 + [{'timestamp': time.time()}] + return dict( + modal_visible=dash.no_update, + dept_tree=dash.no_update, + form_value=[dash.no_update] * len(form_value_list), + form_label_validate_status=[dash.no_update] * len(form_label_list), + form_label_validate_info=[dash.no_update] * len(form_label_list), + user_post=dash.no_update, + user_role=dash.no_update, + post_option=dash.no_update, + role_option=dash.no_update, + edit_row_info=dash.no_update, + api_check_token_trigger={'timestamp': time.time()} + ) - return [dash.no_update] * 15 + raise PreventUpdate @app.callback( - [Output('user-edit-nick_name-form-item', 'validateStatus'), - Output('user-edit-nick_name-form-item', 'help'), - Output('user-edit-modal', 'visible', allow_duplicate=True), - Output('user-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('user-edit-modal', 'okCounts'), - [State('user-edit-nick_name', 'value'), - State('user-edit-dept_id', 'value'), - State('user-edit-phone_number', 'value'), - State('user-edit-email', 'value'), - State('user-edit-sex', 'value'), - State('user-edit-status', 'value'), - State('user-edit-post', 'value'), - State('user-edit-role', 'value'), - State('user-edit-remark', 'value'), - State('user-edit-id-store', 'data')], + output=dict( + form_label_validate_status=Output({'type': 'user_edit-form-label', 'index': ALL, 'required': True}, 'validateStatus', allow_duplicate=True), + form_label_validate_info=Output({'type': 'user_edit-form-label', 'index': ALL, 'required': True}, 'help', allow_duplicate=True), + modal_visible=Output('user-edit-modal', 'visible', allow_duplicate=True), + operations=Output('user-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( + edit_confirm=Input('user-edit-modal', 'okCounts') + ), + state=dict( + post=State('user-edit-post', 'value'), + role=State('user-edit-role', 'value'), + edit_row_info=State('user-edit-id-store', 'data'), + form_value=State({'type': 'user_edit-form-value', 'index': ALL}, 'value'), + form_label=State({'type': 'user_edit-form-label', 'index': ALL, 'required': True}, 'label') + ), prevent_initial_call=True ) -def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, status, post, role, remark, user_id): +def usr_edit_confirm(edit_confirm, edit_row_info, post, role, form_value, form_label): if edit_confirm: - - if all([nick_name]): - params = dict(user_id=user_id['user_id'], nick_name=nick_name, dept_id=dept_id if dept_id else -1, - phonenumber=phone_number, email=email, sex=sex, status=status, - post_id=','.join(map(str, post)), role_id=','.join(map(str, role)), remark=remark) + # 获取所有输出表单项对应label的index + form_label_output_list = [x['id']['index'] for x in dash.ctx.outputs_list[0]] + # 获取所有输入表单项对应的value及label + form_value_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-2]} + form_label_state = {x['id']['index']: x.get('value') for x in dash.ctx.states_list[-1]} + + if all([form_value_state.get(k) for k in form_label_output_list]): + params = form_value_state + params['user_id'] = edit_row_info.get('user_id') if edit_row_info else None + params['post_id'] = ','.join(map(str, post)) if post else '' + params['role_id'] = ','.join(map(str, role)) if role else '' edit_button_result = edit_user_api(params) if edit_button_result['code'] == 200: - return [ - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - - return [ - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑失败', type='error') - ] + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=False, + operations={'type': 'edit'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑成功', type='success') + ) + + return dict( + form_label_validate_status=[None] * len(form_label_output_list), + form_label_validate_info=[None] * len(form_label_output_list), + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑失败', type='error') + ) - return [ - None if nick_name else 'error', - None if nick_name else '请输入用户昵称!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑失败', type='error') - ] + return dict( + form_label_validate_status=[None if form_value_state.get(k) else 'error' for k in form_label_output_list], + form_label_validate_info=[None if form_value_state.get(k) else f'{form_label_state.get(k)}不能为空!' for k in form_label_output_list], + modal_visible=dash.no_update, + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('编辑失败', type='error') + ) - return [dash.no_update] * 6 + raise PreventUpdate @app.callback( @@ -460,6 +530,9 @@ def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, prevent_initial_call=True ) def table_switch_user_status(recently_switch_data_index, recently_switch_status, recently_switch_row): + """ + 表格内切换用户状态回调 + """ if recently_switch_data_index: if recently_switch_status: params = dict(user_id=int(recently_switch_row['key']), status='0', type='status') @@ -475,12 +548,12 @@ def table_switch_user_status(recently_switch_data_index, recently_switch_status, ] return [ - dash.no_update, + {'type': 'switch-status'}, {'timestamp': time.time()}, fuc.FefferyFancyMessage('修改失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -496,6 +569,9 @@ def table_switch_user_status(recently_switch_data_index, recently_switch_status, ) def user_delete_modal(operation_click, dropdown_click, selected_row_keys, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + """ + 显示删除用户二次确认弹窗回调 + """ trigger_id = dash.ctx.triggered_id if trigger_id == {'index': 'delete', 'type': 'user-operation-button'} or (trigger_id == 'user-list-table' and recently_clicked_dropdown_item_title == '删除'): @@ -505,7 +581,7 @@ def user_delete_modal(operation_click, dropdown_click, if recently_clicked_dropdown_item_title == '删除': user_ids = recently_dropdown_item_clicked_row['key'] else: - return [dash.no_update] * 3 + raise PreventUpdate return [ f'是否确认删除用户编号为{user_ids}的用户?', @@ -513,7 +589,7 @@ def user_delete_modal(operation_click, dropdown_click, {'user_ids': user_ids} ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -525,6 +601,9 @@ def user_delete_modal(operation_click, dropdown_click, prevent_initial_call=True ) def user_delete_confirm(delete_confirm, user_ids_data): + """ + 删除用户弹窗确认回调,实现删除操作 + """ if delete_confirm: params = user_ids_data @@ -542,7 +621,7 @@ def user_delete_confirm(delete_confirm, user_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -555,11 +634,14 @@ def user_delete_confirm(delete_confirm, user_ids_data): prevent_initial_call=True ) def user_reset_password_modal(dropdown_click, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + """ + 显示重置用户密码弹窗回调 + """ if dropdown_click: if recently_clicked_dropdown_item_title == '重置密码': user_id = recently_dropdown_item_clicked_row['key'] else: - return [dash.no_update] * 3 + raise PreventUpdate return [ True, @@ -567,7 +649,7 @@ def user_reset_password_modal(dropdown_click, recently_clicked_dropdown_item_tit None ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -580,6 +662,9 @@ def user_reset_password_modal(dropdown_click, recently_clicked_dropdown_item_tit prevent_initial_call=True ) def user_reset_password_confirm(reset_confirm, user_id_data, reset_password): + """ + 重置用户密码弹窗确认回调,实现重置密码操作 + """ if reset_confirm: user_id_data['password'] = reset_password @@ -598,7 +683,7 @@ def user_reset_password_confirm(reset_confirm, user_id_data, reset_password): fuc.FefferyFancyMessage('重置失败', type='error') ] - return [dash.no_update] * 3 + raise PreventUpdate @app.callback( @@ -612,6 +697,9 @@ def user_reset_password_confirm(reset_confirm, user_id_data, reset_password): prevent_initial_call=True ) def role_to_allocated_user_modal(dropdown_click, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row, allocated_role_search_nclick): + """ + 显示用户分配角色弹窗回调 + """ if dropdown_click and recently_clicked_dropdown_item_title == '分配角色': return [ @@ -620,9 +708,10 @@ def role_to_allocated_user_modal(dropdown_click, recently_clicked_dropdown_item_ recently_dropdown_item_clicked_row['key'] ] - return [dash.no_update] * 3 + raise PreventUpdate +# 显示用户导入弹窗及重置上传弹窗组件状态回调 app.clientside_callback( ''' (nClicks) => { @@ -652,52 +741,61 @@ app.clientside_callback( @app.callback( - [Output('user-import-confirm-modal', 'confirmLoading'), - Output('batch-result-modal', 'visible'), - Output('batch-result-content', 'children'), - Output('user-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('user-import-confirm-modal', 'okCounts'), - [State('user-upload-choose', 'listUploadTaskRecord'), - State('user-import-update-check', 'checked')], + output=dict( + confirm_loading=Output('user-import-confirm-modal', 'confirmLoading'), + modal_visible=Output('batch-result-modal', 'visible'), + batch_result=Output('batch-result-content', 'children'), + operations=Output('user-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( + import_confirm=Input('user-import-confirm-modal', 'okCounts') + ), + state=dict( + list_upload_task_record=State('user-upload-choose', 'listUploadTaskRecord'), + is_update=State('user-import-update-check', 'checked') + ), prevent_initial_call=True ) def user_import_confirm(import_confirm, list_upload_task_record, is_update): + """ + 用户导入弹窗确认回调,实现批量导入用户操作 + """ if import_confirm: if list_upload_task_record: url = list_upload_task_record[-1].get('url') batch_param = dict(url=url, is_update=is_update) batch_import_result = batch_import_user_api(batch_param) if batch_import_result.get('code') == 200: - return [ - False, - True if batch_import_result.get('message') else False, - batch_import_result.get('message'), - {'type': 'batch-import'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('导入成功', type='success') - ] - - return [ - False, - True, - batch_import_result.get('message'), - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('导入失败', type='error') - ] + return dict( + confirm_loading=False, + modal_visible=True if batch_import_result.get('message') else False, + batch_result=batch_import_result.get('message'), + operations={'type': 'batch-import'}, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('导入成功', type='success') + ) + + return dict( + confirm_loading=False, + modal_visible=True, + batch_result=batch_import_result.get('message'), + operations=dash.no_update, + api_check_token_trigger={'timestamp': time.time()}, + global_message_container=fuc.FefferyFancyMessage('导入失败', type='error') + ) else: - return [ - False, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update, - fuc.FefferyFancyMessage('请上传需要导入的文件', type='error') - ] + return dict( + confirm_loading=False, + modal_visible=dash.no_update, + batch_result=dash.no_update, + operations=dash.no_update, + api_check_token_trigger=dash.no_update, + global_message_container=fuc.FefferyFancyMessage('请上传需要导入的文件', type='error') + ) - return [dash.no_update] * 6 + raise PreventUpdate @app.callback( @@ -710,6 +808,9 @@ def user_import_confirm(import_confirm, list_upload_task_record, is_update): prevent_initial_call=True ) def export_user_list(export_click, download_click): + """ + 导出用户信息回调 + """ trigger_id = dash.ctx.triggered_id if export_click or download_click: @@ -751,7 +852,7 @@ def export_user_list(export_click, download_click): fuc.FefferyFancyMessage('下载失败', type='error') ] - return [dash.no_update] * 4 + raise PreventUpdate @app.callback( @@ -760,9 +861,12 @@ def export_user_list(export_click, download_click): prevent_initial_call=True ) def reset_user_export_status(data): + """ + 导出完成后重置下载组件数据回调,防止重复下载文件 + """ time.sleep(0.5) if data: return None - return dash.no_update + raise PreventUpdate diff --git a/dash-fastapi-frontend/views/monitor/job/__init__.py b/dash-fastapi-frontend/views/monitor/job/__init__.py index 2ac8bf4..7785e34 100644 --- a/dash-fastapi-frontend/views/monitor/job/__init__.py +++ b/dash-fastapi-frontend/views/monitor/job/__init__.py @@ -415,13 +415,20 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='job-job_name', + id={ + 'type': 'job-form-value', + 'index': 'job_name' + }, placeholder='请输入任务名称', style={ 'width': '100%' } ), - id='job-job_name-form-item', + id={ + 'type': 'job-form-label', + 'index': 'job_name', + 'required': True + }, required=True, label='任务名称', labelCol={ @@ -436,14 +443,21 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdSelect( - id='job-job_group', + id={ + 'type': 'job-form-value', + 'index': 'job_group' + }, placeholder='请选择任务分组', options=option, style={ 'width': '100%' } ), - id='job-job_group-form-item', + id={ + 'type': 'job-form-label', + 'index': 'job_group', + 'required': False + }, label='任务分组', labelCol={ 'span': 6 @@ -462,13 +476,20 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='job-invoke_target', + id={ + 'type': 'job-form-value', + 'index': 'invoke_target' + }, placeholder='请输入调用目标字符串', style={ 'width': '100%' } ), - id='job-invoke_target-form-item', + id={ + 'type': 'job-form-label', + 'index': 'invoke_target', + 'required': True + }, required=True, label='调用方法', labelCol={ @@ -488,13 +509,20 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='job-job_args', + id={ + 'type': 'job-form-value', + 'index': 'job_args' + }, placeholder='请输入位置参数', style={ 'width': '100%' } ), - id='job-job_args-form-item', + id={ + 'type': 'job-form-label', + 'index': 'job_args', + 'required': False + }, label='位置参数', labelCol={ 'span': 6 @@ -508,13 +536,20 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='job-job_kwargs', + id={ + 'type': 'job-form-value', + 'index': 'job_kwargs' + }, placeholder='请输入关键字参数', style={ 'width': '100%' } ), - id='job-job_kwargs-form-item', + id={ + 'type': 'job-form-label', + 'index': 'job_kwargs', + 'required': False + }, label='关键字参数', labelCol={ 'span': 6 @@ -533,7 +568,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='job-cron_expression', + id={ + 'type': 'job-form-value', + 'index': 'cron_expression' + }, placeholder='请输入cron执行表达式', addonAfter=html.Div( [ @@ -550,7 +588,11 @@ def render(button_perms): 'width': '100%' } ), - id='job-cron_expression-form-item', + id={ + 'type': 'job-form-label', + 'index': 'cron_expression', + 'required': True + }, required=True, label='cron表达式', labelCol={ @@ -570,7 +612,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdRadioGroup( - id='job-misfire_policy', + id={ + 'type': 'job-form-value', + 'index': 'misfire_policy' + }, options=[ { 'label': '立即执行', @@ -589,7 +634,11 @@ def render(button_perms): optionType='button', buttonStyle='solid' ), - id='job-misfire_policy-form-item', + id={ + 'type': 'job-form-label', + 'index': 'misfire_policy', + 'required': False + }, label='执行策略', labelCol={ 'span': 3 @@ -608,7 +657,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdRadioGroup( - id='job-concurrent', + id={ + 'type': 'job-form-value', + 'index': 'concurrent' + }, options=[ { 'label': '允许', @@ -623,7 +675,11 @@ def render(button_perms): optionType='button', buttonStyle='solid' ), - id='job-concurrent-form-item', + id={ + 'type': 'job-form-label', + 'index': 'concurrent', + 'required': False + }, label='是否并发', labelCol={ 'span': 6 @@ -637,7 +693,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdRadioGroup( - id='job-status', + id={ + 'type': 'job-form-value', + 'index': 'status' + }, options=[ { 'label': '正常', @@ -650,7 +709,11 @@ def render(button_perms): ], defaultValue='0', ), - id='job-status-form-item', + id={ + 'type': 'job-form-label', + 'index': 'status', + 'required': False + }, label='状态', labelCol={ 'span': 6 @@ -707,10 +770,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-job_name-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'job_name' + } + ), label='任务名称', required=True, - id='job_detail-job_name-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'job_name' + }, labelCol={ 'span': 8 }, @@ -722,10 +793,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-job_group-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'job_group' + } + ), label='任务分组', required=True, - id='job_detail-job_group-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'job_group' + }, labelCol={ 'span': 8 }, @@ -742,10 +821,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-job_executor-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'job_executor' + } + ), label='任务执行器', required=True, - id='job_detail-job_executor-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'job_executor' + }, labelCol={ 'span': 8 }, @@ -757,10 +844,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-invoke_target-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'invoke_target' + } + ), label='调用目标函数', required=True, - id='job_detail-invoke_target-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'invoke_target' + }, labelCol={ 'span': 8 }, @@ -777,10 +872,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-job_args-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'job_args' + } + ), label='位置参数', required=True, - id='job_detail-job_args-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'job_args' + }, labelCol={ 'span': 8 }, @@ -792,10 +895,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-job_kwargs-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'job_kwargs' + } + ), label='关键字参数', required=True, - id='job_detail-job_kwargs-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'job_kwargs' + }, labelCol={ 'span': 8 }, @@ -812,10 +923,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-cron_expression-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'cron_expression' + } + ), label='cron表达式', required=True, - id='job_detail-cron_expression-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'cron_expression' + }, labelCol={ 'span': 4 }, @@ -831,10 +950,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-misfire_policy-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'misfire_policy' + } + ), label='执行策略', required=True, - id='job_detail-misfire_policy-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'misfire_policy' + }, labelCol={ 'span': 8 }, @@ -846,10 +973,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-concurrent-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'concurrent' + } + ), label='是否并发', required=True, - id='job_detail-concurrent-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'concurrent' + }, labelCol={ 'span': 8 }, @@ -866,10 +1001,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-status-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'status' + } + ), label='任务状态', required=True, - id='job_detail-status-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'status' + }, labelCol={ 'span': 8 }, @@ -881,10 +1024,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_detail-create_time-text'), + fac.AntdText( + id={ + 'type': 'job_detail-form-value', + 'index': 'create_time' + } + ), label='创建时间', required=True, - id='job_detail-create_time-form-item', + id={ + 'type': 'job_detail-form-label', + 'index': 'create_time' + }, labelCol={ 'span': 8 }, diff --git a/dash-fastapi-frontend/views/monitor/job/job_log.py b/dash-fastapi-frontend/views/monitor/job/job_log.py index a62f2b2..ba5f97d 100644 --- a/dash-fastapi-frontend/views/monitor/job/job_log.py +++ b/dash-fastapi-frontend/views/monitor/job/job_log.py @@ -5,7 +5,6 @@ import callbacks.monitor_c.job_c.job_log_c def render(button_perms): - return [ dcc.Store(id='job_log-button-perms-container', data=button_perms), # 用于导出成功后重置dcc.Download的状态,防止多次下载文件 @@ -311,7 +310,7 @@ def render(button_perms): gutter=5 ), - # 任务调度日志明细modal + # 任务调度日志明细modal,表单项id使用字典类型,index与后端数据库字段一一对应 fac.AntdModal( [ fac.AntdForm( @@ -320,10 +319,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-job_name-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'job_name' + } + ), label='任务名称', required=True, - id='job_log-job_name-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'job_name' + }, labelCol={ 'span': 8 }, @@ -335,10 +342,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-job_group-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'job_group' + } + ), label='任务分组', required=True, - id='job_log-job_group-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'job_group' + }, labelCol={ 'span': 8 }, @@ -355,10 +370,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-job_executor-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'job_executor' + } + ), label='任务执行器', required=True, - id='job_log-job_executor-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'job_executor' + }, labelCol={ 'span': 8 }, @@ -370,10 +393,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-invoke_target-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'invoke_target' + } + ), label='调用目标字符串', required=True, - id='job_log-invoke_target-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'invoke_target' + }, labelCol={ 'span': 8 }, @@ -390,10 +421,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-job_args-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'job_args' + } + ), label='位置参数', required=True, - id='job_log-job_args-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'job_args' + }, labelCol={ 'span': 8 }, @@ -405,10 +444,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-job_kwargs-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'job_kwargs' + } + ), label='关键字参数', required=True, - id='job_log-job_kwargs-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'job_kwargs' + }, labelCol={ 'span': 8 }, @@ -425,10 +472,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-job_trigger-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'job_trigger' + } + ), label='任务触发器', required=True, - id='job_log-job_trigger-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'job_trigger' + }, labelCol={ 'span': 4 }, @@ -444,10 +499,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-job_message-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'job_message' + } + ), label='日志信息', required=True, - id='job_log-job_message-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'job_message' + }, labelCol={ 'span': 4 }, @@ -463,10 +526,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-status-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'status' + } + ), label='执行状态', required=True, - id='job_log-status-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'status' + }, labelCol={ 'span': 8 }, @@ -478,10 +549,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-create_time-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'create_time' + } + ), label='执行时间', required=True, - id='job_log-create_time-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'create_time' + }, labelCol={ 'span': 8 }, @@ -498,10 +577,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='job_log-exception_info-text'), + fac.AntdText( + id={ + 'type': 'job_log-form-value', + 'index': 'exception_info' + } + ), label='异常信息', required=True, - id='job_log-exception_info-form-item', + id={ + 'type': 'job_log-form-label', + 'index': 'exception_info' + }, labelCol={ 'span': 4 }, diff --git a/dash-fastapi-frontend/views/monitor/operlog/__init__.py b/dash-fastapi-frontend/views/monitor/operlog/__init__.py index 7e41ce5..1ee38e0 100644 --- a/dash-fastapi-frontend/views/monitor/operlog/__init__.py +++ b/dash-fastapi-frontend/views/monitor/operlog/__init__.py @@ -396,10 +396,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-title-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'title' + } + ), label='操作模块', required=True, - id='operation_log-title-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'title' + }, labelCol={ 'span': 8 }, @@ -411,10 +419,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-oper_url-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'oper_url' + } + ), label='请求地址', required=True, - id='operation_log-oper_url-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'oper_url' + }, labelCol={ 'span': 8 }, @@ -431,10 +447,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-login_info-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'login_info' + } + ), label='登录信息', required=True, - id='operation_log-login_info-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'login_info' + }, labelCol={ 'span': 8 }, @@ -446,10 +470,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-request_method-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'request_method' + } + ), label='请求方式', required=True, - id='operation_log-request_method-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'request_method' + }, labelCol={ 'span': 8 }, @@ -466,10 +498,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-method-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'method' + } + ), label='操作方法', required=True, - id='operation_log-method-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'method' + }, labelCol={ 'span': 4 }, @@ -485,10 +525,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-oper_param-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'oper_param' + } + ), label='请求参数', required=True, - id='operation_log-oper_param-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'oper_param' + }, labelCol={ 'span': 4 }, @@ -504,10 +552,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-json_result-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'json_result' + } + ), label='返回参数', required=True, - id='operation_log-json_result-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'json_result' + }, labelCol={ 'span': 4 }, @@ -523,10 +579,18 @@ def render(button_perms): [ fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-status-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'status' + } + ), label='操作状态', required=True, - id='operation_log-status-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'status' + }, labelCol={ 'span': 12 }, @@ -538,10 +602,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-cost_time-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'cost_time' + } + ), label='消耗时间', required=True, - id='operation_log-cost_time-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'cost_time' + }, labelCol={ 'span': 12 }, @@ -553,10 +625,18 @@ def render(button_perms): ), fac.AntdCol( fac.AntdFormItem( - fac.AntdText(id='operation_log-oper_time-text'), + fac.AntdText( + id={ + 'type': 'operation_log-form-value', + 'index': 'oper_time' + } + ), label='操作时间', required=True, - id='operation_log-oper_time-form-item', + id={ + 'type': 'operation_log-form-label', + 'index': 'oper_time' + }, labelCol={ 'span': 8 }, diff --git a/dash-fastapi-frontend/views/system/config/__init__.py b/dash-fastapi-frontend/views/system/config/__init__.py index c6aabb0..9a34575 100644 --- a/dash-fastapi-frontend/views/system/config/__init__.py +++ b/dash-fastapi-frontend/views/system/config/__init__.py @@ -393,7 +393,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='config-config_name', + id={ + 'type': 'config-form-value', + 'index': 'config_name' + }, placeholder='请输入参数名称', allowClear=True, style={ @@ -402,7 +405,11 @@ def render(button_perms): ), label='参数名称', required=True, - id='config-config_name-form-item' + id={ + 'type': 'config-form-label', + 'index': 'config_name', + 'required': True + } ), span=24 ), @@ -413,7 +420,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='config-config_key', + id={ + 'type': 'config-form-value', + 'index': 'config_key' + }, placeholder='请输入参数键名', allowClear=True, style={ @@ -422,7 +432,11 @@ def render(button_perms): ), label='参数键名', required=True, - id='config-config_key-form-item' + id={ + 'type': 'config-form-label', + 'index': 'config_key', + 'required': True + } ), span=24 ), @@ -433,7 +447,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='config-config_value', + id={ + 'type': 'config-form-value', + 'index': 'config_value' + }, placeholder='请输入参数键值', allowClear=True, style={ @@ -442,7 +459,11 @@ def render(button_perms): ), label='参数键值', required=True, - id='config-config_value-form-item' + id={ + 'type': 'config-form-label', + 'index': 'config_value', + 'required': True + } ), span=24 ), @@ -453,7 +474,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdRadioGroup( - id='config-config_type', + id={ + 'type': 'config-form-value', + 'index': 'config_type' + }, options=[ { 'label': '是', @@ -470,7 +494,11 @@ def render(button_perms): } ), label='系统内置', - id='config-config_type-form-item' + id={ + 'type': 'config-form-label', + 'index': 'config_type', + 'required': False + } ), span=24 ), @@ -481,7 +509,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='config-remark', + id={ + 'type': 'config-form-value', + 'index': 'remark' + }, placeholder='请输入内容', allowClear=True, mode='text-area', @@ -490,7 +521,11 @@ def render(button_perms): } ), label='备注', - id='config-remark-form-item' + id={ + 'type': 'config-form-label', + 'index': 'remark', + 'required': False + } ), span=24 ), diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py index b5d2a29..bc8de1b 100644 --- a/dash-fastapi-frontend/views/system/dept/__init__.py +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -329,7 +329,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdTreeSelect( - id='dept-parent_id', + id={ + 'type': 'dept-form-value', + 'index': 'parent_id' + }, placeholder='请选择上级部门', treeData=[], treeNodeFilterProp='title', @@ -339,7 +342,11 @@ def render(button_perms): ), label='上级部门', required=True, - id='dept-parent_id-form-item', + id={ + 'type': 'dept-form-label', + 'index': 'parent_id', + 'required': True + }, labelCol={ 'span': 4 }, @@ -360,7 +367,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dept-dept_name', + id={ + 'type': 'dept-form-value', + 'index': 'dept_name' + }, placeholder='请输入部门名称', allowClear=True, style={ @@ -369,14 +379,21 @@ def render(button_perms): ), label='部门名称', required=True, - id='dept-dept_name-form-item' + id={ + 'type': 'dept-form-label', + 'index': 'dept_name', + 'required': True + } ), span=12 ), fac.AntdCol( fac.AntdFormItem( fac.AntdInputNumber( - id='dept-order_num', + id={ + 'type': 'dept-form-value', + 'index': 'order_num' + }, min=0, style={ 'width': '100%' @@ -384,7 +401,11 @@ def render(button_perms): ), label='显示顺序', required=True, - id='dept-order_num-form-item' + id={ + 'type': 'dept-form-label', + 'index': 'order_num', + 'required': True + } ), span=12 ) @@ -396,7 +417,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dept-leader', + id={ + 'type': 'dept-form-value', + 'index': 'leader' + }, placeholder='请输入负责人', allowClear=True, style={ @@ -404,14 +428,21 @@ def render(button_perms): } ), label='负责人', - id='dept-leader-form-item' + id={ + 'type': 'dept-form-label', + 'index': 'leader', + 'required': False + } ), span=12 ), fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dept-phone', + id={ + 'type': 'dept-form-value', + 'index': 'phone' + }, placeholder='请输入联系电话', allowClear=True, style={ @@ -419,7 +450,11 @@ def render(button_perms): } ), label='联系电话', - id='dept-phone-form-item' + id={ + 'type': 'dept-form-label', + 'index': 'phone', + 'required': False + } ), span=12 ), @@ -431,7 +466,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dept-email', + id={ + 'type': 'dept-form-value', + 'index': 'email' + }, placeholder='请输入邮箱', allowClear=True, style={ @@ -439,14 +477,21 @@ def render(button_perms): } ), label='邮箱', - id='dept-email-form-item' + id={ + 'type': 'dept-form-label', + 'index': 'email', + 'required': False + } ), span=12 ), fac.AntdCol( fac.AntdFormItem( fac.AntdRadioGroup( - id='dept-status', + id={ + 'type': 'dept-form-value', + 'index': 'status' + }, options=[ { 'label': '正常', @@ -463,7 +508,11 @@ def render(button_perms): } ), label='部门状态', - id='dept-status-form-item' + id={ + 'type': 'dept-form-label', + 'index': 'status', + 'required': False + } ), span=12 ), diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index a3b0a1f..608f59e 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -394,7 +394,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_type-dict_name', + id={ + 'type': 'dict_type-form-value', + 'index': 'dict_name' + }, placeholder='请输入字典名称', allowClear=True, style={ @@ -403,7 +406,11 @@ def render(button_perms): ), label='字典名称', required=True, - id='dict_type-dict_name-form-item' + id={ + 'type': 'dict_type-form-label', + 'index': 'dict_name', + 'required': True + } ), span=24 ), @@ -414,7 +421,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_type-dict_type', + id={ + 'type': 'dict_type-form-value', + 'index': 'dict_type' + }, placeholder='请输入字典类型', allowClear=True, style={ @@ -423,7 +433,11 @@ def render(button_perms): ), label='字典类型', required=True, - id='dict_type-dict_type-form-item' + id={ + 'type': 'dict_type-form-label', + 'index': 'dict_type', + 'required': True + } ), span=24 ), @@ -434,7 +448,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdRadioGroup( - id='dict_type-status', + id={ + 'type': 'dict_type-form-value', + 'index': 'status' + }, options=[ { 'label': '正常', @@ -451,7 +468,11 @@ def render(button_perms): } ), label='状态', - id='dict_type-status-form-item' + id={ + 'type': 'dict_type-form-label', + 'index': 'status', + 'required': False + } ), span=24 ), @@ -462,7 +483,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_type-remark', + id={ + 'type': 'dict_type-form-value', + 'index': 'remark' + }, placeholder='请输入内容', allowClear=True, mode='text-area', @@ -471,7 +495,11 @@ def render(button_perms): } ), label='备注', - id='dict_type-remark-form-item' + id={ + 'type': 'dict_type-form-label', + 'index': 'remark', + 'required': False + } ), span=24 ), diff --git a/dash-fastapi-frontend/views/system/dict/dict_data.py b/dash-fastapi-frontend/views/system/dict/dict_data.py index 57aa97e..6352390 100644 --- a/dash-fastapi-frontend/views/system/dict/dict_data.py +++ b/dash-fastapi-frontend/views/system/dict/dict_data.py @@ -333,7 +333,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_data-dict_type', + id={ + 'type': 'dict_data-form-value', + 'index': 'dict_type' + }, placeholder='请输入字典类型', disabled=True, style={ @@ -341,7 +344,11 @@ def render(button_perms): } ), label='字典类型', - id='dict_data-dict_type-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'dict_type', + 'required': False + } ), span=24 ), @@ -352,7 +359,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_data-dict_label', + id={ + 'type': 'dict_data-form-value', + 'index': 'dict_label' + }, placeholder='请输入数据标签', allowClear=True, style={ @@ -361,7 +371,11 @@ def render(button_perms): ), label='数据标签', required=True, - id='dict_data-dict_label-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'dict_label', + 'required': True + } ), span=24 ), @@ -372,7 +386,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_data-dict_value', + id={ + 'type': 'dict_data-form-value', + 'index': 'dict_value' + }, placeholder='请输入数据键值', allowClear=True, style={ @@ -381,7 +398,11 @@ def render(button_perms): ), label='数据键值', required=True, - id='dict_data-dict_value-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'dict_value', + 'required': True + } ), span=24 ), @@ -392,7 +413,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_data-css_class', + id={ + 'type': 'dict_data-form-value', + 'index': 'css_class' + }, placeholder='请输入样式属性', allowClear=True, style={ @@ -400,7 +424,11 @@ def render(button_perms): } ), label='样式属性', - id='dict_data-css_class-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'css_class', + 'required': False + } ), span=24 ), @@ -411,7 +439,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInputNumber( - id='dict_data-dict_sort', + id={ + 'type': 'dict_data-form-value', + 'index': 'dict_sort' + }, defaultValue=0, min=0, style={ @@ -420,7 +451,11 @@ def render(button_perms): ), label='显示排序', required=True, - id='dict_data-dict_sort-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'dict_sort', + 'required': True + } ), span=24 ), @@ -431,7 +466,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdSelect( - id='dict_data-list_class', + id={ + 'type': 'dict_data-form-value', + 'index': 'list_class' + }, placeholder='回显样式', options=[ { @@ -464,7 +502,11 @@ def render(button_perms): } ), label='回显样式', - id='dict_data-list_class-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'list_class', + 'required': False + } ), span=24 ), @@ -475,7 +517,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdRadioGroup( - id='dict_data-status', + id={ + 'type': 'dict_data-form-value', + 'index': 'status' + }, options=[ { 'label': '正常', @@ -492,7 +537,11 @@ def render(button_perms): } ), label='状态', - id='dict_data-status-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'status', + 'required': False + } ), span=24 ), @@ -503,7 +552,10 @@ def render(button_perms): fac.AntdCol( fac.AntdFormItem( fac.AntdInput( - id='dict_data-remark', + id={ + 'type': 'dict_data-form-value', + 'index': 'remark' + }, placeholder='请输入内容', allowClear=True, mode='text-area', @@ -512,7 +564,11 @@ def render(button_perms): } ), label='备注', - id='dict_data-remark-form-item' + id={ + 'type': 'dict_data-form-label', + 'index': 'remark', + 'required': False + } ), span=24 ), diff --git a/dash-fastapi-frontend/views/system/post/__init__.py b/dash-fastapi-frontend/views/system/post/__init__.py index a3110e8..af668e8 100644 --- a/dash-fastapi-frontend/views/system/post/__init__.py +++ b/dash-fastapi-frontend/views/system/post/__init__.py @@ -360,7 +360,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='post-post_name', + id={ + 'type': 'post-form-value', + 'index': 'post_name' + }, placeholder='请输入岗位名称', allowClear=True, style={ @@ -369,11 +372,18 @@ def render(button_perms): ), label='岗位名称', required=True, - id='post-post_name-form-item' + id={ + 'type': 'post-form-label', + 'index': 'post_name', + 'required': True + } ), fac.AntdFormItem( fac.AntdInput( - id='post-post_code', + id={ + 'type': 'post-form-value', + 'index': 'post_code' + }, placeholder='请输入岗位编码', allowClear=True, style={ @@ -382,11 +392,18 @@ def render(button_perms): ), label='岗位编码', required=True, - id='post-post_code-form-item' + id={ + 'type': 'post-form-label', + 'index': 'post_code', + 'required': True + } ), fac.AntdFormItem( fac.AntdInputNumber( - id='post-post_sort', + id={ + 'type': 'post-form-value', + 'index': 'post_sort' + }, defaultValue=0, min=0, style={ @@ -395,11 +412,18 @@ def render(button_perms): ), label='岗位顺序', required=True, - id='post-post_sort-form-item' + id={ + 'type': 'post-form-label', + 'index': 'post_sort', + 'required': True + } ), fac.AntdFormItem( fac.AntdRadioGroup( - id='post-status', + id={ + 'type': 'post-form-value', + 'index': 'status' + }, options=[ { 'label': '正常', @@ -416,11 +440,18 @@ def render(button_perms): } ), label='岗位状态', - id='post-status-form-item' + id={ + 'type': 'post-form-label', + 'index': 'status', + 'required': False + } ), fac.AntdFormItem( fac.AntdInput( - id='post-remark', + id={ + 'type': 'post-form-value', + 'index': 'remark' + }, placeholder='请输入内容', allowClear=True, mode='text-area', @@ -429,7 +460,11 @@ def render(button_perms): } ), label='备注', - id='post-remark-form-item' + id={ + 'type': 'post-form-label', + 'index': 'remark', + 'required': False + } ), ], labelCol={ diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index 073ff9b..212fce1 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -21,9 +21,9 @@ def render(button_perms): total = table_info['data']['total'] for item in table_data: if item['status'] == '0': - item['status'] = dict(checked=True) + item['status'] = dict(checked=True, disabled=item['role_id'] == 1) else: - item['status'] = dict(checked=False) + item['status'] = dict(checked=False, disabled=item['role_id'] == 1) item['key'] = str(item['role_id']) if item['role_id'] == 1: item['operation'] = [] @@ -409,6 +409,7 @@ def render(button_perms): }, { 'title': '操作', + 'width': 180, 'dataIndex': 'operation', } ], @@ -448,7 +449,11 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='role-role_name', + id={ + 'type': 'role-form-value', + 'index': 'role_name', + 'required': True + }, placeholder='请输入角色名称', allowClear=True, style={ @@ -457,7 +462,11 @@ def render(button_perms): ), label='角色名称', required=True, - id='role-role_name-form-item', + id={ + 'type': 'role-form-label', + 'index': 'role_name', + 'required': True + }, labelCol={ 'span': 6 }, @@ -467,7 +476,11 @@ def render(button_perms): ), fac.AntdFormItem( fac.AntdInput( - id='role-role_key', + id={ + 'type': 'role-form-value', + 'index': 'role_key', + 'required': True + }, placeholder='请输入权限字符', allowClear=True, style={ @@ -486,7 +499,11 @@ def render(button_perms): ] ), required=True, - id='role-role_Key-form-item', + id={ + 'type': 'role-form-label', + 'index': 'role_key', + 'required': True + }, labelCol={ 'span': 6 }, @@ -496,7 +513,11 @@ def render(button_perms): ), fac.AntdFormItem( fac.AntdInputNumber( - id='role-role_sort', + id={ + 'type': 'role-form-value', + 'index': 'role_sort', + 'required': True + }, placeholder='请输入角色顺序', defaultValue=0, min=0, @@ -506,7 +527,11 @@ def render(button_perms): ), label='角色顺序', required=True, - id='role-role_sort-form-item', + id={ + 'type': 'role-form-label', + 'index': 'role_sort', + 'required': True + }, labelCol={ 'span': 6 }, @@ -516,7 +541,11 @@ def render(button_perms): ), fac.AntdFormItem( fac.AntdRadioGroup( - id='role-status', + id={ + 'type': 'role-form-value', + 'index': 'status', + 'required': False + }, options=[ { 'label': '正常', @@ -532,7 +561,11 @@ def render(button_perms): } ), label='状态', - id='role-status-form-item', + id={ + 'type': 'role-form-label', + 'index': 'status', + 'required': False + }, labelCol={ 'span': 6 }, @@ -607,7 +640,11 @@ def render(button_perms): ), fac.AntdFormItem( fac.AntdInput( - id='role-remark', + id={ + 'type': 'role-form-value', + 'index': 'remark', + 'required': False + }, placeholder='请输入内容', allowClear=True, mode='text-area', @@ -616,7 +653,11 @@ def render(button_perms): } ), label='备注', - id='role-remark-form-item', + id={ + 'type': 'role-form-label', + 'index': 'remark', + 'required': False + }, labelCol={ 'span': 6 }, diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index 498ba16..34651a8 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -449,7 +449,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='user-add-nick_name', + id={ + 'type': 'user_add-form-value', + 'index': 'nick_name' + }, placeholder='请输入用户昵称', allowClear=True, style={ @@ -458,11 +461,18 @@ def render(button_perms): ), label='用户昵称', required=True, - id='user-add-nick_name-form-item' + id={ + 'type': 'user_add-form-label', + 'index': 'nick_name', + 'required': True + } ), fac.AntdFormItem( fac.AntdTreeSelect( - id='user-add-dept_id', + id={ + 'type': 'user_add-form-value', + 'index': 'dept_id' + }, placeholder='请选择归属部门', treeData=[], treeNodeFilterProp='title', @@ -471,7 +481,11 @@ def render(button_perms): } ), label='归属部门', - id='user-add-dept_id-form-item', + id={ + 'type': 'user_add-form-label', + 'index': 'dept_id', + 'required': False + }, labelCol={ 'offset': 1 }, @@ -483,7 +497,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='user-add-phone_number', + id={ + 'type': 'user_add-form-value', + 'index': 'phonenumber' + }, placeholder='请输入手机号码', allowClear=True, style={ @@ -491,14 +508,21 @@ def render(button_perms): } ), label='手机号码', - id='user-add-phone_number-form-item', + id={ + 'type': 'user_add-form-label', + 'index': 'phonenumber', + 'required': False + }, labelCol={ 'offset': 1 }, ), fac.AntdFormItem( fac.AntdInput( - id='user-add-email', + id={ + 'type': 'user_add-form-value', + 'index': 'email' + }, placeholder='请输入邮箱', allowClear=True, style={ @@ -506,7 +530,11 @@ def render(button_perms): } ), label='邮箱', - id='user-add-email-form-item', + id={ + 'type': 'user_add-form-label', + 'index': 'email', + 'required': False + }, labelCol={ 'offset': 5 }, @@ -518,7 +546,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='user-add-user_name', + id={ + 'type': 'user_add-form-value', + 'index': 'user_name' + }, placeholder='请输入用户名称', allowClear=True, style={ @@ -527,11 +558,18 @@ def render(button_perms): ), label='用户名称', required=True, - id='user-add-user_name-form-item' + id={ + 'type': 'user_add-form-label', + 'index': 'user_name', + 'required': True + } ), fac.AntdFormItem( fac.AntdInput( - id='user-add-password', + id={ + 'type': 'user_add-form-value', + 'index': 'password' + }, placeholder='请输入密码', mode='password', passwordUseMd5=True, @@ -541,7 +579,11 @@ def render(button_perms): ), label='用户密码', required=True, - id='user-add-password-form-item' + id={ + 'type': 'user_add-form-label', + 'index': 'password', + 'required': True + } ), ], size="middle" @@ -550,7 +592,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdSelect( - id='user-add-sex', + id={ + 'type': 'user_add-form-value', + 'index': 'sex' + }, placeholder='请选择性别', options=[ { @@ -571,14 +616,21 @@ def render(button_perms): } ), label='用户性别', - id='user-add-sex-form-item', + id={ + 'type': 'user_add-form-label', + 'index': 'sex', + 'required': False + }, labelCol={ 'offset': 1 }, ), fac.AntdFormItem( fac.AntdRadioGroup( - id='user-add-status', + id={ + 'type': 'user_add-form-value', + 'index': 'status' + }, options=[ { 'label': '正常', @@ -595,7 +647,11 @@ def render(button_perms): } ), label='用户状态', - id='user-add-status-form-item', + id={ + 'type': 'user_add-form-label', + 'index': 'status', + 'required': False + }, labelCol={ 'offset': 2 }, @@ -646,7 +702,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='user-add-remark', + id={ + 'type': 'user_add-form-value', + 'index': 'remark' + }, placeholder='请输入内容', allowClear=True, mode='text-area', @@ -655,7 +714,11 @@ def render(button_perms): } ), label='备注', - id='user-add-remark-form-item', + id={ + 'type': 'user_add-form-label', + 'index': 'remark', + 'required': False + }, labelCol={ 'offset': 2 }, @@ -682,7 +745,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='user-edit-nick_name', + id={ + 'type': 'user_edit-form-value', + 'index': 'nick_name' + }, placeholder='请输入用户昵称', allowClear=True, style={ @@ -691,11 +757,18 @@ def render(button_perms): ), label='用户昵称', required=True, - id='user-edit-nick_name-form-item' + id={ + 'type': 'user_edit-form-label', + 'index': 'nick_name', + 'required': True + } ), fac.AntdFormItem( fac.AntdTreeSelect( - id='user-edit-dept_id', + id={ + 'type': 'user_edit-form-value', + 'index': 'dept_id' + }, placeholder='请选择归属部门', treeData=[], treeNodeFilterProp='title', @@ -704,7 +777,11 @@ def render(button_perms): } ), label='归属部门', - id='user-edit-dept_id-form-item' + id={ + 'type': 'user_edit-form-label', + 'index': 'dept_id', + 'required': False + } ), ], size="middle" @@ -713,7 +790,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='user-edit-phone_number', + id={ + 'type': 'user_edit-form-value', + 'index': 'phonenumber' + }, placeholder='请输入手机号码', allowClear=True, style={ @@ -721,14 +801,21 @@ def render(button_perms): } ), label='手机号码', - id='user-edit-phone_number-form-item', + id={ + 'type': 'user_edit-form-label', + 'index': 'phonenumber', + 'required': False + }, labelCol={ 'offset': 1 }, ), fac.AntdFormItem( fac.AntdInput( - id='user-edit-email', + id={ + 'type': 'user_edit-form-value', + 'index': 'email' + }, placeholder='请输入邮箱', allowClear=True, style={ @@ -736,7 +823,11 @@ def render(button_perms): } ), label='邮箱', - id='user-edit-email-form-item', + id={ + 'type': 'user_edit-form-label', + 'index': 'email', + 'required': False + }, labelCol={ 'offset': 4 }, @@ -748,7 +839,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdSelect( - id='user-edit-sex', + id={ + 'type': 'user_edit-form-value', + 'index': 'sex' + }, placeholder='请选择性别', options=[ { @@ -769,14 +863,21 @@ def render(button_perms): } ), label='用户性别', - id='user-edit-sex-form-item', + id={ + 'type': 'user_edit-form-label', + 'index': 'sex', + 'required': False + }, labelCol={ 'offset': 1 }, ), fac.AntdFormItem( fac.AntdRadioGroup( - id='user-edit-status', + id={ + 'type': 'user_edit-form-value', + 'index': 'status' + }, options=[ { 'label': '正常', @@ -792,7 +893,11 @@ def render(button_perms): } ), label='用户状态', - id='user-edit-status-form-item', + id={ + 'type': 'user_edit-form-label', + 'index': 'status', + 'required': False + }, labelCol={ 'offset': 1 }, @@ -843,7 +948,10 @@ def render(button_perms): [ fac.AntdFormItem( fac.AntdInput( - id='user-edit-remark', + id={ + 'type': 'user_edit-form-value', + 'index': 'remark' + }, placeholder='请输入内容', allowClear=True, mode='text-area', @@ -852,7 +960,11 @@ def render(button_perms): } ), label='备注', - id='user-edit-remark-form-item', + id={ + 'type': 'user_edit-form-label', + 'index': 'remark', + 'required': False + }, labelCol={ 'offset': 2 }, -- Gitee