WholeProcessPlatform/frontend/src/views/dianZhanZhuanTi/components/powerStationReleasetaStistics.vue
2026-08-20 09:23:32 +08:00

444 lines
11 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>
<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>