فهرست منبع

添加土壤酸化数据统计图

yes-yes-yes-k 4 روز پیش
والد
کامیت
6262cba8fa

+ 2 - 1
components.d.ts

@@ -74,7 +74,8 @@ declare module 'vue' {
     Irrigationstatistics: typeof import('./src/components/detectionStatistics/irrigationstatistics.vue')['default']
     Irrwatermap: typeof import('./src/components/irrpollution/irrwatermap.vue')['default']
     PaginationComponent: typeof import('./src/components/PaginationComponent.vue')['default']
-    ReducedataStatistics: typeof import('./src/components/detectionStatistics/reducedataStatistics.vue')['default']
+    ReducedataStatistics: typeof import('./src/components/cdStatictics/reducedataStatistics.vue')['default']
+    RefluxcdStatictics: typeof import('./src/components/cdStatictics/refluxcdStatictics.vue')['default']
     Riverwaterassay: typeof import('./src/components/irrpollution/riverwaterassay.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 2 - 3
src/App.vue

@@ -1,8 +1,7 @@
 <script setup lang='ts'>
 import { RouterView } from "vue-router"
-import request from './utils/request';
-import ReducedataStatistics from "./components/detectionStatistics/reducedataStatistics.vue";
-
+import ReducedataStatistics from "./components/cdStatictics/reducedataStatistics.vue";
+import RefluxcdStatictics from "./components/cdStatictics/refluxcdStatictics.vue";
 </script>
 
 <template>

+ 234 - 0
src/components/cdStatictics/reducedataStatistics.vue

@@ -0,0 +1,234 @@
+<template>
+  <div class="chart-container">
+
+    <div v-if="loading" class="loading">
+      <p>数据加载中...</p>
+    </div>
+    <div ref="chartRef" class="chart"></div>
+  </div>
+</template>
+
+<script>
+import { ref, onMounted, onUnmounted, watch ,nextTick} from 'vue';
+import * as echarts from 'echarts';
+import axios from 'axios';
+
+export default {
+  name: 'HeavyMetalChart',
+  setup() {
+    // 接口数据
+    const interfaces = [
+      { time: '20241229_201018', url: 'http://localhost:5000/api/table-averages?table_name=dataset_5' },
+      { time: '20250104_171827', url: 'http://localhost:5000/api/table-averages?table_name=dataset_35' },
+      { time: '20250104_171959', url: 'http://localhost:5000/api/table-averages?table_name=dataset_36' },
+      { time: '20250104_214026', url: 'http://localhost:5000/api/table-averages?table_name=dataset_37' },
+      { time: '20250308_161945', url: 'http://localhost:5000/api/table-averages?table_name=dataset_65' },
+      { time: '20250308_163248', url: 'http://localhost:5000/api/table-averages?table_name=dataset_66' },
+    ];
+    
+    const chartRef = ref(null);
+    const loading = ref(true);
+    const error = ref(null);
+    const selectedData = ref('Al');
+    const displayOption = ref('all');
+    const lastUpdate = ref(new Date().toLocaleString());
+    
+    const chartData = ref({
+      timestamps: [],
+      series: {
+        Al: [],
+        CL: [],
+        H: [],
+        OM: [],
+        Q_over_b: [],
+        pH: []
+      }
+    });
+    
+    let chartInstance = null;
+    
+    // 获取数据
+    async function fetchData() {
+      try {
+        loading.value = true;
+        error.value = null;
+        
+        // 尝试获取真实数据,失败则使用模拟数据
+        try {
+          const responses = await Promise.all(
+            interfaces.map(intf => 
+              fetch(intf.url)
+                .then(response => {
+                  if (!response.ok) throw new Error(`网络响应错误: ${response.status}`);
+                  return response.json();
+                })
+                .then(data => {
+                  if (data.success !== "true" && data.success !== true) {
+                    throw new Error('接口返回失败状态');
+                  }
+                  return { time: intf.time, data: data.averages };
+                })
+            )
+          );
+          
+          // 处理数据 - 直接使用原始时间字符串
+          chartData.value.timestamps = responses.map(item => item.time);
+          
+          // 初始化所有数据系列
+          Object.keys(chartData.value.series).forEach(key => {
+            chartData.value.series[key] = responses.map(item => parseFloat(item.data[key]));
+          });
+        } catch (err) {
+          console.error('获取真实数据失败,使用模拟数据:', err);
+          error.value = '获取真实数据失败: ' + err.message;
+          chartData.value = generateMockData();
+        }
+        
+        renderChart();
+        lastUpdate.value = new Date().toLocaleString();
+      } catch (err) {
+        error.value = err.message || '获取数据失败';
+        console.error('获取数据错误:', err);
+      } finally {
+        loading.value = false;
+      }
+    }
+    
+    // 渲染图表
+    function renderChart() {
+      if (!chartRef.value) return;
+      
+      // 初始化或更新ECharts实例
+      if (!chartInstance) {
+        chartInstance = echarts.init(chartRef.value);
+      }
+      
+      // 准备系列数据
+      const series = [];
+      
+      if (displayOption.value === 'all') {
+        // 显示所有数据
+        Object.keys(chartData.value.series).forEach(key => {
+          series.push({
+            name: key,
+            type: 'line',
+            symbol: 'circle',
+            symbolSize: 6,
+            data: chartData.value.series[key],
+            lineStyle: { width: 2 },
+            emphasis: {
+              focus: 'series'
+            }
+          });
+        });
+      } else {
+   
+      }
+      
+      const option = {
+        title: {
+          text: displayOption.value === 'all' ? '重金属数据趋势图' : `${selectedData.value} 数据趋势`,
+          left: 'center',
+          top: 10
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'cross',
+            label: {
+              backgroundColor: '#6a7985'
+            }
+          }
+        },
+        legend: {
+          data: displayOption.value === 'all' ? Object.keys(chartData.value.series) : [selectedData.value],
+          top: 40,
+          type: 'scroll'
+        },
+        grid: {
+          left: '15%',
+          right: '4%',
+          bottom: '3%',
+          top: '80px',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          boundaryGap: false,
+          data: chartData.value.timestamps, // 直接使用原始时间字符串
+          axisLabel:{rotate:30}
+        },
+        yAxis: {
+          type: 'value',
+          scale: true,
+          
+        },
+        series: series
+      };
+      
+      chartInstance.setOption(option);
+      chartInstance.resize();
+    }
+    
+    // 刷新数据
+    function refreshData() {
+      fetchData();
+    }
+    
+onMounted(() => {
+  fetchData().then(() => {
+    // 等待 Vue 完成 DOM 更新(如父元素布局、样式生效)
+    nextTick(() => {
+      if (chartInstance) {
+        chartInstance.resize(); // 确保图表适配最终尺寸
+      }
+    });
+  });
+
+  // 保留原有窗口 resize 监听
+  window.addEventListener('resize', () => {
+    if (chartInstance) {
+      chartInstance.resize();
+    }
+  });
+});
+    
+    onUnmounted(() => {
+      if (chartInstance) {
+        chartInstance.dispose();
+      }
+    });
+    
+    // 监听显示选项变化
+    watch([selectedData, displayOption], () => {
+      renderChart();
+    });
+    
+    return {
+      chartRef,
+      loading,
+      error,
+      chartData,
+      selectedData,
+      displayOption,
+      lastUpdate,
+      refreshData
+    };
+  }
+}
+</script>
+
+<style scoped>
+.chart-container {
+  width: 100%;
+  padding: 20px;
+  height: 470px;
+  box-sizing: border-box;
+}
+
+.chart {
+  width: 100%;
+  height: 100%;
+  margin-bottom: 20px;
+}
+</style>

+ 259 - 0
src/components/cdStatictics/refluxcdStatictics.vue

@@ -0,0 +1,259 @@
+<template>
+  <div class="container">    
+    <div class="chart-container">
+      <div ref="chartRef" class="chart-wrapper"></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, nextTick } from 'vue' 
+import * as echarts from 'echarts' 
+
+const indicators = [
+  { key: 'Al3_plus', name: 'Al3_plus', color: '#3498db' },
+  { key: 'CEC', name: 'CEC', color: '#2ecc71' },
+  { key: 'CL', name: 'CL', color: '#e74c3c' },
+  { key: 'Delta_pH', name: 'Delta_pH', color: '#f39c12' },
+  { key: 'H_plus', name: 'H_plus', color: '#9b59b6' },
+  { key: 'N', name: 'N', color: '#1abc9c' },
+  { key: 'OM', name: 'OM', color: '#d35400' }
+];
+
+let chart = ref(null) // 存储 ECharts 实例
+let chartData = ref([]) // 存储图表数据
+const chartRef = ref(null) // 图表容器
+const statsByIndex = ref([]);//存储每个指标的统计量
+const isLoading = ref(true);
+const error = ref(false);
+const errorMessage = ref('');
+
+function initChart() {
+  // 确保图表容器已渲染
+  if (!chartRef.value) return;
+  
+ 
+  //console.log('图表容器宽高:', chartRef.value.clientWidth, chartRef.value.clientHeight);
+  
+  // 初始化 ECharts 实例
+  chart.value = echarts.init(chartRef.value);
+  
+
+  const option = {
+    tooltip: {
+      trigger: 'item',
+      axisPointer: { type: 'shadow' },
+      formatter: function(params) {
+        const stat = statsByIndex.value[params.dataIndex];
+        if (!stat || stat.min === null) {
+          return `<div style="font-weight:bold;margin-bottom:8px;color:#2c3e50;">${params.name}</div>
+                  <div>无有效数据</div>`;
+        }  
+        return `
+          <div style="font-weight:bold;margin-bottom:8px;color:#2c3e50;">${stat.name}</div>
+          <div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;">
+            <span style="color:#7f8c8d;">最小值:</span> <span style="text-align:right;font-weight:bold;">${stat.min}</span>
+            <span style="color:#7f8c8d;">下四分位:</span> <span style="text-align:right;font-weight:bold;color:#e74c3c;">${stat.q1}</span>
+            <span style="color:#7f8c8d;">中位数:</span> <span style="text-align:right;font-weight:bold;color:#3498db;">${stat.median}</span>
+            <span style="color:#7f8c8d;">上四分位:</span> <span style="text-align:right;font-weight:bold;color:#e74c3c;">${stat.q3}</span>
+            <span style="color:#7f8c8d;">最大值:</span> <span style="text-align:right;font-weight:bold;">${stat.max}</span>
+          </div>
+        `;
+      }
+    },
+    title: {
+      text: '酸化加剧指标箱线图展示',
+      left: 'center',
+      textStyle: {
+        fontSize: 18,
+        fontWeight: 'normal'
+      },
+      top: 10
+    },
+    grid: {
+      left: '0px',
+      right: '0px',
+      bottom: '0px',
+      top: '70px',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: indicators.map(i => i.name),
+      axisLabel: {
+        rotate: 30,
+        fontSize: 12,
+        margin: 15
+      },
+      axisTick: {
+        alignWithLabel: true
+      },
+    },
+    yAxis: {
+      type: 'value',
+      name: '数值',
+      nameTextStyle: {
+        fontSize: 14
+      },
+      nameGap: 25,
+      splitLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#ddd'
+        }
+      }
+    },
+    series: [{
+      name: '指标分布',
+      type: 'boxplot',
+      itemStyle: {
+        color: function(params) {
+          return indicators[params.dataIndex].color;
+        },
+        borderWidth: 2
+      },
+      emphasis: {
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: 'rgba(0, 0, 0, 0.3)'
+        }
+      }
+    }]
+  };
+  
+  chart.value.setOption(option);
+}
+
+
+function calculatePercentile(sortedArray, percentile) {
+  const n = sortedArray.length;
+  if (n === 0) return null;
+  if (percentile <= 0) return sortedArray[0];
+  if (percentile >= 100) return sortedArray[n - 1];
+  
+  const index = (n - 1) * (percentile / 100);
+  const lowerIndex = Math.floor(index);
+  const upperIndex = lowerIndex + 1;
+  const fraction = index - lowerIndex;
+  
+  if (upperIndex >= n) return sortedArray[lowerIndex];
+  return sortedArray[lowerIndex] + fraction * (sortedArray[upperIndex] - sortedArray[lowerIndex]);
+}
+
+
+function calculateBoxplotStats(data, indicators) {
+  const boxplotData = [];
+  const statsArray = [];
+  
+  indicators.forEach(indicator => {
+    const values = data
+      .map(item => Number(item[indicator.key]))
+      .filter(val => !isNaN(val))
+      .sort((a, b) => a - b);
+    
+    if (values.length === 0) {
+      boxplotData.push([null, null, null, null, null]);
+      statsArray.push({
+        min: null, q1: null, median: null, q3: null, max: null,
+        name: indicator.name,
+        color: indicator.color
+      });
+    } else {
+      const min = Math.min(...values);
+      const max = Math.max(...values);
+      const q1 = calculatePercentile(values, 25);
+      const median = calculatePercentile(values, 50);
+      const q3 = calculatePercentile(values, 75);
+      boxplotData.push([min, q1, median, q3, max]);
+      
+      statsArray.push({
+        min, q1, median, q3, max,
+        name: indicator.name,
+        color: indicator.color
+      });
+    }
+  });
+  
+  return { boxplotData, statsArray };
+}
+
+
+async function loadData() {
+  isLoading.value = true;
+  error.value = false;
+  
+  try {
+    const response = await fetch('http://localhost:5000/api/table-data?table_name=dataset_60');
+    const result = await response.json();
+    chartData.value = result.data;
+
+    const { boxplotData, statsArray } = calculateBoxplotStats(chartData.value, indicators);
+    statsByIndex.value = statsArray;
+    
+    chart.value.setOption({
+      series: [{
+        data: boxplotData
+      }]
+    });
+    
+  } catch (err) {
+    error.value = true;
+    errorMessage.value = `加载失败: ${err.message || '网络错误'}`;
+    console.error('数据加载失败:', err);
+  } finally {
+    isLoading.value = false;
+  }
+}
+
+const handleResize = () => {
+  if (chart.value) {
+    chart.value.resize();
+  }
+};
+
+onMounted(() => {
+  nextTick(() => {
+    initChart();
+    loadData();
+  });
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  if (chart.value) {
+    chart.value.dispose(); // 销毁 ECharts 实例
+    chart.value = null;
+  }
+  window.removeEventListener('resize', handleResize);
+});
+</script>
+
+<style scoped>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+.container {
+  width: 100%;
+  height: 100%;
+  margin: 0 auto;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+}
+.chart-container {
+    width: 100%;
+    height: 100%;
+  padding: 20px;
+  position: relative;
+}
+.chart-wrapper {
+  width: 100%;
+  height: 100%; 
+  min-height: 200px;
+}
+</style>

