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:
2026-06-15 12:29:34 +08:00
parent 5eae031679
commit 61acfd8f8b
11 changed files with 2689 additions and 0 deletions
+50
View File
@@ -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
+412
View File
@@ -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;
}
+67
View File
@@ -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">&times;</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>
+267
View File
@@ -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);
}
};
}
+510
View File
@@ -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;
+215
View File
@@ -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": ["安全", "监控", "警报"]
}
]
}
+8
View File
@@ -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")
+4
View File
@@ -0,0 +1,4 @@
"""
BI模块 - 自助BI看板和数据可视化
集成Superset/Metabase功能
"""
+273
View File
@@ -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
}
}
+321
View File
@@ -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
}
+562
View File
@@ -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)
]