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
This commit is contained in:
parent
855f376671
commit
9cc59160e8
@ -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
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
"padding": "20px",
|
||||
"borderRadius": "12px",
|
||||
"flex": "1",
|
||||
"minHeight": "110px"
|
||||
"minHeight": "110px",
|
||||
"borderLeft": "4px solid #22c55e"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
"padding": "20px",
|
||||
"borderRadius": "12px",
|
||||
"flex": "1",
|
||||
"minHeight": "110px"
|
||||
"minHeight": "110px",
|
||||
"borderLeft": "4px solid #06b6d4"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
"padding": "20px",
|
||||
"borderRadius": "12px",
|
||||
"flex": "1",
|
||||
"minHeight": "110px"
|
||||
"minHeight": "110px",
|
||||
"borderLeft": "4px solid #ef4444"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
|
||||
@ -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": [
|
||||
{
|
||||
|
||||
@ -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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M12 6v12m6-6H6\"/></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": "HBox",
|
||||
"options": {
|
||||
"alignItems": "center",
|
||||
"marginTop": "4px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"css": "stat-label",
|
||||
"fontSize": "14px",
|
||||
"marginTop": "4px",
|
||||
"otext": "今日交易金额",
|
||||
"i18n": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Filler"
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"alignItems": "center",
|
||||
"gap": "4px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Svg",
|
||||
"options": {
|
||||
"svg": "{% if trend.trend == 'up' %}<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22c55e\" stroke-width=\"2\"><polyline points=\"18 15 12 9 6 15\"></polyline></svg>{% elif trend.trend == 'down' %}<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ef4444\" stroke-width=\"2\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>{% else %}<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#94a3b8\" stroke-width=\"2\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line></svg>{% 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 %}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" 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"
|
||||
"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": "HBox",
|
||||
"options": {
|
||||
"alignItems": "center",
|
||||
"marginTop": "4px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"css": "stat-label",
|
||||
"fontSize": "14px",
|
||||
"marginTop": "4px",
|
||||
"otext": "今日调用笔数",
|
||||
"i18n": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Filler"
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"alignItems": "center",
|
||||
"gap": "4px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Svg",
|
||||
"options": {
|
||||
"svg": "{% if trend.trend == 'up' %}<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22c55e\" stroke-width=\"2\"><polyline points=\"18 15 12 9 6 15\"></polyline></svg>{% elif trend.trend == 'down' %}<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ef4444\" stroke-width=\"2\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>{% else %}<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#94a3b8\" stroke-width=\"2\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line></svg>{% 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 %}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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": [
|
||||
{
|
||||
|
||||
@ -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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" 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"
|
||||
"height": "24px",
|
||||
"color": "#3b82f6"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user