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 return approvals
elif list_type == 'overdue_tasks': elif list_type == 'overdue_tasks':
tasks = await self.db.select('approval_task', now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
fields=['title', 'step_name', 'due_at'], tasks = await self.db.sqlExe(
where={'org_id': org_id, 'status': 'pending', 'due_at < NOW()'}, "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",
order_by='due_at ASC', {'org_id': org_id, 'status': 'pending', 'now': now},
limit=10) limit=10
)
return tasks return tasks
else: else:

View File

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