+ 9 - 9
src/components/detectionStatistics/atmsampleStatistics.vue

@@ -82,10 +82,10 @@ export default {
     const calculateBoxplotStats = () => {
       const stats = []
       heavyMetals.forEach((metal) => {
-        log(`开始处理 ${metal.name}`, metal.name)
+        //log(`开始处理 ${metal.name}`, metal.name)
         // 1. 提取原始值
         const rawValues = sampleData.value.map(item => item[metal.key])
-        log(`原始值:[${rawValues.slice(0, 5)}${rawValues.length > 5 ? ', ...' : ''}]`, metal.name)
+        //log(`原始值:[${rawValues.slice(0, 5)}${rawValues.length > 5 ? ', ...' : ''}]`, metal.name)
 
         // 2. 过滤无效值(NaN、非数字)
         const values = rawValues
@@ -98,7 +98,7 @@ export default {
             return num
           })
           .filter(v => v !== null)
-        log(`有效数据量:${values.length} 条`, metal.name)
+        //log(`有效数据量:${values.length} 条`, metal.name)
 
         // 3. 无有效数据时,记录空统计
         if (values.length === 0) {
@@ -114,7 +114,7 @@ export default {
         const median = calculatePercentile(sorted, 50)
         const q3 = calculatePercentile(sorted, 75)
 
-        log(`统计结果:min=${min}, q1=${q1}, median=${median}, q3=${q3}, max=${max}`, metal.name)
+        //log(`统计结果:min=${min}, q1=${q1}, median=${median}, q3=${q3}, max=${max}`, metal.name)
         stats.push({ ...metal, min, q1, median, q3, max })
       })
       return stats
@@ -136,7 +136,7 @@ export default {
 
     // -------- 图表初始化 --------
     const initChart = () => {
-      log('开始初始化图表')
+      //log('开始初始化图表')
       const stats = calculateBoxplotStats()
       const { xAxisData, data } = buildBoxplotData(stats)
 
@@ -191,15 +191,15 @@ export default {
         grid: { top: '8%', right: '5%', left: '12%', bottom: '20%' }
       }
       isLoading.value = false
-      log('图表初始化完成')
+      //log('图表初始化完成')
     }
 
     // -------- 接口请求 --------
    onMounted(async () => {
   try {
-    log('发起API请求...')
+    //log('发起API请求...')
     const response = await axios.get(apiUrl.value)
-    console.log('接口原始响应:', response.data) // 调试必看!
+    //console.log('接口原始响应:', response.data) // 调试必看!
 
     let data = response.data
 
@@ -235,7 +235,7 @@ export default {
     if (sampleData.value.length === 0) {
       throw new Error('接口返回数据为空(properties 为空)')
     }
-    log(`成功提取 ${sampleData.value.length} 条数据`, '接口')
+    //log(`成功提取 ${sampleData.value.length} 条数据`, '接口')
 
     // 3. 初始化图表
     initChart()

+ 2 - 2
src/components/detectionStatistics/irrigationstatistics.vue

@@ -200,7 +200,7 @@ export default {
     // -------- 拉取接口并绘图 --------
     onMounted(async () => {
       try {
-        log('发起API请求...')
+        //log('发起API请求...')
         const response = await axios.get(apiUrl.value)
         apiTimestamp.value = new Date().toLocaleString()
 
@@ -208,7 +208,7 @@ export default {
           // 你的接口:GeoJSON -> features[].properties
           sampleData.value = response.data.features.map(f => f.properties)
           sampleCount.value = sampleData.value.length
-          log(`接口返回数据量: ${sampleCount.value} 条`, '接口')
+          //log(`接口返回数据量: ${sampleCount.value} 条`, '接口')
           initChart()
         } else {
           throw new Error('接口返回数据格式不正确(无features字段)')

+ 6 - 0
src/components/layout/menuItems.ts

@@ -358,6 +358,12 @@ import {
       label: 'PlantingRiskStatistics.Title',//<!--i18n:PlantingRiskStatistics.Title-->作物风险评估统计
       icon: List,
       tab: 'dataStatistics'
+    },
+    {
+      index: '/SoilacidificationStatistics',
+      label: 'SoilacidificationStatistics.Title',//<!--i18n:PlantingRiskStatistics.Title-->酸化预测数据统计
+      icon: List,
+      tab: 'dataStatistics'
     }
   ].filter(({ tab: menuTab }) => !["shuJuKanBan", "mapView", "introduction"].includes(menuTab));
 

+ 3 - 0
src/locales/en.json

@@ -211,5 +211,8 @@
   },
   "PlantingRiskStatistics": {
     "Title": "Crop risk assessment statistics"
+  },
+  "SoilacidificationStatistics": {
+    "Title": "Soil acidification data statistics"
   }
 }

+ 3 - 0
src/locales/zh.json

@@ -212,5 +212,8 @@
   },
   "PlantingRiskStatistics": {
     "Title": "作物风险评估统计"
+  },
+  "SoilacidificationStatistics": {
+    "Title": "土壤酸化数据统计"
   }
 }

+ 7 - 0
src/router/index.ts

@@ -401,6 +401,13 @@ const routes = [
           import("@/views/User/dataStatistics/PlantingRiskStatistics.vue"), // 修复路径
         meta: { title: "作物风险评估系统" },
       },
+      {
+        path: "SoilacidificationStatistics",
+        name: "SoilacidificationStatistics",
+        component: () =>
+          import("@/views/User/dataStatistics/SoilacidificationStatistics.vue"), // 修复路径
+        meta: { title: "作物风险评估系统" },
+      },
       {
         path: "Visualizatio",
         name: "Visualizatio",

+ 70 - 0
src/views/User/dataStatistics/SoilacidificationStatistics.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="layout-container">
+    <!-- 第一列:酸化缓解数据组件 -->
+    <div class="column column-1"> 
+      <div class="component-container">
+        <ReducedataStatistics />
+      </div>
+    </div>
+
+    <!-- 第二列:酸化加剧数据组件 -->
+    <div class="column column-2">
+      <div class="component-container">
+        <RefluxcdStatictics />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import ReducedataStatistics from '@/components/cdStatictics/reducedataStatistics.vue';
+import RefluxcdStatictics from '@/components/cdStatictics/refluxcdStatictics.vue';
+
+export default {
+  components: {
+    ReducedataStatistics,
+    RefluxcdStatictics,
+  }
+}
+</script>
+
+<style scoped>
+/* 核心:网格布局统一控制尺寸 */
+.layout-container {
+  display: grid;
+  grid-template-columns: 1fr 1fr;  /* 两列等宽 */
+  height: 400px;
+  gap: 16px;                      /* 列/行间距 */
+  padding: 16px;                  /* 内边距 */
+  box-sizing: border-box;
+  width: 100%;
+  min-height: 100vh;
+}
+
+/* 统一子组件外层容器:确保填充网格单元格 */
+.component-container {
+  width: 500px;
+  height: 500px;
+  padding: 16px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+  box-sizing: border-box;
+}
+
+/* 区分两列背景色(保留原有视觉差异) */
+.column-1 .component-container {
+  background: #f9fafc;
+}
+.column-2 .component-container {
+  background: #f3f4f6;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .layout-container {
+    grid-template-columns: 1fr; /* 小屏幕下单列布局 */
+  }
+}
+</style>