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>
|