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