diff --git a/magic_dash/__init__.py b/magic_dash/__init__.py index 8664271..92fc665 100644 --- a/magic_dash/__init__.py +++ b/magic_dash/__init__.py @@ -1,7 +1,8 @@ import os -import click import shutil +import click + __version__ = "0.2.8" # 现有内置项目模板信息 @@ -12,6 +13,9 @@ "magic-dash-pro": { "description": "多页面+用户登录应用模板", }, + "magic-dash-pro-sqlalchemy": { + "description": "多页面+用户登录应用模板(SQLAlchemy版)", + }, "simple-tool": { "description": "单页面工具应用模板", }, diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/app.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/app.py new file mode 100644 index 0000000..6dc3835 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/app.py @@ -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) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/base.css b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/base.css new file mode 100644 index 0000000..14b9e43 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/base.css @@ -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; +} \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/core.css b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/core.css new file mode 100644 index 0000000..cd944c0 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/core.css @@ -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; +} \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/login.css b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/login.css new file mode 100644 index 0000000..fb1e510 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/css/login.css @@ -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; +} \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/favicon.ico b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/favicon.ico new file mode 100644 index 0000000..45c6545 Binary files /dev/null and b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/favicon.ico differ diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/init_loading.gif b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/init_loading.gif new file mode 100644 index 0000000..3472e87 Binary files /dev/null and b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/init_loading.gif differ diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/left-side-bg.svg b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/left-side-bg.svg new file mode 100644 index 0000000..32e44b6 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/left-side-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git "a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2761.svg" "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2761.svg" new file mode 100644 index 0000000..77de3c7 --- /dev/null +++ "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2761.svg" @@ -0,0 +1 @@ + \ No newline at end of file diff --git "a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2762.svg" "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2762.svg" new file mode 100644 index 0000000..652aaa0 --- /dev/null +++ "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2762.svg" @@ -0,0 +1 @@ + \ No newline at end of file diff --git "a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2763.svg" "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2763.svg" new file mode 100644 index 0000000..b80234d --- /dev/null +++ "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2763.svg" @@ -0,0 +1 @@ + \ No newline at end of file diff --git "a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2764.svg" "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2764.svg" new file mode 100644 index 0000000..e25442b --- /dev/null +++ "b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/login/\346\217\222\345\233\2764.svg" @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/logo.svg b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/logo.svg new file mode 100644 index 0000000..cc7950a --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/403.svg b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/403.svg new file mode 100644 index 0000000..7b0e744 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/404.svg b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/404.svg new file mode 100644 index 0000000..1bd2c0f --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/500.svg b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/500.svg new file mode 100644 index 0000000..15e8622 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/imgs/status/500.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/js/basic_callbacks.js b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/js/basic_callbacks.js new file mode 100644 index 0000000..cd5ad48 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/js/basic_callbacks.js @@ -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()] + } + } +}); \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/videos/login-bg.mp4 b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/videos/login-bg.mp4 new file mode 100644 index 0000000..dc982dc Binary files /dev/null and b/magic_dash/templates/magic-dash-pro-sqlalchemy/assets/videos/login-bg.mp4 differ diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/core_pages_c/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/core_pages_c/__init__.py new file mode 100644 index 0000000..ba1d9d9 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/core_pages_c/__init__.py @@ -0,0 +1,143 @@ +import time +import dash +from dash import set_props +import feffery_antd_components as fac +from dash.dependencies import Input, Output, State, ClientsideFunction + +from server import app +from views.status_pages import _404 +from views.core_pages import ( + index, + page1, + sub_menu_page1, + sub_menu_page2, + sub_menu_page3, + independent_page, +) + +# 路由配置参数 +from configs import RouterConfig + +app.clientside_callback( + # 控制核心页面侧边栏折叠 + ClientsideFunction( + namespace="clientside_basic", function_name="handleSideCollapse" + ), + [ + Output("core-side-menu-collapse-button-icon", "icon"), + Output("core-header-side", "style"), + Output("core-header-title", "style"), + Output("core-side-menu-affix", "style"), + Output("core-side-menu", "inlineCollapsed"), + ], + Input("core-side-menu-collapse-button", "nClicks"), + [ + State("core-side-menu-collapse-button-icon", "icon"), + State("core-header-side", "style"), + State("core-page-config", "data"), + ], + prevent_initial_call=True, +) + +app.clientside_callback( + # 控制页首页面搜索切换功能 + ClientsideFunction( + namespace="clientside_basic", function_name="handleCorePageSearch" + ), + Input("core-page-search", "value"), +) + +app.clientside_callback( + # 控制ctrl+k快捷键聚焦页面搜索框 + ClientsideFunction( + namespace="clientside_basic", function_name="handleCorePageSearchFocus" + ), + # 其中更新key用于强制刷新状态 + [ + Output("core-page-search", "autoFocus"), + Output("core-page-search", "key"), + ], + Input("core-ctrl-k-key-press", "pressedCounts"), + prevent_initial_call=True, +) + + +@app.callback( + Input("core-pages-header-user-dropdown", "nClicks"), + State("core-pages-header-user-dropdown", "clickedKey"), +) +def open_user_manage_drawer(nClicks, clickedKey): + """打开个人信息、用户管理面板""" + + if clickedKey == "个人信息": + set_props("personal-info-modal", {"visible": True, "loading": True}) + + elif clickedKey == "用户管理": + set_props("user-manage-drawer", {"visible": True, "loading": True}) + + +@app.callback( + [ + Output("core-container", "children"), + Output("core-side-menu", "currentKey"), + Output("core-side-menu", "openKeys"), + ], + Input("core-url", "pathname"), +) +def core_router(pathname): + """核心页面路由控制及侧边菜单同步""" + + # 统一首页pathname + if pathname == RouterConfig.index_pathname: + pathname = "/" + + # 若当前目标pathname不合法 + if pathname not in RouterConfig.valid_pathnames.keys(): + return _404.render(), pathname, dash.no_update + + # 增加一点加载动画延迟^_^ + time.sleep(0.5) + + # 初始化页面返回内容 + page_content = fac.AntdAlert( + type="warning", + showIcon=True, + message=f"这里是{pathname}", + description="该页面尚未进行开发哦🤔~", + ) + + # 以首页做简单示例 + if pathname == "/": + # 更新页面返回内容 + page_content = index.render() + + # 以主要页面1做简单示例 + elif pathname == "/core/page1": + # 更新页面返回内容 + page_content = page1.render() + + # 以子菜单演示1做简单示例 + elif pathname == "/core/sub-menu-page1": + # 更新页面返回内容 + page_content = sub_menu_page1.render() + + # 以子菜单演示2做简单示例 + elif pathname == "/core/sub-menu-page2": + # 更新页面返回内容 + page_content = sub_menu_page2.render() + + # 以子菜单演示3做简单示例 + elif pathname == "/core/sub-menu-page3": + # 更新页面返回内容 + page_content = sub_menu_page3.render() + + # 以独立页面做简单示例 + elif pathname == "/core/independent-page": + # 更新页面返回内容 + page_content = independent_page.render() + + return [ + page_content, + pathname, + RouterConfig.side_menu_open_keys.get(pathname, dash.no_update), + ] diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/core_pages_c/page1_c.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/core_pages_c/page1_c.py new file mode 100644 index 0000000..ba4a22e --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/core_pages_c/page1_c.py @@ -0,0 +1,14 @@ +from dash.dependencies import Input, Output + +from server import app + + +@app.callback( + Output("core-page1-demo-output", "children"), + Input("core-page1-demo-button", "nClicks"), + prevent_initial_call=True, +) +def page1_callback_demo(nClicks): + """主要页面1演示示例回调函数""" + + return f"累计点击次数:{nClicks}" diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/login_c.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/login_c.py new file mode 100644 index 0000000..08892f5 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/callbacks/login_c.py @@ -0,0 +1,128 @@ +import uuid +import time +import dash +from dash import set_props, dcc +from flask_login import login_user +import feffery_antd_components as fac +from dash.dependencies import Input, Output, State +from flask_principal import identity_changed, Identity + +from server import app, User +from models.users import Users + + +@app.callback( + [Output("login-form", "helps"), Output("login-form", "validateStatuses")], + [Input("login-button", "nClicks"), Input("login-password", "nSubmit")], + [State("login-form", "values"), State("login-remember-me", "checked")], + running=[ + [Output("login-button", "loading"), True, False], + ], + prevent_initial_call=True, +) +def handle_login(nClicks, nSubmit, values, remember_me): + """处理用户登录逻辑""" + + time.sleep(0.25) + + values = values or {} + + # 若表单必要信息不完整 + if not (values.get("login-user-name") and values.get("login-password")): + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="error", + content="请完善登录信息", + ) + }, + ) + + return [ + # 表单帮助信息 + { + "用户名": "请输入用户名" if not values.get("login-user-name") else None, + "密码": "请输入密码" if not values.get("login-password") else None, + }, + # 表单帮助状态 + { + "用户名": "error" if not values.get("login-user-name") else None, + "密码": "error" if not values.get("login-password") else None, + }, + ] + + # 校验用户登录信息 + + # 根据用户名尝试查询用户 + match_user = Users.get_user_by_name(values["login-user-name"]) + + # 若用户不存在 + if not match_user: + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="error", + content="用户不存在", + ) + }, + ) + + return [ + # 表单帮助信息 + {"用户名": "用户不存在"}, + # 表单帮助状态 + {"用户名": "error"}, + ] + + else: + # 校验密码 + + # 若密码不正确 + if not Users.check_user_password(match_user.user_id, values["login-password"]): + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="error", + content="密码错误", + ) + }, + ) + + return [ + # 表单帮助信息 + {"密码": "密码错误"}, + # 表单帮助状态 + {"密码": "error"}, + ] + + # 更新用户信息表session_token字段 + new_session_token = str(uuid.uuid4()) + Users.update_user(match_user.user_id, session_token=new_session_token) + + # 进行用户登录 + new_user = User( + id=match_user.user_id, + user_name=match_user.user_name, + user_role=match_user.user_role, + session_token=new_session_token, + ) + + # 会话登录状态切换 + login_user(new_user, remember=remember_me) + + # 在cookies更新ession_token字段 + dash.ctx.response.set_cookie("session_token", new_session_token) + + # 更新用户身份信息 + identity_changed.send(app.server, identity=Identity(new_user.id)) + + # 重定向至首页 + set_props( + "global-redirect", + {"children": dcc.Location(pathname="/", id="global-redirect")}, + ) + + return [{}, {}] diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/components/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/components/core_side_menu.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/components/core_side_menu.py new file mode 100644 index 0000000..f9d57ea --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/components/core_side_menu.py @@ -0,0 +1,73 @@ +from copy import deepcopy +import feffery_antd_components as fac +import feffery_utils_components as fuc +from feffery_dash_utils.style_utils import style +from feffery_dash_utils.tree_utils import TreeManager + +# 路由配置参数 +from configs import RouterConfig, LayoutConfig + + +def render(current_user_access_rule: str): + """渲染核心功能页面侧边菜单栏 + + Args: + current_user_access_rule (str): 当前用户页面可访问性规则 + """ + + current_menu_items = deepcopy(RouterConfig.core_side_menu) + + # 根据current_user_access_rule规则过滤菜单结构 + if current_user_access_rule["type"] == "include": + for key in RouterConfig.valid_pathnames.keys(): + if key not in current_user_access_rule["keys"]: + # 首页不受权限控制影响 + if key not in [ + "/", + RouterConfig.index_pathname, + ]: + current_menu_items = TreeManager.delete_node( + current_menu_items, + key, + data_type="menu", # 菜单数据模式 + keep_empty_children_node=False, # 去除children字段为空列表的节点 + ) + + elif current_user_access_rule["type"] == "exclude": + for key in RouterConfig.valid_pathnames.keys(): + if key in current_user_access_rule["keys"]: + # 首页不受权限控制影响 + if key not in [ + "/", + RouterConfig.index_pathname, + ]: + current_menu_items = TreeManager.delete_node( + current_menu_items, + key, + data_type="menu", # 菜单数据模式 + keep_empty_children_node=False, # 去除children字段为空列表的节点 + ) + + return fac.AntdAffix( + fuc.FefferyDiv( + [ + # 侧边菜单 + fac.AntdMenu( + id="core-side-menu", + menuItems=current_menu_items, + mode="inline", + style=style(border="none", width="100%"), + ) + ], + scrollbar="hidden", + style=style( + height="calc(100vh - 72px)", + overflowY="auto", + borderRight="1px solid #dae0ea", + padding="0 8px", + ), + ), + id="core-side-menu-affix", + offsetTop=72.1, + style=style(width=LayoutConfig.core_side_width), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/components/personal_info.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/components/personal_info.py new file mode 100644 index 0000000..d52b788 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/components/personal_info.py @@ -0,0 +1,148 @@ +import uuid +import time +import dash +from dash import set_props +from flask_login import current_user +import feffery_antd_components as fac +import feffery_utils_components as fuc +from feffery_dash_utils.style_utils import style +from dash.dependencies import Input, Output, State +from werkzeug.security import generate_password_hash + +from server import app +from models.users import Users + + +def render(): + """渲染个人信息模态框""" + + return fac.AntdModal( + id="personal-info-modal", + title=fac.AntdSpace([fac.AntdIcon(icon="antd-user"), "个人信息"]), + renderFooter=True, + okClickClose=False, + ) + + +@app.callback( + [ + Output("personal-info-modal", "children"), + Output("personal-info-modal", "loading", allow_duplicate=True), + ], + Input("personal-info-modal", "visible"), + prevent_initial_call=True, +) +def render_personal_info_modal(visible): + """每次个人信息模态框打开后,动态更新内容""" + + if visible: + time.sleep(0.5) + + # 查询当前用户信息 + match_user = Users.get_user(current_user.id) + + return [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id="personal-info-user-name", + placeholder="请输入用户名", + allowClear=True, + ), + label="用户名", + ), + fac.AntdFormItem( + fac.AntdInput( + id="personal-info-user-password", + placeholder="请输入新的密码", + mode="password", + allowClear=True, + ), + label="密码", + tooltip="一旦填写并确定更新,则视作新的密码", + ), + ], + id="personal-info-form", + key=str(uuid.uuid4()), # 强制刷新 + enableBatchControl=True, + layout="vertical", + values={"personal-info-user-name": match_user.user_name}, + style=style(marginTop=32), + ), + False, + ] + + return dash.no_update + + +@app.callback( + Input("personal-info-modal", "okCounts"), + [State("personal-info-form", "values")], + prevent_initial_call=True, +) +def handle_personal_info_update(okCounts, values): + """处理个人信息更新逻辑""" + + # 获取表单数据 + values = values or {} + + # 检查表单数据完整性 + if not values.get("personal-info-user-name"): + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="error", + content="请完善用户信息后再提交", + ) + }, + ) + + else: + # 检查用户名是否重复 + match_user = Users.get_user_by_name(values["personal-info-user-name"]) + + # 若与其他用户用户名重复 + if match_user and match_user.user_id != current_user.id: + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="error", + content="用户名已存在", + ) + }, + ) + + else: + # 更新用户信息 + if values.get("personal-info-user-password"): + # 同时更新用户名和密码散列值 + Users.update_user( + user_id=current_user.id, + user_name=values["personal-info-user-name"], + password_hash=generate_password_hash( + values["personal-info-user-password"] + ), + ) + else: + # 只更新用户名 + Users.update_user( + user_id=current_user.id, user_name=values["personal-info-user-name"] + ) + + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="success", + content="用户信息更新成功,页面即将刷新", + ) + }, + ) + # 页面延时刷新 + set_props( + "global-reload", + {"children": fuc.FefferyReload(reload=True, delay=2000)}, + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/components/user_manage.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/components/user_manage.py new file mode 100644 index 0000000..aafcb7b --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/components/user_manage.py @@ -0,0 +1,298 @@ +import uuid +import time +import dash +from dash import set_props +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style +from dash.dependencies import Input, Output, State +from werkzeug.security import generate_password_hash + +from server import app +from models.users import Users +from configs import AuthConfig + + +def render(): + """渲染用户管理抽屉""" + + return fac.AntdDrawer( + id="user-manage-drawer", + title=fac.AntdSpace([fac.AntdIcon(icon="antd-team"), "用户管理"]), + width="65vw", + ) + + +def refresh_user_manage_table_data(): + """当前模块内复用工具函数,刷新用户管理表格数据""" + + # 查询全部用户信息 + all_users = Users.get_all_users() + + return [ + { + "user_id": item["user_id"], + "user_name": item["user_name"], + "user_role": { + "tag": AuthConfig.roles.get(item["user_role"])["description"], + "color": ( + "gold" if item["user_role"] == AuthConfig.admin_role else "blue" + ), + }, + "操作": { + "content": "删除", + "type": "link", + "danger": True, + "disabled": item["user_role"] == AuthConfig.admin_role, + }, + } + for item in all_users + ] + + +@app.callback( + [ + Output("user-manage-drawer", "children"), + Output("user-manage-drawer", "loading", allow_duplicate=True), + ], + Input("user-manage-drawer", "visible"), + prevent_initial_call=True, +) +def render_user_manage_drawer(visible): + """每次用户管理抽屉打开后,动态更新内容""" + + if visible: + time.sleep(0.5) + + return [ + [ + # 新增用户模态框 + fac.AntdModal( + id="user-manage-add-user-modal", + title=fac.AntdSpace( + [fac.AntdIcon(icon="antd-user-add"), "新增用户"] + ), + mask=False, + renderFooter=True, + okClickClose=False, + ), + fac.AntdSpace( + [ + fac.AntdTable( + id="user-manage-table", + columns=[ + { + "dataIndex": "user_id", + "title": "用户id", + "renderOptions": { + "renderType": "ellipsis-copyable", + }, + }, + { + "dataIndex": "user_name", + "title": "用户名", + "renderOptions": { + "renderType": "ellipsis-copyable", + }, + }, + { + "dataIndex": "user_role", + "title": "用户角色", + "renderOptions": {"renderType": "tags"}, + }, + { + "dataIndex": "操作", + "title": "操作", + "renderOptions": { + "renderType": "button", + }, + }, + ], + data=refresh_user_manage_table_data(), + tableLayout="fixed", + filterOptions={ + "user_name": { + "filterMode": "keyword", + }, + "user_role": { + "filterMode": "checkbox", + }, + }, + bordered=True, + title=fac.AntdSpace( + [ + fac.AntdButton( + "新增用户", + id="user-manage-add-user", + type="primary", + size="small", + ) + ] + ), + ) + ], + direction="vertical", + style=style(width="100%"), + ), + ], + False, + ] + + return dash.no_update + + +@app.callback( + [ + Output("user-manage-add-user-modal", "visible"), + Output("user-manage-add-user-modal", "children"), + ], + Input("user-manage-add-user", "nClicks"), + prevent_initial_call=True, +) +def open_add_user_modal(nClicks): + """打开新增用户模态框""" + + return [ + True, + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id="user-manage-add-user-form-user-name", + placeholder="请输入用户名", + allowClear=True, + ), + label="用户名", + ), + fac.AntdFormItem( + fac.AntdInput( + id="user-manage-add-user-form-user-password", + placeholder="请输入密码", + mode="password", + allowClear=True, + ), + label="密码", + ), + fac.AntdFormItem( + fac.AntdSelect( + id="user-manage-add-user-form-user-role", + options=[ + {"label": value["description"], "value": key} + for key, value in AuthConfig.roles.items() + ], + allowClear=False, + ), + label="用户角色", + ), + ], + id="user-manage-add-user-form", + key=str(uuid.uuid4()), # 强制刷新 + enableBatchControl=True, + layout="vertical", + values={"user-manage-add-user-form-user-role": AuthConfig.normal_role}, + style=style(marginTop=32), + ), + ] + + +@app.callback( + Input("user-manage-add-user-modal", "okCounts"), + [State("user-manage-add-user-form", "values")], + prevent_initial_call=True, +) +def handle_add_user(okCounts, values): + """处理新增用户逻辑""" + + # 获取表单数据 + values = values or {} + + # 检查表单数据完整性 + if not ( + values.get("user-manage-add-user-form-user-name") + and values.get("user-manage-add-user-form-user-password") + ): + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="error", + content="请完善用户信息后再提交", + ) + }, + ) + + else: + # 检查用户名是否重复 + match_user = Users.get_user_by_name( + values["user-manage-add-user-form-user-name"] + ) + + # 若用户名重复 + if match_user: + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="error", + content="用户名已存在", + ) + }, + ) + + else: + # 新增用户 + Users.add_user( + user_id=str(uuid.uuid4()), + user_name=values["user-manage-add-user-form-user-name"], + password_hash=generate_password_hash( + values["user-manage-add-user-form-user-password"] + ), + user_role=values["user-manage-add-user-form-user-role"], + ) + + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="success", + content="用户添加成功", + ) + }, + ) + + # 刷新用户列表 + set_props( + "user-manage-table", + {"data": refresh_user_manage_table_data()}, + ) + + +@app.callback( + Input("user-manage-table", "nClicksButton"), + [ + State("user-manage-table", "clickedContent"), + State("user-manage-table", "recentlyButtonClickedRow"), + ], + prevent_initial_call=True, +) +def handle_user_delete(nClicksButton, clickedContent, recentlyButtonClickedRow): + """处理用户删除逻辑""" + + if clickedContent == "删除": + # 删除用户 + Users.delete_user(user_id=recentlyButtonClickedRow["user_id"]) + + set_props( + "global-message", + { + "children": fac.AntdMessage( + type="success", + content="用户删除成功", + ) + }, + ) + + # 刷新用户列表 + set_props( + "user-manage-table", + {"data": refresh_user_manage_table_data()}, + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/__init__.py new file mode 100644 index 0000000..5c0fa5f --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/__init__.py @@ -0,0 +1,4 @@ +from .base_config import BaseConfig # noqa: F401 +from .router_config import RouterConfig # noqa: F401 +from .layout_config import LayoutConfig # noqa: F401 +from .auth_config import AuthConfig # noqa: F401 diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/auth_config.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/auth_config.py new file mode 100644 index 0000000..754f344 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/auth_config.py @@ -0,0 +1,35 @@ +class AuthConfig: + """用户鉴权配置参数""" + + # 角色权限类别 + roles: dict = { + "admin": { + "description": "系统管理员", + }, + "normal": { + "description": "常规用户", + }, + } + + # 常规用户角色 + normal_role: str = "normal" + + # 管理员角色 + admin_role: str = "admin" + + # 不同角色权限页面可访问性规则,其中'include'模式下自动会纳入首页,无需额外设置 + # type:规则类型,可选项有'all'(可访问全部页面)、'include'(可访问指定若干页面)、'exclude'(不可访问指定若干页面) + # keys:受type字段影响,定义可访问的指定若干页面/不可访问的指定若干页面,所对应RouterConfig.core_side_menu中菜单结构的key值 + pathname_access_rules: dict = { + "admin": {"type": "all"}, + "normal": { + "type": "exclude", + "keys": [ + "/core/sub-menu-page3", + "/core/independent-page", + "/core/independent-page/demo", + "/core/other-page1", + ], + }, + # "normal": {"type": "include", "keys": ["/core/page2", "/core/page5"]}, + } diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/base_config.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/base_config.py new file mode 100644 index 0000000..1e27776 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/base_config.py @@ -0,0 +1,31 @@ +from typing import List, Union + + +class BaseConfig: + """应用基础配置参数""" + + # 应用基础标题 + app_title: str = "Magic Dash Pro" + + # 应用版本 + app_version: str = "0.2.8" + + # 应用密钥 + app_secret_key: str = "magic-dash-pro-demo" + + # 浏览器最低版本限制规则 + min_browser_versions: List[dict] = [ + {"browser": "Chrome", "version": 88}, + {"browser": "Firefox", "version": 78}, + {"browser": "Edge", "version": 100}, + ] + + # 是否基于min_browser_versions开启严格的浏览器类型限制 + # 不在min_browser_versions规则内的浏览器将被直接拦截 + strict_browser_type_check: bool = False + + # 是否启用重复登录辅助检查 + enable_duplicate_login_check: bool = True + + # 重复登录辅助检查轮询间隔时间,单位:秒 + duplicate_login_check_interval: Union[int, float] = 10 diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/layout_config.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/layout_config.py new file mode 100644 index 0000000..ab73f9c --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/layout_config.py @@ -0,0 +1,11 @@ +from typing import Literal + + +class LayoutConfig: + """页面布局相关配置参数""" + + # 核心页面侧边栏像素宽度 + core_side_width: int = 350 + + # 登录页面左侧内容形式,可选项有'image'(图片内容)、'video'(视频内容) + login_left_side_content_type: Literal["image", "video"] = "image" diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/router_config.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/router_config.py new file mode 100644 index 0000000..863718e --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/configs/router_config.py @@ -0,0 +1,137 @@ +from typing import List + + +class RouterConfig: + """路由配置参数""" + + # 与应用首页对应的pathname地址 + index_pathname: str = "/index" + + # 核心页面侧边菜单完整结构 + core_side_menu: List[dict] = [ + { + "component": "ItemGroup", + "props": { + "title": "主要页面", + "key": "主要页面", + }, + "children": [ + { + "component": "Item", + "props": { + "title": "首页", + "key": "/", + "icon": "antd-home", + "href": "/", + }, + }, + { + "component": "Item", + "props": { + "title": "主要页面1", + "key": "/core/page1", + "icon": "antd-app-store", + "href": "/core/page1", + }, + }, + { + "component": "SubMenu", + "props": { + "key": "子菜单演示", + "title": "子菜单演示", + "icon": "antd-catalog", + }, + "children": [ + { + "component": "Item", + "props": { + "key": "/core/sub-menu-page1", + "title": "子菜单演示1", + "href": "/core/sub-menu-page1", + }, + }, + { + "component": "Item", + "props": { + "key": "/core/sub-menu-page2", + "title": "子菜单演示2", + "href": "/core/sub-menu-page2", + }, + }, + { + "component": "Item", + "props": { + "key": "/core/sub-menu-page3", + "title": "子菜单演示3", + "href": "/core/sub-menu-page3", + }, + }, + ], + }, + { + "component": "Item", + "props": { + "title": "独立页面渲染入口页", + "key": "/core/independent-page", + "icon": "antd-app-store", + "href": "/core/independent-page", + }, + }, + ], + }, + { + "component": "ItemGroup", + "props": { + "title": "其他页面", + "key": "其他页面", + }, + "children": [ + { + "component": "Item", + "props": { + "title": "其他页面1", + "key": "/core/other-page1", + "icon": "antd-app-store", + "href": "/core/other-page1", + }, + } + ], + }, + ] + + # 有效页面pathname地址 -> 页面标题映射字典 + valid_pathnames: dict = { + "/login": "登录页", + "/": "首页", + index_pathname: "首页", + "/core/page1": "主要页面1", + "/core/sub-menu-page1": "子菜单演示1", + "/core/sub-menu-page2": "子菜单演示2", + "/core/sub-menu-page3": "子菜单演示3", + "/core/independent-page": "独立页面渲染入口页", + "/core/other-page1": "其他页面1", + "/403-demo": "403状态页演示", + "/404-demo": "404状态页演示", + "/500-demo": "500状态页演示", + # 独立渲染页面 + "/core/independent-page/demo": "独立页面演示示例", + } + + # 独立渲染展示的核心页面 + independent_core_pathnames: List[str] = ["/core/independent-page/demo"] + + # 无需权限校验的公开页面 + public_pathnames: List[str] = [ + "/login", + "/logout", + "/403-demo", + "/404-demo", + "/500-demo", + ] + + # 部分页面pathname对应要展开的子菜单层级 + side_menu_open_keys: dict = { + "/core/sub-menu-page1": ["子菜单演示"], + "/core/sub-menu-page2": ["子菜单演示"], + "/core/sub-menu-page3": ["子菜单演示"], + } diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/models/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/__init__.py new file mode 100644 index 0000000..e5dab7c --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/__init__.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker + +# 创建数据库引擎 +engine = create_engine('sqlite:///magic_dash.db') + +# 创建Session工厂 +db = scoped_session(sessionmaker(bind=engine)) + +# 创建声明基类 +Base = declarative_base() +Base.query = db.query_property() diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/models/exceptions.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/exceptions.py new file mode 100644 index 0000000..ddc47c1 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/exceptions.py @@ -0,0 +1,10 @@ +class InvalidUserError(Exception): + """非法用户信息""" + + pass + + +class ExistingUserError(Exception): + """用户信息已存在""" + + pass diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/models/init_db.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/init_db.py new file mode 100644 index 0000000..3d4e1f9 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/init_db.py @@ -0,0 +1,21 @@ +from configs import AuthConfig +from models import Base, engine +from werkzeug.security import generate_password_hash + +# 导入相关数据表模型 +from .users import Users + +# 创建表(如果表不存在) +Base.metadata.create_all(engine) + +if __name__ == "__main__": + # 初始化管理员用户 + # 命令:python -m models.init_db + Users.delete_user("admin") + Users.add_user( + user_id="admin", + user_name="admin", + password_hash=generate_password_hash("admin123"), + user_role=AuthConfig.admin_role, + ) + print("管理员用户 admin 初始化完成") diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/models/users.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/users.py new file mode 100644 index 0000000..fb05c92 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/models/users.py @@ -0,0 +1,134 @@ +from typing import Dict, List, Union + +from configs import AuthConfig +from sqlalchemy import Column, String +from sqlalchemy.types import JSON +from werkzeug.security import check_password_hash + +from . import Base, db +from .exceptions import ExistingUserError, InvalidUserError + + +class Users(Base): + """用户信息表模型类""" + __tablename__ = 'users' + + # 用户id,主键 + user_id = Column(String, primary_key=True) + + # 用户名,唯一 + user_name = Column(String, unique=True, nullable=False) + + # 用户密码散列值 + password_hash = Column(String, nullable=False) + + # 用户角色,全部可选项见configs.AuthConfig.roles + user_role = Column(String, default=AuthConfig.normal_role) + + # 用户最近一次登录会话token + session_token = Column(String, nullable=True) + + # 用户其他辅助信息,任意JSON格式,允许空值 + other_info = Column(JSON, nullable=True) + + def __repr__(self): + return f"" + + @classmethod + def get_user(cls, user_id: str): + """根据用户id查询用户信息""" + user = cls.query.get(user_id) + return user if user else None + + @classmethod + def get_user_by_name(cls, user_name: str): + """根据用户名查询用户信息""" + user = cls.query.filter_by(user_name=user_name).first() + return user if user else None + + @classmethod + def get_all_users(cls): + """获取所有用户信息""" + users = cls.query.all() + return [ + { + 'user_id': user.user_id, + 'user_name': user.user_name, + 'user_role': user.user_role, + 'session_token': user.session_token, + 'other_info': user.other_info + } + for user in users + ] + + @classmethod + def check_user_password(cls, user_id: str, password: str): + """校验用户密码""" + user = cls.get_user(user_id) + if user: + return check_password_hash(user.password_hash, password) + return False + + @classmethod + def add_user( + cls, + user_id: str, + user_name: str, + password_hash: str, + user_role: str = "normal", + other_info: Union[Dict, List] = None, + ): + """添加用户""" + # 若必要用户信息不完整 + if not (user_id and user_name and password_hash): + raise InvalidUserError("用户信息不完整") + + # 若用户id已存在 + elif cls.get_user(user_id): + raise ExistingUserError("用户id已存在") + + # 若用户名存在重复 + elif cls.get_user_by_name(user_name): + raise ExistingUserError("用户名已存在") + + # 执行用户添加操作 + try: + new_user = cls( + user_id=user_id, + user_name=user_name, + password_hash=password_hash, + user_role=user_role, + other_info=other_info, + ) + db.add(new_user) + db.commit() + except: + db.rollback() + raise + + @classmethod + def delete_user(cls, user_id: str): + """删除用户""" + try: + user = cls.get_user(user_id) + if user: + db.delete(user) + db.commit() + except: + db.rollback() + raise + + @classmethod + def update_user(cls, user_id: str, **kwargs): + """更新用户信息""" + try: + user = cls.get_user(user_id) + if user: + for key, value in kwargs.items(): + setattr(user, key, value) + db.commit() + return user + except: + db.rollback() + raise + return None diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/requirements.txt b/magic_dash/templates/magic-dash-pro-sqlalchemy/requirements.txt new file mode 100644 index 0000000..9b845fb --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/requirements.txt @@ -0,0 +1,9 @@ +dash>=2.18.2 +feffery_antd_components>=0.3.12 +feffery_dash_utils>=0.1.5 +feffery_utils_components>=0.2.0rc25 +Flask_Login +sqlalchemy +user_agents +flask-compress +flask-principal \ No newline at end of file diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/server.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/server.py new file mode 100644 index 0000000..64c0026 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/server.py @@ -0,0 +1,121 @@ +import dash +from flask import request +from user_agents import parse +from flask_principal import Principal, Permission, RoleNeed, identity_loaded +from flask_login import LoginManager, UserMixin, current_user, AnonymousUserMixin + +# 应用基础参数 +from models.users import Users +from configs import BaseConfig, AuthConfig + +app = dash.Dash( + __name__, + title=BaseConfig.app_title, + suppress_callback_exceptions=True, + compress=True, # 隐式依赖flask-compress + update_title=None, +) +server = app.server + +# 设置应用密钥 +app.server.secret_key = BaseConfig.app_secret_key + +# 为当前应用添加flask-login用户登录管理 +login_manager = LoginManager() +login_manager.init_app(app.server) + +# 为当前应用添加flask-principal权限管理 +principals = Principal(app.server) + + +class User(UserMixin): + """flask-login专用用户类""" + + def __init__( + self, id: str, user_name: str, user_role: str, session_token: str = None + ) -> None: + """初始化用户信息""" + + self.id = id + self.user_name = user_name + self.user_role = user_role + self.session_token = session_token + + +@login_manager.user_loader +def user_loader(user_id): + """flask-login内部专用用户加载函数""" + + # 根据当前要加载的用户id,从数据库中获取匹配用户信息 + match_user = Users.get_user(user_id) + + # 处理未匹配到有效用户的情况 + if not match_user: + return AnonymousUserMixin() + + # 当前用户实例化 + user = User( + id=match_user.user_id, + user_name=match_user.user_name, + user_role=match_user.user_role, + session_token=match_user.session_token, + ) + + return user + + +# 定义不同用户角色 +user_permissions = {role: Permission(RoleNeed(role)) for role in AuthConfig.roles} + + +@identity_loaded.connect_via(app.server) +def on_identity_loaded(sender, identity): + """flask-principal身份加载回调函数""" + + identity.user = current_user + + if hasattr(current_user, "user_role"): + identity.provides.add(RoleNeed(current_user.user_role)) + + +@app.server.before_request +def check_browser(): + """检查浏览器版本是否符合最低要求""" + + # 提取当前请求对应的浏览器信息 + user_agent = parse(str(request.user_agent)) + + # 若浏览器版本信息有效 + if user_agent.browser.version != (): + # IE相关浏览器直接拦截 + if user_agent.browser.family == "IE": + return ( + "
" + "请不要使用IE浏览器,或开启了IE内核兼容模式的其他浏览器访问本应用
" + ) + # 基于BaseConfig.min_browser_versions配置,对相关浏览器最低版本进行检查 + for rule in BaseConfig.min_browser_versions: + # 若当前请求对应的浏览器版本,低于声明的最低支持版本 + if ( + user_agent.browser.family == rule["browser"] + and user_agent.browser.version[0] < rule["version"] + ): + return ( + "
" + "您的{}浏览器版本低于本应用最低支持版本({}),请升级浏览器后再访问
" + ).format(rule["browser"], rule["version"]) + + # 若开启了严格的浏览器类型限制 + if BaseConfig.strict_browser_type_check: + # 若当前浏览器不在声明的浏览器范围内 + if user_agent.browser.family not in [ + rule["browser"] for rule in BaseConfig.min_browser_versions + ]: + return ( + "
" + "当前浏览器类型不在支持的范围内,支持的浏览器类型有:{}
" + ).format( + "、".join( + [rule["browser"] for rule in BaseConfig.min_browser_versions] + ) + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/utils/clear_pycache.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/utils/clear_pycache.py new file mode 100644 index 0000000..cc01cb8 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/utils/clear_pycache.py @@ -0,0 +1,15 @@ +import os +import shutil + + +def clear_pycache(): + """清除当前目录下任何位置的__pycache__文件夹""" + + for root, dirs, files in os.walk("."): + if "__pycache__" in dirs: + shutil.rmtree(os.path.join(root, "__pycache__")) + + +if __name__ == "__main__": + clear_pycache() + print("__pycache__清除完成") diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/__init__.py new file mode 100644 index 0000000..182e7f5 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/__init__.py @@ -0,0 +1,342 @@ +from dash import html, dcc +from flask_login import current_user +import feffery_antd_components as fac +import feffery_utils_components as fuc +from feffery_dash_utils.style_utils import style + +from views.core_pages import independent_page_demo +from components import core_side_menu, personal_info, user_manage +from configs import BaseConfig, RouterConfig, LayoutConfig, AuthConfig + +# 令绑定的回调函数子模块生效 +import callbacks.core_pages_c # noqa: F401 + + +def get_page_search_options(current_user_access_rule: str): + """当前模块内工具函数,生成页面搜索选项""" + + options = [{"label": "首页", "value": "/"}] + + for pathname, title in RouterConfig.valid_pathnames.items(): + # 忽略已添加的首页 + if pathname in [RouterConfig.index_pathname, "/"]: + pass + + elif ( + # 公开页面全部放行 + pathname in RouterConfig.public_pathnames + or current_user_access_rule["type"] == "all" + ): + options.append( + { + "label": title, + "value": f"{pathname}|{title}", + } + ) + + elif current_user_access_rule["type"] == "include": + if pathname in current_user_access_rule["keys"]: + options.append( + { + "label": title, + "value": f"{pathname}|{title}", + } + ) + + elif current_user_access_rule["type"] == "exclude": + if pathname not in current_user_access_rule["keys"]: + options.append( + { + "label": title, + "value": f"{pathname}|{title}", + } + ) + + return options + + +def render(current_user_access_rule: str, current_pathname: str = None): + """渲染核心页面骨架 + + Args: + current_user_access_rule (str): 当前用户页面可访问性规则 + current_pathname (str, optional): 当前页面pathname. Defaults to None. + """ + + # 判断是否需要独立渲染 + if current_pathname in RouterConfig.independent_core_pathnames: + # 返回不同地址规则对应页面内容 + if current_pathname == "/core/independent-page/demo": + return independent_page_demo.render() + + return html.Div( + [ + # 核心页面常量参数数据 + dcc.Store( + id="core-page-config", + data=dict(core_side_width=LayoutConfig.core_side_width), + ), + # 核心页面独立路由监听 + dcc.Location(id="core-url"), + # ctrl+k快捷键监听 + fuc.FefferyKeyPress(id="core-ctrl-k-key-press", keys="ctrl.k"), + # 注入个人信息模态框 + personal_info.render(), + # 若当前用户角色为系统管理员 + *( + # 注入用户管理抽屉 + [ + user_manage.render(), + ] + if current_user.user_role == AuthConfig.admin_role + else [] + ), + # 页首 + fac.AntdRow( + [ + # logo+标题+版本+侧边折叠按钮 + fac.AntdCol( + fac.AntdFlex( + [ + dcc.Link( + fac.AntdSpace( + [ + # logo + html.Img( + src="/assets/imgs/logo.svg", + height=32, + style=style(display="block"), + ), + fac.AntdSpace( + [ + # 标题 + fac.AntdText( + BaseConfig.app_title, + strong=True, + style=style(fontSize=20), + ), + fac.AntdText( + BaseConfig.app_version, + className="global-help-text", + style=style(fontSize=12), + ), + ], + align="baseline", + size=3, + id="core-header-title", + ), + ] + ), + href="/", + ), + # 侧边折叠按钮 + fac.AntdButton( + fac.AntdIcon( + id="core-side-menu-collapse-button-icon", + icon="antd-menu-fold", + className="global-help-text", + ), + id="core-side-menu-collapse-button", + type="text", + size="small", + ), + ], + id="core-header-side", + justify="space-between", + align="center", + style=style( + width=LayoutConfig.core_side_width, + height="100%", + paddingLeft=20, + paddingRight=20, + borderRight="1px solid #dae0ea", + boxSizing="border-box", + ), + ), + flex="none", + ), + # 页面搜索+功能图标+用户信息 + fac.AntdCol( + fac.AntdFlex( + [ + # 页面搜索 + fac.AntdSpace( + [ + fac.AntdSelect( + id="core-page-search", + placeholder="输入关键词搜索页面", + options=get_page_search_options( + current_user_access_rule + ), + variant="filled", + style=style(width=250), + ), + fac.AntdText( + [ + fac.AntdText( + "Ctrl", + keyboard=True, + className="global-help-text", + ), + fac.AntdText( + "K", + keyboard=True, + className="global-help-text", + ), + ] + ), + ], + size=5, + ), + # 功能图标+用户信息 + fac.AntdSpace( + [ + # 示例功能图标1 + fac.AntdButton( + icon=fac.AntdIcon( + icon="antd-setting", + className="global-help-text", + ), + type="text", + ), + # 示例功能图标2 + fac.AntdButton( + icon=fac.AntdIcon( + icon="antd-bell", + className="global-help-text", + ), + type="text", + ), + # 示例功能图标3 + fac.AntdButton( + icon=fac.AntdIcon( + icon="antd-question-circle", + className="global-help-text", + ), + type="text", + ), + # 自定义分隔符 + html.Div( + style=style( + width=0, + height=42, + borderLeft="1px solid #e1e5ee", + margin="0 12px", + ) + ), + # 用户头像 + fac.AntdAvatar( + mode="text", + text="🤩", + size=36, + style=style(background="#f4f6f9"), + ), + # 用户名+角色 + fac.AntdFlex( + [ + fac.AntdText( + current_user.user_name.capitalize(), + strong=True, + ), + fac.AntdText( + "角色:{}".format( + AuthConfig.roles.get( + current_user.user_role + )["description"] + ), + className="global-help-text", + style=style(fontSize=12), + ), + ], + vertical=True, + ), + # 用户管理菜单 + fac.AntdDropdown( + fac.AntdButton( + icon=fac.AntdIcon( + icon="antd-more", + className="global-help-text", + ), + type="text", + ), + id="core-pages-header-user-dropdown", + menuItems=[ + { + "title": "个人信息", + "key": "个人信息", + }, + # 若当前用户角色为系统管理员 + *( + [ + { + "title": "用户管理", + "key": "用户管理", + } + ] + if ( + current_user.user_role + == AuthConfig.admin_role + ) + else [] + ), + {"isDivider": True}, + { + "title": "退出登录", + "href": "/logout", + }, + ], + trigger="click", + ), + ] + ), + ], + justify="space-between", + align="center", + style=style( + height="100%", + paddingLeft=20, + paddingRight=20, + ), + ), + flex="auto", + ), + ], + wrap=False, + align="middle", + style=style( + height=72, + borderBottom="1px solid #dae0ea", + position="sticky", + top=0, + zIndex=1000, + background="#fff", + ), + ), + # 主题区域 + fac.AntdRow( + [ + # 侧边栏 + fac.AntdCol( + core_side_menu.render( + current_user_access_rule=current_user_access_rule + ), + flex="none", + ), + # 内容区域 + fac.AntdCol( + fac.AntdSkeleton( + html.Div( + id="core-container", style=style(padding="36px 42px") + ), + listenPropsMode="include", + includeProps=["core-container.children"], + active=True, + style=style(padding="36px 42px"), + ), + flex="auto", + ), + ], + wrap=False, + ), + ] + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/independent_page.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/independent_page.py new file mode 100644 index 0000000..adc63ab --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/independent_page.py @@ -0,0 +1,36 @@ +from dash import html +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """子页面:独立页面渲染入口页简单示例""" + + return fac.AntdSpace( + [ + fac.AntdBreadcrumb( + items=[{"title": "主要页面"}, {"title": "独立页面渲染入口页"}] + ), + fac.AntdAlert( + type="info", + showIcon=True, + message="这里是独立页面渲染入口页演示示例", + description=fac.AntdText( + [ + "点击", + html.A( + "此处", href="/core/independent-page/demo", target="_blank" + ), + "打开示例独立显示页面。", + html.Br(), + "本页面模块路径:", + fac.AntdText( + "views/core_pages/independent_page.py", strong=True + ), + ] + ), + ), + ], + direction="vertical", + style=style(width="100%"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/independent_page_demo.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/independent_page_demo.py new file mode 100644 index 0000000..ca0be77 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/independent_page_demo.py @@ -0,0 +1,24 @@ +from dash import html +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """子页面:独立页面渲染简单示例""" + + return html.Div( + fac.AntdAlert( + type="info", + showIcon=True, + message="这里是独立页面演示示例", + description=fac.AntdText( + [ + "本页面模块路径:", + fac.AntdText( + "views/core_pages/independent_page_demo.py", strong=True + ), + ] + ), + ), + style=style(padding="24px 32px"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/index.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/index.py new file mode 100644 index 0000000..66425a8 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/index.py @@ -0,0 +1,28 @@ +from dash import html +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """子页面:首页渲染简单示例""" + + return fac.AntdSpace( + [ + fac.AntdBreadcrumb(items=[{"title": "主要页面"}, {"title": "首页"}]), + fac.AntdAlert( + type="info", + showIcon=True, + message="欢迎来到首页!", + description=fac.AntdText( + [ + "这里以首页为例,演示核心页面下,各子页面构建方式的简单示例😉~", + html.Br(), + "本页面模块路径:", + fac.AntdText("views/core_pages/index.py", strong=True), + ] + ), + ), + ], + direction="vertical", + style=style(width="100%"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/page1.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/page1.py new file mode 100644 index 0000000..00b9f7e --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/page1.py @@ -0,0 +1,43 @@ +from dash import html +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + +# 令对应当前页面的回调函数子模块生效 +import callbacks.core_pages_c.page1_c # noqa: F401 + + +def render(): + """子页面:主要页面1渲染简单示例""" + + return fac.AntdSpace( + [ + fac.AntdBreadcrumb(items=[{"title": "主要页面"}, {"title": "主要页面1"}]), + fac.AntdAlert( + type="info", + showIcon=True, + message="这里是主要页面1演示示例", + description=fac.AntdText( + [ + "本页面简单演示了如何在当前项目模板中添加新的自定义页面,且页面内容中包含由回调函数控制的简单演示用交互功能。", + html.Br(), + "本页面模块路径:", + fac.AntdText("views/core_pages/page1.py", strong=True), + html.Br(), + "本页面回调模块路径:", + fac.AntdText("callbacks/core_pages_c/page1_c.py", strong=True), + ] + ), + ), + fac.AntdText("回调功能演示:"), + fac.AntdSpace( + [ + fac.AntdButton( + "点击测试", id="core-page1-demo-button", type="primary" + ), + fac.AntdText("累计点击次数:0", id="core-page1-demo-output"), + ] + ), + ], + direction="vertical", + style=style(width="100%"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page1.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page1.py new file mode 100644 index 0000000..8c03469 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page1.py @@ -0,0 +1,45 @@ +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """子页面:子菜单演示1渲染简单示例""" + + return fac.AntdSpace( + [ + fac.AntdBreadcrumb( + items=[ + {"title": "主要页面"}, + {"title": "子菜单演示"}, + { + "title": "子菜单演示1", + "menuItems": [ + { + "title": "子菜单演示2", + "href": "/core/sub-menu-page2", + "target": "_blank", + }, + { + "title": "子菜单演示3", + "href": "/core/sub-menu-page3", + "target": "_blank", + }, + ], + }, + ] + ), + fac.AntdAlert( + type="info", + showIcon=True, + message="这里是子菜单演示1演示示例", + description=fac.AntdText( + [ + "本页面模块路径:", + fac.AntdText("views/core_pages/sub_menu_page1.py", strong=True), + ] + ), + ), + ], + direction="vertical", + style=style(width="100%"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page2.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page2.py new file mode 100644 index 0000000..94cb19c --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page2.py @@ -0,0 +1,45 @@ +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """子页面:子菜单演示2渲染简单示例""" + + return fac.AntdSpace( + [ + fac.AntdBreadcrumb( + items=[ + {"title": "主要页面"}, + {"title": "子菜单演示"}, + { + "title": "子菜单演示2", + "menuItems": [ + { + "title": "子菜单演示1", + "href": "/core/sub-menu-page1", + "target": "_blank", + }, + { + "title": "子菜单演示3", + "href": "/core/sub-menu-page3", + "target": "_blank", + }, + ], + }, + ] + ), + fac.AntdAlert( + type="info", + showIcon=True, + message="这里是子菜单演示2演示示例", + description=fac.AntdText( + [ + "本页面模块路径:", + fac.AntdText("views/core_pages/sub_menu_page2.py", strong=True), + ] + ), + ), + ], + direction="vertical", + style=style(width="100%"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page3.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page3.py new file mode 100644 index 0000000..2cd4ec1 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/core_pages/sub_menu_page3.py @@ -0,0 +1,45 @@ +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """子页面:子菜单演示3渲染简单示例""" + + return fac.AntdSpace( + [ + fac.AntdBreadcrumb( + items=[ + {"title": "主要页面"}, + {"title": "子菜单演示"}, + { + "title": "子菜单演示3", + "menuItems": [ + { + "title": "子菜单演示1", + "href": "/core/sub-menu-page1", + "target": "_blank", + }, + { + "title": "子菜单演示2", + "href": "/core/sub-menu-page2", + "target": "_blank", + }, + ], + }, + ] + ), + fac.AntdAlert( + type="info", + showIcon=True, + message="这里是子菜单演示3演示示例", + description=fac.AntdText( + [ + "本页面模块路径:", + fac.AntdText("views/core_pages/sub_menu_page3.py", strong=True), + ] + ), + ), + ], + direction="vertical", + style=style(width="100%"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/login.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/login.py new file mode 100644 index 0000000..2c70d0f --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/login.py @@ -0,0 +1,194 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + +from configs import BaseConfig, LayoutConfig + +# 令绑定的回调函数子模块生效 +import callbacks.login_c # noqa: F401 + + +def render(): + """渲染用户登录页面""" + + return fac.AntdRow( + [ + # 左侧半边 + fac.AntdCol( + *( + [ + fuc.FefferyMotion( + html.Img( + src="/assets/imgs/login/插图1.svg", + style=style(width="25vw"), + ), + style={ + "position": "absolute", + "left": "10%", + "top": "15%", + "rotateZ": "-5deg", + }, + animate={"y": [25, -25, 25]}, + transition={ + "duration": 4.5, + "repeat": "infinity", + "type": "spring", + }, + ), + fuc.FefferyMotion( + html.Img( + src="/assets/imgs/login/插图2.svg", + style=style(width="15vw"), + ), + style={ + "position": "absolute", + "right": "20%", + "top": "25%", + "rotateZ": "15deg", + }, + animate={"y": [-15, 15, -15]}, + transition={ + "duration": 5.5, + "repeat": "infinity", + "type": "spring", + }, + ), + fuc.FefferyMotion( + html.Img( + src="/assets/imgs/login/插图3.svg", + style=style(width="12vw"), + ), + style={ + "position": "absolute", + "left": "25%", + "bottom": "25%", + "rotateZ": "-8deg", + }, + animate={"y": [10, -10, 10]}, + transition={ + "duration": 5, + "repeat": "infinity", + "type": "spring", + }, + ), + fuc.FefferyMotion( + html.Img( + src="/assets/imgs/login/插图4.svg", + style=style(width="25vw"), + ), + style={ + "position": "absolute", + "right": "15%", + "bottom": "8%", + "rotateZ": "5deg", + }, + animate={"y": [20, -20, 20]}, + transition={ + "duration": 6, + "repeat": "infinity", + "type": "spring", + }, + ), + ] + if LayoutConfig.login_left_side_content_type == "image" + else [ + html.Video( + src="/assets/videos/login-bg.mp4", + autoPlay=True, + muted=True, + loop=True, + style=style( + width="100%", + height="100%", + position="absolute", + objectFit="cover", + borderTopRightRadius=12, + borderBottomRightRadius=12, + pointerEvents="none", + ), + ) + ], + ), + span=14, + className="login-left-side", + style=( + style() + if LayoutConfig.login_left_side_content_type == "image" + else style(backgroundImage="none") + ), + ), + # 右侧半边 + fac.AntdCol( + fac.AntdCenter( + [ + fac.AntdSpace( + [ + html.Img( + src="/assets/imgs/logo.svg", + height=72, + ), + fac.AntdText( + BaseConfig.app_title, + style=style(fontSize=36), + ), + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id="login-user-name", + placeholder="请输入用户名", + size="large", + prefix=fac.AntdIcon( + icon="antd-user", + className="global-help-text", + ), + autoComplete="off", + ), + label="用户名", + ), + fac.AntdFormItem( + fac.AntdInput( + id="login-password", + placeholder="请输入密码", + size="large", + mode="password", + prefix=fac.AntdIcon( + icon="antd-lock", + className="global-help-text", + ), + ), + label="密码", + ), + fac.AntdCheckbox( + id="login-remember-me", label="记住我" + ), + fac.AntdButton( + "登录", + id="login-button", + loadingChildren="校验中", + type="primary", + block=True, + size="large", + style=style(marginTop=18), + ), + ], + id="login-form", + enableBatchControl=True, + layout="vertical", + style=style(width=350), + ), + ], + direction="vertical", + align="center", + ) + ], + style=style(height="calc(100% - 200px)"), + ), + span=10, + className="login-right-side", + ), + ], + wrap=False, + style=style(height="100vh"), + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_403.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_403.py new file mode 100644 index 0000000..c5eeb6b --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_403.py @@ -0,0 +1,21 @@ +from dash import html +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """渲染403状态页面""" + + return fac.AntdCenter( + fac.AntdResult( + # 自定义状态图片 + icon=html.Img( + src="/assets/imgs/status/403.svg", + style=style(height="50vh", pointerEvents="none"), + ), + title=fac.AntdText("权限不足", style=style(fontSize=20)), + subTitle="您没有访问该页面的权限", + extra=fac.AntdButton("返回首页", type="primary", href="/", target="_self"), + ), + style={"height": "calc(60vh + 100px)"}, + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_404.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_404.py new file mode 100644 index 0000000..07ff147 --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_404.py @@ -0,0 +1,21 @@ +from dash import html +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(): + """渲染404状态页面""" + + return fac.AntdCenter( + fac.AntdResult( + # 自定义状态图片 + icon=html.Img( + src="/assets/imgs/status/404.svg", + style=style(height="50vh", pointerEvents="none"), + ), + title=fac.AntdText("当前页面不存在", style=style(fontSize=20)), + subTitle="请检查您输入的网址是否正确", + extra=fac.AntdButton("返回首页", type="primary", href="/", target="_self"), + ), + style={"height": "calc(60vh + 100px)"}, + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_500.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_500.py new file mode 100644 index 0000000..fbe226e --- /dev/null +++ b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/_500.py @@ -0,0 +1,24 @@ +from dash import html +import feffery_antd_components as fac +from feffery_dash_utils.style_utils import style + + +def render(e: str = None): + """渲染500状态页面""" + + if e is None: + e = Exception("500状态页演示示例错误") + + return fac.AntdCenter( + fac.AntdResult( + # 自定义状态图片 + icon=html.Img( + src="/assets/imgs/status/500.svg", + style=style(height="50vh", pointerEvents="none"), + ), + title=fac.AntdText("系统内部错误", style=style(fontSize=20)), + subTitle="错误信息:" + str(e), + extra=fac.AntdButton("返回首页", type="primary", href="/", target="_self"), + ), + style={"height": "calc(60vh + 100px)"}, + ) diff --git a/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/__init__.py b/magic_dash/templates/magic-dash-pro-sqlalchemy/views/status_pages/__init__.py new file mode 100644 index 0000000..e69de29