Compare commits

..

87 Commits

Author SHA1 Message Date
Hermes Agent
2a7be96ec9 fix: use accounting_status='accounted' instead of !='failed'
Only explicitly accounted records count as successful transactions.
2026-06-19 15:23:03 +08:00
Hermes Agent
7dd85acafa feat: add i18n translations (zh/en/jp/ko) for all modules 2026-06-19 15:01:23 +08:00
Hermes Agent
35ba31a554 fix: exclude failed accounting from today stats + add click nav to failed records
1. load_dashboard.py: get_today_usage/amount and trend functions now
   filter out accounting_status='failed' records so failed transactions
   are not counted as successful.
2. stat_errors.ui: added click bind to navigate to llmage failed
   accounting records page (app.sage_main_content target).
2026-06-19 14:45:26 +08:00
Hermes Agent
2fcf32c863 dashboard: 去除热门模型刷新按钮 2026-06-18 10:48:15 +08:00
Hermes Agent
39e8df47b5 chore: 添加json/build.sh脚本 2026-06-17 17:26:20 +08:00
Hermes Agent
0d0c5b70bd docs: 添加Seedance R2V多模态参考生视频示例 2026-06-17 15:40:50 +08:00
d51018aeff docs: add GET /v1/pricing endpoint to API documentation 2026-06-11 15:59:30 +08:00
677ebae525 docs: add music, TTS, ASR API endpoints to customer-facing documentation
- POST /v1/music/generations: MiniMax Music 2.6/2.5 with lyrics structure tags
- POST /v1/audio/speech: MiniMax Speech 2.6 Turbo/HD, 2.5 HD, F5-TTS
- POST /v1/audio/transcriptions: qwen3-asr-flash, Nvidia parakeet-tdt-0.6b-v2
- Includes curl examples, response formats, model lists, error codes
2026-06-04 14:07:00 +08:00
23816650ab docs: 图像生成API全部改为同步模式,修正响应格式与实际API一致
- 删除所有异步模型描述(qwen-image-plus系列改为同步)
- 修正成功响应:usage字段按实际返回(万象含tokens,千问含宽高)
- 修正失败响应:两种实际格式(参数缺失/模型不存在)
- 修正image URL:dashscope OSS → token.opencomputing.cn/idfile
- 修正size格式:1024x1024 → 1024*1024
- 各模型明细节删除重复的成功/失败响应示例
- /v1/tasks标注仅用于视频生成等异步任务
2026-06-03 23:59:26 +08:00
e1bde5d801 fix: add H1 heading for LLM API section to enable proper sidebar grouping 2026-06-03 10:47:48 +08:00
5ed5053bfd feat: 百分比后添加↑↓箭头指示趋势方向 2026-06-03 10:39:38 +08:00
066baae242 bugfix 2026-06-02 20:17:56 +08:00
ed6c9eb31f bugfix 2026-06-02 20:14:51 +08:00
8a1ec5efdc bugfix 2026-06-02 20:11:38 +08:00
bf4bc441b1 fix: virtual human API section heading level (H2 -> H1, H3 -> H2) 2026-06-02 17:58:25 +08:00
e64e08da6f fix: close unclosed Bearer token quotes in curl examples (8 lines) 2026-06-02 17:45:17 +08:00
4dc0603d5f docs: add virtual human asset API documentation
- Add 5 virtual human API endpoints (rl_virtual_create_group, rl_virtual_groups, rl_virtual_upload, rl_virtual_status, rl_virtual_assets)
- Include complete request/response examples with curl test cases
- Add virtual human error documentation
- Restore real person asset error documentation
2026-06-02 17:34:15 +08:00
8b05c8628f docs: add curl test examples for all API endpoints in api_doc.md 2026-06-02 17:11:17 +08:00
067b1003e3 docs: fix image/generations general response example to match actual UAPI response 2026-06-02 16:36:13 +08:00
4ac95125d7 docs: fix image generation response format - remove outer wrapper, use direct UAPI response 2026-06-02 16:25:17 +08:00
0d73c74c4d docs: add wan2.7 image models, fix qwen-image response format to match UAPI template 2026-06-02 15:49:44 +08:00
89ff1cdbb3 docs: remove jimeng, cogview and wan-async image models, keep qwen-image only 2026-06-02 15:18:35 +08:00
ad58dab697 docs: add qwen-image-2.0 and qwen-image-plus models to image generation API docs 2026-06-02 15:08:27 +08:00
aa2c553ff0 docs: add GET /v1/models/catelog endpoint to API docs 2026-06-02 11:21:30 +08:00
398e90fa17 fix: dashboard布局重构 - 修复遮挡/热门模型金额/今日指标缺失
1. 热门模型: valueFields改为[total_amount, cnt],与用户/供应商一致
2. 布局重构:
   - 第一行: 今日调用总量+今日交易金额(2个50%宽卡片)
   - 第二行: 用户总数+活跃用户+在线用户+记账异常(4个25%宽卡片)
   - 第三行: 热门模型+用户排行+供应商排行(3个33%宽图表)
3. 图表高度统一250px,间距16px
4. 移除ResponsableBox改用HBox,避免卡片溢出遮挡
5. 移除stat_new_users_month和stat_total_orgs,精简指标
2026-06-02 00:25:12 +08:00
a1fb0089ad force: re-sync load_dashboard.py (ensure full 614-line version) 2026-06-01 16:50:58 +08:00
401c7a15bb fix: restore load_dashboard.py (truncated), use CSS vars for dark mode, improve card spacing 2026-06-01 16:39:57 +08:00
759d629bb3 fix: fullscreen background color and stat card layout 2026-06-01 16:14:56 +08:00
bf5cf2b1b2 fix: fullscreen targets dashboard container instead of entire page 2026-06-01 16:09:05 +08:00
966c89080b fix: reduce margins, padding, chart heights to eliminate scrollbar 2026-06-01 16:08:14 +08:00
c89c0d89de refactor: arrange model/user/provider charts in single row 2026-06-01 15:56:14 +08:00
80785e7ace fix: use entire_url() for chart data URLs to include module prefix 2026-06-01 15:52:10 +08:00
9cc59160e8 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
2026-06-01 15:49:46 +08:00
855f376671 refactor: consolidate rankings into combined charts
- Change top models from Top 3 to Top 5
- Simplify titles: remove 'Top X' and parenthetical details
- Replace separate user tables (by amount/by count) with single ChartBar
- Replace separate provider tables with single ChartBar
- Add get_top_users_combined and get_top_providers_combined functions
2026-06-01 15:41:40 +08:00
c9e860c691 remove quick-links panel, chart takes full width 2026-06-01 15:34:18 +08:00
8bed983919 fix: hide '我的今日模型使用' card for non-customer roles
Remove duplicate unprotected card that was always rendered regardless
of role, causing 403 for non-customer users. Keep only the customer-
only version wrapped in {% if 'customer.*' in roles %}.
2026-06-01 15:32:16 +08:00
66f588cd80 add reseller.* role support alongside owner.* 2026-06-01 15:28:36 +08:00
69264b6ec6 restrict my-today-models and customer-monitor to customer.* role only 2026-06-01 15:25:29 +08:00
3cbb0a4719 fix: double comma causing blank page for owner role 2026-06-01 15:06:06 +08:00
14881f83f2 fix: remove stray endif that caused 500 error 2026-06-01 15:01:36 +08:00
40706c4c72 hide customer monitoring from non-customer users 2026-06-01 14:57:27 +08:00
9b52cd2e04 fix: 使用Iframe widget替代urlwidget加载HTML页面 2026-06-01 13:32:34 +08:00
afdbb2ed37 feat: Apifox风格API文档页面,替代MarkdownViewer平铺展示 2026-06-01 13:26:46 +08:00
d65629afb2 bugfix 2026-05-31 15:03:55 +08:00
650d2e6feb docs: add reallife_asset API section to unified api_doc.md 2026-05-31 13:24:31 +08:00
0d62b568e2 fix: sageOnLogin skips dashboard load when Router has saved route
Added sageOnLogin(dashboardUrl) function that checks if the SPA Router
has a route (via Router.current() or URL ?page= param) before loading
the dashboard. This prevents user_logined event from overwriting
Router-restored content on page refresh with deep links.
2026-05-31 13:08:45 +08:00
97ca142092 feat: add fullscreen toggle button to dashboard
Added a fullscreen button (⛶) next to the last-update timestamp.
Clicking it toggles browser fullscreen mode using the Fullscreen API.
2026-05-31 13:02:10 +08:00
28e538750b docs: 补充catelogid必填参数说明
在图像生成API文档中明确catelogid为必填参数,文生图固定为t2i
2026-05-31 12:28:58 +08:00
84817a6805 docs: 添加图像生成API详细模型参数文档
补充 POST /v1/image/generations 接口的4个生图模型详细参数说明:
- 百炼万象系列(wan2.2-t2i-plus, wan2.5-t2i-preview, wan2.2-t2i-flash)- 异步模式
- 智谱CogView(cogview-3-flash)- 同步模式

包含:输入参数表、可用模型列表、响应示例
2026-05-31 12:28:10 +08:00
915b77cab7 feat: 复制api_doc到dashboard_for_sage供客户用户访问大模型API文档
- 新增 api_doc.ui 和 api_doc.md(从llmage模块复制)
- api_doc.ui中md_url指向/dashboard_for_sage/api_doc.md
- load_path.py注册新路径权限
2026-05-31 10:33:27 +08:00
b648717339 feat: role-based dashboard visibility - customer users see only customer monitoring 2026-05-31 10:22:10 +08:00
98a28d9770 feat: 添加全局今日模型使用图表,替换用户级监控为全量监控
- 新增 get_all_today_models() 函数查询所有用户今日模型使用
- 新增 all_today_models_chart.ui 和 api/all_today_models.dspy
- index.ui 标题改为「今日模型使用(全部)」,数据源改为全量
- load_path.py 注册新路径权限
- 保留原有 user_today_models 供后续个性化需求使用
2026-05-31 10:01:28 +08:00
929ee0e319 feat: 添加客户专属监控 - 组织级每日/每月模型调用统计
- 新增5个后端函数: customer_daily/monthly_models, daily/monthly_summary, daily_trend
- 新增customer_usage.ui主页面: 4个汇总卡片 + 日趋势折线图 + 日/月模型柱状图
- 新增3个API端点(.dspy)和3个图表UI组件
- index.ui添加客户专属监控入口卡片
- RBAC权限配置已更新(load_path.py)
2026-05-31 08:07:25 +08:00
69b7ec5cd0 feat: add user-level model usage chart (我的今日模型使用)
- Add get_user_today_models() function to load_dashboard.py
  Shows current user's today model call counts and amounts
- Create api/user_today_models.dspy endpoint
- Create user_today_models_chart.ui ChartBar widget (30s auto-refresh)
- Add '我的今日模型使用' card section to index.ui with refresh button
- Register new paths in load_path.py (logined permission)
2026-05-31 08:00:22 +08:00
c36ada56b1 fix: sidebar collapse width, CRUD height overflow, dashboard VScrollPanel 2026-05-30 21:20:29 +08:00
d8ec4e7142 fix: tabular row selection visibility - CSS specificity fix for dark/light themes
- Dark theme: .tabular-cell color was overriding .tabular-row-selected color
  due to same specificity and later position. Added !important and combined
  selector .tabular-row-selected .tabular-cell to fix.
