From be1ac95ac78cc5cf0b04b388224b851dc42b53f2 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 25 May 2026 18:49:25 +0800 Subject: [PATCH] feat: add user statistics cards to dashboard - Add get_active_users_today(), get_new_users_month(), get_total_orgs() to load_dashboard.py - Create stat_active_users.ui, stat_new_users_month.ui, stat_total_orgs.ui widgets - Add active users card to main stats row - Add new row with new users this month and total organizations cards --- dashboard_for_sage/load_dashboard.py | 41 ++++++++++++++++++++++ wwwroot/index.ui | 34 ++++++++++++++++++ wwwroot/stat_active_users.ui | 52 ++++++++++++++++++++++++++++ wwwroot/stat_new_users_month.ui | 52 ++++++++++++++++++++++++++++ wwwroot/stat_total_orgs.ui | 52 ++++++++++++++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 wwwroot/stat_active_users.ui create mode 100644 wwwroot/stat_new_users_month.ui create mode 100644 wwwroot/stat_total_orgs.ui diff --git a/dashboard_for_sage/load_dashboard.py b/dashboard_for_sage/load_dashboard.py index 9400bee..b60e74a 100644 --- a/dashboard_for_sage/load_dashboard.py +++ b/dashboard_for_sage/load_dashboard.py @@ -208,6 +208,44 @@ async def get_top_providers_by_count(request): return result +async def get_active_users_today(request): + """获取今日活跃用户数(今日有llmusage记录的去重用户)""" + env = request._run_ns + today = env.curDateString() + tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT COUNT(DISTINCT userid) as cnt FROM llmusage + WHERE use_date >= ${today}$ AND use_date < ${tomorrow}$ + """ + recs = await sor.sqlExe(sql, {'today': today, 'tomorrow': tomorrow}) + cnt = int(recs[0].get('cnt', 0)) if recs else 0 + return cnt + + +async def get_new_users_month(request): + """获取本月新增用户数""" + env = request._run_ns + month_start = datetime.now().strftime('%Y-%m-01') + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT COUNT(*) as cnt FROM users WHERE created_date >= ${month_start}$ + """ + recs = await sor.sqlExe(sql, {'month_start': month_start}) + cnt = int(recs[0].get('cnt', 0)) if recs else 0 + return cnt + + +async def get_total_orgs(request): + """获取组织机构总数""" + env = request._run_ns + async with get_sor_context(env, 'sage') as sor: + sql = "SELECT COUNT(*) as cnt FROM organization" + recs = await sor.sqlExe(sql, {}) + cnt = int(recs[0].get('cnt', 0)) if recs else 0 + return cnt + + def load_dashboard(): """Register dashboard functions on ServerEnv""" g = ServerEnv() @@ -221,3 +259,6 @@ def load_dashboard(): g.get_top_users_by_count = get_top_users_by_count g.get_top_providers_by_amount = get_top_providers_by_amount g.get_top_providers_by_count = get_top_providers_by_count + g.get_active_users_today = get_active_users_today + g.get_new_users_month = get_new_users_month + g.get_total_orgs = get_total_orgs diff --git a/wwwroot/index.ui b/wwwroot/index.ui index b19f2af..24fb68c 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -66,6 +66,14 @@ "url": "{{entire_url('stat_total_users.ui')}}" } }, + { + "widgettype": "RefreshWidget", + "id": "stat_active_users", + "options": { + "period_seconds": 60, + "url": "{{entire_url('stat_active_users.ui')}}" + } + }, { "widgettype": "RefreshWidget", "id": "stat_concurrent", @@ -84,6 +92,32 @@ } ] }, + { + "widgettype": "ResponsableBox", + "options": { + "gap": "16px", + "minWidth": "220px", + "marginBottom": "24px" + }, + "subwidgets": [ + { + "widgettype": "RefreshWidget", + "id": "stat_new_users_month", + "options": { + "period_seconds": 120, + "url": "{{entire_url('stat_new_users_month.ui')}}" + } + }, + { + "widgettype": "RefreshWidget", + "id": "stat_total_orgs", + "options": { + "period_seconds": 120, + "url": "{{entire_url('stat_total_orgs.ui')}}" + } + } + ] + }, { "widgettype": "HBox", "options": { diff --git a/wwwroot/stat_active_users.ui b/wwwroot/stat_active_users.ui new file mode 100644 index 0000000..2bd56b7 --- /dev/null +++ b/wwwroot/stat_active_users.ui @@ -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": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{get_active_users_today(request)}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "今日活跃用户", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +} diff --git a/wwwroot/stat_new_users_month.ui b/wwwroot/stat_new_users_month.ui new file mode 100644 index 0000000..d78847e --- /dev/null +++ b/wwwroot/stat_new_users_month.ui @@ -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": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{get_new_users_month(request)}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "本月新增用户", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +} diff --git a/wwwroot/stat_total_orgs.ui b/wwwroot/stat_total_orgs.ui new file mode 100644 index 0000000..b71931c --- /dev/null +++ b/wwwroot/stat_total_orgs.ui @@ -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": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{get_total_orgs(request)}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "组织机构数", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +}