- 新增BI模块(src/bi/),包含数据模型、服务和控制器 - 支持数据源管理、图表创建、看板配置 - 实现多图表类型:折线图、柱状图、饼图、散点图、面积图、仪表盘、表格 - 提供REST API(/bi/)和前端API(/bi-api/)接口 - 创建响应式前端界面,支持拖拽和实时数据展示 - 默认包含运营总览、设备管理、安全监控看板 - 支持与Superset和Metabase集成 🤖 Generated with [OpenClaw](https://github.com/robocomp/openclaw)
510 lines
15 KiB
JavaScript
510 lines
15 KiB
JavaScript
/**
|
|
* 图表绘制模块
|
|
* 使用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; |