Browse Source

数据统计页面,检测数据和土壤Cd统计页面完善绘制

yes-yes-yes-k 3 months ago
parent
commit
c30e91aac3

+ 3 - 5
.eslintrc-auto-import.json

@@ -13,6 +13,8 @@
     "MaybeRefOrGetter": true,
     "PropType": true,
     "Ref": true,
+    "Slot": true,
+    "Slots": true,
     "VNode": true,
     "WritableComputedRef": true,
     "computed": true,
@@ -69,10 +71,6 @@
     "watch": true,
     "watchEffect": true,
     "watchPostEffect": true,
-    "watchSyncEffect": true,
-    "ElMessage": true,
-    "ElM": true,
-    "Slot": true,
-    "Slots": true
+    "watchSyncEffect": true
   }
 }

+ 1 - 1
auto-imports.d.ts

@@ -68,6 +68,6 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

+ 9 - 0
components.d.ts

@@ -2,6 +2,7 @@
 // @ts-nocheck
 // Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
+// biome-ignore lint: disable
 export {}
 
 /* prettier-ignore */
@@ -16,13 +17,18 @@ declare module 'vue' {
     AppLayout: typeof import('./src/components/layout/AppLayout.vue')['default']
     Atmcompanyline: typeof import('./src/components/atmpollution/atmcompanyline.vue')['default']
     Atmcompanymap: typeof import('./src/components/atmpollution/atmcompanymap.vue')['default']
+    AtmcompanyStatics: typeof import('./src/components/detectionStatistics/atmcompanyStatics.vue')['default']
     AtmCompanytencentMap: typeof import('./src/components/atmpollution/atmCompanytencentMap.vue')['default']
     Atmsamplemap: typeof import('./src/components/atmpollution/atmsamplemap.vue')['default']
+    AtmsampleStatistics: typeof import('./src/components/detectionStatistics/atmsampleStatistics.vue')['default']
+    CropcdStatictics: typeof import('./src/components/soilcdStatistics/cropcdStatictics.vue')['default']
+    CrosscetionStatistics: typeof import('./src/components/detectionStatistics/crosscetionStatistics.vue')['default']
     Crosssectionmap: typeof import('./src/components/irrpollution/crosssectionmap.vue')['default']
     CrossSectionSamplelineData: typeof import('./src/components/irrpollution/crossSectionSamplelineData.vue')['default']
     CrossSetionData1: typeof import('./src/components/irrpollution/crossSetionData1.vue')['default']
     CrossSetionData2: typeof import('./src/components/irrpollution/crossSetionData2.vue')['default']
     CrossSetionTencentmap: typeof import('./src/components/irrpollution/crossSetionTencentmap.vue')['default']
+    EffcdStatistics: typeof import('./src/components/soilcdStatistics/effcdStatistics.vue')['default']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
@@ -57,6 +63,7 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    FluxcdStatictics: typeof import('./src/components/soilcdStatistics/fluxcdStatictics.vue')['default']
     HeavyMetalEnterprisechart: typeof import('./src/components/atmpollution/heavyMetalEnterprisechart.vue')['default']
     HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
     IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
@@ -64,8 +71,10 @@ declare module 'vue' {
     IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
     IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
     IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
+    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']
     Riverwaterassay: typeof import('./src/components/irrpollution/riverwaterassay.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 2 - 2
myenv/Lib/site-packages/pip/_vendor/pygments/formatters/img.py

@@ -19,13 +19,13 @@ import subprocess
 
 # Import this carefully
 try:
-    from PIL import Image, ImageDraw, ImageFont
+    from PIL import Image, ImageDraw, ImageFont # pyright: ignore[reportMissingImports]
     pil_available = True
 except ImportError:
     pil_available = False
 
 try:
-    import _winreg
+    import _winreg # pyright: ignore[reportMissingImports]
 except ImportError:
     try:
         import winreg as _winreg

+ 6 - 6
myenv/Lib/site-packages/setuptools/_distutils/command/bdist_rpm.py

@@ -7,17 +7,17 @@ import subprocess
 import sys
 import os
 
-from distutils.core import Command
-from distutils.debug import DEBUG
-from distutils.file_util import write_file
-from distutils.errors import (
+from distutils.core import Command # type: ignore
+from distutils.debug import DEBUG # type: ignore
+from distutils.file_util import write_file # type: ignore
+from distutils.errors import ( # type: ignore
     DistutilsOptionError,
     DistutilsPlatformError,
     DistutilsFileError,
     DistutilsExecError,
 )
-from distutils.sysconfig import get_python_version
-from distutils import log
+from distutils.sysconfig import get_python_version # type: ignore
+from distutils import log # type: ignore
 
 
 class bdist_rpm(Command):

File diff suppressed because it is too large
+ 260 - 246
package-lock.json


+ 2 - 2
package.json

@@ -19,12 +19,12 @@
     "@vue-leaflet/vue-leaflet": "^0.10.1",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
-    "axios": "^1.7.9",
+    "axios": "^1.11.0",
     "chart.js": "^4.4.9",
     "coordtransform": "^2.1.2",
     "d3": "^7.9.0",
     "dom-to-image": "^2.6.0",
-    "echarts": "^5.6.0",
+    "echarts": "^5.5.1",
     "echarts-gl": "^2.0.9",
     "element-plus": "^2.9.3",
     "file-saver": "^2.0.5",

+ 3 - 1
src/App.vue

@@ -1,6 +1,7 @@
 <script setup lang='ts'>
 import { RouterView } from "vue-router"
 import request from './utils/request';
+import ReducedataStatistics from "./components/detectionStatistics/reducedataStatistics.vue";
 
 </script>
 
@@ -8,4 +9,5 @@ import request from './utils/request';
   <RouterView/>
 </template>
 
-<style scoped></style>
+<style scoped>
+</style>

+ 410 - 0
src/components/detectionStatistics/atmcompanyStatics.vue

@@ -0,0 +1,410 @@
+<template>
+  <div class="dashboard">
+    <div class="chart-container">
+
+      <div class="chart-header">
+        <div class="title-group">
+             <div class="chart-title">大气污染企业各地区颗粒物排放量统计</div>
+             <p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
+        </div>
+       
+        <div class="method-selector">
+          <button 
+            class="method-btn" 
+            :class="{active: currentMethod === 'max'}"
+            @click="currentMethod = 'max'"
+          >
+            最大值
+          </button>
+          <button 
+            class="method-btn" 
+            :class="{active: currentMethod === 'avg'}"
+            @click="currentMethod = 'avg'"
+          >
+            平均值
+          </button>
+        </div>
+      </div>
+
+      <div class="chart-wrapper">
+        <div v-if="loading" class="loading">
+          <div class="spinner"></div>
+          <div class="loading-text">数据加载中...</div>
+        </div>
+        <div v-else-if="error" class="error">
+          {{ error }}
+        </div>
+        <div v-else-if="!processedData.length" class="no-data">
+          无有效数据可展示
+        </div>
+        <div v-else ref="mainChart" style="height:100%;width:100%"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      error: null,
+      processedData: [],
+      currentMethod: 'max',
+      apiEndpoint: 'http://localhost:8000/api/vector/export/all?table_name=atmo_company',
+      totalPoints: 0,
+    };
+  },
+  mounted() {
+    this.fetchData();
+  },
+  watch: {
+    currentMethod() {
+      if (this.processedData.length) {
+        this.renderChart();
+      }
+    }
+  },
+  methods: {
+    async fetchData() {
+      try {
+        const response = await fetch(this.apiEndpoint);
+        if (!response.ok) {
+          throw new Error(`网络请求失败: ${response.status}`);
+        }
+        
+        // 处理含NaN的非法JSON:先转为文本替换NaN为null
+        const rawText = await response.text();
+        const cleanedText = rawText.replace(/\bNaN\b/g, 'null'); // 关键修复
+        const data = JSON.parse(cleanedText);
+        
+        if (data && data.features && data.features.length > 0) {
+          this.processData(data.features);
+        } else {
+          this.loading = false;
+          this.error = "接口返回空数据";
+        }
+        
+      } catch (err) {
+        this.loading = false;
+        this.error = `数据加载失败: ${err.message}`;
+        console.error("API请求错误:", err);
+      }
+    },
+    
+    processData(features) {
+      try {
+        const countyMap = new Map();
+        
+        features.forEach(feature => {
+          const props = feature.properties || {};
+          let county = props.county || '';
+          
+          // 标准化地区名称
+          county = this.standardizeCountyName(county);
+          
+          if (county) {
+            // 提取并校验排放数据
+            const rawEmission = props.particulate_emission;
+            const emissionValue = parseFloat(rawEmission);
+            
+            // 过滤无效值(NaN、非数字等)
+            if (isNaN(emissionValue)) {
+              return;
+            }
+            
+            // 统计有效数据
+            if (!countyMap.has(county)) {
+              countyMap.set(county, {
+                county,
+                values: [emissionValue],
+                maxEmission: emissionValue,
+                sumEmission: emissionValue,
+                pointCount: 1
+              });
+            } else {
+              const countyData = countyMap.get(county);
+              countyData.values.push(emissionValue);
+              countyData.pointCount++;
+              countyData.sumEmission += emissionValue;
+              
+              if (emissionValue > countyData.maxEmission) {
+                countyData.maxEmission = emissionValue;
+              }
+            }
+          }
+        });
+        
+        // 计算平均值
+        countyMap.forEach(data => {
+          data.avgEmission = data.sumEmission / data.values.length;
+        });
+        
+        // 转换为数组并排序
+        this.processedData = Array.from(countyMap.values())
+          .sort((a, b) => b.maxEmission - a.maxEmission);
+        
+        // 计算有效样本总数
+        this.totalPoints = this.processedData.reduce(
+          (sum, item) => sum + item.pointCount,
+          0
+        );
+
+        this.loading = false;
+        this.$nextTick(() => {
+          this.renderChart();
+        });
+        
+      } catch (err) {
+        this.loading = false;
+        this.error = `数据处理错误: ${err.message}`;
+        console.error("数据处理异常:", err);
+      }
+    },
+    
+    // 标准化地区名称
+    standardizeCountyName(county) {
+      if (!county) return '';
+      
+      // 特殊处理曲江地区
+      if (county === '曲江' || county === '曲江新' || county === '曲江开发区') {
+        return '曲江区';
+      }
+      
+      return county;
+    },
+    
+    renderChart() {
+      const chart = echarts.init(this.$refs.mainChart);
+      
+      // 根据选择的方法确定数据
+      const method = this.currentMethod;
+      const valueField = method === 'max' ? 'maxEmission' : 'avgEmission';
+      const methodLabel = method === 'max' ? '最高值' : '平均值';
+      
+      const data = this.processedData.map(item => ({
+        name: item.county,
+        value: item[valueField],
+        pointCount: item.pointCount,
+        maxEmission: item.maxEmission,
+        avgEmission: item.avgEmission
+      }));
+      
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          formatter: params => {
+            const d = params[0].data;
+            return `
+              <div style="margin-bottom:5px;font-weight:bold">${d.name}</div>
+              <div>最高排放量: ${d.maxEmission} t/a</div>
+              <div>平均排放量: ${d.avgEmission} t/a</div>
+              <div style="margin-top:5px">监测点数量: ${d.pointCount}个</div>
+            `;
+          }
+        },
+        grid: {
+          left: 0,
+          right: "15%",
+          top: 20,
+          bottom: 10,
+          containLabel: true
+        },
+        xAxis: {
+          type: 'value',
+          name: `颗粒物排放量`,
+          nameLocation: 'end',
+          nameTextStyle:{
+            padding:-30,
+            fontSize:12
+          },
+          axisLabel: {
+            formatter: value => value + ' t/a',
+            fontSize:11,//字体大小
+            rotate: 30
+          },
+          splitLine: {
+            lineStyle: {
+              type: 'dashed'
+            }
+          }
+        },
+        yAxis: {
+          type: 'category',
+          data: data.map(d => d.name),
+          axisLabel: {
+            formatter: value => value,
+            fontSize:11,//字体大小
+          }
+        },
+        series: [{
+          name: '颗粒物排放量',
+          type: 'bar',
+          data: data,
+          label: {
+            show: true,
+            position: 'right',
+            formatter: params => params.value + ' t/a',
+            fontSize:10,//字体大小
+            rotate: 15
+          },
+          barWidth: '60%',
+          itemStyle: {
+            color: '#3498db'
+          }
+        }]
+      };
+      
+      chart.setOption(option);
+      
+      // 响应窗口大小变化
+      window.addEventListener('resize', chart.resize);
+    }
+  }
+}
+</script>
+
+<style>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+body {
+  background: #f8fafc;
+  color: #34495e;
+  min-height: 100vh;
+  padding: 20px;
+}
+
+.dashboard {
+  height: 350px;
+  width: 100%;
+  max-width: 1000px;
+  margin: 0 auto;
+}
+
+header {
+  text-align: center;
+  padding: 20px 0;
+  margin-bottom: 20px;
+}
+
+h1 {
+  font-size: 24px;
+  color: #2980b9;
+  margin-bottom: 8px;
+}
+
+.subtitle {
+  color: #7f8c8d;
+  font-size: 14px;
+}
+
+.chart-container {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+  padding: 15px;
+  height: 100%;
+  display: flex;
+  flex-direction:column;
+}
+
+.chart-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 10px;
+}
+
+.chart-title {
+  font-size: 16px;
+  color: #2980b9;
+  font-weight: 600;
+}
+
+.title-group {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+}
+
+.method-selector {
+  background: #f1f8ff;
+  border-radius: 8px;
+  padding: 4px 8px;
+  display: flex;
+  gap: 6px;
+}
+
+.method-btn {
+  padding: 3px 8px;
+  border-radius: 6px;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  font-size: 12px;
+  transition: all 0.3s;
+}
+
+.method-btn.active {
+  background: #3498db;
+  color: white;
+}
+
+.chart-wrapper {
+  flex: 1;
+  min-height: 0;
+  height: 400px;
+}
+
+.loading, .no-data {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  width: 100%;
+}
+
+.spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid rgba(52, 152, 219, 0.2);
+  border-radius: 50%;
+  border-top-color: #3498db;
+  animation: spin 1s linear infinite;
+  margin-bottom: 10px;
+}
+
+.loading-text {
+  font-size: 14px;
+  color: #3498db;
+}
+
+.no-data {
+  height: 300px;
+  font-size: 18px;
+  font-weight: bold;
+  color: #dc3232;
+  background-color: #fce4e4;
+  border-radius: 8px;
+  text-align: center;
+  padding: 10px;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.sample-subtitle {
+    color: #7f8c8d;
+    font-size: 12px;
+    margin-top: 2px;
+}
+</style>

+ 323 - 0
src/components/detectionStatistics/atmsampleStatistics.vue

@@ -0,0 +1,323 @@
+<template>
+  <div class="boxplot-container">
+    <div class="chart-container">
+      <div class="header">
+        <div class="chart-title">大气重金属浓度统计箱线图</div>
+        <p>展示各重金属浓度的分布特征(最小值、四分位数、中位数、最大值)</p>
+        <p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
+      </div>
+
+      <div v-if="isLoading" class="loading-state">
+        <span class="spinner"></span> 数据加载中...
+      </div>
+
+      <div v-else-if="error" class="error-state">
+        ❌ 加载失败:{{ error.message }}
+      </div>
+
+      <div v-else>
+        <div class="chart-wrapper">
+          <v-chart :option="chartOption" autoresize />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import VChart from 'vue-echarts'
+import axios from 'axios'
+import { ref, onMounted ,computed} from 'vue'
+
+export default {
+  components: { VChart },
+  setup() {
+    // -------- 核心配置 --------
+    const apiUrl = ref('http://localhost:8000/api/vector/export/all?table_name=Atmo_sample_data')
+    const heavyMetals = [
+      { key: 'Cr_particulate', name: '铬 (Cr)', color: '#FF9800' },
+      { key: 'As_particulate', name: '砷 (As)', color: '#4CAF50' },
+      { key: 'Cd_particulate', name: '镉 (Cd)', color: '#9C27B0' },
+      { key: 'Hg_particulate', name: '汞 (Hg)', color: '#2196F3' },
+      { key: 'Pb_particulate', name: '铅 (Pb)', color: '#F44336' },
+     // { key: 'particle_weight', name:'大气污染物重量' }
+    ]
+
+    // -------- 状态 --------
+    const sampleData = ref([])    // 存储 properties 数据
+    const chartOption = ref({})   // ECharts 配置
+    const isLoading = ref(true)   // 加载状态
+    const error = ref(null)       // 错误信息
+    const statsByIndex = ref([])  // 缓存统计结果(与 x 轴对齐)
+    const totalPoints = computed(() => sampleData.value.length)
+
+
+    // -------- 工具函数 --------
+    /** 日志工具(带颜色区分) */
+    const log = (message, metalName = '') => {
+      console.log(
+        `%c[${metalName || '全局'}] %c${message}`,
+        'color:#4CAF50;font-weight:bold',
+        'color:#333'
+      )
+    }
+
+    /** 计算百分位数(线性插值法) */
+    const 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])
+    }
+
+    // -------- 数据统计 --------
+    /** 计算每个重金属的箱线图统计量(min/q1/median/q3/max) */
+    const calculateBoxplotStats = () => {
+      const stats = []
+      heavyMetals.forEach((metal) => {
+        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)
+
+        // 2. 过滤无效值(NaN、非数字)
+        const values = rawValues
+          .map((val, idx) => {
+            const num = Number(val)
+            if (isNaN(num)) {
+              log(`⚠️ 第${idx+1}条数据无效: ${val}`, metal.name)
+              return null
+            }
+            return num
+          })
+          .filter(v => v !== null)
+        log(`有效数据量:${values.length} 条`, metal.name)
+
+        // 3. 无有效数据时,记录空统计
+        if (values.length === 0) {
+          stats.push({ ...metal, min: null, q1: null, median: null, q3: null, max: null })
+          return
+        }
+
+        // 4. 排序并计算统计量
+        const sorted = [...values].sort((a, b) => a - b)
+        const min = sorted[0]
+        const max = sorted[sorted.length - 1]
+        const q1 = calculatePercentile(sorted, 25)
+        const median = calculatePercentile(sorted, 50)
+        const q3 = calculatePercentile(sorted, 75)
+
+        log(`统计结果:min=${min}, q1=${q1}, median=${median}, q3=${q3}, max=${max}`, metal.name)
+        stats.push({ ...metal, min, q1, median, q3, max })
+      })
+      return stats
+    }
+
+    /** 构建 ECharts 箱线图数据 */
+    const buildBoxplotData = (stats) => {
+      const xAxisData = heavyMetals.map(m => m.name)
+      // 与 x 轴顺序对齐(确保 tooltip 能正确匹配)
+      statsByIndex.value = heavyMetals.map(m => 
+        stats.find(s => s.key === m.key) || { ...m, min: null, q1: null, median: null, q3: null, max: null }
+      )
+      // 生成箱线图数据:[min, q1, median, q3, max]
+      const data = statsByIndex.value.map(s => 
+        s.min === null ? [null, null, null, null, null] : [s.min, s.q1, s.median, s.q3, s.max]
+      )
+      return { xAxisData, data }
+    }
+
+    // -------- 图表初始化 --------
+    const initChart = () => {
+      log('开始初始化图表')
+      const stats = calculateBoxplotStats()
+      const { xAxisData, data } = buildBoxplotData(stats)
+
+      // ECharts 配置(重点检查 series 数据格式)
+      chartOption.value = {
+        tooltip: {
+          trigger: 'item',
+          formatter: (params) => {
+            const s = statsByIndex.value[params.dataIndex]
+            if (!s || s.min === null) {
+              return `<div style="font-weight:bold;color:#f56c6c">${xAxisData[params.dataIndex]}</div><div>无有效数据</div>`
+            }
+            
+            return `<div style="font-weight:bold">${xAxisData[params.dataIndex]}</div>
+              <div style="margin-top:8px">
+                <div>最小值:<span style="color:#5a5;">${s.min.toFixed(4)}</span></div>
+                <div>下四分位:<span style="color:#d87a80;">${s.q1.toFixed(4)}</span></div>
+                <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${s.median.toFixed(4)}</span></div>
+                <div>上四分位:<span style="color:#d87a80;">${s.q3.toFixed(4)}</span></div>
+                <div>最大值:<span style="color:#5a5;">${s.max.toFixed(4)}</span></div>
+              </div>`
+          }
+        },
+        xAxis: {
+          type: 'category',
+          data: xAxisData,
+          name: '重金属类型',
+          nameLocation: 'middle',
+          nameGap: 45,
+          axisLabel: { color: '#555', rotate: 30, fontWeight: 'bold',fontSize:11 }
+        },
+        yAxis: {
+          type: 'value',
+          name: 'mg/kg',
+          nameLocation: 'end',
+          nameGap: 5,
+          axisLabel: { color: '#555', fontWeight: 'bold' ,fontSize:11},
+          splitLine: { lineStyle: { color: '#f0f0f0' } }
+        },
+        series: [{
+          name: '重金属浓度分布',
+          type: 'boxplot',
+          data, // 必须是 [[min,q1,median,q3,max], ...] 格式
+          itemStyle: {
+            color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
+            borderWidth: 2
+          },
+          emphasis: {
+            itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.2)', borderWidth: 3 }
+          }
+        }],
+        grid: { top: '8%', right: '5%', left: '12%', bottom: '20%' }
+      }
+      isLoading.value = false
+      log('图表初始化完成')
+    }
+
+    // -------- 接口请求 --------
+   onMounted(async () => {
+  try {
+    log('发起API请求...')
+    const response = await axios.get(apiUrl.value)
+    console.log('接口原始响应:', response.data) // 调试必看!
+
+    let data = response.data
+
+    // ✅ 兼容接口返回字符串的情况(比如后端没设置 application/json)
+    if (typeof data === 'string') {
+      try {
+        // 兼容 NaN(非标准 JSON 值)→ 替换为 null
+        data = JSON.parse(data.replace(/\bNaN\b/g, 'null'))
+      } catch (parseErr) {
+        throw new Error('接口返回的是字符串,但 JSON 解析失败')
+      }
+    }
+
+    // 1. 分情况提取 features(严格校验结构)
+    let features = []
+    if (data?.type === 'FeatureCollection') {
+      // 情况1:标准 GeoJSON FeatureCollection
+      if (Array.isArray(data.features)) {
+        features = data.features
+      } else {
+        throw new Error('FeatureCollection 中 features 不是数组')
+      }
+    } else if (Array.isArray(data)) {
+      // 情况2:直接返回 features 数组
+      features = data
+    } else {
+      // 情况3:其他非法结构
+      throw new Error(`接口结构异常,响应:${JSON.stringify(data)}`)
+    }
+
+    // 2. 提取 properties 数据(确保非空)
+    sampleData.value = features.map(f => f.properties)
+    if (sampleData.value.length === 0) {
+      throw new Error('接口返回数据为空(properties 为空)')
+    }
+    log(`成功提取 ${sampleData.value.length} 条数据`, '接口')
+
+    // 3. 初始化图表
+    initChart()
+
+  } catch (err) {
+    error.value = err
+    isLoading.value = false
+    console.error('接口请求失败:', err)
+  }
+})
+
+
+    return {
+      chartOption,
+      isLoading,
+      error,
+      totalPoints
+    }
+  }
+}
+</script>
+
+<style scoped>
+.boxplot-container {
+  width: 100%;
+  height: 300px;
+  max-width: 1000px;
+  margin: 0 auto;
+}
+.header { 
+  text-align: left;
+  margin-bottom: 20px; 
+}
+.chart-title {
+  font-size: 14px;
+  color: #2980b9;
+  font-weight: 600;
+}
+.header p { 
+  font-size: 0.6rem; 
+  color: #666;
+  margin: 0; 
+}
+.loading-state { 
+  text-align: center;
+  padding: 40px 0;
+  color: #666; 
+}
+.loading-state .spinner {
+  display: inline-block; 
+  width: 24px; 
+  height: 24px; 
+  margin-right: 8px;
+  border: 3px solid #ccc; 
+  border-top-color: #1890ff; 
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin { 
+  to { transform: rotate(360deg); } 
+}
+.error-state { 
+  text-align: center; 
+  padding: 40px 0; 
+  color: #f56c6c; 
+}
+.chart-wrapper { 
+  width: 100%; 
+  height: 220px; /* 确保高度有效 */
+}
+.chart-container {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+  padding: 20px;
+  margin-bottom: 25px;
+}
+
+.sample-subtitle {
+  font-size: 0.85rem;
+  color: #888;
+  margin-top: 4px;
+}
+
+</style>

+ 398 - 0
src/components/detectionStatistics/crosscetionStatistics.vue

@@ -0,0 +1,398 @@
+<template>
+  <div class="dashboard">
+    <div class="chart-container">
+
+      <div class="chart-header">
+        <div class="title-group">
+             <div class="chart-title">断面采样点各地区镉浓度统计</div>
+             <p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
+        </div>
+       
+        <div class="method-selector">
+          <button 
+            class="method-btn" 
+            :class="{active: currentMethod === 'max'}"
+            @click="currentMethod = 'max'"
+          >
+            最大值
+          </button>
+          <button 
+            class="method-btn" 
+            :class="{active: currentMethod === 'avg'}"
+            @click="currentMethod = 'avg'"
+          >
+            平均值
+          </button>
+        </div>
+      </div>
+
+      <div class="chart-wrapper">
+        <div v-if="loading" class="loading">
+          <div class="spinner"></div>
+          <div class="loading-text">数据加载中...</div>
+        </div>
+        <div v-else-if="error" class="error">
+          {{ error }}
+        </div>
+        <div v-else-if="!processedData.length" class="no-data">
+          无有效数据可展示
+        </div>
+        <div v-else ref="mainChart" style="height:100%;width:100%"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      error: null,
+      processedData: [],
+      currentMethod: 'max',
+      apiEndpoint: 'http://localhost:8000/api/vector/export/all?table_name=cross_section',
+      totalPoints: 0,
+      chartInstance: null // 新增:存储图表实例,避免内存泄漏
+    };
+  },
+  mounted() {
+    this.fetchData();
+  },
+  watch: {
+    currentMethod() {
+      if (this.processedData.length) {
+        this.renderChart();
+      }
+    }
+  },
+  beforeDestroy() {
+    // 销毁图表实例
+    if (this.chartInstance) {
+      this.chartInstance.dispose();
+    }
+  },
+  methods: {
+    async fetchData() {
+      try {
+        const response = await fetch(this.apiEndpoint);
+        if (!response.ok) {
+          throw new Error(`网络请求失败: ${response.status}`);
+        }
+        
+        const data = await response.json();
+        
+        if (data && data.features && data.features.length > 0) {
+          this.processData(data.features);
+        } else {
+          this.loading = false;
+          this.error = "接口返回空数据";
+        }
+        
+      } catch (err) {
+        this.loading = false;
+        this.error = `数据加载失败: ${err.message}`;
+        console.error("API请求错误:", err);
+      }
+    },
+    
+    processData(features) {
+      try {
+        const countyMap = new Map();
+        
+        features.forEach(feature => {
+          const props = feature.properties || {};
+          let county = props.county || '';
+          
+          county = this.standardizeCountyName(county);
+          
+          if (county) {
+            const cdValue = parseFloat(props.cd_concentration) || 0;
+            
+            if (!countyMap.has(county)) {
+              countyMap.set(county, {
+                county,
+                values: [cdValue],
+                maxCd: cdValue,
+                sumCd: cdValue,
+                pointCount: 1
+              });
+            } else {
+              const countyData = countyMap.get(county);
+              countyData.values.push(cdValue);
+              countyData.pointCount++;
+              countyData.sumCd += cdValue;
+              
+              if (cdValue > countyData.maxCd) {
+                countyData.maxCd = cdValue;
+              }
+            }
+          }
+        });
+        
+        countyMap.forEach(data => {
+          data.avgCd = data.sumCd / data.values.length;
+        });
+        
+        this.processedData = Array.from(countyMap.values())
+          .sort((a, b) => b.maxCd - a.maxCd);
+        
+        this.totalPoints = this.processedData.reduce(
+            (sum, item) => sum + item.pointCount,
+            0
+        );
+
+        this.loading = false;
+        this.$nextTick(() => {
+            this.renderChart();
+        });
+        
+      } catch (err) {
+        this.loading = false;
+        this.error = `数据处理错误: ${err.message}`;
+        console.error("数据处理异常:", err);
+      }
+    },
+    
+    standardizeCountyName(county) {
+      if (!county) return '';
+      
+      if (county === '曲江' || county === '曲江新' || county === '曲江开发区') {
+        return '曲江区';
+      }
+      
+      return county;
+    },
+    
+    renderChart() {
+      // 销毁已有实例
+      if (this.chartInstance) {
+        this.chartInstance.dispose();
+      }
+      
+      this.chartInstance = echarts.init(this.$refs.mainChart);
+      
+      const method = this.currentMethod;
+      const valueField = method === 'max' ? 'maxCd' : 'avgCd';
+      const methodLabel = method === 'max' ? '最高值' : '平均值';
+      
+      const data = this.processedData.map(item => ({
+        name: item.county,
+        value: item[valueField],
+        pointCount: item.pointCount,
+        maxCd: item.maxCd,
+        avgCd: item.avgCd
+      }));
+      
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          formatter: params => {
+            const d = params[0].data;
+            return `
+              <div style="margin-bottom:3px;font-weight:bold;font-size:12px">${d.name}</div>
+              <div style="font-size:11px">最高浓度: ${d.maxCd} mg/L</div>
+              <div style="font-size:11px">平均浓度: ${d.avgCd} mg/L</div>
+              <div style="margin-top:3px;font-size:11px">监测点数量: ${d.pointCount}个</div>
+            `;
+          }
+        },
+        // 缩小图表内边距,让内容更紧凑
+        grid: {
+          left: 0,
+          right: '15%',
+          top: 20, // 缩减顶部边距
+          bottom: 10, // 缩减底部边距
+          containLabel: true
+        },
+        xAxis: {
+          type: 'value',
+          name: `镉浓度`,
+          nameLocation: 'end',
+          nameTextStyle: {
+            fontSize: 10 ,// 缩小坐标轴名称字体
+            padding:-10,
+          },
+          axisLabel: {
+            formatter: value => value + ' mg/L',
+            fontSize: 10 ,// 缩小坐标轴标签字体
+          },
+          splitLine: {
+            lineStyle: {
+              type: 'dashed'
+            }
+          }
+        },
+        yAxis: {
+          type: 'category',
+          data: data.map(d => d.name),
+          axisLabel: {
+            formatter: value => value,
+            fontSize: 10 // 缩小坐标轴标签字体
+          }
+        },
+        series: [{
+          name: '镉浓度',
+          type: 'bar',
+          data: data,
+          label: {
+            show: true,
+            position: 'right',
+            formatter: params => params.value + ' mg/L',
+            fontSize: 9, // 缩小数据标签字体
+            rotate:15
+          },
+          barWidth: '60%',
+          itemStyle: {
+            color: '#3498db'
+          }
+        }]
+      };
+      
+      this.chartInstance.setOption(option);
+      
+      // 响应窗口大小变化
+      const resizeHandler = () => {
+        this.chartInstance.resize();
+      };
+      
+      window.addEventListener('resize', resizeHandler);
+    }
+  }
+}
+</script>
+
+<style>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+body {
+  background: #f8fafc;
+  color: #3498db;
+  min-height: 100vh;
+  padding: 20px;
+}
+
+/* 固定组件总高度为350px */
+.dashboard {
+  width: 100%;
+  height: 300px;
+  max-width: 1000px;
+  margin: 0 auto;
+}
+
+/* 让图表容器占满总高度,使用flex布局分配空间 */
+.chart-container {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+  padding: 10px 15px; /* 缩减内边距 */
+  height: 100%;
+  display: flex;
+  flex-direction: column; /* 垂直排列子元素 */
+}
+
+/* 缩减头部高度占用 */
+.chart-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 6px; /* 减少底部间距 */
+}
+
+/* 缩小标题字体 */
+.chart-title {
+  font-size: 14px;
+  color: #2980b9;
+  font-weight: 600;
+}
+
+.title-group {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+/* 缩小选择器按钮区域 */
+.method-selector {
+  background: #f1f8ff;
+  border-radius: 8px;
+  padding: 3px 8px; /* 缩减内边距 */
+  display: flex;
+  gap: 6px; /* 减少按钮间距 */
+}
+
+.method-btn {
+  padding: 3px 8px; /* 缩小按钮尺寸 */
+  border-radius: 6px;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  font-size: 12px; /* 缩小按钮字体 */
+  transition: all 0.3s;
+}
+
+.method-btn.active {
+  background: #3498db;
+  color: white;
+}
+
+/* 让图表容器占据剩余空间 */
+.chart-wrapper {
+  flex: 1; /* 占据剩余高度 */
+}
+
+.loading, .no-data {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  width: 100%;
+}
+
+/* 缩小加载动画 */
+.spinner {
+  width: 35px;
+  height: 35px;
+  border: 3px solid rgba(52, 152, 219, 0.2);
+  border-radius: 50%;
+  border-top-color: #3498db;
+  animation: spin 1s linear infinite;
+  margin-bottom: 8px;
+}
+
+/* 缩小加载文本 */
+.loading-text {
+  font-size: 13px;
+  color: #3498db;
+}
+
+/* 调整无数据提示样式 */
+.no-data {
+  font-size: 16px;
+  font-weight: bold;
+  color: #dc3232;
+  background-color: #fce4e4;
+  border-radius: 8px;
+  text-align: center;
+  padding: 10px;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+/* 缩小副标题字体和间距 */
+.sample-subtitle {
+  color: #7f8c8d;
+  font-size: 12px;
+  margin-top: 2px;
+}
+</style>

+ 303 - 0
src/components/detectionStatistics/irrigationstatistics.vue

@@ -0,0 +1,303 @@
+<template>
+  <div class="boxplot-container">
+
+    <div class="chart-container">
+      <div class="header">
+      <div class="chart-title">灌溉水重金属浓度统计箱线图</div>
+      <p>展示各重金属浓度的分布特征(最小值、四分位数、中位数、最大值)</p>
+      <p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
+    </div>
+
+    <div v-if="isLoading" class="loading-state">
+      <span class="spinner"></span> 数据加载中...
+    </div>
+
+    <div v-else-if="error" class="error-state">
+      ❌ 加载失败:{{ error.message }}
+    </div>
+
+    <div v-else>
+      <div class="chart-wrapper">
+        <v-chart :option="chartOption" autoresize />
+      </div>
+    </div>
+    </div>
+    
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import VChart from 'vue-echarts'
+import axios from 'axios'
+import { ref, onMounted ,computed} from 'vue'
+
+export default {
+  components: { VChart },
+  setup() {
+    // -------- 基本状态 --------
+    const apiUrl = ref('http://localhost:8000/api/vector/export/all?table_name=water_sampling_data')
+    const apiTimestamp = ref(null)
+    const sampleCount = ref(0)
+
+    const sampleData = ref([])
+    const chartOption = ref({})
+    const isLoading = ref(true)
+    const error = ref(null)
+    const totalPoints = computed(() => sampleData.value.length)
+
+    // 关键:缓存每个品类的统计量(与 x 轴顺序一致)
+    const statsByIndex = ref([])
+
+    // -------- 配置:金属字段(保持你原样) --------
+    const heavyMetals = [
+      { key: 'cr_concentration', name: '铬 (Cr)', color: '#FF9800' },
+      { key: 'as_concentration', name: '砷 (As)', color: '#4CAF50' },
+      { key: 'cd_concentration', name: '镉 (Cd)', color: '#9C27B0' },
+      { key: 'hg_concentration', name: '汞 (Hg)', color: '#2196F3' },
+      { key: 'pb_concentration', name: '铅 (Pb)', color: '#F44336' }
+    ]
+
+    // -------- 日志 --------
+    const log = (message, metalName = '') => {
+      console.log(`%c[${metalName || '全局'}] %c${message}`,
+        'color:#4CAF50;font-weight:bold', 'color:#333')
+    }
+
+    // -------- 工具:百分位(线性插值) --------
+    const 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])
+    }
+
+    // -------- 计算统计量 --------
+    const calculateBoxplotStats = () => {
+      const stats = []
+
+      heavyMetals.forEach((metal) => {
+        //log(`开始处理重金属: ${metal.name}`, metal.name)
+        const rawValues = sampleData.value.map(item => item[metal.key])
+        //log(`原始值(未过滤): [${rawValues.join(', ')}]`, metal.name)
+
+        const values = rawValues
+          .map((val, idx) => {
+            const num = Number(val)
+            if (isNaN(num)) log(`⚠️ 第${idx + 1}条数据无效: ${val}`, metal.name)
+            return isNaN(num) ? null : num
+          })
+          .filter(v => v !== null)
+
+        //log(`有效数值(过滤后): [${values.join(', ')}]`, metal.name)
+
+        const sorted = [...values].sort((a, b) => a - b)
+        //log(`排序后(升序): [${sorted.join(', ')}]`, metal.name)
+
+        let min = null, q1 = null, median = null, q3 = null, max = null
+        if (sorted.length > 0) {
+          min = sorted[0]
+          max = sorted[sorted.length - 1]
+          q1 = calculatePercentile(sorted, 25)
+          median = calculatePercentile(sorted, 50)
+          q3 = calculatePercentile(sorted, 75)
+          const ok = min <= q1 && q1 <= median && median <= q3 && q3 <= max
+          //log(`统计量: min=${min}, q1=${q1}, median=${median}, q3=${q3}, max=${max} → 顺序OK: ${ok}`, metal.name)
+        } else {
+          log(`⚠️ 无有效数据,跳过统计`, metal.name)
+        }
+
+        stats.push({ key: metal.key, name: metal.name, min, q1, median, q3, max, color: metal.color })
+      })
+
+      return stats
+    }
+
+    // -------- 构建箱线数据(保留你自己的顺序) --------
+    const buildBoxplotData = (stats) => {
+      const xAxisData = heavyMetals.map(m => m.name)
+
+      // 与 x 轴顺序对齐的统计量缓存(**tooltip 用它,不用 params.data**)
+      statsByIndex.value = heavyMetals.map(m =>
+        stats.find(s => s.key === m.key) || { min: null, q1: null, median: null, q3: null, max: null }
+      )
+
+      const data = statsByIndex.value.map((s, i) => {
+        if (s.min === null) return [null, null, null, null, null]
+        const box = [s.min, s.q1, s.median, s.q3, s.max]
+        //log(`箱线数据(${xAxisData[i]}): [${box.join(', ')}]`, '箱线构建')
+        return box
+      })
+
+      return { xAxisData, data }
+    }
+
+    // -------- 初始化图表 --------
+    const initChart = () => {
+      const stats = calculateBoxplotStats()
+      const { xAxisData, data } = buildBoxplotData(stats)
+
+      chartOption.value = {
+        tooltip: {
+          trigger: 'item',
+          // 关键:不从 params.data 取,直接读我们缓存的原始统计值,彻底避免被内部处理影响
+          formatter: (params) => {
+            const s = statsByIndex.value[params.dataIndex]
+            if (!s || s.min === null) {
+              return `<div style="font-weight:bold;color:#f56c6c">${xAxisData[params.dataIndex]}</div><div>无有效数据</div>`
+            }
+            return `<div style="font-weight:bold">${xAxisData[params.dataIndex]}</div>
+              <div style="margin-top:8px">
+                <div>最小值:<span style="color:#5a5;">${s.min.toFixed(4)}</span></div>
+                <div>下四分位:<span style="color:#d87a80;">${s.q1.toFixed(4)}</span></div>
+                <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${s.median.toFixed(4)}</span></div>
+                <div>上四分位:<span style="color:#d87a80;">${s.q3.toFixed(4)}</span></div>
+                <div>最大值:<span style="color:#5a5;">${s.max.toFixed(4)}</span></div>
+              </div>`
+          },
+        },
+        xAxis: {
+          type: 'category',
+          data: xAxisData,
+          name: '重金属类型',
+          nameLocation: 'middle',
+          nameGap: 30,
+          axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold' ,fontSize :11}
+        },
+        yAxis: {
+          type: 'value',
+          name: 'ug/L',
+          nameTextStyle: { fontSize: 12 }, 
+          nameLocation: 'end',
+          nameGap: 8,
+          axisLabel: { color: '#555', fontWeight: 'bold',fontSize:11 },
+          splitLine: { lineStyle: { color: '#f0f0f0' } }
+        },
+        series: [{
+          name: '重金属浓度分布',
+          type: 'boxplot',
+          data,
+          itemStyle: {
+            color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
+            borderWidth: 2
+          },
+          emphasis: {
+            itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.2)', borderWidth: 3 }
+          }
+        }],
+        grid: { top: '10%', right: '3%', left: '6%', bottom: '10%' }
+      }
+
+      isLoading.value = false
+    }
+
+    // -------- 拉取接口并绘图 --------
+    onMounted(async () => {
+      try {
+        log('发起API请求...')
+        const response = await axios.get(apiUrl.value)
+        apiTimestamp.value = new Date().toLocaleString()
+
+        if (response.data && response.data.features) {
+          // 你的接口:GeoJSON -> features[].properties
+          sampleData.value = response.data.features.map(f => f.properties)
+          sampleCount.value = sampleData.value.length
+          log(`接口返回数据量: ${sampleCount.value} 条`, '接口')
+          initChart()
+        } else {
+          throw new Error('接口返回数据格式不正确(无features字段)')
+        }
+      } catch (err) {
+        error.value = err
+        isLoading.value = false
+        console.error('接口请求失败:', err)
+      }
+    })
+
+    return {
+      apiUrl,
+      apiTimestamp,
+      sampleCount,
+      chartOption,
+      isLoading,
+      error,
+      totalPoints
+    }
+  }
+}
+</script>
+
+<style scoped>
+.boxplot-container {
+  width: 100%;
+  height: 300px;
+  max-width: 1000px;
+  margin: 0 auto;
+}
+.header { 
+  text-align: left;
+   margin-bottom: 10px; 
+}
+
+.header h2 { 
+  font-size: 0.6rem; 
+  color: #333; 
+  margin-bottom: 4px;
+ }
+.header p { 
+  font-size: 0.6rem; 
+  color: #666;
+  margin: 0; 
+}
+.loading-state { 
+  text-align: center;
+   padding: 40px 0;
+   color: #666; 
+}
+.loading-state .spinner {
+  display: inline-block; width: 24px; height: 24px; margin-right: 8px;
+  border: 3px solid #ccc; border-top-color: #1890ff; border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+.error-state { text-align: center; padding: 40px 0; color: #f56c6c; }
+.chart-wrapper { width: 100%; height: 220px; }
+.chart-container {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+  margin-bottom: 25px;
+  height: 100%;
+  box-sizing: border-box;
+}
+
+.chart-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start; /**与顶部对齐 */
+  margin-bottom: 15px;
+}
+
+.chart-title {
+  font-size: 14px;
+  color: #2980b9;
+  font-weight: 600;
+}
+
+.title-group {
+    display: flex;
+    align-items: left;
+}
+
+.sample-subtitle {
+  font-size: 0.85rem;
+  color: #888;
+  margin-top: 4px;
+}
+</style>

