386 lines
12 KiB
Vue
386 lines
12 KiB
Vue
<!-- 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> |