- Added background-color change for selected rows (both themes)
- Added cursor:pointer on .tabular-row for better UX
- Light theme: added blue-tinted background + darker text for selected rows
2026-05-29 13:44:46 +08:00
37b648da0e feat: wire Menu collapse/expand to sidebar toggle button + menu-collapsed CSS 2026-05-29 11:12:56 +08:00
e2687054df fix dashboard UI: quick-link css class, stat-card theming, remove hardcoded colors, light theme overrides 2026-05-28 18:10:59 +08:00
3659533102 fix: responsive layout - sidebar collapse, mobile adaptation, prevent text jumping
- Fix text jumping on right side when screen narrows (min-width:0 on flex item)
- Fix sidebar toggle button not working (retry icon init, proper state sync)
- Mobile adaptation: sidebar as overlay with slide animation
- Auto-collapse sidebar on mobile viewport (<=768px)
- Click outside sidebar to close on mobile
- Responsive padding for stat cards and main content
- Hide brand title on very small screens (<=480px)
- Smooth transitions for sidebar collapse/expand
2026-05-28 16:58:29 +08:00
79a04be92b fix: remove hardcoded dark theme colors from all .ui files for light theme support 2026-05-28 16:14:01 +08:00
cdd812f935 i18n: convert dashboard text fields to otext+i18n:true for translation
All Chinese text strings converted to use otext with i18n:true so
bricks framework can look up translations. Template strings with
{{...}} left as text (correct - dynamic content).

Affected files:
- index.ui: 数据概览, 快捷入口, 模型管理, 用户管理, 知识库, 异常记录,
  用户消费排行, 用户调用排行, 供应商交易排行, 供应商调用排行, Top 3 模型
- stat_*.ui: 今日活跃用户, 在线用户, 记账异常, 本月新增用户, 今日消费金额,
  今日调用笔数, 组织机构数, 用户总数
- today_amount.ui, today_usage.ui: 今日交易金额, 今日调用笔数
- accounting_errors.ui, concurrent_users.ui, total_users.ui
- table_top_*.ui: 暂无数据
- top_users_amount.ui: 用户金额TOP5, 排名, 用户, 金额, 调用次数
2026-05-28 14:53:20 +08:00
9f8e5a6d1c fix: retry theme icon update until bricks button element exists 2026-05-28 14:33:59 +08:00
22a8dc7ceb fix: ensure theme switching works for all containers including sage-shell, topbar, sidebar, and main content area 2026-05-28 14:14:19 +08:00
4170c0b009 fix: remove global_menu_widget id from sageReloadMenu urlwidget 2026-05-28 10:35:10 +08:00
56a9a13db1 refactor: remove shell.ui and global_menu.ui (moved to sage/wwwroot)
dashboard_for_sage now only handles dashboard content when the
dashboard menu item is clicked. Shell layout and global menu are
sage-level concerns managed in sage/wwwroot/index.ui and
sage/wwwroot/global_menu.ui.
2026-05-27 18:43:51 +08:00
741daafdef refactor: rename event sage_login to user_logined 2026-05-27 18:01:53 +08:00
cbe725bcee feat: dynamic menu reload on login/logout
- shell.ui: add id to menu urlwidget, binds for sage_login/sage_logout events
- shell_theme.js: add sageReloadMenu() to rebuild menu urlwidget
- global_menu.ui: complete with all 22 modules and role-based visibility
2026-05-27 17:57:40 +08:00
dfe6c0e14f feat: 完善global_menu.ui,添加所有模块菜单项
新增模块:
- 统一仪表板 (unified_dashboard)
- CRM系统 (integrated_crm_app)
- 客户管理 (customer_management)
- 商机管理 (opportunity_management)
- 合同管理 (contract_management)
- 折扣管理 (discount)
- 财务管理 (financial_management)
- 工作流审批 (workflow_approval)
- 算力中心管理 (cpcc)
- 运维管理 (msp)
- 内容管理 (cms/entcms)
- 钉钉审批 (cms/dingdingflow)

总计22个模块菜单项,覆盖所有业务模块
2026-05-27 17:50:19 +08:00
61a1b2b2fa feat: integrate bricks.Router into shell
- shell_theme.js: call bricks.Router.init() with sage_main_content target
- Remove standalone spa_router.js (now built into bricks)
- Remove spa_router.js from load_path.py RBAC
2026-05-27 15:19:19 +08:00
0032e364b1 feat: add spa_router.js RBAC permission (any) 2026-05-27 14:10:14 +08:00
39fe93438c feat: SPA router for bricks - URL state management
- Intercepts buildUrlwidgetHandler to track navigation to sage_main_content
- Updates browser URL via History API (pushState) on page changes
- Restores page state on browser refresh via ?page= URL parameter
- Supports browser back/forward buttons via popstate event
- Supports deep linking (direct URL access to specific pages)
- URL format: /?page=/module/index.ui
2026-05-27 14:08:39 +08:00
7987c24e26 fix: 完善shell_theme.css深色/浅色主题CRUD组件覆盖(tabular/popup/form/accordion等) 2026-05-26 23:44:14 +08:00
548dc4d15b fix: 快捷入口模型管理和用户管理按钮底色改为#1E293B+白字加粗提升对比度 2026-05-26 23:28:12 +08:00
8ceb769356 fix dashboard: clickable quick entries, full-data ranking tables, dark bg
1. Quick entry shortcuts: Replace VBox (no native click) with Button
   widgets for reliable click event handling. Each entry wraps its
   icon+text in a VBox inside the Button for layout.

2. Ranking tables: Changed get_top_users/ providers queries from
   today-only (WHERE use_date=today) to all-time data so transaction
   counts and amounts always display meaningful results.

3. Background color: Added bgcolor=#0B1120 to top-level index.ui VBox
   so the page background matches the shell theme, eliminating the
   white-vs-dark-blue contrast.
2026-05-26 16:06:54 +08:00
95d18e7ce0 feat: add product_management and supplychain to global menu 2026-05-26 14:07:48 +08:00
b8ac00ea16 feat: switch all modules to index.ui, add pricing and accounting entries 2026-05-26 13:59:24 +08:00
583f35ad6d feat: switch llmage and rag global menu to index.ui 2026-05-26 12:21:01 +08:00
93a387c9ed Revert "feat: update global_menu.ui - all modules point to index.ui, add pricing and accounting, remove submenu"
This reverts commit 27213ed3c50a221b90ae904eff06ed057826d4aa.
2026-05-26 12:17:34 +08:00
27213ed3c5 feat: update global_menu.ui - all modules point to index.ui, add pricing and accounting, remove submenu 2026-05-26 12:11:21 +08:00
a8eaa4e219 fix: get_new_users_month - use 'created_at' instead of non-existent 'created_date' column in users table 2026-05-26 11:18:53 +08:00
87dc6da0af fix: top_models.dspy return data via 'return' instead of 'print' 2026-05-26 11:14:56 +08:00
f74f8aed8d fix: restore shell_theme.css/js permissions in load_path.py (ahserver still requires RBAC check for auto-served static files) 2026-05-26 08:40:08 +08:00
0f470fca61 fix: remove manual css/js references from shell.ui and load_path.py
- Remove Html widget in shell.ui that manually loaded shell_theme.css/js
  (ahserver auto-serves wwwroot js/css files)
- Remove shell_theme.css/js from load_path.py permission list
  (ahserver handles static resources automatically)
2026-05-26 07:36:52 +08:00
f2b2e5d6e7 fix: set shell_theme.css/js to 'any' role (static resources need public access) 2026-05-25 22:32:06 +08:00
fd8443f445 feat: add scripts/load_path.py for RBAC permission initialization
- Add scripts/load_path.py with all 28 wwwroot paths
- menu.ui set to 'any' role (public nav access)
- All other paths set to 'logined' role (dashboard visible to authenticated users)
- Idempotent: skips already-registered paths
2026-05-25 22:24:48 +08:00
e7fc646372 feat: add user call count top5 and provider transaction top5 monitoring cards
- Add table_top_users_count.ui: user call count ranking (Top 5)
- Add table_top_providers_amount.ui: provider transaction amount ranking (Top 5)
- Add table_top_providers_count.ui: provider call count ranking (Top 5)
- Update index.ui: integrate three new monitoring cards
  - User call ranking full-width card
  - Provider amount + count rankings side-by-side in HBox layout
2026-05-25 21:48:59 +08:00
be1ac95ac7 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
2026-05-25 18:49:25 +08:00
50 changed files with 4648 additions and 606 deletions

View File

