فهرست منبع

Merge remote-tracking branch 'origin/lili' into ding

# Conflicts:
#	components.d.ts
#	package-lock.json
#	src/components/layout/AppLayout.vue
#	src/components/layout/menuItems.ts
#	src/locales/en.json
#	src/locales/zh.json
#	src/utils/request.ts
#	src/views/User/HmOutFlux/atmosDeposition/airSampleData.vue
#	src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue
#	src/views/User/HmOutFlux/irrigationWater/crossSection.vue
#	src/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue
#	src/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue
yangtaodemon 7 ماه پیش
والد
کامیت
fb79902600
33فایلهای تغییر یافته به همراه4020 افزوده شده و 389 حذف شده
  1. 3 5
      .eslintrc-auto-import.json
  2. 3 1
      .hintrc
  3. 7 0
      components.d.ts
  4. 2 2
      myenv/Lib/site-packages/pip/_vendor/pygments/formatters/img.py
  5. 6 6
      myenv/Lib/site-packages/setuptools/_distutils/command/bdist_rpm.py
  6. 257 241
      package-lock.json
  7. 5 3
      package.json
  8. 178 0
      scripts/extract-i18n.js
  9. 4 7
      src/App.vue
  10. 410 0
      src/components/detectionStatistics/atmcompanyStatics.vue
  11. 323 0
      src/components/detectionStatistics/atmsampleStatistics.vue
  12. 398 0
      src/components/detectionStatistics/crosscetionStatistics.vue
  13. 303 0
      src/components/detectionStatistics/irrigationstatistics.vue
  14. 3 3
      src/components/layout/AppAside.vue
  15. 49 49
      src/components/layout/menuItems.ts
  16. 491 0
      src/components/soilcdStatistics/cropcdStatictics.vue
  17. 440 0
      src/components/soilcdStatistics/effcdStatistics.vue
  18. 433 0
      src/components/soilcdStatistics/fluxcdStatictics.vue
  19. 6 0
      src/i18n.d.ts
  20. 166 0
      src/locales/en.json
  21. 171 0
      src/locales/zh.json
  22. 3 3
      src/router/index.ts
  23. 31 0
      src/views/User/HmOutFlux/atmosDeposition/airSampleData.vue
  24. 21 0
      src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue
  25. 14 0
      src/views/User/HmOutFlux/irrigationWater/crossSection.vue
  26. 23 0
      src/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue
  27. 49 0
      src/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue
  28. 95 44
      src/views/User/dataStatistics/DetectionStatistics.vue
  29. 0 23
      src/views/User/dataStatistics/FarmlandPollutionStatistics.vue
  30. 111 0
      src/views/User/dataStatistics/SoilCdStatistics.vue
  31. 4 1
      tsconfig.json
  32. 10 1
      vite.config.ts
  33. 1 0
      语言切换.txt

+ 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
   }
 }

+ 3 - 1
.hintrc

@@ -9,6 +9,8 @@
       {
         "html-has-lang": "off"
       }
-    ]
+    ],
+    "typescript-config/consistent-casing": "off",
+    "typescript-config/strict": "off"
   }
 }

+ 7 - 0
components.d.ts

@@ -16,13 +16,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']
@@ -59,6 +64,7 @@ declare module 'vue' {
     ElTag: typeof import('element-plus/es')['ElTag']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElUpload: typeof import('element-plus/es')['ElUpload']
+    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']
@@ -66,6 +72,7 @@ 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']
     Riverwaterassay: typeof import('./src/components/irrpollution/riverwaterassay.vue')['default']

+ 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):

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 257 - 241
package-lock.json


+ 5 - 3
package.json

@@ -13,17 +13,18 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
+    "@intlify/unplugin-vue-i18n": "^6.0.8",
     "@turf/turf": "^7.2.0",
     "@types/d3": "^7.4.3",
     "@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",
@@ -32,13 +33,14 @@
     "leaflet": "^1.9.4",
     "leaflet-compass": "^1.5.6",
     "leaflet.chinatmsproviders": "^3.0.6",
+    "lodash.kebabcase": "^4.1.1",
     "pinia": "^2.3.0",
     "proj4": "^2.19.10",
     "proj4leaflet": "^1.0.2",
     "sass": "^1.84.0",
     "vue": "^3.5.13",
     "vue-echarts": "^7.0.3",
-    "vue-i18n": "^11.1.3",
+    "vue-i18n": "^9.8.0",
     "vue-leaflet": "^0.1.0",
     "vue-router": "^4.5.0",
     "xlsx": "^0.18.5"

+ 178 - 0
scripts/extract-i18n.js

