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;