+ 2 - 2
src/components/layout/menuItems.ts

@@ -349,13 +349,13 @@ import {
     },
     {
       index: '/FarmlandPollutionStatistics',
-      label: 'FarmlandPollutionStatistics.Title',//<!--i18n:FarmlandPollutionStatistics.Title-->耕地污染信息统计
+      label: 'FarmlandPollutionStatistics.Title',//<!--i18n:FarmlandPollutionStatistics.Title-->土壤镉含量统计
       icon: List,
       tab: 'dataStatistics'
     },
     {
       index: '/PlantingRiskStatistics',
-      label: 'PlantingRiskStatistics.Title',//<!--i18n:PlantingRiskStatistics.Title-->种植风险信息统计
+      label: 'PlantingRiskStatistics.Title',//<!--i18n:PlantingRiskStatistics.Title-->作物风险评估统计
       icon: List,
       tab: 'dataStatistics'
     }

+ 491 - 0
src/components/soilcdStatistics/cropcdStatictics.vue

@@ -0,0 +1,491 @@
+<template>
+  <div class="crop-cd-dashboard p-4 bg-white min-h-screen">
+    <div class="flex justify-between items-center mb-6">
+      <h1 class="text-xl font-bold text-gray-800">作物态 Cd 数据统计分析</h1>
+      <div class="flex items-center">
+        <div class="stat-card inline-block px-3 py-2">
+          <div class="stat-value text-lg">样本数量{{ stats.samples }}</div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 加载状态 -->
+    <div v-if="isLoading" class="loading-overlay">
+      <span class="spinner"></span>
+      <span class="ml-3 text-gray-700">数据加载中...</span>
+    </div>
+    
+    <!-- 错误提示 -->
+    <div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
+      <p>数据加载失败: {{ error.message }}</p>
+      <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">重试</button>
+    </div>
+    
+    
+    <!-- 1️⃣ 作物态Cd指标 -->
+    <section class="mb-4 chart-container">
+      <h3 class="section-title text-base font-semibold">作物态Cd主要指标</h3>
+      <div class="flex flex-wrap mb-3">
+        <div class="legend-item" v-for="(item, index) in fieldConfig.pollution" :key="index">
+          <div class="legend-color" :style="{ backgroundColor: item.color }"></div>
+          <span>{{ item.legendName }}</span>
+        </div>
+      </div>
+      <div ref="cdBarChart" style="width: 100%; height: 320px;"></div>
+    </section>
+
+    <!-- 2️⃣ 养分元素 -->
+    <section class="mb-4 chart-container">
+      <h3 class="section-title text-base font-semibold">主要养分元素</h3>
+      <div ref="nutrientBoxChart" style="width: 100%; height: 400px;"></div>
+    </section>
+
+    <!-- 3️⃣ 其他理化性质 -->
+    <section class="chart-container">
+      <div class="flex justify-between items-center mb-3">
+        <h3 class="section-title text-base font-semibold">其他理化性质</h3>
+      </div>
+      <div ref="extraBoxChart" style="width: 100%; height: 400px;"></div>
+    </section>
+
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import axios from 'axios';
+
+
+// 图表实例引用
+const cdBarChart = ref(null);
+const nutrientBoxChart = ref(null);
+const extraBoxChart = ref(null);
+
+
+// 图表实例变量
+let chartInstanceCd = null;
+let chartInstanceNutrient = null;
+let chartInstanceExtra = null;
+
+
+// 响应式状态
+const showDialog = ref(false);
+const dialogTitle = ref("");
+
+const isLoading = ref(true);
+const error = ref(null);
+const lastUpdate = ref("");
+const stats = ref({
+  cd002Avg: 0,
+  cd02Avg: 0,
+  cd2Avg: 0,
+  samples: 0
+});
+
+// 按类别缓存统计数据(与x轴顺序对应)
+const pollutionStats = ref([]);
+const nutrientStats = ref([]);
+const extraStats = ref([]);
+
+// 字段配置(根据接口返回的作物态Cd数据结构定义)
+const fieldConfig = {
+ pollution: [
+    { 
+      key: '002_0002IDW', 
+      name: '002_0002IDW',  // 横坐标保持原标识
+      legendName: '002_0002IDW:粒径在0.002~0.02mm的粉粒组分质量占比,%',  // 图例显示中文
+      color: '#5470c6' ,
+      unit:"%",
+      convert: false
+    },
+    { 
+      key: '02_002IDW', 
+      name: '02_002IDW',  // 横坐标保持原标识
+      legendName: '02_002IDW:粒径在0.02~0.2mm的砂粒组分质量占比,%',  // 图例显示中文
+      color: '#91cc75' ,
+      unit:"%",
+      convert: false
+    },
+    { 
+      key: '2_02IDW', 
+      name: '2_02IDW',  // 横坐标保持原标识
+      legendName: '2_02IDW:粒径大于2mm的石砾组分质量占比,%',  // 图例显示中文
+      color: '#fac858' ,
+      unit:"%",
+      convert: false
+    }
+  ],
+  nutrient: [
+    { key: 'AvaK_IDW', name: '速效钾', color: '#ee6666',unit:'mg/kg' ,convert: false},
+    { key: 'SAvaK_IDW', name: '交换性速效钾', color: '#ee6666' ,unit:'mg/kg' ,convert: false},
+    { key: 'AvaP_IDW', name: '速效磷', color: '#ee6666' ,unit:'mg/kg' ,convert: false},
+    { key: 'TMn_IDW', name: '全锰', color: '#73c0de' ,unit:'mg/kg' ,convert: false},
+    { key: 'TN_IDW', name: '全氮', color: '#ee6666' ,unit:'mg/kg' ,convert: true,conversionFactor:1000},
+    { key: 'TS_IDW', name: '全硫', color: '#ee6666',unit:'mg/kg' ,convert: true,conversionFactor:1000}
+  ],
+  extra: [
+    { key: 'TAl_IDW', name: '全铝', color: '#73c0de' ,unit:'%',convert: false },
+    { key: 'TCa_IDW', name: '全钙', color: '#73c0de' ,unit:'%',convert: false},
+    { key: 'TFe_IDW', name: '全铁', color: '#73c0de' ,unit:'%',convert: false},
+    { key: 'TMg_IDW', name: '全镁', color: '#73c0de' ,unit:'%',convert: false},
+  ]
+};
+
+
+// 数据请求(作物态Cd接口)
+const fetchData = async () => {
+  try {
+    const apiUrl = 'http://localhost:8000/api/vector/export/all?table_name=CropCd_input_data&format=json';
+    const response = await axios.get(apiUrl);
+    
+    // 接口返回格式判断(GeoJSON或直接数组)
+    const rawData = response.data.features 
+      ? response.data.features.map(f => f.properties) 
+      : response.data;
+    return rawData;
+  } catch (err) {
+    throw new Error('数据加载失败: ' + err.message);
+  }
+};
+
+// 分位数计算(采用QUARTILE.INC算法)
+const 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]);
+};
+
+// 计算单个字段的统计量
+const calculateFieldStats = (data, fieldKey, fieldName) => {
+  // 获取字段配置
+  const fieldConfigItem = [
+    ...fieldConfig.pollution,
+    ...fieldConfig.nutrient,
+    ...fieldConfig.extra
+  ].find(f => f.key === fieldKey);
+  // 提取并清洗数据
+  const rawValues = data.map(item => item[fieldKey]);
+  const values = rawValues
+    .map((val, idx) => {
+      let num = Number(val);
+      // 应用单位转换
+      if (fieldConfigItem?.convert && fieldConfigItem.conversionFactor) {
+        num = num * fieldConfigItem.conversionFactor;
+      }
+      //if (isNaN(num)) log(`无效数据: 第${idx+1}条 → ${val}`, fieldName);
+      return isNaN(num) ? null : num;
+    })
+    .filter(v => v !== null);
+  
+  if (values.length === 0) {
+    //log(`无有效数据`, fieldName);
+    return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
+  }
+  
+  // 排序并计算统计量
+  const sorted = [...values].sort((a, b) => a - b);
+  const min = sorted[0];
+  const max = sorted[sorted.length - 1];
+  const q1 = calculatePercentile(sorted, 25);
+  const median = calculatePercentile(sorted, 50);
+  const q3 = calculatePercentile(sorted, 75);
+  
+  // 强制校正顺序
+  const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
+  return {
+    key: fieldKey,
+    name: fieldName,
+    min: sortedStats[0],
+    q1: sortedStats[1],
+    median: sortedStats[2],
+    q3: sortedStats[3],
+    max: sortedStats[4],
+    avg: values.reduce((sum, val) => sum + val, 0) / values.length // 计算平均值
+  };
+};
+
+// 批量计算所有统计量
+const calculateAllStats = (data) => {
+  // 1. 作物态Cd指标统计
+  pollutionStats.value = fieldConfig.pollution.map(field => {
+     const stats =calculateFieldStats(data, field.key, field.name);
+    //console.log(`${field.name}统计结果:`,stats);
+    return stats;
+  });
+  
+  // 2. 养分元素统计
+  nutrientStats.value = fieldConfig.nutrient.map(field => 
+    calculateFieldStats(data, field.key, field.name)
+  );
+  
+  // 3. 其他理化性质统计
+  extraStats.value = fieldConfig.extra.map(field => 
+    calculateFieldStats(data, field.key, field.name)
+  );
+  
+  // 更新平均值统计
+  const cd002Stats = pollutionStats.value.find(s => s.key === '002_0002IDW');
+  const cd02Stats = pollutionStats.value.find(s => s.key === '02_002IDW');
+  const cd2Stats = pollutionStats.value.find(s => s.key === '2_02IDW');
+  
+  stats.value = {
+    cd002Avg: cd002Stats?.avg || 0,
+    cd02Avg: cd02Stats?.avg || 0,
+    cd2Avg: cd2Stats?.avg || 0,
+    samples: data.length
+  };
+};
+
+// 构建箱线图数据
+const buildBoxplotData = (statsArray) => {
+  return statsArray.map(stat => {
+    if (!stat.min) return [null, null, null, null, null];
+    return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
+  });
+};
+
+// 初始化作物态Cd指标图表
+const initPollutionChart = () => {
+  if (chartInstanceCd) chartInstanceCd.dispose();
+  chartInstanceCd = echarts.init(cdBarChart.value);
+  
+  const xAxisData = fieldConfig.pollution.map(f => f.name);
+  const barData = pollutionStats.value.map(stat => stat.avg || 0);
+  
+  chartInstanceCd.setOption({
+    title: { },
+    tooltip: {
+      trigger: 'axis',
+      formatter: (params) => `${params[0].name}<br/>平均值: ${params[0].value.toFixed(4)}`
+    },
+    grid: { top: 40, right: 15, bottom: 50, left: 40 },
+    xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 12,rotate:30 } },
+    yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
+    series: [{
+      name: '平均值', type: "bar",
+      itemStyle: { color: (p) => fieldConfig.pollution[p.dataIndex].color },
+      data: barData
+    }]
+  });
+  
+};
+
+// 初始化养分元素图表
+const initNutrientChart = () => {
+  if (chartInstanceNutrient) chartInstanceNutrient.dispose();
+  chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
+  
+  const xAxisData = fieldConfig.nutrient.map(f => f.name);
+  const boxData = buildBoxplotData(nutrientStats.value);
+  
+  chartInstanceNutrient.setOption({
+    title: { text: "主要养分元素分布", left: 'center', textStyle: { fontSize: 14 } },
+    tooltip: {
+      trigger: "item",
+      formatter: (params) => {
+        const stat = nutrientStats.value[params.dataIndex];
+        const fieldConfigItem = fieldConfig.nutrient.find(f => f.key === stat.key);
+        return formatTooltip(stat, fieldConfigItem?.unit);
+      }
+    },
+    grid: { top: 40, right: 15, bottom: 45, left: 40 },
+    xAxis: { 
+      type: "category", 
+      data: xAxisData, 
+      axisLabel: { 
+        fontSize: 11, 
+        rotate: 30,
+      } 
+    },
+    yAxis: { 
+      type: "value", 
+      name: 'mg/kg', 
+      nameTextStyle: { fontSize: 12 }, 
+      axisLabel: { fontSize: 11 } 
+    },
+    series: [{
+      name: '养分元素', 
+      type: "boxplot",
+      itemStyle: { color: '#ee6666', borderColor: '#fac858' },
+      data: boxData
+    }]
+  });
+};
+
+// 初始化其他理化性质图表
+const initExtraChart = () => {
+  const xAxisData = fieldConfig.extra.map(f => f.name);
+  const boxData = buildBoxplotData(extraStats.value);
+  
+    nextTick(() => {
+      if (chartInstanceExtra) chartInstanceExtra.dispose();
+      chartInstanceExtra = echarts.init(extraBoxChart.value);
+      chartInstanceExtra.setOption({
+         title: { text: "其他理化性质分布", left: 'center', textStyle: { fontSize: 14 } },
+         tooltip: {
+           trigger: "item",
+           formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
+        },
+         grid: { top: 40, right: 15, bottom: 40, left: 40 },
+         xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11,rotate:30} },
+        yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
+        series: [{
+          name: '理化性质', type: "boxplot",
+           itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
+           data: boxData
+        }]
+        });
+      });
+};
+
+
+// 格式化Tooltip
+const formatTooltip = (stat , unit ='') => {
+  if (!stat || !stat.min) {
+    return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</div>`;
+  }
+  return `<div style="font-weight:bold">${stat.name}</div>
+    <div style="margin-top:8px">
+      <div>最小值:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
+      <div>下四分位:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
+      <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
+      <div>上四分位:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
+      <div>最大值:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
+    </div>`;
+};
+
+// 初始化图表主流程
+const initCharts = async () => {
+  try {
+    isLoading.value = true;
+    error.value = null;
+    
+    const data = await fetchData();
+    calculateAllStats(data);
+    
+    // 初始化图表
+    initPollutionChart();
+    initNutrientChart();
+    initExtraChart();
+    
+    isLoading.value = false;
+  } catch (err) {
+    isLoading.value = false;
+    error.value = err;
+    console.error('初始化失败:', err);
+  }
+};
+
+// 组件挂载
+onMounted(() => {
+  initCharts();
+  
+  // 窗口resize处理
+  const handleResize = () => {
+    [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
+      .forEach(inst => inst && inst.resize());
+  };
+  window.addEventListener('resize', handleResize);
+  
+  // 组件卸载清理
+  return () => {
+    window.removeEventListener('resize', handleResize);
+    [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
+      .forEach(inst => inst && inst.dispose());
+  };
+});
+</script>
+
+<style>
+.crop-cd-dashboard {
+  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  max-width: 1200px;
+  margin: 0 auto;
+  font-size: 14px;
+}
+.chart-container {
+  background: white;
+  border-radius: 6px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  padding: 12px;
+  margin-bottom: 16px;
+}
+.section-title {
+  color: #2c3e50;
+  border-left: 3px solid #3498db;
+  padding-left: 10px;
+  margin-bottom: 12px;
+}
+.toggle-btn {
+  background: #f8f9fa;
+  border: 1px solid #e9ecef;
+  padding: 6px 12px;
+  border-radius: 3px;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  transition: all 0.3s;
+}
+.toggle-btn:hover {
+  background: #e9ecef;
+}
+.loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.8);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10;
+}
+.spinner {
+  width: 30px;
+  height: 30px;
+  border: 3px solid rgba(0, 0, 0, 0.1);
+  border-radius: 50%;
+  border-left-color: #3498db;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+.stat-card {
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
+  border-radius: 6px;
+  padding: 8px 12px;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.05);
+}
+.stat-value {
+  font-size: 16px;
+  font-weight: bold;
+  color: #2c3e50;
+}
+.stat-label {
+  font-size: 12px;
+  color: #7f8c8d;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  margin-right: 12px;
+  font-size: 13px;
+  margin-bottom: 8px;
+}
+.legend-color {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  margin-right: 5px;
+}
+</style>

+ 440 - 0
src/components/soilcdStatistics/effcdStatistics.vue

@@ -0,0 +1,440 @@
+<template>
+  <div class="soil-dashboard p-4 bg-white min-h-screen">
+    <div class="flex justify-between items-center mb-6">
+      <h1 class="text-xl font-bold text-gray-800">有效态 Cd 数据统计分析</h1>
+      <div class="flex items-center">
+        <div class="stat-card inline-block px-3 py-2">
+          <div class="stat-value text-lg">样本数量{{ stats.samples }}</div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 加载状态 -->
+    <div v-if="isLoading" class="loading-overlay">
+      <span class="spinner"></span>
+      <span class="ml-3 text-gray-700">数据加载中...</span>
+    </div>
+    
+    <!-- 错误提示 -->
+    <div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
+      <p>数据加载失败: {{ error.message }}</p>
+      <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">重试</button>
+    </div>
+    
+    <!-- 1️⃣ 总Cd & 有效态Cd -->
+    <section class="mb-4 chart-container">
+      <h3 class="section-title text-base font-semibold">污染指标</h3>
+      <div ref="cdBarChart" style="width: 100%; height: 415px;"></div>
+    </section>
+
+    <!-- 2️⃣ 养分元素 -->
+    <section class="mb-4 chart-container">
+      <h3 class="section-title text-base font-semibold">主要养分元素</h3>
+      <div ref="nutrientBoxChart" style="width: 100%; height: 400px;"></div>
+    </section>
+
+    <!-- 3️⃣ 其他理化性质 -->
+    <section class="chart-container">
+      <div class="flex justify-between items-center mb-3">
+        <h3 class="section-title text-base font-semibold">其他理化性质</h3>
+      </div>
+      <div ref="extraBoxChart" style="height: 400px; width: 100%;"></div>
+    </section>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import axios from 'axios';
+
+// 图表实例引用
+const cdBarChart = ref(null);
+const nutrientBoxChart = ref(null);
+const extraBoxChart = ref(null);
+
+// 图表实例变量
+let chartInstanceCd = null;
+let chartInstanceNutrient = null;
+let chartInstanceExtra = null;
+
+
+// 响应式状态
+const isLoading = ref(true);
+const error = ref(null);
+const stats = ref({
+  totalCdAvg: 0,
+  effCdAvg: 0,
+  samples: 0
+});
+
+// 参考灌溉水代码:按图表类型缓存统计数据(与x轴顺序严格对应)
+const pollutionStats = ref([]);       // 污染指标统计(总镉/有效态镉)
+const nutrientStats = ref([]);        // 养分元素统计
+const extraStats = ref([]);           // 其他理化性质统计
+
+// 字段配置(参考灌溉水的重金属配置方式)
+const fieldConfig = {
+  pollution: [
+    { key: 'TCd_IDW', name: '总镉', color: '#5470c6' ,unit:'mg/kg' , convert: false },
+     { key: 'AvaK_IDW', name: '速效钾', color: '#fac858',unit:'mg/kg', convert: false },
+    { key: 'AvaP_IDW', name: '有效磷', color: '#fac858' ,unit:'mg/kg' , convert: false},
+    { key: 'TMn_IDW', name: '全锰', color: '#73c0de' ,unit:'mg/kg' , convert: false},
+    { key: 'TN_IDW', name: '全氮', color: '#fac858' ,unit:'mg/kg' , convert: true, conversionFactor: 1000},
+    { key: 'TP_IDW', name: '全磷', color: '#fac858' ,unit:'mg/kg', convert: true, conversionFactor: 1000},
+    { key: 'TK_IDW', name: '全钾', color: '#fac858' ,unit:'mg/kg', convert: true, conversionFactor: 1000},
+  ],
+  nutrient: [
+    { key: 'AvaK_IDW', name: '速效钾', color: '#fac858',unit:'mg/kg' , convert: false},
+    { key: 'AvaP_IDW', name: '有效磷', color: '#fac858' ,unit:'mg/kg', convert: false },
+    { key: 'TMn_IDW', name: '全锰', color: '#73c0de' ,unit:'mg/kg', convert: false },
+    { key: 'TN_IDW', name: '全氮', color: '#fac858' ,unit:'mg/kg' , convert: true, conversionFactor: 1000},
+    { key: 'TP_IDW', name: '全磷', color: '#fac858' ,unit:'mg/kg' , convert: true, conversionFactor: 1000},
+    { key: 'TK_IDW', name: '全钾', color: '#fac858' ,unit:'mg/kg', convert: true, conversionFactor: 1000},
+    { key: 'TS_IDW', name: '全硫', unit:'mg/kg',convert:true,conversionFactor:1000}
+  ],
+  extra: [
+    { key: 'TFe_IDW', name: '全铁', color: '#73c0de' ,unit:'%', convert: false},
+    { key: 'TCa_IDW', name: '全钙', color: '#73c0de' ,unit:'%', convert: false},
+    { key: 'TMg_IDW', name: '全镁', color: '#73c0de' ,unit:'%', convert: false},
+    { key: 'TAl_IDW', name: '全铝', color: '#73c0de' ,unit:'%', convert: false}
+  ]
+};
+
+
+// 数据请求
+const fetchData = async () => {
+  try {
+    // 实际项目中替换为真实API
+     const res = await axios.get("http://localhost:8000/api/vector/export/all?table_name=EffCd_input_data&format=json");
+     return res.data;
+
+  } catch (err) {
+    throw new Error('数据加载失败: ' + err.message);
+  }
+};
+
+// 核心:分位数计算(参考灌溉水的精准计算逻辑)
+const 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];
+  
+  // 采用与Excel QUARTILE.INC一致的算法
+  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]);
+};
+
+// 计算单个字段的箱线图统计量(带顺序校正)
+const calculateFieldStats = (data, fieldKey, fieldName) => {
+  // 获取字段配置
+  const fieldConfigItem = [
+    ...fieldConfig.pollution,
+    ...fieldConfig.nutrient,
+    ...fieldConfig.extra
+  ].find(f => f.key === fieldKey);
+  // 提取并清洗数据
+  const rawValues = data.map(item => item[fieldKey]);
+  const values = rawValues
+    .map((val, idx) => {
+      let num = Number(val);
+      if(fieldConfigItem?.convert && fieldConfigItem.conversionFactor){
+        num = num*fieldConfigItem.conversionFactor;
+      }
+      //if (isNaN(num)) log(`无效数据: 第${idx+1}条 → ${val}`, fieldName);
+      return isNaN(num) ? null : num;
+    })
+    .filter(v => v !== null);
+  
+  if (values.length === 0) {
+    //log(`无有效数据`, fieldName);
+    return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
+  }
+  
+  // 排序并计算统计量
+  const sorted = [...values].sort((a, b) => a - b);
+  const min = sorted[0];
+  const max = sorted[sorted.length - 1];
+  const q1 = calculatePercentile(sorted, 25);
+  const median = calculatePercentile(sorted, 50);
+  const q3 = calculatePercentile(sorted, 75);
+
+  const mean = values.reduce((sum,val)=>sum+val,0)/values.length;
+  
+  // 强制校正顺序(核心修复)
+  const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
+  
+  //log(`统计量: min=${finalStats.min.toFixed(4)}, q1=${finalStats.q1.toFixed(4)}, 
+    //median=${finalStats.median.toFixed(4)}, q3=${finalStats.q3.toFixed(4)}, max=${finalStats.max.toFixed(4)}`, 
+    //fieldName);
+  
+  return {
+    key: fieldKey,
+    name: fieldName,
+    min: sortedStats[0],
+    q1: sortedStats[1],
+    median: sortedStats[2],
+    q3: sortedStats[3],
+    max: sortedStats[4],
+    mean:mean
+  };
+};
+
+// 批量计算所有字段的统计量(按图表类型缓存)
+const calculateAllStats = (data) => {
+  // 1. 污染指标统计(与x轴顺序一致)
+  pollutionStats.value = fieldConfig.pollution.map(field => 
+    calculateFieldStats(data, field.key, field.name)
+  );
+  
+  // 2. 养分元素统计(与x轴顺序一致)
+  nutrientStats.value = fieldConfig.nutrient.map(field => 
+    calculateFieldStats(data, field.key, field.name)
+  );
+  
+  // 3. 其他理化性质统计(与x轴顺序一致)
+  extraStats.value = fieldConfig.extra.map(field => 
+    calculateFieldStats(data, field.key, field.name)
+  );
+  
+  // 更新平均值统计
+  const totalCdStats = pollutionStats.value.find(s => s.key === 'TCd_IDW');
+  const effCdStats = pollutionStats.value.find(s => s.key === 'Cdsolution');
+  stats.value = {
+    totalCdAvg: totalCdStats ? (totalCdStats.min + totalCdStats.max) / 2 : 0, // 示例:用范围中点模拟平均值
+    effCdAvg: effCdStats ? (effCdStats.min + effCdStats.max) / 2 : 0,
+    samples: data.length
+  };
+};
+
+// 构建箱线图数据(将统计量转换为ECharts所需格式)
+const buildBoxplotData = (statsArray) => {
+  return statsArray.map(stat => {
+    if (!stat.min) return [null, null, null, null, null];
+    return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
+  });
+};
+
+// 初始化污染指标图表(柱状图)
+const initPollutionChart = () => {
+  if (chartInstanceCd) chartInstanceCd.dispose();
+  chartInstanceCd = echarts.init(cdBarChart.value);
+  
+  // 提取x轴标签和数据
+  const xAxisData = fieldConfig.pollution.map(f => f.name);
+  const barData = pollutionStats.value.map(stat =>stat.mean);
+  
+  chartInstanceCd.setOption({
+    title: { text: '主要指标含量对比', left: 'center', textStyle: { fontSize: 14 } },
+    tooltip: {
+      trigger: 'axis',
+      formatter: (params) => `${params[0].name}<br/>平均值: ${params[0].value.toFixed(4)} mg/kg`
+    },
+    grid: { top: 40, right: 15, bottom: 30, left: 40 },
+    xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 12 ,rotate:30 } },
+    yAxis: { type: "value", name: '含量 (mg/kg)', nameTextStyle: { fontSize: 12  }, axisLabel: { fontSize: 11 ,rotate:30} },
+    series: [{
+      name: '平均值', type: "bar",
+      itemStyle: {color: '#5470c6' },
+      data: barData
+    }]
+  });
+};
+
+// 初始化养分元素图表(箱线图)
+const initNutrientChart = () => {
+  if (chartInstanceNutrient) chartInstanceNutrient.dispose();
+  chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
+  
+  const xAxisData = fieldConfig.nutrient.map(f => f.name);
+  const boxData = buildBoxplotData(nutrientStats.value);
+  
+  chartInstanceNutrient.setOption({
+    title: { text: "主要养分元素分布", left: 'center', textStyle: { fontSize: 14 } },
+    tooltip: {
+      trigger: "item",
+      formatter: (params) => formatTooltip(nutrientStats.value[params.dataIndex])
+    },
+    grid: { top: 40, right: 15, bottom: 40, left: 40 },
+    xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11, rotate: 30 } },
+    yAxis: { type: "value", name: '含量(mg/kg)', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 , rotate: 30 } },
+    series: [{
+      name: '养分元素', type: "boxplot",
+      itemStyle: { color: '#fac858', borderColor: '#ee6666' },
+      data: boxData
+    }]
+  });
+};
+
+// 初始化其他理化性质图表(箱线图)
+const initExtraChart = () => {
+  const xAxisData = fieldConfig.extra.map(f => f.name);
+  const boxData = buildBoxplotData(extraStats.value);
+  
+      nextTick(() => {
+        if (chartInstanceExtra) chartInstanceExtra.dispose();
+        chartInstanceExtra = echarts.init(extraBoxChart.value);
+        chartInstanceExtra.setOption({
+          title: { text: "其他理化性质分布", left: 'center', textStyle: { fontSize: 14 } },
+          tooltip: {
+            trigger: "item",
+            formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
+          },
+          grid: { top: 40, right: 15, bottom: 40, left: 40 },
+          xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11, rotate: 30 } },
+          yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
+          series: [{
+            name: '理化性质', type: "boxplot",
+            itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
+            data: boxData
+          }]
+        });
+  });
+};
+
+
+// 格式化Tooltip(复用缓存的统计数据)
+const formatTooltip = (stat) => {
+  if (!stat || !stat.min) {
+    return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</div>`;
+  }
+  return `<div style="font-weight:bold">${stat.name}</div>
+    <div style="margin-top:8px">
+      <div>最小值:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
+      <div>下四分位:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
+      <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
+      <div>上四分位:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
+      <div>最大值:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
+    </div>`;
+};
+
+// 初始化图表主流程
+const initCharts = async () => {
+  try {
+    isLoading.value = true;
+    error.value = null;
+    
+    const data = await fetchData();
+    calculateAllStats(data); // 计算并缓存所有统计数据
+    
+    // 初始化图表
+    initPollutionChart();
+    initNutrientChart();
+    initExtraChart();
+    
+    isLoading.value = false;
+  } catch (err) {
+    isLoading.value = false;
+    error.value = err;
+    console.error('初始化失败:', err);
+  }
+};
+
+// 组件挂载
+onMounted(() => {
+  initCharts();
+  
+  // 窗口 resize 处理
+  const handleResize = () => {
+    [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
+      .forEach(inst => inst && inst.resize());
+  };
+  window.addEventListener('resize', handleResize);
+  
+  // 组件卸载清理
+  return () => {
+    window.removeEventListener('resize', handleResize);
+    [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
+      .forEach(inst => inst && inst.dispose());
+  };
+});
+</script>
+
+<style>
+.soil-dashboard {
+  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  max-width: 1200px;
+  margin: 0 auto;
+  font-size: 14px;
+}
+.chart-container {
+  background: white;
+  border-radius: 6px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  padding: 12px;
+  margin-bottom: 16px;
+}
+.section-title {
+  color: #2c3e50;
+  border-left: 3px solid #3498db;
+  padding-left: 10px;
+  margin-bottom: 12px;
+}
+.toggle-btn {
+  background: #f8f9fa;
+  border: 1px solid #e9ecef;
+  padding: 6px 12px;
+  border-radius: 3px;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  transition: all 0.3s;
+}
+.toggle-btn:hover {
+  background: #e9ecef;
+}
+.loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.8);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10;
+}
+.spinner {
+  width: 30px;
+  height: 30px;
+  border: 3px solid rgba(0, 0, 0, 0.1);
+  border-radius: 50%;
+  border-left-color: #3498db;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+.stat-card {
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
+  border-radius: 6px;
+  padding: 8px 12px;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.05);
+}
+.stat-value {
+  font-size: 16px;
+  font-weight: bold;
+  color: #2c3e50;
+}
+.stat-label {
+  font-size: 12px;
+  color: #7f8c8d;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  margin-right: 12px;
+  font-size: 13px;
+}
+.legend-color {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  margin-right: 5px;
+}
+</style>

+ 433 - 0
src/components/soilcdStatistics/fluxcdStatictics.vue

@@ -0,0 +1,433 @@
+<template>
+  <div class="flux-cd-dashboard p-4 bg-white min-h-screen">
+    <div class="flex justify-between items-center mb-6">
+      <h1 class="text-xl font-bold text-gray-800">通量Cd数据统计分析</h1>
+      <div class="flex items-center">
+        <div class="stat-card inline-block px-3 py-2">
+          <div class="stat-value text-lg">样本数量:{{ stats.samples }}</div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 加载状态 & 错误提示 -->
+    <div v-if="isLoading" class="fixed inset-0 bg-white bg-opacity-80 flex items-center justify-center z-50">
+      <div class="text-center">
+        <div class="spinner mx-auto"></div>
+        <p class="mt-4 text-gray-700">数据加载中,请稍候...</p>
+        <p class="text-sm text-gray-500 mt-2">正在初始化图表容器</p>
+      </div>
+    </div>
+    <div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
+      <p>数据加载失败: {{ error.message }}</p>
+      <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">重试</button>
+    </div>
+    
+    <!-- 1. 初始Cd 单独箱线图 -->
+    <section class="mb-6 chart-container" v-if="!isLoading && !error">
+      <h3 class="section-title text-base font-semibold">初始Cd(Initial_Cd)分布箱线图</h3>
+      <div ref="initialCdChart" style="width: 100%; height: 400px;"></div>
+      <div v-if="!chartInstanceInitial" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
+        <p class="text-yellow-700">图表初始化中...</p>
+        <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initInitialCdChart">
+          重新尝试初始化
+        </button>
+      </div>
+    </section>
+    
+    <!-- 2. 其他指标 合并箱线图 -->
+    <section class="mb-6 chart-container" v-if="!isLoading && !error">
+      <div class="flex flex-wrap justify-between items-center mb-4">
+        <h3 class="section-title text-base font-semibold">其他通量Cd指标分布箱线图</h3>
+      </div>
+      <div ref="otherIndicatorsChart" style="width: 100%; height: 400px;"></div>
+      <div v-if="!chartInstanceOther" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
+        <p class="text-yellow-700">图表初始化中...</p>
+        <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initOtherIndicatorsChart">
+          重新尝试初始化
+        </button>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import axios from 'axios';
+
+// 图表容器 & 实例
+const initialCdChart = ref(null);   // 初始Cd图表
+const otherIndicatorsChart = ref(null); // 其他指标图表
+const chartInstanceInitial = ref(null);   // 初始Cd实例
+const chartInstanceOther = ref(null);     // 其他指标实例
+const chartInstancePopup = ref(null);     // 弹窗实例
+
+// 响应式状态
+
+const isLoading = ref(true);
+const error = ref(null);
+const stats = ref({ samples: 0 });
+
+// 统计数据(拆分两组)
+const initialCdStats = ref([]);     // 初始Cd统计
+const otherIndicatorsStats = ref([]); // 其他指标统计
+
+// 字段配置(拆分初始Cd和其他指标)
+const fieldConfig = {
+  initialCd: [
+    { key: 'Initial_Cd', name: '土壤初始Cd总量', color: '#5470c6' }
+  ],
+  otherIndicators: [
+    { key: 'DQCJ_Cd', name: '大气沉降输入Cd', color: '#91cc75' },
+    { key: 'GGS_Cd', name: '灌溉水输入Cd', color: '#fac858' },
+    { key: 'NCP_Cd', name: '农业投入输入Cd', color: '#ee6666' },
+    { key: 'DX_Cd', name: '地下渗漏Cd', color: '#73c0de' },
+    { key: 'DB_Cd', name: '地表径流Cd', color: '#38b2ac' },
+    { key: 'ZL_Cd', name: '籽粒移除Cd', color: '#4169e1' },
+  ]
+};
+
+// 日志工具
+const log = (message, field = '') => {
+  console.log(`%c[${field || '全局'}] %c${message}`,
+    'color:#2196F3;font-weight:bold', 'color:#333');
+};
+
+
+// 数据请求
+const fetchData = async () => {
+  try {
+    const apiUrl = 'http://localhost:8000/api/vector/export/all?table_name=FluxCd_input_data&format=json';
+    const response = await axios.get(apiUrl);
+    const rawData = response.data.features 
+      ? response.data.features.map(f => f.properties) 
+      : response.data;
+    return rawData;
+  } catch (err) {
+    throw new Error('数据加载失败: ' + err.message);
+  }
+};
+
+// 分位数计算(QUARTILE.INC)
+const 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]);
+};
+
+// 计算单个字段的统计量
+const calculateFieldStats = (data, fieldKey, fieldName) => {
+  const rawValues = data.map(item => item[fieldKey]);
+  const values = rawValues
+    .map((val, idx) => {
+      const num = Number(val);
+      if (isNaN(num)) log(`无效数据: ${fieldName} 第${idx+1}条 →`, val);
+      return isNaN(num) ? null : num;
+    })
+    .filter(v => v !== null);
+  //console.log(`${fieldName}有效数据量:`,values.length);
+  
+  if (values.length === 0) {
+    log(`无有效数据: ${fieldName}`);
+    return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
+  }
+  
+  const sorted = [...values].sort((a, b) => a - b);
+  const min = sorted[0];
+  const max = sorted[sorted.length - 1];
+  const q1 = calculatePercentile(sorted, 25);
+  const median = calculatePercentile(sorted, 50);
+  const q3 = calculatePercentile(sorted, 75);
+  
+  // 强制校正顺序
+  const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
+  return {
+    key: fieldKey,
+    name: fieldName,
+    min: sortedStats[0],
+    q1: sortedStats[1],
+    median: sortedStats[2],
+    q3: sortedStats[3],
+    max: sortedStats[4]
+  };
+};
+
+// 计算所有统计数据(拆分两组)
+const calculateAllStats = (data) => {
+  // 初始Cd统计
+  initialCdStats.value = fieldConfig.initialCd.map(indicator => 
+    calculateFieldStats(data, indicator.key, indicator.name)
+  );
+  
+  // 其他指标统计
+  otherIndicatorsStats.value = fieldConfig.otherIndicators.map(indicator => 
+    calculateFieldStats(data, indicator.key, indicator.name)
+  );
+  
+  // 更新样本数
+  stats.value.samples = data.length;
+};
+
+// 构建箱线图数据(通用函数)
+const buildBoxplotData = (statsArray) => {
+  return statsArray.map(stat => {
+    if (!stat.min) return [null, null, null, null, null];
+    return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
+  });
+};
+
+// 初始化【初始Cd】图表(独立箱线图)
+const initInitialCdChart = (retryCount = 0) => {
+  // 容器检查与重试机制
+  if (!initialCdChart.value) {
+    if (retryCount < 10) {
+      console.warn(`initialCdChart容器未找到,第${retryCount+1}次重试...`);
+      setTimeout(() => initInitialCdChart(retryCount + 1), 100);
+      return;
+    } else {
+      console.error('initialCdChart容器未找到,重试超时');
+      return;
+    }
+  }
+  
+  // 容器尺寸检查
+  if (initialCdChart.value.offsetWidth === 0 || initialCdChart.value.offsetHeight === 0) {
+    if (retryCount < 5) {
+      setTimeout(() => initInitialCdChart(retryCount + 1), 200);
+      return;
+    }
+  }
+
+  if (chartInstanceInitial.value) chartInstanceInitial.value.dispose();
+  chartInstanceInitial.value = echarts.init(initialCdChart.value);
+  const xAxisData = fieldConfig.initialCd.map(ind => ind.name);
+  const boxData = buildBoxplotData(initialCdStats.value);
+  
+  chartInstanceInitial.value.setOption({
+    title: { text: '初始Cd分布箱线图', left: 'center', textStyle: { fontSize: 14 } },
+    tooltip: {
+      trigger: "item",
+      formatter: (params) => formatTooltip(initialCdStats.value[params.dataIndex])
+    },
+    grid: { top: 60, right: 30, bottom: 25, left: 60 },
+    xAxis: { 
+      type: "category", 
+      data: xAxisData,
+      axisLabel: { fontSize: 12 }
+    },
+    yAxis: { 
+      type: "value", 
+      name: 'g/ha', 
+      nameTextStyle: { fontSize: 12 }, 
+      axisLabel: { fontSize: 11 },
+      scale: true
+    },
+    series: [{
+      name: '初始Cd', 
+      type: "boxplot",
+      itemStyle: { 
+        color: (p) => fieldConfig.initialCd[p.dataIndex].color,
+        borderWidth: 2 
+      },
+      data: boxData
+    }]
+  });
+};
+
+// 初始化【其他指标】合并图表
+const initOtherIndicatorsChart = (retryCount = 0) => {
+  if (!otherIndicatorsChart.value) {
+    if (retryCount < 10) {
+      console.warn(`otherIndicatorsChart容器未找到,第${retryCount+1}次重试...`);
+      setTimeout(() => initOtherIndicatorsChart(retryCount + 1), 100);
+      return;
+    } else {
+      console.error('otherIndicatorsChart容器未找到,重试超时');
+      return;
+    }
+  }
+  
+  // 容器尺寸检查
+  if (otherIndicatorsChart.value.offsetWidth === 0 || otherIndicatorsChart.value.offsetHeight === 0) {
+    if (retryCount < 5) {
+      setTimeout(() => initOtherIndicatorsChart(retryCount + 1), 200);
+      return;
+    }
+  }
+
+  if (chartInstanceOther.value) chartInstanceOther.value.dispose();
+  chartInstanceOther.value = echarts.init(otherIndicatorsChart.value);
+  const xAxisData = fieldConfig.otherIndicators.map(ind => ind.name);
+  const boxData = buildBoxplotData(otherIndicatorsStats.value);
+  
+  chartInstanceOther.value.setOption({
+    title: { text: '其他通量Cd指标分布对比', left: 'center', textStyle: { fontSize: 14 } },
+    tooltip: {
+      trigger: "item",
+      formatter: (params) => formatTooltip(otherIndicatorsStats.value[params.dataIndex])
+    },
+    grid: { top: 60, right: 30, bottom: 70, left: 60 },
+    xAxis: { 
+      type: "category", 
+      data: xAxisData,
+      axisLabel: { fontSize: 11, rotate: 45 }
+    },
+    yAxis: { 
+      type: "value", 
+      name: 'g/ha/a', 
+      nameTextStyle: { fontSize: 12 }, 
+      axisLabel: { fontSize: 11 }
+    },
+    series: [{
+      name: '其他指标', 
+      type: "boxplot",
+      itemStyle: { 
+        color: (p) => fieldConfig.otherIndicators[p.dataIndex].color,
+        borderWidth: 2 
+      },
+      data: boxData
+    }]
+  });
+  
+
+};
+
+
+
+// Tooltip格式化(通用逻辑)
+const formatTooltip = (stat) => {
+  if (!stat || !stat.min) {
+    return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</div>`;
+  }
+  return `<div style="font-weight:bold">${stat.name}</div>
+    <div style="margin-top:8px">
+      <div>最小值:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
+      <div>下四分位:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
+      <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
+      <div>上四分位:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
+      <div>最大值:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
+    </div>`;
+};
+
+// 初始化图表主流程
+const initCharts = async () => {
+  try {
+    isLoading.value = true;
+    error.value = null;
+    
+    const data = await fetchData();
+    calculateAllStats(data);
+    
+    await nextTick(); // 等待DOM渲染完成
+    
+    // 添加额外延迟确保容器渲染
+    await new Promise(resolve => setTimeout(resolve, 300));
+    
+    initInitialCdChart();
+    initOtherIndicatorsChart();
+    
+    isLoading.value = false;
+  } catch (err) {
+    isLoading.value = false;
+    error.value = err;
+    console.error('初始化失败:', err);
+  }
+};
+
+// 组件挂载 & 销毁
+onMounted(() => {
+  initCharts();
+  
+  // 窗口resize响应
+  const handleResize = () => {
+    if (chartInstanceInitial.value) chartInstanceInitial.value.resize();
+    if (chartInstanceOther.value) chartInstanceOther.value.resize();
+    if (chartInstancePopup.value) chartInstancePopup.value.resize();
+  };
+  window.addEventListener('resize', handleResize);
+  
+  return () => {
+    window.removeEventListener('resize', handleResize);
+    if (chartInstanceInitial.value) chartInstanceInitial.value.dispose();
+    if (chartInstanceOther.value) chartInstanceOther.value.dispose();
+    if (chartInstancePopup.value) chartInstancePopup.value.dispose();
+  };
+});
+</script>
+
+<style scoped>
+.flux-cd-dashboard {
+  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  max-width: 1200px;
+  margin: 0 auto;
+  font-size: 14px;
+}
+.chart-container {
+  background: white;
+  border-radius: 6px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  padding: 16px;
+  margin-bottom: 16px;
+  min-height: 400px;
+  position: relative;
+}
+[ref="initialCdChart"], 
+[ref="otherIndicatorsChart"] {
+  min-height: 400px;
+  background-color: #f9f9f9;
+  border: 1px dashed #eee;
+}
+
+.section-title {
+  color: #2c3e50;
+  border-left: 3px solid #3498db;
+  padding-left: 10px;
+  margin-bottom: 12px;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  margin-right: 12px;
+  margin-bottom: 6px;
+  font-size: 12px;
+}
+.legend-color {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  margin-right: 5px;
+}
+.spinner {
+  width: 30px;
+  height: 30px;
+  border: 3px solid rgba(0, 0, 0, 0.1);
+  border-radius: 50%;
+  border-left-color: #3498db;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+.stat-card {
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
+  border-radius: 6px;
+  padding: 8px 12px;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.05);
+}
+.stat-value {
+  font-size: 16px;
+  font-weight: bold;
+  color: #2c3e50;
+}
+.stat-label {
+  font-size: 12px;
+  color: #7f8c8d;
+}
+</style>

