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

698 lines
22 KiB
Vue
Raw Normal View History

2026-05-09 17:04:48 +08:00
<!-- SidePanelItem.vue -->
<template>
2026-06-03 17:10:34 +08:00
<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>
2026-05-09 17:04:48 +08:00
</SidePanelItem>
</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';
2026-06-03 17:10:34 +08:00
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
import { msstbprptGetKendoList, sdrvwtsGetKendoList } from '@/api/qxd';
import { useModelStore } from "@/store/modules/model";
2026-05-09 17:04:48 +08:00
// 定义组件名
defineOptions({
name: 'qixidishuiwenbianhua'
});
// ==================== 响应式变量定义 ====================
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;
2026-06-03 17:10:34 +08:00
const baseid = ref('')
// 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-06-03 17:10:34 +08:00
const datetimePicker: any = ref({
2026-05-09 17:04:48 +08:00
show: true,
2026-05-12 08:47:27 +08:00
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,
2026-05-09 17:04:48 +08:00
options: []
});
// 图表数据
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'];
// ==================== 工具函数 ====================
/**
* 生成模拟数据 - 季节性水温变化
*/
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);
2026-06-03 17:10:34 +08:00
// 绑定数据点点击事件
chartInstance.on('click', handleDataPointClick);
2026-05-09 17:04:48 +08:00
// 更新图表
updateChart();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
};
/**
* 更新图表配置
*/
const updateChart = () => {
if (!chartInstance) return;
const _legendData = [`水温(${unita.value})`];
2026-06-03 17:10:34 +08:00
// 处理空数据情况
const hasData = echartsData.value && echartsData.value.length > 0;
2026-05-09 17:04:48 +08:00
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);
2026-06-03 17:10:34 +08:00
// 数据保留一位小数
const finalValue = item.value !== undefined && item.value !== null
? Number(item.value).toFixed(1)
: '-';
2026-05-09 17:04:48 +08:00
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,
2026-06-03 17:10:34 +08:00
top: 0,
2026-05-09 17:04:48 +08:00
left: 'center',
selectedMode: 'multiple'
},
grid: {
2026-06-03 17:10:34 +08:00
top: 25,
left: '3%',
right: '3%',
bottom: '3%',
2026-05-09 17:04:48 +08:00
containLabel: true
},
dataZoom: {
type: 'inside',
start: 0,
end: 100
},
xAxis: [
{
type: 'category',
data: echartsData.value?.map((item: any) => item.dt) || [],
boundaryGap: true,
2026-06-03 17:10:34 +08:00
// 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;
}
},
2026-05-09 17:04:48 +08:00
// 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',
2026-06-03 17:10:34 +08:00
// 移除 name避免与 legend 重复,在刻度上显示单位
name: _legendData[0], // 显示 "水温(℃)"
nameLocation: 'end', // 显示在 Y 轴顶部
nameGap: 8, // 与轴线距离 20px
nameTextStyle: {
color: '#333333',
fontSize: 12,
2026-06-03 17:10:34 +08:00
},
// 计算 Y 轴范围:最小值向下取整,最大值向上取整
min: hasData && echartsData.value.length > 0
? Math.floor(Math.min(...echartsData.value.map(item => item.wt)))
2026-06-03 17:10:34 +08:00
: undefined,
max: hasData && echartsData.value.length > 0
? Math.ceil(Math.max(...echartsData.value.map(item => item.wt)))
2026-06-03 17:10:34 +08:00
: undefined,
// 确保显示刻度和标签,保持整数
2026-05-09 17:04:48 +08:00
axisLabel: {
show: true,
2026-06-03 17:10:34 +08:00
color: '#333',
formatter: (value: number) => `${Math.round(value)}` // 取整并显示单位
2026-05-09 17:04:48 +08:00
},
axisTick: {
show: true
},
axisLine: {
show: true,
lineStyle: {
2026-06-03 17:10:34 +08:00
color: '#333'
2026-05-09 17:04:48 +08:00
}
},
splitLine: {
show: true,
lineStyle: {
2026-06-03 17:10:34 +08:00
type: 'solid'
2026-05-09 17:04:48 +08:00
}
},
2026-06-03 17:10:34 +08:00
// 确保 Y 轴刻度为整数
minInterval: 1,
maxInterval: 1
2026-05-09 17:04:48 +08:00
}
],
series: [
{
name: _legendData[0],
2026-06-03 17:10:34 +08:00
data: hasData ? (echartsData.value?.map((item: any) => item.wt) || []) : [],
2026-05-09 17:04:48 +08:00
type: 'line',
smooth: true,
symbol: 'circle',
2026-06-03 17:10:34 +08:00
symbolSize: 4,
2026-05-09 17:04:48 +08:00
lineStyle: {
width: 2
}
}
]
};
// 使用完全替换模式更新配置
chartInstance.setOption(option, true);
};
/**
* 处理窗口大小变化
*/
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
2026-06-03 17:10:34 +08:00
/**
* 处理数据点点击事件
*/
const handleDataPointClick = (params: any) => {
console.log('=== 数据点点击事件 ===');
console.log('点击参数:', params);
2026-06-03 17:10:34 +08:00
if (!params || !params.dataIndex) {
console.warn('无效的数据点点击');
return;
}
const dataIndex = params.dataIndex;
const dataItem = echartsData.value[dataIndex];
2026-06-03 17:10:34 +08:00
if (dataItem) {
console.log('点击的数据:', {
日期: dataItem.dt,
水温: dataItem.wt,
测站代码: dataItem.stcd,
索引: dataIndex
});
modelStore.modalVisible = true;
modelStore.params.sttp = "wt_point";
modelStore.title = '水温监测';
modelStore.params.stcd = dataItem.stcd;
modelStore.showStcdSelector = true
modelStore.stcdOptions = select.value.options
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-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",
2026-06-03 17:10:34 +08:00
"operator": "eq",
"dataType": "string",
"value": stcd
}
]
},
"sort": [
{
"field": "dt",
"dir": "asc"
}
]
2026-06-03 17:10:34 +08:00
};
// 调用实际的API获取数据
const res = await sdrvwtsGetKendoList(params);
debugger
2026-06-03 17:10:34 +08:00
console.log('API返回结果:', res);
2026-06-03 17:10:34 +08:00
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;
}
2026-06-03 17:10:34 +08:00
// 确保 wt 是数值类型
const wt = typeof item.wt === 'number' ? item.wt : parseFloat(item.wt);
if (isNaN(wt)) {
console.warn('无效的水温数据:', item);
return null;
}
2026-06-03 17:10:34 +08:00
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;
}
// 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
};
};
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) {
2026-06-03 17:10:34 +08:00
select.value.value = ''
select.value.options = []
return
}
2026-06-03 17:10:34 +08:00
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
};
////
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
baseid.value = newVal.wbsCode;
getselsectData()
},
{ deep: true, immediate: true }
);
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();
2026-06-03 17:10:34 +08:00
// 注释掉模拟数据加载,等待用户交互后加载真实数据
// loadData();
console.log('✅ 图表初始化完成,等待用户交互加载数据');
2026-05-09 17:04:48 +08:00
} 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();
2026-06-03 17:10:34 +08:00
// loadData();
console.log('✅ 图表初始化完成(重试成功)');
2026-05-09 17:04:48 +08:00
} else {
retryInit();
}
}, 50 * Math.pow(2, retryCount));
} else {
console.error('图表容器初始化失败,已达到最大重试次数');
}
};
retryInit();
}
}
}, 50);
});
});
// 组件卸载时清理
onUnmounted(() => {
// 移除事件监听
window.removeEventListener('resize', handleResize);
// 销毁图表实例
if (chartInstance) {
2026-06-03 17:10:34 +08:00
// 解绑数据点点击事件
chartInstance.off('click', handleDataPointClick);
2026-05-09 17:04:48 +08:00
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style lang="scss" scoped>
2026-06-03 17:10:34 +08:00
.chart-wrapper {
2026-05-09 17:04:48 +08:00
width: 100%;
height: 231px;
min-height: 231px;
2026-06-03 17:10:34 +08:00
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;
}
2026-05-09 17:04:48 +08:00
}
</style>