WholeProcessPlatform/frontend/src/modules/qixidishuiwenbianhua/index.vue
2026-07-14 16:08:10 +08:00

739 lines
20 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="water-temp-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, sdrvwtsGetKendoList } from '@/api/qxd';
import { useModelStore } from '@/store/modules/model';
import dayjs from 'dayjs';
// 定义组件名
defineOptions({
name: 'qixidishuiwenbianhua'
});
// ==================== 响应式变量定义 ====================
const JidiSelectEventStore = useJidiSelectEventStore();
// 图表容器引用
const chartRef = ref<HTMLDivElement>();
let chartInstance: echarts.ECharts | null = null;
const baseid = ref('');
// Loading 状态和数据状态
const loading = ref(true);
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 echartsData = ref<any[]>([]);
const modelStore = useModelStore();
// 单位配置
const unita = ref('℃');
// 颜色配置 - 使用HSL色彩空间生成舒适的颜色
const Color = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4'
];
// ==================== 工具函数 ====================
/**
* 生成模拟数据 - 季节性水温变化
*/
const generateMockData = () => {
const data = [];
const baseTemp = 15; // 基础温度
for (let i = 0; i < 12; i++) {
const month = String(i + 1).padStart(2, '0');
// 模拟季节性变化:夏季高,冬季低
// 正弦波模拟峰值在7-8月谷值在1-2月
const temp =
baseTemp +
Math.sin((i / 12) * Math.PI * 2 - Math.PI / 2) * 10 +
Math.random() * 2;
data.push({
dt: `2024-${month}`,
wt: parseFloat(temp.toFixed(1))
});
}
return data;
};
/**
* 初始化图表
*/
const initChart = () => {
if (!chartRef.value) {
console.warn('图表容器未找到');
return;
}
// 销毁已存在的实例
if (chartInstance) {
chartInstance.dispose();
}
// 创建新实例
chartInstance = echarts.init(chartRef.value);
// 绑定数据点点击事件
chartInstance.on('click', handleDataPointClick);
// 更新图表
updateChart();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
};
/**
* 更新图表配置
*/
const updateChart = () => {
if (!chartInstance) return;
const _legendData = [`水温(${unita.value})`];
// 处理空数据情况
const hasData = echartsData.value && echartsData.value.length > 0;
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderColor: 'transparent',
textStyle: {
color: '#ffffff'
},
formatter: function (params: any) {
if (!params || params.length === 0) return '';
let res = `${params[0].name} <br/>`;
for (const item of params) {
const seriesName = item.seriesName ?? '';
let regx = /\(([^()]+?)\)/;
let unit = seriesName.match(regx);
// 数据保留一位小数
const finalValue =
item.value !== undefined && item.value !== null
? Number(item.value).toFixed(1)
: '-';
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;
}
},
// axisPointer 配置:鼠标经过时显示阴影块
axisPointer: {
type: 'shadow',
label: {
show: false
},
shadowStyle: {
color: 'rgba(84, 112, 198, 0.2)'
}
},
color: Color,
legend: {
data: _legendData,
top: 0,
left: 'center',
selectedMode: 'multiple'
},
grid: {
top: 25,
left: '3%',
right: '3%',
bottom: '3%',
containLabel: true
},
dataZoom: {
type: 'inside',
start: 0,
end: 100
},
xAxis: [
{
type: 'category',
data: echartsData.value?.map((item: any) => item.dt) || [],
boundaryGap: true,
// X轴标签配置 - 最多显示4个标签强制显示第一个
axisLabel: {
show: true,
rotate: 0, // 不倾斜
interval: (index: number, value: string): boolean => {
const total = echartsData.value?.length || 0;
if (total <= 4) return true; // 数据少于4条全部显示
// 强制显示第一个
if (index === 0) return true;
// 其余均匀分布3个标签总共4个
const step = Math.floor((total - 1) / 3);
return index % step === 0 && index < total;
},
formatter: function (value: string) {
// 显示完整日期 "YYYY-MM-DD" 格式
return value;
}
},
// X轴网格线垂直方向
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#e0e0e0'
}
},
// X轴的axisPointer配置鼠标经过时显示阴影块
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(84, 112, 198, 0.2)'
}
}
}
],
yAxis: [
{
type: 'value',
// 移除 name避免与 legend 重复,在刻度上显示单位
name: _legendData[0], // 显示 "水温(℃)"
nameLocation: 'end', // 显示在 Y 轴顶部
nameGap: 8, // 与轴线距离 20px
nameTextStyle: {
color: '#333333',
fontSize: 12
},
// 计算 Y 轴范围:最小值向下取整,最大值向上取整
min:
hasData && echartsData.value.length > 0
? Math.floor(Math.min(...echartsData.value.map(item => item.wt)))
: undefined,
max:
hasData && echartsData.value.length > 0
? Math.ceil(Math.max(...echartsData.value.map(item => item.wt)))
: undefined,
// 确保显示刻度和标签,保持整数
axisLabel: {
show: true,
color: '#333',
formatter: (value: number) => `${Math.round(value)} ` // 取整并显示单位
},
axisTick: {
show: true
},
axisLine: {
show: true,
lineStyle: {
color: '#333'
}
},
splitLine: {
show: true,
lineStyle: {
type: 'solid'
}
},
// 确保 Y 轴刻度为整数
minInterval: 1,
maxInterval: 1
}
],
series: [
{
name: _legendData[0],
data: hasData
? echartsData.value?.map((item: any) => item.wt) || []
: [],
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: {
width: 2
}
}
]
};
// 使用完全替换模式更新配置
chartInstance.setOption(option, true);
};
/**
* 处理窗口大小变化
*/
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
/**
* 处理数据点点击事件
*/
const handleDataPointClick = (params: any) => {
console.log('=== 数据点点击事件 ===');
console.log('点击参数:', params);
if (!params || !params.dataIndex) {
console.warn('无效的数据点点击');
return;
}
const dataIndex = params.dataIndex;
const dataItem = echartsData.value[dataIndex];
if (dataItem) {
console.log('点击的数据:', {
日期: dataItem.dt,
水温: dataItem.wt,
测站代码: dataItem.stcd,
索引: dataIndex
});
modelStore.modalVisible = true;
modelStore.params.sttp = 'WT';
modelStore.title = '水温监测';
modelStore.params.stcd = dataItem.stcd;
modelStore.showStcdSelector = true;
modelStore.stcdOptions = select.value.options;
modelStore.filter.rangeTm = [
dayjs(dataItem.dt).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
dayjs(dataItem.dt).endOf('day').format('YYYY-MM-DD HH:mm:ss')
];
}
};
/**
* 加载模拟数据
*/
const loadData = async () => {
try {
// 生成模拟数据
echartsData.value = generateMockData();
// 数据加载完成后更新图表
await nextTick();
updateChart();
} catch (error) {
console.error('加载水温数据失败:', error);
}
};
/**
* 加载图表数据
* @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;
// TODO: 实现API调用逻辑
// 参考选中的代码结构,构建请求参数
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'DT',
operator: 'gte',
value: startDate // 例如: "2026-05-01 00:00:00"
},
{
field: 'DT',
operator: 'lte',
value: endDate // 例如: "2026-06-30 23:59:59"
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
},
sort: [
{
field: 'dt',
dir: 'asc'
}
]
};
// 调用实际的API获取数据
const res = await sdrvwtsGetKendoList(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;
}
// 转换数据格式提取日期部分用于X轴显示并进行数据验证和类型转换
echartsData.value = data
.map(item => {
// 验证必要字段
if (!item.dt || typeof item.dt !== 'string') {
console.warn('无效的日期数据:', item);
return null;
}
// 确保 wt 是数值类型
const wt = typeof item.wt === 'number' ? item.wt : parseFloat(item.wt);
if (isNaN(wt)) {
console.warn('无效的水温数据:', item);
return null;
}
return {
dt: item.dt.substring(0, 10), // 提取 "2026-05-02" 格式
wt: wt, // 确保是 number 类型
stcd: item.stcd || stcd // 保留 stcd 字段,如果 API 返回中没有则使用传入的 stcd
};
})
.filter(Boolean); // 过滤掉无效数据
console.log('转换后的图表数据:', echartsData.value);
console.log('数据类型检查:', {
第一条数据: echartsData.value[0],
dt类型: typeof echartsData.value[0]?.dt,
wt类型: typeof echartsData.value[0]?.wt,
wt값: echartsData.value[0]?.wt
});
// 判断是否有数据
hasData.value = echartsData.value.length > 0;
console.log('hasData 设置为:', hasData.value);
// 数据加载完成后更新图表
await nextTick();
updateChart();
console.log('✅ 图表数据加载完成');
} catch (error) {
console.error('获取图表数据失败:', error);
hasData.value = false;
} finally {
// 关闭 loading 状态
await nextTick();
console.log('关闭 loading 状态');
loading.value = false;
}
};
/**
* 将 YY-MM 格式转换为完整的日期时间范围
* @param dateRange ['26-05', '26-06'] 格式的数组
* @returns { startDate: string, endDate: string } 格式化后的日期范围
*/
const convertDateRange = (dateRange: string[]) => {
if (!dateRange || dateRange.length !== 2) {
return null;
}
//
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
};
};
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
});
}
};
//获取水温监测下拉框
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: 'WTRV'
}
]
}
].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
};
});
//
};
////
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
baseid.value = newVal.wbsCode;
getselsectData();
},
{ deep: true, immediate: true }
);
// ==================== 生命周期钩子 ====================
// 页面加载时执行
onMounted(() => {
// 双重等待确保DOM和样式计算完成
nextTick(() => {
setTimeout(() => {
// 校验容器尺寸
if (chartRef.value) {
const rect = chartRef.value.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
initChart();
// 注释掉模拟数据加载,等待用户交互后加载真实数据
// loadData();
console.log('✅ 图表初始化完成,等待用户交互加载数据');
} else {
console.warn('图表容器尺寸为0启动重试机制');
// 指数退避重试
let retryCount = 0;
const maxRetries = 5;
const retryInit = () => {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(() => {
const newRect = chartRef.value?.getBoundingClientRect();
if (newRect && newRect.width > 0 && newRect.height > 0) {
initChart();
// loadData();
console.log('✅ 图表初始化完成(重试成功)');
} else {
retryInit();
}
}, 50 * Math.pow(2, retryCount));
} else {
console.error('图表容器初始化失败,已达到最大重试次数');
}
};
retryInit();
}
}
}, 50);
});
});
// 组件卸载时清理
onUnmounted(() => {
// 移除事件监听
window.removeEventListener('resize', handleResize);
// 销毁图表实例
if (chartInstance) {
// 解绑数据点点击事件
chartInstance.off('click', handleDataPointClick);
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style lang="scss" scoped>
.chart-wrapper {
width: 100%;
height: 231px;
min-height: 231px;
position: relative;
/* 为empty-overlay提供定位上下文 */
.water-temp-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;
}
}
:deep(.ant-spin-nested-loading >div>.ant-spin){
background: rgba(255, 255, 255) !important;
}
</style>