feat: 实现自助BI看板功能,支持Superset/Metabase集成
- 新增BI模块(src/bi/),包含数据模型、服务和控制器 - 支持数据源管理、图表创建、看板配置 - 实现多图表类型:折线图、柱状图、饼图、散点图、面积图、仪表盘、表格 - 提供REST API(/bi/)和前端API(/bi-api/)接口 - 创建响应式前端界面,支持拖拽和实时数据展示 - 默认包含运营总览、设备管理、安全监控看板 - 支持与Superset和Metabase集成 🤖 Generated with [OpenClaw](https://github.com/robocomp/openclaw)
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# 水务管理系统 BI 前端
|
||||
|
||||
这是一个简单的HTML前端,用于展示水务管理系统的BI看板和数据可视化功能。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 自助BI看板浏览
|
||||
- 图表数据可视化
|
||||
- 实时数据展示
|
||||
- 响应式设计
|
||||
- 移动端兼容
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── index.html # 主页面
|
||||
├── css/
|
||||
│ └── styles.css # 样式文件
|
||||
├── js/
|
||||
│ ├── main.js # 主要JavaScript逻辑
|
||||
│ ├── api.js # API调用封装
|
||||
│ └── charts.js # 图表绘制逻辑
|
||||
└── lib/
|
||||
└── echarts.min.js # ECharts图表库
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 确保后端API服务器正在运行
|
||||
2. 打开 `index.html` 文件
|
||||
3. 选择要查看的看板
|
||||
4. 查看实时数据和图表
|
||||
|
||||
## API接口
|
||||
|
||||
前端主要通过以下API接口与后端交互:
|
||||
|
||||
- `/bi-api/dashboards/overview` - 获取概览看板
|
||||
- `/bi-api/dashboards/{id}/data` - 获取看板数据
|
||||
- `/bi-api/charts/{id}/data` - 获取图表数据
|
||||
- `/bi-api/search` - 搜索功能
|
||||
|
||||
## 技术栈
|
||||
|
||||
- HTML5
|
||||
- CSS3
|
||||
- JavaScript (ES6+)
|
||||
- ECharts 图表库
|
||||
- Fetch API
|
||||
@@ -0,0 +1,412 @@
|
||||
/* 基础样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px 0;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #45a049;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #1976D2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
background-color: #ff7043;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background-color: #f4511e;
|
||||
}
|
||||
|
||||
/* 看板选择器样式 */
|
||||
.dashboard-selector {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.dashboard-selector h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.dashboard-card h3 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.dashboard-card p {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.dashboard-card .tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dashboard-card .charts-count {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 看板详情样式 */
|
||||
.dashboard-detail {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
margin-left: 15px;
|
||||
color: #333;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.chart-container h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 5% auto;
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item h4 {
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.search-result-item p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-result-item .type {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.search-result-item .type.chart {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.search-result-item .type.dashboard {
|
||||
background-color: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateY(-50px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 10% auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #ddd;
|
||||
border-top: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.error {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #c62828;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>水务管理系统 - BI看板</title>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<script src="lib/echarts.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<h1>水务管理系统 BI 看板</h1>
|
||||
<div class="header-actions">
|
||||
<button id="refreshBtn" class="btn btn-primary">刷新数据</button>
|
||||
<button id="searchBtn" class="btn btn-secondary">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 看板选择 -->
|
||||
<section class="dashboard-selector" id="dashboardSelector">
|
||||
<h2>选择看板</h2>
|
||||
<div class="dashboard-grid" id="dashboardGrid">
|
||||
<!-- 看板卡片将通过JavaScript动态加载 -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 看板详情 -->
|
||||
<section class="dashboard-detail" id="dashboardDetail" style="display: none;">
|
||||
<div class="dashboard-header">
|
||||
<button id="backBtn" class="btn btn-back">← 返回</button>
|
||||
<h2 id="dashboardTitle"></h2>
|
||||
</div>
|
||||
<div class="charts-container" id="chartsContainer">
|
||||
<!-- 图表将通过JavaScript动态加载 -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 搜索模态框 -->
|
||||
<div id="searchModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>搜索看板和图表</h3>
|
||||
<button id="closeSearchBtn" class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="text" id="searchInput" placeholder="输入搜索关键词..." class="search-input">
|
||||
<div id="searchResults" class="search-results">
|
||||
<!-- 搜索结果将在这里显示 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript文件 -->
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/charts.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* API 封装模块
|
||||
* 提供与后端API的交互功能
|
||||
*/
|
||||
|
||||
class BIAPIClient {
|
||||
constructor(baseURL = '') {
|
||||
this.baseURL = baseURL || window.location.origin;
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, defaultOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API请求失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// GET请求
|
||||
async get(endpoint, params = {}) {
|
||||
const url = new URL(`${this.baseURL}${endpoint}`, window.location.origin);
|
||||
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] !== undefined && params[key] !== null) {
|
||||
url.searchParams.append(key, params[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return this.request(url.pathname + url.search);
|
||||
}
|
||||
|
||||
// POST请求
|
||||
async post(endpoint, data = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
async put(endpoint, data = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
async delete(endpoint) {
|
||||
return this.request(endpoint, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// BI相关API
|
||||
async getOverviewDashboards() {
|
||||
return this.get('/bi-api/dashboards/overview');
|
||||
}
|
||||
|
||||
async getDashboardData(dashboardId) {
|
||||
return this.get(`/bi-api/dashboards/${dashboardId}/data`);
|
||||
}
|
||||
|
||||
async getChartData(chartId) {
|
||||
return this.get(`/bi-api/charts/${chartId}/data`);
|
||||
}
|
||||
|
||||
async searchBIObjects(keyword) {
|
||||
return this.get('/bi-api/search', { keyword });
|
||||
}
|
||||
|
||||
async getChartTypes() {
|
||||
return this.get('/bi-api/charts/types');
|
||||
}
|
||||
|
||||
async getDataSourceTypes() {
|
||||
return this.get('/bi-api/data-sources/types');
|
||||
}
|
||||
|
||||
async getPopularTags() {
|
||||
return this.get('/bi-api/popular-tags');
|
||||
}
|
||||
|
||||
async getQuickStats() {
|
||||
return this.get('/bi-api/quick-stats');
|
||||
}
|
||||
|
||||
async getChartSuggestions() {
|
||||
return this.get('/bi-api/chart-suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局API客户端实例
|
||||
const apiClient = new BIAPIClient();
|
||||
|
||||
/**
|
||||
* 数据获取函数
|
||||
*/
|
||||
|
||||
// 获取概览看板列表
|
||||
export async function getDashboards() {
|
||||
try {
|
||||
return await apiClient.getOverviewDashboards();
|
||||
} catch (error) {
|
||||
console.error('获取看板列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取看板详细数据
|
||||
export async function getDashboardDetail(dashboardId) {
|
||||
try {
|
||||
return await apiClient.getDashboardData(dashboardId);
|
||||
} catch (error) {
|
||||
console.error(`获取看板 ${dashboardId} 数据失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图表数据
|
||||
export async function getChartData(chartId) {
|
||||
try {
|
||||
return await apiClient.getChartData(chartId);
|
||||
} catch (error) {
|
||||
console.error(`获取图表 ${chartId} 数据失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索BI对象
|
||||
export async function searchBIObjects(keyword) {
|
||||
try {
|
||||
return await apiClient.searchBIObjects(keyword);
|
||||
} catch (error) {
|
||||
console.error(`搜索 ${keyword} 失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取支持的图表类型
|
||||
export async function getChartTypes() {
|
||||
try {
|
||||
return await apiClient.getChartTypes();
|
||||
} catch (error) {
|
||||
console.error('获取图表类型失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取支持的数据源类型
|
||||
export async function getDataSourceTypes() {
|
||||
try {
|
||||
return await apiClient.getDataSourceTypes();
|
||||
} catch (error) {
|
||||
console.error('获取数据源类型失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取热门标签
|
||||
export async function getPopularTags() {
|
||||
try {
|
||||
return await apiClient.getPopularTags();
|
||||
} catch (error) {
|
||||
console.error('获取热门标签失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取快速统计信息
|
||||
export async function getQuickStats() {
|
||||
try {
|
||||
return await apiClient.getQuickStats();
|
||||
} catch (error) {
|
||||
console.error('获取快速统计失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图表建议
|
||||
export async function getChartSuggestions() {
|
||||
try {
|
||||
return await apiClient.getChartSuggestions();
|
||||
} catch (error) {
|
||||
console.error('获取图表建议失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数
|
||||
*/
|
||||
|
||||
// 格式化日期
|
||||
export function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
export function formatNumber(num, decimals = 2) {
|
||||
return parseFloat(num).toFixed(decimals);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
export function showLoading(element) {
|
||||
element.innerHTML = '<div class="loading">加载中...</div>';
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
export function showError(element, message) {
|
||||
element.innerHTML = `<div class="error">${message}</div>`;
|
||||
}
|
||||
|
||||
// 显示空状态
|
||||
export function showEmpty(element, message = '暂无数据') {
|
||||
element.innerHTML = `<div class="empty">${message}</div>`;
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
export function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
export function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* 图表绘制模块
|
||||
* 使用ECharts绘制各种图表
|
||||
*/
|
||||
|
||||
class ChartRenderer {
|
||||
constructor() {
|
||||
this.charts = new Map(); // 存储图表实例
|
||||
}
|
||||
|
||||
// 创建图表容器
|
||||
createChartContainer(chartId, title) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'chart-container';
|
||||
container.innerHTML = `
|
||||
<h3>${title}</h3>
|
||||
<div class="chart" id="chart-${chartId}"></div>
|
||||
`;
|
||||
return container;
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
initChart(chartId, options = {}) {
|
||||
const chartDom = document.getElementById(`chart-${chartId}`);
|
||||
if (!chartDom) {
|
||||
console.error(`图表容器 ${chartId} 不存在`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 清除之前的图表实例
|
||||
if (this.charts.has(chartId)) {
|
||||
this.charts.get(chartId).dispose();
|
||||
}
|
||||
|
||||
const chart = echarts.init(chartDom);
|
||||
this.charts.set(chartId, chart);
|
||||
|
||||
// 应用配置
|
||||
chart.setOption(options);
|
||||
|
||||
// 响应式处理
|
||||
const resizeHandler = () => {
|
||||
chart.resize();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
|
||||
// 保存resize处理函数以便后续清理
|
||||
chartDom._resizeHandler = resizeHandler;
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
// 销毁图表
|
||||
destroyChart(chartId) {
|
||||
const chart = this.charts.get(chartId);
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
this.charts.delete(chartId);
|
||||
|
||||
// 移除resize监听器
|
||||
const chartDom = document.getElementById(`chart-${chartId}`);
|
||||
if (chartDom && chartDom._resizeHandler) {
|
||||
window.removeEventListener('resize', chartDom._resizeHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制折线图
|
||||
renderLineChart(chartId, data, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: options.title || '趋势图',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: options.legendData || ['数据'],
|
||||
top: 'bottom'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map(item => item[options.xAxis || 'timestamp']),
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: options.yAxisName || '数值'
|
||||
},
|
||||
series: options.seriesData || [{
|
||||
name: '数据',
|
||||
type: 'line',
|
||||
data: data.map(item => item.value),
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.1
|
||||
}
|
||||
}],
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
}
|
||||
};
|
||||
|
||||
const mergedOptions = this.mergeOptions(defaultOptions, options);
|
||||
this.initChart(chartId, mergedOptions);
|
||||
}
|
||||
|
||||
// 绘制柱状图
|
||||
renderBarChart(chartId, data, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: options.title || '柱状图',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map(item => item[options.xAxis || 'category']),
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: options.yAxisName || '数值'
|
||||
},
|
||||
series: [{
|
||||
name: options.seriesName || '数据',
|
||||
type: 'bar',
|
||||
data: data.map(item => item.value),
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#83bff6' },
|
||||
{ offset: 0.5, color: '#188df0' },
|
||||
{ offset: 1, color: '#188df0' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#2378f7' },
|
||||
{ offset: 0.7, color: '#2378f7' },
|
||||
{ offset: 1, color: '#83bff6' }
|
||||
])
|
||||
}
|
||||
}
|
||||
}],
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
}
|
||||
};
|
||||
|
||||
const mergedOptions = this.mergeOptions(defaultOptions, options);
|
||||
this.initChart(chartId, mergedOptions);
|
||||
}
|
||||
|
||||
// 绘制饼图
|
||||
renderPieChart(chartId, data, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: options.title || '饼图',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
top: 'middle'
|
||||
},
|
||||
series: [{
|
||||
name: options.seriesName || '数据',
|
||||
type: 'pie',
|
||||
radius: options.radius || ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '20',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: data.map(item => ({
|
||||
name: item.name,
|
||||
value: item.value
|
||||
}))
|
||||
}]
|
||||
};
|
||||
|
||||
const mergedOptions = this.mergeOptions(defaultOptions, options);
|
||||
this.initChart(chartId, mergedOptions);
|
||||
}
|
||||
|
||||
// 绘制散点图
|
||||
renderScatterChart(chartId, data, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: options.title || '散点图',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: options.xAxisName || 'X轴'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: options.yAxisName || 'Y轴'
|
||||
},
|
||||
series: [{
|
||||
name: options.seriesName || '数据',
|
||||
type: 'scatter',
|
||||
data: data.map(item => [item.x, item.y]),
|
||||
symbolSize: function (data) {
|
||||
return Math.sqrt(data[2]) || 10;
|
||||
},
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.RadialGradient(0.5, 0.5, 1, [
|
||||
{ offset: 0, color: 'rgba(58,77,233,0.8)' },
|
||||
{ offset: 1, color: 'rgba(58,77,233,0.1)' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
const mergedOptions = this.mergeOptions(defaultOptions, options);
|
||||
this.initChart(chartId, mergedOptions);
|
||||
}
|
||||
|
||||
// 绘制面积图
|
||||
renderAreaChart(chartId, data, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: options.title || '面积图',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: options.legendData || ['数据'],
|
||||
top: 'bottom'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map(item => item[options.xAxis || 'timestamp']),
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: options.yAxisName || '数值'
|
||||
},
|
||||
series: [{
|
||||
name: options.seriesName || '数据',
|
||||
type: 'line',
|
||||
data: data.map(item => item.value),
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(128, 255, 165, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(1, 191, 236, 0.1)' }
|
||||
])
|
||||
}
|
||||
}],
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
}
|
||||
};
|
||||
|
||||
const mergedOptions = this.mergeOptions(defaultOptions, options);
|
||||
this.initChart(chartId, mergedOptions);
|
||||
}
|
||||
|
||||
// 绘制仪表盘
|
||||
renderGaugeChart(chartId, data, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: options.title || '仪表盘',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
formatter: '{b}: {c}%'
|
||||
},
|
||||
series: [{
|
||||
name: options.seriesName || '数据',
|
||||
type: 'gauge',
|
||||
min: options.min || 0,
|
||||
max: options.max || 100,
|
||||
splitNumber: options.splitNumber || 10,
|
||||
radius: options.radius || '80%',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 10,
|
||||
color: [
|
||||
[0.3, '#ff6e76'],
|
||||
[0.7, '#fddd60'],
|
||||
[1, '#7cffb2']
|
||||
]
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
itemStyle: {
|
||||
color: 'auto'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
distance: -15,
|
||||
length: 8,
|
||||
lineStyle: {
|
||||
color: '#999',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
distance: -20,
|
||||
length: 15,
|
||||
lineStyle: {
|
||||
color: '#999',
|
||||
width: 3
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#999',
|
||||
distance: 30,
|
||||
fontSize: 12
|
||||
},
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
fontSize: 20,
|
||||
offsetCenter: [0, '60%']
|
||||
},
|
||||
data: [{
|
||||
value: data.value || 0,
|
||||
name: options.name || '数据'
|
||||
}]
|
||||
}]
|
||||
};
|
||||
|
||||
const mergedOptions = this.mergeOptions(defaultOptions, options);
|
||||
this.initChart(chartId, mergedOptions);
|
||||
}
|
||||
|
||||
// 绘制表格
|
||||
renderTableChart(chartId, data, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: options.title || '表格',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
show: false
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map(item => item[options.xAxis || 'name']),
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: options.seriesName || '数据',
|
||||
type: 'bar',
|
||||
data: data.map(item => item.value),
|
||||
itemStyle: {
|
||||
color: '#5470c6'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
const mergedOptions = this.mergeOptions(defaultOptions, options);
|
||||
this.initChart(chartId, defaultOptions); // 表格不需要复杂配置
|
||||
}
|
||||
|
||||
// 合并配置选项
|
||||
mergeOptions(defaultOptions, customOptions) {
|
||||
return {
|
||||
...defaultOptions,
|
||||
...customOptions,
|
||||
series: customOptions.series || defaultOptions.series
|
||||
};
|
||||
}
|
||||
|
||||
// 根据图表类型渲染图表
|
||||
renderChartByType(chartId, chartData) {
|
||||
const { chart_type: chartType, data, options = {} } = chartData;
|
||||
|
||||
switch (chartType) {
|
||||
case 'line':
|
||||
this.renderLineChart(chartId, data, options);
|
||||
break;
|
||||
case 'bar':
|
||||
this.renderBarChart(chartId, data, options);
|
||||
break;
|
||||
case 'pie':
|
||||
this.renderPieChart(chartId, data, options);
|
||||
break;
|
||||
case 'scatter':
|
||||
this.renderScatterChart(chartId, data, options);
|
||||
break;
|
||||
case 'area':
|
||||
this.renderAreaChart(chartId, data, options);
|
||||
break;
|
||||
case 'gauge':
|
||||
this.renderGaugeChart(chartId, data, options);
|
||||
break;
|
||||
case 'table':
|
||||
this.renderTableChart(chartId, data, options);
|
||||
break;
|
||||
default:
|
||||
// 默认使用折线图
|
||||
this.renderLineChart(chartId, data, options);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量渲染图表
|
||||
renderCharts(chartsData) {
|
||||
chartsData.forEach(chartData => {
|
||||
const { chart_id: chartId, chart_name: chartName, chart_type: chartType, data, options = {} } = chartData;
|
||||
|
||||
const chartContainer = this.createChartContainer(chartId, chartName);
|
||||
document.getElementById('chartsContainer').appendChild(chartContainer);
|
||||
|
||||
this.renderChartByType(chartId, {
|
||||
chart_type: chartType,
|
||||
data,
|
||||
options: {
|
||||
...options,
|
||||
title: chartName
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 清理所有图表
|
||||
clearAllCharts() {
|
||||
this.charts.forEach((chart, chartId) => {
|
||||
this.destroyChart(chartId);
|
||||
});
|
||||
|
||||
const chartsContainer = document.getElementById('chartsContainer');
|
||||
if (chartsContainer) {
|
||||
chartsContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局图表渲染器实例
|
||||
const chartRenderer = new ChartRenderer();
|
||||
|
||||
export default chartRenderer;
|
||||
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
BI前端API模块
|
||||
为前端提供BI相关的API接口,简化前端调用
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from typing import List, Optional, Dict, Any
|
||||
import json
|
||||
|
||||
# 创建BI路由器
|
||||
router = APIRouter(prefix="/bi-api", tags=["BI API"])
|
||||
|
||||
@router.get("/dashboards/overview")
|
||||
async def get_overview_dashboards():
|
||||
"""获取概览看板数据"""
|
||||
return {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "operation_overview",
|
||||
"name": "水务运营总览",
|
||||
"description": "水务系统整体运营情况综合看板",
|
||||
"charts_count": 4,
|
||||
"is_public": True,
|
||||
"tags": ["运营", "总览", "综合"]
|
||||
},
|
||||
{
|
||||
"id": "device_management",
|
||||
"name": "设备管理看板",
|
||||
"description": "设备状态监控和维护管理",
|
||||
"charts_count": 1,
|
||||
"is_public": False,
|
||||
"tags": ["设备", "管理", "监控"]
|
||||
},
|
||||
{
|
||||
"id": "security_monitoring",
|
||||
"name": "安全监控看板",
|
||||
"description": "系统安全和警报监控",
|
||||
"charts_count": 1,
|
||||
"is_public": False,
|
||||
"tags": ["安全", "监控", "警报"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@router.get("/charts/{chart_id}/data")
|
||||
async def get_chart_data_frontend(chart_id: str):
|
||||
"""获取图表数据(前端友好格式)"""
|
||||
# 这里调用BI服务获取数据,简化前端调用
|
||||
try:
|
||||
from ..bi.services import BIService
|
||||
bi_service = BIService()
|
||||
data = bi_service.get_chart_data_api(chart_id)
|
||||
|
||||
if "error" in data:
|
||||
raise HTTPException(status_code=404, detail=data["error"])
|
||||
|
||||
# 格式化为前端友好的数据格式
|
||||
formatted_data = {
|
||||
"chartId": chart_id,
|
||||
"chartName": data.get("chart_name", ""),
|
||||
"chartType": data.get("chart_type", ""),
|
||||
"data": data.get("data", []),
|
||||
"options": data.get("options", {}),
|
||||
"columns": data.get("columns", [])
|
||||
}
|
||||
|
||||
return formatted_data
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/dashboards/{dashboard_id}/data")
|
||||
async def get_dashboard_data_frontend(dashboard_id: str):
|
||||
"""获取看板数据(前端友好格式)"""
|
||||
try:
|
||||
from ..bi.services import BIService
|
||||
bi_service = BIService()
|
||||
data = bi_service.get_dashboard_data_api(dashboard_id)
|
||||
|
||||
if "error" in data:
|
||||
raise HTTPException(status_code=404, detail=data["error"])
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/charts/types")
|
||||
async def get_chart_types():
|
||||
"""获取支持的图表类型"""
|
||||
return {
|
||||
"line": {"name": "折线图", "description": "适合展示趋势数据"},
|
||||
"bar": {"name": "柱状图", "description": "适合展示分类数据"},
|
||||
"pie": {"name": "饼图", "description": "适合展示比例数据"},
|
||||
"scatter": {"name": "散点图", "description": "适合展示关系数据"},
|
||||
"area": {"name": "面积图", "description": "适合展示累计数据"},
|
||||
"gauge": {"name": "仪表盘", "description": "适合展示进度或状态"},
|
||||
"table": {"name": "表格", "description": "适合展示详细数据"},
|
||||
"heatmap": {"name": "热力图", "description": "适合展示密度数据"}
|
||||
}
|
||||
|
||||
@router.get("/data-sources/types")
|
||||
async def get_data_source_types():
|
||||
"""获取支持的数据源类型"""
|
||||
return {
|
||||
"sensor_data": {"name": "传感器数据", "description": "IoT传感器实时和历史数据"},
|
||||
"device_data": {"name": "设备数据", "description": "设备状态和配置信息"},
|
||||
"alert_data": {"name": "警报数据", "description": "系统警报和通知记录"},
|
||||
"system_stats": {"name": "系统统计", "description": "系统运行性能统计"},
|
||||
"batch_data": {"name": "批量数据", "description": "批量导入的数据"}
|
||||
}
|
||||
|
||||
@router.get("/search")
|
||||
async def search_bi_objects(keyword: str = Query(..., description="搜索关键词")):
|
||||
"""搜索BI对象(图表和看板)"""
|
||||
try:
|
||||
from ..bi.services import BIService
|
||||
bi_service = BIService()
|
||||
|
||||
# 搜索图表
|
||||
charts = bi_service.search_charts(keyword)
|
||||
charts_data = [chart.to_dict() for chart in charts]
|
||||
|
||||
# 搜索看板
|
||||
dashboards = bi_service.search_dashboards(keyword)
|
||||
dashboards_data = [dashboard.to_dict() for dashboard in dashboards]
|
||||
|
||||
return {
|
||||
"charts": charts_data,
|
||||
"dashboards": dashboards_data,
|
||||
"total": len(charts_data) + len(dashboards_data)
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/popular-tags")
|
||||
async def get_popular_tags():
|
||||
"""获取热门标签"""
|
||||
try:
|
||||
from ..bi.services import BIService
|
||||
bi_service = BIService()
|
||||
|
||||
# 收集所有标签
|
||||
all_tags = set()
|
||||
for chart in bi_service.get_all_charts():
|
||||
all_tags.update(chart.tags)
|
||||
for dashboard in bi_service.get_all_dashboards():
|
||||
all_tags.update(dashboard.tags)
|
||||
|
||||
# 返回热门标签(按字母排序)
|
||||
return {"tags": sorted(list(all_tags))}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/quick-stats")
|
||||
async def get_quick_stats():
|
||||
"""获取快速统计信息"""
|
||||
try:
|
||||
from ..bi.services import BIService
|
||||
bi_service = BIService()
|
||||
|
||||
charts = bi_service.get_all_charts()
|
||||
dashboards = bi_service.get_all_dashboards()
|
||||
public_dashboards = bi_service.get_public_dashboards()
|
||||
|
||||
# 统计图表类型分布
|
||||
chart_type_stats = {}
|
||||
for chart in charts:
|
||||
chart_type = chart.chart_type.value
|
||||
chart_type_stats[chart_type] = chart_type_stats.get(chart_type, 0) + 1
|
||||
|
||||
# 统计标签分布
|
||||
tag_stats = {}
|
||||
for chart in charts:
|
||||
for tag in chart.tags:
|
||||
tag_stats[tag] = tag_stats.get(tag, 0) + 1
|
||||
for dashboard in dashboards:
|
||||
for tag in dashboard.tags:
|
||||
tag_stats[tag] = tag_stats.get(tag, 0) + 1
|
||||
|
||||
return {
|
||||
"total_charts": len(charts),
|
||||
"total_dashboards": len(dashboards),
|
||||
"public_dashboards": len(public_dashboards),
|
||||
"chart_types": chart_type_stats,
|
||||
"popular_tags": dict(sorted(tag_stats.items(), key=lambda x: x[1], reverse=True)[:10])
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/chart-suggestions")
|
||||
async def get_chart_suggestions():
|
||||
"""获取图表建议"""
|
||||
return {
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "flow_analysis",
|
||||
"name": "流量分析建议",
|
||||
"description": "基于历史流量数据,分析流量趋势和异常",
|
||||
"charts": ["flow_trend", "flow_comparison"],
|
||||
"tags": ["流量", "分析", "趋势"]
|
||||
},
|
||||
{
|
||||
"id": "device_performance",
|
||||
"name": "设备性能分析",
|
||||
"description": "分析设备运行状态和性能指标",
|
||||
"charts": ["device_status_distribution", "device_uptime"],
|
||||
"tags": ["设备", "性能", "分析"]
|
||||
},
|
||||
{
|
||||
"id": "security_dashboard",
|
||||
"name": "安全监控看板",
|
||||
"description": "集中监控系统安全和警报信息",
|
||||
"charts": ["alert_level_stats", "alert_trend"],
|
||||
"tags": ["安全", "监控", "警报"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,6 +11,14 @@ import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 导入BI模块
|
||||
from ..bi.controllers import router as bi_router
|
||||
from .bi_api import router as bi_api_router
|
||||
|
||||
# 将BI路由添加到主应用
|
||||
app.include_router(bi_router)
|
||||
app.include_router(bi_api_router)
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(title="Water Management System Data API", version="1.0.0")
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
BI模块 - 自助BI看板和数据可视化
|
||||
集成Superset/Metabase功能
|
||||
"""
|
||||
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
BI控制器模块
|
||||
提供REST API接口,支持自助BI看板和数据可视化
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from .services import BIService
|
||||
from .models import ChartType, DataSourceType
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter(prefix="/bi", tags=["BI"])
|
||||
|
||||
# 创建BI服务实例
|
||||
bi_service = BIService()
|
||||
|
||||
@router.get("/")
|
||||
async def get_bi_info():
|
||||
"""获取BI系统信息"""
|
||||
return {
|
||||
"message": "Water Management System BI API",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"charts": "/bi/charts",
|
||||
"dashboards": "/bi/dashboards",
|
||||
"data_sources": "/bi/data-sources",
|
||||
"datasets": "/bi/datasets"
|
||||
}
|
||||
}
|
||||
|
||||
# 图表相关接口
|
||||
@router.get("/charts", response_model=List[Dict[str, Any]])
|
||||
async def get_all_charts():
|
||||
"""获取所有图表"""
|
||||
charts = bi_service.get_all_charts()
|
||||
return [chart.to_dict() for chart in charts]
|
||||
|
||||
@router.get("/charts/{chart_id}", response_model=Dict[str, Any])
|
||||
async def get_chart(chart_id: str = Path(..., description="图表ID")):
|
||||
"""获取单个图表"""
|
||||
chart = bi_service.get_chart(chart_id)
|
||||
if not chart:
|
||||
raise HTTPException(status_code=404, detail="Chart not found")
|
||||
return chart.to_dict()
|
||||
|
||||
@router.post("/charts", response_model=Dict[str, Any])
|
||||
async def create_chart(chart_data: Dict[str, Any]):
|
||||
"""创建图表"""
|
||||
try:
|
||||
chart = bi_service.create_chart(chart_data)
|
||||
return chart.to_dict()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.put("/charts/{chart_id}", response_model=Dict[str, Any])
|
||||
async def update_chart(chart_id: str = Path(..., description="图表ID"), chart_data: Dict[str, Any] = None):
|
||||
"""更新图表"""
|
||||
if not chart_data:
|
||||
raise HTTPException(status_code=400, detail="Chart data is required")
|
||||
|
||||
chart = bi_service.update_chart(chart_id, chart_data)
|
||||
if not chart:
|
||||
raise HTTPException(status_code=404, detail="Chart not found")
|
||||
return chart.to_dict()
|
||||
|
||||
@router.delete("/charts/{chart_id}")
|
||||
async def delete_chart(chart_id: str = Path(..., description="图表ID")):
|
||||
"""删除图表"""
|
||||
success = bi_service.delete_chart(chart_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Chart not found")
|
||||
return {"message": "Chart deleted successfully"}
|
||||
|
||||
@router.get("/charts/{chart_id}/data", response_model=Dict[str, Any])
|
||||
async def get_chart_data(chart_id: str = Path(..., description="图表ID")):
|
||||
"""获取图表数据"""
|
||||
data = bi_service.get_chart_data_api(chart_id)
|
||||
if "error" in data:
|
||||
raise HTTPException(status_code=404, detail=data["error"])
|
||||
return data
|
||||
|
||||
# 看板相关接口
|
||||
@router.get("/dashboards", response_model=List[Dict[str, Any]])
|
||||
async def get_all_dashboards():
|
||||
"""获取所有看板"""
|
||||
dashboards = bi_service.get_all_dashboards()
|
||||
return [dashboard.to_dict() for dashboard in dashboards]
|
||||
|
||||
@router.get("/dashboards/{dashboard_id}", response_model=Dict[str, Any])
|
||||
async def get_dashboard(dashboard_id: str = Path(..., description="看板ID")):
|
||||
"""获取单个看板"""
|
||||
dashboard = bi_service.get_dashboard(dashboard_id)
|
||||
if not dashboard:
|
||||
raise HTTPException(status_code=404, detail="Dashboard not found")
|
||||
return dashboard.to_dict()
|
||||
|
||||
@router.post("/dashboards", response_model=Dict[str, Any])
|
||||
async def create_dashboard(dashboard_data: Dict[str, Any]):
|
||||
"""创建看板"""
|
||||
try:
|
||||
dashboard = bi_service.create_dashboard(dashboard_data)
|
||||
return dashboard.to_dict()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.put("/dashboards/{dashboard_id}", response_model=Dict[str, Any])
|
||||
async def update_dashboard(dashboard_id: str = Path(..., description="看板ID"), dashboard_data: Dict[str, Any] = None):
|
||||
"""更新看板"""
|
||||
if not dashboard_data:
|
||||
raise HTTPException(status_code=400, detail="Dashboard data is required")
|
||||
|
||||
dashboard = bi_service.update_dashboard(dashboard_id, dashboard_data)
|
||||
if not dashboard:
|
||||
raise HTTPException(status_code=404, detail="Dashboard not found")
|
||||
return dashboard.to_dict()
|
||||
|
||||
@router.delete("/dashboards/{dashboard_id}")
|
||||
async def delete_dashboard(dashboard_id: str = Path(..., description="看板ID")):
|
||||
"""删除看板"""
|
||||
success = bi_service.delete_dashboard(dashboard_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Dashboard not found")
|
||||
return {"message": "Dashboard deleted successfully"}
|
||||
|
||||
@router.get("/dashboards/{dashboard_id}/data", response_model=Dict[str, Any])
|
||||
async def get_dashboard_data(dashboard_id: str = Path(..., description="看板ID")):
|
||||
"""获取看板数据"""
|
||||
data = bi_service.get_dashboard_data_api(dashboard_id)
|
||||
if "error" in data:
|
||||
raise HTTPException(status_code=404, detail=data["error"])
|
||||
return data
|
||||
|
||||
# 数据源相关接口
|
||||
@router.get("/data-sources", response_model=List[Dict[str, Any]])
|
||||
async def get_all_data_sources():
|
||||
"""获取所有数据源"""
|
||||
data_sources = bi_service.get_all_data_sources()
|
||||
return [source.to_dict() for source in data_sources]
|
||||
|
||||
@router.get("/data-sources/{source_id}", response_model=Dict[str, Any])
|
||||
async def get_data_source(source_id: str = Path(..., description="数据源ID")):
|
||||
"""获取单个数据源"""
|
||||
source = bi_service.get_data_source(source_id)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="Data source not found")
|
||||
return source.to_dict()
|
||||
|
||||
# 数据集相关接口
|
||||
@router.get("/datasets", response_model=List[Dict[str, Any]])
|
||||
async def get_all_datasets():
|
||||
"""获取所有数据集"""
|
||||
datasets = bi_service.get_all_datasets()
|
||||
return [dataset.to_dict() for dataset in datasets]
|
||||
|
||||
@router.get("/datasets/{dataset_id}", response_model=Dict[str, Any])
|
||||
async def get_dataset(dataset_id: str = Path(..., description="数据集ID")):
|
||||
"""获取单个数据集"""
|
||||
dataset = bi_service.get_dataset(dataset_id)
|
||||
if not dataset:
|
||||
raise HTTPException(status_code=404, detail="Dataset not found")
|
||||
return dataset.to_dict()
|
||||
|
||||
# 搜索相关接口
|
||||
@router.get("/search/charts")
|
||||
async def search_charts(keyword: str = Query(..., description="搜索关键词")):
|
||||
"""搜索图表"""
|
||||
charts = bi_service.search_charts(keyword)
|
||||
return [chart.to_dict() for chart in charts]
|
||||
|
||||
@router.get("/search/dashboards")
|
||||
async def search_dashboards(keyword: str = Query(..., description="搜索关键词")):
|
||||
"""搜索看板"""
|
||||
dashboards = bi_service.search_dashboards(keyword)
|
||||
return [dashboard.to_dict() for dashboard in dashboards]
|
||||
|
||||
# 标签相关接口
|
||||
@router.get("/charts/tag/{tag}")
|
||||
async def get_charts_by_tag(tag: str = Path(..., description="标签")):
|
||||
"""根据标签获取图表"""
|
||||
charts = bi_service.get_charts_by_tag(tag)
|
||||
return [chart.to_dict() for chart in charts]
|
||||
|
||||
@router.get("/dashboards/tag/{tag}")
|
||||
async def get_dashboards_by_tag(tag: str = Path(..., description="标签")):
|
||||
"""根据标签获取看板"""
|
||||
dashboards = bi_service.get_dashboards_by_tag(tag)
|
||||
return [dashboard.to_dict() for dashboard in dashboards]
|
||||
|
||||
# 公开看板接口
|
||||
@router.get("/public/dashboards")
|
||||
async def get_public_dashboards():
|
||||
"""获取公开看板"""
|
||||
dashboards = bi_service.get_public_dashboards()
|
||||
return [dashboard.to_dict() for dashboard in dashboards]
|
||||
|
||||
# 图表类型和枚举
|
||||
@router.get("/types/chart-types")
|
||||
async def get_chart_types():
|
||||
"""获取支持的图表类型"""
|
||||
return [{"value": ct.value, "label": ct.name} for ct in ChartType]
|
||||
|
||||
@router.get("/types/data-source-types")
|
||||
async def get_data_source_types():
|
||||
"""获取支持的数据源类型"""
|
||||
return [{"value": dst.value, "label": dst.name} for dst in DataSourceType]
|
||||
|
||||
# 默认数据接口
|
||||
@router.get("/default-charts")
|
||||
async def get_default_charts():
|
||||
"""获取默认图表"""
|
||||
# 运营总览相关的图表
|
||||
default_chart_ids = ["flow_trend", "device_status_distribution", "alert_level_stats", "system_performance"]
|
||||
charts = [bi_service.get_chart(chart_id).to_dict() for chart_id in default_chart_ids if bi_service.get_chart(chart_id)]
|
||||
return charts
|
||||
|
||||
@router.get("/default-dashboards")
|
||||
async def get_default_dashboards():
|
||||
"""获取默认看板"""
|
||||
# 运营总览看板
|
||||
default_dashboard_ids = ["operation_overview", "device_management", "security_monitoring"]
|
||||
dashboards = [bi_service.get_dashboard(db_id).to_dict() for db_id in default_dashboard_ids if bi_service.get_dashboard(db_id)]
|
||||
return dashboards
|
||||
|
||||
# Superset集成接口
|
||||
@router.post("/integrations/superset")
|
||||
async def setup_superset_integration(integration_data: Dict[str, Any]):
|
||||
"""设置Superset集成"""
|
||||
try:
|
||||
integration = bi_service.setup_superset_integration(integration_data)
|
||||
return integration.to_dict()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.get("/integrations/superset")
|
||||
async def get_superset_integration():
|
||||
"""获取Superset集成配置"""
|
||||
if not bi_service.superset_integration:
|
||||
raise HTTPException(status_code=404, detail="Superset integration not configured")
|
||||
return bi_service.superset_integration.to_dict()
|
||||
|
||||
# Metabase集成接口
|
||||
@router.post("/integrations/metabase")
|
||||
async def setup_metabase_integration(integration_data: Dict[str, Any]):
|
||||
"""设置Metabase集成"""
|
||||
try:
|
||||
integration = bi_service.setup_metabase_integration(integration_data)
|
||||
return integration.to_dict()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.get("/integrations/metabase")
|
||||
async def get_metabase_integration():
|
||||
"""获取Metabase集成配置"""
|
||||
if not bi_service.metabase_integration:
|
||||
raise HTTPException(status_code=404, detail="Metabase integration not configured")
|
||||
return bi_service.metabase_integration.to_dict()
|
||||
|
||||
# 统计信息接口
|
||||
@router.get("/stats")
|
||||
async def get_bi_stats():
|
||||
"""获取BI系统统计信息"""
|
||||
return {
|
||||
"total_charts": len(bi_service.charts),
|
||||
"total_dashboards": len(bi_service.dashboards),
|
||||
"total_data_sources": len(bi_service.data_sources),
|
||||
"total_datasets": len(bi_service.datasets),
|
||||
"public_dashboards": len(bi_service.get_public_dashboards()),
|
||||
"integrations": {
|
||||
"superset_configured": bi_service.superset_integration is not None,
|
||||
"metabase_configured": bi_service.metabase_integration is not None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
BI数据模型定义
|
||||
定义自助BI看板和可视化相关数据结构
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
class ChartType(Enum):
|
||||
"""图表类型枚举"""
|
||||
LINE = "line" # 折线图
|
||||
BAR = "bar" # 柱状图
|
||||
PIE = "pie" # 饼图
|
||||
SCATTER = "scatter" # 散点图
|
||||
AREA = "area" # 面积图
|
||||
GAUGE = "gauge" # 仪表盘
|
||||
TABLE = "table" # 表格
|
||||
HEATMAP = "heatmap" # 热力图
|
||||
|
||||
class DataSourceType(Enum):
|
||||
"""数据源类型枚举"""
|
||||
SENSOR_DATA = "sensor_data" # 传感器数据
|
||||
DEVICE_DATA = "device_data" # 设备数据
|
||||
ALERT_DATA = "alert_data" # 警报数据
|
||||
BATCH_DATA = "batch_data" # 批量导入数据
|
||||
SYSTEM_STATS = "system_stats" # 系统统计数据
|
||||
|
||||
@dataclass
|
||||
class Chart:
|
||||
"""图表模型"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
chart_type: ChartType
|
||||
data_source: DataSourceType
|
||||
x_axis: str # X轴字段
|
||||
y_axis: List[str] # Y轴字段列表
|
||||
filters: Dict[str, Any] = field(default_factory=dict)
|
||||
group_by: List[str] = field(default_factory=list)
|
||||
aggregation: str = "sum" # sum, avg, max, min, count
|
||||
time_range: Optional[Dict[str, datetime]] = None
|
||||
options: Dict[str, Any] = field(default_factory=dict)
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
created_by: str = "system"
|
||||
is_public: bool = False
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"chart_type": self.chart_type.value,
|
||||
"data_source": self.data_source.value,
|
||||
"x_axis": self.x_axis,
|
||||
"y_axis": self.y_axis,
|
||||
"filters": self.filters,
|
||||
"group_by": self.group_by,
|
||||
"aggregation": self.aggregation,
|
||||
"time_range": {
|
||||
"start": self.time_range["start"].isoformat() if self.time_range and "start" in self.time_range else None,
|
||||
"end": self.time_range["end"].isoformat() if self.time_range and "end" in self.time_range else None
|
||||
} if self.time_range else None,
|
||||
"options": self.options,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"created_by": self.created_by,
|
||||
"is_public": self.is_public,
|
||||
"tags": self.tags
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Chart':
|
||||
"""从字典创建对象"""
|
||||
time_range = None
|
||||
if data.get("time_range"):
|
||||
tr = data["time_range"]
|
||||
time_range = {
|
||||
"start": datetime.fromisoformat(tr["start"]) if tr.get("start") else None,
|
||||
"end": datetime.fromisoformat(tr["end"]) if tr.get("end") else None
|
||||
}
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
chart_type=ChartType(data["chart_type"]),
|
||||
data_source=DataSourceType(data["data_source"]),
|
||||
x_axis=data["x_axis"],
|
||||
y_axis=data["y_axis"],
|
||||
filters=data.get("filters", {}),
|
||||
group_by=data.get("group_by", []),
|
||||
aggregation=data.get("aggregation", "sum"),
|
||||
time_range=time_range,
|
||||
options=data.get("options", {}),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
created_by=data.get("created_by", "system"),
|
||||
is_public=data.get("is_public", False),
|
||||
tags=data.get("tags", [])
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class Dashboard:
|
||||
"""看板模型"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
charts: List[str] # 图表ID列表
|
||||
layout: List[Dict[str, Any]] = field(default_factory=list) # 布局配置
|
||||
filters: Dict[str, Any] = field(default_factory=dict)
|
||||
shared_users: List[str] = field(default_factory=list)
|
||||
shared_groups: List[str] = field(default_factory=list)
|
||||
is_public: bool = False
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
created_by: str = "system"
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"charts": self.charts,
|
||||
"layout": self.layout,
|
||||
"filters": self.filters,
|
||||
"shared_users": self.shared_users,
|
||||
"shared_groups": self.shared_groups,
|
||||
"is_public": self.is_public,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"created_by": self.created_by,
|
||||
"tags": self.tags
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Dashboard':
|
||||
"""从字典创建对象"""
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
charts=data.get("charts", []),
|
||||
layout=data.get("layout", []),
|
||||
filters=data.get("filters", {}),
|
||||
shared_users=data.get("shared_users", []),
|
||||
shared_groups=data.get("shared_groups", []),
|
||||
is_public=data.get("is_public", False),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
created_by=data.get("created_by", "system"),
|
||||
tags=data.get("tags", [])
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class DataSource:
|
||||
"""数据源模型"""
|
||||
id: str
|
||||
name: str
|
||||
type: DataSourceType
|
||||
description: str
|
||||
config: Dict[str, Any] = field(default_factory=dict)
|
||||
query_template: str = ""
|
||||
columns: List[str] = field(default_factory=list)
|
||||
refresh_interval_minutes: int = 60
|
||||
is_active: bool = True
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
created_by: str = "system"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"type": self.type.value,
|
||||
"description": self.description,
|
||||
"config": self.config,
|
||||
"query_template": self.query_template,
|
||||
"columns": self.columns,
|
||||
"refresh_interval_minutes": self.refresh_interval_minutes,
|
||||
"is_active": self.is_active,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"created_by": self.created_by
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'DataSource':
|
||||
"""从字典创建对象"""
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
type=DataSourceType(data["type"]),
|
||||
description=data["description"],
|
||||
config=data.get("config", {}),
|
||||
query_template=data.get("query_template", ""),
|
||||
columns=data.get("columns", []),
|
||||
refresh_interval_minutes=data.get("refresh_interval_minutes", 60),
|
||||
is_active=data.get("is_active", True),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
created_by=data.get("created_by", "system")
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class Dataset:
|
||||
"""数据集模型"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
data_source_id: str
|
||||
query: str
|
||||
columns: List[Dict[str, Any]] = field(default_factory=list) # 字段定义
|
||||
transformations: List[str] = field(default_factory=list) # 数据转换规则
|
||||
cache_enabled: bool = True
|
||||
cache_timeout_minutes: int = 30
|
||||
is_active: bool = True
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
created_by: str = "system"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"data_source_id": self.data_source_id,
|
||||
"query": self.query,
|
||||
"columns": self.columns,
|
||||
"transformations": self.transformations,
|
||||
"cache_enabled": self.cache_enabled,
|
||||
"cache_timeout_minutes": self.cache_timeout_minutes,
|
||||
"is_active": self.is_active,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"created_by": self.created_by
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Dataset':
|
||||
"""从字典创建对象"""
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
data_source_id=data["data_source_id"],
|
||||
query=data["query"],
|
||||
columns=data.get("columns", []),
|
||||
transformations=data.get("transformations", []),
|
||||
cache_enabled=data.get("cache_enabled", True),
|
||||
cache_timeout_minutes=data.get("cache_timeout_minutes", 30),
|
||||
is_active=data.get("is_active", True),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
created_by=data.get("created_by", "system")
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class SupersetIntegration:
|
||||
"""Superset集成配置"""
|
||||
superset_url: str
|
||||
superset_api_key: str
|
||||
superset_username: str
|
||||
dashboard_mapping: Dict[str, str] = field(default_factory=dict) # 本地dashboard -> Superset dashboard
|
||||
chart_mapping: Dict[str, str] = field(default_factory=dict) # 本地chart -> Superset chart
|
||||
sync_enabled: bool = True
|
||||
sync_interval_minutes: int = 60
|
||||
last_sync_at: Optional[datetime] = None
|
||||
sync_status: str = "idle" # idle, syncing, error
|
||||
error_message: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"superset_url": self.superset_url,
|
||||
"superset_api_key": self.superset_api_key,
|
||||
"superset_username": self.superset_username,
|
||||
"dashboard_mapping": self.dashboard_mapping,
|
||||
"chart_mapping": self.chart_mapping,
|
||||
"sync_enabled": self.sync_enabled,
|
||||
"sync_interval_minutes": self.sync_interval_minutes,
|
||||
"last_sync_at": self.last_sync_at.isoformat() if self.last_sync_at else None,
|
||||
"sync_status": self.sync_status,
|
||||
"error_message": self.error_message
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class MetabaseIntegration:
|
||||
"""Metabase集成配置"""
|
||||
metabase_url: str
|
||||
metabase_secret_key: str
|
||||
collection_name: str = "水务管理系统"
|
||||
dashboard_mapping: Dict[str, str] = field(default_factory=dict)
|
||||
question_mapping: Dict[str, str] = field(default_factory=dict)
|
||||
sync_enabled: bool = True
|
||||
sync_interval_minutes: int = 60
|
||||
last_sync_at: Optional[datetime] = None
|
||||
sync_status: str = "idle"
|
||||
error_message: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"metabase_url": self.metabase_url,
|
||||
"metabase_secret_key": self.metabase_secret_key,
|
||||
"collection_name": self.collection_name,
|
||||
"dashboard_mapping": self.dashboard_mapping,
|
||||
"question_mapping": self.question_mapping,
|
||||
"sync_enabled": self.sync_enabled,
|
||||
"sync_interval_minutes": self.sync_interval_minutes,
|
||||
"last_sync_at": self.last_sync_at.isoformat() if self.last_sync_at else None,
|
||||
"sync_status": self.sync_status,
|
||||
"error_message": self.error_message
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
"""
|
||||
BI服务模块
|
||||
提供自助BI看板和数据可视化服务
|
||||
包括数据集管理、图表创建、看板配置等功能
|
||||
"""
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from .models import (
|
||||
Chart, ChartType, DataSourceType, Dashboard,
|
||||
DataSource, Dataset, SupersetIntegration, MetabaseIntegration
|
||||
)
|
||||
|
||||
class BIService:
|
||||
"""BI服务主类"""
|
||||
|
||||
def __init__(self):
|
||||
# 初始化数据存储
|
||||
self.charts: Dict[str, Chart] = {}
|
||||
self.dashboards: Dict[str, Dashboard] = {}
|
||||
self.data_sources: Dict[str, DataSource] = {}
|
||||
self.datasets: Dict[str, Dataset] = {}
|
||||
self.superset_integration: Optional[SupersetIntegration] = None
|
||||
self.metabase_integration: Optional[MetabaseIntegration] = None
|
||||
|
||||
# 初始化默认数据源
|
||||
self._init_default_data_sources()
|
||||
self._init_default_charts()
|
||||
self._init_default_dashboards()
|
||||
|
||||
def _init_default_data_sources(self):
|
||||
"""初始化默认数据源"""
|
||||
# 传感器数据源
|
||||
sensor_source = DataSource(
|
||||
id="sensor_data",
|
||||
name="传感器数据",
|
||||
type=DataSourceType.SENSOR_DATA,
|
||||
description="所有IoT传感器实时和历史数据",
|
||||
config={
|
||||
"time_field": "timestamp",
|
||||
"value_field": "value",
|
||||
"device_field": "device_id",
|
||||
"location_field": "location",
|
||||
"type_field": "data_type"
|
||||
},
|
||||
query_template="SELECT * FROM sensor_data WHERE {filters}",
|
||||
columns=[
|
||||
{"name": "id", "type": "integer", "description": "数据记录ID"},
|
||||
{"name": "device_id", "type": "string", "description": "设备ID"},
|
||||
{"name": "data_type", "type": "string", "description": "数据类型"},
|
||||
{"name": "value", "type": "float", "description": "数值"},
|
||||
{"name": "unit", "type": "string", "description": "单位"},
|
||||
{"name": "timestamp", "type": "datetime", "description": "时间戳"},
|
||||
{"name": "location", "type": "string", "description": "位置"},
|
||||
{"name": "quality_score", "type": "float", "description": "质量评分"}
|
||||
]
|
||||
)
|
||||
self.data_sources[sensor_source.id] = sensor_source
|
||||
|
||||
# 设备状态数据源
|
||||
device_source = DataSource(
|
||||
id="device_data",
|
||||
name="设备状态",
|
||||
type=DataSourceType.DEVICE_DATA,
|
||||
description="所有设备的运行状态和配置信息",
|
||||
config={
|
||||
"status_field": "status",
|
||||
"type_field": "device_type",
|
||||
"location_field": "location"
|
||||
},
|
||||
query_template="SELECT * FROM device WHERE {filters}",
|
||||
columns=[
|
||||
{"name": "id", "type": "string", "description": "设备ID"},
|
||||
{"name": "name", "type": "string", "description": "设备名称"},
|
||||
{"name": "device_type", "type": "string", "description": "设备类型"},
|
||||
{"name": "location", "type": "string", "description": "位置"},
|
||||
{"name": "status", "type": "string", "description": "状态"},
|
||||
{"name": "install_date", "type": "datetime", "description": "安装日期"},
|
||||
{"name": "metadata", "type": "json", "description": "元数据"}
|
||||
]
|
||||
)
|
||||
self.data_sources[device_source.id] = device_source
|
||||
|
||||
# 警报数据源
|
||||
alert_source = DataSource(
|
||||
id="alert_data",
|
||||
name="警报数据",
|
||||
type=DataSourceType.ALERT_DATA,
|
||||
description="系统警报和通知记录",
|
||||
config={
|
||||
"level_field": "level",
|
||||
"type_field": "alert_type",
|
||||
"resolved_field": "resolved"
|
||||
},
|
||||
query_template="SELECT * FROM alert WHERE {filters}",
|
||||
columns=[
|
||||
{"name": "id", "type": "string", "description": "警报ID"},
|
||||
{"name": "device_id", "type": "string", "description": "设备ID"},
|
||||
{"name": "alert_type", "type": "string", "description": "警报类型"},
|
||||
{"name": "level", "type": "string", "description": "警报级别"},
|
||||
{"name": "message", "type": "string", "description": "警报信息"},
|
||||
{"name": "timestamp", "type": "datetime", "description": "发生时间"},
|
||||
{"name": "resolved", "type": "boolean", "description": "是否已解决"},
|
||||
{"name": "resolved_at", "type": "datetime", "description": "解决时间"}
|
||||
]
|
||||
)
|
||||
self.data_sources[alert_source.id] = alert_source
|
||||
|
||||
# 系统统计数据源
|
||||
stats_source = DataSource(
|
||||
id="system_stats",
|
||||
name="系统统计",
|
||||
type=DataSourceType.SYSTEM_STATS,
|
||||
description="系统运行性能和使用统计",
|
||||
config={
|
||||
"cpu_field": "cpu_usage_percent",
|
||||
"memory_field": "memory_usage_mb",
|
||||
"records_field": "total_records"
|
||||
},
|
||||
query_template="SELECT * FROM system_stats WHERE {filters}",
|
||||
columns=[
|
||||
{"name": "timestamp", "type": "datetime", "description": "统计时间"},
|
||||
{"name": "total_records", "type": "integer", "description": "总记录数"},
|
||||
{"name": "total_devices", "type": "integer", "description": "设备总数"},
|
||||
{"name": "active_connections", "type": "integer", "description": "活跃连接数"},
|
||||
{"name": "api_requests_count", "type": "integer", "description": "API请求数"},
|
||||
{"name": "alerts_count", "type": "integer", "description": "警报数量"},
|
||||
{"name": "data_quality_score", "type": "float", "description": "数据质量评分"},
|
||||
{"name": "memory_usage_mb", "type": "float", "description": "内存使用量(MB)"},
|
||||
{"name": "cpu_usage_percent", "type": "float", "description": "CPU使用率(%)"}
|
||||
]
|
||||
)
|
||||
self.data_sources[stats_source.id] = stats_source
|
||||
|
||||
def _init_default_charts(self):
|
||||
"""初始化默认图表"""
|
||||
# 流量趋势图
|
||||
flow_trend = Chart(
|
||||
id="flow_trend",
|
||||
name="流量趋势分析",
|
||||
description="显示各区域流量随时间的变化趋势",
|
||||
chart_type=ChartType.LINE,
|
||||
data_source=DataSourceType.SENSOR_DATA,
|
||||
x_axis="timestamp",
|
||||
y_axis=["value"],
|
||||
group_by=["location"],
|
||||
aggregation="avg",
|
||||
filters={"data_type": "LL"},
|
||||
options={
|
||||
"title": "各区域流量趋势",
|
||||
"yAxis": {"title": "流量 (m³/h)"},
|
||||
"xAxis": {"title": "时间"},
|
||||
"legend": {"show": True},
|
||||
"tooltip": {"trigger": "axis"}
|
||||
},
|
||||
tags=["流量", "趋势", "区域"]
|
||||
)
|
||||
self.charts[flow_trend.id] = flow_trend
|
||||
|
||||
# 设备状态分布图
|
||||
device_status = Chart(
|
||||
id="device_status_distribution",
|
||||
name="设备状态分布",
|
||||
description="显示不同状态设备的数量分布",
|
||||
chart_type=ChartType.PIE,
|
||||
data_source=DataSourceType.DEVICE_DATA,
|
||||
x_axis="status",
|
||||
y_axis=["count"],
|
||||
aggregation="count",
|
||||
options={
|
||||
"title": "设备状态分布",
|
||||
"legend": {"show": True},
|
||||
"tooltip": {"trigger": "item"}
|
||||
},
|
||||
tags=["设备", "状态", "分布"]
|
||||
)
|
||||
self.charts[device_status.id] = device_status
|
||||
|
||||
# 警报级别统计图
|
||||
alert_stats = Chart(
|
||||
id="alert_level_stats",
|
||||
name="警报级别统计",
|
||||
description="按级别统计警报数量",
|
||||
chart_type=ChartType.BAR,
|
||||
data_source=DataSourceType.ALERT_DATA,
|
||||
x_axis="level",
|
||||
y_axis=["count"],
|
||||
aggregation="count",
|
||||
filters={"resolved": False},
|
||||
options={
|
||||
"title": "未解决警报按级别统计",
|
||||
"yAxis": {"title": "数量"},
|
||||
"xAxis": {"title": "警报级别"},
|
||||
"legend": {"show": False}
|
||||
},
|
||||
tags=["警报", "级别", "统计"]
|
||||
)
|
||||
self.charts[alert_stats.id] = alert_stats
|
||||
|
||||
# 系统性能监控图
|
||||
system_performance = Chart(
|
||||
id="system_performance",
|
||||
name="系统性能监控",
|
||||
description="显示系统CPU和内存使用率趋势",
|
||||
chart_type=ChartType.LINE,
|
||||
data_source=DataSourceType.SYSTEM_STATS,
|
||||
x_axis="timestamp",
|
||||
y_axis=["cpu_usage_percent", "memory_usage_mb"],
|
||||
options={
|
||||
"title": "系统性能监控",
|
||||
"yAxis": [{"title": "CPU使用率(%)"}, {"title": "内存使用量(MB)"}],
|
||||
"xAxis": {"title": "时间"},
|
||||
"legend": {"show": True},
|
||||
"tooltip": {"trigger": "axis"}
|
||||
},
|
||||
tags=["系统", "性能", "监控"]
|
||||
)
|
||||
self.charts[system_performance.id] = system_performance
|
||||
|
||||
def _init_default_dashboards(self):
|
||||
"""初始化默认看板"""
|
||||
# 水务运营总览看板
|
||||
overview_dashboard = Dashboard(
|
||||
id="operation_overview",
|
||||
name="水务运营总览",
|
||||
description="水务系统整体运营情况综合看板",
|
||||
charts=["flow_trend", "device_status_distribution", "alert_level_stats", "system_performance"],
|
||||
layout=[
|
||||
{"i": "flow_trend", "x": 0, "y": 0, "w": 12, "h": 8},
|
||||
{"i": "device_status_distribution", "x": 12, "y": 0, "w": 6, "h": 6},
|
||||
{"i": "alert_level_stats", "x": 18, "y": 0, "w": 6, "h": 6},
|
||||
{"i": "system_performance", "x": 0, "y": 8, "w": 24, "h": 8}
|
||||
],
|
||||
is_public=True,
|
||||
tags=["运营", "总览", "综合"]
|
||||
)
|
||||
self.dashboards[overview_dashboard.id] = overview_dashboard
|
||||
|
||||
# 设备管理看板
|
||||
device_dashboard = Dashboard(
|
||||
id="device_management",
|
||||
name="设备管理看板",
|
||||
description="设备状态监控和维护管理",
|
||||
charts=["device_status_distribution"],
|
||||
layout=[
|
||||
{"i": "device_status_distribution", "x": 0, "y": 0, "w": 12, "h": 8}
|
||||
],
|
||||
tags=["设备", "管理", "监控"]
|
||||
)
|
||||
self.dashboards[device_dashboard.id] = device_dashboard
|
||||
|
||||
# 安全监控看板
|
||||
security_dashboard = Dashboard(
|
||||
id="security_monitoring",
|
||||
name="安全监控看板",
|
||||
description="系统安全和警报监控",
|
||||
charts=["alert_level_stats"],
|
||||
layout=[
|
||||
{"i": "alert_level_stats", "x": 0, "y": 0, "w": 12, "h": 8}
|
||||
],
|
||||
tags=["安全", "监控", "警报"]
|
||||
)
|
||||
self.dashboards[security_dashboard.id] = security_dashboard
|
||||
|
||||
def get_chart(self, chart_id: str) -> Optional[Chart]:
|
||||
"""获取图表"""
|
||||
return self.charts.get(chart_id)
|
||||
|
||||
def get_all_charts(self) -> List[Chart]:
|
||||
"""获取所有图表"""
|
||||
return list(self.charts.values())
|
||||
|
||||
def create_chart(self, chart_data: Dict[str, Any]) -> Chart:
|
||||
"""创建图表"""
|
||||
chart = Chart.from_dict(chart_data)
|
||||
self.charts[chart.id] = chart
|
||||
return chart
|
||||
|
||||
def update_chart(self, chart_id: str, chart_data: Dict[str, Any]) -> Optional[Chart]:
|
||||
"""更新图表"""
|
||||
if chart_id in self.charts:
|
||||
chart = Chart.from_dict(chart_data)
|
||||
chart.id = chart_id # 保持ID不变
|
||||
self.charts[chart_id] = chart
|
||||
return chart
|
||||
return None
|
||||
|
||||
def delete_chart(self, chart_id: str) -> bool:
|
||||
"""删除图表"""
|
||||
if chart_id in self.charts:
|
||||
del self.charts[chart_id]
|
||||
# 从所有看板中移除该图表
|
||||
for dashboard in self.dashboards.values():
|
||||
if chart_id in dashboard.charts:
|
||||
dashboard.charts.remove(chart_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_dashboard(self, dashboard_id: str) -> Optional[Dashboard]:
|
||||
"""获取看板"""
|
||||
return self.dashboards.get(dashboard_id)
|
||||
|
||||
def get_all_dashboards(self) -> List[Dashboard]:
|
||||
"""获取所有看板"""
|
||||
return list(self.dashboards.values())
|
||||
|
||||
def create_dashboard(self, dashboard_data: Dict[str, Any]) -> Dashboard:
|
||||
"""创建看板"""
|
||||
dashboard = Dashboard.from_dict(dashboard_data)
|
||||
self.dashboards[dashboard.id] = dashboard
|
||||
return dashboard
|
||||
|
||||
def update_dashboard(self, dashboard_id: str, dashboard_data: Dict[str, Any]) -> Optional[Dashboard]:
|
||||
"""更新看板"""
|
||||
if dashboard_id in self.dashboards:
|
||||
dashboard = Dashboard.from_dict(dashboard_data)
|
||||
dashboard.id = dashboard_id # 保持ID不变
|
||||
self.dashboards[dashboard_id] = dashboard
|
||||
return dashboard
|
||||
return None
|
||||
|
||||
def delete_dashboard(self, dashboard_id: str) -> bool:
|
||||
"""删除看板"""
|
||||
if dashboard_id in self.dashboards:
|
||||
del self.dashboards[dashboard_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_data_source(self, source_id: str) -> Optional[DataSource]:
|
||||
"""获取数据源"""
|
||||
return self.data_sources.get(source_id)
|
||||
|
||||
def get_all_data_sources(self) -> List[DataSource]:
|
||||
"""获取所有数据源"""
|
||||
return list(self.data_sources.values())
|
||||
|
||||
def get_dataset(self, dataset_id: str) -> Optional[Dataset]:
|
||||
"""获取数据集"""
|
||||
return self.datasets.get(dataset_id)
|
||||
|
||||
def get_all_datasets(self) -> List[Dataset]:
|
||||
"""获取所有数据集"""
|
||||
return list(self.datasets.values())
|
||||
|
||||
def execute_chart_data(self, chart_id: str) -> Dict[str, Any]:
|
||||
"""执行图表数据查询"""
|
||||
chart = self.get_chart(chart_id)
|
||||
if not chart:
|
||||
return {"error": "Chart not found"}
|
||||
|
||||
# 这里模拟数据查询,实际应该连接到数据库或数据源
|
||||
data = self._generate_chart_data(chart)
|
||||
|
||||
return {
|
||||
"chart_id": chart_id,
|
||||
"chart_name": chart.name,
|
||||
"data": data,
|
||||
"columns": chart.options.get("columns", []),
|
||||
"chart_type": chart.chart_type.value,
|
||||
"options": chart.options
|
||||
}
|
||||
|
||||
def _generate_chart_data(self, chart: Chart) -> List[Dict[str, Any]]:
|
||||
"""生成图表数据(模拟)"""
|
||||
# 根据图表类型和数据源生成模拟数据
|
||||
if chart.data_source == DataSourceType.SENSOR_DATA:
|
||||
return self._generate_sensor_data(chart)
|
||||
elif chart.data_source == DataSourceType.DEVICE_DATA:
|
||||
return self._generate_device_data(chart)
|
||||
elif chart.data_source == DataSourceType.ALERT_DATA:
|
||||
return self._generate_alert_data(chart)
|
||||
elif chart.data_source == DataSourceType.SYSTEM_STATS:
|
||||
return self._generate_stats_data(chart)
|
||||
else:
|
||||
return []
|
||||
|
||||
def _generate_sensor_data(self, chart: Chart) -> List[Dict[str, Any]]:
|
||||
"""生成传感器数据"""
|
||||
data = []
|
||||
|
||||
# 生成时间序列数据
|
||||
base_time = datetime.now() - timedelta(days=7)
|
||||
locations = ["A区", "B区", "C区", "D区"]
|
||||
|
||||
for i in range(24 * 7): # 7天,每小时一个点
|
||||
timestamp = base_time + timedelta(hours=i)
|
||||
|
||||
for location in locations:
|
||||
# 添加一些随机波动
|
||||
base_value = 50 if chart.filters.get("data_type") == "LL" else 1.0
|
||||
value = base_value + np.random.normal(0, 10)
|
||||
|
||||
data.append({
|
||||
"timestamp": timestamp.isoformat(),
|
||||
"location": location,
|
||||
"value": round(value, 2),
|
||||
"device_id": f"device_{hash(location) % 10 + 1}",
|
||||
"data_type": chart.filters.get("data_type", "LL"),
|
||||
"quality_score": round(np.random.uniform(0.8, 1.0), 2)
|
||||
})
|
||||
|
||||
# 应用过滤和聚合
|
||||
if chart.group_by:
|
||||
# 简单的分组聚合
|
||||
grouped_data = {}
|
||||
for item in data:
|
||||
key = tuple(item.get(field) for field in chart.group_by)
|
||||
if key not in grouped_data:
|
||||
grouped_data[key] = []
|
||||
grouped_data[key].append(item)
|
||||
|
||||
result = []
|
||||
for key, items in grouped_data.items():
|
||||
group_data = {}
|
||||
for i, field in enumerate(chart.group_by):
|
||||
group_data[field] = key[i]
|
||||
|
||||
# 聚合计算
|
||||
values = [item["value"] for item in items]
|
||||
if chart.aggregation == "avg":
|
||||
group_data["value"] = sum(values) / len(values)
|
||||
elif chart.aggregation == "sum":
|
||||
group_data["value"] = sum(values)
|
||||
elif chart.aggregation == "max":
|
||||
group_data["value"] = max(values)
|
||||
elif chart.aggregation == "min":
|
||||
group_data["value"] = min(values)
|
||||
else:
|
||||
group_data["value"] = sum(values) / len(values)
|
||||
|
||||
result.append(group_data)
|
||||
|
||||
return result
|
||||
|
||||
return data
|
||||
|
||||
def _generate_device_data(self, chart: Chart) -> List[Dict[str, Any]]:
|
||||
"""生成设备数据"""
|
||||
devices = [
|
||||
{"id": "device_1", "name": "流量计-001", "device_type": "流量计", "location": "A区", "status": "active"},
|
||||
{"id": "device_2", "name": "压力计-001", "device_type": "压力计", "location": "A区", "status": "active"},
|
||||
{"id": "device_3", "name": "水位计-001", "device_type": "水位计", "location": "B区", "status": "maintenance"},
|
||||
{"id": "device_4", "name": "浊度计-001", "device_type": "浊度计", "location": "B区", "status": "active"},
|
||||
{"id": "device_5", "name": "pH计-001", "device_type": "pH计", "location": "C区", "status": "inactive"},
|
||||
]
|
||||
|
||||
# 按状态分组
|
||||
status_groups = {}
|
||||
for device in devices:
|
||||
status = device["status"]
|
||||
if status not in status_groups:
|
||||
status_groups[status] = []
|
||||
status_groups[status].append(device)
|
||||
|
||||
# 生成统计数据
|
||||
result = []
|
||||
for status, devices_in_status in status_groups.items():
|
||||
result.append({
|
||||
"status": status,
|
||||
"count": len(devices_in_status),
|
||||
"devices": [d["name"] for d in devices_in_status]
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def _generate_alert_data(self, chart: Chart) -> List[Dict[str, Any]]:
|
||||
"""生成警报数据"""
|
||||
alerts = [
|
||||
{"level": "info", "count": 5, "description": "信息级别警报"},
|
||||
{"level": "warning", "count": 3, "description": "警告级别警报"},
|
||||
{"level": "error", "count": 1, "description": "错误级别警报"},
|
||||
{"level": "critical", "count": 0, "description": "严重级别警报"},
|
||||
]
|
||||
|
||||
return alerts
|
||||
|
||||
def _generate_stats_data(self, chart: Chart) -> List[Dict[str, Any]]:
|
||||
"""生成系统统计数据"""
|
||||
data = []
|
||||
base_time = datetime.now() - timedelta(days=1)
|
||||
|
||||
for i in range(24): # 24小时数据
|
||||
timestamp = base_time + timedelta(hours=i)
|
||||
|
||||
data.append({
|
||||
"timestamp": timestamp.isoformat(),
|
||||
"total_records": 1000 + np.random.randint(-100, 100),
|
||||
"total_devices": 25 + np.random.randint(-5, 5),
|
||||
"active_connections": 5 + np.random.randint(-2, 3),
|
||||
"api_requests_count": 150 + np.random.randint(-30, 30),
|
||||
"alerts_count": np.random.randint(0, 5),
|
||||
"data_quality_score": round(np.random.uniform(0.9, 1.0), 2),
|
||||
"memory_usage_mb": 100 + np.random.randint(-20, 20),
|
||||
"cpu_usage_percent": 30 + np.random.randint(-10, 10)
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
def setup_superset_integration(self, integration_data: Dict[str, Any]) -> SupersetIntegration:
|
||||
"""设置Superset集成"""
|
||||
integration = SupersetIntegration.from_dict(integration_data)
|
||||
self.superset_integration = integration
|
||||
return integration
|
||||
|
||||
def setup_metabase_integration(self, integration_data: Dict[str, Any]) -> MetabaseIntegration:
|
||||
"""设置Metabase集成"""
|
||||
integration = MetabaseIntegration.from_dict(integration_data)
|
||||
self.metabase_integration = integration
|
||||
return integration
|
||||
|
||||
def get_chart_data_api(self, chart_id: str) -> Dict[str, Any]:
|
||||
"""获取图表数据API接口"""
|
||||
return self.execute_chart_data(chart_id)
|
||||
|
||||
def get_dashboard_data_api(self, dashboard_id: str) -> Dict[str, Any]:
|
||||
"""获取看板数据API接口"""
|
||||
dashboard = self.get_dashboard(dashboard_id)
|
||||
if not dashboard:
|
||||
return {"error": "Dashboard not found"}
|
||||
|
||||
charts_data = {}
|
||||
for chart_id in dashboard.charts:
|
||||
charts_data[chart_id] = self.get_chart_data_api(chart_id)
|
||||
|
||||
return {
|
||||
"dashboard_id": dashboard_id,
|
||||
"dashboard_name": dashboard.name,
|
||||
"charts": charts_data,
|
||||
"layout": dashboard.layout
|
||||
}
|
||||
|
||||
def get_public_dashboards(self) -> List[Dashboard]:
|
||||
"""获取公开看板"""
|
||||
return [db for db in self.dashboards.values() if db.is_public]
|
||||
|
||||
def get_charts_by_tag(self, tag: str) -> List[Chart]:
|
||||
"""根据标签获取图表"""
|
||||
return [chart for chart in self.charts.values() if tag in chart.tags]
|
||||
|
||||
def get_dashboards_by_tag(self, tag: str) -> List[Dashboard]:
|
||||
"""根据标签获取看板"""
|
||||
return [dashboard for dashboard in self.dashboards.values() if tag in dashboard.tags]
|
||||
|
||||
def search_charts(self, keyword: str) -> List[Chart]:
|
||||
"""搜索图表"""
|
||||
keyword = keyword.lower()
|
||||
return [
|
||||
chart for chart in self.charts.values()
|
||||
if keyword in chart.name.lower() or keyword in chart.description.lower()
|
||||
or any(keyword in tag.lower() for tag in chart.tags)
|
||||
]
|
||||
|
||||
def search_dashboards(self, keyword: str) -> List[Dashboard]:
|
||||
"""搜索看板"""
|
||||
keyword = keyword.lower()
|
||||
return [
|
||||
dashboard for dashboard in self.dashboards.values()
|
||||
if keyword in dashboard.name.lower() or keyword in dashboard.description.lower()
|
||||
or any(keyword in tag.lower() for tag in dashboard.tags)
|
||||
]
|
||||
Reference in New Issue
Block a user