482 lines
11 KiB
Vue
482 lines
11 KiB
Vue
<template>
|
||
<SidePanelItem title="泄放方式分布情况">
|
||
<div ref="chartRef" class="bar-chart-container"></div>
|
||
|
||
<!-- 空状态提示 -->
|
||
<a-empty v-if="showEmpty" description="暂无数据" />
|
||
</SidePanelItem>
|
||
|
||
<!-- 详情弹框 -->
|
||
<a-modal
|
||
v-model:open="modalVisible"
|
||
title="泄放方式分布情况"
|
||
width="1536px"
|
||
:footer="null"
|
||
@cancel="handleModalCancel"
|
||
>
|
||
<FBQKTwolayer
|
||
v-if="modalVisible"
|
||
:selectData="selectData"
|
||
:jidiName="jidiName"
|
||
:jiDiList="JidiSelectEventStore.jidiData"
|
||
></FBQKTwolayer>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import {
|
||
ref,
|
||
onMounted,
|
||
onBeforeUnmount,
|
||
watch,
|
||
computed,
|
||
nextTick
|
||
} from 'vue';
|
||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||
import * as echarts from 'echarts';
|
||
import type { ECharts } from 'echarts';
|
||
import { vmsstbprptGetKendoList } from '@/api/stll';
|
||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||
import FBQKTwolayer from './TwoLayer/FenBuQingKuangTwoLayer.vue';
|
||
import { useDraggable } from '@/utils/drag';
|
||
|
||
// 定义组件名
|
||
defineOptions({
|
||
name: 'XieFangFenBu'
|
||
});
|
||
|
||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||
|
||
// DOM 引用
|
||
const chartRef = ref<HTMLElement | null>(null);
|
||
let chartInstance: ECharts | null = null;
|
||
|
||
// 响应式数据
|
||
const loading = ref(false);
|
||
const data = ref<any[]>([]);
|
||
const echartsXData = ref<string[]>([]);
|
||
const wbsCode = ref('');
|
||
|
||
// 弹框状态
|
||
const modalVisible = ref(false);
|
||
const selectData = ref(''); // 选中的设施类型名称
|
||
const jidiName = ref(''); // 选中的基地名称
|
||
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
|
||
// 设施类型配置
|
||
const typeName = [
|
||
{ label: '生态放流孔', eqtp: 'EQ_1' },
|
||
{ label: '生态放流闸', eqtp: 'EQ_2' },
|
||
{ label: '生态放流洞', eqtp: 'EQ_3' },
|
||
{ label: '生态放流管', eqtp: 'EQ_4' },
|
||
{ label: '基荷发电', eqtp: 'EQ_5' },
|
||
{ label: '生态机组', eqtp: 'EQ_6' },
|
||
{ label: '泄洪设施', eqtp: 'EQ_7' }
|
||
];
|
||
|
||
// 默认配色方案(当 getUnitConfigByCode 不可用时使用)
|
||
const defaultColors = [
|
||
'#9556a4', // EQ_1 - 紫色
|
||
'#30b7b9', // EQ_2 - 青绿色
|
||
'#4b79ab', // EQ_3 - 蓝色
|
||
'#dbb629', // EQ_4 - 黄色
|
||
'#df91ab', // EQ_5 - 粉色
|
||
'#78c300', // EQ_6 - 浅绿色
|
||
'#7399c6' // EQ_7 - 浅蓝色
|
||
];
|
||
|
||
// 计算是否显示空状态
|
||
const showEmpty = computed(() => {
|
||
return !loading.value && (!data.value || data.value.length === 0);
|
||
});
|
||
|
||
/**
|
||
* 手写 omit 函数(替代 lodash.omit)
|
||
*/
|
||
const omit = (obj: any, keys: string[]) => {
|
||
const result = { ...obj };
|
||
keys.forEach(key => {
|
||
delete result[key];
|
||
});
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* 获取颜色配置(模拟 getUnitConfigByCode)
|
||
* TODO: 如果项目中有等效实现,请替换此处逻辑
|
||
*/
|
||
const getColorByType = (type: string): string => {
|
||
// 这里可以替换为实际的 getUnitConfigByCode 调用
|
||
// const config = getUnitConfigByCode('Other', type);
|
||
// return config?.lineColor || config?.pieColor;
|
||
|
||
// 暂时使用默认配色
|
||
const index = typeName.findIndex(item => item.eqtp === type);
|
||
return defaultColors[index] || '#999999';
|
||
};
|
||
|
||
/**
|
||
* 初始化图表
|
||
*/
|
||
const initChart = () => {
|
||
if (!chartRef.value) return;
|
||
|
||
// 确保容器有正确的高度
|
||
if (chartRef.value.clientHeight === 0) {
|
||
setTimeout(() => {
|
||
initChart();
|
||
}, 100);
|
||
return;
|
||
}
|
||
|
||
if (!chartInstance) {
|
||
chartInstance = echarts.init(chartRef.value);
|
||
}
|
||
|
||
const option = buildChartOption();
|
||
chartInstance.setOption(option, true);
|
||
|
||
// 绑定点击事件
|
||
chartInstance.off('click');
|
||
chartInstance.on('click', handleChartClick);
|
||
};
|
||
|
||
/**
|
||
* 构建图表配置
|
||
*/
|
||
const buildChartOption = () => {
|
||
// 计算每个基地的总数
|
||
const countSums: Record<string, number> = {};
|
||
data.value.forEach(item => {
|
||
const baseid = item.baseid;
|
||
const count = Number(item.count) || 0;
|
||
if (countSums[baseid]) {
|
||
countSums[baseid] += count;
|
||
} else {
|
||
countSums[baseid] = count;
|
||
}
|
||
});
|
||
|
||
const values = Object.values(countSums);
|
||
const maxValue = values.length > 0 ? Math.max(...values) : 0;
|
||
|
||
// 计算总计数据
|
||
const totalData: number[] = [];
|
||
const uniqueBaseNames = [...new Set(echartsXData.value)];
|
||
uniqueBaseNames.forEach((basename, index) => {
|
||
let total = 0;
|
||
data.value.forEach(item => {
|
||
if (item.basename === basename) {
|
||
total += Number(item.count) || 0;
|
||
}
|
||
});
|
||
totalData[index] = total;
|
||
});
|
||
|
||
// 构建 series 数据
|
||
const seriesData = typeName.map(type => {
|
||
return uniqueBaseNames.map(basename => {
|
||
const found = data.value.find(
|
||
item => item.eqtp === type.eqtp && item.basename === basename
|
||
);
|
||
return found ? Number(found.count) || 0 : 0;
|
||
});
|
||
});
|
||
|
||
// 获取颜色数组
|
||
const colors = typeName.map(type => getColorByType(type.eqtp));
|
||
|
||
const option: any = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
confine: true,
|
||
formatter: (params: any) => {
|
||
const content = params
|
||
.map((item: any) => {
|
||
let value = item.value || 0;
|
||
if (item.seriesName === '总计') {
|
||
value = totalData[item.dataIndex];
|
||
}
|
||
return `<div>${item.marker}${item.seriesName}: <span style="float:right">${value} 个</span></div>`;
|
||
})
|
||
.join('');
|
||
return `<h3>${params[0].name}</h3>${content}`;
|
||
},
|
||
axisPointer: {
|
||
type: 'line'
|
||
}
|
||
},
|
||
grid: {
|
||
left: '8%',
|
||
right: '4%',
|
||
bottom: '20%',
|
||
top: '20%'
|
||
},
|
||
legend: {
|
||
type: 'scroll',
|
||
icon: 'rect',
|
||
orient: 'horizontal',
|
||
itemWidth: 12,
|
||
itemHeight: 12,
|
||
textStyle: {
|
||
fontSize: 12,
|
||
color: '#6A93B9',
|
||
height: 8
|
||
},
|
||
data: typeName.map(item => item.label)
|
||
},
|
||
xAxis: {
|
||
show: true,
|
||
type: 'category',
|
||
axisLine: { show: true },
|
||
axisLabel: {
|
||
fontSize: 12,
|
||
color: '#686868',
|
||
rotate: 37
|
||
},
|
||
axisTick: {
|
||
alignWithLabel: true
|
||
},
|
||
data: uniqueBaseNames
|
||
},
|
||
yAxis: {
|
||
max: maxValue,
|
||
type: 'value',
|
||
barWidth: 10,
|
||
name: '数量(个)',
|
||
nameTextStyle: {
|
||
fontSize: 12,
|
||
color: '#848484',
|
||
align: 'center'
|
||
},
|
||
axisTick: { show: false },
|
||
axisLabel: {
|
||
fontSize: 12,
|
||
color: '#343434',
|
||
fontFamily: 'Bebas'
|
||
}
|
||
},
|
||
series: [
|
||
...typeName.map((type, index) => ({
|
||
name: type.label,
|
||
type: 'bar' as const,
|
||
stack: 'total',
|
||
barCategoryGap: '50%',
|
||
data: seriesData[index]
|
||
})),
|
||
{
|
||
name: '总计',
|
||
type: 'bar' as const,
|
||
stack: 'total',
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
formatter: (params: any) => {
|
||
return String(totalData[params.dataIndex]);
|
||
}
|
||
},
|
||
itemStyle: {
|
||
color: 'transparent'
|
||
},
|
||
data: Array(totalData.length).fill(0)
|
||
}
|
||
],
|
||
color: colors
|
||
};
|
||
|
||
return option;
|
||
};
|
||
|
||
/**
|
||
* 图表点击事件
|
||
*/
|
||
const handleChartClick = (params: any) => {
|
||
// 根据系列名称找到对应的 sttpCode (eqtp)
|
||
const typeConfig = typeName.find(item => item.label === params.seriesName);
|
||
selectData.value = typeConfig ? typeConfig.eqtp : '';
|
||
|
||
// 将基地名称转换为 wbsCode
|
||
const jidiInfo = JidiSelectEventStore.jidiData.find(
|
||
(item: any) => item.wbsName === params.name
|
||
);
|
||
jidiName.value = jidiInfo ? jidiInfo.wbsCode : params.name;
|
||
|
||
modalVisible.value = true;
|
||
};
|
||
|
||
/**
|
||
* 弹框取消事件
|
||
*/
|
||
const handleModalCancel = () => {
|
||
modalVisible.value = false;
|
||
selectData.value = '';
|
||
jidiName.value = '';
|
||
};
|
||
|
||
/**
|
||
* 获取数据
|
||
*/
|
||
const fetchData = async () => {
|
||
if (!wbsCode.value) return;
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
// 构建请求参数
|
||
let params: any = {
|
||
filter: {
|
||
logic: 'and',
|
||
filters: [
|
||
wbsCode.value !== 'all'
|
||
? {
|
||
field: 'baseId',
|
||
operator: 'eq',
|
||
dataType: 'string',
|
||
value: wbsCode.value
|
||
}
|
||
: null,
|
||
{
|
||
field: 'sttpFullPath',
|
||
operator: 'contains',
|
||
dataType: 'string',
|
||
value: 'ENV,ENVP,EQ,'
|
||
}
|
||
].filter(Boolean)
|
||
},
|
||
group: [
|
||
{
|
||
dir: 'asc',
|
||
field: 'baseStepSort'
|
||
},
|
||
{
|
||
dir: 'asc',
|
||
field: 'baseId'
|
||
},
|
||
{
|
||
dir: 'desc',
|
||
field: 'sttpCode'
|
||
}
|
||
],
|
||
groupResultFlat: 'true'
|
||
};
|
||
|
||
// 如果是全部基地,移除 baseId 过滤条件
|
||
if (wbsCode.value === 'all') {
|
||
params = omit(params, ['filter']);
|
||
params.filter = {
|
||
logic: 'and',
|
||
filters: [
|
||
{
|
||
field: 'sttpFullPath',
|
||
operator: 'contains',
|
||
dataType: 'string',
|
||
value: 'ENV,ENVP,EQ,'
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
const res = await vmsstbprptGetKendoList(params);
|
||
|
||
if (res.data && res.data.data) {
|
||
const apiData = res.data.data;
|
||
|
||
// 处理数据
|
||
const list: any[] = [];
|
||
apiData.forEach((it: any) => {
|
||
list.push({
|
||
key: it.baseId,
|
||
count: it.count_sttpCode,
|
||
eqtp: it.sttpCode
|
||
});
|
||
});
|
||
|
||
// 映射基地名称:使用 JidiSelectEventStore.jidiData
|
||
const processedData = list.map((item: any) => {
|
||
// 从 jidiData 中查找匹配的基地名称
|
||
const jidiInfo = JidiSelectEventStore.jidiData.find(
|
||
(jidi: any) => jidi.wbsCode === item.key
|
||
);
|
||
|
||
return {
|
||
baseid: item.key,
|
||
basename: jidiInfo ? jidiInfo.wbsName : item.key, // 如果找到则使用 wbsName,否则使用 baseId
|
||
count: item.count,
|
||
eqtp: item.eqtp
|
||
};
|
||
});
|
||
|
||
data.value = processedData;
|
||
//
|
||
// 提取唯一的基地名称作为 X 轴数据
|
||
echartsXData.value = [
|
||
...new Set(processedData.map(item => item.basename))
|
||
];
|
||
|
||
// 数据更新后重新渲染图表
|
||
await nextTick();
|
||
initChart();
|
||
} else {
|
||
data.value = [];
|
||
echartsXData.value = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('获取泄放方式分布数据失败:', error);
|
||
data.value = [];
|
||
echartsXData.value = [];
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理窗口大小变化
|
||
*/
|
||
const handleResize = () => {
|
||
chartInstance?.resize();
|
||
};
|
||
|
||
// 生命周期钩子
|
||
onMounted(() => {
|
||
// 延迟初始化图表,确保 DOM 已渲染
|
||
setTimeout(() => {
|
||
initChart();
|
||
setTimeout(() => {
|
||
chartInstance?.resize();
|
||
}, 200);
|
||
}, 50);
|
||
|
||
window.addEventListener('resize', handleResize);
|
||
|
||
// 组件挂载时获取数据
|
||
if (wbsCode.value) {
|
||
fetchData();
|
||
}
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('resize', handleResize);
|
||
chartInstance?.dispose();
|
||
chartInstance = null;
|
||
});
|
||
|
||
// 监听基地选择变化
|
||
watch(
|
||
() => JidiSelectEventStore.selectedItem,
|
||
newVal => {
|
||
console.log('流域切换:', newVal);
|
||
if (newVal?.wbsCode) {
|
||
wbsCode.value = newVal.wbsCode;
|
||
fetchData();
|
||
}
|
||
},
|
||
{ deep: true, immediate: true }
|
||
);
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.bar-chart-container {
|
||
width: 100%;
|
||
height: 350px;
|
||
}
|
||
</style>
|