feat: modern UI redesign - shell layout, theme switching, dashboard revamp

Phase 1-2 deliverables:
- shell.ui: Global layout framework (topbar + collapsible sidebar + main content)
- shell_theme.css: Dark/light theme CSS custom properties system
- shell_theme.js: Theme toggle + sidebar collapse with localStorage persistence
- global_menu.ui: Unified module navigation menu with RBAC visibility
- index.ui: Redesigned dashboard homepage with modern stat cards + quick links
- Stat card widgets: today_usage, today_amount, total_users, concurrent, errors
- chart_top_models.ui + api/top_models.dspy: ChartBar with data_url pattern
- table_top_users_amount.ui: Jinja2-rendered user ranking table
- build.sh: Added .css file linking support

Design system:
- Dark theme (default): slate color palette (#0B1120, #111827, #1E293B)
- Light theme: clean white palette with matching structure
- Theme persisted in localStorage, toggled via topbar button
- Sidebar collapsible with icon-only mode, state persisted in localStorage
- Responsive stat cards with hover effects and trend indicators
- Quick link cards for model management, users, knowledge base, errors
This commit is contained in:
yumoqing 2026-05-25 16:53:36 +08:00
parent 753887a4e2
commit ffdc7fc983
18 changed files with 1432 additions and 43 deletions

View File

@ -34,8 +34,8 @@ SAGE_MODULE_WWWROOT="$WWWROOT/$MODULE_NAME"
echo "Linking wwwroot..." echo "Linking wwwroot..."
mkdir -p "$SAGE_MODULE_WWWROOT/api" mkdir -p "$SAGE_MODULE_WWWROOT/api"
# Link all .ui and .js files (must be at wwwroot root per bricks convention) # Link all .ui, .js, and .css files (must be at wwwroot root per bricks convention)
for f in "$MODULE_WWWROOT"/*.ui "$MODULE_WWWROOT"/*.js; do for f in "$MODULE_WWWROOT"/*.ui "$MODULE_WWWROOT"/*.js "$MODULE_WWWROOT"/*.css; do
[ -f "$f" ] && ln -sf "$f" "$SAGE_MODULE_WWWROOT/" [ -f "$f" ] && ln -sf "$f" "$SAGE_MODULE_WWWROOT/"
done done

View File

@ -2,6 +2,7 @@ README.md
pyproject.toml pyproject.toml
dashboard_for_sage/__init__.py dashboard_for_sage/__init__.py
dashboard_for_sage/init.py dashboard_for_sage/init.py
dashboard_for_sage/load_dashboard.py
dashboard_for_sage.egg-info/PKG-INFO dashboard_for_sage.egg-info/PKG-INFO
dashboard_for_sage.egg-info/SOURCES.txt dashboard_for_sage.egg-info/SOURCES.txt
dashboard_for_sage.egg-info/dependency_links.txt dashboard_for_sage.egg-info/dependency_links.txt

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Top models data API for ChartBar"""
import json
models = await get_top_models(request)
print(json.dumps(models))

View File

@ -0,0 +1,10 @@
{
"widgettype": "ChartBar",
"options": {
"height": "280px",
"width": "100%",
"data_url": "{{entire_url('api/top_models.dspy')}}",
"nameField": "model_name",
"valueFields": ["cnt", "total_amount"]
}
}

58
wwwroot/global_menu.ui Normal file
View File

@ -0,0 +1,58 @@
{% set roles = get_user_roles(get_user()) %}
{
"widgettype": "Menu",
"id": "global_nav_menu",
"options": {
"width": "100%",
"height": "100%",
"bgcolor": "#111827",
"items": [
{
"name": "dashboard",
"label": "仪表盘",
"icon": "fa fa-dashboard",
"url": "{{entire_url('index.ui')}}",
"target": "app.sage_main_content"
},
{% if get_user() %}
{
"name": "llmage",
"label": "LLM 模型管理",
"icon": "fa fa-brain",
"submenu": "{{entire_url('/llmage/menu.ui')}}"
},
{
"name": "rag",
"label": "知识库管理",
"icon": "fa fa-database",
"url": "{{entire_url('/rag/menu.ui')}}",
"target": "app.sage_main_content"
},
{% endif %}
{% if 'reseller.operator' in roles or 'owner.superuser' in roles %}
{
"name": "platformbiz",
"label": "平台业务",
"icon": "fa fa-building",
"submenu": "{{entire_url('/platformbiz/menu.ui')}}"
},
{% endif %}
{% if get_user() %}
{
"name": "rbac",
"label": "用户与权限",
"icon": "fa fa-users",
"submenu": "{{entire_url('/rbac/admin_menu.ui')}}"
},
{% endif %}
{
"name": "hermes_web_cli",
"label": "AI Agent",
"icon": "fa fa-robot",
"url": "{{entire_url('/hermes-web-cli/index.ui')}}",
"target": "app.sage_main_content"
}
],
"menuitem_css": "menuitem"
}
}

View File

@ -2,66 +2,155 @@
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"width": "100%", "width": "100%",
"height": "100%", "height": "100%"
"padding": "20px",
"bgcolor": "#f0f2f5"
}, },
"subwidgets": [ "subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "24px"
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
"text": "数据概览",
"color": "#F1F5F9",
"fontWeight": "700"
}
},
{
"widgettype": "Filler"
},
{ {
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "Dashboard", "text": "最后更新: {{get_today_usage(request) and request._run_ns.curDateString() or ''}}",
"fontSize": "24px", "fontSize": "13px",
"fontWeight": "bold", "color": "#64748B"
"color": "#333",
"marginBottom": "20px"
} }
}
]
}, },
{ {
"widgettype": "ResponsableBox", "widgettype": "ResponsableBox",
"options": { "options": {
"gap": "16px", "gap": "16px",
"minWidth": "250px" "minWidth": "220px",
"marginBottom": "24px"
}, },
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "RefreshWidget", "widgettype": "RefreshWidget",
"id": "refresh_today_usage", "id": "stat_today_usage",
"options": { "options": {
"period_seconds": 10, "period_seconds": 30,
"url": "{{entire_url('today_usage.ui')}}" "url": "{{entire_url('stat_today_usage.ui')}}"
} }
}, },
{ {
"widgettype": "RefreshWidget", "widgettype": "RefreshWidget",
"id": "refresh_today_amount", "id": "stat_today_amount",
"options": { "options": {
"period_seconds": 10, "period_seconds": 30,
"url": "{{entire_url('today_amount.ui')}}" "url": "{{entire_url('stat_today_amount.ui')}}"
} }
}, },
{ {
"widgettype": "RefreshWidget", "widgettype": "RefreshWidget",
"id": "refresh_total_users", "id": "stat_total_users",
"options": { "options": {
"period_seconds": 10, "period_seconds": 60,
"url": "{{entire_url('total_users.ui')}}" "url": "{{entire_url('stat_total_users.ui')}}"
} }
}, },
{ {
"widgettype": "RefreshWidget", "widgettype": "RefreshWidget",
"id": "refresh_concurrent_users", "id": "stat_concurrent",
"options": { "options": {
"period_seconds": 10, "period_seconds": 15,
"url": "{{entire_url('concurrent_users.ui')}}" "url": "{{entire_url('stat_concurrent.ui')}}"
} }
}, },
{ {
"widgettype": "RefreshWidget", "widgettype": "RefreshWidget",
"id": "refresh_accounting_errors", "id": "stat_errors",
"options": { "options": {
"period_seconds": 10, "period_seconds": 30,
"url": "{{entire_url('accounting_errors.ui')}}" "url": "{{entire_url('stat_errors.ui')}}"
}
}
]
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"gap": "20px",
"height": "auto"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"width": "60%",
"bgcolor": "#1E293B",
"borderRadius": "12px",
"padding": "20px",
"border": "1px solid #334155"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "Top 3 模型(今日调用)",
"color": "#F1F5F9",
"fontWeight": "600"
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"label": "刷新",
"bgcolor": "#334155",
"color": "#94A3B8",
"border": "none",
"borderRadius": "6px",
"padding": "4px 12px",
"fontSize": "12px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "method",
"target": "-@RefreshWidget",
"method": "render_urldata",
"params": {}
}
]
}
]
},
{
"widgettype": "RefreshWidget",
"id": "chart_top_models",
"options": {
"period_seconds": 30,
"url": "{{entire_url('chart_top_models.ui')}}"
} }
} }
] ]
@ -69,30 +158,217 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#FFFFFF", "width": "40%",
"padding": "24px", "bgcolor": "#1E293B",
"borderRadius": "8px", "borderRadius": "12px",
"marginTop": "20px", "padding": "20px",
"minHeight": "350px", "border": "1px solid #334155"
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
}, },
"subwidgets": [ "subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "快捷入口",
"color": "#F1F5F9",
"fontWeight": "600",
"marginBottom": "16px"
}
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "12px",
"minWidth": "120px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/llmage/llm')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#90caf9\" stroke-width=\"2\"><path d=\"M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15.75c-2.062 0-4.024-.614-5.67-1.757l-1.57-.393m15.04 0L12 21 5.25 13.893\"/></svg>"
}
},
{ {
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "Top 3 模型(今日)", "text": "模型管理",
"fontSize": "18px", "color": "#E2E8F0",
"fontWeight": "bold", "fontSize": "13px",
"color": "#333", "marginTop": "8px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/rbac/users')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#4caf50\" stroke-width=\"2\"><path d=\"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z\"/></svg>"
}
},
{
"widgettype": "Text",
"options": {
"text": "用户管理",
"color": "#E2E8F0",
"fontSize": "13px",
"marginTop": "8px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/rag/kdb')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#f59e0b\" stroke-width=\"2\"><path d=\"M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125\"/></svg>"
}
},
{
"widgettype": "Text",
"options": {
"text": "知识库",
"color": "#E2E8F0",
"fontSize": "13px",
"marginTop": "8px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/llmage/failed_accounting.ui')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ef4444\" stroke-width=\"2\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></svg>"
}
},
{
"widgettype": "Text",
"options": {
"text": "异常记录",
"color": "#E2E8F0",
"fontSize": "13px",
"marginTop": "8px"
}
}
]
}
]
}
]
}
]
},
{
"widgettype": "VBox",
"options": {
"width": "100%",
"bgcolor": "#1E293B",
"borderRadius": "12px",
"padding": "20px",
"border": "1px solid #334155",
"marginTop": "20px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "用户消费排行Top 5",
"color": "#F1F5F9",
"fontWeight": "600",
"marginBottom": "16px" "marginBottom": "16px"
} }
}, },
{ {
"widgettype": "RefreshWidget", "widgettype": "RefreshWidget",
"id": "refresh_top_models_chart", "id": "table_top_users",
"options": { "options": {
"period_seconds": 10, "period_seconds": 30,
"url": "{{entire_url('top_models_chart.ui')}}" "url": "{{entire_url('table_top_users.ui')}}"
} }
} }
] ]

View File

@ -6,7 +6,7 @@
{ {
"name": "dashboard", "name": "dashboard",
"label": "数据看板", "label": "数据看板",
"url": "{{entire_url('index.ui')}}" "url": "{{entire_url('shell.ui')}}"
} }
] ]
} }

147
wwwroot/shell.ui Normal file
View File

@ -0,0 +1,147 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"bgcolor": "#0B1120"
},
"subwidgets": [
{
"widgettype": "Html",
"options": {
"html": "<link rel=\"stylesheet\" href=\"/dashboard_for_sage/shell_theme.css\"><script src=\"/dashboard_for_sage/shell_theme.js\"><\\/script>"
}
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"height": "56px",
"bgcolor": "#111827",
"borderBottom": "1px solid #334155",
"padding": "0 16px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Button",
"id": "sidebar_toggle_btn",
"options": {
"label": "",
"bgcolor": "transparent",
"color": "#94A3B8",
"border": "1px solid #334155",
"borderRadius": "8px",
"width": "36px",
"height": "36px",
"padding": "0"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "sageToggleSidebar()"
}
]
},
{
"widgettype": "Image",
"options": {
"url": "{{entire_url('/imgs/msp.png')}}",
"height": "32px",
"marginLeft": "12px"
}
},
{
"widgettype": "Title4",
"options": {
"text": "Sage",
"color": "#F1F5F9",
"fontWeight": "bold",
"marginLeft": "8px"
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"id": "theme_toggle_btn",
"options": {
"label": "",
"bgcolor": "transparent",
"color": "#94A3B8",
"border": "1px solid #334155",
"borderRadius": "50%",
"width": "36px",
"height": "36px",
"padding": "0"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "sageToggleTheme()"
}
]
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('/rbac/user/user_panel.ui')}}"
}
}
]
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"height": "calc(100% - 56px)"
},
"subwidgets": [
{
"widgettype": "VBox",
"id": "sage_sidebar",
"options": {
"width": "240px",
"height": "100%",
"bgcolor": "#111827",
"borderRight": "1px solid #334155"
},
"subwidgets": [
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('global_menu.ui')}}"
}
}
]
},
{
"widgettype": "VBox",
"id": "sage_main_content",
"options": {
"css": "filler",
"height": "100%",
"padding": "24px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "加载中...",
"fontSize": "16px",
"color": "#94A3B8"
}
}
]
}
]
}
]
}

393
wwwroot/shell_theme.css Normal file
View File

@ -0,0 +1,393 @@
/* Sage Modern UI Theme System
Dark/Light theme switching via CSS custom properties
Applied to <html> element data-theme attribute
*/
/* ===== Dark Theme (default) ===== */
:root,
[data-theme="dark"] {
--sage-bg-primary: #0B1120;
--sage-bg-secondary: #111827;
--sage-bg-card: #1E293B;
--sage-bg-hover: #334155;
--sage-bg-input: #0F172A;
--sage-bg-toolbar: #111827;
--sage-border-primary: #334155;
--sage-border-weak: #1E293B;
--sage-border-focus: #3B82F6;
--sage-text-primary: #F1F5F9;
--sage-text-secondary: #94A3B8;
--sage-text-muted: #64748B;
--sage-text-inverse: #0F172A;
--sage-brand: #3B82F6;
--sage-brand-hover: #2563EB;
--sage-success: #22C55E;
--sage-warning: #F59E0B;
--sage-danger: #EF4444;
--sage-info: #06B6D4;
--sage-sidebar-width: 240px;
--sage-sidebar-collapsed-width: 64px;
--sage-topbar-height: 56px;
--sage-radius: 12px;
--sage-radius-sm: 8px;
--sage-shadow-card: 0 1px 3px rgba(0,0,0,0.3);
--sage-shadow-lg: 0 4px 12px rgba(0,0,0,0.4);
}
/* ===== Light Theme ===== */
[data-theme="light"] {
--sage-bg-primary: #F8FAFC;
--sage-bg-secondary: #FFFFFF;
--sage-bg-card: #FFFFFF;
--sage-bg-hover: #F1F5F9;
--sage-bg-input: #F8FAFC;
--sage-bg-toolbar: #FFFFFF;
--sage-border-primary: #E2E8F0;
--sage-border-weak: #F1F5F9;
--sage-border-focus: #3B82F6;
--sage-text-primary: #0F172A;
--sage-text-secondary: #475569;
--sage-text-muted: #94A3B8;
--sage-text-inverse: #FFFFFF;
--sage-brand: #3B82F6;
--sage-brand-hover: #2563EB;
--sage-success: #16A34A;
--sage-warning: #D97706;
--sage-danger: #DC2626;
--sage-info: #0891B2;
--sage-shadow-card: 0 1px 3px rgba(0,0,0,0.08);
--sage-shadow-lg: 0 4px 12px rgba(0,0,0,0.1);
}
/* ===== Global Overrides ===== */
body {
margin: 0;
padding: 0;
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: background-color 0.2s ease, color 0.2s ease;
}
/* ===== Shell Layout ===== */
.sage-shell {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sage-topbar {
height: var(--sage-topbar-height);
background-color: var(--sage-bg-secondary);
border-bottom: 1px solid var(--sage-border-primary);
display: flex;
align-items: center;
padding: 0 16px;
gap: 12px;
flex-shrink: 0;
z-index: 100;
}
.sage-body {
display: flex;
flex: 1;
overflow: hidden;
}
.sage-sidebar {
width: var(--sage-sidebar-width);
background-color: var(--sage-bg-secondary);
border-right: 1px solid var(--sage-border-primary);
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
flex-shrink: 0;
transition: width 0.25s ease;
}
.sage-sidebar.collapsed {
width: var(--sage-sidebar-collapsed-width);
}
.sage-sidebar.collapsed .sidebar-text {
opacity: 0;
width: 0;
overflow: hidden;
}
.sage-main {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 24px;
background-color: var(--sage-bg-primary);
}
/* ===== Stat Cards ===== */
.stat-card {
background-color: var(--sage-bg-card);
border-radius: var(--sage-radius);
padding: 20px;
border: 1px solid var(--sage-border-primary);
box-shadow: var(--sage-shadow-card);
transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.2s ease;
cursor: pointer;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--sage-shadow-lg);
}
.stat-card .stat-icon {
width: 40px;
height: 40px;
border-radius: var(--sage-radius-sm);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.stat-card .stat-value {
font-size: 28px;
font-weight: 700;
color: var(--sage-text-primary);
line-height: 1.2;
}
.stat-card .stat-label {
font-size: 14px;
color: var(--sage-text-secondary);
margin-top: 4px;
}
.stat-card .stat-trend {
font-size: 13px;
margin-top: 8px;
}
.stat-card .stat-trend.up {
color: var(--sage-success);
}
.stat-card .stat-trend.down {
color: var(--sage-danger);
}
/* ===== Quick Links ===== */
.quick-link {
background-color: var(--sage-bg-card);
border-radius: var(--sage-radius);
padding: 20px;
border: 1px solid var(--sage-border-primary);
box-shadow: var(--sage-shadow-card);
transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.2s ease;
cursor: pointer;
text-align: center;
}
.quick-link:hover {
transform: translateY(-2px);
box-shadow: var(--sage-shadow-lg);
border-color: var(--sage-brand);
}
/* ===== Section Headers ===== */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--sage-text-primary);
}
/* ===== Buttons ===== */
.sage-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: var(--sage-radius-sm);
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.15s ease;
}
.sage-btn-primary {
background-color: var(--sage-brand);
color: #FFFFFF;
}
.sage-btn-primary:hover {
background-color: var(--sage-brand-hover);
}
.sage-btn-ghost {
background-color: transparent;
color: var(--sage-text-secondary);
border: 1px solid var(--sage-border-primary);
}
.sage-btn-ghost:hover {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
/* ===== Theme Toggle Button ===== */
.theme-toggle {
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid var(--sage-border-primary);
background-color: transparent;
color: var(--sage-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease, color 0.15s ease;
}
.theme-toggle:hover {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
/* ===== Sidebar Toggle ===== */
.sidebar-toggle {
width: 36px;
height: 36px;
border-radius: var(--sage-radius-sm);
border: 1px solid var(--sage-border-primary);
background-color: transparent;
color: var(--sage-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease;
}
.sidebar-toggle:hover {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
/* ===== Breadcrumb ===== */
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--sage-text-secondary);
}
.breadcrumb a {
color: var(--sage-text-secondary);
text-decoration: none;
}
.breadcrumb a:hover {
color: var(--sage-brand);
}
.breadcrumb .separator {
color: var(--sage-text-muted);
}
.breadcrumb .current {
color: var(--sage-text-primary);
font-weight: 500;
}
/* ===== DataViewer Overrides for theme ===== */
[data-theme="dark"] .dataviewer-toolbar,
[data-theme="dark"] .tabular,
[data-theme="dark"] .data-row {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="dark"] .tabular th {
background-color: var(--sage-bg-secondary);
color: var(--sage-text-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .tabular td {
border-color: var(--sage-border-weak);
}
[data-theme="light"] .dataviewer-toolbar,
[data-theme="light"] .tabular,
[data-theme="light"] .data-row {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular th {
background-color: var(--sage-bg-hover);
color: var(--sage-text-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .tabular td {
border-color: var(--sage-border-weak);
}
/* ===== Menu Overrides ===== */
[data-theme="dark"] .menu-item {
color: var(--sage-text-secondary);
}
[data-theme="dark"] .menu-item:hover,
[data-theme="dark"] .menu-item.active {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
[data-theme="light"] .menu-item {
color: var(--sage-text-secondary);
}
[data-theme="light"] .menu-item:hover,
[data-theme="light"] .menu-item.active {
background-color: var(--sage-bg-hover);
color: var(--sage-brand);
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.sage-sidebar {
position: fixed;
left: 0;
top: var(--sage-topbar-height);
bottom: 0;
z-index: 200;
transform: translateX(-100%);
transition: transform 0.25s ease;
}
.sage-sidebar.mobile-open {
transform: translateX(0);
}
}

87
wwwroot/shell_theme.js Normal file
View File

@ -0,0 +1,87 @@
/* Sage Modern UI Shell Theme & Layout Controller
Handles: theme switching (dark/light), sidebar collapse, localStorage persistence
*/
(function() {
'use strict';
var THEME_KEY = 'sage_ui_theme';
var SIDEBAR_KEY = 'sage_sidebar_collapsed';
// Initialize theme on page load
function initTheme() {
var saved = null;
try { saved = localStorage.getItem(THEME_KEY); } catch(e) {}
var theme = saved || 'dark';
document.documentElement.setAttribute('data-theme', theme);
updateThemeIcon(theme);
}
// Toggle between dark and light
function toggleTheme() {
var current = document.documentElement.getAttribute('data-theme') || 'dark';
var next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem(THEME_KEY, next); } catch(e) {}
updateThemeIcon(next);
}
// Update the theme toggle icon based on current theme
function updateThemeIcon(theme) {
var btn = document.getElementById('theme_toggle_btn');
if (!btn) return;
if (theme === 'light') {
// Show moon icon (switch to dark)
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
} else {
// Show sun icon (switch to light)
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
}
}
// Initialize sidebar state
function initSidebar() {
var collapsed = false;
try { collapsed = localStorage.getItem(SIDEBAR_KEY) === 'true'; } catch(e) {}
var sidebar = document.getElementById('sage_sidebar');
if (sidebar && collapsed) {
sidebar.classList.add('collapsed');
}
}
// Toggle sidebar collapse
function toggleSidebar() {
var sidebar = document.getElementById('sage_sidebar');
if (!sidebar) return;
sidebar.classList.toggle('collapsed');
var isCollapsed = sidebar.classList.contains('collapsed');
try { localStorage.setItem(SIDEBAR_KEY, isCollapsed); } catch(e) {}
// Update toggle icon
var btn = document.getElementById('sidebar_toggle_btn');
if (btn) {
if (isCollapsed) {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>';
} else {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>';
}
}
}
// Run on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initTheme();
initSidebar();
});
} else {
initTheme();
initSidebar();
}
// Expose global functions for bricks bind access
window.sageToggleTheme = toggleTheme;
window.sageToggleSidebar = toggleSidebar;
window.sageInitTheme = initTheme;
window.sageInitSidebar = initSidebar;
})();

View File

@ -0,0 +1,52 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#06B6D4\" stroke-width=\"2\"><path d=\"M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "{{get_concurrent_users(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "在线用户",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

52
wwwroot/stat_errors.ui Normal file
View File

@ -0,0 +1,52 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#EF4444\" stroke-width=\"2\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "{{get_accounting_errors(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#EF4444",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "记账异常",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

View File

@ -0,0 +1,52 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{get_today_amount(request)|round(2)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "今日消费金额",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

View File

@ -0,0 +1,53 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px",
"cursor": "pointer"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\"><path d=\"M7.5 21L3 16.5m0 0L7.5 12M12 9v7.5m0 0l4.5-4.5M12 9l4.5 4.5m0 0L12 16.5\"/><path d=\"M21 12h-4.5M12 3v4.5m0 0L7.5 12\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "{{get_today_usage(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "今日调用笔数",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

View File

@ -0,0 +1,52 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#A78BFA\" stroke-width=\"2\"><path d=\"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "{{get_total_users(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "用户总数",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

View File

@ -0,0 +1,16 @@
{
"widgettype": "VBox",
"options": {
"width": "100%"
},
"subwidgets": [
{
"widgettype": "RefreshWidget",
"id": "table_top_users_amount",
"options": {
"period_seconds": 30,
"url": "{{entire_url('table_top_users_amount.ui')}}"
}
}
]
}

View File

@ -0,0 +1,77 @@
{% set users = get_top_users_by_amount(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%"
},
"subwidgets": [
{% for u in users %}
{
"widgettype": "HBox",
"options": {
"width": "100%",
"padding": "12px 0",
{% if not loop.first %}
"borderTop": "1px solid #334155",
{% endif %}
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{loop.index}}",
"width": "30px",
"color": "#64748B",
"fontSize": "14px",
"textAlign": "center"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{u.user_name}}",
"flex": "1",
"color": "#F1F5F9",
"fontSize": "14px",
"fontWeight": "500"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{u.cnt}} 笔",
"width": "80px",
"color": "#94A3B8",
"fontSize": "13px",
"textAlign": "right"
}
},
{
"widgettype": "Text",
"options": {
"text": "¥{{u.total_amount}}",
"width": "100px",
"color": "#22C55E",
"fontSize": "14px",
"fontWeight": "600",
"textAlign": "right"
}
}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% if not users %}
{
"widgettype": "Text",
"options": {
"text": "暂无数据",
"color": "#64748B",
"fontSize": "14px",
"textAlign": "center",
"padding": "20px 0"
}
}
{% endif %}
]
}

View File

@ -0,0 +1,57 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"minHeight": "280px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "用户金额 TOP 5今日",
"fontSize": "16px",
"fontWeight": "bold",
"color": "#333",
"marginBottom": "12px"
}
},
{% set rows = get_top_users_by_amount(request) %}
{% if rows %}
{
"widgettype": "VBox",
"options": {"spacing": "8px"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"bgcolor": "#f5f5f5", "padding": "8px 12px", "borderRadius": "4px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "排名", "fontSize": "12px", "color": "#888", "width": "50px"}},
{"widgettype": "Text", "options": {"text": "用户", "fontSize": "12px", "color": "#888", "flex": "1"}},
{"widgettype": "Text", "options": {"text": "金额", "fontSize": "12px", "color": "#888", "width": "100px", "textAlign": "right"}},
{"widgettype": "Text", "options": {"text": "笔数", "fontSize": "12px", "color": "#888", "width": "60px", "textAlign": "right"}}
]
},
{% for item in rows %}
{
"widgettype": "HBox",
"options": {"padding": "8px 12px", "borderRadius": "4px", "bgcolor": "{% if loop.index == 1 %}#fff3e0{% elif loop.index == 2 %}#fff8e1{% elif loop.index == 3 %}#fffde7{% else %}#fafafa{% endif %}"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "{{loop.index}}", "fontSize": "13px", "color": "{% if loop.index <= 3 %}#e65100{% else %}#666{% endif %}", "width": "50px", "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "{{item.user_name}}", "fontSize": "13px", "color": "#333", "flex": "1"}},
{"widgettype": "Text", "options": {"text": "{{item.total_amount|round(2)}}", "fontSize": "13px", "color": "#2e7d32", "width": "100px", "textAlign": "right", "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "{{item.cnt}}", "fontSize": "13px", "color": "#666", "width": "60px", "textAlign": "right"}}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
{% else %}
{
"widgettype": "Text",
"options": {"text": "暂无数据", "fontSize": "14px", "color": "#999", "textAlign": "center"}
}
{% endif %}
]
}