refactor: use RefreshWidget for stat cards + fix .dspy import violations
Architecture: - index.ui: title + RefreshWidget(cards) + ChartBar with refresh_period - RefreshWidget wraps dashboard_cards.dspy → returns full card widget tree with live data (cnt, amount, total_users, concurrent_users) - ChartBar handles its own auto-refresh via refresh_period: 10 - No more JS polling file needed .dspy import fixes: - get_today_usage.dspy: remove import json, from datetime import date - get_user_stats.dspy: remove from datetime import datetime, timedelta - get_top_models.dspy: remove from datetime import date - All use pre-loaded datetime module (datetime.date.today(), etc.) - dashboard_cards.dspy: same pattern, no imports Permission: - load_path.py: add dashboard_cards.dspy logined
This commit is contained in:
parent
99e6fed5ef
commit
d2210a2996
73
wwwroot/api/dashboard_cards.dspy
Normal file
73
wwwroot/api/dashboard_cards.dspy
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""获取当天llmusage笔数和交易金额 + 用户统计 — 返回完整卡片widget树供RefreshWidget加载"""
|
||||||
|
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||||
|
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
five_min_ago = (now - datetime.timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
|
||||||
|
# 今日用量
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$",
|
||||||
|
{'today': today}
|
||||||
|
)
|
||||||
|
cnt = int(recs[0].get('cnt', 0)) if recs else 0
|
||||||
|
total_amount = float(recs[0].get('total_amount', 0)) if recs else 0.0
|
||||||
|
|
||||||
|
# 总用户数
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
user_recs = await sor.sqlExe("SELECT COUNT(*) as total_users FROM users", {})
|
||||||
|
total_users = int(user_recs[0].get('total_users', 0)) if user_recs else 0
|
||||||
|
|
||||||
|
# 并发用户
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
conc = await sor.sqlExe(
|
||||||
|
"SELECT COUNT(DISTINCT userid) as concurrent_users FROM llmusage WHERE use_date = ${today}$ AND use_time >= ${five_min_ago}$",
|
||||||
|
{'today': today, 'five_min_ago': five_min_ago}
|
||||||
|
)
|
||||||
|
concurrent_users = int(conc[0].get('concurrent_users', 0)) if conc else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widgettype": "ResponsableBox",
|
||||||
|
"options": {"gap": "16px", "minWidth": "250px"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "card_today_cnt",
|
||||||
|
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "今日调用笔数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||||||
|
{"widgettype": "Text", "id": "today_cnt_value", "options": {"text": str(cnt), "fontSize": "32px", "fontWeight": "bold", "color": "#1890ff"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "card_today_amount",
|
||||||
|
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "今日交易金额", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||||||
|
{"widgettype": "Text", "id": "today_amount_value", "options": {"text": "\u00a5%.2f" % total_amount, "fontSize": "32px", "fontWeight": "bold", "color": "#52c41a"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "card_total_users",
|
||||||
|
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "用户总数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||||||
|
{"widgettype": "Text", "id": "total_users_value", "options": {"text": str(total_users), "fontSize": "32px", "fontWeight": "bold", "color": "#722ed1"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "card_concurrent_users",
|
||||||
|
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "当前并发用户", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||||||
|
{"widgettype": "Text", "id": "concurrent_users_value", "options": {"text": str(concurrent_users), "fontSize": "32px", "fontWeight": "bold", "color": "#fa8c16"}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,100 +0,0 @@
|
|||||||
"""返回 dashboard 动态 UI 描述(含实时数据)"""
|
|
||||||
from datetime import date, datetime, timedelta
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
today = date.today().isoformat()
|
|
||||||
now = datetime.now()
|
|
||||||
five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
# 今日用量
|
|
||||||
async with db.sqlorContext('sage') as sor:
|
|
||||||
usage = await sor.sqlExe(
|
|
||||||
"SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$",
|
|
||||||
{'today': today}
|
|
||||||
)
|
|
||||||
cnt = int(usage[0]['cnt']) if usage else 0
|
|
||||||
total_amount = float(usage[0]['total_amount']) if usage else 0.0
|
|
||||||
|
|
||||||
# 总用户数
|
|
||||||
async with db.sqlorContext('sage') as sor:
|
|
||||||
users = await sor.sqlExe("SELECT COUNT(*) as total_users FROM users", {})
|
|
||||||
total_users = int(users[0]['total_users']) if users else 0
|
|
||||||
|
|
||||||
# 并发用户
|
|
||||||
async with db.sqlorContext('sage') as sor:
|
|
||||||
conc = await sor.sqlExe(
|
|
||||||
"SELECT COUNT(DISTINCT userid) as concurrent_users FROM llmusage WHERE use_date = ${today}$ AND use_time >= ${five_min_ago}$",
|
|
||||||
{'today': today, 'five_min_ago': five_min_ago}
|
|
||||||
)
|
|
||||||
concurrent_users = int(conc[0]['concurrent_users']) if conc else 0
|
|
||||||
|
|
||||||
# ChartBar data_url — 用相对路径,bricks 会基于当前上下文解析
|
|
||||||
chart_data_url = 'api/get_top_models.dspy'
|
|
||||||
|
|
||||||
return {
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {"width": "100%"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "ResponsableBox",
|
|
||||||
"options": {"gap": "16px", "minWidth": "250px"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"id": "card_today_cnt",
|
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
|
||||||
"subwidgets": [
|
|
||||||
{"widgettype": "Text", "options": {"text": "今日调用笔数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
|
||||||
{"widgettype": "Text", "id": "today_cnt_value", "options": {"text": str(cnt), "fontSize": "32px", "fontWeight": "bold", "color": "#1890ff"}}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"id": "card_today_amount",
|
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
|
||||||
"subwidgets": [
|
|
||||||
{"widgettype": "Text", "options": {"text": "今日交易金额", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
|
||||||
{"widgettype": "Text", "id": "today_amount_value", "options": {"text": f"\u00a5{total_amount:.2f}", "fontSize": "32px", "fontWeight": "bold", "color": "#52c41a"}}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"id": "card_total_users",
|
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
|
||||||
"subwidgets": [
|
|
||||||
{"widgettype": "Text", "options": {"text": "用户总数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
|
||||||
{"widgettype": "Text", "id": "total_users_value", "options": {"text": str(total_users), "fontSize": "32px", "fontWeight": "bold", "color": "#722ed1"}}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"id": "card_concurrent_users",
|
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
|
||||||
"subwidgets": [
|
|
||||||
{"widgettype": "Text", "options": {"text": "当前并发用户", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
|
||||||
{"widgettype": "Text", "id": "concurrent_users_value", "options": {"text": str(concurrent_users), "fontSize": "32px", "fontWeight": "bold", "color": "#fa8c16"}}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"id": "chart_section",
|
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "marginTop": "20px", "minHeight": "350px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
|
||||||
"subwidgets": [
|
|
||||||
{"widgettype": "Text", "options": {"text": "Top 3 模型(今日)", "fontSize": "18px", "fontWeight": "bold", "color": "#333", "marginBottom": "16px"}},
|
|
||||||
{
|
|
||||||
"widgettype": "ChartBar",
|
|
||||||
"id": "top_models_chart",
|
|
||||||
"options": {
|
|
||||||
"height": "300px",
|
|
||||||
"width": "100%",
|
|
||||||
"data_url": chart_data_url,
|
|
||||||
"nameField": "model_name",
|
|
||||||
"valueFields": ["cnt", "total_amount"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
"""获取当天llmusage笔数和交易金额"""
|
"""获取当天llmusage笔数和交易金额"""
|
||||||
import json
|
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
ns = params_kw.copy()
|
today = datetime.date.today().isoformat()
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
@ -12,7 +11,6 @@ FROM llmusage
|
|||||||
WHERE use_date = ${today}$
|
WHERE use_date = ${today}$
|
||||||
"""
|
"""
|
||||||
|
|
||||||
today = date.today().isoformat()
|
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
async with db.sqlorContext('sage') as sor:
|
async with db.sqlorContext('sage') as sor:
|
||||||
recs = await sor.sqlExe(sql, {'today': today})
|
recs = await sor.sqlExe(sql, {'today': today})
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
"""获取当天排名前三的模型数量和金额"""
|
"""获取当天排名前三的模型数量和金额"""
|
||||||
from datetime import date
|
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||||
|
|
||||||
ns = params_kw.copy()
|
today = datetime.date.today().isoformat()
|
||||||
today = date.today().isoformat()
|
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
"""获取当前用户总数和并发用户数(近5分钟有活跃记录的用户)"""
|
"""获取当前用户总数和并发用户数(近5分钟有活跃记录的用户)"""
|
||||||
from datetime import datetime, timedelta
|
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||||
|
|
||||||
ns = params_kw.copy()
|
now = datetime.datetime.now()
|
||||||
db = DBPools()
|
five_min_ago = (now - datetime.timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
# 总用户数
|
|
||||||
sql_users = "SELECT COUNT(*) as total_users FROM users"
|
|
||||||
|
|
||||||
# 并发用户数:近5分钟内在llmusage中有记录的不同用户
|
|
||||||
now = datetime.now()
|
|
||||||
five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
today = now.strftime('%Y-%m-%d')
|
today = now.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
sql_users = "SELECT COUNT(*) as total_users FROM users"
|
||||||
|
|
||||||
sql_concurrent = """
|
sql_concurrent = """
|
||||||
SELECT COUNT(DISTINCT userid) as concurrent_users
|
SELECT COUNT(DISTINCT userid) as concurrent_users
|
||||||
FROM llmusage
|
FROM llmusage
|
||||||
@ -19,6 +14,7 @@ WHERE use_date = ${today}$
|
|||||||
AND use_time >= ${five_min_ago}$
|
AND use_time >= ${five_min_ago}$
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
async with db.sqlorContext('sage') as sor:
|
async with db.sqlorContext('sage') as sor:
|
||||||
user_recs = await sor.sqlExe(sql_users, {})
|
user_recs = await sor.sqlExe(sql_users, {})
|
||||||
total_users = int(user_recs[0].get('total_users', 0)) if user_recs else 0
|
total_users = int(user_recs[0].get('total_users', 0)) if user_recs else 0
|
||||||
|
|||||||
@ -19,11 +19,47 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"widgettype": "RefreshWidget",
|
"widgettype": "RefreshWidget",
|
||||||
"id": "dashboard_refresh",
|
"id": "cards_refresh",
|
||||||
"options": {
|
"options": {
|
||||||
"period_seconds": 10,
|
"period_seconds": 10,
|
||||||
"url": "{{entire_url('api/dashboard_content.dspy')}}"
|
"url": "{{entire_url('api/dashboard_cards.dspy')}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "chart_section",
|
||||||
|
"options": {
|
||||||
|
"bgcolor": "#FFFFFF",
|
||||||
|
"padding": "24px",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"marginTop": "20px",
|
||||||
|
"minHeight": "350px",
|
||||||
|
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "Top 3 模型(今日)",
|
||||||
|
"fontSize": "18px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"color": "#333",
|
||||||
|
"marginBottom": "16px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "ChartBar",
|
||||||
|
"id": "top_models_chart",
|
||||||
|
"options": {
|
||||||
|
"height": "300px",
|
||||||
|
"width": "100%",
|
||||||
|
"data_url": "{{entire_url('api/get_top_models.dspy')}}",
|
||||||
|
"nameField": "model_name",
|
||||||
|
"valueFields": ["cnt", "total_amount"],
|
||||||
|
"refresh_period": 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user