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

615 lines
14 KiB
Vue
Raw Normal View History

2026-06-16 17:39:51 +08:00
<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) {
2026-06-18 09:29:15 +08:00
// ;
2026-06-16 17:39:51 +08:00
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>