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

736 lines
19 KiB
Vue
Raw Normal View History

2026-05-09 17:04:48 +08:00
<!-- 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="暂无数据" />
2026-06-03 17:10:34 +08:00
</div>
</a-spin>
</div>
</SidePanelItem>
2026-05-09 17:04:48 +08:00
</template>
<script lang="ts" setup>
2026-06-03 17:10:34 +08:00
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
2026-05-09 17:04:48 +08:00
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
2026-06-03 17:10:34 +08:00
import { msstbprptGetKendoList, sdrvwtsGetKendoList } from '@/api/qxd';
import { useModelStore } from '@/store/modules/model';
import dayjs from 'dayjs';
2026-05-09 17:04:48 +08:00
// 定义组件名
defineOptions({
name: 'qixidishuiwenbianhua'
2026-05-09 17:04:48 +08:00
});
// ==================== 响应式变量定义 ====================
2026-06-03 17:10:34 +08:00
const JidiSelectEventStore = useJidiSelectEventStore();
2026-05-09 17:04:48 +08:00
// 图表容器引用
const chartRef = ref<HTMLDivElement>();
let chartInstance: echarts.ECharts | null = null;
const baseid = ref('');
2026-06-03 17:10:34 +08:00
// Loading 状态和数据状态
const loading = ref(false);
const hasData = ref(true); // 初始化为 true因为 initChart 时会渲染
2026-05-09 17:04:48 +08:00
// 选择器配置
const select = ref({
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined
2026-05-09 17:04:48 +08:00
});
// 日期选择器配置
2026-06-03 17:10:34 +08:00
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: []
2026-05-09 17:04:48 +08:00
});
// 图表数据
const echartsData = ref<any[]>([]);
2026-06-03 17:10:34 +08:00
const modelStore = useModelStore();
2026-05-09 17:04:48 +08:00
// 单位配置
const unita = ref('℃');
// 颜色配置 - 使用HSL色彩空间生成舒适的颜色
const Color = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4'
];
2026-05-09 17:04:48 +08:00
// ==================== 工具函数 ====================
/**
* 生成模拟数据 - 季节性水温变化
*/
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))
});
}
2026-05-09 17:04:48 +08:00
return data;
2026-05-09 17:04:48 +08:00
};
/**
* 初始化图表
*/
const initChart = () => {
if (!chartRef.value) {
console.warn('图表容器未找到');
return;
}
2026-05-09 17:04:48 +08:00
// 销毁已存在的实例
if (chartInstance) {
chartInstance.dispose();
}
2026-05-09 17:04:48 +08:00
// 创建新实例
chartInstance = echarts.init(chartRef.value);
2026-05-09 17:04:48 +08:00
// 绑定数据点点击事件
chartInstance.on('click', handleDataPointClick);
2026-06-03 17:10:34 +08:00
// 更新图表
updateChart();
2026-05-09 17:04:48 +08:00
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
2026-05-09 17:04:48 +08:00
};
/**
* 更新图表配置
*/
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'
}
2026-05-09 17:04:48 +08:00
},
// X轴的axisPointer配置鼠标经过时显示阴影块
2026-05-09 17:04:48 +08:00
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
2026-05-09 17:04:48 +08:00
},
// 计算 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)}` // 取整并显示单位
2026-05-09 17:04:48 +08:00
},
axisTick: {
show: true
2026-05-09 17:04:48 +08:00
},
axisLine: {
show: true,
lineStyle: {
color: '#333'
}
2026-05-09 17:04:48 +08:00
},
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);
2026-05-09 17:04:48 +08:00
};
/**
* 处理窗口大小变化
*/
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
2026-05-09 17:04:48 +08:00
};
2026-06-03 17:10:34 +08:00
/**
* 处理数据点点击事件
*/
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')
];
}
2026-06-03 17:10:34 +08:00
};
2026-05-09 17:04:48 +08:00
/**
* 加载模拟数据
*/
const loadData = async () => {
try {
// 生成模拟数据
echartsData.value = generateMockData();
// 数据加载完成后更新图表
await nextTick();
updateChart();
} catch (error) {
console.error('加载水温数据失败:', error);
}
2026-05-09 17:04:48 +08:00
};
2026-06-03 17:10:34 +08:00
/**
* 加载图表数据
* @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'
2026-06-03 17:10:34 +08:00
}
]
};
2026-06-03 17:10:34 +08:00
// 调用实际的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;
2026-06-03 17:10:34 +08:00
}
// 转换数据格式提取日期部分用于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;
}
2026-06-03 17:10:34 +08:00
};
/**
* YY-MM 格式转换为完整的日期时间范围
* @param dateRange ['26-05', '26-06'] 格式的数组
* @returns { startDate: string, endDate: string } 格式化后的日期范围
*/
const convertDateRange = (dateRange: string[]) => {
if (!dateRange || dateRange.length !== 2) {
return null;
}
2026-06-18 09:29:15 +08:00
//
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
};
2026-06-03 17:10:34 +08:00
};
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);
2026-06-03 17:10:34 +08:00
} else {
console.error('日期转换失败');
2026-06-03 17:10:34 +08:00
}
} else {
loading.value = false;
hasData.value = false;
// 清空图表数据
echartsData.value = [];
// 更新图表显示空状态
await nextTick();
updateChart();
console.warn('参数不完整:', {
scopeDate: data.scopeDate,
select: data.select
});
}
};
2026-06-03 17:10:34 +08:00
//获取水温监测下拉框
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'
2026-06-03 17:10:34 +08:00
},
{
logic: 'or',
filters: [
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WTRV'
}
]
2026-06-03 17:10:34 +08:00
}
].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
};
});
2026-06-18 09:29:15 +08:00
//
2026-06-03 17:10:34 +08:00
};
////
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
baseid.value = newVal.wbsCode;
getselsectData();
},
{ deep: true, immediate: true }
2026-06-03 17:10:34 +08:00
);
2026-05-09 17:04:48 +08:00
// ==================== 生命周期钩子 ====================
// 页面加载时执行
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('✅ 图表初始化完成(重试成功)');
2026-05-09 17:04:48 +08:00
} else {
retryInit();
2026-05-09 17:04:48 +08:00
}
}, 50 * Math.pow(2, retryCount));
} else {
console.error('图表容器初始化失败,已达到最大重试次数');
2026-05-09 17:04:48 +08:00
}
};
retryInit();
}
}
}, 50);
});
2026-05-09 17:04:48 +08:00
});
// 组件卸载时清理
onUnmounted(() => {
// 移除事件监听
window.removeEventListener('resize', handleResize);
// 销毁图表实例
if (chartInstance) {
// 解绑数据点点击事件
chartInstance.off('click', handleDataPointClick);
chartInstance.dispose();
chartInstance = null;
}
2026-05-09 17:04:48 +08:00
});
</script>
<style lang="scss" scoped>
2026-06-03 17:10:34 +08:00
.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%;
2026-05-09 17:04:48 +08:00
min-height: 231px;
}
2026-06-03 17:10:34 +08:00
.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;
}
2026-05-09 17:04:48 +08:00
}
</style>