新增模块:生态流量泄放方式,泄放方式分布情况,模块生态流量达标情况BUG修改

This commit is contained in:
王兴凯 2026-04-02 09:26:02 +08:00
parent 58b1c9d663
commit 3d34172c58
6 changed files with 743 additions and 34 deletions

View File

@ -0,0 +1,14 @@
import request from '@/utils/request';
import type { EcoFlowStandard, EcoFlowQueryParams } from './types';
/**
*
* @param params
*/
export function getEcoFlowStandardData(params?: EcoFlowQueryParams): Promise<{ data: EcoFlowStandard[] }> {
return request({
url: '/api/eco-flow/standard',
method: 'get',
params
});
}

View File

@ -0,0 +1,12 @@
export interface EcoFlowStandard {
id: number;
baseName: string;
category: string;
currentRate: number;
lastYearRate: number;
}
export interface EcoFlowQueryParams {
mode?: 'top' | 'left';
baseId?: number;
}

View File

@ -0,0 +1,194 @@
<!-- SidePanelItem.vue -->
<template>
<div class="xie-fang-fang-shi-container">
<SidePanelItem title="生态流量泄放方式">
<div ref="chartRef" class="pie-chart"></div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import * as echarts from 'echarts';
import type { ECharts } from 'echarts';
// 便
defineOptions({
name: 'xieFangFangShi'
});
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: ECharts | null = null;
//
const pieData = [
{ value: 44.83, name: '基荷发电' },
{ value: 19.31, name: '泄洪设施' },
{ value: 8.28, name: '生态机组' },
{ value: 8.96, name: '生态放流管' },
{ value: 9.65, name: '生态放流闸' },
{ value: 5.52, name: '生态放流孔' },
{ value: 3.45, name: '生态放流洞' }
];
//
const colors = [
'#9556a4', // -
'#30b7b9', // - 绿
'#4b79ab', // -
'#dbb629', // -
'#df91ab', // -
'#78c300', // - 绿
'#7399c6' // -
];
//
const initChart = () => {
if (!chartRef.value) return;
//
if (chartRef.value.clientHeight === 0) {
// 0
setTimeout(() => {
initChart();
}, 100);
return;
}
chartInstance = echarts.init(chartRef.value);
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}%'
},
series: [
{
name: '泄放设施',
type: 'pie',
radius: ['42%', '62%'], // 42% 62%
center: ['50%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'outside',
alignTo: 'edge', //
margin: 8, //
formatter: (params: any) => {
return `{name|${params.name}}\n{value|${params.value}%}`;
},
rich: {
name: {
fontSize: 12,
color: '#666666', //
lineHeight: 18,
fontFamily: 'Microsoft YaHei, sans-serif',
align: 'left' //
},
value: {
fontSize: 12,
color: '#2f6b98', //
lineHeight: 18,
fontFamily: 'Microsoft YaHei, sans-serif',
align: 'center', //
fontWeight: 'bold' //
}
},
overflow: 'none', //
padding: [0, 0]
},
labelLine: {
show: true,
length: 12,
length2: 12,
lineStyle: {
width: 1
}
},
emphasis: {
scale: false, //
itemStyle: {
brightness: 1.2, // 20%
shadowBlur: 0 //
},
labelLine: {
show: true // 线
}
},
data: pieData.map((item, index) => ({
...item,
itemStyle: { color: colors[index % colors.length] },
labelLine: {
lineStyle: {
color: colors[index % colors.length]
}
}
}))
}
],
//
title: {
text: '145',
subtext: '泄放设施总数量\n(个)',
left: 'center',
top: 'center',
textStyle: {
color: '#2e86de',
fontSize: 30,
fontWeight: 'bold',
fontFamily: 'Arial'
},
subtextStyle: {
color: '#666',
fontSize: 12,
lineHeight: 16,
fontFamily: 'Microsoft YaHei, sans-serif'
}
}
};
chartInstance.setOption(option);
};
//
const handleResize = () => {
chartInstance?.resize();
};
//
onMounted(() => {
// 使 nextTick DOM
setTimeout(() => {
initChart();
// resize
setTimeout(() => {
chartInstance?.resize();
}, 200);
}, 50);
window.addEventListener('resize', handleResize);
});
//
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
});
</script>
<style lang="scss" scoped>
.xie-fang-fang-shi-container {
width: 100%;
height: 100%;
}
.pie-chart {
width: 100%;
height: 350px;
}
</style>

View File

