Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion magic_dash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os
import click
import shutil

import click

__version__ = "0.2.8"

# 现有内置项目模板信息
Expand All @@ -12,6 +13,9 @@
"magic-dash-pro": {
"description": "多页面+用户登录应用模板",
},
"magic-dash-pro-sqlalchemy": {
"description": "多页面+用户登录应用模板(SQLAlchemy版)",
},
"simple-tool": {
"description": "单页面工具应用模板",
},
Expand Down
204 changes: 204 additions & 0 deletions magic_dash/templates/magic-dash-pro-sqlalchemy/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import dash
from flask import request
from dash import html, set_props, dcc
import feffery_antd_components as fac
import feffery_utils_components as fuc
from dash.dependencies import Input, Output, State
from flask_principal import identity_changed, AnonymousIdentity
from flask_login import current_user, logout_user, AnonymousUserMixin

from server import app
from models.users import Users
from views import core_pages, login
from views.status_pages import _403, _404, _500
from configs import BaseConfig, RouterConfig, AuthConfig


app.layout = lambda: fuc.FefferyTopProgress(
[
# 全局消息提示
fac.Fragment(id="global-message"),
# 全局重定向
fac.Fragment(id="global-redirect"),
# 全局页面重载
fac.Fragment(id="global-reload"),
*(
[
# 重复登录辅助检查轮询
dcc.Interval(
id="duplicate-login-check-interval",
interval=BaseConfig.duplicate_login_check_interval * 1000,
)
]
# 若开启了重复登录辅助检查
if BaseConfig.enable_duplicate_login_check
else []
),
# 根节点url监听
fuc.FefferyLocation(id="root-url"),
# 应用根容器
html.Div(
id="root-container",
),
],
listenPropsMode="include",
includeProps=["root-container.children"],
minimum=0.33,
color="#1677ff",
)


def handle_root_router_error(e):
"""处理根节点路由错误"""

set_props(
"root-container",
{
"children": _500.render(e),
},
)


@app.callback(
Output("root-container", "children"),
Input("root-url", "pathname"),
State("root-url", "trigger"),
prevent_initial_call=True,
on_error=handle_root_router_error,
)
def root_router(pathname, trigger):
"""根节点路由控制"""

# 在动态路由切换时阻止根节点路由更新
if trigger != "load":
return dash.no_update

# 无需校验登录状态的公共页面
if pathname in RouterConfig.public_pathnames:
if pathname == "/403-demo":
return _403.render()

elif pathname == "/404-demo":
return _404.render()

elif pathname == "/500-demo":
return _500.render()

elif pathname == "/login":
return login.render()

elif pathname == "/logout":
# 当前用户登出
logout_user()

# 重置当前用户身份
identity_changed.send(
app.server,
identity=AnonymousIdentity(),
)

# 重定向至登录页面
set_props(
"global-redirect",
{"children": dcc.Location(pathname="/login", id="global-redirect")},
)
return dash.no_update

# 登录状态校验:若当前用户未登录
if not current_user.is_authenticated:
# 重定向至登录页面
set_props(
"global-redirect",
{"children": dcc.Location(pathname="/login", id="global-redirect")},
)

return dash.no_update

# 检查当前访问目标pathname是否为有效页面
if pathname in RouterConfig.valid_pathnames.keys():
# 校验当前用户是否具有针对当前访问目标页面的权限
current_user_access_rule = AuthConfig.pathname_access_rules.get(
current_user.user_role
)

# 若当前用户页面权限规则类型为'include'
if current_user_access_rule["type"] == "include":
# 若当前用户不具有针对当前访问目标页面的权限
if pathname not in current_user_access_rule["keys"]:
# 首页不受权限控制影响
if pathname not in [
"/",
RouterConfig.index_pathname,
]:
# 重定向至403页面
set_props(
"global-redirect",
{
"children": dcc.Location(
pathname="/403-demo", id="global-redirect"
)
},
)

return dash.no_update

# 若当前用户页面权限规则类型为'exclude'
elif current_user_access_rule["type"] == "exclude":
# 若当前用户不具有针对当前访问目标页面的权限
if pathname in current_user_access_rule["keys"]:
# 重定向至403页面
set_props(
"global-redirect",
{
"children": dcc.Location(
pathname="/403-demo", id="global-redirect"
)
},
)

return dash.no_update

# 处理核心功能页面渲染
return core_pages.render(
current_user_access_rule=current_user_access_rule, current_pathname=pathname
)

# 返回404状态页面
return _404.render()


@app.callback(
Input("duplicate-login-check-interval", "n_intervals"),
State("root-url", "pathname"),
)
def duplicate_login_check(n_intervals, pathname):
"""重复登录辅助轮询检查"""

