489 lines
14 KiB
Vue
489 lines
14 KiB
Vue
|
|
<!-- SidePanelItem.vue -->
|
|||
|
|
<template>
|
|||
|
|
<SidePanelItem title="水文监测" :select="select">
|
|||
|
|
<div class="chart-container">
|
|||
|
|
<div ref="chartRef" class="echarts-chart"></div>
|
|||
|
|
</div>
|
|||
|
|
</SidePanelItem>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
|||
|
|
import * as echarts from 'echarts';
|
|||
|
|
import type { EChartsOption } from 'echarts';
|
|||
|
|
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
|||
|
|
|
|||
|
|
// 定义组件名
|
|||
|
|
defineOptions({
|
|||
|
|
name: 'qixidiliuliangbianhua'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ==================== 选择器配置 ====================
|
|||
|
|
const select = ref({
|
|||
|
|
show: true,
|
|||
|
|
value: undefined,
|
|||
|
|
options: [],
|
|||
|
|
picker: undefined,
|
|||
|
|
format: undefined
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ==================== 图表相关响应式数据 ====================
|
|||
|
|
const chartRef = ref<HTMLElement | null>(null);
|
|||
|
|
let chartInstance: echarts.ECharts | null = null;
|
|||
|
|
|
|||
|
|
// 模拟图表数据(水温监测 - 12个月)
|
|||
|
|
const echartsData = ref<any[]>([
|
|||
|
|
{ dt: '2024-01', wt: 8.5, q: 120.5, z: 45.2 },
|
|||
|
|
{ dt: '2024-02', wt: 9.2, q: 135.8, z: 46.1 },
|
|||
|
|
{ dt: '2024-03', wt: 12.8, q: 180.3, z: 47.5 },
|
|||
|
|
{ dt: '2024-04', wt: 16.3, q: 220.6, z: 48.8 },
|
|||
|
|
{ dt: '2024-05', wt: 20.5, q: 280.2, z: 50.3 },
|
|||
|
|
{ dt: '2024-06', wt: 24.8, q: 350.5, z: 52.1 },
|
|||
|
|
{ dt: '2024-07', wt: 28.3, q: 420.8, z: 53.6 },
|
|||
|
|
{ dt: '2024-08', wt: 27.5, q: 390.3, z: 52.9 },
|
|||
|
|
{ dt: '2024-09', wt: 23.2, q: 310.7, z: 51.2 },
|
|||
|
|
{ dt: '2024-10', wt: 18.6, q: 240.5, z: 49.5 },
|
|||
|
|
{ dt: '2024-11', wt: 13.4, q: 165.2, z: 47.8 },
|
|||
|
|
{ dt: '2024-12', wt: 9.8, q: 130.6, z: 46.3 }
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// 图表配置引用(用于 legendselectchanged 时读取最新配置)
|
|||
|
|
const echartOptionRef = ref<EChartsOption>({});
|
|||
|
|
|
|||
|
|
// ==================== 工具函数:omit ====================
|
|||
|
|
const omit = (obj: any, key: string) => {
|
|||
|
|
const newObj = { ...obj };
|
|||
|
|
delete newObj[key];
|
|||
|
|
return newObj;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ==================== 多Y轴动态布局算法 ====================
|
|||
|
|
/**
|
|||
|
|
* 根据图例选中状态动态调整 Y 轴布局和 grid 边距
|
|||
|
|
* @param selected - 图例选中状态对象 { '系列名': boolean }
|
|||
|
|
* @param options - ECharts 配置对象
|
|||
|
|
*
|
|||
|
|
* 规则:
|
|||
|
|
* - 左侧最多 1 个 Y 轴(第一个可见系列)
|
|||
|
|
* - 右侧可以显示多个 Y 轴(其余可见系列)
|
|||
|
|
*/
|
|||
|
|
const yAxisShowDynamic = (selected: Record<string, boolean>, options: any) => {
|
|||
|
|
const allShow = options.yAxis?.filter((item: any) => item.show);
|
|||
|
|
const showCount = allShow?.length || 0;
|
|||
|
|
|
|||
|
|
// 没有显示的Y轴
|
|||
|
|
if (showCount === 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只有一个Y轴:统一置于左侧
|
|||
|
|
if (showCount === 1) {
|
|||
|
|
options.grid = omit(options.grid, 'right');
|
|||
|
|
options.grid.left = '80px';
|
|||
|
|
|
|||
|
|
options.yAxis = options.yAxis.map((item: any) => {
|
|||
|
|
if (item.show) {
|
|||
|
|
return {
|
|||
|
|
...item,
|
|||
|
|
position: 'left',
|
|||
|
|
offset: 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return item;
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 两个及以上Y轴:左侧1个,右侧其余
|
|||
|
|
if (showCount >= 2) {
|
|||
|
|
let leftIndex = 0; // 左侧Y轴索引
|
|||
|
|
let rightIndex = 0; // 右侧Y轴计数
|
|||
|
|
|
|||
|
|
options.yAxis = options.yAxis.map((item: any) => {
|
|||
|
|
if (!item.show) {
|
|||
|
|
return item;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 第一个可见的Y轴放在左侧
|
|||
|
|
if (leftIndex === 0) {
|
|||
|
|
leftIndex++;
|
|||
|
|
options.grid = omit(options.grid, 'right');
|
|||
|
|
options.grid.left = '80px';
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
...item,
|
|||
|
|
position: 'left',
|
|||
|
|
offset: 0
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
// 其余Y轴放在右侧
|
|||
|
|
rightIndex++;
|
|||
|
|
|
|||
|
|
// 右侧有多个Y轴时,需要增加右边距并设置偏移
|
|||
|
|
if (rightIndex > 1) {
|
|||
|
|
options.grid.right = '100px';
|
|||
|
|
return {
|
|||
|
|
...item,
|
|||
|
|
position: 'right',
|
|||
|
|
offset: 60
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
options.grid.right = '60px';
|
|||
|
|
return {
|
|||
|
|
...item,
|
|||
|
|
position: 'right',
|
|||
|
|
offset: 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ==================== 图表初始化 ====================
|
|||
|
|
const initChart = () => {
|
|||
|
|
if (!chartRef.value) {
|
|||
|
|
console.warn('图表容器未就绪');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化 ECharts 实例
|
|||
|
|
chartInstance = echarts.init(chartRef.value);
|
|||
|
|
|
|||
|
|
// 监听图例选择变化事件
|
|||
|
|
chartInstance.on('legendselectchanged', (params: any) => {
|
|||
|
|
const { selected } = params;
|
|||
|
|
|
|||
|
|
// 深拷贝当前配置
|
|||
|
|
const options = JSON.parse(JSON.stringify(echartOptionRef.value));
|
|||
|
|
|
|||
|
|
// 同步图例选中状态
|
|||
|
|
options.legend.selected = selected;
|
|||
|
|
|
|||
|
|
// 根据图例状态更新 Y 轴显隐
|
|||
|
|
const newYAxis = options.yAxis.map((item: any) => {
|
|||
|
|
let isShow = true;
|
|||
|
|
for (const key in selected) {
|
|||
|
|
if (key === item.name) {
|
|||
|
|
isShow = selected[key];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return { ...item, show: isShow };
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
options.yAxis = newYAxis;
|
|||
|
|
|
|||
|
|
// 触发布局调整
|
|||
|
|
yAxisShowDynamic(selected, options);
|
|||
|
|
|
|||
|
|
// 完全替换模式更新配置
|
|||
|
|
chartInstance?.setOption(options, true);
|
|||
|
|
echartOptionRef.value = options;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听数据点点击事件
|
|||
|
|
chartInstance.on('click', (params: any) => {
|
|||
|
|
console.log('点击数据点:', {
|
|||
|
|
date: params.name,
|
|||
|
|
seriesName: params.seriesName,
|
|||
|
|
value: params.value
|
|||
|
|
});
|
|||
|
|
// TODO: 可扩展为打开详情弹窗
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ==================== 图表配置生成 ====================
|
|||
|
|
const updateChart = () => {
|
|||
|
|
if (!chartInstance) return;
|
|||
|
|
|
|||
|
|
// 单位和颜色配置(可根据实际需求从配置系统获取)
|
|||
|
|
const Color = ['#5470c6', '#91cc75', '#fac858'];
|
|||
|
|
|
|||
|
|
const _legendData = ['水温(℃)', '流量(m³/s)', '水位(m)'];
|
|||
|
|
|
|||
|
|
const setting: EChartsOption = {
|
|||
|
|
// Tooltip 配置
|
|||
|
|
tooltip: {
|
|||
|
|
trigger: 'axis',
|
|||
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|||
|
|
borderColor: 'transparent',
|
|||
|
|
textStyle: {
|
|||
|
|
color: '#ffffff'
|
|||
|
|
},
|
|||
|
|
valueFormatter: (value: any) => (value === undefined ? '-' : value),
|
|||
|
|
formatter: (params: any) => {
|
|||
|
|
let res = `${params[0].name}<br/>`;
|
|||
|
|
params.forEach((item: any) => {
|
|||
|
|
const seriesName = item.seriesName ?? '';
|
|||
|
|
const regx = /\(([^()]+?)\)/;
|
|||
|
|
const unit = seriesName.match(regx);
|
|||
|
|
const finalValue = item.value !== undefined && item.value !== null ? item.value : '-';
|
|||
|
|
|
|||
|
|
if (item.value !== undefined && item.value !== null) {
|
|||
|
|
res += `<span style="background: ${item.color}; height:10px; width: 10px; border-radius: 50%; display: inline-block; margin-right:10px;"></span>${item.seriesName} ${finalValue}${unit?.[1] || ''}<br/>`;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return res;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 颜色配置
|
|||
|
|
color: Color,
|
|||
|
|
|
|||
|
|
// 图例配置
|
|||
|
|
legend: {
|
|||
|
|
data: _legendData,
|
|||
|
|
selectedMode: 'multiple',
|
|||
|
|
top: 0,
|
|||
|
|
left: 'center',
|
|||
|
|
itemGap: 15,
|
|||
|
|
// 默认只选中"水温",其他系列隐藏
|
|||
|
|
selected: {
|
|||
|
|
'水温(℃)': true,
|
|||
|
|
'流量(m³/s)': false,
|
|||
|
|
'水位(m)': false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
axisPointer: {
|
|||
|
|
type: 'shadow',
|
|||
|
|
label: {
|
|||
|
|
show: false
|
|||
|
|
},
|
|||
|
|
shadowStyle: {
|
|||
|
|
color: 'rgba(84, 112, 198, 0.2)'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
// 网格配置 - 为三个Y轴预留空间
|
|||
|
|
grid: {
|
|||
|
|
left: '0px',
|
|||
|
|
right: '60px',
|
|||
|
|
bottom: '10%',
|
|||
|
|
top: '65px'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 数据缩放配置
|
|||
|
|
dataZoom: {
|
|||
|
|
type: 'inside',
|
|||
|
|
start: 0,
|
|||
|
|
end: 100
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// X轴配置
|
|||
|
|
xAxis: {
|
|||
|
|
type: 'category',
|
|||
|
|
data: echartsData.value.map(item => item.dt),
|
|||
|
|
boundaryGap: true,
|
|||
|
|
axisLabel: {
|
|||
|
|
show: true
|
|||
|
|
},
|
|||
|
|
axisLine: {
|
|||
|
|
show: true
|
|||
|
|
},
|
|||
|
|
splitLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
type: 'solid',
|
|||
|
|
color: '#e0e0e0'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
axisPointer: {
|
|||
|
|
type: 'shadow',
|
|||
|
|
shadowStyle: {
|
|||
|
|
color: 'rgba(84, 112, 198, 0.2)'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// Y轴配置 - 三个独立Y轴
|
|||
|
|
yAxis: [
|
|||
|
|
{
|
|||
|
|
type: 'value',
|
|||
|
|
name: '水温(℃)',
|
|||
|
|
position: 'left',
|
|||
|
|
offset: 0,
|
|||
|
|
show: true,
|
|||
|
|
splitLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
type: 'solid',
|
|||
|
|
color: '#e0e0e0'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
axisTick: {
|
|||
|
|
show: true
|
|||
|
|
},
|
|||
|
|
axisLabel: {
|
|||
|
|
show: true,
|
|||
|
|
formatter: '{value}',
|
|||
|
|
color: '#5470c6'
|
|||
|
|
},
|
|||
|
|
axisLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
color: '#6e7079',
|
|||
|
|
width: 2
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
nameTextStyle: {
|
|||
|
|
color: '#5470c6',
|
|||
|
|
fontSize: 12
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: 'value',
|
|||
|
|
name: '流量(m³/s)',
|
|||
|
|
position: 'left',
|
|||
|
|
offset: 60,
|
|||
|
|
show: false,
|
|||
|
|
splitLine: { show: false },
|
|||
|
|
axisTick: {
|
|||
|
|
show: true
|
|||
|
|
},
|
|||
|
|
axisLabel: {
|
|||
|
|
show: true,
|
|||
|
|
formatter: '{value}',
|
|||
|
|
color: '#91cc75'
|
|||
|
|
},
|
|||
|
|
axisLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
color: '#6e7079',
|
|||
|
|
width: 2
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
nameTextStyle: {
|
|||
|
|
color: '#91cc75',
|
|||
|
|
fontSize: 12
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: 'value',
|
|||
|
|
name: '水位(m)',
|
|||
|
|
position: 'right',
|
|||
|
|
offset: 0,
|
|||
|
|
show: false,
|
|||
|
|
splitLine: { show: false },
|
|||
|
|
axisTick: {
|
|||
|
|
show: true
|
|||
|
|
},
|
|||
|
|
axisLabel: {
|
|||
|
|
show: true,
|
|||
|
|
formatter: '{value}',
|
|||
|
|
color: '#fac858'
|
|||
|
|
},
|
|||
|
|
axisLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
color: '#6e7079',
|
|||
|
|
width: 2
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
nameTextStyle: {
|
|||
|
|
color: '#fac858',
|
|||
|
|
fontSize: 12
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
|
|||
|
|
// 系列配置
|
|||
|
|
series: [
|
|||
|
|
{
|
|||
|
|
name: '水温(℃)',
|
|||
|
|
data: echartsData.value.map(item => item.wt),
|
|||
|
|
type: 'line',
|
|||
|
|
smooth: true,
|
|||
|
|
connectNulls: true,
|
|||
|
|
symbolSize: 4,
|
|||
|
|
symbol: 'circle',
|
|||
|
|
itemStyle: {
|
|||
|
|
color: '#5470c6'
|
|||
|
|
},
|
|||
|
|
lineStyle: {
|
|||
|
|
width: 2.5
|
|||
|
|
},
|
|||
|
|
yAxisIndex: 0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '流量(m³/s)',
|
|||
|
|
data: echartsData.value.map(item => item.q),
|
|||
|
|
type: 'line',
|
|||
|
|
smooth: true,
|
|||
|
|
connectNulls: true,
|
|||
|
|
symbolSize: 4,
|
|||
|
|
symbol: 'circle',
|
|||
|
|
itemStyle: {
|
|||
|
|
color: '#91cc75'
|
|||
|
|
},
|
|||
|
|
lineStyle: {
|
|||
|
|
width: 2.5
|
|||
|
|
},
|
|||
|
|
yAxisIndex: 1
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '水位(m)',
|
|||
|
|
data: echartsData.value.map(item => item.z),
|
|||
|
|
type: 'line',
|
|||
|
|
smooth: true,
|
|||
|
|
connectNulls: true,
|
|||
|
|
symbolSize: 4,
|
|||
|
|
symbol: 'circle',
|
|||
|
|
itemStyle: {
|
|||
|
|
color: '#fac858'
|
|||
|
|
},
|
|||
|
|
lineStyle: {
|
|||
|
|
width: 2.5
|
|||
|
|
},
|
|||
|
|
yAxisIndex: 2
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 保存配置引用
|
|||
|
|
echartOptionRef.value = setting;
|
|||
|
|
|
|||
|
|
// 完全替换模式更新图表
|
|||
|
|
chartInstance.setOption(setting, true);
|
|||
|
|
|
|||
|
|
// ✅ 初始化时应用动态布局算法,确保只有水温Y轴显示
|
|||
|
|
yAxisShowDynamic({
|
|||
|
|
'水温(℃)': true,
|
|||
|
|
'流量(m³/s)': false,
|
|||
|
|
'水位(m)': false
|
|||
|
|
}, setting);
|
|||
|
|
|
|||
|
|
// 再次应用调整后的配置
|
|||
|
|
chartInstance.setOption(setting, true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ==================== 生命周期钩子 ====================
|
|||
|
|
onMounted(async () => {
|
|||
|
|
// 双重等待机制:nextTick + setTimeout
|
|||
|
|
await nextTick();
|
|||
|
|
setTimeout(() => {
|
|||
|
|
initChart();
|
|||
|
|
updateChart();
|
|||
|
|
}, 50);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
// 销毁图表实例,防止内存泄漏
|
|||
|
|
if (chartInstance) {
|
|||
|
|
chartInstance.dispose();
|
|||
|
|
chartInstance = null;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.chart-container {
|
|||
|
|
height: 231px;
|
|||
|
|
min-height: 231px;
|
|||
|
|
width: 100%;
|
|||
|
|
position: relative;
|
|||
|
|
|
|||
|
|
.echarts-chart {
|
|||
|
|
height: 100%;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|