sync: local modifications to unified_dashboard

- Updated core.py, init.py, mysql.ddl.sql
- Added __init__.py
- Added API files: dashboard_kpi, report_list
- Added UI files: base.ui, dashboard.ui, reports.ui
This commit is contained in:
yumoqing 2026-04-28 18:55:07 +08:00
parent 1d4a6b2893
commit aec650dcef
9 changed files with 351 additions and 11 deletions

View File

@ -0,0 +1,49 @@
-- Table from dashboard_config.json
CREATE TABLE IF NOT EXISTS `dashboard_config` (
`id` VARCHAR(32) NOT NULL COMMENT '主键UUID',
`dashboard_name` VARCHAR(100) COMMENT '仪表板显示名称',
`dashboard_type` VARCHAR(50) COMMENT 'sales/finance/customer/executive',
`config_json` VARCHAR(2000) COMMENT '仪表板布局和组件配置',
`is_default` VARCHAR(1) COMMENT 'Y/N',
`org_id` VARCHAR(32) COMMENT '多租户组织隔离',
`created_by` VARCHAR(32) COMMENT '创建用户ID',
`created_at` TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='仪表板配置';
CREATE INDEX `idx_dashboard_org` ON `dashboard_config` (`org_id`);
CREATE INDEX `idx_dashboard_type` ON `dashboard_config` (`dashboard_type`);
CREATE UNIQUE INDEX `uk_dashboard_name_org` ON `dashboard_config` (`dashboard_name`, `org_id`);
-- Table from report_template.json
CREATE TABLE IF NOT EXISTS `report_template` (
`id` VARCHAR(32) NOT NULL COMMENT '主键UUID',
`template_name` VARCHAR(100) COMMENT '报表模板名称',
`report_type` VARCHAR(50) COMMENT 'sales/finance/customer/contract',
`sql_query` VARCHAR(2000) COMMENT '报表数据查询SQL',
`columns_config` VARCHAR(1000) NOT NULL COMMENT 'JSON格式的列配置',
`filters_config` VARCHAR(1000) NOT NULL COMMENT 'JSON格式的过滤器配置',
`chart_config` VARCHAR(1000) NOT NULL COMMENT 'JSON格式的图表配置',
`org_id` VARCHAR(32) COMMENT '多租户组织隔离',
`created_by` VARCHAR(32) COMMENT '创建用户ID',
`created_at` TIMESTAMP COMMENT '创建时间',
`is_active` VARCHAR(1) COMMENT 'Y/N',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报表模板';
CREATE INDEX `idx_template_org` ON `report_template` (`org_id`);
CREATE INDEX `idx_template_type` ON `report_template` (`report_type`);
-- Table from user_dashboard.json
CREATE TABLE IF NOT EXISTS `user_dashboard` (
`id` VARCHAR(32) NOT NULL COMMENT '主键UUID',
`user_id` VARCHAR(32) COMMENT '关联用户',
`dashboard_config_id` VARCHAR(32) COMMENT '关联的仪表板配置',
`layout_json` VARCHAR(2000) NOT NULL COMMENT '用户自定义布局',
`is_favorite` VARCHAR(1) COMMENT 'Y/N',
`org_id` VARCHAR(32) COMMENT '多租户组织隔离',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户仪表板';
CREATE UNIQUE INDEX `idx_user_dashboard_user` ON `user_dashboard` (`user_id`, `dashboard_config_id`);
CREATE INDEX `idx_user_dashboard_org` ON `user_dashboard` (`org_id`);

View File

@ -0,0 +1 @@
# Unified Dashboard Module

View File

@ -176,11 +176,12 @@ class DashboardCore:
return approvals
elif list_type == 'overdue_tasks':
tasks = await self.db.select('approval_task',
fields=['title', 'step_name', 'due_at'],
where={'org_id': org_id, 'status': 'pending', 'due_at < NOW()'},
order_by='due_at ASC',
limit=10)
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
tasks = await self.db.sqlExe(
"SELECT title, step_name, due_at FROM approval_task WHERE org_id = ${org_id}$ AND status = ${status}$ AND due_at < ${now}$ ORDER BY due_at ASC",
{'org_id': org_id, 'status': 'pending', 'now': now},
limit=10
)
return tasks
else:

View File

@ -1,6 +1,8 @@
from ahserver.serverenv import ServerEnv
from appPublic.worker import awaitify
from .core import DashboardCore, ReportCore
from sqlor.dbpools import DBPools
def load_unified_dashboard():
"""加载统一仪表板和报表模块"""
@ -8,14 +10,14 @@ def load_unified_dashboard():
# 创建核心实例的工厂函数
async def create_dashboard_core(org_id):
from sqlor.dbp import getDBP
db = await getDBP(org_id)
return DashboardCore(db)
dbname = get_module_dbname('unified_dashboard')
sor = await DBPools().sqlorContext(dbname)
return DashboardCore(sor)
async def create_report_core(org_id):
from sqlor.dbp import getDBP
db = await getDBP(org_id)
return ReportCore(db)
dbname = get_module_dbname('unified_dashboard')
sor = await DBPools().sqlorContext(dbname)
return ReportCore(sor)
env.create_dashboard_core = create_dashboard_core
env.create_report_core = create_report_core

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Dashboard KPI and chart data API"""
import json
result = {'success': False, 'rows': [], 'total': 0}
try:
kpi_type = params_kw.get('type', '').strip()
if kpi_type == 'sales_funnel':
dbname = get_module_dbname('opportunity_management')
async with DBPools().sqlorContext(dbname) as sor:
stages = await sor.sqlExe("SELECT id, stage_name, stage_order FROM sales_stages ORDER BY stage_order ASC")
funnel_data = []
for stage in stages:
rows = await sor.sqlExe("SELECT COUNT(*) as cnt, COALESCE(SUM(estimated_amount),0) as amt FROM opportunities WHERE current_stage = ${stage_id}$", {'stage_id': stage['id']})
funnel_data.append({
'stage': stage['stage_name'],
'count': rows[0]['cnt'] if rows else 0,
'amount': float(rows[0]['amt'] or 0) if rows else 0
})
result['rows'] = funnel_data
result['total'] = len(funnel_data)
result['success'] = True
elif kpi_type == 'recent_opportunities':
dbname = get_module_dbname('opportunity_management')
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe("SELECT customer_name, estimated_amount, current_stage FROM opportunities ORDER BY created_at DESC LIMIT 10")
result['rows'] = [dict(r) for r in (rows or [])]
result['total'] = len(result['rows'])
result['success'] = True
elif kpi_type == 'kpi_summary':
# Aggregate KPIs from all modules
kpis = {}
# Customer count
try:
dbname = get_module_dbname('customer_management')
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe("SELECT COUNT(*) as cnt FROM customers")
kpis['total_customers'] = rows[0]['cnt'] if rows else 0
except:
kpis['total_customers'] = 0
# Active opportunities
try:
dbname = get_module_dbname('opportunity_management')
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe("SELECT COUNT(*) as cnt FROM opportunities WHERE status = 'active'")
kpis['active_opportunities'] = rows[0]['cnt'] if rows else 0
except:
kpis['active_opportunities'] = 0
# Contract count
try:
dbname = get_module_dbname('contract_management')
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe("SELECT COUNT(*) as cnt FROM contract")
kpis['total_contracts'] = rows[0]['cnt'] if rows else 0
except:
kpis['total_contracts'] = 0
# Total receivables
try:
dbname = get_module_dbname('financial_management')
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe("SELECT COALESCE(SUM(receivable_amount),0) as total FROM receivables")
kpis['total_receivables'] = float(rows[0]['total'] or 0) if rows else 0
except:
kpis['total_receivables'] = 0
result['success'] = True
result['data'] = kpis
else:
result['error'] = f'Unknown KPI type: {kpi_type}'
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Report list API"""
import json
result = {'success': False, 'rows': [], 'total': 0}
try:
dbname = get_module_dbname('unified_dashboard')
ns = {
'page': int(params_kw.get('page', 1)),
'rows': int(params_kw.get('rows', 20)),
'sort': 'created_at desc'
}
sql = "SELECT id, template_name, category, description, is_active, created_at FROM report_template WHERE 1=1"
keyword = params_kw.get('keyword', '').strip()
if keyword:
sql += " AND template_name LIKE ${keyword}$"
ns['keyword'] = f'%{keyword}%'
async with DBPools().sqlorContext(dbname) as sor:
data = await sor.sqlExe(sql, ns)
if isinstance(data, dict):
result['total'] = data.get('total', 0)
result['rows'] = [dict(r) for r in data.get('rows', [])]
else:
result['rows'] = [dict(r) for r in (data or [])]
result['total'] = len(result['rows'])
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

44
wwwroot/base.ui Normal file
View File

@ -0,0 +1,44 @@
{
"widgettype": "Page",
"options": {
"title": "统一仪表板",
"style": {"height": "100vh", "padding": "0"}
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"style": {"height": "100%"}},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"width": "220px", "backgroundColor": "#1A1E2F", "padding": "8px"}},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "数据看板", "style": {"color": "#fff", "fontSize": "16px", "fontWeight": "bold", "padding": "12px 8px"}}
},
{
"widgettype": "Menu",
"options": {
"width": "100%",
"bgcolor": "#1A1E2F",
"items": [
{"label": "高管视图", "icon": "fa fa-dashboard", "url": "{{entire_url('dashboard.ui')}}?type=executive", "target": "app.dashboard-content"},
{"label": "销售视图", "icon": "fa fa-line-chart", "url": "{{entire_url('dashboard.ui')}}?type=sales", "target": "app.dashboard-content"},
{"label": "财务视图", "icon": "fa fa-money", "url": "{{entire_url('dashboard.ui')}}?type=finance", "target": "app.dashboard-content"},
{"label": "报表中心", "icon": "fa fa-file-text", "url": "{{entire_url('reports.ui')}}", "target": "app.dashboard-content"}
],
"menuitem_css": "menuitem"
}
}
]
},
{
"widgettype": "VBox",
"id": "dashboard-content",
"options": {"style": {"flex": 1, "overflow": "auto", "backgroundColor": "#f5f5f5"}}
}
]
}
]
}

