615 lines
14 KiB
Vue
615 lines
14 KiB
Vue
<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}: <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>
|