WholeProcessPlatform/frontend/src/views/dianZhanZhuanTi/components/powerStationReleasetaStistics.vue

444 lines
11 KiB
Vue
Raw Normal View History

2026-07-31 11:13:49 +08:00
<template>
<SidePanelItem
title="电站放流统计"
:datetimePicker="datePickerConfig"
@update-values="onDateChange"
>
<div class="release-statistics">
<a-spin
v-show="hasData"
:spinning="loading"
tip="加载中..."
class="chart-container"
>
<div ref="chartRef" class="chart-container"></div>
</a-spin>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</div>
</SidePanelItem>
<!-- 弹窗 -->
<a-modal
v-model:open="showModal"
title="电站放流详情"
width="80vw"
:footer="null"
:destroyOnClose="true"
>
<PowerStationReleasetaStisticsDetail
v-if="showModal"
:year="modalPageData.year"
:stcdSelectOptions="modalPageData.stcdSelectOptions"
:stcd="modalPageData.stcd"
/>
</a-modal>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import PowerStationReleasetaStisticsDetail from './PowerStationReleasetaStisticsDetail.vue';
import {
getPowerStationReleaseYear,
getPowerStationReleaseData
} from '@/api/dianZhanZhuanTi';
defineOptions({ name: 'PowerStationReleasetaStistics' });
// ==================== 注入电站上下文 ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== 常量 ====================
const currentBaseId = (window as any).__lyConfigs?.baseId ?? '02';
const UNIT = '万尾';
const COLORS = ['#6ca4f7', '#91cc75'];
// ==================== 状态 ====================
const loading = ref(false);
const hasData = ref(false);
const searchDate = ref<dayjs.Dayjs | null>(null);
const resData = ref<any[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== 弹窗状态 ====================
const showModal = ref(false);
const modalPageData = ref({
year: '',
stcdSelectOptions: [] as { label: string; value: string }[],
stcd: ''
});
// ==================== 年份选择器配置 ====================
const datePickerConfig = computed(() => ({
show: true,
picker: 'year' as const,
value: searchDate.value ? searchDate.value.format('YYYY') : undefined
}));
// ==================== 数据处理 ====================
interface ChartData {
stationList: string[];
planList: number[];
actualList: number[];
}
const chartData = computed<ChartData>(() => {
if (!resData.value?.length)
return { stationList: [], planList: [], actualList: [] };
return {
stationList: resData.value.map((item: any) => item.ennm),
planList: resData.value.map((item: any) =>
parseFloat(((item.fcntjh ?? 0) / 10000).toFixed(4))
),
actualList: resData.value.map((item: any) =>
parseFloat(((item.fcntjc ?? 0) / 10000).toFixed(4))
)
};
});
// ==================== 电站下拉选项(弹窗用) ====================
const stcdSelectOptions = computed(() => {
if (!resData.value?.length) return [];
const options = resData.value.map((item: any) => ({
label: item.ennm,
value: item.stcd
}));
return [{ label: '全部', value: '' }, ...options];
});
// ==================== ECharts ====================
const buildOption = (data: ChartData) => {
const allValues = [...data.planList, ...data.actualList];
const maxVal = Math.max(...allValues, 1);
const minVal = Math.min(0, ...allValues);
// 计算 y 轴刻度等分成4档
const rawRange = maxVal - minVal;
const rawStep = rawRange / 4;
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
const normalizedStep = rawStep / magnitude;
let niceStep: number;
if (normalizedStep <= 1) niceStep = 1;
else if (normalizedStep <= 2) niceStep = 2;
else if (normalizedStep <= 5) niceStep = 5;
else niceStep = 10;
niceStep *= magnitude;
const yMin = minVal < 0 ? 0 : minVal;
return {
tooltip: {
trigger: 'axis' as const,
formatter: (params: any) => {
if (!params || params.length === 0) return '';
let html = `<div style="font-size:14px;margin-bottom:6px;">${params[0].name}</div>`;
params.forEach((p: any) => {
const val = p.value ?? 0;
html += `
<div style="display:flex;align-items:center;margin:2px 0;">
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:${p.color};margin-right:6px;"></span>
<span>${p.seriesName}: <strong>${val}</strong> ${UNIT}</span>
</div>`;
});
return html;
}
},
legend: {
top: 0,
left: 'right',
orient: 'horizontal' as const,
icon: 'roundRect' as const,
itemGap: 10,
data: [
{ name: '计划放流', itemStyle: { color: COLORS[0] } },
{ name: '实际放流', itemStyle: { color: COLORS[1] } }
],
textStyle: { fontSize: 12 }
},
color: COLORS,
grid: {
top: '15%',
left: '8%',
right: '8%',
bottom: '0%',
containLabel: true
},
xAxis: {
type: 'category' as const,
data: data.stationList,
axisLabel: {
rotate: 45,
fontSize: 12
}
},
yAxis: {
type: 'value' as const,
name: `数量(${UNIT})`,
nameGap: 15,
nameTextStyle: { fontSize: 12 },
splitNumber: 4,
min: yMin,
max: maxVal + niceStep,
interval: niceStep
},
series: [
{
name: '计划放流',
type: 'bar',
data: data.planList,
barMaxWidth: 15,
barGap: '40%',
label: {
show: true,
position: 'bottom',
distance: -25,
rotate: 90,
fontSize: 12,
color: '#333',
align: 'top',
verticalAlign: 'middle'
}
},
{
name: '实际放流',
type: 'bar',
data: data.actualList,
barMaxWidth: 15,
barGap: '40%',
label: {
show: true,
position: 'bottom',
distance: -25,
rotate: 90,
fontSize: 13,
color: '#333',
align: 'top',
verticalAlign: 'middle'
}
}
]
};
};
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
nextTick(() => {
chartInstance = echarts.init(chartRef.value);
chartInstance.on('click', handleChartClick);
if (chartData.value.stationList.length > 0) {
chartInstance.setOption(buildOption(chartData.value), true);
}
});
};
const handleResize = () => {
if (chartInstance) chartInstance.resize();
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// ==================== API 请求 ====================
/** 获取数据年份(第一次加载时确定默认年份) */
const fetchYear = () => {
if (!stcd.value) return;
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
},
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcd.value
}
]
}
};
getPowerStationReleaseYear(params)
.then((res: any) => {
const list = res?.data?.data?.[0] ?? [];
if (list.length > 0) {
searchDate.value = dayjs(list[0].plansd);
} else {
searchDate.value = dayjs();
}
})
.catch(() => {
searchDate.value = dayjs();
});
};
/** 获取明细数据 */
const fetchData = () => {
if (!stcd.value || !searchDate.value) return;
loading.value = true;
const startTime = searchDate.value
.startOf('year')
.format('YYYY-MM-DD HH:mm:ss');
const endTime = searchDate.value.endOf('year').format('YYYY-MM-DD HH:mm:ss');
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startTime
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endTime
},
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcd.value
}
]
}
};
getPowerStationReleaseData(params)
.then((res: any) => {
loading.value = false;
const list = res?.data?.data ?? [];
resData.value = list;
hasData.value = list.length > 0;
if (chartInstance) {
chartInstance.setOption(buildOption(chartData.value), true);
}
nextTick(() => {
chartInstance?.resize();
});
})
.catch(() => {
loading.value = false;
resData.value = [];
hasData.value = false;
});
};
// ==================== 事件处理 ====================
const onDateChange = (payload: any) => {
if (payload.datetime) {
searchDate.value = dayjs(payload.datetime, 'YYYY');
}
};
/** 图表点击 → 打开弹窗 */
const handleChartClick = (params: any) => {
const name = params?.name;
if (!name) return;
const matched = stcdSelectOptions.value.find(item => item.label === name);
modalPageData.value = {
year: searchDate.value ? searchDate.value.format('YYYY') : '',
stcdSelectOptions: stcdSelectOptions.value,
stcd: matched?.value ?? ''
};
showModal.value = true;
};
// ==================== 监听 ====================
watch(stcd, newStcd => {
if (newStcd) {
fetchYear();
}
});
watch(searchDate, newDate => {
if (newDate) {
fetchData();
}
});
watch(
chartData,
newData => {
if (chartInstance && newData.stationList.length > 0) {
chartInstance.setOption(buildOption(newData), true);
nextTick(() => chartInstance?.resize());
}
},
{ deep: true }
);
// ==================== 生命周期 ====================
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
if (stcd.value) {
fetchYear();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped>
.release-statistics {
width: 100%;
height: 196px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 100%;
}
.empty-wrapper {
width: 100%;
height: 100%;
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
</style>