WholeProcessPlatform/frontend/src/modules/qixidiliuliangbianhua/index.vue

814 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="水文监测" :select="select" :scopeDate="datetimePicker" @update-values="handlePanelChange1">
<div class="chart-wrapper">
<a-spin :spinning="loading">
<!-- 始终渲染图表容器确保有固定宽高 -->
<div ref="chartRef" class="echarts-chart"></div>
<!-- 无数据时显示Empty但不影响容器尺寸 -->
<div v-if="!loading && !hasData" class="empty-overlay">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
import { msstbprptGetKendoList, sdriverdaysGetKendoList } from '@/api/qxd';
import { useModelStore } from "@/store/modules/model";
// 定义组件名
defineOptions({
name: 'qixidiliuliangbianhua'
});
// ==================== 选择器配置 ====================
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('')
const modelStore = useModelStore();
// Loading 状态和数据状态
const loading = ref(false);
const hasData = ref(true); // 初始化为 true因为 initChart 时会渲染
const select = ref({
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined
});
// 日期选择器配置
const datetimePicker: any = ref({
show: true,
value: (() => {
const now = new Date();
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const lastMonthStr = `${lastMonth.getFullYear()}-${String(lastMonth.getMonth() + 1).padStart(2, '0')}`;
return [lastMonthStr, currentMonth];
})(),
format: 'YY-MM',
picker: 'month' as const,
options: []
});
// ==================== 图表相关响应式数据 ====================
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// 图表数据(初始为空数组)
const echartsData = ref<any[]>([]);
// 图表配置引用(用于 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 = '60px';
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 = '60px';
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;
// ✅ 方案A手动构建新配置对象保留所有函数引用包括formatter
const currentOptions = echartOptionRef.value as any;
const options: any = {
...currentOptions,
legend: {
...currentOptions.legend,
selected: selected
},
yAxis: Array.isArray(currentOptions.yAxis)
? currentOptions.yAxis.map((item: any) => {
let isShow = true;
for (const key in selected) {
if (key === item.name) {
isShow = selected[key];
}
}
return { ...item, show: isShow }; // ✅ 保留formatter等函数
})
: currentOptions.yAxis
};
// 触发布局调整
yAxisShowDynamic(selected, options);
// 完全替换模式更新配置
chartInstance?.setOption(options, true);
echartOptionRef.value = options;
});
// 监听数据点点击事件
chartInstance.on('click', (params: any) => {
// ✅ 判断是否点击的是数据点series中的点
if (params.componentType === 'series') {
modelStore.modalVisible = true;
modelStore.params.sttp = "fh_zq_point";
modelStore.title = "水文监测";
modelStore.params.stcd = select.value.value;
modelStore.showStcdSelector = true
modelStore.stcdOptions = select.value.options
}
});
};
// ==================== 图表配置生成 ====================
const updateChart = () => {
if (!chartInstance) return;
// ✅ 计算数据统计值用于Y轴动态范围
const qValues = echartsData.value.map(item => item.q).filter(v => v != null && !isNaN(v));
const zValues = echartsData.value.map(item => item.z).filter(v => v != null && !isNaN(v));
const vValues = echartsData.value.map(item => item.v).filter(v => v != null && !isNaN(v));
const minQ = qValues.length > 0 ? Math.min(...qValues) : 0;
const maxQ = qValues.length > 0 ? Math.max(...qValues) : 0;
const minZ = zValues.length > 0 ? Math.min(...zValues) : 0;
const maxZ = zValues.length > 0 ? Math.max(...zValues) : 0;
const minV = vValues.length > 0 ? Math.min(...vValues) : 0;
const maxV = vValues.length > 0 ? Math.max(...vValues) : 0;
// ✅ 颜色和Y轴配置
const Color = ['#9556a4', '#56c2e3', '#78c300'];
const _legendData = ['流量(m³/s)', '水位(m)', '流速(m/s)'];
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);
if (item.value !== undefined && item.value !== null) {
// ✅ 保留两位小数
const formattedValue = Number(item.value).toFixed(2);
res += `<span style="background: ${item.color}; height:10px; width: 10px; border-radius: 50%; display: inline-block; margin-right:10px;"></span>${item.seriesName} ${formattedValue}${unit?.[1] || ''}<br/>`;
} else {
res += `<span style="background: ${item.color}; height:10px; width: 10px; border-radius: 50%; display: inline-block; margin-right:10px;"></span>${item.seriesName} -${unit?.[1] || ''}<br/>`;
}
});
return res;
}
},
// ✅ 颜色配置:流量-紫色,水位-青色,流速-绿色
color: Color,
// 图例配置
legend: {
data: _legendData,
selectedMode: 'multiple',
top: 0,
left: 'center',
itemGap: 15,
// 默认只选中"水温",其他系列隐藏
selected: {
'流速(m/s)': false,
'流量(m³/s)': true,
'水位(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,
// ✅ X轴标签显示策略最多显示5个标签强制显示首尾
interval: (index: number) => {
const total = echartsData.value.length;
if (total <= 5) return true; // 数据少于等于5个全部显示
if (index === 0 || index === total - 1) return true; // 首尾必显
// 中间标签均匀分布
const step = Math.floor((total - 1) / 3); // 中间显示3个
return index % step === 0;
}
},
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: '流量(m³/s)',
position: 'left',
offset: 0,
show: true,
// ✅ 动态范围floor(min) ~ ceil(max)+5
min: qValues.length > 0 ? Math.floor(minQ) : undefined,
max: qValues.length > 0 ? Math.ceil(maxQ) : undefined,
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#e0e0e0'
}
},
axisTick: {
show: true
},
axisLabel: {
show: true,
formatter: '{value}',
color: '#9556a4'
},
axisLine: {
show: true,
lineStyle: {
color: '#6e7079',
width: 2
}
},
nameTextStyle: {
color: '#9556a4',
fontSize: 12
}
},
{
type: 'value',
name: '水位(m)',
position: 'left',
offset: 60,
show: false,
// ✅ 动态范围min-0.01 ~ max+0.04
min: zValues.length > 0 ? minZ : undefined,
max: zValues.length > 0 ? maxZ : undefined,
splitLine: { show: false },
axisTick: {
show: true
},
axisLabel: {
show: true,
// ✅ Y轴刻度保留两位小数使用函数形式
formatter: (value: any) => value.toFixed(2),
color: '#56c2e3'
},
axisLine: {
show: true,
lineStyle: {
color: '#6e7079',
width: 2
}
},
nameTextStyle: {
color: '#56c2e3',
fontSize: 12
}
},
{
type: 'value',
name: '流速(m/s)',
position: 'right',
offset: 0,
show: false,
// ✅ 动态范围min-0.2 ~ max+0.5
min: vValues.length > 0 ? minV - 0.02 : undefined,
max: vValues.length > 0 ? maxV + 0.02 : undefined,
splitLine: { show: false },
axisTick: {
show: true
},
axisLabel: {
show: true,
// ✅ Y轴刻度保留三位小数
formatter: (value: any) => value.toFixed(3),
color: '#78c300'
},
axisLine: {
show: true,
lineStyle: {
color: '#6e7079',
width: 2
}
},
nameTextStyle: {
color: '#78c300',
fontSize: 12
}
}
],
// 系列配置(按期望顺序:流量、水位、流速)
series: [
{
name: '流量(m³/s)',
data: echartsData.value.map(item => item.q),
type: 'line',
smooth: true,
// connectNulls: true,
symbolSize: 3,
symbol: 'circle',
itemStyle: {
color: '#9556a4'
},
lineStyle: {
width: 2
},
yAxisIndex: 0
},
{
name: '水位(m)',
data: echartsData.value.map(item => item.z),
type: 'line',
smooth: true,
// connectNulls: true,
symbolSize: 3,
symbol: 'circle',
itemStyle: {
color: '#56c2e3'
},
lineStyle: {
width: 2
},
yAxisIndex: 1
},
{
name: '流速(m/s)',
data: echartsData.value.map(item => item.v),
type: 'line',
smooth: true,
// connectNulls: true,
symbolSize: 3,
symbol: 'circle',
itemStyle: {
color: '#78c300'
},
lineStyle: {
width: 2
},
yAxisIndex: 2
},
]
};
// 保存配置引用
echartOptionRef.value = setting;
// 完全替换模式更新图表
chartInstance.setOption(setting, true);
// ✅ 初始化时应用动态布局算法确保只有流量Y轴显示
yAxisShowDynamic({
'流量(m³/s)': true,
'水位(m)': false,
'流速(m/s)': false
}, setting);
// 再次应用调整后的配置
chartInstance.setOption(setting, true);
};
// ==================== 生命周期钩子 ====================
onMounted(async () => {
// 双重等待机制nextTick + setTimeout
await nextTick();
setTimeout(() => {
initChart();
updateChart();
}, 50);
});
onUnmounted(() => {
// 销毁图表实例,防止内存泄漏
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
//获取水温监测下拉框
const getselsectData = async () => {
let params = {
"filter": {
"logic": "and",
"filters": [
baseid.value != 'all' ? {
"field": "baseId",
"operator": "eq",
"dataType": "string",
"value": baseid.value
} : null,
{
"field": "fhstcd",
"operator": "isnotnull"
},
{
"logic": "or",
"filters": [
{
"field": "sttpCode",
"operator": "eq",
"dataType": "string",
"value": "ZQ"
}
]
}
].filter(Boolean)
},
"select": [
"stcd",
"stnm",
"fhstcd",
"fhstnm",
"sttpCode"
]
}
let res = await msstbprptGetKendoList(params)
console.log(res)
if (!res?.data?.data || res.data.data.length == 0) {
select.value.value = ''
select.value.options = []
return
}
select.value.value = res.data.data[0].stcd
select.value.options = res.data.data.map((item: any) => {
return {
label: item.stnm,
value: item.stcd
}
})
// debugger
};
const convertDateRange = (dateRange: string[]) => {
if (!dateRange || dateRange.length !== 2) {
return null;
}
// debugger
const [startStr, endStr] = dateRange;
// 获取今天的日期(日部分)
const today = new Date();
const currentDay = today.getDate();
// 解析开始日期 YY-MM
const [startYearStr, startMonthStr] = startStr.split('-');
const startYear = parseInt(startYearStr);
const startMonth = parseInt(startMonthStr);
const fullStartYear = startYear < 100 ? 2000 + startYear : startYear;
// 解析结束日期 YY-MM
const [endYearStr, endMonthStr] = endStr.split('-');
const endYear = parseInt(endYearStr);
const endMonth = parseInt(endMonthStr);
const fullEndYear = endYear < 100 ? 2000 + endYear : endYear;
// 开始日期:基于第一个值的年月 + 今天的日期 + 00:00:00
// 需要处理月份天数边界问题如今天是31号但2月只有28天
const startMonthLastDay = new Date(fullStartYear, startMonth, 0).getDate();
const actualStartDay = Math.min(currentDay, startMonthLastDay);
const startDate = `${fullStartYear}-${String(startMonth).padStart(2, '0')}-${String(actualStartDay).padStart(2, '0')} 00:00:00`;
// 结束日期:基于第二个值的年月 + 今天的日期 + 23:59:59
// 同样需要处理月份天数边界问题
const endMonthLastDay = new Date(fullEndYear, endMonth, 0).getDate();
const actualEndDay = Math.min(currentDay, endMonthLastDay);
const endDate = `${fullEndYear}-${String(endMonth).padStart(2, '0')}-${String(actualEndDay).padStart(2, '0')} 23:59:59`;
return {
startDate,
endDate
};
};
/**
* 加载图表数据
* @param startDate 开始日期 (格式: YYYY-MM-DD HH:mm:ss)
* @param endDate 结束日期 (格式: YYYY-MM-DD HH:mm:ss)
* @param stcd 测站代码
*/
const getChartData = async (startDate: string, endDate: string, stcd: string) => {
try {
console.log('=== 开始加载图表数据 ===');
console.log('请求参数:', { startDate, endDate, stcd });
// 设置 loading 状态
loading.value = true;
hasData.value = false;
const params = {
"filter": {
"logic": "and",
"filters": [
{
"field": "DT",
"operator": "gte",
"value": startDate
},
{
"field": "DT",
"operator": "lte",
"value": endDate
},
{
"field": "stcd",
"operator": "eq",
"dataType": "string",
"value": stcd
}
]
},
"sort": [
{
"field": "dt",
"dir": "asc"
}
]
};
// 调用实际的API获取数据
const res = await sdriverdaysGetKendoList(params);
console.log('API返回结果:', res);
let data = res?.data?.data || [];
console.log('原始数据条数:', data.length);
// 处理空数据情况
if (!data || data.length === 0) {
echartsData.value = [];
hasData.value = false;
await nextTick();
updateChart();
console.warn('未查询到水文数据');
return;
}
// ✅ 数据预处理:验证字段存在性并转换类型
const processedData = data
.map(item => {
// 验证必要字段
if (!item.dt || typeof item.dt !== 'string') {
console.warn('无效的日期数据:', item);
return null;
}
// 确保数值类型
const q = typeof item.q === 'number' ? item.q : parseFloat(item.q);
const z = typeof item.z === 'number' ? item.z : parseFloat(item.z);
const v = typeof item.v === 'number' ? item.v : parseFloat(item.v);
if (isNaN(q) || isNaN(z) || isNaN(v)) {
console.warn('无效的水文数据:', item);
return null;
}
return {
dt: item.dt.split(' ')[0], // 提取日期部分 "2026-05-03"
v: v, // 确保数值类型
q: q,
z: z
};
})
.filter(Boolean); // 过滤掉无效数据
console.log('处理后数据条数:', processedData.length);
console.log('处理后数据示例:', processedData[0]);
// ✅ 更新响应式数据
echartsData.value = processedData;
// 判断是否有数据
hasData.value = echartsData.value.length > 0;
console.log('hasData 设置为:', hasData.value);
// ✅ 刷新图表确保DOM更新后执行
await nextTick();
updateChart();
console.log('图表已更新');
} catch (error) {
console.error('获取图表数据失败:', error);
hasData.value = false;
} finally {
// 关闭 loading 状态
await nextTick();
console.log('关闭 loading 状态');
loading.value = false;
}
};
const handlePanelChange1 = async (data) => {
console.log('=== 用户交互触发 ===');
console.log('当前所有控件状态:', data);
// scopeDate: ['26-05', '26-06']
// select: "0836000011"
if (data.scopeDate && data.scopeDate.length > 0 && data.select) {
console.log('参数验证通过,开始加载数据');
// 转换日期格式并加载数据
const dateRange = convertDateRange(data.scopeDate);
if (dateRange) {
console.log('日期转换结果:', dateRange);
await getChartData(dateRange.startDate, dateRange.endDate, data.select);
} else {
console.error('日期转换失败');
}
} else {
loading.value = false;
hasData.value = false;
// 清空图表数据
echartsData.value = [];
// 更新图表显示空状态
await nextTick();
updateChart();
console.warn('参数不完整:', {
scopeDate: data.scopeDate,
select: data.select
});
}
}
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
baseid.value = newVal.wbsCode;
getselsectData()
},
{ deep: true, immediate: true }
);
</script>
<style lang="scss" scoped>
.chart-wrapper {
width: 100%;
height: 231px;
min-height: 231px;
position: relative;
/* 为empty-overlay提供定位上下文 */
.echarts-chart {
width: 100% !important;
height: 100% !important;
min-width: 100%;
min-height: 231px;
}
.empty-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255);
z-index: 10;
}
}
</style>