@ -1,31 +1,96 @@
"""Dashboard data functions - exposed via load_dashboard() on ServerEnv"""
# Force re-sync: file was truncated in working copy on some deployments
from ahserver.serverenv import ServerEnv
from sqlor.dbpools import get_sor_context
from datetime import datetime, timedelta, date
async def get_today_usage(request):
"""获取当天llmusage笔数"""
"""获取当天llmusage笔数(排除记账失败)"""
env = request._run_ns
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = "SELECT COUNT(*) as cnt FROM llmusage WHERE use_date = ${today}$"
sql = "SELECT COUNT(*) as cnt FROM llmusage WHERE use_date = ${today}$ AND accounting_status = 'accounted'"
recs = await sor.sqlExe(sql, {'today': today})
cnt = int(recs[0].get('cnt', 0)) if recs else 0
return cnt
async def get_today_amount(request):
"""获取当天交易金额"""
"""获取当天交易金额(排除记账失败)"""
env = request._run_ns
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = "SELECT COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$"
sql = "SELECT COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$ AND accounting_status = 'accounted'"
recs = await sor.sqlExe(sql, {'today': today})
amount = float(recs[0].get('total_amount', 0)) if recs else 0.0
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}$ AND accounting_status = 'accounted'"
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}$ AND accounting_status = 'accounted'"
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}$ AND accounting_status = 'accounted'"
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}$ AND accounting_status = 'accounted'"
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
@ -70,7 +135,7 @@ async def get_top_models(request):
WHERE a.use_date = ${today}$
GROUP BY a.llmid, b.name
ORDER BY cnt DESC
LIMIT 3
LIMIT 5
"""
recs = await sor.sqlExe(sql, {'today': today})
result = []
@ -95,9 +160,8 @@ async def get_accounting_errors(request):
async def get_top_users_by_amount(request):
"""获取当天用户金额前5"""
"""获取用户金额前5(全量)"""
env = request._run_ns
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
@ -106,12 +170,11 @@ async def get_top_users_by_amount(request):
COUNT(*) as cnt
FROM llmusage a
LEFT JOIN users b ON a.userid = b.id
WHERE a.use_date = ${today}$
GROUP BY a.userid, b.nick_name, b.username
ORDER BY total_amount DESC
LIMIT 5
"""
recs = await sor.sqlExe(sql, {'today': today})
recs = await sor.sqlExe(sql, {})
result = []
for r in recs:
result.append({
@ -123,9 +186,8 @@ async def get_top_users_by_amount(request):
async def get_top_users_by_count(request):
"""获取当天用户笔数前5"""
"""获取用户笔数前5(全量)"""
env = request._run_ns
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
@ -134,12 +196,11 @@ async def get_top_users_by_count(request):
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN users b ON a.userid = b.id
WHERE a.use_date = ${today}$
GROUP BY a.userid, b.nick_name, b.username
ORDER BY cnt DESC
LIMIT 5
"""
recs = await sor.sqlExe(sql, {'today': today})
recs = await sor.sqlExe(sql, {})
result = []
for r in recs:
result.append({
@ -151,9 +212,8 @@ async def get_top_users_by_count(request):
async def get_top_providers_by_amount(request):
"""获取模型供应商金额前5"""
"""获取模型供应商金额前5(全量)"""
env = request._run_ns
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
@ -163,12 +223,11 @@ async def get_top_providers_by_amount(request):
FROM llmusage a
LEFT JOIN llm b ON a.llmid = b.id
LEFT JOIN organization c ON b.providerid = c.id
WHERE a.use_date = ${today}$
GROUP BY b.providerid, c.orgname
ORDER BY total_amount DESC
LIMIT 5
"""
recs = await sor.sqlExe(sql, {'today': today})
recs = await sor.sqlExe(sql, {})
result = []
for r in recs:
result.append({
@ -180,9 +239,8 @@ async def get_top_providers_by_amount(request):
async def get_top_providers_by_count(request):
"""获取模型供应商笔数前5"""
"""获取模型供应商笔数前5(全量)"""
env = request._run_ns
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
@ -192,12 +250,11 @@ async def get_top_providers_by_count(request):
FROM llmusage a
LEFT JOIN llm b ON a.llmid = b.id
LEFT JOIN organization c ON b.providerid = c.id
WHERE a.use_date = ${today}$
GROUP BY b.providerid, c.orgname
ORDER BY cnt DESC
LIMIT 5
"""
recs = await sor.sqlExe(sql, {'today': today})
recs = await sor.sqlExe(sql, {})
result = []
for r in recs:
result.append({
@ -208,11 +265,334 @@ async def get_top_providers_by_count(request):
return result
async def get_top_users_combined(request):
"""Top 5 users by amount with count - for combined ChartBar"""
env = request._run_ns
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COALESCE(b.nick_name, b.username) as user_name,
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN users b ON a.userid = b.id
GROUP BY a.userid, b.nick_name, b.username
ORDER BY total_amount DESC
LIMIT 5
"""
recs = await sor.sqlExe(sql, {})
result = []
for r in recs:
result.append({
'user_name': r.get('user_name', 'Unknown'),
'cnt': int(r.get('cnt', 0)),
'total_amount': round(float(r.get('total_amount', 0)), 2)
})
return result
async def get_top_providers_combined(request):
"""Top 5 providers by amount with count - for combined ChartBar"""
env = request._run_ns
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COALESCE(c.orgname, 'Unknown') as provider_name,
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN llm b ON a.llmid = b.id
LEFT JOIN organization c ON b.providerid = c.id
GROUP BY b.providerid, c.orgname
ORDER BY total_amount DESC
LIMIT 5
"""
recs = await sor.sqlExe(sql, {})
result = []
for r in recs:
result.append({
'provider_name': r.get('provider_name', 'Unknown'),
'cnt': int(r.get('cnt', 0)),
'total_amount': round(float(r.get('total_amount', 0)), 2)
})
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_at >= ${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
async def get_user_today_models(request):
"""获取当前用户当天各模型调用次数和金额(用户级监控项)"""
env = request._run_ns
userid = await env.get_user()
if not userid:
return []
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COALESCE(b.name, 'Unknown') as model_name,
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN llm b ON a.llmid = b.id
WHERE a.use_date = ${today}$
AND a.userid = ${userid}$
GROUP BY a.llmid, b.name
ORDER BY cnt DESC
"""
recs = await sor.sqlExe(sql, {'today': today, 'userid': userid})
result = []
for r in recs:
result.append({
'model_name': r.get('model_name', 'Unknown'),
'cnt': int(r.get('cnt', 0)),
'total_amount': round(float(r.get('total_amount', 0)), 4)
})
return result
async def get_all_today_models(request):
"""获取所有用户当天各模型调用次数和金额(全局监控项)"""
env = request._run_ns
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COALESCE(b.name, 'Unknown') as model_name,
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN llm b ON a.llmid = b.id
WHERE a.use_date = ${today}$
GROUP BY a.llmid, b.name
ORDER BY cnt DESC
"""
recs = await sor.sqlExe(sql, {'today': today})
result = []
for r in recs:
result.append({
'model_name': r.get('model_name', 'Unknown'),
'cnt': int(r.get('cnt', 0)),
'total_amount': round(float(r.get('total_amount', 0)), 4)
})
return result
# ── Customer (org-level) monitoring functions ──
async def _get_org_id(request):
"""Helper: get current user's org_id from request context."""
env = request._run_ns
org_id = await env.get_userorgid()
return org_id or '0'
async def get_customer_daily_models(request):
"""获取当前客户组织当天各模型调用次数和金额"""
env = request._run_ns
org_id = await _get_org_id(request)
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COALESCE(b.name, 'Unknown') as model_name,
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN llm b ON a.llmid = b.id
WHERE a.use_date = ${today}$
AND a.userorgid = ${org_id}$
GROUP BY a.llmid, b.name
ORDER BY cnt DESC
"""
recs = await sor.sqlExe(sql, {'today': today, 'org_id': org_id})
result = []
for r in recs:
result.append({
'model_name': r.get('model_name', 'Unknown'),
'cnt': int(r.get('cnt', 0)),
'total_amount': round(float(r.get('total_amount', 0)), 4)
})
return result
async def get_customer_monthly_models(request):
"""获取当前客户组织当月各模型调用次数和金额"""
env = request._run_ns
org_id = await _get_org_id(request)
now = datetime.now()
month_start = now.strftime('%Y-%m-01')
if now.month == 12:
month_end = f'{now.year + 1}-01-01'
else:
month_end = f'{now.year}-{now.month + 1:02d}-01'
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COALESCE(b.name, 'Unknown') as model_name,
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN llm b ON a.llmid = b.id
WHERE a.use_date >= ${month_start}$
AND a.use_date < ${month_end}$
AND a.userorgid = ${org_id}$
GROUP BY a.llmid, b.name
ORDER BY cnt DESC
"""
recs = await sor.sqlExe(sql, {
'month_start': month_start,
'month_end': month_end,
'org_id': org_id
})
result = []
for r in recs:
result.append({
'model_name': r.get('model_name', 'Unknown'),
'cnt': int(r.get('cnt', 0)),
'total_amount': round(float(r.get('total_amount', 0)), 4)
})
return result
async def get_customer_daily_summary(request):
"""获取当前客户组织今日汇总(调用次数+金额)"""
env = request._run_ns
org_id = await _get_org_id(request)
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
WHERE a.use_date = ${today}$
AND a.userorgid = ${org_id}$
"""
recs = await sor.sqlExe(sql, {'today': today, 'org_id': org_id})
if recs:
return {
'cnt': int(recs[0].get('cnt', 0)),
'total_amount': round(float(recs[0].get('total_amount', 0)), 4)
}
return {'cnt': 0, 'total_amount': 0}
async def get_customer_month_summary(request):
"""获取当前客户组织当月汇总(调用次数+金额)"""
env = request._run_ns
org_id = await _get_org_id(request)
now = datetime.now()
month_start = now.strftime('%Y-%m-01')
if now.month == 12:
month_end = f'{now.year + 1}-01-01'
else:
month_end = f'{now.year}-{now.month + 1:02d}-01'
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
WHERE a.use_date >= ${month_start}$
AND a.use_date < ${month_end}$
AND a.userorgid = ${org_id}$
"""
recs = await sor.sqlExe(sql, {
'month_start': month_start,
'month_end': month_end,
'org_id': org_id
})
if recs:
return {
'cnt': int(recs[0].get('cnt', 0)),
'total_amount': round(float(recs[0].get('total_amount', 0)), 4)
}
return {'cnt': 0, 'total_amount': 0}
async def get_customer_daily_trend(request):
"""获取当前客户组织当月每日调用趋势(每天的调用次数和金额)"""
env = request._run_ns
org_id = await _get_org_id(request)
now = datetime.now()
month_start = now.strftime('%Y-%m-01')
if now.month == 12:
month_end = f'{now.year + 1}-01-01'
else:
month_end = f'{now.year}-{now.month + 1:02d}-01'
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
a.use_date as date,
COUNT(*) as cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
WHERE a.use_date >= ${month_start}$
AND a.use_date < ${month_end}$
AND a.userorgid = ${org_id}$
GROUP BY a.use_date
ORDER BY a.use_date ASC
"""
recs = await sor.sqlExe(sql, {
'month_start': month_start,
'month_end': month_end,
'org_id': org_id
})
result = []
for r in recs:
result.append({
'date': r.get('date', ''),
'cnt': int(r.get('cnt', 0)),
'total_amount': round(float(r.get('total_amount', 0)), 4)
})
return result
def load_dashboard():
"""Register dashboard functions on ServerEnv"""
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
@ -221,3 +601,15 @@ 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_top_users_combined = get_top_users_combined
g.get_top_providers_combined = get_top_providers_combined
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
g.get_user_today_models = get_user_today_models
g.get_all_today_models = get_all_today_models
g.get_customer_daily_models = get_customer_daily_models
g.get_customer_monthly_models = get_customer_monthly_models
g.get_customer_daily_summary = get_customer_daily_summary
g.get_customer_month_summary = get_customer_month_summary
g.get_customer_daily_trend = get_customer_daily_trend

38
i18n/en/msg.txt Normal file
View File

@ -0,0 +1,38 @@
Cancel: Cancel
Conform: Confirm
Discard: Discard
Reset: Reset
Submit: Submit
今日交易金额: Today's Transaction Amount
今日各模型调用: Today's Model Calls
今日活跃用户: Today's Active Users
今日消费金额: Today's Spending Amount
今日调用次数: Today's Call Count
今日调用笔数: Today's Transaction Count
供应商排行: Supplier Ranking
刷新: Refresh
在线用户: Online Users
客户专属监控: Customer Dedicated Monitoring
当前并发用户: Current Concurrent Users
我的今日模型使用: My Today's Model Usage
排名: Ranking
数据概览: Data Overview
数据看板: Data Dashboard
暂无数据: No Data Available
本月各模型调用: This Month's Model Calls
本月新增用户: This Month's New Users
本月每日调用趋势: This Month's Daily Call Trend
本月消费金额: This Month's Spending Amount
本月调用次数: This Month's Call Count
查看本组织各模型每日/每月调用次数与金额统计: View Daily/Monthly Call Count and Amount Statistics for Each Model in Your Organization
热门模型: Popular Models
用户: User
用户总数: Total Users
用户排行: User Ranking
用户金额 TOP 5今日: User Amount TOP 5 (Today)
笔数: Transactions
组织机构数: Number of Organizations
记账异常: Billing Exception
记账错误笔数: Billing Error Count
返回首页: Back to Home
金额: Amount

38
i18n/jp/msg.txt Normal file
View File

@ -0,0 +1,38 @@
Cancel: キャンセル
Conform: 確認
Discard: 破棄
Reset: リセット
Submit: 送信
今日交易金额: 今日の取引金額
今日各模型调用: 今日の各モデル呼び出し
今日活跃用户: 今日のアクティブユーザー
今日消费金额: 今日の消費金額
今日调用次数: 今日の呼び出し回数
今日调用笔数: 今日の取引件数
供应商排行: サプライヤーランキング
刷新: 更新
在线用户: オンラインユーザー
客户专属监控: お客様専用モニタリング
当前并发用户: 現在の同時接続ユーザー
我的今日模型使用: 今日のモデル使用状況
排名: ランキング
数据概览: データ概要
数据看板: データダッシュボード
暂无数据: データなし
本月各模型调用: 今月の各モデル呼び出し
本月新增用户: 今月の新規ユーザー
本月每日调用趋势: 今月の日次呼び出しトレンド
本月消费金额: 今月の消費金額
本月调用次数: 今月の呼び出し回数
查看本组织各模型每日/每月调用次数与金额统计: 組織内の各モデルの日次/月次呼び出し回数と金額統計を表示
热门模型: 人気のモデル
用户: ユーザー
用户总数: ユーザー総数
用户排行: ユーザーランキング
用户金额 TOP 5今日: ユーザー金額 TOP 5今日
笔数: 取引件数
组织机构数: 組織数
记账异常: 請求異常
记账错误笔数: 請求エラー件数
返回首页: ホームに戻る
金额: 金額

38
i18n/ko/msg.txt Normal file
View File

@ -0,0 +1,38 @@
Cancel: 취소
Conform: 확인
Discard: 폐기
Reset: 초기화
Submit: 제출
今日交易金额: 오늘 거래 금액
今日各模型调用: 오늘 각 모델 호출
今日活跃用户: 오늘 활성 사용자
今日消费金额: 오늘 소비 금액
今日调用次数: 오늘 호출 횟수
今日调用笔数: 오늘 거래 건수
供应商排行: 공급업체 순위
刷新: 새로고침
在线用户: 온라인 사용자
客户专属监控: 고객 전용 모니터링
当前并发用户: 현재 동시 접속 사용자
我的今日模型使用: 나의 오늘 모델 사용량
排名: 순위
数据概览: 데이터 개요
数据看板: 데이터 대시보드
暂无数据: 데이터 없음
本月各模型调用: 이번 달 각 모델 호출
本月新增用户: 이번 달 신규 사용자
本月每日调用趋势: 이번 달 일별 호출 추세
本月消费金额: 이번 달 소비 금액
本月调用次数: 이번 달 호출 횟수
查看本组织各模型每日/每月调用次数与金额统计: 조직 내 각 모델의 일별/월별 호출 횟수 및 금액 통계 보기
热门模型: 인기 모델
用户: 사용자
用户总数: 전체 사용자 수
用户排行: 사용자 순위
用户金额 TOP 5今日: 사용자 금액 TOP 5 (오늘)
笔数: 거래 건수
组织机构数: 조직 수
记账异常: 과금 이상
记账错误笔数: 과금 오류 건수
返回首页: 홈으로 돌아가기
金额: 금액

38
i18n/zh/msg.txt Normal file
View File

@ -0,0 +1,38 @@
Cancel: Cancel
Conform: Conform
Discard: Discard
Reset: Reset
Submit: Submit
今日交易金额: 今日交易金额
今日各模型调用: 今日各模型调用
今日活跃用户: 今日活跃用户
今日消费金额: 今日消费金额
今日调用次数: 今日调用次数
今日调用笔数: 今日调用笔数
供应商排行: 供应商排行
刷新: 刷新
在线用户: 在线用户
客户专属监控: 客户专属监控
当前并发用户: 当前并发用户
我的今日模型使用: 我的今日模型使用
排名: 排名
数据概览: 数据概览
数据看板: 数据看板
暂无数据: 暂无数据
本月各模型调用: 本月各模型调用
本月新增用户: 本月新增用户
本月每日调用趋势: 本月每日调用趋势
本月消费金额: 本月消费金额
本月调用次数: 本月调用次数
查看本组织各模型每日/每月调用次数与金额统计: 查看本组织各模型每日/每月调用次数与金额统计
热门模型: 热门模型
用户: 用户
用户总数: 用户总数
用户排行: 用户排行
用户金额 TOP 5今日: 用户金额 TOP 5今日
笔数: 笔数
组织机构数: 组织机构数
记账异常: 记账异常
记账错误笔数: 记账错误笔数
返回首页: 返回首页
金额: 金额

3
json/build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
xls2ui -m ../models -o ../wwwroot dashboard_for_sage *.json

158
scripts/load_path.py Normal file
View File

@ -0,0 +1,158 @@
"""Generate RBAC permissions for dashboard_for_sage module paths.
Run from Sage root with Sage venv:
cd ~/repos/sage && ./py3/bin/python ../dashboard_for_sage/scripts/load_path.py
Or set SAGE_ROOT environment variable.
"""
import os
import sys
import asyncio
# Ensure Sage root is in path
sage_root = os.environ.get('SAGE_ROOT')
if sage_root and sage_root not in sys.path:
sys.path.insert(0, sage_root)
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from appPublic.dictObject import DictObject
from appPublic.uniqueID import getID
# ── Permission definitions ──
# Format: (path, role)
# Dashboard files are accessible to all logined users except menu.ui (any)
paths = [
# Module root and index
("/dashboard_for_sage", "logined"),
("/dashboard_for_sage/index.ui", "logined"),
# Menu — must be any so unauthenticated users can see nav
("/dashboard_for_sage/menu.ui", "any"),
# Shell
("/dashboard_for_sage/shell.ui", "logined"),
("/dashboard_for_sage/shell_theme.css", "any"),
("/dashboard_for_sage/shell_theme.js", "any"),
# Global menu
("/dashboard_for_sage/global_menu.ui", "logined"),
# Stat cards
("/dashboard_for_sage/stat_today_usage.ui", "logined"),
("/dashboard_for_sage/stat_today_amount.ui", "logined"),
("/dashboard_for_sage/stat_total_users.ui", "logined"),
("/dashboard_for_sage/stat_active_users.ui", "logined"),
("/dashboard_for_sage/stat_concurrent.ui", "logined"),
("/dashboard_for_sage/stat_errors.ui", "logined"),
("/dashboard_for_sage/stat_new_users_month.ui", "logined"),
("/dashboard_for_sage/stat_total_orgs.ui", "logined"),
# Legacy stat cards (backward compat)
("/dashboard_for_sage/today_usage.ui", "logined"),
("/dashboard_for_sage/today_amount.ui", "logined"),
("/dashboard_for_sage/total_users.ui", "logined"),
("/dashboard_for_sage/concurrent_users.ui", "logined"),
("/dashboard_for_sage/accounting_errors.ui", "logined"),
# Top 5 ranking cards
("/dashboard_for_sage/table_top_users.ui", "logined"),
("/dashboard_for_sage/table_top_users_amount.ui", "logined"),
("/dashboard_for_sage/table_top_users_count.ui", "logined"),
("/dashboard_for_sage/table_top_providers_amount.ui", "logined"),
("/dashboard_for_sage/table_top_providers_count.ui", "logined"),
("/dashboard_for_sage/top_users_amount.ui", "logined"),
# API doc
("/dashboard_for_sage/api_doc.ui", "logined"),
("/dashboard_for_sage/api_doc.md", "logined"),
# Charts
("/dashboard_for_sage/chart_top_models.ui", "logined"),
("/dashboard_for_sage/top_models_chart.ui", "logined"),
("/dashboard_for_sage/user_today_models_chart.ui", "logined"),
("/dashboard_for_sage/all_today_models_chart.ui", "logined"),
# Customer monitoring
("/dashboard_for_sage/customer_usage.ui", "logined"),
("/dashboard_for_sage/customer_daily_chart.ui", "logined"),
("/dashboard_for_sage/customer_monthly_chart.ui", "logined"),
("/dashboard_for_sage/customer_daily_trend.ui", "logined"),
# API endpoints
("/dashboard_for_sage/api/top_models.dspy", "logined"),
("/dashboard_for_sage/api/user_today_models.dspy", "logined"),
("/dashboard_for_sage/api/all_today_models.dspy", "logined"),
("/dashboard_for_sage/api/customer_daily_models.dspy", "logined"),
("/dashboard_for_sage/api/customer_monthly_models.dspy", "logined"),
("/dashboard_for_sage/api/customer_daily_trend.dspy", "logined"),
]
async def add_roleperm(sor, roleid, permid):
"""Add role-permission mapping if not exists."""
ns = {'roleid': roleid, 'permid': permid}
recs = await sor.R('rolepermission', ns.copy())
if not recs:
ns['id'] = getID()
await sor.C('rolepermission', ns.copy())
async def add_roles_perm(sor, perm, roles):
"""Register permission for special roles."""
if roles in [['any'], ['anonymous'], ['logined']]:
role = roles[0]
await add_roleperm(sor, role, perm.id)
return
for role in roles:
if '.' in role:
orgtypeid, name = role.split('.', 1)
else:
orgtypeid, name = '*', role
ns = {'orgtypeid': orgtypeid, 'name': name}
roles_rec = await sor.R('role', ns.copy())
if not roles_rec:
ns['id'] = getID()
await sor.C('role', ns.copy())
else:
ns['id'] = roles_rec[0].id
await add_roleperm(sor, ns['id'], perm.id)
# Remove 'any' fallback for this perm
ns_any = {'roleid': 'any', 'permid': perm.id}
existing = await sor.R('rolepermission', ns_any.copy())
if existing:
await sor.D('rolepermission', {'id': existing[0].id})
async def main():
config = getConfig('.')
db = DBPools(config.databases)
cnt = 0
async with db.sqlorContext('sage') as sor:
for path, role in paths:
ns = {'path': path}
recs = await sor.R('permission', ns.copy())
if recs:
# Permission exists, skip (idempotent)
continue
cnt += 1
pid = getID()
ns['id'] = pid
await sor.C('permission', ns.copy())
perm = DictObject(**ns)
await add_roles_perm(sor, perm, [role])
print(f'{cnt} path(s) inserted for dashboard_for_sage')
if cnt == 0:
print('All paths already registered — no changes needed.')
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

View File

@ -12,10 +12,11 @@
{
"widgettype": "Text",
"options": {
"text": "记账错误笔数",
"fontSize": "14px",
"color": "#888",
"marginBottom": "8px"
"marginBottom": "8px",
"otext": "记账错误笔数",
"i18n": true
}
},
{
@ -29,4 +30,4 @@
}
}
]
}
}

View File

@ -0,0 +1,11 @@
{
"widgettype": "ChartBar",
"options": {
"height": "280px",
"width": "100%",
"data_url": "{{entire_url('api/all_today_models.dspy')}}",
"nameField": "model_name",
"valueFields": ["cnt", "total_amount"],
"refresh_period": 30
}
}

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""All users' today model usage data API for ChartBar"""
import json
models = await get_all_today_models(request)
return json.dumps(models, ensure_ascii=False, default=str)

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Customer daily per-model usage data API for ChartBar"""
import json
models = await get_customer_daily_models(request)
return json.dumps(models, ensure_ascii=False, default=str)

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Customer daily trend data API for ChartLine (current month)"""
import json
trend = await get_customer_daily_trend(request)
return json.dumps(trend, ensure_ascii=False, default=str)

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Customer monthly per-model usage data API for ChartBar"""
import json
models = await get_customer_monthly_models(request)
return json.dumps(models, ensure_ascii=False, default=str)

View File

@ -3,4 +3,4 @@
import json
models = await get_top_models(request)
print(json.dumps(models))
return json.dumps(models, ensure_ascii=False, default=str)

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Top providers data API for ChartBar"""
import json
providers = await get_top_providers_combined(request)
return json.dumps(providers, ensure_ascii=False, default=str)

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Top users data API for ChartBar"""
import json
users = await get_top_users_combined(request)
return json.dumps(users, ensure_ascii=False, default=str)

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""User's today model usage data API for ChartBar"""
import json
models = await get_user_today_models(request)
return json.dumps(models, ensure_ascii=False, default=str)

1783
wwwroot/api_doc.md Normal file

File diff suppressed because it is too large Load Diff

7
wwwroot/api_doc.ui Normal file
View File

@ -0,0 +1,7 @@
{
"widgettype": "ApiDoc",
"options": {
"md_url": "{{entire_url('api_doc.md')}}",
"height": "100%"
}
}

View File

@ -1,10 +1,10 @@
{
"widgettype": "ChartBar",
"options": {
"height": "280px",
"height": "250px",
"width": "100%",
"data_url": "{{entire_url('api/top_models.dspy')}}",
"nameField": "model_name",
"valueFields": ["cnt", "total_amount"]
"valueFields": ["total_amount", "cnt"]
}
}

View File

@ -0,0 +1,9 @@
{
"widgettype": "ChartBar",
"options": {
"height": "250px",
"data_url": "{{entire_url('api/top_providers.dspy')}}",
"nameField": "provider_name",
"valueFields": ["total_amount", "cnt"]
}
}

View File

@ -0,0 +1,9 @@
{
"widgettype": "ChartBar",
"options": {
"height": "250px",
"data_url": "{{entire_url('api/top_users.dspy')}}",
"nameField": "user_name",
"valueFields": ["total_amount", "cnt"]
}
}

View File

@ -1,7 +1,7 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"bgcolor": "var(--sage-bg-card, #FFFFFF)",
"padding": "24px",
"borderRadius": "8px",
"flex": "1",
@ -12,10 +12,11 @@
{
"widgettype": "Text",
"options": {
"text": "当前并发用户",
"fontSize": "14px",
"color": "#888",
"marginBottom": "8px"
"marginBottom": "8px",
"otext": "当前并发用户",
"i18n": true
}
},
{
@ -29,4 +30,4 @@
}
}
]
}
}