83
wwwroot/dashboard.ui Normal file
View File

@ -0,0 +1,83 @@
{
"widgettype": "Page",
"options": {
"title": "仪表板",
"style": {"height": "100%", "padding": "0"}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"style": {"marginBottom": "16px", "gap": "12px", "flexWrap": "wrap"}},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "客户总数", "style": {"fontSize": "14px", "color": "#666"}}},
{"widgettype": "Text", "id": "kpi_customers", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#1E40AF"}}}
]
},
{
"widgettype": "VBox",
"options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "活跃商机", "style": {"fontSize": "14px", "color": "#666"}}},
{"widgettype": "Text", "id": "kpi_opportunities", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#059669"}}}
]
},
{
"widgettype": "VBox",
"options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "合同总数", "style": {"fontSize": "14px", "color": "#666"}}},
{"widgettype": "Text", "id": "kpi_contracts", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#D97706"}}}
]
},
{
"widgettype": "VBox",
"options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "应收总额", "style": {"fontSize": "14px", "color": "#666"}}},
{"widgettype": "Text", "id": "kpi_receivables", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#DC2626"}}}
]
}
]
},
{
"widgettype": "HBox",
"options": {"style": {"gap": "16px", "flexWrap": "wrap"}},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"flex": 2, "minWidth": "400px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "销售漏斗", "style": {"fontSize": "16px", "fontWeight": "bold", "marginBottom": "12px"}}},
{"widgettype": "DataGrid", "id": "funnel_grid", "options": {"url": "{{entire_url('api/dashboard_kpi.dspy')}}?type=sales_funnel", "columns": [
{"field": "stage", "header": "阶段", "width": 120},
{"field": "count", "header": "数量", "width": 80},
{"field": "amount", "header": "金额", "width": 120}
]}}
]
},
{
"widgettype": "VBox",
"options": {"style": {"flex": 1, "minWidth": "300px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "最新商机", "style": {"fontSize": "16px", "fontWeight": "bold", "marginBottom": "12px"}}},
{"widgettype": "DataGrid", "id": "recent_opp_grid", "options": {"url": "{{entire_url('api/dashboard_kpi.dspy')}}?type=recent_opportunities", "columns": [
{"field": "customer_name", "header": "客户", "width": 120},
{"field": "estimated_amount", "header": "金额", "width": 100},
{"field": "current_stage", "header": "阶段", "width": 80}
]}}
]
}
]
}
]
}
]
}

