diff --git a/sage_datamart/dashboards.py b/sage_datamart/dashboards.py index 99602d8..a65891d 100644 --- a/sage_datamart/dashboards.py +++ b/sage_datamart/dashboards.py @@ -1,12 +1,137 @@ """ -统一 Dashboard API — 从集市表查询所有运营指标 + 性能指标 -替代 dashboard_for_sage 中对 llmusage 的直接查询 +统一 Dashboard API — 从集市表 / llmusage 查询运营指标 + 性能指标 +支持客户(customer)和分销商(distributor)两种视图 +实时指标直接查 llmusage,非实时从集市表取 """ from sqlor.dbpools import get_sor_context from datetime import datetime, timedelta +import redis.asyncio as redis + +# ── 角色/范围辅助 ── + +CUSTOMER = 'customer' +DISTRIBUTOR = 'distributor' -# ── 统计卡片 ── +def _fact_where(scope, org_filter): + """根据角色返回 dm_model_call_fact 的 WHERE 条件""" + if scope == DISTRIBUTOR and org_filter: + return "distributor_orgid = ${org}$" + elif scope == CUSTOMER and org_filter: + return "userorgid = ${org}$" + return "1=1" + + +def _usage_where(scope, org_filter): + """根据角色返回 llmusage 的 WHERE 条件""" + if scope == DISTRIBUTOR and org_filter: + return "distributor_orgid = ${org}$" + elif scope == CUSTOMER and org_filter: + return "userorgid = ${org}$" + return "1=1" + + +# ── 实时指标卡片(直接查 llmusage,无 ETL 延迟)── + +async def get_realtime_calls(sor, today, scope=None, org_filter=None): + """实时今日调用笔数""" + w = _usage_where(scope, org_filter) + sql = ("SELECT COUNT(*) as cnt FROM llmusage " + "WHERE use_date = ${today}$ AND " + w) + ns = {'today': today} + if org_filter: + ns['org'] = org_filter + recs = await sor.sqlExe(sql, ns) + return int(recs[0].cnt) if recs else 0 + + +async def get_realtime_amount(sor, today, scope=None, org_filter=None): + """实时今日交易金额(原币汇总)""" + w = _usage_where(scope, org_filter) + sql = ("SELECT COALESCE(SUM(amount), 0) as total FROM llmusage " + "WHERE use_date = ${today}$ AND " + w) + ns = {'today': today} + if org_filter: + ns['org'] = org_filter + recs = await sor.sqlExe(sql, ns) + return round(float(recs[0].total), 2) if recs else 0 + + +async def get_total_users_count(sor, scope=None, org_filter=None): + """用户总数""" + if scope == CUSTOMER and org_filter: + sql = "SELECT COUNT(*) as cnt FROM users WHERE userorgid = ${org}$" + ns = {'org': org_filter} + elif scope == DISTRIBUTOR and org_filter: + # 分销商下所有客户机构的用户 + sql = ("SELECT COUNT(DISTINCT u.id) as cnt FROM users u " + "JOIN organization o ON o.id = u.userorgid " + "WHERE o.resellerid = ${org}$") + ns = {'org': org_filter} + else: + sql = "SELECT COUNT(*) as cnt FROM users" + ns = {} + recs = await sor.sqlExe(sql, ns) + return int(recs[0].cnt) if recs else 0 + + +async def get_realtime_active_users(sor, today, scope=None, org_filter=None): + """实时今日活跃用户数""" + w = _usage_where(scope, org_filter) + sql = ("SELECT COUNT(DISTINCT userid) as cnt FROM llmusage " + "WHERE use_date = ${today}$ AND " + w) + ns = {'today': today} + if org_filter: + ns['org'] = org_filter + recs = await sor.sqlExe(sql, ns) + return int(recs[0].cnt) if recs else 0 + + +async def get_online_users(env, scope=None, org_filter=None): + """在线用户数 — 从 Redis session 中统计""" + try: + config = env.get_config() + redis_url = getattr(config, 'redis_url', 'redis://127.0.0.1:6379') + r = redis.from_url(redis_url, decode_responses=True) + count = 0 + cursor = 0 + while True: + cursor, keys = await r.scan(cursor, match='AIOHTTP_SESSION_*', count=100) + for k in keys: + data = await r.get(k) + if data: + count += 1 + if cursor == 0: + break + await r.close() + return count + except Exception: + return 0 + + +async def get_online_users_by_org(env, org_filter, scope=None): + """按机构统计在线用户数""" + try: + config = env.get_config() + redis_url = getattr(config, 'redis_url', 'redis://127.0.0.1:6379') + r = redis.from_url(redis_url, decode_responses=True) + count = 0 + cursor = 0 + while True: + cursor, keys = await r.scan(cursor, match='AIOHTTP_SESSION_*', count=100) + for k in keys: + data = await r.get(k) + if data and org_filter in data: + count += 1 + if cursor == 0: + break + await r.close() + return count + except Exception: + return 0 + + +# ── 统计卡片(集市表,ETL 更新)── async def get_today_usage(env, sor): """当天调用总数""" @@ -163,3 +288,76 @@ async def get_hourly_concurrency(env, sor): GROUP BY call_hour ORDER BY call_hour""" recs = await sor.sqlExe(sql, {'today': env.curDateString()}) return [{'hour': r.call_hour, 'users': int(r.users), 'calls': int(r.calls)} for r in recs] + + +# ── 分销商专用 ── + +async def get_distributor_customers_count(sor, distributor_orgid): + """分销商旗下客户数""" + sql = """SELECT COUNT(DISTINCT userorgid) as cnt FROM dm_model_call_fact + WHERE distributor_orgid = ${org}$""" + recs = await sor.sqlExe(sql, {'org': distributor_orgid}) + return int(recs[0].cnt) if recs else 0 + + +async def get_distributor_active_customers(sor, today, distributor_orgid): + """分销商今日活跃客户数""" + sql = """SELECT COUNT(DISTINCT userorgid) as cnt FROM dm_model_call_fact + WHERE call_date = ${today}$ AND distributor_orgid = ${org}$""" + recs = await sor.sqlExe(sql, {'today': today, 'org': distributor_orgid}) + return int(recs[0].cnt) if recs else 0 + + +async def get_distributor_top_customers(sor, distributor_orgid, limit=5): + """分销商旗下客户排行(按金额)""" + sql = """SELECT userorgid, COUNT(*) as cnt, COALESCE(SUM(amount_cny), 0) as total_amount + FROM dm_model_call_fact WHERE distributor_orgid = ${org}$ + GROUP BY userorgid ORDER BY total_amount DESC LIMIT ${limit}$""" + recs = await sor.sqlExe(sql, {'org': distributor_orgid, 'limit': str(limit)}) + return [{'org_id': r.userorgid, 'cnt': int(r.cnt), + 'total_amount': round(float(r.total_amount), 2)} for r in recs] + + +# ── scope 过滤版(在全局函数基础上加 org_filter)── + +async def get_scoped_today_usage(sor, today, scope=None, org_filter=None): + w = _fact_where(scope, org_filter) + sql = "SELECT COUNT(*) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$ AND " + w + ns = {'today': today} + if org_filter: + ns['org'] = org_filter + recs = await sor.sqlExe(sql, ns) + return int(recs[0].cnt) if recs else 0 + + +async def get_scoped_today_amount(sor, today, scope=None, org_filter=None): + w = _fact_where(scope, org_filter) + sql = "SELECT COALESCE(SUM(amount_cny), 0) as total FROM dm_model_call_fact WHERE call_date = ${today}$ AND " + w + ns = {'today': today} + if org_filter: + ns['org'] = org_filter + recs = await sor.sqlExe(sql, ns) + return round(float(recs[0].total), 2) if recs else 0 + + +async def get_scoped_success_rate(sor, today, scope=None, org_filter=None): + w = _fact_where(scope, org_filter) + sql = ("SELECT COUNT(*) as total, SUM(CASE WHEN status='SUCCEEDED' THEN 1 ELSE 0 END) as success " + "FROM dm_model_call_fact WHERE call_date = ${today}$ AND " + w) + ns = {'today': today} + if org_filter: + ns['org'] = org_filter + recs = await sor.sqlExe(sql, ns) + if recs and recs[0].total: + return round(recs[0].success * 100.0 / recs[0].total, 1) + return 0 + + +async def get_scoped_fail_count(sor, today, scope=None, org_filter=None): + w = _fact_where(scope, org_filter) + sql = "SELECT COUNT(*) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$ AND status = 'FAILED' AND " + w + ns = {'today': today} + if org_filter: + ns['org'] = org_filter + recs = await sor.sqlExe(sql, ns) + return int(recs[0].cnt) if recs else 0 diff --git a/sage_datamart/init.py b/sage_datamart/init.py index 171ad04..2def3c7 100644 --- a/sage_datamart/init.py +++ b/sage_datamart/init.py @@ -1,20 +1,50 @@ -"""sage_datamart 模块初始化 — 统一 Dashboard API""" +"""sage_datamart 模块初始化 — 统一 Dashboard API,支持客户/分销商角色""" from ahserver.serverenv import ServerEnv from appPublic.log import debug from sqlor.dbpools import get_sor_context from .etl import sync_call_fact, aggregate_daily_perf, aggregate_provider_cost from .dashboards import ( + CUSTOMER, DISTRIBUTOR, get_today_usage, get_today_amount, get_success_rate, get_fail_count, get_active_users, get_top_models, get_top_providers, get_top_users, get_customer_daily_models, get_customer_monthly_models, get_customer_user_today, get_daily_trend, get_hourly_concurrency, get_avg_ttft, get_currency_breakdown, + get_realtime_calls, get_realtime_amount, get_total_users_count, + get_realtime_active_users, get_online_users, + get_distributor_customers_count, get_distributor_active_customers, + get_distributor_top_customers, + get_scoped_today_usage, get_scoped_today_amount, + get_scoped_success_rate, get_scoped_fail_count, ) MODULE_NAME = "sage_datamart" +async def _resolve_view(request): + """解析当前用户角色和机构过滤条件 + 返回 (scope, org_filter) — scope: 'customer'|'distributor', org_filter: org_id + """ + env = request._run_ns + userorgid = await env.get_userorgid() + scope = CUSTOMER + org_filter = userorgid + try: + async with get_sor_context(env, MODULE_NAME) as sor: + recs = await sor.sqlExe( + "SELECT org_type FROM organization WHERE id = ${id}$", + {'id': userorgid}) + if recs: + org_type = (recs[0].org_type if hasattr(recs[0], 'org_type') + else recs[0].get('org_type', '')) + if org_type == 'reseller': + scope = DISTRIBUTOR + except Exception: + pass + return scope, org_filter + + # ── 性能指标 API ── async def api_model_perf(sor, params_kw=None): @@ -49,10 +79,9 @@ async def api_provider_roi(sor, params_kw=None): return [dict(r) for r in rows] -# ── 运营指标 API(替代 dashboard_for_sage)── +# ── 运营指标 API ── async def api_stats(request): - """统计卡片数据 — /sage_datamart/api/stats.dspy""" env = request._run_ns async with get_sor_context(env, MODULE_NAME) as sor: return { @@ -65,28 +94,24 @@ async def api_stats(request): async def api_top_models(request): - """Top 模型 — /sage_datamart/api/top_models.dspy""" env = request._run_ns async with get_sor_context(env, MODULE_NAME) as sor: return await get_top_models(env, sor) async def api_top_providers(request): - """Top 供应商 — /sage_datamart/api/top_providers.dspy""" env = request._run_ns async with get_sor_context(env, MODULE_NAME) as sor: return await get_top_providers(env, sor) async def api_top_users(request): - """Top 用户 — /sage_datamart/api/top_users.dspy""" env = request._run_ns async with get_sor_context(env, MODULE_NAME) as sor: return await get_top_users(env, sor) async def api_customer_models(request): - """客户用量 — /sage_datamart/api/customer_models.dspy""" env = request._run_ns userorgid = await env.get_userorgid() async with get_sor_context(env, MODULE_NAME) as sor: @@ -97,27 +122,124 @@ async def api_customer_models(request): async def api_daily_trend(request): - """7天趋势 — /sage_datamart/api/daily_trend.dspy""" env = request._run_ns async with get_sor_context(env, MODULE_NAME) as sor: return await get_daily_trend(env, sor) async def api_hourly_concurrency(request): - """每小时并发 — /sage_datamart/api/hourly_concurrency.dspy""" env = request._run_ns async with get_sor_context(env, MODULE_NAME) as sor: return await get_hourly_concurrency(env, sor) async def api_currency_stats(request): - """币种分布统计 — /sage_datamart/api/currency_stats.dspy""" env = request._run_ns async with get_sor_context(env, MODULE_NAME) as sor: return await get_currency_breakdown(env, sor) -# ── Jinja2 模板函数(给 stat_*.ui 调用)── +# ── 实时指标 API(角色感知)── + +async def api_realtime_cards(request): + """实时统计卡片 — /sage_datamart/api/realtime_cards.dspy""" + env = request._run_ns + scope, org_filter = await _resolve_view(request) + today = env.curDateString() + async with get_sor_context(env, MODULE_NAME) as sor: + if scope == DISTRIBUTOR: + return { + 'scope': scope, + 'calls': await get_realtime_calls(sor, today, scope, org_filter), + 'amount': await get_realtime_amount(sor, today, scope, org_filter), + 'total_users': await get_distributor_customers_count(sor, org_filter), + 'active_customers': await get_distributor_active_customers(sor, today, org_filter), + 'online_users': await get_online_users(env), + 'success_rate': await get_scoped_success_rate(sor, today, scope, org_filter), + 'fail_count': await get_scoped_fail_count(sor, today, scope, org_filter), + } + else: + return { + 'scope': scope, + 'calls': await get_realtime_calls(sor, today, scope, org_filter), + 'amount': await get_realtime_amount(sor, today, scope, org_filter), + 'total_users': await get_total_users_count(sor, scope, org_filter), + 'active_users': await get_realtime_active_users(sor, today, scope, org_filter), + 'online_users': await get_online_users(env), + 'success_rate': await get_scoped_success_rate(sor, today, scope, org_filter), + 'fail_count': await get_scoped_fail_count(sor, today, scope, org_filter), + } + + +# ── 分销商专用 API ── + +async def api_distributor_customers(request): + """分销商客户排行 — /sage_datamart/api/distributor_customers.dspy""" + env = request._run_ns + scope, org_filter = await _resolve_view(request) + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_distributor_top_customers(sor, org_filter) + + +# ── Jinja2 模板函数(实时,角色感知)── + +async def j2_realtime_calls(request): + env = request._run_ns + scope, org_filter = await _resolve_view(request) + today = env.curDateString() + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_realtime_calls(sor, today, scope, org_filter) + + +async def j2_realtime_amount(request): + env = request._run_ns + scope, org_filter = await _resolve_view(request) + today = env.curDateString() + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_realtime_amount(sor, today, scope, org_filter) + + +async def j2_total_users(request): + env = request._run_ns + scope, org_filter = await _resolve_view(request) + async with get_sor_context(env, MODULE_NAME) as sor: + if scope == DISTRIBUTOR: + return await get_distributor_customers_count(sor, org_filter) + return await get_total_users_count(sor, scope, org_filter) + + +async def j2_active_today(request): + env = request._run_ns + scope, org_filter = await _resolve_view(request) + today = env.curDateString() + async with get_sor_context(env, MODULE_NAME) as sor: + if scope == DISTRIBUTOR: + return await get_distributor_active_customers(sor, today, org_filter) + return await get_realtime_active_users(sor, today, scope, org_filter) + + +async def j2_online_users(request): + env = request._run_ns + return await get_online_users(env) + + +async def j2_scoped_success_rate(request): + env = request._run_ns + scope, org_filter = await _resolve_view(request) + today = env.curDateString() + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_scoped_success_rate(sor, today, scope, org_filter) + + +async def j2_scoped_fail_count(request): + env = request._run_ns + scope, org_filter = await _resolve_view(request) + today = env.curDateString() + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_scoped_fail_count(sor, today, scope, org_filter) + + +# ── 向后兼容 Jinja2 函数 ── async def j2_today_usage(request): env = request._run_ns @@ -155,10 +277,9 @@ async def j2_avg_ttft(request): return await get_avg_ttft(env, sor) -# ── Cron ETL 函数 ── +# ── Cron ETL ── async def cron_etl_sync(request): - """ETL: 增量同步 llmusage → dm_model_call_fact""" ip = request['client_ip'] if ip not in ['127.0.0.1']: return {'error': f'IP {ip} not allowed'} @@ -169,7 +290,6 @@ async def cron_etl_sync(request): async def cron_etl_aggregate(request): - """ETL: 聚合 dm_model_call_fact → dm_model_perf_daily""" ip = request['client_ip'] if ip not in ['127.0.0.1']: return {'error': f'IP {ip} not allowed'} @@ -180,7 +300,6 @@ async def cron_etl_aggregate(request): async def cron_etl_provider_cost(request): - """ETL: 聚合供应商性价比 dm_provider_cost_daily""" ip = request['client_ip'] if ip not in ['127.0.0.1']: return {'error': f'IP {ip} not allowed'} @@ -190,9 +309,6 @@ async def cron_etl_provider_cost(request): return {'status': 'ok', 'aggregated': count} - - - def load_sage_datamart(): env = ServerEnv() env.api_model_perf = api_model_perf @@ -205,14 +321,23 @@ def load_sage_datamart(): env.api_daily_trend = api_daily_trend env.api_hourly_concurrency = api_hourly_concurrency env.api_currency_stats = api_currency_stats + env.api_realtime_cards = api_realtime_cards + env.api_distributor_customers = api_distributor_customers env.j2_today_usage = j2_today_usage env.j2_today_amount = j2_today_amount env.j2_active_users = j2_active_users env.j2_fail_count = j2_fail_count env.j2_success_rate = j2_success_rate env.j2_avg_ttft = j2_avg_ttft + env.j2_realtime_calls = j2_realtime_calls + env.j2_realtime_amount = j2_realtime_amount + env.j2_total_users = j2_total_users + env.j2_active_today = j2_active_today + env.j2_online_users = j2_online_users + env.j2_scoped_success_rate = j2_scoped_success_rate + env.j2_scoped_fail_count = j2_scoped_fail_count env.cron_etl_sync = cron_etl_sync env.cron_etl_aggregate = cron_etl_aggregate env.cron_etl_provider_cost = cron_etl_provider_cost - debug("[sage_datamart] registered all API endpoints and cron functions") + debug("[sage_datamart] registered all API + realtime + role-aware endpoints") diff --git a/scripts/load_path.py b/scripts/load_path.py index 45b2020..61c5dcb 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -28,6 +28,13 @@ PATHS_LOGINED = [ f"/{MOD}/stat_fail_calls.ui", f"/{MOD}/stat_today_usage.ui", f"/{MOD}/stat_today_amount.ui", + f"/{MOD}/rt_card_calls.ui", + f"/{MOD}/rt_card_amount.ui", + f"/{MOD}/rt_card_users.ui", + f"/{MOD}/rt_card_active.ui", + f"/{MOD}/rt_card_online.ui", + f"/{MOD}/rt_card_success.ui", + f"/{MOD}/rt_card_fail.ui", ] def run(role, path): diff --git a/wwwroot/api/distributor_customers.dspy b/wwwroot/api/distributor_customers.dspy new file mode 100644 index 0000000..dbfad46 --- /dev/null +++ b/wwwroot/api/distributor_customers.dspy @@ -0,0 +1 @@ +return await api_distributor_customers(request) diff --git a/wwwroot/api/realtime_cards.dspy b/wwwroot/api/realtime_cards.dspy new file mode 100644 index 0000000..ad6b247 --- /dev/null +++ b/wwwroot/api/realtime_cards.dspy @@ -0,0 +1 @@ +return await api_realtime_cards(request) diff --git a/wwwroot/index.ui b/wwwroot/index.ui index cb78f5f..10a686e 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -12,20 +12,21 @@ "widgettype": "HBox", "options": {"width": "100%", "alignItems": "center", "marginBottom": "12px"}, "subwidgets": [ - {"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "数据概览", "i18n": true}}, + {"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "实时数据概览", "i18n": true}}, {"widgettype": "Filler"} ] }, { "widgettype": "HBox", - "options": {"width": "100%", "gap": "16px", "marginBottom": "16px"}, + "options": {"width": "100%", "gap": "12px", "marginBottom": "16px", "flexWrap": "wrap"}, "subwidgets": [ - {"widgettype": "RefreshWidget", "id": "stat_today_usage", "options": {"period_seconds": 30, "url": "{{entire_url('stat_today_usage.ui')}}", "width": "16%"}}, - {"widgettype": "RefreshWidget", "id": "stat_today_amount", "options": {"period_seconds": 30, "url": "{{entire_url('stat_today_amount.ui')}}", "width": "16%"}}, - {"widgettype": "RefreshWidget", "id": "stat_success_rate", "options": {"period_seconds": 60, "url": "{{entire_url('stat_success_rate.ui')}}", "width": "16%"}}, - {"widgettype": "RefreshWidget", "id": "stat_fail_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_fail_calls.ui')}}", "width": "16%"}}, - {"widgettype": "RefreshWidget", "id": "stat_avg_ttft", "options": {"period_seconds": 60, "url": "{{entire_url('stat_avg_ttft.ui')}}", "width": "16%"}}, - {"widgettype": "RefreshWidget", "id": "stat_total_users", "options": {"period_seconds": 60, "url": "{{entire_url('stat_total_users.ui')}}", "width": "16%"}} + {"widgettype": "RefreshWidget", "id": "rt_calls", "options": {"period_seconds": 15, "url": "{{entire_url('rt_card_calls.ui')}}", "width": "13%"}}, + {"widgettype": "RefreshWidget", "id": "rt_amount", "options": {"period_seconds": 15, "url": "{{entire_url('rt_card_amount.ui')}}", "width": "13%"}}, + {"widgettype": "RefreshWidget", "id": "rt_users", "options": {"period_seconds": 30, "url": "{{entire_url('rt_card_users.ui')}}", "width": "13%"}}, + {"widgettype": "RefreshWidget", "id": "rt_active", "options": {"period_seconds": 30, "url": "{{entire_url('rt_card_active.ui')}}", "width": "13%"}}, + {"widgettype": "RefreshWidget", "id": "rt_online", "options": {"period_seconds": 30, "url": "{{entire_url('rt_card_online.ui')}}", "width": "13%"}}, + {"widgettype": "RefreshWidget", "id": "rt_success", "options": {"period_seconds": 30, "url": "{{entire_url('rt_card_success.ui')}}", "width": "13%"}}, + {"widgettype": "RefreshWidget", "id": "rt_fail", "options": {"period_seconds": 30, "url": "{{entire_url('rt_card_fail.ui')}}", "width": "13%"}} ] }, { diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui index 7b8be02..9449a45 100644 --- a/wwwroot/menu.ui +++ b/wwwroot/menu.ui @@ -1,21 +1,28 @@ +{% set roles = get_user_roles(get_user()) %} +{% set userorgid = get_user_orgid() %} { "widgettype": "VBox", "options": {"width": "100%"}, "subwidgets": [ + { + "widgettype": "MenuItem", + "options": { + "label": "实时数据概览", + "url": "{{entire_url('/sage_datamart/index.ui')}}" + } + }, { "widgettype": "MenuItem", "options": { "label": "模型性能", - "url": "{{entire_url('/sage_datamart/index.ui')}}", - "icon": "{{entire_url('/sage_datamart/imgs/perf.svg')}}" + "url": "{{entire_url('/sage_datamart/index.ui')}}" } }, { "widgettype": "MenuItem", "options": { "label": "供应商性价比", - "url": "{{entire_url('/sage_datamart/provider_roi.ui')}}", - "icon": "{{entire_url('/sage_datamart/imgs/roi.svg')}}" + "url": "{{entire_url('/sage_datamart/provider_roi.ui')}}" } } ] diff --git a/wwwroot/rt_card_active.ui b/wwwroot/rt_card_active.ui new file mode 100644 index 0000000..f820789 --- /dev/null +++ b/wwwroot/rt_card_active.ui @@ -0,0 +1,2 @@ +{% set cnt = j2_active_today(request) %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"今日活跃","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{cnt}}","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/rt_card_amount.ui b/wwwroot/rt_card_amount.ui new file mode 100644 index 0000000..c2872d7 --- /dev/null +++ b/wwwroot/rt_card_amount.ui @@ -0,0 +1,2 @@ +{% set amt = j2_realtime_amount(request) %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"今日交易金额","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"¥{{'%.2f' % amt}}","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/rt_card_calls.ui b/wwwroot/rt_card_calls.ui new file mode 100644 index 0000000..10a3ecf --- /dev/null +++ b/wwwroot/rt_card_calls.ui @@ -0,0 +1,2 @@ +{% set cnt = j2_realtime_calls(request) %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"今日调用笔数","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{cnt}}","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/rt_card_fail.ui b/wwwroot/rt_card_fail.ui new file mode 100644 index 0000000..59f068a --- /dev/null +++ b/wwwroot/rt_card_fail.ui @@ -0,0 +1,2 @@ +{% set cnt = j2_scoped_fail_count(request) %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"失败数","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{cnt}}","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/rt_card_online.ui b/wwwroot/rt_card_online.ui new file mode 100644 index 0000000..1db5e53 --- /dev/null +++ b/wwwroot/rt_card_online.ui @@ -0,0 +1,2 @@ +{% set cnt = j2_online_users(request) %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"在线用户","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{cnt}}","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/rt_card_success.ui b/wwwroot/rt_card_success.ui new file mode 100644 index 0000000..9dc34da --- /dev/null +++ b/wwwroot/rt_card_success.ui @@ -0,0 +1,2 @@ +{% set rate = j2_scoped_success_rate(request) %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"成功率","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{rate}}%","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/rt_card_users.ui b/wwwroot/rt_card_users.ui new file mode 100644 index 0000000..46c7cc1 --- /dev/null +++ b/wwwroot/rt_card_users.ui @@ -0,0 +1,2 @@ +{% set cnt = j2_total_users(request) %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"用户总数","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{cnt}}","fontWeight":"700","marginTop":"4px"}}]}