View File

@ -0,0 +1,11 @@
{
"widgettype": "ChartBar",
"options": {
"height": "300px",
"width": "100%",
"data_url": "{{entire_url('api/customer_daily_models.dspy')}}",
"nameField": "model_name",
"valueFields": ["cnt", "total_amount"],
"refresh_period": 60
}
}

View File

@ -0,0 +1,11 @@
{
"widgettype": "ChartLine",
"options": {
"height": "280px",
"width": "100%",
"data_url": "{{entire_url('api/customer_daily_trend.dspy')}}",
"nameField": "date",
"valueFields": ["cnt", "total_amount"],
"refresh_period": 120
}
}

View File

@ -0,0 +1,11 @@
{
"widgettype": "ChartBar",
"options": {
"height": "300px",
"width": "100%",
"data_url": "{{entire_url('api/customer_monthly_models.dspy')}}",
"nameField": "model_name",
"valueFields": ["cnt", "total_amount"],
"refresh_period": 120
}
}

462
wwwroot/customer_usage.ui Normal file
View File

@ -0,0 +1,462 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%"
},
"subwidgets": [
{
"widgettype": "VScrollPanel",
"options": {
"css": "filler"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "24px"
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
"fontWeight": "700",
"otext": "客户专属监控",
"i18n": true
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"label": "返回首页",
"borderRadius": "6px",
"padding": "6px 16px",
"fontSize": "13px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/dashboard_for_sage/index.ui')}}"
},
"mode": "replace"
}
]
}
]
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "16px",
"minWidth": "220px",
"marginBottom": "24px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"css": "stat-card",
"padding": "20px",
"borderRadius": "12px",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M3 3v18h18\"/><path d=\"M18 17V9\"/><path d=\"M13 17V5\"/><path d=\"M8 17v-3\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_customer_daily_summary(request).cnt}}",
"fontSize": "32px",
"fontWeight": "700",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"css": "stat-label",
"fontSize": "14px",
"marginTop": "4px",
"otext": "今日调用次数",
"i18n": true
}
}
]
},
{
"widgettype": "VBox",
"options": {
"css": "stat-card",
"padding": "20px",
"borderRadius": "12px",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"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"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "¥{{get_customer_daily_summary(request).total_amount|round(2)}}",
"fontSize": "32px",
"fontWeight": "700",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"css": "stat-label",
"fontSize": "14px",
"marginTop": "4px",
"otext": "今日消费金额",
"i18n": true
}
}
]
},
{
"widgettype": "VBox",
"options": {
"css": "stat-card",
"padding": "20px",
"borderRadius": "12px",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"/><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"/><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_customer_month_summary(request).cnt}}",
"fontSize": "32px",
"fontWeight": "700",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"css": "stat-label",
"fontSize": "14px",
"marginTop": "4px",
"otext": "本月调用次数",
"i18n": true
}
}
]
},
{
"widgettype": "VBox",
"options": {
"css": "stat-card",
"padding": "20px",
"borderRadius": "12px",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"23\"/><path d=\"M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "¥{{get_customer_month_summary(request).total_amount|round(2)}}",
"fontSize": "32px",
"fontWeight": "700",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"css": "stat-label",
"fontSize": "14px",
"marginTop": "4px",
"otext": "本月消费金额",
"i18n": true
}
}
]
}
]
},
{
"widgettype": "VBox",
"options": {
"css": "card",
"width": "100%",
"borderRadius": "12px",
"padding": "20px",
"marginBottom": "20px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"fontWeight": "600",
"otext": "本月每日调用趋势",
"i18n": true
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"label": "刷新",
"border": "none",
"borderRadius": "6px",
"padding": "4px 12px",
"fontSize": "12px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "method",
"target": "-@ChartLine",
"method": "render_urldata",
"params": {}
}
]
}
]
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('customer_daily_trend.ui')}}"
}
}
]
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"gap": "20px",
"marginBottom": "20px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"css": "card",
"width": "50%",
"borderRadius": "12px",
"padding": "20px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"fontWeight": "600",
"otext": "今日各模型调用",
"i18n": true
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"label": "刷新",
"border": "none",
"borderRadius": "6px",
"padding": "4px 12px",
"fontSize": "12px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "method",
"target": "-@ChartBar",
"method": "render_urldata",
"params": {}
}
]
}
]
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('customer_daily_chart.ui')}}"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"css": "card",
"width": "50%",
"borderRadius": "12px",
"padding": "20px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"fontWeight": "600",
"otext": "本月各模型调用",
"i18n": true
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"label": "刷新",
"border": "none",
"borderRadius": "6px",
"padding": "4px 12px",
"fontSize": "12px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "method",
"target": "-@ChartBar",
"method": "render_urldata",
"params": {}
}
]
}
]
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('customer_monthly_chart.ui')}}"
}
}
]
}
]
}
]
}
]
}

