WholeProcessPlatform/frontend/src/modules/stllyanchengChangeMod/index.vue
2026-06-18 09:29:15 +08:00

615 lines
14 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.

<template>
<div>
<SidePanelItem
title="生态流量沿程变化"
:iconmap="iconmap"
:select="select"
:datetimePicker="datetimePicker"
@update-values="handlePanelChange"
>
<!-- 加载状态 -->
<div v-if="dataLoading" class="chart-loading">
<a-spin tip="加载中..." />
</div>
<!-- 无数据提示 -->
<a-empty
v-if="!chartData.length && !dataLoading"
description="暂无数据"
class="chart-empty"
/>
<!-- 图表容器(使用 display 控制,避免 v-if 销毁 DOM-->
<div
ref="chartRef"
class="chart-container"
:style="{
display: chartData.length && !dataLoading ? 'block' : 'none'
}"
></div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, onBeforeUnmount } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
// import { wbsbGetKendoList } from '@/api/sz';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
// TODO: 导入生态流量 API
import { qgcHourVGetKendoListCust, wbsbGetKendoList } from '@/api/stll';
import { useModelStore } from '@/store/modules/model';
// 定义组件名
defineOptions({
name: 'StllyanchengChangeMod'
});
// ==================== 响应式状态 ====================
// 图标提示信息
const iconmap = ref({
show: true,
value:
'入库流量是基于库容曲线和水量平衡的推算值,非实测数据。出库流量是基于机组出力特征曲线和闸门开度的推算值,非实测数据。',
icon: 'iconfont icon-time'
});
// 日期时间选择器配置
const datetimePicker = ref({
show: true,
value: dayjs().format('YYYY-MM-DD HH:mm'), // 默认当前时间
format: 'YYYY-MM-DD HH:mm',
picker: 'date',
timeFormat: 'HH:mm',
options: []
});
// 河段选择器配置
const select = ref({
show: true,
value: '',
options: [],
picker: undefined,
format: undefined
});
const modelStore = useModelStore();
// 业务数据状态
const wbsCode = ref(''); // 流域编码
const selectData = ref(''); // 选中的河段编码
const chartData = ref<any[]>([]); // 图表数据
const dataLoading = ref(false); // 加载状态
const latestTime = ref(''); // 最新数据时间
const hasTimeData = ref(false); // 是否有时间数据
// 图表相关
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// 弹窗状态
const modalVisible = ref(false);
const modalData = ref<any>({
stcd: '',
stnm: '',
titleName: ''
});
// Store
const JidiSelectEventStore = useJidiSelectEventStore();
// ==================== 核心方法 ====================
/**
* 获取河段列表
*/
const getSelect = async () => {
if (!wbsCode.value) {
console.warn('wbsCode 为空,无法获取河段数据');
return;
}
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'objId',
operator: 'eq',
value: wbsCode.value
},
{
field: 'wbsType',
operator: 'eq',
value: 'PSB_RVCD'
}
]
},
group: [
{
dir: 'asc',
field: 'orderIndex'
},
{
dir: 'asc',
field: 'wbsCode'
},
{
dir: 'asc',
field: 'wbsName'
},
{
dir: 'asc',
field: 'objid'
}
],
groupResultFlat: 'true'
};
try {
let res = await wbsbGetKendoList(params);
// 处理返回的数据
let data;
if (res && res.data && res.data.data) {
if (Array.isArray(res.data.data)) {
data = res.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;
selectData.value = data[0].wbsCode; // 同步选中第一个
select.value.options = data.map(item => ({
value: item.wbsCode,
label: item.wbsName
}));
// 获取完河段后立即加载图表数据
await loadChartData();
} else {
console.warn('河段数据为空');
select.value.value = '';
select.value.options = [];
}
} catch (error) {
console.error('获取河段数据失败:', error);
}
};
/**
* 获取生态流量数据(预留 API 位置)
*/
const getList = async (rvcd: string, sDate: string) => {
// TODO: 调用实际的 API
// 参数结构参考 React 版本:
// - baseId: 从 JidiSelectEventStore.selectedItem 获取
// - rvcd: 河段编码
// - tm: 时间范围当前时间往前推1个月
const currentTime = dayjs(sDate);
const startTime = currentTime.subtract(1, 'month');
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: wbsCode.value
},
{
field: 'rvcd',
operator: 'eq',
dataType: 'string',
value: rvcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: startTime.format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: currentTime.format('YYYY-MM-DD HH:mm:ss')
}
]
},
sort: [
{
field: 'sort',
dir: 'asc'
}
]
};
// TODO: 替换为实际 API 调用FAA
const res = await qgcHourVGetKendoListCust(params);
// 处理返回的数据结构
if (!res || !res.data || !res.data.data || !Array.isArray(res.data.data)) {
console.warn('未获取到有效的生态流量数据:', res);
return [];
}
// 映射数据结构,适配图表展示需求
return res.data.data.map((item: any) => ({
TM: dayjs(item.tm).format('YYYY-MM-DD H时'),
stnm: item.stnm,
stcd: item.stcd,
qec: item.qec, // 生态流量
qecLimit: item.qecLimit, // 环评限值
qi: item.qi // 入库流量
}));
};
/**
* 加载图表数据
*/
const loadChartData = async () => {
if (!selectData.value || !datetimePicker.value.value) {
return;
}
dataLoading.value = true;
try {
const data = await getList(selectData.value, datetimePicker.value.value);
if (data && data.length > 0) {
chartData.value = data;
hasTimeData.value = true;
// 提取最新时间
if (data[0]?.TM) {
latestTime.value = data[0].TM;
}
// 数据加载完成后渲染图表
await nextTick();
initChart();
} else {
chartData.value = [];
hasTimeData.value = false;
latestTime.value = '';
// 清空图表
if (chartInstance) {
chartInstance.clear();
}
}
} catch (error) {
console.error('加载图表数据失败:', error);
chartData.value = [];
} finally {
dataLoading.value = false;
}
};
/**
* 初始化 ECharts 图表
*/
const initChart = () => {
if (!chartRef.value) {
console.warn('图表容器未准备好');
return;
}
// 如果实例已存在,先销毁
if (chartInstance) {
chartInstance.dispose();
}
// 创建新实例
chartInstance = echarts.init(chartRef.value);
// 准备数据
const xData: string[] = [];
const inData: (number | string)[] = []; // 入库流量
const outData: (number | string)[] = []; // 生态流量
const limitData: (number | string)[] = []; // 环评限值
let hasValidData = false; // 标记是否有有效数据
chartData.value.forEach(item => {
xData.push(item.stnm);
const qecValue = item.qec ?? '-';
const qiValue = item.qi ?? '-';
const qecLimitValue = item.qecLimit ?? '-';
outData.push(qecValue);
inData.push(qiValue);
limitData.push(qecLimitValue);
// 检查是否有任意一个字段有有效数据
if (qecValue !== '-' || qiValue !== '-' || qecLimitValue !== '-') {
hasValidData = true;
}
});
// 如果所有数据都为空,显示暂无数据
if (!hasValidData && chartData.value.length > 0) {
chartData.value = [];
if (chartInstance) {
chartInstance.clear();
}
return;
}
// 图表配置
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
confine: true,
formatter: function (params: any) {
const datum = chartData.value.find(
(el: any) => el.stnm === params[0].name
);
const unit = 'm³/s'; // TODO: 根据单位配置动态获取
const content = params
.map((item: any) => {
const value = item.value ?? '-';
return `<div>${item.marker}${item.seriesName}: &nbsp;&nbsp;&nbsp;&nbsp;<span style="float:right">${value} ${unit}</span></div>`;
})
.join('');
return `<h3 style="color:white">${params[0].name}<br/>${
datum?.TM || ''
}</h3>${content}`;
},
axisPointer: {
type: 'line'
}
},
legend: {
data: ['入库流量', '生态流量', '环评要求限值'],
icon: 'roundRect',
top: 10
},
grid: {
left: 10,
right: 10,
bottom: 10,
top: 70,
containLabel: true
},
xAxis: {
type: 'category',
data: xData,
axisLine: {
lineStyle: {
color: '#505050'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#8f8f8f',
type: 'solid'
}
},
axisLabel: {
interval: xData.length > 12 ? 2 : 0,
rotate: 0,
formatter: (value: string, index: number) => {
const shortValue =
value.substring(0, 6) + (value.length > 6 ? '...' : '');
if (index % 2 !== 0) {
return `{a|${shortValue}}`;
} else {
return `\n{b|}${shortValue}`;
}
},
rich: {
a: {
height: 20,
align: 'center'
},
b: {
height: 30,
align: 'center'
}
}
}
},
yAxis: {
type: 'value',
name: '流量(m³/s)', // TODO: 根据单位配置动态获取
minInterval: 1,
axisLine: {
lineStyle: {
color: '#505050'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#8f8f8f',
type: 'solid'
}
}
},
dataZoom: [
{
type: 'inside',
xAxisIndex: 0,
start: 0,
end: 100,
zoomOnMouseWheel: true,
moveOnMouseMove: true
}
],
series: [
{
name: '入库流量',
data: inData,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
connectNulls: true,
color: '#5470c6' // TODO: 根据配置动态获取
},
{
name: '生态流量',
data: outData,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
connectNulls: true,
color: '#91cc75' // TODO: 根据配置动态获取
},
{
name: '环评要求限值',
data: limitData,
type: 'bar',
barMaxWidth: 10,
color: '#fac858' // TODO: 根据配置动态获取
}
]
};
chartInstance.setOption(option);
// 绑定点击事件
chartInstance.on('click', handleChartClick);
// 自适应窗口大小
window.addEventListener('resize', handleResize);
};
/**
* 图表点击事件
*/
const handleChartClick = (params: any) => {
const datum = chartData.value.find((item: any) => item.stnm === params.name);
if (datum) {
const currentTime = dayjs(datetimePicker.value.value);
const startTime = currentTime.subtract(1, 'month');
modalVisible.value = true;
modelStore.modalVisible = true;
modelStore.params.sttp = 'ENG';
modelStore.title = datum.stnm;
modelStore.params.stcd = datum.stcd;
modelStore.params.eqtp = 'QEC';
modelStore.filter.rangeTm = [startTime, currentTime];
modelStore.filter.stllgzlx = 'qecInterval';
modelStore.filter.stllTmType = 'day';
}
};
/**
* 窗口大小改变时重新调整图表
*/
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
/**
* 弹窗取消事件
*/
const handleModalCancel = () => {
modalVisible.value = false;
modalData.value = {
stcd: '',
stnm: '',
titleName: ''
};
};
/**
* 面板数据变化处理
*/
const handlePanelChange = async (data: any) => {
console.log('当前所有控件状态:', data);
// 当选择器或日期变化时,重新加载图表数据
if (data.select) {
select.value.value = data.select;
selectData.value = data.select;
}
if (data.datetime) {
datetimePicker.value.value = data.datetime;
}
// 重新加载数据
if (data.select || data.datetime) {
await loadChartData();
}
};
// ==================== 生命周期与监听 ====================
// 监听流域切换
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
console.log('流域切换:', newVal);
if (newVal?.wbsCode) {
wbsCode.value = newVal.wbsCode;
getSelect();
}
},
{ deep: true, immediate: true }
);
// 组件卸载时清理
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
});
</script>
<style lang="scss" scoped>
.chart-loading {
display: flex;
justify-content: center;
align-items: center;
height: 424px;
}
.chart-empty {
display: flex;
align-items: center;
height: 424px;
flex-direction: column;
/* align-content: center; */
justify-content: center;
text-align: center !important;
:deep(.ant-empty-description) {
text-align: center !important;
text-indent: 0;
width: 100%;
}
}
.chart-container {
width: 406px;
height: 424px;
}
.time-tip {
text-align: right;
font-size: 12px;
color: #8e8e8e;
padding: 8px 0;
margin-top: 4px;
}
</style>