You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
510 lines
19 KiB
510 lines
19 KiB
import uuid
|
|
|
|
import dash
|
|
import time
|
|
from dash import html, dcc
|
|
from dash.dependencies import Input, Output, State, ClientsideFunction
|
|
import feffery_antd_components as fac
|
|
import feffery_utils_components as fuc
|
|
from flask import session
|
|
from operator import itemgetter
|
|
|
|
from server import app
|
|
from config.env import AppConfig
|
|
from config.global_config import RouterConfig
|
|
from store.store import render_store_container
|
|
|
|
# 载入子页面
|
|
import views
|
|
|
|
from callbacks import app_c
|
|
from api.login import get_current_user_info_api
|
|
from utils.tree_tool import find_node_values, find_key_by_href, deal_user_menu_info, get_search_panel_data
|
|
|
|
from views.aichat.components import aichat
|
|
|
|
|
|
app.layout = html.Div(
|
|
[
|
|
# 注入url监听
|
|
fuc.FefferyLocation(id='url-container'),
|
|
# 用于回调pathname信息
|
|
dcc.Location(id='dcc-url', refresh=False),
|
|
|
|
# 注入js执行容器
|
|
fuc.FefferyExecuteJs(
|
|
id='execute-js-container'
|
|
),
|
|
|
|
# 注入页面内容挂载点
|
|
html.Div(id='app-mount'),
|
|
|
|
# 注入全局配置容器
|
|
fac.AntdConfigProvider(id='app-config-provider'),
|
|
|
|
# 注入快捷搜索面板
|
|
fuc.FefferyShortcutPanel(
|
|
id='search-panel',
|
|
data=[],
|
|
placeholder='输入你想要搜索的菜单...',
|
|
panelStyles={
|
|
'accentColor': '#1890ff',
|
|
'zIndex': 99999
|
|
}
|
|
),
|
|
# 聊天记录的id
|
|
dcc.Store("chat_session_id"),
|
|
# 聊天窗口显示按钮
|
|
fac.AntdButton(
|
|
id='ai_chat_button',
|
|
type='link',
|
|
children=html.Img(src="/assets/imgs/aichat.gif",
|
|
style={"width": "100%", "height": "100%"}),
|
|
style={
|
|
'z-index': '90000',
|
|
'position': 'fixed',
|
|
'bottom': '30px',
|
|
'right': '0px',
|
|
'display': 'none',
|
|
'width': '64px',
|
|
'height': '64px',
|
|
'padding': '0px'
|
|
}
|
|
),
|
|
html.Div(
|
|
[aichat.render(),
|
|
html.Div(
|
|
children=[
|
|
fac.AntdButton(
|
|
id="aichat_zoom-close",
|
|
children=html.Img(src="/assets/imgs/collapse-diagonal-line.png"),
|
|
type='link',
|
|
style={
|
|
"font-size": "20px",
|
|
"padding": '5px',
|
|
"display": "none",
|
|
"margin-top": "-15px"
|
|
}
|
|
),
|
|
fac.AntdButton(
|
|
id="aichat_zoom-open",
|
|
children=html.Img(src="/assets/imgs/expand-diagonal-fill.png"),
|
|
type='link',
|
|
style={
|
|
"font-size": "20px",
|
|
"padding": '5px',
|
|
"display": "block",
|
|
"margin-top": "-15px"
|
|
}
|
|
),
|
|
fac.AntdButton(
|
|
id="close-chat-window",
|
|
children=(
|
|
fac.AntdIcon(
|
|
style={"color": "#909399"},
|
|
icon="antd-close",
|
|
)
|
|
),
|
|
type='link',
|
|
style={
|
|
"font-size": "20px",
|
|
"padding": '8px'
|
|
}
|
|
)
|
|
],
|
|
id="aichat_operate",
|
|
style={
|
|
"top": "15px",
|
|
"right": "8px",
|
|
"position": "absolute",
|
|
"display": "flex",
|
|
"z-index": "90000",
|
|
"align-items": "center"
|
|
}
|
|
),
|
|
dcc.Store(id="ai_chat_width", data="412px")
|
|
],
|
|
id="ai_chat_container",
|
|
style={
|
|
"z-index": "90000",
|
|
"border-radius": "8px",
|
|
"border": "1px solid #ffffff",
|
|
"background": "linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%,"
|
|
" rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1",
|
|
"box-shadow": "0px 4px 8px 0px rgba(31, 35, 41, 0.10)",
|
|
"position": "fixed",
|
|
"bottom": "16px",
|
|
"right": "16px",
|
|
"overflow": "auto",
|
|
"width": "450px",
|
|
"height": "600px",
|
|
"display": "none"
|
|
}
|
|
),
|
|
dcc.Store(id="current_g6"),
|
|
html.Div(
|
|
[
|
|
fac.AntdIcon(
|
|
id="g6-exit-full-screen",
|
|
icon="antd-full-screen-exit",
|
|
style={"float": "right",
|
|
"font-size": "25px",
|
|
"cursor": "pointer"}
|
|
),
|
|
html.Div(
|
|
id="g6-full-screen-body",
|
|
style={"width": "100%", "height": "100%"}
|
|
)
|
|
],
|
|
id='g6-modal',
|
|
style={
|
|
"z-index": '99999',
|
|
"width": '100%',
|
|
"height": '100%',
|
|
"position": 'absolute',
|
|
"left": '0px',
|
|
"top": '0px',
|
|
"display": 'none'
|
|
}
|
|
),
|
|
|
|
# 辅助处理多输入 -> 存储接口返回token校验信息
|
|
render_store_container(),
|
|
|
|
# 重定向容器
|
|
html.Div(id='redirect-container'),
|
|
|
|
# 登录消息失效对话框提示
|
|
fac.AntdModal(
|
|
html.Div(
|
|
[
|
|
fac.AntdIcon(icon='fc-high-priority', style={'font-size': '28px'}),
|
|
fac.AntdText('用户信息已过期,请重新登录', style={'margin-left': '5px'}),
|
|
]
|
|
),
|
|
id='token-invalid-modal',
|
|
visible=False,
|
|
title='提示',
|
|
renderFooter=True,
|
|
centered=True
|
|
),
|
|
|
|
# 注入全局消息提示容器
|
|
html.Div(id='global-message-container'),
|
|
# 注入全局通知信息容器
|
|
html.Div(id='global-notification-container')
|
|
]
|
|
)
|
|
|
|
|
|
@app.callback(
|
|
Output("ai_chat_button", "style"),
|
|
Input('url-container', 'pathname')
|
|
)
|
|
def listen_path_show_aichat(pathname):
|
|
if pathname == '/login':
|
|
return {'display': 'none'}
|
|
elif pathname == '/forget':
|
|
return {'display': 'none'}
|
|
else:
|
|
return {'z-index': '90000',
|
|
'position': 'fixed',
|
|
'bottom': '30px',
|
|
'right': '0px',
|
|
'display': 'block',
|
|
'width': '64px',
|
|
'padding': '0px',
|
|
'height': '64px'}
|
|
|
|
|
|
@app.callback(
|
|
[Output('ai_chat_container', 'style', allow_duplicate=True),
|
|
Output('aichat_zoom-open', 'style', allow_duplicate=True),
|
|
Output('aichat_zoom-close', 'style', allow_duplicate=True),
|
|
Output('ai_chat_width', 'data', allow_duplicate=True),
|
|
Output('ai_chat_button', 'style', allow_duplicate=True),
|
|
Output('chat_session_id', 'data', allow_duplicate=True),
|
|
],
|
|
Input('ai_chat_button', 'nClicks'),
|
|
[State('ai_chat_container', 'style'),
|
|
State('aichat_zoom-open', 'style'),
|
|
State('aichat_zoom-close', 'style'),
|
|
State('ai_chat_button', 'style'),
|
|
State('chat_data_list', 'data')
|
|
],
|
|
prevent_initial_call=True
|
|
)
|
|
def click_ai_chat_button_event(nClicks, style, open_style, close_style, button_style, chat_data_list):
|
|
if nClicks:
|
|
style['display'] = 'block'
|
|
style['width'] = '450px'
|
|
style['height'] = '600px'
|
|
style['bottom'] = '16px'
|
|
style['right'] = '16px'
|
|
open_style['display'] = 'block'
|
|
close_style['display'] = 'none'
|
|
button_style['display'] = 'none'
|
|
if chat_data_list and len(chat_data_list) > 0:
|
|
return style, open_style, close_style, '412px', button_style, dash.no_update
|
|
else:
|
|
return style, open_style, close_style, '412px', button_style, uuid.uuid4()
|
|
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
|
|
|
|
|
|
@app.callback(
|
|
[Output('ai_chat_container', 'style', allow_duplicate=True),
|
|
Output('ai_chat_button', 'style', allow_duplicate=True)],
|
|
Input('close-chat-window', 'nClicks'),
|
|
[State('ai_chat_container', 'style'),
|
|
State('ai_chat_button', 'style')],
|
|
prevent_initial_call=True
|
|
)
|
|
def close_chat_window(nClicks, style, button_style):
|
|
if nClicks:
|
|
style['display'] = 'none'
|
|
button_style['display'] = 'block'
|
|
return style, button_style
|
|
return dash.no_update
|
|
|
|
|
|
@app.callback(
|
|
[Output('ai_chat_container', 'style', allow_duplicate=True),
|
|
Output('aichat_zoom-close', 'style'),
|
|
Output('aichat_zoom-open', 'style'),
|
|
Output('ai_chat_width', 'data', allow_duplicate=True)
|
|
],
|
|
Input('aichat_zoom-open', 'nClicks'),
|
|
[State('ai_chat_container', 'style'),
|
|
State('aichat_zoom-close', 'style'),
|
|
State('aichat_zoom-open', 'style')],
|
|
prevent_initial_call=True
|
|
)
|
|
def aichat_zoom_open_click(nClicks, style, close_style, open_style):
|
|
if nClicks:
|
|
close_style['display'] = 'block'
|
|
open_style['display'] = 'none'
|
|
style['width'] = '50%'
|
|
style['height'] = '100%'
|
|
style['bottom'] = '0px'
|
|
style['right'] = '0px'
|
|
return style, close_style, open_style, "calc(50vw - 48px)"
|
|
return dash.no_update, dash.no_update, dash.no_update, dash.no_update
|
|
|
|
|
|
app.clientside_callback(
|
|
# 使用目标容器id为Input,利用初次自动回调触发即时渲染
|
|
ClientsideFunction(namespace="aichat_clientside", function_name="refresh_chat_div"),
|
|
Input('ai_chat_container', 'style'),
|
|
State("chat_data_list", "data"),
|
|
prevent_initial_call=True
|
|
)
|
|
|
|
|
|
@app.callback(
|
|
[Output('ai_chat_container', 'style', allow_duplicate=True),
|
|
Output('aichat_zoom-close', 'style', allow_duplicate=True),
|
|
Output('aichat_zoom-open', 'style', allow_duplicate=True),
|
|
Output('ai_chat_width', 'data', allow_duplicate=True)
|
|
],
|
|
Input('aichat_zoom-close', 'nClicks'),
|
|
[State('ai_chat_container', 'style'),
|
|
State('aichat_zoom-close', 'style'),
|
|
State('aichat_zoom-open', 'style')],
|
|
prevent_initial_call=True
|
|
)
|
|
def aichat_zoom_close_click(nClicks, style, close_style, open_style):
|
|
if nClicks:
|
|
close_style['display'] = 'none'
|
|
open_style['display'] = 'block'
|
|
style['display'] = 'block'
|
|
style['width'] = '450px'
|
|
style['height'] = '600px'
|
|
style['bottom'] = '16px'
|
|
style['right'] = '16px'
|
|
return style, close_style, open_style, '412px'
|
|
return dash.no_update, dash.no_update, dash.no_update, dash.no_update
|
|
|
|
|
|
@app.callback(
|
|
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, url_trigger, session_token):
|
|
"""
|
|
全局路由回调
|
|
"""
|
|
# 检查当前会话是否已经登录
|
|
token_result = session.get('Authorization')
|
|
# 若已登录
|
|
if token_result and session_token and token_result == session_token:
|
|
try:
|
|
current_user_result = get_current_user_info_api()
|
|
if current_user_result['code'] == 200:
|
|
current_user = current_user_result['data']
|
|
menu_list = current_user['menu']
|
|
user_menu_list = sorted([item for item in menu_list if item.get('visible') == '0'],
|
|
key=itemgetter('order_num'))
|
|
menu_info = deal_user_menu_info(0, menu_list)
|
|
user_menu_info = deal_user_menu_info(0, user_menu_list)
|
|
search_panel_data = get_search_panel_data(user_menu_list)
|
|
session['user_info'] = current_user['user']
|
|
session['dept_info'] = current_user['dept']
|
|
session['role_info'] = current_user['role']
|
|
session['post_info'] = current_user['post']
|
|
# session['newtest_info'] = current_user['newtest']
|
|
dynamic_valid_pathname_list = find_node_values(menu_info, 'href')
|
|
valid_href_list = dynamic_valid_pathname_list + RouterConfig.STATIC_VALID_PATHNAME
|
|
if pathname in valid_href_list:
|
|
current_key = find_key_by_href(menu_info, pathname)
|
|
if pathname == '/':
|
|
current_key = '首页'
|
|
if pathname == '/user/profile':
|
|
current_key = '个人资料'
|
|
if url_trigger == 'load':
|
|
|
|
# 根据pathname控制渲染行为
|
|
if pathname == '/login' or pathname == '/forget':
|
|
# 重定向到主页面
|
|
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 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 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 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 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 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 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 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 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 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__':
|
|
app.run(host=AppConfig.app_host, port=AppConfig.app_port, debug=AppConfig.app_debug)
|
|
|