+ 2 - 2
src/locales/en.json

@@ -207,9 +207,9 @@
     "Title": "Detection information statistics"
   },
   "FarmlandPollutionStatistics": {
-    "Title": "Statistics of cultivated land pollution information"
+    "Title": "Soil cadmium content statistics"
   },
   "PlantingRiskStatistics": {
-    "Title": "Planting risk information statistics"
+    "Title": "Crop risk assessment statistics"
   }
 }

+ 2 - 2
src/locales/zh.json

@@ -208,9 +208,9 @@
     "Title": "检测信息统计"
   },
   "FarmlandPollutionStatistics": {
-    "Title": "耕地污染信息统计"
+    "Title": "土壤镉含量统计"
   },
   "PlantingRiskStatistics": {
-    "Title": "种植风险信息统计"
+    "Title": "作物风险评估统计"
   }
 }

+ 3 - 3
src/router/index.ts

@@ -391,15 +391,15 @@ const routes = [
         path: "FarmlandPollutionStatistics",
         name: "FarmlandPollutionStatistics",
         component: () =>
-          import("@/views/User/dataStatistics/FarmlandPollutionStatistics.vue"), // 修复路径
-        meta: { title: "耕地污染信息统计" },
+          import("@/views/User/dataStatistics/SoilCdStatistics.vue"), // 修复路径
+        meta: { title: "土壤镉含量统计" },
       },
       {
         path: "PlantingRiskStatistics",
         name: "PlantingRiskStatistics",
         component: () =>
           import("@/views/User/dataStatistics/PlantingRiskStatistics.vue"), // 修复路径
-        meta: { title: "种植风险信息统计" },
+        meta: { title: "作物风险评估系统" },
       },
       {
         path: "Visualizatio",

+ 78 - 518
src/views/User/dataStatistics/DetectionStatistics.vue

@@ -1,544 +1,104 @@
 <template>
-  <div class="metal-tables-container">
-    <!-- 灌溉水表格 -->
-    <div class="table-section">
-      <el-row :gutter="10" align="middle">
-        <el-col :span="20">
-          <div class="title">灌溉水采样点各重金属平均值</div>
-        </el-col>
-        <el-col :span="4" style="text-align: right;">
-          <div class="sample-count" v-if="waterValidSamples > 0">
-            有效样本:{{ waterValidSamples }}个
-          </div>
-        </el-col>
-      </el-row>
-      
-      <el-table 
-        v-if="waterTableData.length && !waterLoading && !waterError" 
-        :data="waterTableData" 
-        border 
-        style="width: 100%"
-      >
-        <el-table-column label="指标" prop="indicator" width="80" />
-        <el-table-column 
-          v-for="(metal, field) in waterMetals" 
-          :key="field" 
-          :label="metal.label" 
-          
-        >
-          <template #default="scope">
-            {{ scope.row[field] }}
-          </template>
-        </el-table-column>
-      </el-table>
-      
-      <div v-else class="empty-state" v-if="!waterLoading && !waterError">
-        <el-empty description="灌溉水数据为空,请刷新" />
+  <div class="scale-wrapper">
+    <div class="layout-container">
+      <!-- 第一列:灌溉水 + 断面数据 -->
+      <div class="column column-1">
+        <div class="component-box irrigation-box">
+          <Irrigationstatistics /> <!-- 灌溉水数据 -->
+        </div>
+        <div class="component-box section-box">
+          <CrosscetionStatistics /> <!-- 断面数据 -->
+        </div>
       </div>
-      <el-alert 
-        v-if="waterError" 
-        type="error" 
-        :description="waterError" 
-        show-icon 
-        class="error-alert"
-      />
-    </div>
-
-    <!-- 断面数据表格 -->
-    <div class="table-section">
-      <el-row :gutter="10" align="middle">
-        <el-col :span="20">
-          <div class="title">断面采样点镉含量平均值</div>
-        </el-col>
-        <el-col :span="4" style="text-align: right;">
-          <div class="sample-count" v-if="sectionValidSamples > 0">
-            有效样本:{{ sectionValidSamples }}个
-          </div>
-        </el-col>
-      </el-row>
-      
-      <el-table 
-        v-if="sectionTableData.length && !sectionLoading && !sectionError" 
-        :data="sectionTableData" 
-        border 
-        style="width: 100%"
-      >
-        <el-table-column label="指标" prop="indicator" width="80" />
-        <el-table-column 
-          v-for="(metal, field) in sectionMetals" 
-          :key="field" 
-          :label="metal.label" 
-          
-        >
-          <template #default="scope">
-            {{ scope.row[field] }}
-          </template>
-        </el-table-column>
-      </el-table>
-      
-      <div v-else class="empty-state" v-if="!sectionLoading && !sectionError">
-        <el-empty description="断面数据为空,请刷新" />
-      </div>
-      <el-alert 
-        v-if="sectionError" 
-        type="error" 
-        :description="sectionError" 
-        show-icon 
-        class="error-alert"
-      />
-    </div>
 
-    <!-- 大气企业表格 -->
-    <div class="table-section">
-      <el-row :gutter="10" align="middle">
-        <el-col :span="20">
-          <div class="title">大气企业颗粒物排放平均值</div>
-        </el-col>
-        <el-col :span="4" style="text-align: right;">
-          <div class="sample-count" v-if="atmoCompanyValidSamples > 0">
-            有效样本:{{ atmoCompanyValidSamples }}个
-          </div>
-        </el-col>
-      </el-row>
-      
-      <el-table 
-        v-if="atmoCompanyTableData.length && !atmoCompanyLoading && !atmoCompanyError" 
-        :data="atmoCompanyTableData" 
-        border 
-        style="width: 100%"
-      >
-        <el-table-column label="指标" prop="indicator" width="80" />
-        <el-table-column 
-          v-for="(metric, field) in atmoCompanyMetrics" 
-          :key="field" 
-          :label="metric.label" 
-          
-        >
-          <template #default="scope">
-            {{ scope.row[field]}}
-          </template>
-        </el-table-column>
-      </el-table>
-      
-      <div v-else class="empty-state" v-if="!atmoCompanyLoading && !atmoCompanyError">
-        <el-empty description="大气企业数据为空,请刷新" />
+      <!-- 第二列:大气采样点 + 大气污染企业 -->
+      <div class="column column-2">
+        <div class="component-box metal-box">
+          <AtmsampleStatistics /> <!-- 大气采样点数据 -->
+        </div>
+        <div class="component-box company-box">
+          <AtmcompanyStatics /> <!-- 大气污染企业数据 -->
+        </div>
       </div>
-      <el-alert 
-        v-if="atmoCompanyError" 
-        type="error" 
-        :description="atmoCompanyError" 
-        show-icon 
-        class="error-alert"
-      />
-    </div>
-
-    <!-- 大气样本表格 -->
-    <div class="table-section">
-      <el-row :gutter="10" align="middle">
-        <el-col :span="20">
-          <div class="title">大气样本重金属平均值</div>
-        </el-col>
-        <el-col :span="4" style="text-align: right;">
-          <div class="sample-count" v-if="atmoSampleValidSamples > 0">
-            有效样本:{{ atmoSampleValidSamples }}个
-          </div>
-        </el-col>
-      </el-row>
-      
-      <el-table 
-        v-if="atmoSampleTableData.length && !atmoSampleLoading && !atmoSampleError" 
-        :data="atmoSampleTableData" 
-        border 
-        style="width: 100%"
-      >
-        <el-table-column label="指标" prop="indicator" width="80" />
-        <el-table-column 
-          v-for="(metric, field) in atmoSampleMetrics" 
-          :key="field" 
-          :label="metric.label" 
-          :width="getColWidth(field)"
-        >
-          <template #default="scope">
-            {{ scope.row[field] }}
-          </template>
-        </el-table-column>
-      </el-table>
-      
-      <div v-else class="empty-state" v-if="!atmoSampleLoading && !atmoSampleError">
-        <el-empty description="大气样本数据为空,请刷新" />
-      </div>
-      <el-alert 
-        v-if="atmoSampleError" 
-        type="error" 
-        :description="atmoSampleError" 
-        show-icon 
-        class="error-alert"
-      />
-    </div>
-
-    <!-- 统一刷新按钮 -->
-    <div class="refresh-btn">
-      <el-button 
-        type="primary" 
-        @click="refreshAll" 
-        :loading="waterLoading || sectionLoading || atmoCompanyLoading || atmoSampleLoading"
-        icon="el-icon-refresh"
-      >
-        刷新所有数据
-      </el-button>
     </div>
   </div>
 </template>
 
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue';
-import axios from 'axios';
-
-// ====================== 通用工具类型 ======================
-type MetricsMap<T extends string> = { [K in T]: { label: string } };
-type TableRow<T extends string> = { indicator: string } & { [K in T]: number };
-
-// ====================== 灌溉水模块(严格类型) ======================
-const waterMetals = {
-  cr_concentration: { label: '铬含量(ug/L)' },
-  as_concentration: { label: '砷含量(ug/L)' },
-  cd_concentration: { label: '镉含量(ug/L)' },
-  hg_concentration: { label: '汞含量(ug/L)' },
-  pb_concentration: { label: '铅含量(ug/L)' },
-};
-type WaterMetalKey = keyof typeof waterMetals;
-type WaterRow = TableRow<WaterMetalKey>;
-
-const waterLoading = ref(false);
-const waterError = ref('');
-const waterData = ref<{ [K in WaterMetalKey]?: number | string }[]>([]);
-const waterTableData = ref<WaterRow[]>([]);
-const waterValidSamples = ref(0); // 有效样本数
-
-const calculateWaterAverage = () => {
-  const stats: Record<WaterMetalKey, { sum: number; count: number }> = {} as any;
-  (Object.keys(waterMetals) as WaterMetalKey[]).forEach(key => {
-    stats[key] = { sum: 0, count: 0 };
-  });
-
-  let validSamples = 0;
-  waterData.value.forEach(item => {
-    let hasValidMetric = false;
-    (Object.keys(waterMetals) as WaterMetalKey[]).forEach(key => {
-      const value = Number(item[key]);
-      if (!isNaN(value)) {
-        stats[key].sum += value;
-        stats[key].count += 1;
-        hasValidMetric = true;
-      }
-    });
-    if (hasValidMetric) validSamples++;
-  });
-
-  waterTableData.value = [(Object.keys(waterMetals) as WaterMetalKey[]).reduce((row, key) => {
-    row.indicator = '平均值';
-    row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
-    return row;
-  }, {} as WaterRow)];
-
-  waterValidSamples.value = validSamples;
-};
-
-const fetchWaterData = async () => {
-  try {
-    waterLoading.value = true;
-    waterError.value = '';
-    const res = await axios.get(
-      'http://localhost:8000/api/vector/export/all?table_name=water_sampling_data'
-    );
-    waterData.value = res.data.features.map((f: { properties: any }) => f.properties);
-    calculateWaterAverage();
-  } catch (err: any) {
-    waterError.value = err.message;
-    waterTableData.value = [];
-  } finally {
-    waterLoading.value = false;
-  }
-};
-
-// ====================== 断面模块(严格类型) ======================
-const sectionMetals = { cd_concentration: { label: '镉含量(ug/L)' } };
-type SectionMetalKey = keyof typeof sectionMetals;
-type SectionRow = TableRow<SectionMetalKey>;
-
-const sectionLoading = ref(false);
-const sectionError = ref('');
-const sectionData = ref<{ [K in SectionMetalKey]?: number | string }[]>([]);
-const sectionTableData = ref<SectionRow[]>([]);
-const sectionValidSamples = ref(0); // 有效样本数
-
-const calculateSectionAverage = () => {
-  const stats: Record<SectionMetalKey, { sum: number; count: number }> = {} as any;
-  (Object.keys(sectionMetals) as SectionMetalKey[]).forEach(key => {
-    stats[key] = { sum: 0, count: 0 };
-  });
-
-  let validSamples = 0;
-  sectionData.value.forEach(item => {
-    let hasValidMetric = false;
-    (Object.keys(sectionMetals) as SectionMetalKey[]).forEach(key => {
-      const value = Number(item[key]);
-      if (!isNaN(value)) {
-        stats[key].sum += value;
-        stats[key].count += 1;
-        hasValidMetric = true;
-      }
-    });
-    if (hasValidMetric) validSamples++;
-  });
-
-  sectionTableData.value = [(Object.keys(sectionMetals) as SectionMetalKey[]).reduce((row, key) => {
-    row.indicator = '平均值';
-    row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
-    return row;
-  }, {} as SectionRow)];
-
-  sectionValidSamples.value = validSamples;
-};
-
-const fetchSectionData = async () => {
-  try {
-    sectionLoading.value = true;
-    sectionError.value = '';
-    const res = await fetch('http://localhost:8000/api/vector/export/all?table_name=cross_section');
-    if (!res.ok) throw new Error(`HTTP 错误:${res.status}`);
-    let rawText = await res.text();
-    rawText = rawText.replace(/:\s*NaN/g, ': null'); // 修复 NaN
-    const geoJSON = JSON.parse(rawText);
-    sectionData.value = geoJSON.features.map((f: { properties: any }) => f.properties);
-    calculateSectionAverage();
-  } catch (err: any) {
-    sectionError.value = err.message;
-    sectionTableData.value = [];
-  } finally {
-    sectionLoading.value = false;
-  }
-};
-
-// ====================== 大气企业模块(严格类型) ======================
-const atmoCompanyMetrics = {
-  particulate_emission: { label: '颗粒物排放量' },
-};
-type AtmoCompanyKey = keyof typeof atmoCompanyMetrics;
-type AtmoCompanyRow = TableRow<AtmoCompanyKey>;
-
-const atmoCompanyLoading = ref(false);
-const atmoCompanyError = ref('');
-const atmoCompanyData = ref<{ [K in AtmoCompanyKey]?: number | string }[]>([]);
-const atmoCompanyTableData = ref<AtmoCompanyRow[]>([]);
-const atmoCompanyValidSamples = ref(0); // 有效样本数
-
-const calculateAtmoCompanyAverage = () => {
-  const stats: Record<AtmoCompanyKey, { sum: number; count: number }> = {} as any;
-  (Object.keys(atmoCompanyMetrics) as AtmoCompanyKey[]).forEach(key => {
-    stats[key] = { sum: 0, count: 0 };
-  });
-
-  let validSamples = 0;
-  atmoCompanyData.value.forEach(item => {
-    let hasValidMetric = false;
-    (Object.keys(atmoCompanyMetrics) as AtmoCompanyKey[]).forEach(key => {
-      const value = Number(item[key]);
-      if (!isNaN(value)) {
-        stats[key].sum += value;
-        stats[key].count += 1;
-        hasValidMetric = true;
-      }
-    });
-    if (hasValidMetric) validSamples++;
-  });
-
-  atmoCompanyTableData.value = [(Object.keys(atmoCompanyMetrics) as AtmoCompanyKey[]).reduce((row, key) => {
-    row.indicator = '平均值';
-    row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
-    return row;
-  }, {} as AtmoCompanyRow)];
-
-  atmoCompanyValidSamples.value = validSamples;
-};
-
-const fetchAtmoCompanyData = async () => {
-  try {
-    atmoCompanyLoading.value = true;
-    atmoCompanyError.value = '';
-    const res = await fetch('http://localhost:8000/api/vector/export/all?table_name=atmo_company');
-    if (!res.ok) throw new Error(`HTTP 错误:${res.status}`);
-    let rawText = await res.text();
-    rawText = rawText.replace(/:\s*NaN/g, ': null'); // 修复 NaN
-    const geoJSON = JSON.parse(rawText);
-    atmoCompanyData.value = geoJSON.features.map((f: { properties: any }) => f.properties);
-    calculateAtmoCompanyAverage();
-  } catch (err: any) {
-    atmoCompanyError.value = err.message;
-    atmoCompanyTableData.value = [];
-  } finally {
-    atmoCompanyLoading.value = false;
+<script>
+import AtmcompanyStatics from '@/components/detectionStatistics/atmcompanyStatics.vue';
+import AtmsampleStatistics from '@/components/detectionStatistics/atmsampleStatistics.vue';
+import CrosscetionStatistics from '@/components/detectionStatistics/crosscetionStatistics.vue';
+import Irrigationstatistics from '@/components/detectionStatistics/irrigationstatistics.vue';
+
+export default {
+  components: {
+    AtmcompanyStatics,
+    AtmsampleStatistics,
+    CrosscetionStatistics,
+    Irrigationstatistics
   }
-};
-
-// ====================== 大气样本模块(严格类型) ======================
-const weightColumns = [
-  { key: 'Cr_particulate', label: 'Cr mg/kg' },
-  { key: 'As_particulate', label: 'As mg/kg' },
-  { key: 'Cd_particulate', label: 'Cd mg/kg' },
-  { key: 'Hg_particulate', label: 'Hg mg/kg' },
-  { key: 'Pb_particulate', label: 'Pb mg/kg' },
-  { key: 'particle_weight', label: '颗粒物重量 mg' },
-];
-
-const atmoSampleMetrics = weightColumns.reduce((obj, col) => {
-  obj[col.key] = { label: col.label };
-  return obj;
-}, {} as Record<string, { label: string }>);
-type AtmoSampleKey = keyof typeof atmoSampleMetrics;
-type AtmoSampleRow = TableRow<AtmoSampleKey>;
-
-const atmoSampleLoading = ref(false);
-const atmoSampleError = ref('');
-const atmoSampleData = ref<{ [K in AtmoSampleKey]?: number | string }[]>([]);
-const atmoSampleTableData = ref<AtmoSampleRow[]>([]);
-const atmoSampleValidSamples = ref(0); // 有效样本数
-
-const getColWidth = computed(() => (field: AtmoSampleKey) => {
-  const col = weightColumns.find(c => c.key === field);
-  return col ;
-});
-
-const calculateAtmoSampleAverage = () => {
-  const stats: Record<AtmoSampleKey, { sum: number; count: number }> = {} as any;
-  (Object.keys(atmoSampleMetrics) as AtmoSampleKey[]).forEach(key => {
-    stats[key] = { sum: 0, count: 0 };
-  });
-
-  let validSamples = 0;
-  atmoSampleData.value.forEach(item => {
-    let hasValidMetric = false;
-    (Object.keys(atmoSampleMetrics) as AtmoSampleKey[]).forEach(key => {
-      const value = Number(item[key]);
-      if (!isNaN(value)) {
-        stats[key].sum += value;
-        stats[key].count += 1;
-        hasValidMetric = true;
-      }
-    });
-    if (hasValidMetric) validSamples++;
-  });
-
-  atmoSampleTableData.value = [(Object.keys(atmoSampleMetrics) as AtmoSampleKey[]).reduce((row, key) => {
-    row.indicator = '平均值';
-    row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
-    return row;
-  }, {} as AtmoSampleRow)];
-
-  atmoSampleValidSamples.value = validSamples;
-};
-
-const fetchAtmoSampleData = async () => {
-  try {
-    atmoSampleLoading.value = true;
-    atmoSampleError.value = '';
-    const res = await fetch('http://localhost:8000/api/vector/export/all?table_name=Atmo_sample_data');
-    if (!res.ok) throw new Error(`HTTP 错误:${res.status}`);
-    let rawText = await res.text();
-    rawText = rawText.replace(/:\s*NaN/g, ': null'); // 修复 NaN
-    const geoJSON = JSON.parse(rawText);
-    atmoSampleData.value = geoJSON.features.map((f: { properties: any }) => f.properties);
-    calculateAtmoSampleAverage();
-  } catch (err: any) {
-    atmoSampleError.value = err.message;
-    atmoSampleTableData.value = [];
-  } finally {
-    atmoSampleLoading.value = false;
-  }
-};
-
-// ====================== 统一刷新 ======================
-const refreshAll = () => {
-  fetchWaterData();
-  fetchSectionData();
-  fetchAtmoCompanyData();
-  fetchAtmoSampleData();
-};
-
-onMounted(() => {
-  refreshAll();
-});
+}
 </script>
-
 <style scoped>
-.metal-tables-container {
-  padding: 20px;
-  background: #fff;
-  border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
-}
-
-.table-section {
-  margin-bottom: 30px;
-  padding-bottom: 20px;
-  border-bottom: 1px solid #eee;
-}
-
-.table-section:last-child {
-  border-bottom: none;
+.scale-wrapper {
+  transform: scale(0.9);
+  transform-origin: top left;
+  width: fit-content;
+  margin-left: 0;
   margin-bottom: 0;
-  padding-bottom: 0;
+  overflow: hidden;
 }
 
-.title {
-  font-size: 20px;
-  font-weight: 500;
-  margin-bottom: 8px;
-  color: #333;
+/* 核心:改为3列网格布局 */
+.layout-container {
+  display: grid;
+  grid-template-columns: 1fr 1fr; /* 三列等宽 */
+  gap: 4px; /* 列之间的间距 */
+  padding: 0 0px;
+  box-sizing: border-box;
+  justify-items: start;
+  align-items: start;    /* 垂直对齐顶部,避免拉伸变形 */
 }
 
-.sample-count {
-  font-size: 14px;
-  color: #666;
-  font-style: italic;
+/* 每列内部垂直排列组件 */
+.column {
+  display: flex;
+  flex-direction: column;
+  gap: 4px; /* 列内两个组件的垂直间距 */
 }
 
-.error-alert {
-  margin: 16px 0;
+/* 组件容器基础样式 */
+.component-box {
+  padding: 4px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  overflow: hidden;
+  position: relative;
+  flex-shrink: 0; /* 避免组件被压缩 */
 }
 
-.empty-state {
-  padding: 40px 0;
-  text-align: center;
+/* 第一列组件样式 */
+.irrigation-box { 
+  background: #f9fafc; 
+  height: 310px;
+  width: 555px;
 }
-
-.refresh-btn {
-  text-align: center;
-  margin-top: 10px;
+.section-box { 
+  background: #f9fafc; 
+  height: 310px;
+  width: 555px;
 }
 
-/* 关键样式:自适应列宽 + 内容不换行 */
-.el-table {
-  table-layout: auto; /* 列宽自适应内容 */
-  min-width: 100%;    /* 确保容器宽度不足时触发滚动 */
+/* 第二列组件样式 */
+.metal-box { 
+  background: #f3f4f6; 
+  height: 310px;
+  width: 555px;
 }
-
-/* 覆盖Element UI的单元格样式(提高优先级) */
-.el-table td,
-.el-table th {
-  white-space: nowrap !important;   /* 强制不换行 */
-  overflow: hidden !important;      /* 溢出隐藏 */
-  text-overflow: ellipsis !important; /* 溢出省略号 */
-  word-break: normal !important;    /* 禁止自动断词 */
+.company-box { 
+  background: #f3f4f6; 
+  height: 310px;
+  width: 555px;
 }
 
-.el-table__cell {
-  white-space: nowrap !important; /* 强制不换行 */
-  overflow: hidden;               /* 溢出隐藏 */
-  text-overflow: ellipsis;        /* 溢出显示省略号 */
-}
-
-.table-section {
-  overflow-x: auto; /* 横向滚动 */
-}
-</style>
+</style>

+ 0 - 23
src/views/User/dataStatistics/FarmlandPollutionStatistics.vue

@@ -1,23 +0,0 @@
-<template>
-  <div class="">
-    
-  </div>
-</template>
-
-<script>
-export default {
-  name: '',
-  data() {
-    return {
-      
-    };
-  },
-  methods: {
-    
-  }
-};
-</script>
-
-<style scoped>
-  
-</style>

+ 111 - 0
src/views/User/dataStatistics/SoilCdStatistics.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="scale-wrapper">
+    <div class="layout-container">
+      <!-- 第一列:有效态Cd的总结性数据 -->
+      <div class="column column-1">
+        <div class="component-box effCd">
+          <EffcdStatistics /> 
+        </div>     
+      </div>
+
+      <!-- 第二列:作物态Cd的总结性数据 -->
+      <div class="column column-2">
+        <div class="component-box cropCd">
+          <CropcdStatictics/> 
+        </div>
+      </div>
+
+      <!-- 第三列:暂空(预留) -->
+      <div class="column column-3">
+        <div class="component-box fluxcd">
+          <FluxcdStatictics/>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import CropcdStatictics from '@/components/soilcdStatistics/cropcdStatictics.vue';
+import EffcdStatistics from '@/components/soilcdStatistics/effcdStatistics.vue';
+import FluxcdStatictics from '@/components/soilcdStatistics/fluxcdStatictics.vue';
+
+export default {
+  components: {
+    CropcdStatictics,
+    EffcdStatistics,
+    FluxcdStatictics,
+  }
+}
+</script>
+<style scoped>
+.scale-wrapper {
+  transform: scale(0.9);
+  transform-origin: top left;
+  width: fit-content;
+  margin-left: 0;
+  margin-bottom: 0;
+  overflow: hidden;
+}
+
+/* 核心:改为3列网格布局 */
+.layout-container {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr; /* 三列等宽 */
+  gap: 4px; /* 列之间的间距 */
+  padding: 0 0px;
+  box-sizing: border-box;
+  justify-items: start;
+  align-items: start;    /* 垂直对齐顶部,避免拉伸变形 */
+}
+
+/* 每列内部垂直排列组件 */
+.column {
+  display: flex;
+  flex-direction: column;
+  gap: 4px; /* 列内两个组件的垂直间距 */
+}
+
+/* 组件容器基础样式 */
+.component-box {
+  padding: 4px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  overflow: hidden;
+  position: relative;
+  flex-shrink: 0; /* 避免组件被压缩 */
+}
+
+/* 第一列组件样式 */
+.effCd { 
+  background: #f9fafc; 
+  width: 395px;
+}
+
+/* 第二列组件样式 */
+.cropCd { 
+  background: #f3f4f6; 
+  width: 395px;
+}
+
+
+/* 第三列占位样式 */
+.fluxcd {
+  background: #f3f4f6; 
+  width: 395px;
+}
+.empty-placeholder {
+  width: 100%;
+  height: 100%; /* 占满第三列高度 */
+  border: 1px dashed #ccc; /* 虚线边框表示预留 */
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #fafafa;
+}
+.empty-text {
+  color: #999;
+  font-size: 14px;
+}
+</style>

Some files were not shown because too many files changed in this diff