@@ -0,0 +1,178 @@
+//提取中文到zh.json和en.json文件中,方便网页的中英文切换
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+// 基础路径处理
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const args = process.argv.slice(2);
+const targetPath = args[0] ? path.resolve(__dirname, '../', args[0]) : null;
+
+// 语言包路径配置
+const localesDir = path.resolve(__dirname, '../src/locales');
+const zhPath = path.join(localesDir, 'zh.json');
+const enPath = path.join(localesDir, 'en.json');
+
+// 初始化语言包数据
+let zh = {};
+let en = {};
+
+// 读取已有语言包(保留已有翻译)
+try {
+  if (fs.existsSync(zhPath)) {
+    zh = JSON.parse(fs.readFileSync(zhPath, 'utf-8'));
+  }
+  if (fs.existsSync(enPath)) {
+    en = JSON.parse(fs.readFileSync(enPath, 'utf-8'));
+  }
+} catch (e) {
+  console.error('⚠️ 语言包解析错误,将使用空对象初始化', e);
+  zh = {};
+  en = {};
+}
+
+/**
+ * 递归设置嵌套对象的值(处理键路径)
+ * @param {Object} obj - 目标对象
+ * @param {string} keyPath - 键路径(如 "a.b.c")
+ * @param {string} value - 要设置的值
+ */
+function setNestedValue(obj, keyPath, value) {
+  const keys = keyPath.split('.');
+  let current = obj;
+
+  for (let i = 0; i < keys.length - 1; i++) {
+    const key = keys[i];
+    // 处理键名冲突(已存在字符串值时跳过)
+    if (typeof current[key] === 'string') {
+      console.warn(`⚠️ 键名冲突:"${key}" 已作为字符串存在,跳过路径 "${keyPath}"`);
+      return;
+    }
+    // 初始化不存在的键为对象
+    if (!current[key] || typeof current[key] !== 'object') {
+      current[key] = {};
+    }
+    current = current[key];
+  }
+
+  const lastKey = keys[keys.length - 1];
+  // 仅添加新键,不覆盖已有值
+  if (current[lastKey] === undefined) {
+    current[lastKey] = value;
+    console.log(`✅ 新增翻译:${keyPath} → ${value}`);
+  }
+}
+
+/**
+ * 扫描目标路径下的所有Vue和TS文件
+ * @param {string[]} fileList - 文件列表容器
+ * @returns {string[]} 扫描到的文件路径列表
+ */
+function scanFiles(fileList = []) {
+  if (!targetPath) {
+    // 未指定路径时默认扫描src目录
+    const scanRoot = path.resolve(__dirname, '../src');
+    scanDir(scanRoot, fileList);
+  } else {
+    const stats = fs.statSync(targetPath);
+    if (stats.isDirectory()) {
+      scanDir(targetPath, fileList);
+    } else if (targetPath.endsWith('.vue') || targetPath.endsWith('.ts')) {
+      fileList.push(targetPath);
+    } else {
+      console.warn(`⚠️ 跳过非Vue/TS文件:${targetPath}`);
+    }
+  }
+  return fileList;
+}
+
+/**
+ * 递归扫描目录中的文件
+ * @param {string} dir - 目录路径
+ * @param {string[]} fileList - 文件列表容器
+ */
+function scanDir(dir, fileList) {
+  const files = fs.readdirSync(dir);
+  for (const file of files) {
+    const fullPath = path.join(dir, file);
+    // 跳过无关目录
+    if (file.includes('node_modules') || file.includes('dist') || file.includes('.git')) {
+      continue;
+    }
+    const stats = fs.statSync(fullPath);
+    if (stats.isDirectory()) {
+      scanDir(fullPath, fileList);
+    } else if (fullPath.endsWith('.vue') || fullPath.endsWith('.ts')) {
+      fileList.push(fullPath);
+    }
+  }
+}
+
+/**
+ * 从文件内容中提取i18n标记文本
+ * @param {string} content - 文件内容
+ * @param {string} filePath - 文件路径(用于错误提示)
+ */
+function extractI18nMarkers(content, filePath) {
+  // 匹配格式:<!--i18n:key-->中文文本(支持换行和常见标点)
+  const regex = /<!--i18n:(\S+)-->([\u4e00-\u9fa5\w\s,.,。;;!!??::()()]+?)(?=\r?\n|['"<]|$)/g;
+  let match;
+
+  while ((match = regex.exec(content)) !== null) {
+    const key = match[1].trim();
+    const text = match[2].trim().replace(/\s+/g, ' '); // 清理多余空格
+
+    if (!key) {
+      console.warn(`⚠️ 缺少键名(文件:${filePath}):${match[0]}`);
+      continue;
+    }
+    if (!text) {
+      console.warn(`⚠️ 缺少文本内容(文件:${filePath},键:${key})`);
+      continue;
+    }
+
+    // 写入语言包
+    setNestedValue(zh, key, text);
+    setNestedValue(en, key, en[key] || ''); // 英文保留已有翻译,否则留空
+  }
+}
+
+/**
+ * 执行提取流程
+ */
+function runExtraction() {
+  const files = scanFiles();
+  
+  if (files.length === 0) {
+    console.log('⚠️ 未找到任何需要处理的Vue/TS文件');
+    return;
+  }
+
+  // 处理所有扫描到的文件
+  files.forEach(file => {
+    try {
+      const content = fs.readFileSync(file, 'utf-8');
+      extractI18nMarkers(content, file);
+    } catch (e) {
+      console.error(`⚠️ 处理文件失败:${file}`, e);
+    }
+  });
+
+  // 确保语言包目录存在
+  if (!fs.existsSync(localesDir)) {
+    fs.mkdirSync(localesDir, { recursive: true });
+  }
+
+  // 写入语言包文件
+  fs.writeFileSync(zhPath, JSON.stringify(zh, null, 2), 'utf-8');
+  fs.writeFileSync(enPath, JSON.stringify(en, null, 2), 'utf-8');
+
+  console.log(`\n✅ 提取完成!共处理 ${files.length} 个文件`);
+  console.log(`📄 中文语言包:${zhPath}`);
+  console.log(`📄 英文语言包:${enPath}`);
+}
+
+// 启动提取
+runExtraction();
+    

+ 4 - 7
src/App.vue

@@ -1,16 +1,13 @@
 <script setup lang='ts'>
 import { RouterView } from "vue-router"
 import request from './utils/request';
-request({
-  url: '/table',
-  method: 'get'
-}).then(res => {
-  console.log(res)
-})
+import ReducedataStatistics from "./components/detectionStatistics/reducedataStatistics.vue";
+
 </script>
 
 <template>
   <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>

+ 3 - 3
src/components/layout/AppAside.vue

@@ -11,7 +11,7 @@
         <el-sub-menu v-if="item.children" :index="item.index">
           <template #title>
             <el-icon><component :is="item.icon" /></el-icon>
-            <span>{{ item.label }}</span>
+            <span>{{$t(item.label)}}</span>
           </template>
           <el-menu-item
             v-for="child in item.children"
@@ -20,7 +20,7 @@
             @click="handleMenuClick(child.index)"
           >
             <el-icon><component :is="child.icon" /></el-icon>
-            <span>{{ child.label }}</span>
+            <span>{{ $t(child.label) }}</span>
           </el-menu-item>
         </el-sub-menu>
 
@@ -30,7 +30,7 @@
           @click="handleMenuClick(item.index)"
         >
           <el-icon><component :is="item.icon" /></el-icon>
-          <span>{{ item.label }}</span>
+          <span>{{ $t(item.label) }}</span>
         </el-menu-item>
       </template>
     </el-menu>

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

@@ -31,61 +31,61 @@ import {
   export const menuItems: MenuItem[] = [
     {
       index: '/shuJuKanBan',
-      label: '数据看板',
+      label: 'shuJuKanBan.Title',//<!--i18n:shuJuKanBan.Title-->数据看板
       icon: Monitor,
      tab: 'shuJuKanBan'
     },
     {
       index: '/SoilPro',
-      label: '软件简介',
+      label: 'SoilPro.Title',//<!--i18n:SoilPro.Title-->软件简介
       icon: InfoFilled,
       tab: 'introduction'
     },
     {
       index: '/Overview',
-      label: '项目简介',
+      label: 'Overview.Title',//<!--i18n:Overview.Title-->项目简介
       icon: Collection,
       tab: 'introduction'
     },
     {
       index: '/ResearchFindings',
-      label: '研究成果',
+      label: 'ResearchFindings.Title',//<!--i18n:ResearchFindings.Title-->研究成果
       icon: Histogram,
       tab: 'introduction'
     },
     {
       index: '/Unit',
-      label: '团队信息',
+      label: 'Unit.Title',//<!--i18n:Unit.Title-->团队信息
       icon: HelpFilled,
       tab: 'introduction'
     },
     {
       index: 'irrigationWater',
-      label: '灌溉水',
+      label: 'irrigationwater.Title',//灌溉水
       icon: Watermelon,
       tab: 'HmOutFlux',
       children: [
         {
           index: '/samplingMethodDevice1',
-          label: '采样方法和装置',
+          label: 'irrigationwater.irrigationwaterMethodsTitle',
           icon: Sunny,
           tab: 'HmOutFlux'
         },
         {
           index: '/irriSampleData',
-          label: '灌溉水采样数据',
+          label: 'irrigationwater.pointTitle',
           icon: Coin,
           tab: 'HmOutFlux'
         },
         {
           index: '/csSampleData',
-          label: '断面采样数据',
+          label: 'irrigationwater.crosssectionTitle',
           icon: Cloudy,
           tab: 'HmOutFlux'
         },
         {
           index: '/irriInputFlux',
-          label: '灌溉水输入通量',
+          label: 'irrigationwater.InputfluxTitle',
           icon: Cloudy,
           tab: 'HmOutFlux'
         }
@@ -93,19 +93,19 @@ import {
     },
     {
       index: 'inputFlux',
-      label: '农产品投入',
+      label: 'agriInput.Title',//农产品投入
       icon: Watermelon,
       tab: 'HmOutFlux',
       children: [
         {
           index: '/farmInputSamplingDesc',
-          label: '采样说明',
+          label: 'agriInput.farmInputSamplingDescTitle',//采样说明
           icon: Sunny,
           tab: 'HmOutFlux'
         },
         {
           index: '/prodInputFlux',
-          label: '农产品输入通量',
+          label: 'agriInput.prodInputFluxTitle',//农产品输入通量
           icon: Coin,
           tab: 'HmOutFlux'
         },
@@ -113,31 +113,31 @@ import {
     },
     {
       index: 'atmosDeposition',
-      label: '大气干湿沉降',
+      label: 'atmosDeposition.Title',//大气干湿沉降
       icon: Watermelon,
       tab: 'HmOutFlux',
       children: [
         {
           index: '/AtmosDepositionSamplingDesc',
-          label: '采样说明',
+          label: 'atmosDeposition.AtmosDepositionSamplingDescTitle',//采样说明
           icon: Sunny,
           tab: 'HmOutFlux'
         },
         {
           index: '/heavyMetalEnterprise',
-          label: '涉重企业',
+          label: 'atmosDeposition.heavyMetalEnterpriseTitle',//涉重企业
           icon: Coin,
           tab: 'HmOutFlux'
         },
         {
           index: '/airSampleData',
-          label: '大气采样数据',
+          label: 'atmosDeposition.airSampleDataTitle',//大气采样数据
           icon: Sunny,
           tab: 'HmOutFlux'
         },
         {
           index: '/airInputFlux',
-          label: '大气输入通量',
+          label: 'atmosDeposition.airInputFluxTitle',//大气输入通量
           icon: Coin,
           tab: 'HmOutFlux'
         },
@@ -151,19 +151,19 @@ import {
     },
     {
       index: 'grainRemoval',
-      label: '籽粒移除',
+      label: 'grainRemoval.Title',//<!--i18n:grainRemoval.Title-->籽粒移除
       icon: WindPower,
       tab: 'hmInFlux',
       children: [
         {
           index: '/samplingDesc1',
-          label: '采样说明',
+          label: 'grainRemoval.samplingDesc1',//<!--i18n:grainRemoval.samplingDesc1-->采样说明
           icon: Watermelon,
           tab: 'hmInFlux'
         },
         {
           index: '/grainRemovalInputFlux',
-          label: '籽粒移除输出通量',
+          label: 'grainRemoval.grainRemovalInputFlux',//<!--i18n:grainRemoval.grainRemovalInputFlux-->籽粒移除输出通量
           icon: List,
           tab: 'hmInFlux'
         }
@@ -171,19 +171,19 @@ import {
     },
     {
       index: 'strawRemoval',
-      label: '秸秆移除',
+      label: 'strawRemoval.Title',//<!--i18n:strawRemoval.Title-->秸秆移除
       icon: WindPower,
       tab: 'hmInFlux',
       children: [
         {
           index: '/samplingDesc2',
-          label: '采样说明',
+          label: 'strawRemoval.samplingDesc2',//<!--i18n:strawRemoval.samplingDesc2-->采样说明
           icon: Watermelon,
           tab: 'hmInFlux'
         },
         {
           index: '/strawRemovalInputFlux',
-          label: '秸秆移除输出通量',
+          label: 'strawRemoval.strawRemovalInputFlux',//<!--i18n:strawRemoval.strawRemovalInputFlux-->秸秆移除输出通量
           icon: List,
           tab: 'hmInFlux'
         }
@@ -191,19 +191,19 @@ import {
     },
     {
       index: 'subsurfaceLeakage',
-      label: '地下渗漏',
+      label: 'subsurfaceLeakage.Title',//<!--i18n:subsurfaceLeakage.Title-->地下渗漏
       icon: WindPower,
       tab: 'hmInFlux',
       children: [
         {
           index: '/samplingDesc3',
-          label: '采样说明',
+          label: 'subsurfaceLeakage.samplingDesc3',//<!--i18n:subsurfaceLeakage.samplingDesc3-->采样说明
           icon: Watermelon,
           tab: 'hmInFlux'
         },
         {
           index: '/subsurfaceLeakageInputFlux',
-          label: '地下渗漏输入通量',
+          label: 'subsurfaceLeakage.subsurfaceLeakageInputFlux',//<!--i18n:subsurfaceLeakage.subsurfaceLeakageInputFlux-->地下渗漏输入通量
           icon: List,
           tab: 'hmInFlux'
         }
@@ -211,19 +211,19 @@ import {
     },
     {
       index: 'surfaceRunoff',
-      label: '地表径流',
+      label: 'surfaceRunoff.Title',//<!--i18n:surfaceRunoff.Title-->地表径流
       icon: WindPower,
       tab: 'hmInFlux',
       children: [
         {
           index: '/samplingDesc4',
-          label: '采样说明',
+          label: 'surfaceRunoff.samplingDesc4',//<!--i18n:surfaceRunoff.samplingDesc4-->采样说明
           icon: Watermelon,
           tab: 'hmInFlux'
         },
         {
           index: '/surfaceRunoffInputFlux',
-          label: '地表径流输入通量',
+          label: 'surfaceRunoff.surfaceRunoffInputFlux',//<!--i18n:surfaceRunoff.surfaceRunoffInputFlux-->地表径流输入通量
           icon: List,
           tab: 'hmInFlux'
         }
@@ -237,61 +237,61 @@ import {
     },
     {
       index: '/mapView',
-      label: '地图展示',
+      label: 'mapView.Title',//<!--i18n:mapView.Title-->地图展示
       icon: Location,
       tab: 'mapView'
     },
     {
       index: '/netFlux',
-      label: '净通量',
+      label: 'netFlux.Title',//<!--i18n:netFlux.Title-->净通量
       icon: PieChart,
       tab: 'cadmiumPrediction'
     },
     {
       index: '/currentYearConcentration',
-      label: '当年浓度',
+      label: 'currentYearConcentration.Title',//<!--i18n:currentYearConcentration.Title-->当年浓度
       icon: PieChart,
       tab: 'cadmiumPrediction'
     },
     {
       index: '/EffectiveCadmiumPrediction',
-      label: '土壤镉有效态含量预测',
+      label: 'EffectiveCadmiumPrediction.Title',//<!--i18n:EffectiveCadmiumPrediction.Title-->土壤镉有效态含量预测
       icon: PieChart,
       tab: 'cadmiumPrediction'
     },
     {
       index: '/CropCadmiumPrediction',
-      label: '土壤镉作物态含量预测',
+      label: 'CropCadmiumPrediction.Title',//<!--i18n:CropCadmiumPrediction.Title-->土壤镉作物态含量预测
       icon: PieChart,
       tab: 'cadmiumPrediction'
     },
     {
       index: '/cropRiskAssessment',
-      label: '水稻镉污染风险',
+      label: 'cropRiskAssessment.Title',//<!--i18n:cropRiskAssessment.Title-->水稻镉污染风险
       icon: Compass,
       tab: 'cropRiskAssessment'
     },
     {
       index: '/farmlandQualityAssessment',
-      label: '韶关',
+      label: 'farmlandQualityAssessment.Title',//<!--i18n:farmlandQualityAssessment.Title-->韶关
       icon: DataLine,
       tab: 'farmlandQualityAssessment'
     },
     {
       index: '/acidModel',
-      label: '土壤反酸',
+      label: 'acidModel.Title',//<!--i18n:acidModel.Title-->土壤反酸
       icon: MagicStick,
       tab: 'soilAcidificationPrediction',
       children: [
         {
           index: '/Calculation',
-          label: '土壤反酸预测',
+          label: 'acidModel.CalculationTitle',//<!--i18n:acidModel.CalculationTitle-->土壤反酸预测
           icon: Sunny,
           tab: 'heavyMetalFluxCalculation'
         },
         {
           index: '/SoilAcidReductionIterativeEvolution',
-          label: '反酸模型迭代可视化',
+          label: 'acidModel.SoilAcidReductionIterativeEvolutionTitle',//<!--i18n:acidModel.SoilAcidReductionIterativeEvolutionTitle-->反酸模型迭代可视化
           icon: Coin,
           tab: 'heavyMetalFluxCalculation'
         }
@@ -299,19 +299,19 @@ import {
     },
     {
       index: '/neutralizationModel',
-      label: '土壤降酸',
+      label: 'neutralizationModel.Title',//<!--i18n:neutralizationModel.Title-->土壤降酸
       icon: MagicStick,
       tab: 'soilAcidificationPrediction',
       children: [
         {
           index: '/AcidNeutralizationModel',
-          label: '土壤降酸预测',
+          label: 'neutralizationModel.AcidNeutralizationModelTitle',//<!--i18n:neutralizationModel.AcidNeutralizationModelTitle-->土壤降酸预测
           icon: Sunny,
           tab: 'heavyMetalFluxCalculation'
         },
         {
           index: '/SoilAcidificationIterativeEvolution',
-          label: '土壤降酸预测',
+          label: 'neutralizationModel.SoilAcidificationIterativeEvolutionTitle',//<!--i18n:neutralizationModel.SoilAcidificationIterativeEvolutionTitle-->土壤降酸可视化
           icon: Coin,
           tab: 'heavyMetalFluxCalculation'
         }
@@ -319,37 +319,37 @@ import {
     },
     {
       index: '/TraditionalFarmingRisk',
-      label: '传统耕种习惯风险趋势',
+      label: 'TraditionalFarmingRisk.Title',//<!--i18n:TraditionalFarmingRisk.Title-->传统耕种习惯风险趋势
       icon: MenuIcon,
       tab: 'scenarioSimulation'
     },
     {
       index: '/HeavyMetalCadmiumControl',
-      label: '重金属镉污染治理',
+      label: 'HeavyMetalCadmiumControl.Title',//<!--i18n:HeavyMetalCadmiumControl.Title-->重金属镉污染治理
       icon: MenuIcon,
       tab: 'scenarioSimulation'
     },
     {
       index: '/SoilAcidificationControl',
-      label: '土壤酸化治理',
+      label: 'SoilAcidificationControl.Title',//<!--i18n:SoilAcidificationControl.Title-->土壤酸化治理
       icon: MenuIcon,
       tab: 'scenarioSimulation'
     },
     {
       index: '/DetectionStatistics',
-      label: '检测信息统计',
+      label: 'DetectionStatistics.Title',//<!--i18n:DetectionStatistics.Title-->检测信息统计
       icon: List,
       tab: 'dataStatistics'
     },
     {
       index: '/FarmlandPollutionStatistics',
-      label: '耕地污染信息统计',
+      label: 'FarmlandPollutionStatistics.Title',//<!--i18n:FarmlandPollutionStatistics.Title-->土壤镉含量统计
       icon: List,
       tab: 'dataStatistics'
     },
     {
       index: '/PlantingRiskStatistics',
-      label: '种植风险信息统计',
+      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>

+ 6 - 0
src/i18n.d.ts

@@ -0,0 +1,6 @@
+type Translation = (path: string, option?: Record<string, any>) => import('vue').ComputedRef<string>
+interface I18n {
+  locale: string
+  language: Record<string, any>
+  t: Translation
+}

+ 166 - 0
src/locales/en.json

@@ -40,5 +40,171 @@
     "requiredUsername": "Username is required",
     "requiredPassword": "Password is required",
     "requiredConfirmPassword": "Confirm password is required"
+  },
+  "irrigationwater": {
+    "Title": "irrigationwater",
+    "irrigationwaterMethodsTitle": "Sampling method and device",
+    "irrigationwaterMethods": {
+      "title1": "1. Sampling container and process",
+      "content1": "The sampling containers are all 500mL white polyethylene bottles, the sampling volume is 500mL, the sampling process is carried out under different weather conditions, the main weather conditions include cloudy, cloudy and light rain, the surrounding environment of the sampling point is mainly rivers, and only a few samples are collected in the canal or waterfall area.",
+      "photo1": "Figure 1-1 Sampling container",
+      "photo2": "Fig.1-2 Sampling site",
+      "photo3": "Fig.1-3 Irrigation water sampling equipment",
+      "title2": "2. Sample preservation and on-site conditions",
+      "content2": "In order to ensure the integrity of the samples and the accuracy of the data, the storage methods after sampling include refrigeration, protection from light, ensuring that the label is intact and taking effective shock absorption measures to avoid vibration and damage during transportation.",
+      "photo4": "Figure 2-1 Sampling site",
+      "photo5": "Figure 2-2 Sampling site",
+      "photo6": "Figure 2-3 Sampling site"
+    },
+    "pointTitle": "Irrigation water sampling data",
+    "point": {
+      "pointMapTitle": "Sampling point map",
+      "pointLineTitle": "The data list of sampling points",
+      "pointChartTitle": "The average concentration of heavy metals in various districts and counties of Shaoguan City"
+    },
+    "crosssectionTitle": "Cross-sectional sampling data",
+    "crosssection": {
+      "crosssectionMapTitle": "Section data map",
+      "crosssectionLineTitle": "Section data details",
+      "crosssectionRiverChartTitle": "Histogram of the average concentration of Cd in each river",
+      "crosssectionCityChartTitle": "Histogram of the average concentration of Cd in each district and county"
+    },
+    "InputfluxTitle": "Irrigation water input flux"
+  },
+  "atmosDeposition": {
+    "Title": "Atmospheric dry and wet deposition",
+    "AtmosDepositionSamplingDescTitle": "Sampling instructions",
+    "heavyMetalEnterpriseTitle": "Involved in heavy enterprises",
+    "heavyMetalEnterprise": {
+      "MapTitle": "The map of heavy enterprises",
+      "LineTitle": "The list of data of important enterprises",
+      "ChartTitle": "Average atmospheric particulate matter emissions of enterprises in various districts and counties(t/a)"
+    },
+    "airSampleDataTitle": "Atmospheric sampling data",
+    "airSampleData": {
+      "button1": "Sampling method:",
+      "button2": "Sampling by weight",
+      "button3": "Sampling by volume",
+      "MapTitle": "Sampling point map",
+      "LineTitle": "The data list of sampling points",
+      "ChartTitle": "Histogram of average atmospheric heavy metal pollution in each district and county"
+    },
+    "airInputFluxTitle": "Atmospheric input flux"
+  },
+  "agriInput": {
+    "Title": "Agricultural product input",
+    "farmInputSamplingDescTitle": "Sampling instructions",
+    "prodInputFluxTitle": "Agricultural input flux"
+  },
+  "Title": "Regional soil heavy metal pollution risk assessment",
+  "Menu": {
+    "dataManagement": "Data management",
+    "infoManagement": "Information management",
+    "modelManagement": "Model management and configuration",
+    "userManagement": "User management",
+    "shuJuKanBan": "Data dashboard",
+    "introduction": "Software Introduction",
+    "HmOutFlux": "Heavy metal input flux",
+    "hmInFlux": "Heavy metal output flux",
+    "mapView": "Map display",
+    "cadmiumPrediction": "Soil pollutant content prediction",
+    "cropRiskAssessment": "Crop risk assessment",
+    "farmlandQualityAssessment": "Cultivated land quality assessment",
+    "soilAcidificationPrediction": "Soil acidification prediction",
+    "scenarioSimulation": "Scenario simulation",
+    "dataStatistics": "Statistics"
+  },
+  "shuJuKanBan": {
+    "Title": "Data dashboard"
+  },
+  "SoilPro": {
+    "Title": "Software Introduction"
+  },
+  "Overview": {
+    "Title": "Project Introduction"
+  },
+  "ResearchFindings": {
+    "Title": "Research results"
+  },
+  "Unit": {
+    "Title": "Team information"
+  },
+  "grainRemoval": {
+    "Title": "Grain removal",
+    "samplingDesc1": "Sampling instructions",
+    "grainRemovalInputFlux": "Grain removal output flux"
+  },
+  "strawRemoval": {
+    "Title": "Straw removal",
+    "samplingDesc2": "Sampling instructions",
+    "strawRemovalInputFlux": "Straw removal output flux"
+  },
+  "subsurfaceLeakage": {
+    "Title": "Underground leakage",
+    "samplingDesc3": "Sampling instructions",
+    "subsurfaceLeakageInputFlux": "Underground seepage input flux"
+  },
+  "surfaceRunoff": {
+    "Title": "Surface runoff",
+    "samplingDesc4": "Sampling instructions",
+    "surfaceRunoffInputFlux": "Surface runoff input flux"
+  },
+  "mapView": {
+    "Title": "Map display"
+  },
+  "totalInputFlux": {
+    "Title": "Enter the total flux"
+  },
+  "totalOutputFlux": {
+    "Title": "Output total throughput"
+  },
+  "netFlux": {
+    "Title": "Net Flux"
+  },
+  "currentYearConcentration": {
+    "Title": "Concentration of the year"
+  },
+  "TotalCadmiumPrediction": {
+    "Title": "Prediction of total soil cadmium content"
+  },
+  "EffectiveCadmiumPrediction": {
+    "Title": "Prediction of available content of cadmium in soil"
+  },
+  "CropCadmiumPrediction": {
+    "Title": "Prediction of soil cadmium crop content"
+  },
+  "cropRiskAssessment": {
+    "Title": "Risk of cadmium contamination in rice"
+  },
+  "farmlandQualityAssessment": {
+    "Title": "Shaoguan"
+  },
+  "acidModel": {
+    "Title": "Soil acid reflux",
+    "CalculationTitle": "Soil acid reflux prediction",
+    "SoilAcidReductionIterativeEvolutionTitle": "Acid reflux model iteration visualization"
+  },
+  "neutralizationModel": {
+    "Title": "Soil acid reduction",
+    "AcidNeutralizationModelTitle": "Soil acid reduction prediction",
+    "SoilAcidificationIterativeEvolutionTitle": "Soil acidification evolves iteratively"
+  },
+  "TraditionalFarmingRisk": {
+    "Title": "Risk trends of traditional farming habits"
+  },
+  "HeavyMetalCadmiumControl": {
+    "Title": "Heavy metal cadmium pollution control"
+  },
+  "SoilAcidificationControl": {
+    "Title": "Soil acidification control"
+  },
+  "DetectionStatistics": {
+    "Title": "Detection information statistics"
+  },
+  "FarmlandPollutionStatistics": {
+    "Title": "Soil cadmium content statistics"
+  },
+  "PlantingRiskStatistics": {
+    "Title": "Crop risk assessment statistics"
   }
 }

+ 171 - 0
src/locales/zh.json

@@ -1,4 +1,8 @@
 {
+  "role":{
+    "admin":"管理员",
+    "user":"用户"
+  },
   "login": {
     "title": "登录",
     "username": "账号",
@@ -40,5 +44,172 @@
     "requiredUsername": "账号为必填项",
     "requiredPassword": "密码为必填项",
     "requiredConfirmPassword": "确认密码为必填项"
+  },
+  "irrigationwater": {
+    "Title": "灌溉水",
+    "irrigationwaterMethodsTitle": "采样方法和装置",
+    "irrigationwaterMethods": {
+      "title1": "1.采样容器与过程",
+      "content1": "采样容器均为500mL的白色聚乙烯瓶,采样体积均为500mL,采样过程在不同天气条件下进行,主要天气状况包括多云、阴天和小雨,采样点周边环境主要为河流,只有少数样品采集于水渠或瀑布区域。",
+      "photo1": "图1-1 采样容器",
+      "photo2": "图1-2 采样现场",
+      "photo3": "图1-3 灌溉水采样设备",
+      "title2": "2.样品保存与现场情况",
+      "content2": "绝大多数样品状态为无色、无沉淀、无味、无悬浮物,只有少量样品稍显浑浊并含有沉淀物,为了保证样品的完整性和数据的准确性,采样后的保存方式包括了冷藏、避光、确保标签完好以及采取有效的减震措施,以避免运输过程中的振动和损坏。",
+      "photo4": "图2-1 采样现场",
+      "photo5": "图2-2 采样现场",
+      "photo6": "图2-3 采样现场"
+    },
+    "pointTitle": "灌溉水采样数据",
+    "point": {
+      "pointMapTitle": "采样点地图展示",
+      "pointLineTitle": "采样点数据列表展示",
+      "pointChartTitle": "韶关市各区县重金属平均浓度"
+    },
+    "crosssectionTitle": "断面采样数据",
+    "crosssection": {
+      "crosssectionMapTitle": "断面数据地图展示",
+      "crosssectionLineTitle": "断面数据详情",
+      "crosssectionRiverChartTitle": "各河流Cd的平均浓度柱状图",
+      "crosssectionCityChartTitle": "各区县的Cd平均浓度柱状图"
+    },
+    "InputfluxTitle": "灌溉水输入通量"
+  },
+  "atmosDeposition": {
+    "Title": "大气干湿沉降",
+    "AtmosDepositionSamplingDescTitle": "采样说明",
+    "heavyMetalEnterpriseTitle": "涉重企业",
+    "heavyMetalEnterprise": {
+      "MapTitle": "涉重企业地图展示",
+      "LineTitle": "涉重企业数据列表展示",
+      "ChartTitle": "各区县企业平均大气颗粒物排放(t/a)"
+    },
+    "airSampleDataTitle": "大气采样数据",
+    "airSampleData": {
+      "button1": "采样方式:",
+      "button2": "按重量采样",
+      "button3": "按体积采样",
+      "MapTitle": "采样点地图展示",
+      "LineTitle": "采样点数据列表展示",
+      "ChartTitle": "各区县平均大气重金属污染柱状图"
+    },
+    "airInputFluxTitle": "大气输入通量"
+  },
+  "agriInput": {
+    "Title": "农产品投入",
+    "farmInputSamplingDescTitle": "采样说明",
+    "prodInputFluxTitle": "农产品输入通量"
+  },
+  "Title": "区域土壤重金属污染风险评估",
+  "Menu": {
+    "dataManagement": "数据管理",
+    "infoManagement": "信息管理",
+    "modelManagement": "模型管理及配置",
+    "userManagement": "用户管理",
+    "shuJuKanBan": "数据看板",
+    "introduction": "软件简介",
+    "HmOutFlux": "重金属输入通量",
+    "hmInFlux": "重金属输出通量",
+    "mapView": "地图展示",
+    "cadmiumPrediction": "土壤污染物含量预测",
+    "cropRiskAssessment": "作物风险评估",
+    "farmlandQualityAssessment": "耕地质量评估",
+    "soilAcidificationPrediction": "土壤酸化预测",
+    "scenarioSimulation": "情景模拟",
+    "dataStatistics": "数据统计"
+  },
+
+  "shuJuKanBan": {
+    "Title": "数据看板"
+  },
+  "SoilPro": {
+    "Title": "软件简介"
+  },
+  "Overview": {
+    "Title": "项目简介"
+  },
+  "ResearchFindings": {
+    "Title": "研究成果"
+  },
+  "Unit": {
+    "Title": "团队信息"
+  },
+  "grainRemoval": {
+    "Title": "籽粒移除",
+    "samplingDesc1": "采样说明",
+    "grainRemovalInputFlux": "籽粒移除输出通量"
+  },
+  "strawRemoval": {
+    "Title": "秸秆移除",
+    "samplingDesc2": "采样说明",
+    "strawRemovalInputFlux": "秸秆移除输出通量"
+  },
+  "subsurfaceLeakage": {
+    "Title": "地下渗漏",
+    "samplingDesc3": "采样说明",
+    "subsurfaceLeakageInputFlux": "地下渗漏输入通量"
+  },
+  "surfaceRunoff": {
+    "Title": "地表径流",
+    "samplingDesc4": "采样说明",
+    "surfaceRunoffInputFlux": "地表径流输入通量"
+  },
+  "mapView": {
+    "Title": "地图展示"
+  },
+  "totalInputFlux": {
+    "Title": "输入总通量"
+  },
+  "totalOutputFlux": {
+    "Title": "输出总通量"
+  },
+  "netFlux": {
+    "Title": "净通量"
+  },
+  "currentYearConcentration": {
+    "Title": "当年浓度"
+  },
+  "TotalCadmiumPrediction": {
+    "Title": "土壤镉的总含量预测"
+  },
+  "EffectiveCadmiumPrediction": {
+    "Title": "土壤镉有效态含量预测"
+  },
+  "CropCadmiumPrediction": {
+    "Title": "土壤镉作物态含量预测"
+  },
+  "cropRiskAssessment": {
+    "Title": "水稻镉污染风险"
+  },
+  "farmlandQualityAssessment": {
+    "Title": "韶关"
+  },
+  "acidModel": {
+    "Title": "土壤反酸",
+    "CalculationTitle": "土壤反酸预测",
+    "SoilAcidReductionIterativeEvolutionTitle": "反酸模型迭代可视化"
+  },
+  "neutralizationModel": {
+    "Title": "土壤降酸",
+    "AcidNeutralizationModelTitle": "土壤降酸预测",
+    "SoilAcidificationIterativeEvolutionTitle": "土壤降酸迭代可视化"
+  },
+  "TraditionalFarmingRisk": {
+    "Title": "传统耕种习惯风险趋势"
+  },
+  "HeavyMetalCadmiumControl": {
+    "Title": "重金属镉污染治理"
+  },
+  "SoilAcidificationControl": {
+    "Title": "土壤酸化治理"
+  },
+  "DetectionStatistics": {
+    "Title": "检测信息统计"
+  },
+  "FarmlandPollutionStatistics": {
+    "Title": "土壤镉含量统计"
+  },
+  "PlantingRiskStatistics": {
+    "Title": "作物风险评估统计"
   }
 }

+ 3 - 3
src/router/index.ts

@@ -421,15 +421,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: "AdminRegionData",

+ 31 - 0
src/views/User/HmOutFlux/atmosDeposition/airSampleData.vue

@@ -67,7 +67,38 @@
       </div>
     </div>
     
+<<<<<<< HEAD
   
+=======
+   <div class="calculation-selector">
+    <span class="selector-title"><!--i18n:atmosDeposition.airSampleData.button1-->{{ $t("atmosDeposition.airSampleData.button1") }}</span>
+    <select 
+      v-model="calculationMethod" 
+      class="calculation-select"
+    >
+      <option value="weight"><!--i18n:atmosDeposition.airSampleData.button2-->{{ $t("atmosDeposition.airSampleData.button2") }}</option>
+      <option value="volume"><!--i18n:atmosDeposition.airSampleData.button3-->{{ $t("atmosDeposition.airSampleData.button3") }}</option>
+    </select>
+  </div>
+
+   <div class="point-map">
+    <div class="component-title"><!--i18n:atmosDeposition.airSampleData.MapTitle-->{{ $t("atmosDeposition.airSampleData.MapTitle") }}</div>
+    <div class="map-holder">
+      <atmsamplemap :calculation-method="calculationMethod"/>
+     </div>
+   </div>
+
+  
+   <div class="point-line">
+    <div class="component-title"><!--i18n:atmosDeposition.airSampleData.LineTitle-->{{ $t("atmosDeposition.airSampleData.LineTitle") }}</div>
+    <AirsampleLine :calculation-method="calculationMethod"/>
+   </div>
+
+   <div>
+    <div class="component-title"><!--i18n:atmosDeposition.airSampleData.ChartTitle-->{{ $t("atmosDeposition.airSampleData.ChartTitle") }}</div>
+    <AirsampleChart :calculation-method="calculationMethod"/>
+   </div>
+>>>>>>> origin/lili
   </div>
 </template>
 

+ 21 - 0
src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="page-container">
+<<<<<<< HEAD
     <!-- 页面标题区域 -->
     <div class="header-section">
       <div class="header-content">
@@ -66,6 +67,26 @@
         </div>
       </div>
     </div>
+=======
+    
+   <div class="point-map">
+    <div class="component-title"><!--i18n:atmosDeposition.heavyMetalEnterprise.MapTitle-->{{ $t("atmosDeposition.heavyMetalEnterprise.MapTitle")}}</div>
+    <div class="map-holder">
+      <atmcompanymap/>
+    </div>
+   </div>
+
+  
+   <div class="point-line">
+    <div class="component-title"><!--i18n:atmosDeposition.heavyMetalEnterprise.LineTitle-->{{ $t("atmosDeposition.heavyMetalEnterprise.LineTitle")}}</div>
+    <atmcompanyline/>
+   </div>
+
+   <div>
+    <div class="component-title"><!--i18n:atmosDeposition.heavyMetalEnterprise.ChartTitle-->{{ $t("atmosDeposition.heavyMetalEnterprise.ChartTitle")}}</div>
+    <HeavyMetalEnterprisechart/>
+   </div>
+>>>>>>> origin/lili
   </div>
 </template>
 

+ 14 - 0
src/views/User/HmOutFlux/irrigationWater/crossSection.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="cross-container">
+<<<<<<< HEAD
     <!-- 页面标题 -->
     <div class="section-header">
       <div class="section-icon">📊</div>
@@ -83,6 +84,19 @@
         </div>
       </div>
     </div>
+=======
+    <h3 class="table-title">{{ $t("irrigationwater.crosssection.crosssectionMapTitle") }}</h3>
+    <div class="map-holder"><crosssectionmap/></div>
+
+     <h3 class="table-title">{{ $t("irrigationwater.crosssection.crosssectionLineTitle") }}</h3>
+     <div><CrossSectionSamplelineData/></div>
+
+     <h3 class="table-title"> {{ $t("irrigationwater.crosssection.crosssectionRiverChartTitle") }}</h3>
+     <div><CrossSetionData1/></div>
+
+     <h3 class="table-title">{{ $t("irrigationwater.crosssection.crosssectionCityChartTitle") }}</h3>
+     <div><CrossSetionData2/></div>
+>>>>>>> origin/lili
   </div>
 </template>
 

+ 23 - 0
src/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="page-container">
+<<<<<<< HEAD
     <!-- 页眉设计 -->
     <div class="header">
       <div class="header-content">
@@ -56,6 +57,28 @@
         </div>
       </div>
     </div>
+=======
+    
+  <div class="main-content">
+   <div class="point-map">
+    <div class="component-title">{{ $t('irrigationwater.point.pointMapTitle') }}</div>
+    <div class="map-holder">
+        <irrwatermap/>
+    </div>
+   </div>
+
+  
+   <div class="point-line">
+    <div class="component-title">{{ $t('irrigationwater.point.pointLineTitle') }}</div>
+    <Waterdataline/>
+   </div>
+
+   <div class="charts-line">
+    <div class="component-title">{{ $t('irrigationwater.point.pointChartTitle') }}</div>
+    <Waterassaydata2/>
+   </div>
+  </div>
+>>>>>>> origin/lili
   </div>
 </template>
 

+ 49 - 0
src/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="sampling-process">
+<<<<<<< HEAD
     <!-- 采样容器与过程部分 -->
     <div class="section-container">
       <div class="section-header">
@@ -105,6 +106,54 @@
             </div>
           </div>
         </div>
+=======
+    <!--i18n:irrigationwaterMethods.title1-->
+    <h2>{{ $t("irrigationwater.irrigationwaterMethods.title1") }}</h2>
+    <!--i18n:irrigationwaterMethods.content1-->
+    <p>
+      {{ $t("irrigationwater.irrigationwaterMethods.content1") }}
+    </p>
+
+    <div class="image-row">
+      <div class="image-container">
+        <el-image :src="image1" alt="采样容器" class="sampling-image"></el-image>
+        <!--i18n:irrigationwaterMethods.photo1-->
+        <p class="image-caption">{{ $t("irrigationwater.irrigationwaterMethods.photo1") }}</p>
+      </div>
+      <div class="image-container">
+        <el-image :src="image2" alt="采样现场" class="sampling-image"></el-image>
+        <!--i18n:irrigationwaterMethods.photo2-->
+        <p class="image-caption">{{ $t("irrigationwater.irrigationwaterMethods.photo2") }}</p>
+      </div>
+      <div class="image-container">
+        <el-image :src="image3" alt="灌溉水采样设备" class="sampling-image"></el-image>
+        <!--i18n:irrigationwaterMethods.photo3-->
+        <p class="image-caption">{{ $t("irrigationwater.irrigationwaterMethods.photo3") }}</p>
+      </div>
+    </div>
+    <!--i18n:irrigationwaterMethods.title2-->
+    <h2>{{ $t("irrigationwater.irrigationwaterMethods.title2") }}</h2>
+    <!--i18n:irrigationwaterMethods.content2-->
+    <p>
+      {{ $t("irrigationwater.irrigationwaterMethods.content2") }}
+    </p>
+
+    <div class="image-row">
+      <div class="image-container">
+        <el-image :src="fieldImage1" alt="工作人员采样现场" class="sampling-image"></el-image>
+        <!--i18n:irrigationwaterMethods.photo4-->
+        <p class="image-caption">{{ $t("irrigationwater.irrigationwaterMethods.photo4") }}</p>
+      </div>
+      <div class="image-container">
+        <el-image :src="fieldImage2" alt="工作人员采样现场" class="sampling-image"></el-image>
+        <!--i18n:irrigationwaterMethods.photo5-->
+        <p class="image-caption">{{ $t("irrigationwater.irrigationwaterMethods.photo5") }}</p>
+      </div>
+      <div class="image-container">
+        <el-image :src="fieldImage3" alt="工作人员采样现场" class="sampling-image"></el-image>
+        <!--i18n:irrigationwaterMethods.photo6-->
+        <p class="image-caption">{{ $t("irrigationwater.irrigationwaterMethods.photo6") }}</p>
+>>>>>>> origin/lili
       </div>
     </div>
   </div>

+ 95 - 44
src/views/User/dataStatistics/DetectionStatistics.vue

@@ -1,53 +1,104 @@
 <template>
-  <div class="">
-    <!-- 添加删除和导入按钮 -->
-    <el-button type="danger" @click="handleBatchDelete" :disabled="!selectedRows.length">删除</el-button>
-    <el-button type="primary" @click="handleImport">导入</el-button>
-
-    <!-- 添加表格 -->
-    <el-table
-      :data="tableData"
-      style="width: 100%"
-      @selection-change="handleSelectionChange"
-    >
-      <el-table-column type="selection" width="55" />
-      <el-table-column prop="name" label="名称" width="180" />
-      <el-table-column prop="date" label="日期" width="180" />
-      <el-table-column prop="status" label="状态" width="180" />
-    </el-table>
+  <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>
+
+      <!-- 第二列:大气采样点 + 大气污染企业 -->
+      <div class="column column-2">
+        <div class="component-box metal-box">
+          <AtmsampleStatistics /> <!-- 大气采样点数据 -->
+        </div>
+        <div class="component-box company-box">
+          <AtmcompanyStatics /> <!-- 大气污染企业数据 -->
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <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 {
-  name: 'DetectionStatistics',
-  data() {
-    return {
-      tableData: [
-        { name: '示例1', date: '2023-01-01', status: '正常' },
-        { name: '示例2', date: '2023-01-02', status: '异常' }
-      ],
-      selectedRows: [] // 存储选中的行
-    };
-  },
-  methods: {
-    handleSelectionChange(selection) {
-      this.selectedRows = selection;
-    },
-    handleBatchDelete() {
-      this.tableData = this.tableData.filter(
-        item => !this.selectedRows.includes(item)
-      );
-      this.selectedRows = [];
-    },
-    handleImport() {
-      // 导入逻辑
-      console.log('导入按钮点击');
-    }
+  components: {
+    AtmcompanyStatics,
+    AtmsampleStatistics,
+    CrosscetionStatistics,
+    Irrigationstatistics
   }
-};
+}
 </script>
-
 <style scoped>
- 
-</style>
+.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; /* 三列等宽 */
+  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; /* 避免组件被压缩 */
+}
+
+/* 第一列组件样式 */
+.irrigation-box { 
+  background: #f9fafc; 
+  height: 310px;
+  width: 555px;
+}
+.section-box { 
+  background: #f9fafc; 
+  height: 310px;
+  width: 555px;
+}
+
+/* 第二列组件样式 */
+.metal-box { 
+  background: #f3f4f6; 
+  height: 310px;
+  width: 555px;
+}
+.company-box { 
+  background: #f3f4f6; 
+  height: 310px;
+  width: 555px;
+}
+
+</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>

+ 4 - 1
tsconfig.json

@@ -10,5 +10,8 @@
     {
       "path": "./tsconfig.vitest.json"
     }
-  ]
+  ],
+  "compilerOptions": {
+    "types": ["unplugin-vue-i18n/client"] // 显式引入类型定义
+  }
 }

+ 10 - 1
vite.config.ts

@@ -11,6 +11,11 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 import Icons from 'unplugin-icons/vite'
 import IconsResolver from 'unplugin-icons/resolver'
 
+import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 
+import path from 'path'
+
+const  __dirname = path.dirname(fileURLToPath(import.meta.url));
+
 // https://vite.dev/config/
 export default defineConfig({
   plugins: [
@@ -52,4 +57,8 @@ export default defineConfig({
       '@': fileURLToPath(new URL('./src', import.meta.url))
     },
   },
-})
+})
+
+function kebabCase(text: any) {
+  throw new Error('Function not implemented.')
+}

+ 1 - 0
语言切换.txt

@@ -0,0 +1 @@
+node scripts/extract-i18n.js 文件路径,详细到.vue/.ts

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است