41
wwwroot/reports.ui Normal file
View File

@ -0,0 +1,41 @@
{
"widgettype": "Page",
"options": {
"title": "报表中心",
"style": {"height": "100%", "padding": "0"}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"style": {"marginBottom": "16px", "gap": "8px"}},
"subwidgets": [
{"widgettype": "TextField", "id": "report_search", "options": {"label": "搜索报表", "placeholder": "报表名称", "style": {"flex": 1}}},
{"widgettype": "Button", "id": "btn_search", "options": {"text": "搜索", "variant": "primary"}}
]
},
{
"widgettype": "DataGrid",
"id": "report_grid",
"options": {
"url": "{{entire_url('api/report_list.dspy')}}",
"style": {"flex": 1},
"columns": [
{"field": "template_name", "header": "报表名称", "width": 200},
{"field": "category", "header": "分类", "width": 120},
{"field": "description", "header": "说明", "width": 300},
{"field": "is_active", "header": "状态", "width": 80},
{"field": "created_at", "header": "创建时间", "width": 160}
],
"toolbar": [
{"type": "button", "text": "查看", "icon": "view", "action": "navigate('main/unified_dashboard/report_view.ui?id={% raw %}{{selectedRow.id}}{% endraw %}')"}
]
}
}
]
}
]
}