WholeProcessPlatform/frontend/src/modules/waterQuality/index.vue
2026-06-12 16:59:35 +08:00

1237 lines
41 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="沿程水质变化" :iconmap="iconmap" :select="select" :datetimePicker="datetimePicker"
@update-values="handlePanelChange1">
<div class="chart-wrapper">
<a-spin :spinning="loading">
<!-- 始终渲染图表容器确保有固定宽高 -->
<div ref="chartRef" class="chart-container"></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, onBeforeUnmount, nextTick, watch } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { qgcGetKendoListCust, wbsbGetKendoList } from '@/api/sz'
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
import { useModelStore } from "@/store/modules/model";
import { WATER_QUALITY_INDICATORS, type WaterQualityIndicator } from '@/modules/waterQuality/padig';
const indicators = WATER_QUALITY_INDICATORS;
// 定义组件名(便于调试和递归)
defineOptions({
name: 'waterQuality'
});
const modelStore = useModelStore();
const JidiSelectEventStore = useJidiSelectEventStore();
const iconmap = ref({
show: true,
value: '',
icon: 'iconfont icon-time',
});
// ==================== 响应式数据 ====================
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// Loading 状态和数据状态
const loading = ref(false);
const hasData = ref(true); // 初始化为 true因为 initChart 会使用 mockData 渲染
// 获取当天早上8:00的时间
const now = new Date();
const todayAtEightAM = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
const defaultValue = `${todayAtEightAM.getFullYear()}-${String(todayAtEightAM.getMonth() + 1).padStart(2, '0')}-${String(todayAtEightAM.getDate()).padStart(2, '0')} ${String(todayAtEightAM.getHours()).padStart(2, '0')}:00`;
// 👆 注意这里分钟固定为 :00与 timeFormat: 'HH' 配置保持一致
const datetimePicker = ref({
show: true,
value: defaultValue,
format: 'YYYY-MM-DD HH:mm', // 显示格式:包含分钟
picker: 'date',
timeFormat: 'HH', // 新增:选择格式:只选择小时
options: []
});
const select = ref({
show: true,
value: '',
options: [],
picker: undefined,
format: undefined,
});
// ==================== 可见系列队列最多2个====================
const visibleSeriesQueue = ref<string[]>([]);
// ==================== 动态指标配置 ====================
const indicatorConfig = ref<Array<{ key: string; name: string; unit: string; color: string; sort: number }>>([]);
// ==================== HSL 随机颜色生成 ====================
const generateRandomColor = (index: number): string => {
// 使用黄金角度确保颜色均匀分布
const goldenAngle = 137.508;
const hue = (index * goldenAngle) % 360;
// 饱和度控制在 45%-65% 之间
const saturation = 45 + Math.random() * 20;
// 亮度控制在 45%-60% 之间,避免太亮或太暗
const lightness = 45 + Math.random() * 15;
// HSL 转 RGB
const c = (1 - Math.abs(2 * lightness / 100 - 1)) * saturation / 100;
const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
const m = lightness / 100 - c / 2;
let r = 0, g = 0, b = 0;
if (hue < 60) {
[r, g, b] = [c, x, 0];
} else if (hue < 120) {
[r, g, b] = [x, c, 0];
} else if (hue < 180) {
[r, g, b] = [0, c, x];
} else if (hue < 240) {
[r, g, b] = [0, x, c];
} else if (hue < 300) {
[r, g, b] = [x, 0, c];
} else {
[r, g, b] = [c, 0, x];
}
const toHex = (value: number) => {
const hex = Math.round((value + m) * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
// ==================== 动态指标筛选和配置生成 ====================
const generateIndicatorConfig = (data: StationData[]) => {
if (!data || data.length === 0) {
indicatorConfig.value = [];
return;
}
// 定义需要排除的元数据字段
const excludeKeys = new Set([
'stcd', 'stcd2', 'stnm', 'rstcd', 'sttp', 'tm', 'SORT',
'limits', 'minTm', 'wwqtg', 'wqgrd', 'wqgrdName', 'min', 'max',
'_tls', 'id', 'recordUser', 'recordTime', 'modifyTime',
'displayRecordUser', 'departmentId', 'displayDepartment'
]);
// 收集所有有效指标的 key
const validKeysSet = new Set<string>();
data.forEach(station => {
Object.keys(station).forEach(key => {
// 跳过排除字段
if (excludeKeys.has(key)) return;
const value = station[key as keyof typeof station];
// 检查是否为有效数值(不为 null 且不是 NaN
if (value !== null && value !== undefined && typeof value === 'number' && !isNaN(value)) {
// 在 indicators 中查找匹配的项(小写比较)
const matchedIndicator = indicators.find(ind => ind.key.toLowerCase() === key.toLowerCase());
if (matchedIndicator) {
validKeysSet.add(matchedIndicator.key);
}
}
});
});
// 如果没有有效指标,返回空数组
if (validKeysSet.size === 0) {
indicatorConfig.value = [];
return;
}
// 根据 indicators 中的 sort 排序,并生成配置
const validIndicators = indicators
.filter(ind => validKeysSet.has(ind.key))
.sort((a, b) => a.sort - b.sort);
// 生成最终配置
indicatorConfig.value = validIndicators.map((ind, index) => ({
key: ind.key,
name: ind.name,
unit: ind.description || '',
color: generateRandomColor(index),
sort: ind.sort
}));
console.log('动态生成指标配置:', indicatorConfig.value);
};
// ==================== 模拟数据 ====================
const generateMockData = () => {
const stations = [
{ stnm: '两河口', stcd: 'ST001', type: '0' },
{ stnm: '锦屏一级', stcd: 'ST002', type: '1' },
{ stnm: '桐子林', stcd: 'ST003', type: '0' },
{ stnm: '溪洛渡', stcd: 'ST004', type: '0' },
{ stnm: '向家坝', stcd: 'ST005', type: '1' },
{ stnm: '三峡', stcd: 'ST006', type: '0' },
{ stnm: '葛洲坝', stcd: 'ST007', type: '0' }
];
const tm = '2024-01-15 08:00:00';
// 模拟限值数据
const mockLimits = {
ph: { min: 6, max: 9 },
dox: { min: 6 },
codmn: { max: 4 },
nh3n: { max: 0.5 },
tp: { max: 0.1 },
tn: { max: 1.0 }
};
return stations.map((station, index) => ({
...station,
tm,
SORT: index + 1,
ph: 7.2 + Math.random() * 0.8,
dox: 6.5 + Math.random() * 2.5,
codmn: 1.5 + Math.random() * 2.0,
nh3n: 0.15 + Math.random() * 0.35,
tp: 0.05 + Math.random() * 0.15,
tn: 0.8 + Math.random() * 1.2,
limits: mockLimits // 新增:存储该站点的所有限值
}));
};
// 定义数据类型接口
interface StationData {
stnm: string;
stcd: string;
type: string;
tm: string;
SORT: number;
// 所有水质指标字段(动态)
ph?: number | null;
dox?: number | null;
codmn?: number | null;
codcr?: number | null;
bod5?: number | null;
nh3n?: number | null;
tp?: number | null;
tn?: number | null;
cu?: number | null;
zn?: number | null;
f?: number | null;
se?: number | null;
ars?: number | null;
hg?: number | null;
cd?: number | null;
cr6?: number | null;
pb?: number | null;
cn?: number | null;
vlph?: number | null;
oil?: number | null;
las?: number | null;
s2?: number | null;
fcg?: number | null;
cl?: number | null;
so4?: number | null;
no3?: number | null;
thrd?: number | null;
cond?: number | null;
fe?: number | null;
mn?: number | null;
al?: number | null;
chla?: number | null;
clarity?: number | null;
tu?: number | null;
wtmp?: number | null;
limits: Record<string, { min?: number; max?: number }>;
[key: string]: any; // 允许任意水质指标字段
}
const mockData = ref<StationData[]>([]);
// ==================== 图表配置生成 ====================
const getChartOption = (): EChartsOption => {
const data = mockData.value;
if (!data || data.length === 0 || indicatorConfig.value.length === 0) {
return {};
}
// X轴数据站点名称
const xData = data.map((item, index) => {
// 去除站点名称中的"出库水质监测站"或"坝上水质监测站"后缀
let stnm = item.stnm || '-';
if (stnm.includes('出库水质监测站')) {
stnm = stnm.replace('出库水质监测站', '');
} else if (stnm.includes('坝上水质监测站')) {
stnm = stnm.replace('坝上水质监测站', '');
}
return {
value: stnm,
textStyle: {
padding: index % 2 !== 0 ? [16, 0, 0, 0] : 0
}
};
});
// 电站标记线数据
const markLineData: any[] = [];
data.forEach((item, index) => {
if (item.type === '1') {
markLineData.push({ xAxis: index });
}
});
// 图例数据
const legendData = indicatorConfig.value.map(item => item.name);
// 根据可见队列构建选中状态
const selectedState: Record<string, boolean> = {};
indicatorConfig.value.forEach(config => {
selectedState[config.name] = visibleSeriesQueue.value.includes(config.name);
});
// Series 数据
const seriesData = indicatorConfig.value.map((config, index) => {
const _key = config.key === 'dox' ? 'do' : config.key;
return {
name: config.name,
type: 'line' as const,
yAxisIndex: index,
smooth: true,
connectNulls: true,
symbol: 'circle' as const,
symbolSize: 6,
itemStyle: {
color: config.color
},
data: data.map(item => {
const value = item[config.key as keyof typeof item];
return typeof value === 'number' && !isNaN(value) ? value : null;
}),
markLine: index === 0 ? {
symbol: ['none', 'none'],
label: { show: false },
lineStyle: {
color: '#ccc',
type: 'dashed' as const
},
data: markLineData
} : undefined
};
});
// 计算每个Y轴的数据范围用于自适应使用整数
const calculateYAxisRange = (configIndex: number) => {
if (!selectedState[indicatorConfig.value[configIndex].name]) {
return null;
}
const values = data
.map(item => item[indicatorConfig.value[configIndex].key as keyof typeof item])
.filter((val): val is number => typeof val === 'number' && !isNaN(val));
if (values.length === 0) {
return { min: 0, max: 10 };
}
const min = Math.min(...values);
const max = Math.max(...values);
// Y轴使用整数向下取整-1向上取整+1
return {
min: Math.floor(min) - 1,
max: Math.ceil(max) + 1
};
};
// Y轴配置左1右1原则最多显示2个Y轴
const yAxisData = indicatorConfig.value.map((config, index) => {
const isShow = selectedState[config.name];
// 如果不显示,直接返回隐藏配置
if (!isShow) {
return {
type: 'value' as const,
name: config.name,
show: false
};
}
// 计算当前是第几个显示的Y轴
let displayIndex = 0;
for (let i = 0; i < index; i++) {
if (selectedState[indicatorConfig.value[i].name]) {
displayIndex++;
}
}
// 最多只显示2个Y轴索引0和1
if (displayIndex >= 2) {
return {
type: 'value' as const,
name: config.name,
show: false
};
}
const isLeft = displayIndex === 0;
const offset = displayIndex === 1 ? 0 : 0;
const range = calculateYAxisRange(index);
return {
type: 'value' as const,
name: config.name,
position: isLeft ? ('left' as const) : ('right' as const),
offset,
min: range ? range.min : undefined,
max: range ? range.max : undefined,
axisLine: {
show: true,
lineStyle: {
color: config.color
}
},
axisLabel: {
color: config.color,
formatter: (value: number) => {
// Y轴刻度使用整数显示
return Math.round(value).toString();
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e0e0e0',
type: 'solid' as const
}
},
show: true
};
});
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
confine: true,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderColor: 'transparent',
textStyle: {
color: '#ffffff'
},
formatter: (params: any) => {
if (!params || params.length === 0) return '';
// 在最上面显示用户选择的时间
let result = `<div style="font-weight: bold; margin-bottom: 8px;">${datetimePicker.value.value}</div>`;
const tm = data.find(item => item.stnm === params[0].name)?.tm;
result += `${params[0].name}<br/>`;
// 判断是否只显示一个系列
const showOnlyOneSeries = visibleSeriesQueue.value.length === 1;
params.forEach((param: any) => {
const config = indicatorConfig.value.find(c => c.name === param.seriesName);
const unit = config?.unit || '';
if (param.value !== null && param.value !== undefined) {
// 折线数据保留一位小数
const numValue = Number(param.value);
let displayValue = param.value;
if (!isNaN(numValue)) {
displayValue = numValue.toFixed(1);
}
result += `${param.marker}${param.seriesName}${displayValue}${unit}`;
// 如果只显示一个系列,且该指标有限值,则显示限值
if (showOnlyOneSeries && config) {
// 从 data 中获取当前站点的限值
const stationData = data.find(item => item.stnm === param.name);
if (stationData && stationData.limits) {
const limitKey = config.key; // 如 'ph', 'dox'
const limits = stationData.limits[limitKey];
if (limits) {
// 构建限值显示字符串
let limitText = '';
const unit = config.unit || ''; // 获取单位
if (limits.min !== undefined && limits.max !== undefined) {
// 既有上限又有下限
limitText = `&nbsp;&nbsp;&nbsp;&nbsp;限值: ${limits.min}-${limits.max}${unit ? unit : ''}`;
} else if (limits.min !== undefined) {
// 只有下限
limitText = `&nbsp;&nbsp;&nbsp;&nbsp;限值: ${limits.min}${unit ? unit : ''}`;
} else if (limits.max !== undefined) {
// 只有上限
limitText = `&nbsp;&nbsp;&nbsp;&nbsp;限值: ${limits.max}${unit ? unit : ''}`;
}
result += ('<br/>'+limitText);
}
}
}
result += '<br/>';
}
});
return result;
}
},
legend: {
type: 'scroll',
top: 0,
right: 10, // 右侧留白,避免箭头按钮被遮挡
data: legendData,
selected: selectedState,
inactiveColor: '#ccc',
itemWidth: 16,
itemHeight: 12,
itemGap: 8,
pageButtonItemGap: 5, // 分页按钮与图例项的间距
pageIconColor: '#2f4554', // 分页按钮颜色
pageIconInactiveColor: '#aaa', // 分页按钮禁用时的颜色
pageIconSize: 12, // 分页按钮大小
pageFormatter: '{current}/{total}', // 分页格式:当前页/总页数
textStyle: {
fontSize: 14
}
},
// 添加 dataZoom 组件实现X轴滚轮缩放仅内置类型无滑动条
dataZoom: [
{
type: 'inside', // 内置型数据区域缩放组件(支持鼠标滚轮)
xAxisIndex: 0, // 控制第一个X轴
start: 0, // 默认显示全部数据
end: 100,
zoomOnMouseWheel: true, // 开启鼠标滚轮缩放
moveOnMouseMove: true, // 开启鼠标移动平移
filterMode: 'filter' // 过滤模式
}
],
grid: {
top: 60,
bottom: 50,
left: '20px',
right: '20px',
containLabel: true
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
interval: 0,
color: '#333',
fontSize: 12
},
axisLine: {
lineStyle: {
color: '#8f8f8f'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e0e0e0',
type: 'solid' as const
}
}
},
yAxis: yAxisData,
series: seriesData
};
return option;
};
// ==================== 图例选择事件处理最多2个系列====================
const handleLegendSelectChanged = (params: any) => {
if (!chartInstance) return;
const { selected, name } = params;
const clickedName = name;
const isNowSelected = selected[clickedName];
console.log(`图例点击: ${clickedName}, 当前状态: ${isNowSelected ? '显示' : '隐藏'}`);
console.log('当前可见队列:', visibleSeriesQueue.value);
if (isNowSelected) {
// 用户想要显示这个系列
if (visibleSeriesQueue.value.length >= 2) {
// 已满2个移除最早的队列头部
const removed = visibleSeriesQueue.value.shift();
console.log(`已达上限,移除最早系列: ${removed}`);
}
// 添加到队列尾部
if (!visibleSeriesQueue.value.includes(clickedName)) {
visibleSeriesQueue.value.push(clickedName);
}
} else {
// 用户想要隐藏这个系列
const index = visibleSeriesQueue.value.indexOf(clickedName);
if (index > -1) {
visibleSeriesQueue.value.splice(index, 1);
console.log(`从队列中移除: ${clickedName}`);
}
}
console.log('更新后可见队列:', visibleSeriesQueue.value);
// 根据队列构建最终的 selected 状态
const finalSelected: Record<string, boolean> = {};
indicatorConfig.value.forEach(config => {
finalSelected[config.name] = visibleSeriesQueue.value.includes(config.name);
});
// 深拷贝当前配置
const currentOption = JSON.parse(JSON.stringify(chartInstance.getOption()));
// 同步图例选中状态
currentOption.legend[0].selected = finalSelected;
// 根据图例状态更新 Y 轴显隐
const newYAxis = currentOption.yAxis.map((item: any) => {
let isShow = false;
let displayIndex = 0;
// 计算这是第几个显示的Y轴
for (const key in finalSelected) {
if (finalSelected[key]) {
const config = indicatorConfig.value.find(c => c.name === key);
if (config && config.name === item.name) {
isShow = true;
break;
}
displayIndex++;
}
}
// 最多只显示2个Y轴
if (!isShow || displayIndex >= 2) {
return { ...item, show: false };
}
return { ...item, show: true };
});
currentOption.yAxis = newYAxis;
// 动态调整 Y 轴布局
yAxisShowDynamic(finalSelected, currentOption);
// 完全替换模式更新
try {
chartInstance.setOption(currentOption, true);
console.log('图表配置已更新');
} catch (error) {
console.error('图表更新失败:', error);
}
};
// ==================== 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) {
delete 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个右侧1个
if (showCount >= 2) {
let leftAssigned = false;
let rightAssigned = false;
options.yAxis = options.yAxis.map((item: any) => {
if (!item.show) {
return item;
}
// 第一个可见的Y轴放在左侧
if (!leftAssigned) {
leftAssigned = true;
delete options.grid.right;
options.grid.left = '80px';
return {
...item,
position: 'left',
offset: 0
};
} else if (!rightAssigned) {
// 第二个Y轴放在右侧
rightAssigned = true;
options.grid.right = '60px';
return {
...item,
position: 'right',
offset: 0
};
} else {
// 第三个及以后的Y轴隐藏理论上不应该到这里
return {
...item,
show: false
};
}
});
}
};
// ==================== 数据点点击事件处理 ====================
const handleDataPointClick = (params: any) => {
if (!params || !params.dataIndex) return;
// 获取点击的数据点对应的站点信息
const dataIndex = params.dataIndex;
const stationData = mockData.value[dataIndex];
if (stationData && stationData.stcd) {
modelStore.modalVisible = true;
modelStore.params.sttp = "WQFB";
modelStore.title = stationData.stnm ;
// modelStore.isBasicEdit = true;
modelStore.params.stcd = stationData.stcd;
}
};
// ==================== 图表初始化 ====================
//获取图参数
const getecharts = async () => {
if (!select.value.value) {
console.warn('选择器值为空,无法获取图表数据');
loading.value = false;
hasData.value = false;
chartInstance?.clear();
return;
}
console.log('开始获取图表数据select.value:', select.value.value);
// 设置 loading 状态
loading.value = true;
hasData.value = false;
let params = {
"filter": {
"logic": "and",
"filters": [
{
"field": "baseId",
"operator": "eq",
"dataType": "string",
"value": wbsCode.value
},
{
"field": "rvcd",
"operator": "eq",
"dataType": "string",
"value": select.value.value
},
{
"field": "tm",
"operator": "gte",
"dataType": "date",
"value": getStartTime()
},
{
"field": "tm",
"operator": "lte",
"dataType": "date",
"value": getEndTime()
}
]
},
"sort": [
{
"field": "sort",
"dir": "asc"
}
]
}
try {
console.log('请求参数:', params);
let res = await qgcGetKendoListCust(params);
console.log('接口返回数据:', res);
// 处理返回的数据 - 兼容多种数据结构
let apiData: any[] = [];
if (res && res.data) {
// 情况1: res.data.data 是数组(当前实际情况)
if (Array.isArray(res.data.data)) {
apiData = res.data.data;
}
// 情况2: res.data.data.data 是数组
else if (res.data.data && Array.isArray(res.data.data.data)) {
apiData = res.data.data.data;
}
// 情况3: res.data 直接是数组
else if (Array.isArray(res.data)) {
apiData = res.data;
}
}
console.log('原始数据条数:', apiData.length);
// 过滤出有水质数据的站点sttp === 'WQ'并按sort排序
const filteredData = apiData
.filter((item: any) => item.sttp === 'WQ')
.sort((a: any, b: any) => {
// 如果有sort字段则使用否则按索引排序
return (a.sort || 0) - (b.sort || 0);
});
console.log('过滤后水质数据条数:', filteredData.length);
// 转换为图表所需的数据格式
const chartData = filteredData.map((item: any, index: number) => {
// 去除站点名称中的"出库水质站"后缀
let stnm = item.stnm || '-';
if (stnm.includes('出库水质站')) {
stnm = stnm.replace('出库水质站', '');
}
if (stnm.includes('出库水质_')) {
stnm = stnm.replace('出库水质_', '');
}
// 解析 min 和 max 数组,构建限值对象
const limits: Record<string, { min?: number; max?: number }> = {};
// 处理下限
if (item.min && Array.isArray(item.min)) {
item.min.forEach((limitItem: any) => {
Object.keys(limitItem).forEach(key => {
// key 是大写,如 "PH", "DOX",转为小写以匹配 indicatorConfig
const lowerKey = key.toLowerCase();
limits[lowerKey] = limits[lowerKey] || {};
limits[lowerKey].min = limitItem[key];
});
});
}
// 处理上限
if (item.max && Array.isArray(item.max)) {
item.max.forEach((limitItem: any) => {
Object.keys(limitItem).forEach(key => {
const lowerKey = key.toLowerCase();
limits[lowerKey] = limits[lowerKey] || {};
limits[lowerKey].max = limitItem[key];
});
});
}
return {
stnm: stnm,
stcd: item.stcd || '',
type: item.rstcd ? '1' : '0', // 有rstcd表示是电站出库站
tm: item.tm || '',
SORT: index + 1,
ph: item.ph != null ? Number(item.ph) : null,
dox: item.dox != null ? Number(item.dox) : null,
codmn: item.codmn != null ? Number(item.codmn) : null,
codcr: item.codcr != null ? Number(item.codcr) : null,
bod5: item.bod5 != null ? Number(item.bod5) : null,
nh3n: item.nh3n != null ? Number(item.nh3n) : null,
tp: item.tp != null ? Number(item.tp) : null,
tn: item.tn != null ? Number(item.tn) : null,
cu: item.cu != null ? Number(item.cu) : null,
zn: item.zn != null ? Number(item.zn) : null,
f: item.f != null ? Number(item.f) : null,
se: item.se != null ? Number(item.se) : null,
ars: item.ars != null ? Number(item.ars) : null,
hg: item.hg != null ? Number(item.hg) : null,
cd: item.cd != null ? Number(item.cd) : null,
cr6: item.cr6 != null ? Number(item.cr6) : null,
pb: item.pb != null ? Number(item.pb) : null,
cn: item.cn != null ? Number(item.cn) : null,
vlph: item.vlph != null ? Number(item.vlph) : null,
oil: item.oil != null ? Number(item.oil) : null,
las: item.las != null ? Number(item.las) : null,
s2: item.s2 != null ? Number(item.s2) : null,
fcg: item.fcg != null ? Number(item.fcg) : null,
cl: item.cl != null ? Number(item.cl) : null,
so4: item.so4 != null ? Number(item.so4) : null,
no3: item.no3 != null ? Number(item.no3) : null,
thrd: item.thrd != null ? Number(item.thrd) : null,
cond: item.cond != null ? Number(item.cond) : null,
fe: item.fe != null ? Number(item.fe) : null,
mn: item.mn != null ? Number(item.mn) : null,
al: item.al != null ? Number(item.al) : null,
chla: item.chla != null ? Number(item.chla) : null,
clarity: item.clarity != null ? Number(item.clarity) : null,
tu: item.tu != null ? Number(item.tu) : null,
wtmp: item.wtmp != null ? Number(item.wtmp) : null,
limits: limits // 新增:存储该站点的所有限值
};
});
// 更新模拟数据
mockData.value = chartData;
// 判断是否有数据
hasData.value = chartData.length > 0;
console.log('hasData 设置为:', hasData.value);
if (!hasData.value) {
// 无数据时清空图表
console.log('无数据,清空图表');
chartInstance?.clear();
indicatorConfig.value = [];
loading.value = false;
return;
}
// 动态生成指标配置
generateIndicatorConfig(chartData);
// 如果没有有效指标,显示空状态
if (indicatorConfig.value.length === 0) {
console.log('没有有效指标,显示空状态');
hasData.value = false;
chartInstance?.clear();
loading.value = false;
return;
}
// 重新渲染图表 - 使用 nextTick 确保 DOM 更新完成
await nextTick();
if (chartInstance) {
// 如果图表已存在,更新配置
requestAnimationFrame(() => {
const option = getChartOption();
chartInstance?.setOption(option, true);
chartInstance?.resize();
console.log('图表数据已更新,共', chartData.length, '个站点,', indicatorConfig.value.length, '个指标');
});
} else {
// 如果图表不存在,初始化图表
await initChart();
}
} catch (error) {
console.error('获取图表数据失败:', error);
hasData.value = false;
indicatorConfig.value = [];
} finally {
// 关闭 loading 状态 - 使用 nextTick 确保在下一个 tick 关闭
await nextTick();
console.log('关闭 loading 状态');
loading.value = false;
}
}
// 获取起始时间(当前选择日期前一个月)
const getStartTime = () => {
const currentDate = new Date(datetimePicker.value.value);
const oneMonthAgo = new Date(currentDate);
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const year = oneMonthAgo.getFullYear();
const month = String(oneMonthAgo.getMonth() + 1).padStart(2, '0');
const day = String(oneMonthAgo.getDate()).padStart(2, '0');
const hours = String(oneMonthAgo.getHours()).padStart(2, '0');
const minutes = String(oneMonthAgo.getMinutes()).padStart(2, '0');
const seconds = String(oneMonthAgo.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 获取结束时间(当前选择日期的当天 23:59:59
const getEndTime = () => {
const currentDate = new Date(datetimePicker.value.value);
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
return `${year}-${month}-${day} 23:59:59`;
};
// ==================== 图表初始化 ====================
const initChart = async () => {
if (!chartRef.value) {
console.warn('图表容器未就绪');
return;
}
// 检查容器尺寸
const rect = chartRef.value.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn('图表容器尺寸为0等待渲染...');
// 使用 requestAnimationFrame 确保在浏览器下一重绘帧再初始化
return new Promise((resolve) => {
requestAnimationFrame(() => {
setTimeout(() => {
initChart().then(resolve);
}, 50);
});
});
}
// 如果图表实例已存在,先销毁
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
// 如果没有有效指标配置,不初始化图表
if (indicatorConfig.value.length === 0) {
console.warn('没有有效的指标配置,跳过图表初始化');
hasData.value = false;
return;
}
// 初始化可见队列默认只显示第一个系列按sort排序后的第一个
visibleSeriesQueue.value = [indicatorConfig.value[0].name];
console.log('初始化可见队列:', visibleSeriesQueue.value);
// 初始化 ECharts 实例
chartInstance = echarts.init(chartRef.value);
// 设置初始配置
const option = getChartOption();
chartInstance.setOption(option);
// 监听图例选择变化事件
chartInstance.on('legendselectchanged', handleLegendSelectChanged);
// 监听数据点点击事件
chartInstance.on('click', handleDataPointClick);
// 强制 resize 确保正确渲染
setTimeout(() => {
chartInstance?.resize();
}, 50);
console.log('图表初始化成功');
};
// ==================== 窗口大小变化处理 ====================
const handleResize = () => {
chartInstance?.resize();
};
// ==================== 生命周期钩子 ====================
onMounted(async () => {
await nextTick();
// 不再立即初始化图表,等待数据加载完成
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.off('legendselectchanged', handleLegendSelectChanged);
chartInstance.off('click', handleDataPointClick);
chartInstance.dispose();
chartInstance = null;
}
});
//根据流域获取河段
const getSelect = async () => {
if (!wbsCode.value) {
console.warn('wbsCode 为空,无法获取河段数据');
return;
}
let params = {
"filter": {
"logic": "and",
"filters": [
{
"field": "objId",
"operator": "eq",
"dataType": "string",
"value": wbsCode.value
},
{
"field": "wbsType",
"operator": "eq",
"dataType": "string",
"value": "PSB_RVCD"
}
]
},
"group": [
{
"dir": "asc",
"field": "orderIndex"
},
{
"dir": "des",
"field": "wbsCode"
},
{
"dir": "des",
"field": "wbsName"
}
],
"groupResultFlat": "true"
}
try {
let res = await wbsbGetKendoList(params);
// 处理返回的数据 - 注意数据结构可能是嵌套的
let data;
if (res && res.data && res.data.data) {
// 如果 res.data.data 是数组,直接使用
if (Array.isArray(res.data.data)) {
data = res.data.data;
}
// 如果 res.data.data 是对象且有 data 属性,使用 res.data.data.data
else if (res.data.data.data && Array.isArray(res.data.data.data)) {
data = res.data.data.data;
} else {
console.warn('无法解析河段数据:', res);
return;
}
} else {
console.warn('未获取到河段数据');
return;
}
if (data && data.length > 0) {
select.value.value = data[0].wbsCode;
select.value.options = data.map(item => {
return {
value: item.wbsCode,
label: item.wbsName
};
});
// 获取完选择器数据后,立即加载图表数据
await nextTick();
getecharts();
} else {
console.warn('河段数据为空');
select.value.value = '';
select.value.options = [];
hasData.value = false;
}
} catch (error) {
console.error('获取河段数据失败:', error);
}
}
//监听子组件的数据变化
const handlePanelChange1 = async (data) => {
console.log('当前所有控件状态:', data);
// 当选择器或日期变化时,重新加载图表数据
if (data.datetime || data.select) {
select.value.value = data.select
datetimePicker.value.value = data.datetime
getecharts()
}
}
// 监听 datetimePicker 变化,更新 iconmap 中的时间显示
watch(
() => datetimePicker.value.value,
(newVal) => {
if (newVal) {
iconmap.value.value = `注:最新数据时间为${newVal}`;
}
},
{ immediate: true }
);
const wbsCode = ref('');
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
console.log(newVal);
wbsCode.value = newVal.wbsCode;
getSelect()
},
{ deep: true, immediate: true }
);
// 监听可见系列数量变化,重新设置图表配置以确保 tooltip formatter 使用最新的 visibleSeriesQueue 值
watch(
() => visibleSeriesQueue.value.length,
(newLength, oldLength) => {
// 只有当长度真正变化时才重新设置
if (newLength !== oldLength && chartInstance && indicatorConfig.value.length > 0) {
// 重新生成 option这会创建新的 formatter 闭包,捕获最新的 visibleSeriesQueue
const newOption = getChartOption();
chartInstance.setOption(newOption, true);
}
}
);
</script>
<style lang="scss" scoped>
.chart-wrapper {
width: 100%;
height: 280px;
min-height: 231px;
position: relative;
/* 为empty-overlay提供定位上下文 */
.chart-container {
width: 100% !important;
height: 100% !important;
min-width: 100%;
min-height: 280px;
}
.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, 0.9);
z-index: 10;
}
}
</style>