View File

@ -1,58 +0,0 @@
{% set roles = get_user_roles(get_user()) %}
{
"widgettype": "Menu",
"id": "global_nav_menu",
"options": {
"width": "100%",
"height": "100%",
"bgcolor": "#111827",
"items": [
{
"name": "dashboard",
"label": "仪表盘",
"icon": "fa fa-dashboard",
"url": "{{entire_url('index.ui')}}",
"target": "app.sage_main_content"
},
{% if get_user() %}
{
"name": "llmage",
"label": "LLM 模型管理",
"icon": "fa fa-brain",
"submenu": "{{entire_url('/llmage/menu.ui')}}"
},
{
"name": "rag",
"label": "知识库管理",
"icon": "fa fa-database",
"url": "{{entire_url('/rag/menu.ui')}}",
"target": "app.sage_main_content"
},
{% endif %}
{% if 'reseller.operator' in roles or 'owner.superuser' in roles %}
{
"name": "platformbiz",
"label": "平台业务",
"icon": "fa fa-building",
"submenu": "{{entire_url('/platformbiz/menu.ui')}}"
},
{% endif %}
{% if get_user() %}
{
"name": "rbac",
"label": "用户与权限",
"icon": "fa fa-users",
"submenu": "{{entire_url('/rbac/admin_menu.ui')}}"
},
{% endif %}
{
"name": "hermes_web_cli",
"label": "AI Agent",
"icon": "fa fa-robot",
"url": "{{entire_url('/hermes-web-cli/index.ui')}}",
"target": "app.sage_main_content"
}
],
"menuitem_css": "menuitem"
}
}

View File

@ -1,24 +1,33 @@
{% set roles = get_user_roles(get_user()) %}
{
"widgettype": "VBox",
"id": "dashboard_root",
"options": {
"width": "100%",
"height": "100%"
"height": "100%",
"bgcolor": "var(--sage-bg-primary, transparent)"
},
"subwidgets": [
{
"widgettype": "VScrollPanel",
"options": {
"css": "filler"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "24px"
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
"text": "数据概览",
"color": "#F1F5F9",
"fontWeight": "700"
"fontWeight": "700",
"otext": "数据概览",
"i18n": true
}
},
{
@ -27,19 +36,40 @@
{
"widgettype": "Text",
"options": {
"text": "最后更新: {{get_today_usage(request) and request._run_ns.curDateString() or ''}}",
"fontSize": "13px",
"color": "#64748B"
"text": "{{get_today_usage(request) and request._run_ns.curDateString() or ''}}",
"fontSize": "13px"
}
},
{
"widgettype": "Button",
"options": {
"label": "⛶",
"border": "none",
"borderRadius": "6px",
"padding": "4px 12px",
"fontSize": "16px",
"marginLeft": "12px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "var el = document.getElementById('dashboard_root'); if (!document.fullscreenElement) { (el || document.documentElement).requestFullscreen(); } else { document.exitFullscreen(); }"
}
]
}
]
},
{
"widgettype": "ResponsableBox",
}
{% if 'owner.*' in roles or 'reseller.*' in roles %}
,{
"widgettype": "HBox",
"options": {
"width": "100%",
"gap": "16px",
"minWidth": "220px",
"marginBottom": "24px"
"marginBottom": "16px"
},
"subwidgets": [
{
@ -47,7 +77,8 @@
"id": "stat_today_usage",
"options": {
"period_seconds": 30,
"url": "{{entire_url('stat_today_usage.ui')}}"
"url": "{{entire_url('stat_today_usage.ui')}}",
"width": "50%"
}
},
{
@ -55,31 +86,8 @@
"id": "stat_today_amount",
"options": {
"period_seconds": 30,
"url": "{{entire_url('stat_today_amount.ui')}}"
}
},
{
"widgettype": "RefreshWidget",
"id": "stat_total_users",
"options": {
"period_seconds": 60,
"url": "{{entire_url('stat_total_users.ui')}}"
}
},
{
"widgettype": "RefreshWidget",
"id": "stat_concurrent",
"options": {
"period_seconds": 15,
"url": "{{entire_url('stat_concurrent.ui')}}"
}
},
{
"widgettype": "RefreshWidget",
"id": "stat_errors",
"options": {
"period_seconds": 30,
"url": "{{entire_url('stat_errors.ui')}}"
"url": "{{entire_url('stat_today_amount.ui')}}",
"width": "50%"
}
}
]
@ -88,18 +96,63 @@
"widgettype": "HBox",
"options": {
"width": "100%",
"gap": "20px",
"gap": "16px",
"marginBottom": "16px"
},
"subwidgets": [
{
"widgettype": "RefreshWidget",
"id": "stat_total_users",
"options": {
"period_seconds": 60,
"url": "{{entire_url('stat_total_users.ui')}}",
"width": "25%"
}
},
{
"widgettype": "RefreshWidget",
"id": "stat_active_users",
"options": {
"period_seconds": 60,
"url": "{{entire_url('stat_active_users.ui')}}",
"width": "25%"
}
},
{
"widgettype": "RefreshWidget",
"id": "stat_concurrent",
"options": {
"period_seconds": 15,
"url": "{{entire_url('stat_concurrent.ui')}}",
"width": "25%"
}
},
{
"widgettype": "RefreshWidget",
"id": "stat_errors",
"options": {
"period_seconds": 30,
"url": "{{entire_url('stat_errors.ui')}}",
"width": "25%"
}
}
]
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"gap": "16px",
"height": "auto"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"width": "60%",
"bgcolor": "#1E293B",
"css": "card",
"width": "33%",
"borderRadius": "12px",
"padding": "20px",
"border": "1px solid #334155"
"padding": "16px"
},
"subwidgets": [
{
@ -107,41 +160,17 @@
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "16px"
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "Top 3 模型(今日调用)",
"color": "#F1F5F9",
"fontWeight": "600"
"fontWeight": "600",
"marginBottom": "12px",
"otext": "热门模型",
"i18n": true
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"label": "刷新",
"bgcolor": "#334155",
"color": "#94A3B8",
"border": "none",
"borderRadius": "6px",
"padding": "4px 12px",
"fontSize": "12px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "method",
"target": "-@RefreshWidget",
"method": "render_urldata",
"params": {}
}
]
}
]
},
@ -158,220 +187,202 @@
{
"widgettype": "VBox",
"options": {
"width": "40%",
"bgcolor": "#1E293B",
"css": "card",
"width": "33%",
"borderRadius": "12px",
"padding": "20px",
"border": "1px solid #334155"
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "快捷入口",
"color": "#F1F5F9",
"fontWeight": "600",
"marginBottom": "16px"
"marginBottom": "12px",
"otext": "用户排行",
"i18n": true
}
},
{
"widgettype": "ResponsableBox",
"widgettype": "RefreshWidget",
"id": "chart_top_users",
"options": {
"gap": "12px",
"minWidth": "120px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/llmage/llm')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#90caf9\" stroke-width=\"2\"><path d=\"M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15.75c-2.062 0-4.024-.614-5.67-1.757l-1.57-.393m15.04 0L12 21 5.25 13.893\"/></svg>"
}
},
{
"widgettype": "Text",
"options": {
"text": "模型管理",
"color": "#E2E8F0",
"fontSize": "13px",
"marginTop": "8px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/rbac/users')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#4caf50\" 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>"
}
},
{
"widgettype": "Text",
"options": {
"text": "用户管理",
"color": "#E2E8F0",
"fontSize": "13px",
"marginTop": "8px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/rag/kdb')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#f59e0b\" stroke-width=\"2\"><path d=\"M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125\"/></svg>"
}
},
{
"widgettype": "Text",
"options": {
"text": "知识库",
"color": "#E2E8F0",
"fontSize": "13px",
"marginTop": "8px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#334155",
"padding": "16px",
"borderRadius": "8px",
"cursor": "pointer",
"textAlign": "center"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/llmage/failed_accounting.ui')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ef4444\" stroke-width=\"2\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></svg>"
}
},
{
"widgettype": "Text",
"options": {
"text": "异常记录",
"color": "#E2E8F0",
"fontSize": "13px",
"marginTop": "8px"
}
}
]
}
]
"period_seconds": 60,
"url": "{{entire_url('chart_top_users.ui')}}"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"css": "card",
"width": "33%",
"borderRadius": "12px",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"fontWeight": "600",
"marginBottom": "12px",
"otext": "供应商排行",
"i18n": true
}
},
{
"widgettype": "RefreshWidget",
"id": "chart_top_providers",
"options": {
"period_seconds": 60,
"url": "{{entire_url('chart_top_providers.ui')}}"
}
}
]
}
]
},
{
}
{% endif %}
{% if 'customer.*' in roles %}
,{
"widgettype": "VBox",
"options": {
"css": "card",
"width": "100%",
"bgcolor": "#1E293B",
"borderRadius": "12px",
"padding": "20px",
"border": "1px solid #334155",
"marginTop": "20px"
},
"subwidgets": [
{
"widgettype": "Title4",
"widgettype": "HBox",
"options": {
"text": "用户消费排行Top 5",
"color": "#F1F5F9",
"fontWeight": "600",
"width": "100%",
"alignItems": "center",
"marginBottom": "16px"
}
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"fontWeight": "600",
"otext": "我的今日模型使用",
"i18n": true
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"label": "刷新",
"border": "none",
"borderRadius": "6px",
"padding": "4px 12px",
"fontSize": "12px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "method",
"target": "-@ChartBar",
"method": "render_urldata",
"params": {}
}
]
}
]
},
{
"widgettype": "RefreshWidget",
"id": "table_top_users",
"widgettype": "urlwidget",
"options": {
"period_seconds": 30,
"url": "{{entire_url('table_top_users.ui')}}"
"url": "{{entire_url('user_today_models_chart.ui')}}"
}
}
]
}
{% endif %}
{% if 'owner.*' not in roles and 'reseller.*' not in roles and 'customer.*' in roles %}
,{
"widgettype": "VBox",
"options": {
"css": "card",
"width": "100%",
"borderRadius": "12px",
"padding": "20px",
"marginTop": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/dashboard_for_sage/customer_usage.ui')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M3 3v18h18\"/><path d=\"M18 17V9\"/><path d=\"M13 17V5\"/><path d=\"M8 17v-3\"/></svg>",
"width": "28px",
"height": "28px",
"marginRight": "12px"
}
},
{
"widgettype": "VBox",
"options": {},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"fontWeight": "600",
"otext": "客户专属监控",
"i18n": true
}
},
{
"widgettype": "Text",
"options": {
"fontSize": "13px",
"otext": "查看本组织各模型每日/每月调用次数与金额统计",
"i18n": true
}
}
]
},
{
"widgettype": "Filler"
},
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"9 18 15 12 9 6\"/></svg>",
"width": "24px",
"height": "24px"
}
}
]
}
]
}
{% endif %}
]
}
]
}