@ -0,0 +1,386 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="泄放方式分布情况">
<div class="chart-container">
<!-- 自定义图例区域 -->
<div class="legend-container">
<div class="legend-items">
<div
v-for="(item, index) in currentLegendItems"
:key="item.name"
class="legend-item"
:class="{ 'inactive': legendInactiveSet.has(item.name) }"
@click="toggleLegend(item.name)"
>
<span class="legend-color" :style="{ backgroundColor: item.color }"></span>
<span class="legend-text">{{ item.name }}</span>
</div>
</div>
<!-- 分页控制 -->
<div class="legend-pagination" v-if="totalPages > 1">
<span
class="pagination-btn"
:class="{ disabled: currentPage === 1 }"
@click="prevPage"
>
</span>
<span class="pagination-text">{{ currentPage }}/{{ totalPages }}</span>
<span
class="pagination-btn"
:class="{ disabled: currentPage === totalPages }"
@click="nextPage"
>
</span>
</div>
</div>
<!-- ECharts 图表容器 -->
<div ref="chartRef" class="chart"></div>
</div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
//
defineOptions({
name: 'xieFangFenBu'
});
//
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
//
const ITEMS_PER_PAGE = 4; // 4
const currentPage = ref(1); //
// 8
const allLegendItems = ref([
{ name: '生态放流孔', color: '#7cb342' },
{ name: '生态放流闸', color: '#ce93d8' },
{ name: '生态放流洞', color: '#64b5f6' },
{ name: '生态放流管', color: '#ffd54f' },
{ name: '生态放流口', color: '#ff8a65' },
{ name: '生态放流道', color: '#4db6ac' },
{ name: '生态放流渠', color: '#ba68c8' },
{ name: '生态放流站', color: '#a1887f' }
]);
//
const totalPages = computed(() => {
return Math.ceil(allLegendItems.value.length / ITEMS_PER_PAGE);
});
//
const currentLegendItems = computed(() => {
const start = (currentPage.value - 1) * ITEMS_PER_PAGE;
const end = start + ITEMS_PER_PAGE;
return allLegendItems.value.slice(start, end);
});
// inactive
const legendInactiveSet = ref<Set<string>>(new Set());
// /
const toggleLegend = (name: string) => {
if (legendInactiveSet.value.has(name)) {
legendInactiveSet.value.delete(name);
} else {
legendInactiveSet.value.add(name);
}
updateChart();
};
//
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
//
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
// - API
const xData = ref([
'金沙江干流',
'雅砻江干流',
'大渡河干流',
'乌江干流',
'湘西',
'黄河上游干流',
'东北',
'闽浙赣',
'其他'
]);
// 8
const seriesData = ref([
{ name: '生态放流孔', data: [2, 1, 3, 1, 2, 1, 0, 1, 5] },
{ name: '生态放流闸', data: [3, 2, 4, 2, 3, 1, 1, 2, 8] },
{ name: '生态放流洞', data: [5, 2, 6, 3, 4, 2, 1, 2, 9] },
{ name: '生态放流管', data: [15, 0, 12, 18, 4, 0, 0, 0, 21] },
{ name: '生态放流口', data: [0, 0, 0, 0, 0, 0, 0, 0, 0] },
{ name: '生态放流道', data: [0, 0, 0, 0, 0, 0, 0, 0, 0] },
{ name: '生态放流渠', data: [0, 0, 0, 0, 0, 0, 0, 0, 0] },
{ name: '生态放流站', data: [0, 0, 0, 0, 0, 0, 0, 0, 0] }
]);
//
const calculateTotal = (index: number) => {
return seriesData.value
.filter(series => !legendInactiveSet.value.has(series.name)) //
.reduce((sum, series) => {
return sum + (series.data[index] || 0);
}, 0);
};
//
const updateChart = () => {
if (!chartInstance) return;
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params: any) => {
let result = `${params[0].name}<br/>`;
params.forEach((param: any) => {
// active
if (!legendInactiveSet.value.has(param.seriesName)) {
result += `${param.marker} ${param.seriesName}: ${param.value}<br/>`;
}
});
return result;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: xData.value,
axisLabel: {
rotate: 45, // X 45
interval: 0 //
},
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value',
min: 0,
// Y
axisLabel: {
formatter: '{value}'
}
},
series: seriesData.value.map((series, index) => {
//
const legendItem = allLegendItems.value.find(item => item.name === series.name);
const color = legendItem?.color;
//
const isHidden = legendInactiveSet.value.has(series.name);
return {
name: series.name,
type: 'bar',
stack: 'total',
barWidth: '60%',
// null使
data: isHidden ? series.data.map(() => null) : series.data,
//
itemStyle: {
color: color
},
// -
label: {
show: index === seriesData.value.length - 1, //
position: 'top',
fontSize: 12,
color: '#333',
formatter: (params: any) => {
//
const total = calculateTotal(params.dataIndex);
return total > 0 ? total.toString() : '';
}
},
//
emphasis: {
itemStyle: {
shadowBlur: 0,
shadowColor: 'rgba(0, 0, 0, 0)'
}
}
};
})
};
chartInstance.setOption(option, true);
};
//
const initChart = () => {
if (!chartRef.value) return;
//
setTimeout(() => {
chartInstance = echarts.init(chartRef.value);
updateChart();
//
setTimeout(() => {
chartInstance?.resize();
}, 100);
}, 50);
};
//
const handleResize = () => {
chartInstance?.resize();
};
//
watch([currentPage, legendInactiveSet], () => {
updateChart();
}, { deep: true });
//
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
//
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
chartInstance = null;
});
//
const refreshData = (newXData: string[], newSeriesData: any[]) => {
xData.value = newXData;
seriesData.value = newSeriesData;
updateChart();
};
//
const setLegendData = (legendData: Array<{ name: string; color: string }>) => {
allLegendItems.value = legendData;
currentPage.value = 1; //
legendInactiveSet.value.clear();
};
// 使
defineExpose({
refreshData,
setLegendData
});
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
.legend-container {
display: flex;
justify-content: space-between;
align-items: center;
.legend-items {
display: flex;
gap: 10px; //
flex-wrap: nowrap; //
flex: 1; //
min-width: 0; //
.legend-item {
display: flex;
align-items: center;
cursor: pointer;
transition: opacity 0.3s;
flex-shrink: 0; //
&:hover {
opacity: 0.8;
}
&.inactive {
opacity: 0.5;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
margin-right: 4px;
flex-shrink: 0;
}
.legend-text {
font-size: 12px; //
color: #333;
white-space: nowrap;
}
}
}
.legend-pagination {
display: flex;
align-items: center;
flex-shrink: 0;
margin-left: 5px; //
.pagination-btn {
cursor: pointer;
font-size: 14px;
color: #666;
user-select: none;
transition: color 0.3s;
&:hover:not(.disabled) {
color: #409eff;
}
&.disabled {
cursor: not-allowed;
color: #ccc;
}
}
.pagination-text {
font-size: 13px;
color: #666;
min-width: 30px;
text-align: center;
}
}
}
.chart {
width: 100%;
height: 350px;
}
}
</style>

