From 9cc59160e8affc0b40575fc7e692311c0d0ea505 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 1 Jun 2026 15:49:46 +0800 Subject: [PATCH] feat: add trend indicators and color accents to stat cards - Add get_usage_trend() and get_amount_trend() functions for day-over-day comparison - Display trend arrows (up/down/flat) with percentage change in stat_today_usage and stat_today_amount - Add colored left borders to all stat cards for visual distinction: - Blue: usage, total users - Purple: amount, total orgs - Green: active users, new users this month - Cyan: concurrent users - Red: errors - Enhance visual hierarchy with accent colors matching SVG icons --- dashboard_for_sage/load_dashboard.py | 66 ++++++++++++++++++++++++++++ wwwroot/stat_active_users.ui | 3 +- wwwroot/stat_concurrent.ui | 3 +- wwwroot/stat_errors.ui | 3 +- wwwroot/stat_new_users_month.ui | 4 +- wwwroot/stat_today_amount.ui | 60 ++++++++++++++++++++----- wwwroot/stat_today_usage.ui | 60 ++++++++++++++++++++----- wwwroot/stat_total_orgs.ui | 4 +- wwwroot/stat_total_users.ui | 6 ++- 9 files changed, 182 insertions(+), 27 deletions(-) diff --git a/dashboard_for_sage/load_dashboard.py b/dashboard_for_sage/load_dashboard.py index bef6f17..457564a 100644 --- a/dashboard_for_sage/load_dashboard.py +++ b/dashboard_for_sage/load_dashboard.py @@ -26,6 +26,70 @@ async def get_today_amount(request): return amount +async def get_usage_trend(request): + """计算今日调用量趋势(与昨日对比)""" + env = request._run_ns + today = env.curDateString() + yesterday = (date.today() - timedelta(days=1)).isoformat() + async with get_sor_context(env, 'sage') as sor: + # 今日 + sql_today = "SELECT COUNT(*) as cnt FROM llmusage WHERE use_date = ${today}$" + recs_today = await sor.sqlExe(sql_today, {'today': today}) + today_cnt = int(recs_today[0].get('cnt', 0)) if recs_today else 0 + + # 昨日 + sql_yesterday = "SELECT COUNT(*) as cnt FROM llmusage WHERE use_date = ${yesterday}$" + recs_yesterday = await sor.sqlExe(sql_yesterday, {'yesterday': yesterday}) + yesterday_cnt = int(recs_yesterday[0].get('cnt', 0)) if recs_yesterday else 0 + + # 计算趋势 + if yesterday_cnt == 0: + return {'trend': 'flat', 'percentage': 0, 'value': today_cnt} + + change_pct = ((today_cnt - yesterday_cnt) / yesterday_cnt) * 100 + + if change_pct > 5: + trend = 'up' + elif change_pct < -5: + trend = 'down' + else: + trend = 'flat' + + return {'trend': trend, 'percentage': abs(change_pct), 'value': today_cnt} + + +async def get_amount_trend(request): + """计算今日金额趋势(与昨日对比)""" + env = request._run_ns + today = env.curDateString() + yesterday = (date.today() - timedelta(days=1)).isoformat() + async with get_sor_context(env, 'sage') as sor: + # 今日 + sql_today = "SELECT COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$" + recs_today = await sor.sqlExe(sql_today, {'today': today}) + today_amount = float(recs_today[0].get('total_amount', 0)) if recs_today else 0.0 + + # 昨日 + sql_yesterday = "SELECT COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${yesterday}$" + recs_yesterday = await sor.sqlExe(sql_yesterday, {'yesterday': yesterday}) + yesterday_amount = float(recs_yesterday[0].get('total_amount', 0)) if recs_yesterday else 0.0 + + # 计算趋势 + if yesterday_amount == 0: + return {'trend': 'flat', 'percentage': 0, 'value': today_amount} + + change_pct = ((today_amount - yesterday_amount) / yesterday_amount) * 100 + + if change_pct > 5: + trend = 'up' + elif change_pct < -5: + trend = 'down' + else: + trend = 'flat' + + return {'trend': trend, 'percentage': abs(change_pct), 'value': today_amount} + + async def get_total_users(request): """获取用户总数""" env = request._run_ns @@ -526,6 +590,8 @@ def load_dashboard(): g = ServerEnv() g.get_today_usage = get_today_usage g.get_today_amount = get_today_amount + g.get_usage_trend = get_usage_trend + g.get_amount_trend = get_amount_trend g.get_total_users = get_total_users g.get_concurrent_users = get_concurrent_users g.get_top_models = get_top_models diff --git a/wwwroot/stat_active_users.ui b/wwwroot/stat_active_users.ui index 76c276f..fbfe264 100644 --- a/wwwroot/stat_active_users.ui +++ b/wwwroot/stat_active_users.ui @@ -5,7 +5,8 @@ "padding": "20px", "borderRadius": "12px", "flex": "1", - "minHeight": "110px" + "minHeight": "110px", + "borderLeft": "4px solid #22c55e" }, "subwidgets": [ { diff --git a/wwwroot/stat_concurrent.ui b/wwwroot/stat_concurrent.ui index dbe99e7..0ecfdeb 100644 --- a/wwwroot/stat_concurrent.ui +++ b/wwwroot/stat_concurrent.ui @@ -4,7 +4,8 @@ "padding": "20px", "borderRadius": "12px", "flex": "1", - "minHeight": "110px" + "minHeight": "110px", + "borderLeft": "4px solid #06b6d4" }, "subwidgets": [ { diff --git a/wwwroot/stat_errors.ui b/wwwroot/stat_errors.ui index 1dcb2ac..132a25c 100644 --- a/wwwroot/stat_errors.ui +++ b/wwwroot/stat_errors.ui @@ -5,7 +5,8 @@ "padding": "20px", "borderRadius": "12px", "flex": "1", - "minHeight": "110px" + "minHeight": "110px", + "borderLeft": "4px solid #ef4444" }, "subwidgets": [ { diff --git a/wwwroot/stat_new_users_month.ui b/wwwroot/stat_new_users_month.ui index 57390fc..20fee02 100644 --- a/wwwroot/stat_new_users_month.ui +++ b/wwwroot/stat_new_users_month.ui @@ -1,10 +1,12 @@ { "widgettype": "VBox", "options": { + "css": "stat-card", "padding": "20px", "borderRadius": "12px", "flex": "1", - "minHeight": "110px" + "minHeight": "110px", + "borderLeft": "4px solid #10b981" }, "subwidgets": [ { diff --git a/wwwroot/stat_today_amount.ui b/wwwroot/stat_today_amount.ui index 8eaa019..0e1ed18 100644 --- a/wwwroot/stat_today_amount.ui +++ b/wwwroot/stat_today_amount.ui @@ -1,3 +1,4 @@ +{% set trend = get_amount_trend(request) %} { "widgettype": "VBox", "options": { @@ -5,7 +6,8 @@ "padding": "20px", "borderRadius": "12px", "flex": "1", - "minHeight": "110px" + "minHeight": "110px", + "borderLeft": "4px solid {% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#8b5cf6{% endif %}" }, "subwidgets": [ { @@ -20,7 +22,8 @@ "options": { "svg": "", "width": "24px", - "height": "24px" + "height": "24px", + "color": "#8b5cf6" } }, { @@ -32,21 +35,58 @@ "widgettype": "Text", "options": { "css": "stat-value", - "text": "¥{{get_today_amount(request)|round(2)}}", + "text": "¥{{trend.value|round(2)}}", "fontSize": "32px", "fontWeight": "700", "lineHeight": "1.1" } }, { - "widgettype": "Text", + "widgettype": "HBox", "options": { - "css": "stat-label", - "fontSize": "14px", - "marginTop": "4px", - "otext": "今日交易金额", - "i18n": true - } + "alignItems": "center", + "marginTop": "4px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "css": "stat-label", + "fontSize": "14px", + "otext": "今日交易金额", + "i18n": true + } + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "gap": "4px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "{% if trend.trend == 'up' %}{% elif trend.trend == 'down' %}{% else %}{% endif %}", + "width": "16px", + "height": "16px" + } + }, + { + "widgettype": "Text", + "options": { + "text": "{{trend.percentage|round(1)}}%", + "fontSize": "12px", + "fontWeight": "600", + "color": "{% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#94a3b8{% endif %}" + } + } + ] + } + ] } ] } diff --git a/wwwroot/stat_today_usage.ui b/wwwroot/stat_today_usage.ui index 2b99454..dc9e7fe 100644 --- a/wwwroot/stat_today_usage.ui +++ b/wwwroot/stat_today_usage.ui @@ -1,3 +1,4 @@ +{% set trend = get_usage_trend(request) %} { "widgettype": "VBox", "options": { @@ -6,7 +7,8 @@ "borderRadius": "12px", "flex": "1", "minHeight": "110px", - "cursor": "pointer" + "cursor": "pointer", + "borderLeft": "4px solid {% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#3b82f6{% endif %}" }, "subwidgets": [ { @@ -21,7 +23,8 @@ "options": { "svg": "", "width": "24px", - "height": "24px" + "height": "24px", + "color": "#3b82f6" } }, { @@ -33,21 +36,58 @@ "widgettype": "Text", "options": { "css": "stat-value", - "text": "{{get_today_usage(request)}}", + "text": "{{trend.value}}", "fontSize": "32px", "fontWeight": "700", "lineHeight": "1.1" } }, { - "widgettype": "Text", + "widgettype": "HBox", "options": { - "css": "stat-label", - "fontSize": "14px", - "marginTop": "4px", - "otext": "今日调用笔数", - "i18n": true - } + "alignItems": "center", + "marginTop": "4px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "css": "stat-label", + "fontSize": "14px", + "otext": "今日调用笔数", + "i18n": true + } + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "gap": "4px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "{% if trend.trend == 'up' %}{% elif trend.trend == 'down' %}{% else %}{% endif %}", + "width": "16px", + "height": "16px" + } + }, + { + "widgettype": "Text", + "options": { + "text": "{{trend.percentage|round(1)}}%", + "fontSize": "12px", + "fontWeight": "600", + "color": "{% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#94a3b8{% endif %}" + } + } + ] + } + ] } ] } \ No newline at end of file diff --git a/wwwroot/stat_total_orgs.ui b/wwwroot/stat_total_orgs.ui index f074316..c01b569 100644 --- a/wwwroot/stat_total_orgs.ui +++ b/wwwroot/stat_total_orgs.ui @@ -1,10 +1,12 @@ { "widgettype": "VBox", "options": { + "css": "stat-card", "padding": "20px", "borderRadius": "12px", "flex": "1", - "minHeight": "110px" + "minHeight": "110px", + "borderLeft": "4px solid #8b5cf6" }, "subwidgets": [ { diff --git a/wwwroot/stat_total_users.ui b/wwwroot/stat_total_users.ui index 6b7dcc4..2b7f824 100644 --- a/wwwroot/stat_total_users.ui +++ b/wwwroot/stat_total_users.ui @@ -5,7 +5,8 @@ "padding": "20px", "borderRadius": "12px", "flex": "1", - "minHeight": "110px" + "minHeight": "110px", + "borderLeft": "4px solid #3b82f6" }, "subwidgets": [ { @@ -20,7 +21,8 @@ "options": { "svg": "", "width": "24px", - "height": "24px" + "height": "24px", + "color": "#3b82f6" } }, {