View File

@ -1,147 +0,0 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"bgcolor": "#0B1120"
},
"subwidgets": [
{
"widgettype": "Html",
"options": {
"html": "<link rel=\"stylesheet\" href=\"/dashboard_for_sage/shell_theme.css\"><script src=\"/dashboard_for_sage/shell_theme.js\"><\\/script>"
}
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"height": "56px",
"bgcolor": "#111827",
"borderBottom": "1px solid #334155",
"padding": "0 16px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Button",
"id": "sidebar_toggle_btn",
"options": {
"label": "",
"bgcolor": "transparent",
"color": "#94A3B8",
"border": "1px solid #334155",
"borderRadius": "8px",
"width": "36px",
"height": "36px",
"padding": "0"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "sageToggleSidebar()"
}
]
},
{
"widgettype": "Image",
"options": {
"url": "{{entire_url('/imgs/msp.png')}}",
"height": "32px",
"marginLeft": "12px"
}
},
{
"widgettype": "Title4",
"options": {
"text": "Sage",
"color": "#F1F5F9",
"fontWeight": "bold",
"marginLeft": "8px"
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"id": "theme_toggle_btn",
"options": {
"label": "",
"bgcolor": "transparent",
"color": "#94A3B8",
"border": "1px solid #334155",
"borderRadius": "50%",
"width": "36px",
"height": "36px",
"padding": "0"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "sageToggleTheme()"
}
]
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('/rbac/user/user_panel.ui')}}"
}
}
]
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"height": "calc(100% - 56px)"
},
"subwidgets": [
{
"widgettype": "VBox",
"id": "sage_sidebar",
"options": {
"width": "240px",
"height": "100%",
"bgcolor": "#111827",
"borderRight": "1px solid #334155"
},
"subwidgets": [
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('global_menu.ui')}}"
}
}
]
},
{
"widgettype": "VBox",
"id": "sage_main_content",
"options": {
"css": "filler",
"height": "100%",
"padding": "24px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "加载中...",
"fontSize": "16px",
"color": "#94A3B8"
}
}
]
}
]
}
]
}

View File