View File

@ -19,7 +19,6 @@ import { ref, onMounted, watch } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import * as echarts from 'echarts';
// 便
defineOptions({
name: 'shengtaidabiaoMod'
@ -34,18 +33,45 @@ const spinning = ref(false)
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
//
const categoryData = [
'其他', '闽浙赣', '澜沧江干流', '东北', '南盘江·红水河',
'黄河中游干流', '黄河上游干流', '湘西', '长江上游干流',
'乌江干流', '大渡河干流', '雅砻江干流', '金沙江干流'
];
const currentData = Array(13).fill(0).map(() => Math.random() * 5 + 86);
const lastYearData = Array(13).fill(0).map(() => Math.random() * 5 + 86);
//
const baseData = {
categories: ['金沙江干流', '雅砻江干流', '大渡河干流', '乌江干流', '长江上游干流', '湘西', '黄河上游干流', '黄河中游干流', '南盘江 - 红水河', '东北', '澜沧江干流', '闽浙赣', '其他'],
currentData: [98, 100, 99.5, 99.8, 100, 98.5, 100, 100, 100, 97, 100, 93, 98],
lastYearData: [92, 100, 99, 100, 100, 98, 100, 100, 100, 100, 100, 88, 93]
};
//
const performanceData = {
categories: ['多年调节', '年调节', '季调节', '周调节', '其他'],
currentData: [95, 92, 88, 85, 90],
lastYearData: [93, 90, 85, 82, 88]
};
//
const loadData = () => {
spinning.value = true;
// DOM
setTimeout(() => {
initChart();
spinning.value = false;
}, 50);
};
//
const initChart = () => {
if (!chartRef.value) return;
if (!chartRef.value) {
console.error('图表容器未渲染');
return;
}
//
const containerHeight = chartRef.value.offsetHeight;
if (!containerHeight || containerHeight === 0) {
console.warn('容器高度为 0延迟重试');
setTimeout(() => initChart(), 50);
return;
}
//
if (chartInstance) {
@ -54,6 +80,10 @@ const initChart = () => {
chartInstance = echarts.init(chartRef.value);
// mode
const data = mode.value === 'top' ? baseData : performanceData;
const isHorizontal = mode.value === 'top';
const option = {
tooltip: {
trigger: 'axis',
@ -80,16 +110,16 @@ const initChart = () => {
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '50',
left: isHorizontal ? '3%' : '10%',
right: isHorizontal ? '4%' : '4%',
bottom: isHorizontal ? '3%' : '10%',
top: isHorizontal ? '50' : '60',
containLabel: true
},
dataZoom: [
{
type: 'inside',
xAxisIndex: 0,
[isHorizontal ? 'yAxisIndex' : 'xAxisIndex']: 0,
filterMode: 'empty',
zoomOnMouseWheel: true,
moveOnMouseMove: false,
@ -97,10 +127,10 @@ const initChart = () => {
start: 0,
end: 100,
minValueSpan: 0,
maxValueSpan: 20
maxValueSpan: isHorizontal ? 20 : 5
}
],
xAxis: {
xAxis: isHorizontal ? {
type: 'value',
min: 80,
max: 100,
@ -115,11 +145,9 @@ const initChart = () => {
color: '#666',
formatter: '{value}'
}
},
yAxis: {
} : {
type: 'category',
data: categoryData,
inverse: true,
data: data.categories,
axisLabel: {
color: '#666',
fontSize: 12,
@ -143,36 +171,83 @@ const initChart = () => {
show: false
}
},
yAxis: isHorizontal ? {
type: 'category',
data: data.categories,
inverse: true,
axisLabel: {
color: '#666',
fontSize: 12,
interval: 0,
rotate: 45,
margin: 10
},
axisLine: {
show: true,
lineStyle: {
color: '#666'
}
},
axisTick: {
show: true,
lineStyle: {
color: '#666'
}
},
splitLine: {
show: false
}
} : {
type: 'value',
min: 0,
max: 100,
splitLine: {
show: true,
lineStyle: {
color: '#E8E8E8',
type: 'solid'
}
},
axisLabel: {
color: '#666',
formatter: '{value}'
}
},
series: [
{
name: '当前',
type: 'bar',
data: currentData,
data: data.currentData,
itemStyle: {
color: '#5470C6'
},
barWidth: 12,
barWidth: isHorizontal ? 6 : 10,
barGap: '30%'
},
{
name: '去年同期',
type: 'bar',
data: lastYearData,
data: data.lastYearData,
itemStyle: {
color: '#91CC75'
},
barWidth: 12,
barWidth: isHorizontal ? 6 : 10,
barGap: '30%'
}
]
};
chartInstance.setOption(option);
//
setTimeout(() => {
chartInstance?.resize();
}, 0);
};
//
onMounted(() => {
initChart();
loadData();
//
window.addEventListener('resize', () => {
@ -182,16 +257,12 @@ onMounted(() => {
// mode
watch(mode, () => {
// mode
initChart();
loadData();
});
</script>
<style lang="scss" scoped>
.ant-radio-group {
// border: 3px solid #2f6b98 !important;
// border-radius: 10px !important;
.ant-radio-button-wrapper-checked {
border: 1px solid #2f6b98 !important;
background-color: #2f6b98 !important;

View File

@ -1,5 +1,37 @@
<script setup lang="ts">
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
import RightDrawer from "@/components/RightDrawer/index.vue";
import XFFS from "@/modules/shengTaiLiuLiangXieFangSheShiMod/xieFangFangShi/index.vue" //
import XFFB from "@/modules/shengTaiLiuLiangXieFangSheShiMod/xieFangFenBu/index.vue" //
</script>
<template>
<div>
<h2>生态流量泄放设施</h2>
<div class="all_width">
<div class="moduleContent">
<div class="leftContent">
<JidiSelectorMod />
</div>
<div class="rightContent">
<RightDrawer>
<XFFS></XFFS>
<XFFB></XFFB>
</RightDrawer>
</div>
</div>
</div>
</template>
<style lang="scss">
.all_width{
width: 100%;
.moduleContent{
display: flex;
justify-content: space-between;
.rightContent{
height: 88vh;
width: 450px;
}
}
}
</style>