diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000..c8c1d233
--- /dev/null
+++ b/frontend/README.md
@@ -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
\ No newline at end of file
diff --git a/frontend/css/styles.css b/frontend/css/styles.css
new file mode 100644
index 00000000..1ff4413d
--- /dev/null
+++ b/frontend/css/styles.css
@@ -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;
+}
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..82dbffad
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ 水务管理系统 - BI看板
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/js/api.js b/frontend/js/api.js
new file mode 100644
index 00000000..0defcc2b
--- /dev/null
+++ b/frontend/js/api.js
@@ -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 = '加载中...
';
+}
+
+// 显示错误状态
+export function showError(element, message) {
+ element.innerHTML = `${message}
`;
+}
+
+// 显示空状态
+export function showEmpty(element, message = '暂无数据') {
+ element.innerHTML = `${message}
`;
+}
+
+// 防抖函数
+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);
+ }
+ };
+}
\ No newline at end of file
diff --git a/frontend/js/charts.js b/frontend/js/charts.js
new file mode 100644
index 00000000..c904b061
--- /dev/null
+++ b/frontend/js/charts.js
@@ -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 = `
+ ${title}
+
+ `;
+ 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;
\ No newline at end of file
diff --git a/src/api/bi_api.py b/src/api/bi_api.py
new file mode 100644
index 00000000..99bcb8eb
--- /dev/null
+++ b/src/api/bi_api.py
@@ -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": ["安全", "监控", "警报"]
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/src/api/rest_api.py b/src/api/rest_api.py
index d1180989..cc2e5478 100644
--- a/src/api/rest_api.py
+++ b/src/api/rest_api.py
@@ -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")
diff --git a/src/bi/__init__.py b/src/bi/__init__.py
new file mode 100644
index 00000000..a7d414e3
--- /dev/null
+++ b/src/bi/__init__.py
@@ -0,0 +1,4 @@
+"""
+BI模块 - 自助BI看板和数据可视化
+集成Superset/Metabase功能
+"""
\ No newline at end of file
diff --git a/src/bi/controllers.py b/src/bi/controllers.py
new file mode 100644
index 00000000..191e38c5
--- /dev/null
+++ b/src/bi/controllers.py
@@ -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
+ }
+ }
\ No newline at end of file
diff --git a/src/bi/models.py b/src/bi/models.py
new file mode 100644
index 00000000..d5a70d33
--- /dev/null
+++ b/src/bi/models.py
@@ -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
+ }
\ No newline at end of file
diff --git a/src/bi/services.py b/src/bi/services.py
new file mode 100644
index 00000000..597859d1
--- /dev/null
+++ b/src/bi/services.py
@@ -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)
+ ]
\ No newline at end of file