@ -78,6 +78,17 @@ body {
transition: background-color 0.2s ease, color 0.2s ease;
}
/* Ensure theme variables override bricks.css hardcoded values */
[data-theme="dark"] body {
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
}
[data-theme="light"] body {
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
}
/* ===== Shell Layout ===== */
.sage-shell {
width: 100%;
@ -85,6 +96,9 @@ body {
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
transition: background-color 0.2s ease, color 0.2s ease;
}
.sage-topbar {
@ -114,7 +128,9 @@ body {
overflow-y: auto;
overflow-x: hidden;
flex-shrink: 0;
transition: width 0.25s ease;
transition: width 0.3s ease, transform 0.3s ease;
position: relative;
z-index: 50;
}
.sage-sidebar.collapsed {
@ -127,12 +143,61 @@ body {
overflow: hidden;
}
/* Menu collapsed state - hide text labels, center icons */
.menu-collapsed .filler {
display: none !important;
}
.menu-collapsed .menuitem {
justify-content: center;
padding: 8px 0;
}
.menu-collapsed .menuitem:hover {
background-color: var(--sage-bg-hover);
}
/* Mobile: sidebar as overlay */
@media (max-width: 768px) {
.sage-sidebar {
position: fixed;
left: 0;
top: var(--sage-topbar-height);
height: calc(100vh - var(--sage-topbar-height));
transform: translateX(0);
box-shadow: var(--sage-shadow-lg);
}
.sage-sidebar.collapsed {
transform: translateX(-100%);
width: var(--sage-sidebar-width); /* Keep full width when hidden on mobile */
}
.sage-main {
width: 100%;
}
}
.sage-main {
flex: 1;
overflow-y: auto;
overflow: hidden;
overflow-x: hidden;
padding: 24px;
background-color: var(--sage-bg-primary);
min-width: 0; /* Prevent flex item from overflowing */
min-height: 0; /* Allow flex children to shrink below content size */
transition: margin-left 0.3s ease;
}
/* Responsive padding for mobile */
@media (max-width: 768px) {
.sage-main {
padding: 16px;
}
}
@media (max-width: 480px) {
.sage-main {
padding: 12px;
}
}
/* ===== Stat Cards ===== */
@ -321,40 +386,390 @@ body {
}
/* ===== DataViewer Overrides for theme ===== */
.tabular-row {
cursor: pointer;
}
[data-theme="dark"] .dataviewer-toolbar,
[data-theme="dark"] .tabular,
[data-theme="dark"] .data-row {
background-color: var(--sage-bg-card);
[data-theme="dark"] .data-row,
[data-theme="dark"] body {
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
}
[data-theme="dark"] .tabular th {
[data-theme="dark"] .tabular-header-row {
background-color: var(--sage-bg-secondary);
color: var(--sage-text-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .tabular td {
border-color: var(--sage-border-weak);
[data-theme="dark"] .tabular-row {
color: var(--sage-text-primary);
}
[data-theme="light"] .dataviewer-toolbar,
[data-theme="light"] .tabular,
[data-theme="light"] .data-row {
[data-theme="dark"] .tabular-row:nth-child(odd) {
background-color: var(--sage-bg-card);
}
[data-theme="dark"] .tabular-row:nth-child(even) {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .tabular-row-selected {
background-color: var(--sage-bg-hover) !important;
}
[data-theme="dark"] .tabular-row-selected,
[data-theme="dark"] .tabular-row-selected .tabular-cell {
color: var(--sage-brand) !important;
}
/* Light theme selected row */
[data-theme="light"] .tabular-row-selected {
background-color: #E0F2FE !important;
}
[data-theme="light"] .tabular-row-selected,
[data-theme="light"] .tabular-row-selected .tabular-cell {
color: #0369A1 !important;
}
[data-theme="dark"] .tabular-cell {
border-color: var(--sage-border-weak);
color: var(--sage-text-primary);
}
[data-theme="dark"] .popup,
[data-theme="dark"] .modal,
[data-theme="dark"] .message {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .titlebar {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .inputbox {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .htoolbar,
[data-theme="dark"] .vtoolbar {
background-color: var(--sage-bg-toolbar);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .accordion-item {
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .accordion-item:nth-child(odd) {
background-color: var(--sage-bg-card);
}
[data-theme="dark"] .accordion-item:nth-child(even) {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .accordion-item-header {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .accordion-item-info {
background-color: var(--sage-bg-card);
}
[data-theme="dark"] .accordion-item-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .accordion-item-info-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .card {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .subcard {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .curpos {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .selected {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .tabpanel {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .tabpanel-content {
background-color: var(--sage-bg-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .toolbar-button {
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .toolbar-button-active {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .auto-textarea {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .droparea {
border-color: var(--sage-border-primary);
color: var(--sage-text-secondary);
}
[data-theme="dark"] .droparea:hover {
border-color: var(--sage-brand);
color: var(--sage-brand);
background: var(--sage-bg-hover);
}
[data-theme="dark"] .thinking-content {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .resp-error {
background-color: rgba(239, 68, 68, 0.15);
color: var(--sage-danger);
}
[data-theme="dark"] .resp-content {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular th {
[data-theme="dark"] .llm_title {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .progress-container {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] pre {
background-color: var(--sage-bg-secondary);
color: var(--sage-text-primary);
}
[data-theme="dark"] .llm_msg {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
}
[data-theme="dark"] .user_msg {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
[data-theme="light"] .dataviewer-toolbar,
[data-theme="light"] .tabular,
[data-theme="light"] .data-row,
[data-theme="light"] body {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular-header-row {
background-color: var(--sage-bg-hover);
color: var(--sage-text-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .tabular td {
border-color: var(--sage-border-weak);
[data-theme="light"] .tabular-row {
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular-row:nth-child(odd) {
background-color: #f9fafb;
}
[data-theme="light"] .tabular-row:nth-child(even) {
background-color: var(--sage-bg-card);
}
[data-theme="light"] .tabular-row-selected {
color: var(--sage-brand-hover);
}
[data-theme="light"] .tabular-cell {
border-color: var(--sage-border-weak);
color: var(--sage-text-primary);
}
[data-theme="light"] .popup,
[data-theme="light"] .modal,
[data-theme="light"] .message {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .titlebar {
background-color: var(--sage-bg-secondary);
}
[data-theme="light"] .inputbox {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .htoolbar,
[data-theme="light"] .vtoolbar {
background-color: var(--sage-bg-toolbar);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .accordion-item {
border-color: var(--sage-border-primary);
}
[data-theme="light"] .accordion-item:nth-child(odd) {
background-color: var(--sage-bg-card);
}
[data-theme="light"] .accordion-item:nth-child(even) {
background-color: #f9fafb;
}
[data-theme="light"] .accordion-item-header {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .accordion-item-info {
background-color: var(--sage-bg-card);
}
[data-theme="light"] .accordion-item-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .accordion-item-info-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .card {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .subcard {
background-color: #f9fafb;
}
[data-theme="light"] .curpos {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .selected {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .tabpanel {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .tabpanel-content {
background-color: var(--sage-bg-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .toolbar-button {
border-color: var(--sage-border-primary);
}
[data-theme="light"] .toolbar-button-active {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .auto-textarea {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .droparea {
border-color: var(--sage-border-primary);
color: var(--sage-text-secondary);
}
[data-theme="light"] .droparea:hover {
border-color: var(--sage-brand);
color: var(--sage-brand);
background: var(--sage-bg-hover);
}
[data-theme="light"] .thinking-content {
background-color: var(--sage-bg-secondary);
}
[data-theme="light"] .resp-error {
background-color: rgba(239, 68, 68, 0.1);
color: var(--sage-danger);
}
[data-theme="light"] .resp-content {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .llm_title {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .progress-container {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] pre {
background-color: var(--sage-bg-secondary);
color: var(--sage-text-primary);
}
[data-theme="light"] .llm_msg {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .user_msg {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular { background-color: #f5f5f5; color: #1E293B; }
[data-theme="light"] .tabular-header-row { background-color: #E2E8F0; color: #0F172A; font-weight: 600; }
[data-theme="light"] .tabular-header-row .tabular-cell { color: #0F172A; }
[data-theme="light"] .tabular-row { color: #1E293B; }
[data-theme="light"] .tabular-row:nth-child(odd) { background-color: #FFFFFF; }
[data-theme="light"] .tabular-row:nth-child(even) { background-color: #F8FAFC; }
[data-theme="light"] .tabular-cell { border-color: #E2E8F0; color: #1E293B; }
[data-theme="light"] .card { color: #1E293B; }
[data-theme="light"] .inputbox { color: #1E293B; background-color: #FFFFFF; }
[data-theme="light"] .popup, [data-theme="light"] .modal { color: #1E293B; }
[data-theme="light"] .accordion-item-info { color: #475569; }
[data-theme="light"] .toolbar-button { color: #334155; }
[data-theme="light"] .message { color: #1E293B; }
/* ===== Menu Overrides ===== */
[data-theme="dark"] .menu-item {
color: var(--sage-text-secondary);
@ -376,18 +791,53 @@ body {
color: var(--sage-brand);
}
/* ===== Responsive ===== */
/* ===== Utility Classes ===== */
.sage-brand-title {
color: var(--sage-text-primary);
font-weight: bold;
}
.sage-text-secondary {
color: var(--sage-text-secondary);
}
/* ===== Responsive - Topbar ===== */
@media (max-width: 768px) {
.sage-sidebar {
position: fixed;
left: 0;
top: var(--sage-topbar-height);
bottom: 0;
z-index: 200;
transform: translateX(-100%);
transition: transform 0.25s ease;
.sage-topbar {
padding: 0 8px;
gap: 6px;
}
.sage-sidebar.mobile-open {
transform: translateX(0);
.sage-brand-title {
font-size: 16px;
}
}
@media (max-width: 480px) {
.sage-brand-title {
display: none;
}
}
/* ===== Responsive - Stat Cards ===== */
@media (max-width: 768px) {
.stat-card {
padding: 14px;
}
.stat-card .stat-value {
font-size: 22px;
}
.stat-card .stat-label {
font-size: 12px;
}
.stat-card .stat-icon {
width: 32px;
height: 32px;
margin-bottom: 8px;
}
.quick-link {
padding: 14px;
}
.section-title {
font-size: 16px;
}
}

View File

@ -42,9 +42,61 @@
function initSidebar() {
var collapsed = false;
try { collapsed = localStorage.getItem(SIDEBAR_KEY) === 'true'; } catch(e) {}
// Auto-collapse on mobile
if (isMobile()) {
collapsed = true;
}
var sidebar = document.getElementById('sage_sidebar');
if (sidebar && collapsed) {
sidebar.classList.add('collapsed');
sidebar.style.width = '64px';
}
}
// Handle window resize - auto-collapse on mobile
function handleResize() {
var sidebar = document.getElementById('sage_sidebar');
if (!sidebar) return;
if (isMobile() && !sidebar.classList.contains('collapsed')) {
sidebar.classList.add('collapsed');
sidebar.style.width = '64px';
try { localStorage.setItem(SIDEBAR_KEY, 'true'); } catch(e) {}
updateSidebarIcon(true);
}
}
// Check if we're on mobile viewport
function isMobile() {
return window.innerWidth <= 768;
}
// Close sidebar when clicking outside on mobile
function setupMobileOverlay() {
document.addEventListener('click', function(e) {
if (!isMobile()) return;
var sidebar = document.getElementById('sage_sidebar');
var toggleBtn = document.getElementById('sidebar_toggle_btn');
if (!sidebar || sidebar.classList.contains('collapsed')) return;
// If click is outside sidebar and toggle button, close sidebar
if (!sidebar.contains(e.target) && (!toggleBtn || !toggleBtn.contains(e.target))) {
sidebar.classList.add('collapsed');
sidebar.style.width = '64px';
try { localStorage.setItem(SIDEBAR_KEY, 'true'); } catch(ex) {}
updateSidebarIcon(true);
}
});
}
function updateSidebarIcon(isCollapsed) {
var btn = document.getElementById('sidebar_toggle_btn');
if (!btn) return;
if (isCollapsed) {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>';
} else {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>';
}
}
@ -55,29 +107,152 @@
sidebar.classList.toggle('collapsed');
var isCollapsed = sidebar.classList.contains('collapsed');
try { localStorage.setItem(SIDEBAR_KEY, isCollapsed); } catch(e) {}
// Update toggle icon
var btn = document.getElementById('sidebar_toggle_btn');
if (btn) {
if (isCollapsed) {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>';
} else {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>';
updateSidebarIcon(isCollapsed);
// Override inline width style (bricks sets inline style which beats CSS class)
sidebar.style.width = isCollapsed ? '64px' : '240px';
// Toggle Menu widget collapse state
if (typeof bricks !== 'undefined') {
var menu = bricks.getWidgetById('global_nav_menu', bricks.app);
if (menu && menu.toggle_collapse) {
menu.toggle_collapse();
}
}
}
// Initialize SPA Router
function initRouter() {
if (typeof bricks === 'undefined' || !bricks.Router) {
console.log('[Shell] Router not available');
return;
}
bricks.Router.init({
targets: [
{ id: 'sage_main_content', param: 'page' }
]
});
}
// Called by user_logined event — skip dashboard if Router has a saved route
window.sageOnLogin = async function(dashboardUrl) {
var target = bricks.getWidgetById('sage_main_content', bricks.app);
if (!target) return;
var hasRoute = false;
if (bricks.Router && bricks.Router._enabled) {
hasRoute = bricks.Router.current('sage_main_content')
|| new URLSearchParams(window.location.search).has('page');
}
if (hasRoute) {
console.log('[Shell] Router has route, skip dashboard load');
return;
}
// No route — load dashboard via Router
if (dashboardUrl && bricks.Router && bricks.Router._enabled) {
bricks.Router.navigate('sage_main_content', dashboardUrl);
} else if (dashboardUrl) {
var desc = { widgettype: 'urlwidget', options: { url: dashboardUrl } };
var w = await bricks.widgetBuild(desc, bricks.app);
if (w) {
target.clear_widgets();
target.add_widget(w);
}
}
};
// Reload global menu after login/logout
window.sageReloadMenu = async function() {
if (typeof bricks === 'undefined') return;
var sidebar = bricks.getWidgetById('sage_sidebar', bricks.app);
if (!sidebar) {
console.log('[Shell] sage_sidebar not found');
return;
}
// Check if sidebar is currently collapsed before clearing
var isCollapsed = sidebar.el && sidebar.el.classList.contains('collapsed');
// Clear existing children
sidebar.subwidgets.forEach(function(w) { w.destroy && w.destroy(); });
sidebar.subwidgets = [];
sidebar.el.innerHTML = '';
// Rebuild menu urlwidget
var menuUrl = bricks.app.baseUrl + '/global_menu.ui?_webbricks_=1';
var desc = {
"widgettype": "urlwidget",
"options": { "url": menuUrl }
};
try {
var w = await bricks.widgetBuild(desc, sidebar);
if (w) {
sidebar.addSubWidget(w);
// Re-apply collapsed state and inline width to newly built menu
if (isCollapsed) {
sidebar.el.style.width = '64px';
var menu = bricks.getWidgetById('global_nav_menu', bricks.app);
if (menu && menu.collapse) {
menu.collapse();
}
}
console.log('[Shell] Menu reloaded');
}
} catch(e) {
console.log('[Shell] Menu reload error:', e);
}
};
// Run on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initTheme();
initSidebar();
initRouter();
setupMobileOverlay();
window.addEventListener('resize', handleResize);
});
} else {
initTheme();
initSidebar();
initRouter();
setupMobileOverlay();
window.addEventListener('resize', handleResize);
}
// Bricks widgets render asynchronously after DOMContentLoaded.
// Retry updateThemeIcon until the button element exists.
(function retryThemeIcon() {
var btn = document.getElementById('theme_toggle_btn');
if (btn) {
var theme = document.documentElement.getAttribute('data-theme') || 'dark';
updateThemeIcon(theme);
} else {
setTimeout(retryThemeIcon, 200);
}
})();
// Retry sidebar icon init until the button element exists
(function retrySidebarIcon() {
var sidebar = document.getElementById('sage_sidebar');
var btn = document.getElementById('sidebar_toggle_btn');
if (btn && sidebar) {
var isCollapsed = sidebar.classList.contains('collapsed');
updateSidebarIcon(isCollapsed);
// Sync inline width with collapsed state (bricks may have overridden it)
sidebar.style.width = isCollapsed ? '64px' : '240px';
// Apply collapsed state to Menu if sidebar was already collapsed
if (isCollapsed && typeof bricks !== 'undefined') {
var menu = bricks.getWidgetById('global_nav_menu', bricks.app);
if (menu && menu.collapse) {
menu.collapse();
} else {
setTimeout(retrySidebarIcon, 200);
return;
}
}
} else {
setTimeout(retrySidebarIcon, 200);
}
})();
// Expose global functions for bricks bind access
window.sageToggleTheme = toggleTheme;
window.sageToggleSidebar = toggleSidebar;

View File

@ -0,0 +1,53 @@
{
"widgettype": "VBox",
"options": {
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"flex": "1",
"minHeight": "90px",
"borderLeft": "4px solid #22c55e"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M15.91 11.672a.375.375 0 010 .656l-5.603 3.113a.375.375 0 01-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112z\"/><path d=\"M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_active_users_today(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"css": "stat-label",
"fontSize": "14px",
"marginTop": "4px",
"otext": "今日活跃用户",
"i18n": true
}
}
]
}

View File

@ -1,12 +1,12 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
"minHeight": "90px",
"borderLeft": "4px solid #06b6d4"
},
"subwidgets": [
{
@ -32,21 +32,21 @@
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_concurrent_users(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "在线用户",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
"marginTop": "4px",
"otext": "在线用户",
"i18n": true
}
}
]
}
}

View File

@ -1,13 +1,26 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
"minHeight": "90px",
"cursor": "pointer",
"borderLeft": "4px solid #ef4444"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/llmage/failed_accounting.ui')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "HBox",
@ -32,21 +45,21 @@
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_accounting_errors(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#EF4444",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "记账异常",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
"marginTop": "4px",
"otext": "记账异常",
"i18n": true
}
}
]
}
}

View File

@ -0,0 +1,52 @@
{
"widgettype": "VBox",
"options": {
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"flex": "1",
"minHeight": "90px",
"borderLeft": "4px solid #10b981"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#10B981\" stroke-width=\"2\"><path d=\"M19 7.5v3m0 0v3m0 0v3m0 0v3m0 0h-3m0 0h-3m0 0h-3m0 0h-3m0 0v-3m0 0V12m0 0V7.5M5 21h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_new_users_month(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"fontSize": "14px",
"marginTop": "4px",
"otext": "本月新增用户",
"i18n": true
}
}
]
}

View File

@ -1,12 +1,13 @@
{% set trend = get_amount_trend(request) %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
"minHeight": "90px",
"borderLeft": "4px solid {% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#8b5cf6{% endif %}"
},
"subwidgets": [
{
@ -19,9 +20,10 @@
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg>",
"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 +34,59 @@
{
"widgettype": "Text",
"options": {
"text": "¥{{get_today_amount(request)|round(2)}}",
"css": "stat-value",
"text": "¥{{trend.value|round(2)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"widgettype": "HBox",
"options": {
"text": "今日消费金额",
"fontSize": "14px",
"color": "#94A3B8",
"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' %}<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)}}%{% if trend.trend == 'up' %} ↑{% elif trend.trend == 'down' %} ↓{% endif %}",
"fontSize": "12px",
"fontWeight": "600",
"color": "{% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#94a3b8{% endif %}"
}
}
]
}
]
}
]
}

View File

@ -1,13 +1,14 @@
{% set trend = get_usage_trend(request) %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px",
"cursor": "pointer"
"minHeight": "90px",
"cursor": "pointer",
"borderLeft": "4px solid {% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#3b82f6{% endif %}"
},
"subwidgets": [
{
@ -20,9 +21,10 @@
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" 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>",
"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 +35,59 @@
{
"widgettype": "Text",
"options": {
"text": "{{get_today_usage(request)}}",
"css": "stat-value",
"text": "{{trend.value}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"widgettype": "HBox",
"options": {
"text": "今日调用笔数",
"fontSize": "14px",
"color": "#94A3B8",
"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' %}<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)}}%{% if trend.trend == 'up' %} ↑{% elif trend.trend == 'down' %} ↓{% endif %}",
"fontSize": "12px",
"fontWeight": "600",
"color": "{% if trend.trend == 'up' %}#22c55e{% elif trend.trend == 'down' %}#ef4444{% else %}#94a3b8{% endif %}"
}
}
]
}
]
}
]
}
}

View File

@ -0,0 +1,52 @@
{
"widgettype": "VBox",
"options": {
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"flex": "1",
"minHeight": "90px",
"borderLeft": "4px solid #8b5cf6"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#8B5CF6\" stroke-width=\"2\"><path d=\"M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_total_orgs(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"fontSize": "14px",
"marginTop": "4px",
"otext": "组织机构数",
"i18n": true
}
}
]
}

View File

@ -1,12 +1,12 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"css": "stat-card",
"padding": "14px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
"minHeight": "90px",
"borderLeft": "4px solid #3b82f6"
},
"subwidgets": [
{
@ -19,9 +19,10 @@
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#A78BFA\" 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>",
"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"
}
},
{
@ -32,20 +33,21 @@
{
"widgettype": "Text",
"options": {
"css": "stat-value",
"text": "{{get_total_users(request)}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "用户总数",
"css": "stat-label",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
"marginTop": "4px",
"otext": "用户总数",
"i18n": true
}
}
]

View File

@ -0,0 +1,73 @@
{% set providers = get_top_providers_by_amount(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%"
},
"subwidgets": [
{% for p in providers %}
{
"widgettype": "HBox",
"options": {
"width": "100%",
"padding": "12px 0",
{% if not loop.first %}
{% endif %}
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{loop.index}}",
"width": "30px",
"fontSize": "14px",
"textAlign": "center"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{p.provider_name}}",
"flex": "1",
"fontSize": "14px",
"fontWeight": "500"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{p.cnt}} 笔",
"width": "80px",
"fontSize": "13px",
"textAlign": "right"
}
},
{
"widgettype": "Text",
"options": {
"text": "¥{{p.total_amount}}",
"width": "100px",
"fontSize": "14px",
"fontWeight": "600",
"textAlign": "right"
}
}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% if not providers %}
{
"widgettype": "Text",
"options": {
"otext": "暂无数据",
"i18n": true,
"fontSize": "14px",
"textAlign": "center",
"padding": "20px 0"
}
}
{% endif %}
]
}

View File

@ -0,0 +1,73 @@
{% set providers = get_top_providers_by_count(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%"
},
"subwidgets": [
{% for p in providers %}
{
"widgettype": "HBox",
"options": {
"width": "100%",
"padding": "12px 0",
{% if not loop.first %}
{% endif %}
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{loop.index}}",
"width": "30px",
"fontSize": "14px",
"textAlign": "center"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{p.provider_name}}",
"flex": "1",
"fontSize": "14px",
"fontWeight": "500"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{p.cnt}} 笔",
"width": "80px",
"fontSize": "14px",
"fontWeight": "600",
"textAlign": "right"
}
},
{
"widgettype": "Text",
"options": {
"text": "¥{{p.total_amount}}",
"width": "100px",
"fontSize": "13px",
"textAlign": "right"
}
}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% if not providers %}
{
"widgettype": "Text",
"options": {
"otext": "暂无数据",
"i18n": true,
"fontSize": "14px",
"textAlign": "center",
"padding": "20px 0"
}
}
{% endif %}
]
}

View File

@ -1,16 +1,74 @@
{% set users = get_top_users_by_amount(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%"
},
"subwidgets": [
{% for u in users %}
{
"widgettype": "RefreshWidget",
"id": "table_top_users_amount",
"widgettype": "HBox",
"options": {
"period_seconds": 30,
"url": "{{entire_url('table_top_users_amount.ui')}}"
"width": "100%",
"padding": "12px 0",
{% if not loop.first %}
"borderTop": "1px solid var(--sage-border-weak)",
{% endif %}
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{loop.index}}",
"width": "30px",
"fontSize": "14px",
"textAlign": "center"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{u.user_name}}",
"flex": "1",
"fontSize": "14px",
"fontWeight": "500"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{u.cnt}} 笔",
"width": "80px",
"fontSize": "13px",
"textAlign": "right"
}
},
{
"widgettype": "Text",
"options": {
"text": "¥{{u.total_amount}}",
"width": "100px",
"fontSize": "14px",
"fontWeight": "600",
"textAlign": "right"
}
}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% if not users %}
{
"widgettype": "Text",
"options": {
"otext": "暂无数据",
"i18n": true,
"fontSize": "14px",
"textAlign": "center",
"padding": "20px 0"
}
}
{% endif %}
]
}

View File

@ -12,7 +12,7 @@
"width": "100%",
"padding": "12px 0",
{% if not loop.first %}
"borderTop": "1px solid #334155",
"borderTop": "1px solid var(--sage-border-weak)",
{% endif %}
"alignItems": "center"
},
@ -22,7 +22,6 @@
"options": {
"text": "{{loop.index}}",
"width": "30px",
"color": "#64748B",
"fontSize": "14px",
"textAlign": "center"
}
@ -32,7 +31,6 @@
"options": {
"text": "{{u.user_name}}",
"flex": "1",
"color": "#F1F5F9",
"fontSize": "14px",
"fontWeight": "500"
}
@ -42,7 +40,6 @@
"options": {
"text": "{{u.cnt}} 笔",
"width": "80px",
"color": "#94A3B8",
"fontSize": "13px",
"textAlign": "right"
}
@ -52,7 +49,6 @@
"options": {
"text": "¥{{u.total_amount}}",
"width": "100px",
"color": "#22C55E",
"fontSize": "14px",
"fontWeight": "600",
"textAlign": "right"
@ -65,7 +61,9 @@
{
"widgettype": "Text",
"options": {
"text": "暂无数据",
"otext": "暂无数据",
"i18n": true,
"color": "#64748B",
"fontSize": "14px",
"textAlign": "center",

View File

@ -0,0 +1,73 @@
{% set users = get_top_users_by_count(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%"
},
"subwidgets": [
{% for u in users %}
{
"widgettype": "HBox",
"options": {
"width": "100%",
"padding": "12px 0",
{% if not loop.first %}
{% endif %}
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{loop.index}}",
"width": "30px",
"fontSize": "14px",
"textAlign": "center"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{u.user_name}}",
"flex": "1",
"fontSize": "14px",
"fontWeight": "500"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{u.cnt}} 笔",
"width": "80px",
"fontSize": "14px",
"fontWeight": "600",
"textAlign": "right"
}
},
{
"widgettype": "Text",
"options": {
"text": "¥{{u.total_amount}}",
"width": "100px",
"fontSize": "13px",
"textAlign": "right"
}
}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% if not users %}
{
"widgettype": "Text",
"options": {
"otext": "暂无数据",
"i18n": true,
"fontSize": "14px",
"textAlign": "center",
"padding": "20px 0"
}
}
{% endif %}
]
}

View File

@ -1,7 +1,7 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"bgcolor": "var(--sage-bg-card, #FFFFFF)",
"padding": "24px",
"borderRadius": "8px",
"flex": "1",
@ -12,10 +12,11 @@
{
"widgettype": "Text",
"options": {
"text": "今日交易金额",
"fontSize": "14px",
"color": "#888",
"marginBottom": "8px"
"marginBottom": "8px",
"otext": "今日交易金额",
"i18n": true
}
},
{
@ -29,4 +30,4 @@
}
}
]
}
}

View File

@ -12,10 +12,11 @@
{
"widgettype": "Text",
"options": {
"text": "今日调用笔数",
"fontSize": "14px",
"color": "#888",
"marginBottom": "8px"
"marginBottom": "8px",
"otext": "今日调用笔数",
"i18n": true
}
},
{
@ -29,4 +30,4 @@
}
}
]
}
}

View File

@ -10,7 +10,9 @@
{
"widgettype": "Text",
"options": {
"text": "用户金额 TOP 5今日",
"otext": "用户金额 TOP 5今日",
"i18n": true,
"fontSize": "16px",
"fontWeight": "bold",
"color": "#333",
@ -27,10 +29,14 @@
"widgettype": "HBox",
"options": {"bgcolor": "#f5f5f5", "padding": "8px 12px", "borderRadius": "4px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "排名", "fontSize": "12px", "color": "#888", "width": "50px"}},
{"widgettype": "Text", "options": {"text": "用户", "fontSize": "12px", "color": "#888", "flex": "1"}},
{"widgettype": "Text", "options": {"text": "金额", "fontSize": "12px", "color": "#888", "width": "100px", "textAlign": "right"}},
{"widgettype": "Text", "options": {"text": "笔数", "fontSize": "12px", "color": "#888", "width": "60px", "textAlign": "right"}}
{"widgettype": "Text", "options": {"otext": "排名",
"i18n": true, "fontSize": "12px", "color": "#888", "width": "50px"}},
{"widgettype": "Text", "options": {"otext": "用户",
"i18n": true, "fontSize": "12px", "color": "#888", "flex": "1"}},
{"widgettype": "Text", "options": {"otext": "金额",
"i18n": true, "fontSize": "12px", "color": "#888", "width": "100px", "textAlign": "right"}},
{"widgettype": "Text", "options": {"otext": "笔数",
"i18n": true, "fontSize": "12px", "color": "#888", "width": "60px", "textAlign": "right"}}
]
},
{% for item in rows %}
@ -50,7 +56,8 @@
{% else %}
{
"widgettype": "Text",
"options": {"text": "暂无数据", "fontSize": "14px", "color": "#999", "textAlign": "center"}
"options": {"otext": "暂无数据",
"i18n": true, "fontSize": "14px", "color": "#999", "textAlign": "center"}
}
{% endif %}
]

View File

@ -1,7 +1,7 @@
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"bgcolor": "var(--sage-bg-card, #FFFFFF)",
"padding": "24px",
"borderRadius": "8px",
"flex": "1",
@ -12,10 +12,11 @@
{
"widgettype": "Text",
"options": {
"text": "用户总数",
"fontSize": "14px",
"color": "#888",
"marginBottom": "8px"
"marginBottom": "8px",
"otext": "用户总数",
"i18n": true
}
},
{
@ -29,4 +30,4 @@
}
}
]
}
}

View File

@ -0,0 +1,11 @@
{
"widgettype": "ChartBar",
"options": {
"height": "280px",
"width": "100%",
"data_url": "{{entire_url('api/user_today_models.dspy')}}",
"nameField": "model_name",
"valueFields": ["cnt", "total_amount"],
"refresh_period": 30
}
}