# 若当前页面属于无需校验登录状态的公共页面,结束检查
if pathname in RouterConfig.public_pathnames:
return

# 若当前用户身份未知
if isinstance(current_user, AnonymousUserMixin):
# 重定向到登出页
set_props(
"global-redirect",
{"children": dcc.Location(pathname="/logout", id="global-redirect")},
)

# 若当前用户已登录
elif current_user.is_authenticated:
match_user = Users.get_user(current_user.id)
# 若当前回调请求携带cookies中的session_token,当前用户数据库中的最新session_token不一致
if match_user.session_token != request.cookies.get("session_token"):
# 重定向到登出页
set_props(
"global-redirect",
{"children": dcc.Location(pathname="/logout", id="global-redirect")},
)


if __name__ == "__main__":
# 非正式环境下开发调试预览用
# 生产环境推荐使用gunicorn启动
app.run(debug=True)
42 changes: 42 additions & 0 deletions magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* 常规基础样式 */

/* 自定义应用初始化加载动画效果 */
._dash-loading {
color: transparent;
position: fixed;
width: calc(95px / 1.2);
height: calc(87px / 1.2);
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
background-image: url('/assets/imgs/init_loading.gif');
background-repeat: no-repeat;
background-size: 100% 100%;
}

._dash-loading::after {
content: '';
}

/* 滚动条美化 */
/* chrome, edge */
*::-webkit-scrollbar-thumb {
background-color: #bfbfbf;
outline: none;
border-radius: 6px;
}

*::-webkit-scrollbar {
width: 6px;
height: 6px;
}

/* firefox */
* {
scrollbar-width: thin;
}

/* 全局辅助性文字样式 */
.global-help-text {
color: #5d7189;
}
34 changes: 34 additions & 0 deletions magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/core.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* 核心页面样式 */

/* 侧边菜单栏相关样式自定义 */

#core-side-menu .ant-menu-item-selected {
background-color: #f4f6f9 !important;
color: #006aff !important;
font-size: 15px;
overflow: visible;
}

#core-side-menu .ant-menu-item {
font-size: 15px;
}

/* 为已选中菜单项,基于伪元素,添加前缀竖向填充线 */
#core-side-menu .ant-menu-item-selected::before {
content: "";
position: absolute;
left: -5px;
top: 0;
width: 3px;
height: 100%;
background-color: #006aff;
}


#core-side-menu .ant-menu-item-icon>span {
font-size: 15px !important;
}

#core-side-menu .ant-menu-submenu {
font-size: 15px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.login-left-side {
position: relative;
background-size: cover;
background-position: center center;
background-repeat: repeat;
background-image: url("/assets/imgs/login/left-side-bg.svg");
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
user-select: none;
}

.login-right-side {
background-color: #ffffff;
background-image: radial-gradient(rgba(0, 106, 255, 0.4) 1px, #ffffff 1px);
background-size: 25px 25px;
}
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 改造console.error()以隐藏无关痛痒的警告信息
const originalConsoleError = console.error;
console.error = function (...args) {
// 检查args中是否包含需要过滤的内容
const shouldFilter = args.some(arg => typeof arg === 'string' && arg.includes('Warning:'));

if (!shouldFilter) {
originalConsoleError.apply(console, args);
}
};

window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside_basic: {
// 处理核心页面侧边栏展开/收起
handleSideCollapse: (nClicks, originIcon, originHeaderSideStyle, coreConfig) => {
// 若先前为展开状态
if (originIcon === 'antd-menu-fold') {
return [
// 更新图标
'antd-menu-unfold',
// 更新页首侧边容器样式
{
...originHeaderSideStyle,
width: 110
},
// 更新页首标题样式
{
display: 'none'
},
// 更新侧边菜单容器样式
{
width: 110
},
// 更新侧边菜单折叠状态
true
]
} else {
return [
// 更新图标
'antd-menu-fold',
// 更新页首侧边容器样式
{
...originHeaderSideStyle,
width: coreConfig.core_side_width
},
// 更新页首标题样式
{},
// 更新侧边菜单容器样式
{
width: coreConfig.core_side_width
},
// 更新侧边菜单折叠状态
false
]
}
},
// 控制页面搜索切换页面的功能
handleCorePageSearch: (value) => {
if (value) {
let pathname = value.split('|')[0]
// 更新pathname
window.location.pathname = pathname
}
},
// 控制ctrl+k快捷键触发页面搜索框聚焦
handleCorePageSearchFocus: (pressedCounts) => {
return [true, pressedCounts.toString()]
}
}
});
Binary file not shown.
Empty file.
Loading