123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- <template>
- <!-- 柱状图容器 -->
- <div class="chart-page">
- <div ref="chartContainer" class="chart-container"></div>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
- import * as echarts from 'echarts'
- // 状态管理
- const chartContainer = ref(null)
- let chart = null
- const state = reactive({
- excelData: [], // 存储解析后的断面数据
- districtAvgData: [] // 存储按区县分组后的平均数据
- })
- // 从接口初始化数据(核心修改:异步获取 + 严格数据解析)
- const initData = async () => {
- try {
- // 接口地址(注意修正空格:localhost)
- const apiUrl = 'http://localhost:8000/api/vector/export/all?table_name=cross_section'
- const response = await fetch(apiUrl)
-
- if (!response.ok) {
- throw new Error(`接口请求失败(状态码:${response.status})`)
- }
-
- const geoJson = await response.json()
-
- // 逐行解析Feature,确保每个样本都被处理
- state.excelData = geoJson.features.map(feature => {
- const props = feature.properties || {}
-
- // 强制转换Cd浓度为数值(处理异常值)
- let cdValue = parseFloat(props.cd_concentration)
- if (isNaN(cdValue)) {
- console.warn('发现无效Cd浓度值,已设为0:', props)
- cdValue = 0 // 保证参与计算,避免数据缺失
- }
-
- return {
- id: props.id || '未知ID', // 兜底处理
- river: props.river_name || '未知河流',
- district: props.county || '未知区县',
- cdValue: cdValue
- }
- })
-
- calculateDistrictAvg() // 计算区县平均值
- } catch (err) {
- console.error('数据加载失败:', err)
- // 可扩展:全局错误提示
- }
- }
- // 按区县计算平均浓度(核心逻辑保持不变)
- const calculateDistrictAvg = () => {
- const districtGroups = {};
-
- // 1. 分组统计(总和 + 数量)
- state.excelData.forEach(item => {
- const district = item.district;
- if (!districtGroups[district]) {
- districtGroups[district] = { total: 0, count: 0 };
- }
- districtGroups[district].total += item.cdValue;
- districtGroups[district].count += 1;
- });
- // 2. 计算平均值 + 总平均
- const districtAvg = [];
- let totalAll = 0;
- let countAll = 0;
- for (const district in districtGroups) {
- const avg = districtGroups[district].total / districtGroups[district].count;
- districtAvg.push({
- district,
- avg: parseFloat(avg) // 保留3位小数
- });
- totalAll += districtGroups[district].total;
- countAll += districtGroups[district].count;
- }
- // 添加总平均值(最后一项)
- districtAvg.push({
- district: '总平均',
- avg: parseFloat((totalAll / countAll))
- });
- state.districtAvgData = districtAvg;
- updateChart(); // 更新图表
- }
- // 初始化图表
- const initChart = () => {
- if (chartContainer.value) {
- chart = echarts.init(chartContainer.value);
- updateChart();
- }
- }
- // 更新图表数据
- const updateChart = () => {
- if (!chart || state.districtAvgData.length === 0) return;
- // 准备图表数据
- const districts = state.districtAvgData.map(item => item.district); // x轴:区县名
- const avgs = state.districtAvgData.map(item => item.avg); // y轴:平均浓度
- // 图表配置
- const option = {
- tooltip: {
- trigger: 'axis',
- formatter: '{b}: {c} μg/L' // 悬停提示:区县名 + 浓度值
- },
- grid: {
- left: '5%',
- right: '5%',
- bottom: '15%', // 底部留空间,防止区县名重叠
- containLabel: true
- },
- xAxis: {
- type: 'category',
- data: districts,
- axisLabel: {
- interval: 0,
- fontSize: 15
- }
- },
- yAxis: {
- type: 'value',
- name: 'Cd浓度 (μg/L)',
- min: 0, // 从0开始更直观
- nameTextStyle:{
- fontSize:15,
- },
- axisLabel: {
- formatter: '{value}',
- fontSize: 15
- }
- },
- series: [
- {
- name: '平均浓度',
- type: 'bar',
- data: avgs,
- itemStyle: {
- // 为"总平均"设置不同颜色(最后一项)
- color: (params) => params.dataIndex === districts.length - 1 ? '#FF4500' : '#1E88E5'
- },
- label: {
- show: true, // 显示数值标签
- position: 'top',
- formatter: '{c}',
- fontSize: 15
- },
- barWidth: '60%'
- }
- ],
- graphic: [
- {
- type: 'rect',
- left: '5%', // 与 grid.left 对齐
- right: '5%', // 与 grid.right 对齐
- bottom: '5%',// 与 grid.bottom 对齐(位于绘图区域底部)
- height: 30, // 圆弧高度
- // 顶部左右圆角(半径 15,与高度 30 配合形成上凸圆弧)
- borderRadius: [15, 15, 0, 0],
- fill: '#FFFFFF', // 白色填充
- z: -1 // 层级低于图表元素(如柱子、坐标轴)
- }
- ]
- };
- chart.setOption(option);
- }
- // 生命周期管理
- onMounted(async () => {
- await initData(); // 先加载接口数据
- initChart(); // 再初始化图表
- // 监听窗口 resize,自动调整图表大小
- window.addEventListener('resize', () => chart && chart.resize());
- })
- onBeforeUnmount(() => {
- // 组件销毁时释放图表资源
- if (chart) chart.dispose();
- })
- </script>
- <style>
- .chart-page {
- width: 100%;
- margin: 0 auto 24px;
- background-color: white;
- border-radius: 12px;
- padding: 20px;
- box-sizing: border-box;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- .chart-container {
- width: 100%;
- height: 500px;
-
- }
- </style>
|