Pārlūkot izejas kodu

Merge branch 'master' of http://139.9.51.218:3000/qw/soilgd12 into ding

yangtaodemon 1 mēnesi atpakaļ
vecāks
revīzija
47189ac38b
29 mainītis faili ar 1842 papildinājumiem un 1359 dzēšanām
  1. 1 1
      src/components/atmpollution/airsampleChart.vue
  2. 1 1
      src/components/atmpollution/airsampleLine.vue
  3. 13 12
      src/components/detectionStatistics/atmcompanyStatics.vue
  4. 89 72
      src/components/detectionStatistics/atmsampleStatistics.vue
  5. 14 14
      src/components/detectionStatistics/crosscetionStatistics.vue
  6. 59 51
      src/components/detectionStatistics/irrigationstatistics.vue
  7. 1 1
      src/components/irrpollution/tencentMapView.vue
  8. 3 3
      src/components/layout/AppAsideForTab2.vue
  9. 210 182
      src/components/layout/AppLayout.vue
  10. 16 16
      src/components/layout/menuItems.ts
  11. 74 32
      src/components/soilStatictics/reducedataStatistics.vue
  12. 80 34
      src/components/soilStatictics/refluxcedataStatictics.vue
  13. 98 88
      src/components/soilcdStatistics/cropcdStatictics.vue
  14. 79 60
      src/components/soilcdStatistics/effcdStatistics.vue
  15. 64 58
      src/components/soilcdStatistics/fluxcdStatictics.vue
  16. 279 168
      src/locales/en.json
  17. 50 0
      src/locales/index.js
  18. 275 163
      src/locales/zh.json
  19. 19 16
      src/views/Admin/userManagement/UserRegistration.vue
  20. 28 25
      src/views/User/acidModel/Calculation.vue
  21. 15 12
      src/views/User/acidModel/ModelIterationVisualization.vue
  22. 86 83
      src/views/User/acidModel/nanxiong_acidmodelmap.vue
  23. 32 32
      src/views/User/acidModel/pHPrediction.vue
  24. 129 127
      src/views/User/acidModel/shaoguan_acidmodelmap.vue
  25. 54 41
      src/views/User/dataStatistics/LandCultivatedStatistics.vue
  26. 37 34
      src/views/User/neutralizationModel/AcidNeutralizationModel.vue
  27. 15 12
      src/views/User/neutralizationModel/ModelIterationVisualization.vue
  28. 18 19
      src/views/login/loginView.vue
  29. 3 2
      vite.config.ts

+ 1 - 1
src/components/atmpollution/airsampleChart.vue

@@ -68,7 +68,7 @@ let myChart = null;
 const log = (message) => {
   const time = new Date().toLocaleTimeString();
   fullLog.value += `[${time}] ${message}\n`;
-  console.log(`[日志] ${message}`);
+  // console.log(`[日志] ${message}`);
 };
 
 

+ 1 - 1
src/components/atmpollution/airsampleLine.vue

@@ -362,7 +362,7 @@ const fetchData = async () => {
       ];
     }
 
-    console.log('✅ 数据加载完成,记录数:', waterData.value.length);
+    // console.log('✅ 数据加载完成,记录数:', waterData.value.length);
     initChart(); // 初始化图表
 
   } catch (err) {

+ 13 - 12
src/components/detectionStatistics/atmcompanyStatics.vue

@@ -4,8 +4,8 @@
 
       <div class="chart-header">
         <div class="title-group">
-             <div class="chart-title">大气污染企业各地区颗粒物排放量统计</div>
-             <p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
+             <div class="chart-title">{{ $t('DetectionStatistics.atmosphericPollutionEnterprise') }}</div>
+             <p class="sample-subtitle">{{ $t('DetectionStatistics.sampleSource') }}{{ totalPoints }}{{ $t('DetectionStatistics.sampleDataCount') }}</p>
         </div>
        
         <div class="method-selector">
@@ -14,14 +14,14 @@
             :class="{active: currentMethod === 'max'}"
             @click="currentMethod = 'max'"
           >
-            最大值
+            {{ $t('DetectionStatistics.maxMethod') }}
           </button>
           <button 
             class="method-btn" 
             :class="{active: currentMethod === 'avg'}"
             @click="currentMethod = 'avg'"
           >
-            平均值
+            {{ $t('DetectionStatistics.avgMethod') }}
           </button>
         </div>
       </div>
@@ -29,13 +29,13 @@
       <div class="chart-wrapper">
         <div v-if="loading" class="loading">
           <div class="spinner"></div>
-          <div class="loading-text">数据加载中...</div>
+          <div class="loading-text">{{ $t('DetectionStatistics.dataLoading') }}</div>
         </div>
         <div v-else-if="error" class="error">
           {{ error }}
         </div>
         <div v-else-if="!processedData.length" class="no-data">
-          无有效数据可展示
+          {{ $t('DetectionStatistics.noDataToShow') }}
         </div>
         <div v-else ref="mainChart" style="height:100%;width:100%"></div>
       </div>
@@ -46,6 +46,7 @@
 <script>
 import * as echarts from 'echarts'
 import { api8000 } from '@/utils/request'
+import { useI18n } from 'vue-i18n'
 
 export default {
   data() {
@@ -226,7 +227,7 @@ export default {
       // 根据选择的方法确定数据
       const method = this.currentMethod;
       const valueField = method === 'max' ? 'maxEmission' : 'avgEmission';
-      const methodLabel = method === 'max' ? '最高值' : '平均值';
+      const methodLabel = method === 'max' ? this.$t('DetectionStatistics.highestValue') : this.$t('DetectionStatistics.averageValue');
       
       const data = this.processedData.map(item => ({
         name: item.county,
@@ -243,9 +244,9 @@ export default {
             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>
+              <div>${this.$t('DetectionStatistics.highestValue')}:${d.maxEmission} ${this.$t('DetectionStatistics.unitTonPerYear')}</div>
+              <div>${this.$t('DetectionStatistics.averageValue')}:${d.avgEmission} ${this.$t('DetectionStatistics.unitTonPerYear')}</div>
+              <div style="margin-top:5px">${this.$t('DetectionStatistics.monitoringPoints')}:${d.pointCount}${this.$t('DetectionStatistics.points')}</div>
             `;
           }
         },
@@ -258,7 +259,7 @@ export default {
         },
         xAxis: {
           type: 'value',
-          name: `颗粒物排放量`,
+          name: this.$t('DetectionStatistics.particulateEmission'),
           nameLocation: 'end',
           nameTextStyle:{
             padding:-30,
@@ -284,7 +285,7 @@ export default {
           }
         },
         series: [{
-          name: '颗粒物排放量',
+          name: this.$t('DetectionStatistics.particulateEmission'),
           type: 'bar',
           data: data,
           label: {

+ 89 - 72
src/components/detectionStatistics/atmsampleStatistics.vue

@@ -2,17 +2,17 @@
   <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 class="chart-title">{{ $t('DetectionStatistics.atmosphericHeavyMetal') }}</div>
+        <p>{{ $t('DetectionStatistics.distributionDescription') }}</p>
+        <p class="sample-subtitle">{{ $t('DetectionStatistics.sampleSource') }}{{ totalPoints }}{{ $t('DetectionStatistics.sampleDataCount') }}</p>
       </div>
 
       <div v-if="isLoading" class="loading-state">
-        <span class="spinner"></span> 数据加载中...
+        <span class="spinner"></span> {{ $t('DetectionStatistics.dataLoading') }}
       </div>
 
       <div v-else-if="error" class="error-state">
-        ❌ 加载失败:{{ error.message }}
+        ❌ {{ $t('DetectionStatistics.loadingFailed') }}{{ error.message }}
       </div>
 
       <div v-else>
@@ -27,36 +27,41 @@
 <script>
 import * as echarts from 'echarts'
 import VChart from 'vue-echarts'
-import { api8000 } from '@/utils/request' // 导入 api8000 实例
-import { ref, onMounted, computed } from 'vue'
+import { api8000 } from '@/utils/request'
+import { ref, onMounted, computed, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
 
 export default {
   components: { VChart },
   setup() {
+    const { t } = useI18n()
+    const { locale } = useI18n()
+
     // -------- 核心配置 --------
-    // 新接口地址(直接返回箱线图所需统计数据)
-    const apiUrl = ref('/api/vector/stats/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' },
+    const apiUrl = ref('/api/vector/stats/Atmo_sample_data')
+    
+    // 获取重金属配置的函数(每次调用都会使用最新的翻译)
+    const getHeavyMetals = () => [
+      { key: 'Cr_particulate', name: t('DetectionStatistics.chromium'), color: '#FF9800' },
+      { key: 'As_particulate', name: t('DetectionStatistics.arsenic'), color: '#4CAF50' },
+      { key: 'Cd_particulate', name: t('DetectionStatistics.cadmium'), color: '#9C27B0' },
+      { key: 'Hg_particulate', name: t('DetectionStatistics.mercury'), color: '#2196F3' },
+      { key: 'Pb_particulate', name: t('DetectionStatistics.lead'), color: '#F44336' },
     ]
 
     // -------- 状态管理 --------
-    const chartOption = ref({})   // ECharts 配置
-    const isLoading = ref(true)   // 加载状态
-    const error = ref(null)       // 错误信息
-    const statsByIndex = ref([])  // 与x轴对齐的统计结果(用于tooltip)
+    const chartOption = ref({})
+    const isLoading = ref(true)
+    const error = ref(null)
+    const statsByIndex = ref([])
+    const apiDataCache = ref(null) // 缓存接口数据
+
     const totalPoints = computed(() => {
-      // 从统计数据中获取样本总数(假设所有金属样本数相同)
       return statsByIndex.value.length > 0 
         ? statsByIndex.value[0].count || 0 
         : 0;
     })
 
-
     // -------- 工具函数 --------
     const log = (message, metalName = '') => {
       console.log(
@@ -67,8 +72,7 @@ export default {
     }
 
     const buildBoxplotData = (stats) => {
-      const xAxisData = heavyMetals.map(m => m.name)
-      // 生成箱线图所需格式:[[min, q1, median, q3, max], ...]
+      const xAxisData = stats.map(s => s.name)
       const data = stats.map(s => 
         s.min === null 
           ? [null, null, null, null, null] 
@@ -77,8 +81,10 @@ export default {
       return { xAxisData, data }
     }
 
-    const initChart = (xAxisData, data, stats) => {
-      statsByIndex.value = stats // 缓存统计数据用于tooltip
+    const initChart = (stats) => {
+      statsByIndex.value = stats
+      
+      const { xAxisData, data } = buildBoxplotData(stats)
       
       chartOption.value = {
         tooltip: {
@@ -86,22 +92,22 @@ export default {
           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;color:#f56c6c">${xAxisData[params.dataIndex]}</div><div>${t('DetectionStatistics.noValidData')}</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>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${s.min.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${s.q1.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${s.median.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${s.q3.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${s.max.toFixed(4)}</span></div>
               </div>`
           }
         },
         xAxis: {
           type: 'category',
           data: xAxisData,
-          name: '重金属类型',
+          name: t('DetectionStatistics.heavyMetalType'),
           nameLocation: 'middle',
           nameGap: 45,
           axisLabel: { color: '#555', rotate: 30, fontWeight: 'bold', fontSize: 11 }
@@ -115,11 +121,11 @@ export default {
           splitLine: { lineStyle: { color: '#f0f0f0' } }
         },
         series: [{
-          name: '重金属浓度分布',
+          name: t('DetectionStatistics.concentrationDistribution'),
           type: 'boxplot',
-          data, // 直接使用接口返回的统计数据
+          data,
           itemStyle: {
-            color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
+            color: (p) => (getHeavyMetals()[p.dataIndex]?.color || '#1890ff'),
             borderWidth: 2
           },
           emphasis: {
@@ -130,48 +136,51 @@ export default {
       }
     }
 
+    // -------- 处理数据并初始化图表 --------
+    const processDataAndInitChart = () => {
+      if (!apiDataCache.value) return
+      
+      const heavyMetals = getHeavyMetals()
+      const stats = heavyMetals.map(metal => {
+        const metalStats = apiDataCache.value[metal.key]
+        if (!metalStats) {
+          log(`警告:接口缺少${metal.name}的统计数据`)
+          return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
+        }
+        
+        const requiredFields = ['min', 'q1', 'median', 'q3', 'max']
+        const hasValidData = requiredFields.every(field => 
+          metalStats[field] !== undefined && !isNaN(metalStats[field])
+        )
+
+        if (!hasValidData) {
+          log(`警告:${metal.name}的统计数据不完整`)
+          return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
+        }
+
+        return {
+          ...metal,
+          min: Number(metalStats.min),
+          q1: Number(metalStats.q1),
+          median: Number(metalStats.median),
+          q3: Number(metalStats.q3),
+          max: Number(metalStats.max),
+          count: metalStats.count ? Number(metalStats.count) : 0
+        }
+      })
+
+      initChart(stats)
+      isLoading.value = false
+    }
+
     // -------- 接口请求 --------
     onMounted(async () => {
       try {
         log('发起新接口请求,获取统计数据...')
-        // 使用 api8000 替代 axios
         const response = await api8000.get(apiUrl.value)
-        const apiData = response.data.data
-
-        // 从接口数据中提取每个重金属的统计量
-        const stats = heavyMetals.map(metal => {
-          const metalStats = apiData[metal.key]
-          if (!metalStats) {
-            log(`警告:接口缺少${metal.name}的统计数据`)
-            return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
-          }
-          
-          // 验证必要的统计字段
-          const requiredFields = ['min', 'q1', 'median', 'q3', 'max']
-          const hasValidData = requiredFields.every(field => 
-            metalStats[field] !== undefined && !isNaN(metalStats[field])
-          )
-
-          if (!hasValidData) {
-            log(`警告:${metal.name}的统计数据不完整`)
-            return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
-          }
-
-          return {
-            ...metal,
-            min: Number(metalStats.min),
-            q1: Number(metalStats.q1),
-            median: Number(metalStats.median),
-            q3: Number(metalStats.q3),
-            max: Number(metalStats.max),
-            count: metalStats.count ? Number(metalStats.count) : 0
-          }
-        })
-
-        // 构建图表数据并初始化图表
-        const { xAxisData, data } = buildBoxplotData(stats)
-        initChart(xAxisData, data, stats)
-        isLoading.value = false
+        apiDataCache.value = response.data.data
+        
+        processDataAndInitChart()
 
       } catch (err) {
         error.value = err
@@ -180,6 +189,14 @@ export default {
       }
     })
 
+    // 监听语言变化,重新初始化图表
+    watch(locale, () => {
+      console.log('语言切换,重新初始化图表')
+      if (apiDataCache.value) {
+        processDataAndInitChart()
+      }
+    })
+
     return {
       chartOption,
       isLoading,

+ 14 - 14
src/components/detectionStatistics/crosscetionStatistics.vue

@@ -3,8 +3,8 @@
     <div class="chart-container">
       <div class="chart-header">
         <div class="title-group">
-          <div class="chart-title">断面采样点各地区镉浓度统计</div>
-          <p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
+          <div class="chart-title">{{ $t('DetectionStatistics.crossSectionCadmium') }}</div>
+          <p class="sample-subtitle">{{ $t('DetectionStatistics.sampleSource') }}{{ totalPoints }}{{ $t('DetectionStatistics.sampleDataCount') }}</p>
         </div>
        
         <div class="method-selector">
@@ -13,14 +13,14 @@
             :class="{active: currentMethod === 'max'}"
             @click="currentMethod = 'max'"
           >
-            最大值
+            {{ $t('DetectionStatistics.maxMethod') }}
           </button>
           <button 
             class="method-btn" 
             :class="{active: currentMethod === 'avg'}"
             @click="currentMethod = 'avg'"
           >
-            平均值
+            {{ $t('DetectionStatistics.avgMethod') }}
           </button>
         </div>
       </div>
@@ -28,13 +28,13 @@
       <div class="chart-wrapper">
         <div v-if="loading" class="loading">
           <div class="spinner"></div>
-          <div class="loading-text">数据加载中...</div>
+          <div class="loading-text">{{ $t('DetectionStatistics.dataLoading') }}</div>
         </div>
         <div v-else-if="error" class="error">
           {{ error }}
         </div>
         <div v-else-if="!processedData.length" class="no-data">
-          无有效数据可展示
+          {{ $t('DetectionStatistics.noDataToShow') }}
         </div>
         <div v-else ref="mainChart" style="height:100%;width:100%"></div>
       </div>
@@ -222,7 +222,7 @@ export default {
       
       const method = this.currentMethod;
       const valueField = method === 'max' ? 'maxCd' : 'avgCd';
-      const methodLabel = method === 'max' ? '最高值' : '平均值';
+      const methodLabel = method === 'max' ? this.$t('DetectionStatistics.highestValue') : this.$t('DetectionStatistics.averageValue');
       
       const data = this.processedData.map(item => ({
         name: item.county,
@@ -239,9 +239,9 @@ export default {
             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.toFixed(4)} mg/L</div>
-              <div style="font-size:11px">平均浓度: ${d.avgCd.toFixed(4)} mg/L</div>
-              <div style="margin-top:3px;font-size:11px">监测点数量: ${d.pointCount}个</div>
+              <div style="font-size:11px">${this.$t('DetectionStatistics.highestValue')}:${d.maxCd.toFixed(4)} mg/L</div>
+              <div style="font-size:11px">${this.$t('DetectionStatistics.averageValue')}:${d.avgCd.toFixed(4)} mg/L</div>
+              <div style="margin-top:3px;font-size:11px">${this.$t('DetectionStatistics.monitoringPoints')}:${d.pointCount}${this.$t('DetectionStatistics.points')}</div>
             `;
           }
         },
@@ -255,14 +255,14 @@ export default {
         },
         xAxis: {
           type: 'value',
-          name: `镉浓度`,
+          name: this.$t('DetectionStatistics.cadmiumConcentration'),
           nameLocation: 'end',
           nameTextStyle: {
             fontSize: 10,
             padding:-10,
           },
           axisLabel: {
-            formatter: value => value.toFixed(2) + ' mg/L',
+            formatter: value => value + ' mg/L',
             fontSize: 10,
           },
           splitLine: {
@@ -280,13 +280,13 @@ export default {
           }
         },
         series: [{
-          name: '镉浓度',
+          name: this.$t('DetectionStatistics.cadmiumConcentration'),
           type: 'bar',
           data: data,
           label: {
             show: true,
             position: 'right',
-            formatter: params => params.value.toFixed(2) + ' mg/L',
+            formatter: params => params.value + ' mg/L',
             fontSize: 9,
             rotate:15
           },

+ 59 - 51
src/components/detectionStatistics/irrigationstatistics.vue

@@ -2,17 +2,17 @@
   <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 class="chart-title">{{ $t('DetectionStatistics.irrigationWaterHeavyMetal') }}</div>
+        <p>{{ $t('DetectionStatistics.distributionDescription') }}</p>
+        <p class="sample-subtitle">{{ $t('DetectionStatistics.sampleSource') }}{{ totalPoints }}{{ $t('DetectionStatistics.sampleDataCount') }}</p>
       </div>
 
       <div v-if="isLoading" class="loading-state">
-        <span class="spinner"></span> 数据加载中...
+        <span class="spinner"></span> {{ $t('DetectionStatistics.dataLoading') }}
       </div>
 
       <div v-else-if="error" class="error-state">
-        ❌ 加载失败:{{ error.message }}
+        ❌ {{ $t('DetectionStatistics.loadingFailed') }}{{ error.message }}
       </div>
 
       <div v-else>
@@ -27,45 +27,49 @@
 <script>
 import * as echarts from 'echarts'
 import VChart from 'vue-echarts'
-import { api8000 } from '@/utils/request' // 导入 api8000 实例
-import { ref, onMounted, computed } from 'vue'
+import { api8000 } from '@/utils/request'
+import { ref, onMounted, computed, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
 
 export default {
   components: { VChart },
   setup() {
+    const { t } = useI18n()
+    const { locale } = useI18n()
+
     // -------- 基本状态 --------
-    const apiUrl = ref('/api/vector/stats/water_sampling_data') // 修改为相对路径
+    const apiUrl = ref('/api/vector/stats/water_sampling_data')
     const apiTimestamp = ref(null)
-    const statsData = ref({}); 
+    const statsData = ref({})
     const chartOption = ref({})
     const isLoading = ref(true)
     const error = ref(null)
     
-    // 样本数统计(从预统计数据中获取)
+    // 样本数统计
     const totalPoints = computed(() => {
-      const firstMetalKey = heavyMetals[0]?.key;
-      return statsData.value[firstMetalKey]?.count || 0;
+      const firstMetalKey = getHeavyMetals()[0]?.key
+      return statsData.value[firstMetalKey]?.count || 0
     })
 
-    // 缓存每个品类的统计量(与 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 getHeavyMetals = () => [
+      { key: 'cr_concentration', name: t('DetectionStatistics.chromium'), color: '#FF9800' },
+      { key: 'as_concentration', name: t('DetectionStatistics.arsenic'), color: '#4CAF50' },
+      { key: 'cd_concentration', name: t('DetectionStatistics.cadmium'), color: '#9C27B0' },
+      { key: 'hg_concentration', name: t('DetectionStatistics.mercury'), color: '#2196F3' },
+      { key: 'pb_concentration', name: t('DetectionStatistics.lead'), color: '#F44336' }
     ]
 
     // -------- 构建箱线数据 --------
     const buildBoxplotData = () => {
-      const xAxisData = heavyMetals.map(m => m.name);
+      const heavyMetals = getHeavyMetals()
+      const xAxisData = heavyMetals.map(m => m.name)
 
-      // 缓存每个重金属的统计量(用于tooltip)
       statsByIndex.value = heavyMetals.map(metal => {
-        const stat = statsData.value[metal.key] || {};
+        const stat = statsData.value[metal.key] || {}
         return {
           key: metal.key,
           name: metal.name,
@@ -75,23 +79,22 @@ export default {
           q3: stat.q3,
           max: stat.max,
           color: metal.color
-        };
-      });
+        }
+      })
 
-      // 构建ECharts箱线图数据
       const data = statsByIndex.value.map(s => {
         if (s.min === undefined || s.min === null) {
-          return [null, null, null, null, null];
+          return [null, null, null, null, null]
         }
-        return [s.min, s.q1, s.median, s.q3, s.max];
-      });
+        return [s.min, s.q1, s.median, s.q3, s.max]
+      })
 
-      return { xAxisData, data };
-    };
+      return { xAxisData, data }
+    }
 
     // -------- 初始化图表 --------
     const initChart = () => {
-      const { xAxisData, data } = buildBoxplotData();
+      const { xAxisData, data } = buildBoxplotData()
 
       chartOption.value = {
         tooltip: {
@@ -99,25 +102,25 @@ export default {
           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;color:#f56c6c">${xAxisData[params.dataIndex]}</div><div>${t('DetectionStatistics.noValidData')}</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>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${s.min.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${s.q1.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${s.median.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${s.q3.toFixed(4)}</span></div>
+                <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${s.max.toFixed(4)}</span></div>
               </div>`
           },
         },
         xAxis: {
           type: 'category',
           data: xAxisData,
-          name: '重金属类型',
+          name: t('DetectionStatistics.heavyMetalType'),
           nameLocation: 'middle',
           nameGap: 30,
-          axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold' ,fontSize :11}
+          axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold', fontSize: 11 }
         },
         yAxis: {
           type: 'value',
@@ -125,15 +128,15 @@ export default {
           nameTextStyle: { fontSize: 12 }, 
           nameLocation: 'end',
           nameGap: 8,
-          axisLabel: { color: '#555', fontWeight: 'bold',fontSize:11 },
+          axisLabel: { color: '#555', fontWeight: 'bold', fontSize: 11 },
           splitLine: { lineStyle: { color: '#f0f0f0' } }
         },
         series: [{
-          name: '重金属浓度分布',
+          name: t('DetectionStatistics.concentrationDistribution'),
           type: 'boxplot',
           data,
           itemStyle: {
-            color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
+            color: (p) => (getHeavyMetals()[p.dataIndex]?.color || '#1890ff'),
             borderWidth: 2
           },
           emphasis: {
@@ -149,18 +152,23 @@ export default {
     // -------- 拉取接口并绘图 --------
     onMounted(async () => {
       try {
-        // 使用api8000替代axios
-        const response = await api8000.get(apiUrl.value);
-        statsData.value = response.data.data; 
-        apiTimestamp.value = new Date().toLocaleString();
-        initChart();
+        const response = await api8000.get(apiUrl.value)
+        statsData.value = response.data.data
+        apiTimestamp.value = new Date().toLocaleString()
+        initChart()
       } catch (err) {
-        error.value = err;
-        isLoading.value = false;
-        console.error('接口请求失败:', err);
+        error.value = err
+        isLoading.value = false
+        console.error('接口请求失败:', err)
       }
     })
 
+    // 监听语言变化,重新初始化图表
+    watch(locale, () => {
+      console.log('语言切换,重新初始化图表')
+      initChart()
+    })
+
     return {
       apiUrl,
       apiTimestamp,

+ 1 - 1
src/components/irrpollution/tencentMapView.vue

@@ -546,7 +546,7 @@ onBeforeUnmount(() => {
   if (map) {
     try {
       map.destroy(); // 腾讯地图销毁方法
-      console.log('[地图] 地图实例已销毁');
+      // console.log('[地图] 地图实例已销毁');
     } catch (e) {
       console.error('[地图] 销毁失败:', e);
     }

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

@@ -17,7 +17,7 @@
               <el-icon v-if="item.icon">
                 <component :is="item.icon" />
               </el-icon>
-              <span>{{ item.label }}</span>
+              <span>{{ $t(item.label) }}</span>
             </template>
 
             <!-- 子菜单 -->
@@ -29,7 +29,7 @@
               <el-icon v-if="child.icon">
                 <component :is="child.icon" />
               </el-icon>
-              <span>{{ child.label }}</span>
+              <span>{{$t( child.label) }}</span>
             </el-menu-item>
           </el-sub-menu>
 
@@ -38,7 +38,7 @@
             <el-icon v-if="item.icon">
               <component :is="item.icon" />
             </el-icon>
-            <span>{{ item.label }}</span>
+            <span>{{$t( item.label) }}</span>
           </el-menu-item>
         </template>
       </template>

+ 210 - 182
src/components/layout/AppLayout.vue

@@ -14,19 +14,32 @@
       :class="{ 'transparent-header': isSpecialBg }"
     >
       <div class="logo-title-row">
-        <!-- 左侧:Logo和标题 -->
+        <!-- 左侧:Logo 和标题 -->
         <div class="left-section">
           <img src="@/assets/logo.png" alt="Logo" class="logo" />
           <span class="project-name" :class="{ 'light-text': isSpecialBg }">
-            土壤酸化智能预测专家系统
+            {{ t('Header.title') }}
           </span>
         </div>
 
         <!-- 右侧:用户信息 -->
         <div class="right-section" v-if="!isSelectCity">
           <div class="user-info-row">
+            <div class="lang-buttons">
+            <!-- 语言切换按钮 -->
+            <button 
+              class="lang-toggle-btn"
+              :class="{ 'light-text': isSpecialBg }"
+              @click="toggleLanguage"
+              :title="currentLang === 'zh' ? 'Switch to English' : '切换到中文'"
+            >
+              <i class="el-icon-globe"></i>
+              <span class="lang-label">{{ currentLang === 'zh' ? '中文' : 'English' }}</span>
+            </button>
+         </div>
+
             <span class="welcome-text" :class="{ 'light-text': isSpecialBg }">
-              欢迎 {{ userInfo.name }} 登录成功
+              {{ t('Header.welcome') }} {{ userInfo.name }} 
             </span>
             <el-dropdown>
               <span class="el-dropdown-link">
@@ -38,12 +51,12 @@
               </span>
               <template #dropdown>
                 <el-dropdown-menu>
-                  <el-dropdown-item disabled
-                    >用户名:{{ userInfo.name }}</el-dropdown-item
-                  >
-                  <el-dropdown-item divided @click="handleLogout"
-                    >退出登录</el-dropdown-item
-                  >
+                  <el-dropdown-item disabled>
+                    {{ t('Header.username') }}: {{ userInfo.name }}
+                  </el-dropdown-item>
+                  <el-dropdown-item divided @click="handleLogout">
+                    {{ t('Header.logout') }}
+                  </el-dropdown-item>
                 </el-dropdown-menu>
               </template>
             </el-dropdown>
@@ -108,8 +121,34 @@
 import { ref, reactive, computed, watch, defineAsyncComponent } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { useTokenStore } from "@/stores/mytoken";
-import { ElMessageBox, ElMessage } from "element-plus"; // 确保导入ElMessage
+import { ElMessageBox, ElMessage } from "element-plus";
 import { logout } from "@/API/users";
+import { useI18n } from 'vue-i18n'
+
+const { t, locale } = useI18n()
+const currentLang = ref(locale.value || 'zh')
+
+// 切换语言
+const changeLanguage = () => {
+  locale.value = currentLang.value
+  localStorage.setItem('language', currentLang.value)
+  
+  // 触发一个自定义事件,让其他组件知道语言改变了
+  window.dispatchEvent(new CustomEvent('languageChanged', { 
+    detail: { lang: currentLang.value } 
+  }))
+}
+
+// 按钮点击切换
+const toggleLanguage = () => {
+  currentLang.value = currentLang.value === 'zh' ? 'en' : 'zh'
+  changeLanguage()
+}
+
+// 监听语言变化
+watch(locale, (newVal) => {
+  currentLang.value = newVal
+})
 
 const router = useRouter();
 const route = useRoute();
@@ -119,11 +158,10 @@ const currentBgImage = ref("");
 // ============ 新增状态 ============
 const showGlobalMessage = ref(false);
 const globalMessage = ref("");
-const messageType = ref("success"); // 消息类型: success/error
+const messageType = ref("success");
 
 currentBgImage.value = `url(${new URL('../../assets/bg/background.jpg', import.meta.url).href})`;
 
-// 是否特殊背景(始终返回true → 所有页面应用特殊背景样式)
 const isSpecialBg = computed(() => true);
 
 const backgroundStyle = computed(() => ({
@@ -144,152 +182,79 @@ const userInfo = reactive({
 });
 
 const tabs = computed(() => {
-
- if (userInfo.type === "admin") {
-
-  return [
-
-   {
-
-    name: "dataManagement",
-
-    label: "数据管理",
-
-    icon: "el-icon-folder",
-
-    routes: [
-
-     "/soilAcidReductionData",
-
-     "/soilAcidificationData",
-
-     "/crossSectionSampleData",
-
-     "/irrigationWaterInputFluxData",
-
-     "/heavyMetalEnterpriseData",
-
-     "/atmosphericSampleData",
-
-    ],
-
-   },
-
-   {
-
-    name: "infoManagement",
-
-    label: "信息管理",
-
-    icon: "el-icon-document",
-
-    routes: ["/IntroductionUpdate"],
-
-   },
-
-   {
-
-    name: "modelManagement",
-
-    label: "模型管理及配置",
-
-    icon: "el-icon-cpu",
-
-    routes: ["/ModelSelection", "/thres", "/ModelTrain"],
-
-   },
-
-   {
-
-    name: "userManagement",
-
-    label: "用户管理",
-
-    icon: "el-icon-user",
-
-    routes: ["/UserManagement"],
-
-   },
-
-  ];
-
- } else {
-
-  return [
-
-   {
-
-    name: "introduction",
-
-    label: "软件简介",
-
-    icon: "el-icon-info-filled",
-
-    routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"],
-
-   },
-
-   {
-   name: "acidmodelmap",
-
-    label: "土壤酸化地块级计算",
-
-    icon: "el-icon-data-analysis",
-
-    routes: ["/shaoguan_acidmodelmap","nanxiong_acidmodelmap"],
-
-   },
-
-   {
-
-    name: "Calculation",
-
-    label: "土壤反酸模型",
-
-    icon: "el-icon-data-analysis",
-
-    routes: ["/Calculation", "/SoilAcidReductionIterativeEvolution"],
-
-   },
-
-   {
-
-    name: "AcidNeutralizationModel",
-
-    label: "土壤降酸模型",
-
-    icon: "el-icon-data-analysis",
-
-    routes: ["/AcidNeutralizationModel", "/SoilAcidificationIterativeEvolution"],
-
-   },
-
-   {
-
-    name: "dataStatistics",
-
-    label: "数据统计",
-
-    icon: "el-icon-pie-chart",
-
-    routes: [
-
-     "/DetectionStatistics",
-
-     "/FarmlandPollutionStatistics",
-
-     "/LandClutivatesStatistics",
-
-     "/SoilacidificationStatistics",
-
-   ],
-
-   },
-
-  ];
-
- }
-
+  if (userInfo.type === "admin") {
+    return [
+      {
+        name: "dataManagement",
+        label: t('Menu.dataManagement'),
+        icon: "el-icon-folder",
+        routes: [
+          "/soilAcidReductionData",
+          "/soilAcidificationData",
+          "/crossSectionSampleData",
+          "/irrigationWaterInputFluxData",
+          "/heavyMetalEnterpriseData",
+          "/atmosphericSampleData",
+        ],
+      },
+      {
+        name: "infoManagement",
+        label: t('Menu.infoManagement'),
+        icon: "el-icon-document",
+        routes: ["/IntroductionUpdate"],
+      },
+      {
+        name: "modelManagement",
+        label: t('Menu.modelManagement'),
+        icon: "el-icon-cpu",
+        routes: ["/ModelSelection", "/thres", "/ModelTrain"],
+      },
+      {
+        name: "userManagement",
+        label: t('Menu.userManagement'),
+        icon: "el-icon-user",
+        routes: ["/UserManagement"],
+      },
+    ];
+  } else {
+    return [
+      {
+        name: "introduction",
+        label: t('Menu.swIntroduce'),
+        icon: "el-icon-info-filled",
+        routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"],
+      },
+      {
+        name: "acidmodelmap",
+        label: t('Menu.mapcalulate'),
+        icon: "el-icon-data-analysis",
+        routes: ["/shaoguan_acidmodelmap","nanxiong_acidmodelmap"],
+      },
+      {
+        name: "Calculation",
+        label: t('Menu.reversionmodel'),
+        icon: "el-icon-data-analysis",
+        routes: ["/Calculation", "/SoilAcidReductionIterativeEvolution"],
+      },
+      {
+        name: "AcidNeutralizationModel",
+        label: t('Menu.reducemodel'),
+        icon: "el-icon-data-analysis",
+        routes: ["/AcidNeutralizationModel", "/SoilAcidificationIterativeEvolution"],
+      },
+      {
+        name: "dataStatistics",
+        label: t('Menu.dataStatistics'),
+        icon: "el-icon-pie-chart",
+        routes: [
+          "/DetectionStatistics",
+          "/FarmlandPollutionStatistics",
+          "/LandClutivatesStatistics",
+          "/SoilacidificationStatistics",
+        ],
+      },
+    ];
+  }
 });
 
 const activeName = ref(tabs.value[0]?.name || "");
@@ -314,13 +279,11 @@ watch(
   { immediate: true }
 );
 
-// 点击 tab
 const activeAsideTab = ref(activeName.value || "");
 const handleClick = (tab: any, _event?: Event) => {
   activeAsideTab.value = tab.name || tab.props?.name;
 };
 
-// 动态加载侧边栏
 const AsideComponent = computed(() => {
   return [
     "dataManagement",
@@ -332,50 +295,47 @@ const AsideComponent = computed(() => {
     : defineAsyncComponent(() => import("./AppAside.vue"));
 });
 
-// 是否显示侧边栏
 const showAside = computed(
   () =>
     !isFullScreen.value && !["cropRiskAssessment"].includes(activeName.value)
 );
 
-// ============ 显示全局消息 ============
+const mainStyle = computed(() => ({
+  padding: ["mapView", "infoManagement"].includes(activeName.value)
+    ? "0"
+    : "20px",
+  overflow: "hidden",
+}));
 
-// 登出逻辑
+// 登出逻辑 - 支持多语言
 const handleLogout = async () => {
   try {
-    await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
-      confirmButtonText: "确定",
-      cancelButtonText: "取消",
-      type: "warning",
-    });
+    await ElMessageBox.confirm(
+      t('logout.confirmMessage'), 
+      t('logout.confirmTitle'),
+      {
+        confirmButtonText: t('logout.confirmButton'),
+        cancelButtonText: t('logout.cancelButton'),
+        type: "warning",
+      }
+    );
     await logout();
     tokenStore.clearToken();
     
-    // 使用ElMessage而不是全局消息提示
-    ElMessage.success("退出登录成功");
+    ElMessage.success(t('logout.logoutSuccess'));
     
-    // 延迟跳转,让用户看到提示消息
     setTimeout(() => {
       router.push("/login");
     }, 1000);
   } catch (error) {
-    // 区分用户取消操作和真正的错误
     if (error === 'cancel' || error?.toString().includes('cancel')) {
-      console.log("用户取消退出登录");
+      // 用户取消操作
     } else {
-      ElMessage.error("退出失败,请重试");
+      ElMessage.error(t('logout.logoutFailed'));
       console.error("退出登录错误:", error);
     }
   }
 };
-
-// 内容区样式
-const mainStyle = computed(() => ({
-  padding: ["mapView", "infoManagement"].includes(activeName.value)
-    ? "0"
-    : "20px",
-  overflow: "hidden",
-}));
 </script>
 
 <style>
@@ -746,4 +706,72 @@ const mainStyle = computed(() => ({
   justify-content: flex-end;
   flex-shrink: 0; /* 防止缩小 */
 }
+
+/* 语言切换按钮样式 */
+.lang-toggle-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  border: 2px solid rgba(255, 255, 255, 0.6);
+  border-radius: 20px;
+  background: rgba(255, 255, 255, 0.15);
+  backdrop-filter: blur(8px);
+  color: #ffffff;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  outline: none;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.lang-toggle-btn:hover {
+  background: rgba(255, 255, 255, 0.25);
+  border-color: rgba(255, 255, 255, 0.9);
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
+}
+
+.lang-toggle-btn:active {
+  transform: translateY(0);
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+}
+
+.lang-toggle-btn .el-icon-globe {
+  font-size: 18px;
+  color: #ffffff;
+}
+
+.lang-toggle-btn .lang-label {
+  font-size: 13px;
+  font-weight: 600;
+  letter-spacing: 0.5px;
+}
+
+/* 浅色文字模式(特殊背景页面) */
+.lang-toggle-btn.light-text {
+  border-color: rgba(255, 255, 255, 0.6);
+  color: #ffffff;
+}
+
+.lang-toggle-btn.light-text .el-icon-globe {
+  color: #ffffff;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .lang-toggle-btn {
+    padding: 6px 12px;
+    font-size: 12px;
+  }
+  
+  .lang-toggle-btn .el-icon-globe {
+    font-size: 16px;
+  }
+  
+  .lang-toggle-btn .lang-label {
+    font-size: 11px;
+  }
+}
 </style>

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

@@ -19,46 +19,46 @@ export const tabMenuMap: Record<string, MenuItem[]> = {
   introduction: [
     {
       index: "/SoilPro",
-      label: "软件简介",
+      label: "Menu.softwareIntroduction",
       icon: InfoFilled,
     },
     {
       index: "/Overview",
-      label: "项目简介",
+      label: "Menu.projectOverview",
       icon: Collection,
     },
     {
       index: "/ResearchFindings",
-      label: "研究成果",
+      label: "Menu.researchFindings",
       icon: Histogram,
     },
     {
       index: "/Unit",
-      label: "团队信息",
+      label: "Menu.teamInfo",
       icon: HelpFilled,
     },
   ],
   acidmodelmap: [
     {
       index: "acidmodelmap",
-      label: "土壤酸化地块级预测",
+      label: "Menu.soilAcidPlotPrediction",
       icon: Location,
       children: [
         {
           index: '/shaoguan_acidmodelmap',
-          label: '韶关土壤酸化地图',
+          label: 'Menu.shaoguanAcidMap',
           icon: Location
         },
         {
           index: '/nanxiong_acidmodelmap',
-          label: '南雄土壤酸化地图',
+          label: 'Menu.nanxiongAcidMap',
           icon: Location
         }
       ]
     },
     {
       index: "/pHPrediction",
-      label: "土壤pH预测",
+      label: "Menu.soilPH Prediction",
       icon: Location
     },
     {
@@ -70,46 +70,46 @@ export const tabMenuMap: Record<string, MenuItem[]> = {
   Calculation: [
     {
       index: "/Calculation",
-      label: "土壤反酸预测",
+      label: "Menu.soilAcidReductionPrediction",
       icon: List,
     },
     {
       index: "/SoilAcidReductionIterativeEvolution",
-      label: "反酸模型显示",
+      label: "Menu.acidReductionModelDisplay",
       icon: List,
     },
   ],
   AcidNeutralizationModel: [
     {
       index: "/AcidNeutralizationModel",
-      label: "土壤降酸预测",
+      label: "Menu.soilAcidNeutralizationPrediction",
       icon: List,
     },
     {
       index: "/SoilAcidificationIterativeEvolution",
-      label: "降酸模型显示",
+      label: "Menu.acidNeutralizationModelDisplay",
       icon: List,
     },
   ],
   dataStatistics: [
     {
       index: "/DetectionStatistics",
-      label: "检测信息统计",
+      label: "Menu.detectionStatistics",
       icon: List,
     },
     {
       index: "/FarmlandPollutionStatistics",
-      label: "土壤镉含量统计",
+      label: "Menu.soilCadmiumStatistics",
       icon: List,
     },
     {
       index: "/LandClutivatesStatistics",
-      label: "作物风险评估系统",
+      label: "Menu.cropRiskAssessment",
       icon: List,
     },
     {
       index: "/SoilacidificationStatistics",
-      label: "土壤酸化统计",
+      label: "Menu.soilAcidificationStatistics",
       icon: List,
     },
   ],

+ 74 - 32
src/components/soilStatictics/reducedataStatistics.vue

@@ -1,14 +1,30 @@
 <template>
   <div class="chart-container">
-    <h3 class="title">酸化缓解Q_delta_pH柱状图</h3>
+    <h3 class="title">{{ $t('AcidificationDataStatistics.reductionTitle') }}</h3>
     <div class="echarts-box" ref="chartRef"></div>
+    
+    <!-- 加载状态提示 -->
+    <div v-if="isLoading" class="loading-tip">
+      {{ $t('DetectionStatistics.dataLoading') }}
+    </div>
+    
+    <!-- 错误提示 -->
+    <div v-if="errorMessage && !isLoading" class="error-tip">
+      {{ errorMessage }}
+      <el-button type="primary" size="small" @click="fetchData">
+        {{ $t('SoilCdStatistics.retry') }}
+      </el-button>
+    </div>
   </div>
 </template>
 
 <script setup>
 import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
 import * as echarts from 'echarts';
-import { api5000 } from '@/utils/request'; // 导入 api5000 实例
+import { api5000 } from '@/utils/request';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 // 图表相关
 const chartRef = ref(null);
@@ -19,42 +35,43 @@ const chartData = ref([]);
 const isLoading = ref(false);
 const errorMessage = ref('');
 
-// 接口地址(使用相对路径)
-const API_URL = '/api/table-data?table_name=dataset_77'; // 相对路径
+// 接口地址
+const API_URL = '/api/table-data?table_name=dataset_77';
 
 // 获取数据函数
 const fetchData = async () => {
-  // 重置状态
   errorMessage.value = '';
   isLoading.value = true;
   
   try {
-    // 使用 api5000 替代 axios
     const response = await api5000.get(API_URL);
     
-    // 检查响应数据是否符合预期格式
     if (response.data && response.data.success) {
       if (Array.isArray(response.data.data) && response.data.data.length > 0) {
-        if (response.data.data[0].id !== undefined && response.data.data[0].Q_delta_pH !== undefined) {
-          chartData.value = response.data.data;
+        // 检查数据结构,确保有 id 和 Q_delta_pH 字段
+        const validData = response.data.data.filter(item => 
+          item.id !== undefined && item.Q_delta_pH !== undefined
+        );
+        
+        if (validData.length > 0) {
+          chartData.value = validData;
         } else {
-          errorMessage.value = '数据字段缺失:缺少 id 或 Delta_pH';
+          errorMessage.value = t('DetectionStatistics.noValidData');
         }
       } else {
-        errorMessage.value = '未获取到有效数据,请稍后重试';
+        errorMessage.value = t('DetectionStatistics.noValidData');
       }
     } else {
-      errorMessage.value = 'API返回失败状态';
+      errorMessage.value = t('AcidificationDataStatistics.apiError');
     }
   } catch (error) {
     console.error('数据获取失败:', error);
-    // 根据错误类型显示不同信息
     if (error.response) {
-      errorMessage.value = `请求失败 (${error.response.status}): ${error.response.statusText}`;
+      errorMessage.value = `${t('AcidificationDataStatistics.requestError')} (${error.response.status})`;
     } else if (error.request) {
-      errorMessage.value = '未收到响应,请检查网络连接';
+      errorMessage.value = t('AcidificationDataStatistics.networkError');
     } else {
-      errorMessage.value = '请求发生错误,请稍后重试';
+      errorMessage.value = t('AcidificationDataStatistics.requestError');
     }
   } finally {
     isLoading.value = false;
@@ -63,7 +80,6 @@ const fetchData = async () => {
 
 // 初始化图表
 const initChart = () => {
-  // 确保DOM已挂载且有数据
   if (!chartRef.value) {
     console.error('图表容器未找到');
     return;
@@ -74,25 +90,20 @@ const initChart = () => {
     return;
   }
   
-  // 销毁已有实例
   if (myChart) {
     myChart.dispose();
   }
   
-  // 初始化图表
   myChart = echarts.init(chartRef.value);
   
-  // 提取数据
   const xAxisData = chartData.value.map(item => item.id);
   const seriesData = chartData.value.map(item => ({
     value: item.Q_delta_pH,
     itemStyle: {
-      // 根据值的正负设置不同颜色
       color: item.Q_delta_pH >= 0 ? '#0F52BA' : '#F44336'
     }
   }));
   
-  // 图表配置
   const option = {
     tooltip: {
       trigger: 'axis',
@@ -100,8 +111,15 @@ const initChart = () => {
         type: 'shadow'
       },
       formatter: function(params) {
-        const item = chartData.value.find(d => d.id === params[0].name);
-        return `ID: ${item.id}<br/>Q_delta_pH: ${item.Q_delta_pH.toFixed(4)}`;
+        // 安全地获取数据,避免 undefined 错误
+        const itemName = params[0].name;
+        const item = chartData.value.find(d => d.id === itemName);
+        
+        if (item) {
+          return `ID: ${item.id}<br/>Q_delta_pH: ${item.Q_delta_pH.toFixed(4)}`;
+        } else {
+          return `ID: ${itemName}<br/>Q_delta_pH: ${params[0].value.toFixed(4)}`;
+        }
       }
     },
     grid: {
@@ -143,34 +161,30 @@ const initChart = () => {
     ]
   };
   
-  // 设置图表配置
   myChart.setOption(option);
 };
 
-// 监听数据变化,重新渲染图表
+// 监听数据变化
 watch(chartData, () => {
   nextTick(()=>{
     initChart();  
   })
 });
 
-// 窗口大小变化时重绘图表
 const handleResize = () => {
   if (myChart) {
-    myChart.resize();
+    setTimeout(()=>{
+      myChart.resize();
+    },100)
   }
 };
 
-// 组件挂载时初始化
 onMounted(async() => {
-  // 首次加载数据
   await fetchData();
   initChart();
-  // 监听窗口大小变化
   window.addEventListener('resize', handleResize);
 });
 
-// 组件卸载时清理
 onUnmounted(() => {
   if (myChart) {
     myChart.dispose();
@@ -185,6 +199,7 @@ onUnmounted(() => {
   height: 470px;
   padding: 20px;
   box-sizing: border-box;
+  position: relative;
 }
 
 .echarts-box {
@@ -197,5 +212,32 @@ onUnmounted(() => {
 
 .title {
   text-align: center;
+  margin-bottom: 15px;
+  font-size: 16px;
+  font-weight: bold;
+  color: #333;
+}
+
+.loading-tip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  font-size: 14px;
+  color: #666;
+}
+
+.error-tip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  text-align: center;
+  color: #f44336;
+  font-size: 14px;
+}
+
+.error-tip .el-button {
+  margin-left: 10px;
 }
 </style>

+ 80 - 34
src/components/soilStatictics/refluxcedataStatictics.vue

@@ -1,14 +1,30 @@
 <template>
   <div class="chart-container">
-    <h3 class="title">酸化加剧Delta_pH_105day柱状图</h3>
+    <h3 class="title">{{ $t('AcidificationDataStatistics.refluxTitle') }}</h3>
     <div class="echarts-box" ref="chartRef"></div>
+    
+    <!-- 加载状态提示 -->
+    <div v-if="isLoading" class="loading-tip">
+      {{ $t('DetectionStatistics.dataLoading') }}
+    </div>
+    
+    <!-- 错误提示 -->
+    <div v-if="errorMessage && !isLoading" class="error-tip">
+      {{ errorMessage }}
+      <el-button type="primary" size="small" @click="fetchData">
+        {{ $t('SoilCdStatistics.retry') }}
+      </el-button>
+    </div>
   </div>
 </template>
 
 <script setup>
 import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
 import * as echarts from 'echarts';
-import { api5000 } from '@/utils/request'; // 导入 api5000 实例
+import { api5000 } from '@/utils/request';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 // 图表相关
 const chartRef = ref(null);
@@ -19,42 +35,43 @@ const chartData = ref([]);
 const isLoading = ref(false);
 const errorMessage = ref('');
 
-// 接口地址(使用相对路径)
-const API_URL = '/api/table-data?table_name=dataset_81'; // 相对路径
+// 接口地址
+const API_URL = '/api/table-data?table_name=dataset_81';
 
 // 获取数据函数
 const fetchData = async () => {
-  // 重置状态
   errorMessage.value = '';
   isLoading.value = true;
   
   try {
-    // 使用 api5000 替代 axios
     const response = await api5000.get(API_URL);
     
-    // 检查响应数据是否符合预期格式
     if (response.data && response.data.success) {
       if (Array.isArray(response.data.data) && response.data.data.length > 0) {
-        if (response.data.data[0].id !== undefined && response.data.data[0].Delta_pH_105day !== undefined) {
-          chartData.value = response.data.data;
+        // 检查数据结构,确保有 id 和 Delta_pH_105day 字段
+        const validData = response.data.data.filter(item => 
+          item.id !== undefined && item.Delta_pH_105day !== undefined
+        );
+        
+        if (validData.length > 0) {
+          chartData.value = validData;
         } else {
-          errorMessage.value = '数据字段缺失:缺少 id 或 Delta_pH';
+          errorMessage.value = t('DetectionStatistics.noValidData');
         }
       } else {
-        errorMessage.value = '未获取到有效数据,请稍后重试';
+        errorMessage.value = t('DetectionStatistics.noValidData');
       }
     } else {
-      errorMessage.value = 'API返回失败状态';
+      errorMessage.value = t('AcidificationDataStatistics.apiError');
     }
   } catch (error) {
     console.error('数据获取失败:', error);
-    // 根据错误类型显示不同信息
     if (error.response) {
-      errorMessage.value = `请求失败 (${error.response.status}): ${error.response.statusText}`;
+      errorMessage.value = `${t('AcidificationDataStatistics.requestError')} (${error.response.status})`;
     } else if (error.request) {
-      errorMessage.value = '未收到响应,请检查网络连接';
+      errorMessage.value = t('AcidificationDataStatistics.networkError');
     } else {
-      errorMessage.value = '请求发生错误,请稍后重试';
+      errorMessage.value = t('AcidificationDataStatistics.requestError');
     }
   } finally {
     isLoading.value = false;
@@ -63,7 +80,6 @@ const fetchData = async () => {
 
 // 初始化图表
 const initChart = () => {
-  // 确保DOM已挂载且有数据
   if (!chartRef.value) {
     console.error('图表容器未找到');
     return;
@@ -74,25 +90,20 @@ const initChart = () => {
     return;
   }
   
-  // 销毁已有实例
   if (myChart) {
     myChart.dispose();
   }
   
-  // 初始化图表
   myChart = echarts.init(chartRef.value);
   
-  // 提取数据
   const xAxisData = chartData.value.map(item => item.id);
   const seriesData = chartData.value.map(item => ({
     value: item.Delta_pH_105day,
     itemStyle: {
-      // 根据值的正负设置不同颜色
       color: item.Delta_pH_105day >= 0 ? '#0F52BA' : '#F44336'
     }
   }));
   
-  // 图表配置
   const option = {
     tooltip: {
       trigger: 'axis',
@@ -100,8 +111,15 @@ const initChart = () => {
         type: 'shadow'
       },
       formatter: function(params) {
-        const item = chartData.value.find(d => d.id === params[0].name);
-        return `ID: ${item.id}<br/>Delta_pH_105day: ${item.Delta_pH_105day.toFixed(4)}`;
+        // 安全地获取数据,避免 undefined 错误
+        const itemName = params[0].name;
+        const item = chartData.value.find(d => d.id === itemName);
+        
+        if (item) {
+          return `ID: ${item.id}<br/>Delta_pH_105day: ${item.Delta_pH_105day.toFixed(4)}`;
+        } else {
+          return `ID: ${itemName}<br/>Delta_pH_105day: ${params[0].value.toFixed(4)}`;
+        }
       }
     },
     grid: {
@@ -143,34 +161,34 @@ const initChart = () => {
     ]
   };
   
-  // 设置图表配置
   myChart.setOption(option);
 };
 
-// 监听数据变化,重新渲染图表
+// 监听数据变化
 watch(chartData, () => {
-  nextTick(()=>{
-    initChart();  
+  nextTick(() => {
+    if (myChart) {
+      myChart.clear(); // 清除现有内容
+      myChart.setOption({}); // 重置选项
+    }
+    initChart(); // 重新初始化
   })
 });
 
-// 窗口大小变化时重绘图表
 const handleResize = () => {
   if (myChart) {
-    myChart.resize();
+    setTimeout(() => {
+      myChart.resize();
+    }, 100);
   }
 };
 
-// 组件挂载时初始化
 onMounted(async() => {
-  // 首次加载数据
   await fetchData();
   initChart();
-  // 监听窗口大小变化
   window.addEventListener('resize', handleResize);
 });
 
-// 组件卸载时清理
 onUnmounted(() => {
   if (myChart) {
     myChart.dispose();
@@ -185,6 +203,7 @@ onUnmounted(() => {
   height: 470px;
   padding: 20px;
   box-sizing: border-box;
+  position: relative;
 }
 
 .echarts-box {
@@ -197,5 +216,32 @@ onUnmounted(() => {
 
 .title {
   text-align: center;
+  margin-bottom: 15px;
+  font-size: 16px;
+  font-weight: bold;
+  color: #333;
+}
+
+.loading-tip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  font-size: 14px;
+  color: #666;
+}
+
+.error-tip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  text-align: center;
+  color: #f44336;
+  font-size: 14px;
+}
+
+.error-tip .el-button {
+  margin-left: 10px;
 }
 </style>

+ 98 - 88
src/components/soilcdStatistics/cropcdStatictics.vue

@@ -1,10 +1,10 @@
 <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>
+      <h1 class="text-xl font-bold text-gray-800">{{ $t('SoilCdStatistics.cropCdAnalysis') }}</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 class="stat-value text-lg">{{ $t('SoilCdStatistics.sampleCount') }}: {{ stats.samples }}</div>
         </div>
       </div>
     </div>
@@ -12,32 +12,31 @@
     <!-- 加载状态 -->
     <div v-if="isLoading" class="loading-overlay">
       <span class="spinner"></span>
-      <span class="ml-3 text-gray-700">数据加载中...</span>
+      <span class="ml-3 text-gray-700">{{ $t('DetectionStatistics.dataLoading') }}</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>
+      <p>{{ $t('SoilCdStatistics.dataLoadFailed') }}: {{ error.message }}</p>
+      <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">{{ $t('SoilCdStatistics.retry') }}</button>
     </div>
     
-    
-    <!-- 1️⃣ 作物态Cd指标 -->
+    <!-- 1️⃣ 作物态 Cd 指标 -->
     <section class="mb-4 chart-container">
-      <h3 class="section-title text-base font-semibold">作物态Cd主要指标</h3>
+      <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.cropCdMainIndicators') }}</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>
+      <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.mainNutrients') }}</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>
+        <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.otherProperties') }}</h3>
       </div>
       <div ref="extraBoxChart" style="width: 100%; height: 400px;"></div>
     </section>
@@ -49,6 +48,10 @@
 import { ref, onMounted, nextTick } from 'vue';
 import * as echarts from 'echarts';
 import { api8000 } from '@/utils/request'; // 导入 api8000 实例import {api8000} from '@/utils/request'
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+const {locale} = useI18n()
 
 
 // 图表实例引用
@@ -76,46 +79,46 @@ const pollutionStats = ref([]);
 const nutrientStats = ref([]);
 const extraStats = ref([]);
 
-// 字段配置(根据接口返回的作物态Cd数据结构定义)
-const fieldConfig = {
- pollution: [
+// 字段配置 - 改为函数形式
+const getFieldConfig = () => ({
+  pollution: [
     { 
       key: '002_0002IDW', 
-      name: '粉粒组分质量占比',  // 横坐标保持原标识
-      color: '#5470c6' ,
-      unit:"%",
+      name: t('SoilCdStatistics.siltContent'),
+      color: '#5470c6',
+      unit: t('SoilCdStatistics.unitPercent'),
       convert: false
     },
     { 
       key: '02_002IDW', 
-      name: '砂粒组分质量占比',  // 横坐标保持原标识
-      color: '#91cc75' ,
-      unit:"%",
+      name: t('SoilCdStatistics.sandContent'),
+      color: '#91cc75',
+      unit: t('SoilCdStatistics.unitPercent'),
       convert: false
     },
     { 
       key: '2_02IDW', 
-      name: '石砾组分质量占比',  // 横坐标保持原标识
-      color: '#fac858' ,
-      unit:"%",
+      name: t('SoilCdStatistics.gravelContent'),
+      color: '#fac858',
+      unit: t('SoilCdStatistics.unitPercent'),
       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}
+    { key: 'AvaK_IDW', name: t('SoilCdStatistics.availablePotassium'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'SAvaK_IDW', name: t('SoilCdStatistics.exchangeablePotassium'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'AvaP_IDW', name: t('SoilCdStatistics.availablePhosphorus'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'TMn_IDW', name: t('SoilCdStatistics.totalManganese'), color: '#73c0de', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'TN_IDW', name: t('SoilCdStatistics.totalNitrogen'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 },
+    { key: 'TS_IDW', name: t('SoilCdStatistics.totalSulfur'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), 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},
+    { key: 'TAl_IDW', name: t('SoilCdStatistics.totalAluminum'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TCa_IDW', name: t('SoilCdStatistics.totalCalcium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TFe_IDW', name: t('SoilCdStatistics.totalIron'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TMg_IDW', name: t('SoilCdStatistics.totalMagnesium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
   ]
-};
+});
 
 
 // 数据请求 - 增强错误处理和调试
@@ -123,13 +126,13 @@ const fetchData = async () => {
   try {
     isLoading.value = true;
     const apiUrl = '/api/vector/stats/CropCd_input_data';
-    console.log('正在请求数据:', apiUrl);
+    // console.log('正在请求数据:', apiUrl);
     
     const response = await api8000.get(apiUrl);
-    console.log('API响应:', response);
+    // console.log('API响应:', response);
     
     // 调试:输出响应结构
-    console.log('响应数据:', response.data);
+    // console.log('响应数据:', response.data);
     
     // 处理不同的响应格式
     let processedData;
@@ -139,7 +142,7 @@ const fetchData = async () => {
       throw new Error('无法解析API返回的数据结构');
     }
     
-    console.log('处理后的数据:', processedData);
+    // console.log('处理后的数据:', processedData);
     return processedData;
   } catch (err) {
     console.error('数据请求失败:', err);
@@ -155,31 +158,29 @@ const buildBoxplotData = (statsArray) => {
   });
 };
 
-// 初始化作物态Cd指标图表
+// 初始化作物态 Cd 指标图表
 const initPollutionChart = () => {
   nextTick(() => {
-    // 若图表实例已存在,先销毁避免内存泄漏
     if (chartInstanceCd) chartInstanceCd.dispose();
-    // 校验 DOM 存在性(防止 ref 未关联到有效 DOM)
     if (!cdBarChart.value) return;
-    // 初始化 ECharts 实例
     chartInstanceCd = echarts.init(cdBarChart.value);
     
-    const xAxisData = fieldConfig.pollution.map(f => f.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.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)}`
+        formatter: (params) => `${params[0].name}<br/>${t('SoilCdStatistics.averageValue')}: ${params[0].value.toFixed(4)}`
       },
       grid: { top: 40, right: 15, bottom: '18%', left: '10%' },
       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 },
+        name: t('SoilCdStatistics.averageValue'), type: "bar",
+        itemStyle: { color: (p) => currentFieldConfig.pollution[p.dataIndex].color },
         data: barData
       }]
     });
@@ -193,16 +194,17 @@ const initNutrientChart = () => {
     if (!nutrientBoxChart.value) return;
     chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
     
-    const xAxisData = fieldConfig.nutrient.map(f => f.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.nutrient.map(f => f.name);
     const boxData = buildBoxplotData(nutrientStats.value);
     
     chartInstanceNutrient.setOption({
-      title: { text: "主要养分元素分布", left: 'center', textStyle: { fontSize: 14 } },
+      title: { text: t('SoilCdStatistics.nutrientDistribution'), 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);
+          const fieldConfigItem = currentFieldConfig.nutrient.find(f => f.key === stat.key);
           return formatTooltip(stat, fieldConfigItem?.unit);
         }
       },
@@ -219,7 +221,7 @@ const initNutrientChart = () => {
         axisLabel: { fontSize: 11 } 
       },
       series: [{
-        name: '养分元素', 
+        name: t('SoilCdStatistics.mainNutrients'), 
         type: "boxplot",
         itemStyle: { color: '#ee6666', borderColor: '#fac858' },
         data: boxData
@@ -230,56 +232,60 @@ const initNutrientChart = () => {
 
 // 初始化其他理化性质图表
 const initExtraChart = () => {
-  const xAxisData = fieldConfig.extra.map(f => f.name);
+  const currentFieldConfig = getFieldConfig();
+  const xAxisData = currentFieldConfig.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} },
-        yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
-        series: [{
-          name: '理化性质', type: "boxplot",
-           itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
-           data: boxData
-        }]
-        });
-      });
+  nextTick(() => {
+    if (chartInstanceExtra) chartInstanceExtra.dispose();
+    chartInstanceExtra = echarts.init(extraBoxChart.value);
+    chartInstanceExtra.setOption({
+      title: { text: t('SoilCdStatistics.propertiesDistribution'), 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 } },
+      yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
+      series: [{
+        name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
+        itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
+        data: boxData
+      }]
+    });
+  });
 };
 
-// 格式化Tooltip
+
+// 格式化 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;color:#f56c6c">${stat?.name || t('DetectionStatistics.noValidData')}</div><div>${t('DetectionStatistics.noValidData')}</div>`;
   }
   return `<div style="font-weight:bold">${stat.name}</div>
     <div style="margin-top:8px">
-      <div>最小值:<span style="color:#5a5;">${stat.min.toFixed(4)} ${unit}</span></div>
-      <div>下四分位:<span style="color:#d87a80;">${stat.q1.toFixed(4)} ${unit}</span></div>
-      <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)} ${unit}</span></div>
-      <div>上四分位:<span style="color:#d87a80;">${stat.q3.toFixed(4)} ${unit}</span></div>
-      <div>最大值:<span style="color:#5a5;">${stat.max.toFixed(4)} ${unit}</span></div>
+      <div>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${stat.min.toFixed(4)} ${unit}</span></div>
+      <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${stat.q1.toFixed(4)} ${unit}</span></div>
+      <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)} ${unit}</span></div>
+      <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${stat.q3.toFixed(4)} ${unit}</span></div>
+      <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${stat.max.toFixed(4)} ${unit}</span></div>
     </div>`;
 };
 
+
 // 初始化图表主流程
 const initCharts = async () => {
   try {
     isLoading.value = true;
     error.value = null;
     
-    const statsData = await fetchData(); // 新接口返回的统计数据
+    const statsData = await fetchData();
+    const currentFieldConfig = getFieldConfig();
     
-    // -------- 1. 处理「作物态Cd指标」统计 --------
-    pollutionStats.value = fieldConfig.pollution.map(field => {
-      const fieldStats = statsData[field.key]; // 从接口数据中取对应字段的统计
+    // -------- 1. 处理「作物态 Cd 指标」统计 --------
+    pollutionStats.value = currentFieldConfig.pollution.map(field => {
+      const fieldStats = statsData[field.key];
       if (!fieldStats) {
         return { 
           key: field.key, 
@@ -292,7 +298,6 @@ const initCharts = async () => {
           avg: null 
         };
       }
-      // (可选)单位转换:若接口返回原始值,需按fieldConfig的convert规则转换
       let min = fieldStats.min;
       let q1 = fieldStats.q1;
       let median = fieldStats.median;
@@ -320,7 +325,7 @@ const initCharts = async () => {
     });
 
     // -------- 2. 处理「主要养分元素」统计 --------
-    nutrientStats.value = fieldConfig.nutrient.map(field => {
+    nutrientStats.value = currentFieldConfig.nutrient.map(field => {
       const fieldStats = statsData[field.key];
       if (!fieldStats) {
         return { 
@@ -334,7 +339,6 @@ const initCharts = async () => {
           avg: null 
         };
       }
-      // (可选)单位转换
       let min = fieldStats.min;
       let q1 = fieldStats.q1;
       let median = fieldStats.median;
@@ -362,7 +366,7 @@ const initCharts = async () => {
     });
 
     // -------- 3. 处理「其他理化性质」统计 --------
-    extraStats.value = fieldConfig.extra.map(field => {
+    extraStats.value = currentFieldConfig.extra.map(field => {
       const fieldStats = statsData[field.key];
       if (!fieldStats) {
         return { 
@@ -376,7 +380,6 @@ const initCharts = async () => {
           avg: null 
         };
       }
-      // (可选)单位转换
       let min = fieldStats.min;
       let q1 = fieldStats.q1;
       let median = fieldStats.median;
@@ -403,13 +406,13 @@ const initCharts = async () => {
       };
     });
 
-    // -------- 更新「样本数量」等汇总统计 --------
-    const firstFieldKey = fieldConfig.pollution[0]?.key;
+    // -------- 更新汇总统计 --------
+    const firstFieldKey = currentFieldConfig.pollution[0]?.key;
     stats.value = {
       cd002Avg: pollutionStats.value.find(s => s.key === '002_0002IDW')?.avg || 0,
       cd02Avg: pollutionStats.value.find(s => s.key === '02_002IDW')?.avg || 0,
       cd2Avg: pollutionStats.value.find(s => s.key === '2_02IDW')?.avg || 0,
-      samples: statsData[firstFieldKey]?.count || 0 // 从接口的count字段取样本数
+      samples: statsData[firstFieldKey]?.count || 0
     };
 
     // 初始化图表
@@ -425,6 +428,7 @@ const initCharts = async () => {
   }
 };
 
+
 // 组件挂载
 onMounted(() => {
   initCharts();
@@ -443,6 +447,12 @@ onMounted(() => {
       .forEach(inst => inst && inst.dispose());
   };
 });
+
+// 监听语言变化
+watch(locale, () => {
+  console.log('语言切换,重新初始化图表');
+  initCharts();
+});
 </script>
 
 <style>

+ 79 - 60
src/components/soilcdStatistics/effcdStatistics.vue

@@ -1,10 +1,10 @@
 <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>
+      <h1 class="text-xl font-bold text-gray-800">{{ $t('SoilCdStatistics.effectiveCdAnalysis') }}</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 class="stat-value text-lg">{{ $t('SoilCdStatistics.sampleCount') }}: {{ stats.samples }}</div>
         </div>
       </div>
     </div>
@@ -12,31 +12,31 @@
     <!-- 加载状态 -->
     <div v-if="isLoading" class="loading-overlay">
       <span class="spinner"></span>
-      <span class="ml-3 text-gray-700">数据加载中...</span>
+      <span class="ml-3 text-gray-700">{{ $t('DetectionStatistics.dataLoading') }}</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>
+      <p>{{ $t('SoilCdStatistics.dataLoadFailed') }}: {{ error.message }}</p>
+      <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">{{ $t('SoilCdStatistics.retry') }}</button>
     </div>
     
-    <!-- 1️⃣ 总Cd & 有效态Cd -->
+    <!-- 1️⃣ 总 Cd & 有效态 Cd -->
     <section class="mb-4 chart-container">
-      <h3 class="section-title text-base font-semibold">污染指标</h3>
+      <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.pollutionIndicators') }}</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>
+      <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.mainNutrients') }}</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>
+        <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.otherProperties') }}</h3>
       </div>
       <div ref="extraBoxChart" style="height: 400px; width: 100%;"></div>
     </section>
@@ -47,6 +47,10 @@
 import { ref, onMounted, nextTick } from 'vue';
 import * as echarts from 'echarts';
 import { api8000 } from '@/utils/request'; // 导入 api8000 实例
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+const { locale } = useI18n();
 
 // 图表实例引用
 const cdBarChart = ref(null);
@@ -72,45 +76,45 @@ const pollutionStats = ref([]);       // 污染指标统计(总镉/有效态
 const nutrientStats = ref([]);        // 养分元素统计
 const extraStats = ref([]);           // 其他理化性质统计
 
-// 字段配置(参考灌溉水的重金属配置方式
-const fieldConfig = {
+// 获取字段配置的函数(每次调用都会使用最新的翻译
+const getFieldConfig = () => ({
   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},
+    { key: 'TCd_IDW', name: t('SoilCdStatistics.totalCadmium'), color: '#5470c6', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'AvaK_IDW', name: t('SoilCdStatistics.availablePotassium'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'AvaP_IDW', name: t('SoilCdStatistics.availablePhosphorus'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'TMn_IDW', name: t('SoilCdStatistics.totalManganese'), color: '#73c0de', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'TN_IDW', name: t('SoilCdStatistics.totalNitrogen'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 },
+    { key: 'TP_IDW', name: t('SoilCdStatistics.totalPhosphorus'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 },
+    { key: 'TK_IDW', name: t('SoilCdStatistics.totalPotassium'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), 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}
+    { key: 'AvaK_IDW', name: t('SoilCdStatistics.availablePotassium'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'AvaP_IDW', name: t('SoilCdStatistics.availablePhosphorus'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'TMn_IDW', name: t('SoilCdStatistics.totalManganese'), color: '#73c0de', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
+    { key: 'TN_IDW', name: t('SoilCdStatistics.totalNitrogen'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 },
+    { key: 'TP_IDW', name: t('SoilCdStatistics.totalPhosphorus'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 },
+    { key: 'TK_IDW', name: t('SoilCdStatistics.totalPotassium'), color: '#fac858', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 },
+    { key: 'TS_IDW', name: t('SoilCdStatistics.totalSulfur'), unit: t('SoilCdStatistics.unitMgKg'), 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}
+    { key: 'TFe_IDW', name: t('SoilCdStatistics.totalIron'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TCa_IDW', name: t('SoilCdStatistics.totalCalcium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TMg_IDW', name: t('SoilCdStatistics.totalMagnesium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TAl_IDW', name: t('SoilCdStatistics.totalAluminum'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false }
   ]
-};
+});
 
 const fetchData = async () => {
   try {
     isLoading.value = true;
     const apiUrl = '/api/vector/stats/EffCd_input_data';
-    console.log('正在请求数据:', apiUrl);
+    // console.log('正在请求数据:', apiUrl);
     
     const response = await api8000.get(apiUrl);
-    console.log('API响应:', response);
+    // console.log('API响应:', response);
     
     // 调试:输出响应结构
-    console.log('响应数据:', response.data);
+    // console.log('响应数据:', response.data);
     
     // 处理不同的响应格式
     let processedData;
@@ -120,7 +124,7 @@ const fetchData = async () => {
       throw new Error('无法解析API返回的数据结构');
     }
     
-    console.log('处理后的数据:', processedData);
+    // console.log('处理后的数据:', processedData);
     return processedData;
   } catch (err) {
     console.error('数据请求失败:', err);
@@ -178,18 +182,20 @@ const calculateFieldStats = (statsData, fieldKey, fieldName, fieldConfigItem) =>
 
 // 批量计算所有字段的统计量(按图表类型缓存)
 const calculateAllStats = (statsData) => {
+  const currentFieldConfig = getFieldConfig(); // 获取最新的字段配置
+
   // 1. 污染指标统计(与x轴顺序一致)
-  pollutionStats.value = fieldConfig.pollution.map(field => 
+  pollutionStats.value = currentFieldConfig.pollution.map(field => 
     calculateFieldStats(statsData, field.key, field.name, field)
   );
   
   // 2. 养分元素统计(与x轴顺序一致)
-  nutrientStats.value = fieldConfig.nutrient.map(field => 
+  nutrientStats.value = currentFieldConfig.nutrient.map(field => 
     calculateFieldStats(statsData, field.key, field.name, field)
   );
   
   // 3. 其他理化性质统计(与x轴顺序一致)
-  extraStats.value = fieldConfig.extra.map(field => 
+  extraStats.value = currentFieldConfig.extra.map(field => 
     calculateFieldStats(statsData, field.key, field.name, field)
   );
   
@@ -216,15 +222,16 @@ const initPollutionChart = () => {
   if (chartInstanceCd) chartInstanceCd.dispose();
   chartInstanceCd = echarts.init(cdBarChart.value);
   
+  const currentFieldConfig = getFieldConfig();
   // 提取x轴标签和数据
-  const xAxisData = fieldConfig.pollution.map(f => f.name);
+  const xAxisData = currentFieldConfig.pollution.map(f => f.name);
   const barData = pollutionStats.value.map(stat => stat.mean);
   
   chartInstanceCd.setOption({
-    title: { text: '主要指标含量对比', left: 'center', textStyle: { fontSize: 14 } },
+    title: { text:  t('SoilCdStatistics.contentComparison'), left: 'center', textStyle: { fontSize: 14 } },
     tooltip: {
       trigger: 'axis',
-      formatter: (params) => `${params[0].name}<br/>平均值: ${params[0].value.toFixed(4)} mg/kg`
+      formatter: (params) => `${params[0].name}<br/>${t('SoilCdStatistics.averageValue')}: ${params[0].value.toFixed(4)} mg/kg`
     },
     grid: { top: 40, right: 15, bottom: 30, left: 40 },
     xAxis: { 
@@ -239,13 +246,13 @@ const initPollutionChart = () => {
     },
     yAxis: { 
       type: "value", 
-      name: '含量 (mg/kg)', 
+      name: `${t('SoilCdStatistics.contentComparison')}(${t('SoilCdStatistics.unitMgKg')})`, 
       nameTextStyle: { fontSize: 12  }, 
       axisLabel: { fontSize: 11 } 
     },
     series: [{
-      name: '平均值', type: "bar",
-      itemStyle: {color: (params) => fieldConfig.pollution[params.dataIndex].color },
+      name: t('SoilCdStatistics.averageValue'), type: "bar",
+      itemStyle: {color: (params) => currentFieldConfig.pollution[params.dataIndex].color },
       data: barData
     }]
   });
@@ -256,11 +263,13 @@ const initNutrientChart = () => {
   if (chartInstanceNutrient) chartInstanceNutrient.dispose();
   chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
   
-  const xAxisData = fieldConfig.nutrient.map(f => f.name);
+  const currentFieldConfig = getFieldConfig();
+
+  const xAxisData = currentFieldConfig.nutrient.map(f => f.name);
   const boxData = buildBoxplotData(nutrientStats.value);
   
   chartInstanceNutrient.setOption({
-    title: { text: "主要养分元素分布", left: 'center', textStyle: { fontSize: 14 } },
+    title: { text: t('SoilCdStatistics.nutrientDistribution'), left: 'center', textStyle: { fontSize: 14 } },
     tooltip: {
       trigger: "item",
       formatter: (params) => formatTooltip(nutrientStats.value[params.dataIndex])
@@ -278,13 +287,13 @@ const initNutrientChart = () => {
     },
     yAxis: { 
       type: "value", 
-      name: '含量(mg/kg)', 
+      name: t('SoilCdStatistics.unitMgKg'), 
       nameTextStyle: { fontSize: 12 }, 
       axisLabel: { fontSize: 11 } 
     },
     series: [{
-      name: '养分元素', type: "boxplot",
-      itemStyle: { color: (params) => fieldConfig.nutrient[params.dataIndex].color },
+      name: t('SoilCdStatistics.mainNutrients'), type: "boxplot",
+      itemStyle: { color: (params) => currentFieldConfig.nutrient[params.dataIndex].color },
       data: boxData
     }]
   });
@@ -292,14 +301,16 @@ const initNutrientChart = () => {
 
 // 初始化其他理化性质图表(箱线图)
 const initExtraChart = () => {
-  const xAxisData = fieldConfig.extra.map(f => f.name);
+  const currentFieldConfig = getFieldConfig();
+
+  const xAxisData = currentFieldConfig.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 } },
+          title: { text: t('SoilCdStatistics.propertiesDistribution'), left: 'center', textStyle: { fontSize: 14 } },
           tooltip: {
             trigger: "item",
             formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
@@ -308,26 +319,28 @@ const initExtraChart = () => {
           xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11 } },
           yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
           series: [{
-            name: '理化性质', type: "boxplot",
-            itemStyle: { color: (params) => fieldConfig.extra[params.dataIndex].color },
+            name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
+            itemStyle: { color: (params) => currentFieldConfig.extra[params.dataIndex].color },
             data: boxData
           }]
         });
   });
 };
 
-// 格式化Tooltip(复用缓存的统计数据)
+
+// Tooltip 格式化
 const formatTooltip = (stat) => {
-  if (!stat || !stat.min) {
-    return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</div>`;
+  if (!stat || stat.min === null) {
+    return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || t('DetectionStatistics.noValidData')}</div><div>${t('DetectionStatistics.noValidData')}</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>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
     </div>`;
 };
 
@@ -353,6 +366,12 @@ const initCharts = async () => {
   }
 };
 
+// 监听语言变化
+watch(locale, () => {
+  // 语言切换后重新初始化所有图表
+  initCharts();
+});
+
 // 组件挂载
 onMounted(() => {
   initCharts();

+ 64 - 58
src/components/soilcdStatistics/fluxcdStatictics.vue

@@ -1,30 +1,30 @@
 <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>
+      <h1 class="text-xl font-bold text-gray-800">{{ $t('SoilCdStatistics.fluxCdAnalysis') }}</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 class="stat-value text-lg">{{ $t('SoilCdStatistics.sampleCount') }}: {{ stats.samples }}</div>
         </div>
       </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>
+      <p>{{ $t('SoilCdStatistics.dataLoadFailed') }}: {{ error.message }}</p>
+      <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">{{ $t('SoilCdStatistics.retry') }}</button>
     </div>
     
-    <!-- 1. 初始Cd 单独箱线图 -->
+    <!-- 1. 初始 Cd 单独箱线图 -->
     <section class="mb-6 chart-container">
-      <h3 class="section-title text-base font-semibold">初始Cd(Initial_Cd)分布箱线图</h3>
+      <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.initialCdDistribution') }}</h3>
       <div ref="initialCdChart" style="width: 100%; height: 400px;"></div>
       <div v-if="isLoading" class="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center">
         <div class="spinner"></div>
       </div>
       <div v-if="error && !chartInstanceInitial" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
-        <p class="text-yellow-700">图表初始化失败: {{ error.message }}</p>
+        <p class="text-yellow-700">{{ $t('SoilCdStatistics.chartInitFailed') }}: {{ error.message }}</p>
         <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initInitialCdChart">
-          重新尝试初始化
+          {{ $t('SoilCdStatistics.reloadInit') }}
         </button>
       </div>
     </section>
@@ -32,18 +32,16 @@
     <!-- 2. 其他指标 合并箱线图 -->
     <section class="mb-6 chart-container">
       <div class="flex flex-wrap justify-between items-center mb-4">
-        <h3 class="section-title text-base font-semibold">其他通量Cd指标分布箱线图</h3>
+        <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.otherFluxIndicators') }}</h3>
       </div>
       <div ref="otherIndicatorsChart" style="width: 100%; height: 400px;"></div>
-      <!-- 容器内的加载遮罩 -->
       <div v-if="isLoading" class="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center">
         <div class="spinner"></div>
       </div>
-      <!-- 错误提示(保留重试按钮) -->
       <div v-if="error && !chartInstanceOther" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
-        <p class="text-yellow-700">图表初始化失败: {{ error.message }}</p>
+        <p class="text-yellow-700">{{ $t('SoilCdStatistics.chartInitFailed') }}: {{ error.message }}</p>
         <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initOtherIndicatorsChart">
-          重新尝试初始化
+          {{ $t('SoilCdStatistics.reloadInit') }}
         </button>
       </div>
     </section>
@@ -54,6 +52,10 @@
 import { ref, onMounted, nextTick } from 'vue';
 import * as echarts from 'echarts';
 import { api8000 } from '@/utils/request'; // 导入 api8000 实例
+import { useI18n } from 'vue-i18n';
+
+const {locale} = useI18n()
+const { t } = useI18n();
 
 // 图表容器 & 实例
 const initialCdChart = ref(null);
@@ -71,31 +73,31 @@ const initialCdStats = ref([]);
 const otherIndicatorsStats = ref([]);
 
 // 字段配置
-const fieldConfig = {
+const getFieldConfig = () => ({
   initialCd: [
-    { key: 'Initial_Cd', name: '土壤初始Cd总量', color: '#5470c6' }
+    { key: 'Initial_Cd', name: t('SoilCdStatistics.initialCadmiumTotal'), 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' },
+    { key: 'DQCJ_Cd', name: t('SoilCdStatistics.atmosphericDepositionInput'), color: '#91cc75' },
+    { key: 'GGS_Cd', name: t('SoilCdStatistics.irrigationWaterInput'), color: '#fac858' },
+    { key: 'NCP_Cd', name: t('SoilCdStatistics.agriculturalInput'), color: '#ee6666' },
+    { key: 'DX_Cd', name: t('SoilCdStatistics.undergroundLeaching'), color: '#73c0de' },
+    { key: 'DB_Cd', name: t('SoilCdStatistics.surfaceRunoff'), color: '#38b2ac' },
+    { key: 'ZL_Cd', name: t('SoilCdStatistics.grainRemoval'), color: '#4169e1' },
   ]
-};
+});
 
 const fetchData = async () => {
   try {
     isLoading.value = true;
     const apiUrl = '/api/vector/stats/FluxCd_input_data';
-    console.log('正在请求数据:', apiUrl);
+    // console.log('正在请求数据:', apiUrl);
     
     const response = await api8000.get(apiUrl);
-    console.log('API响应:', response);
+    // console.log('API响应:', response);
     
     // 调试:输出响应结构
-    console.log('响应数据:', response.data);
+    // console.log('响应数据:', response.data);
     
     // 处理不同的响应格式
     let processedData;
@@ -105,7 +107,7 @@ const fetchData = async () => {
       throw new Error('无法解析API返回的数据结构');
     }
     
-    console.log('处理后的数据:', processedData);
+    // console.log('处理后的数据:', processedData);
     return processedData;
   } catch (err) {
     console.error('数据请求失败:', err);
@@ -144,29 +146,24 @@ const calculateFieldStats = (statsData, fieldKey, fieldName) => {
 
 // 计算所有统计数据
 const calculateAllStats = (data) => {
-  console.log('计算统计数据,输入数据:', data);
+  const currentFieldConfig = getFieldConfig();
   
-  // 初始Cd统计
-  initialCdStats.value = fieldConfig.initialCd.map(indicator => 
+  // 初始 Cd 统计
+  initialCdStats.value = currentFieldConfig.initialCd.map(indicator => 
     calculateFieldStats(data, indicator.key, indicator.name)
   );
   
   // 其他指标统计
-  otherIndicatorsStats.value = fieldConfig.otherIndicators.map(indicator => 
+  otherIndicatorsStats.value = currentFieldConfig.otherIndicators.map(indicator => 
     calculateFieldStats(data, indicator.key, indicator.name)
   );
   
   // 更新样本数
   const firstStat = initialCdStats.value[0] || otherIndicatorsStats.value[0];
   stats.value.samples = firstStat?.count || 0;
-  
-  console.log('计算后的统计数据:', {
-    initialCdStats: initialCdStats.value,
-    otherIndicatorsStats: otherIndicatorsStats.value,
-    sampleCount: stats.value.samples
-  });
 };
 
+
 // 构建箱线图数据
 const buildBoxplotData = (statsArray) => {
   return statsArray.map(stat => {
@@ -177,10 +174,10 @@ const buildBoxplotData = (statsArray) => {
   });
 };
 
-// 初始化【初始Cd】图表
+// 初始化【初始 Cd】图表
 const initInitialCdChart = () => {
   if (!initialCdChart.value) {
-    error.value = new Error('初始Cd图表容器未找到');
+    error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
     return;
   }
   
@@ -190,11 +187,12 @@ const initInitialCdChart = () => {
     }
     
     chartInstanceInitial.value = echarts.init(initialCdChart.value);
-    const xAxisData = fieldConfig.initialCd.map(ind => ind.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.initialCd.map(ind => ind.name);
     const boxData = buildBoxplotData(initialCdStats.value);
     
     const option = {
-      title: { text: '初始Cd分布箱线图', left: 'center' },
+      title: { text: t('SoilCdStatistics.initialCdDistribution'), left: 'center' },
       tooltip: {
         trigger: "item",
         formatter: (params) => {
@@ -214,7 +212,7 @@ const initInitialCdChart = () => {
       series: [{
         type: "boxplot",
         itemStyle: { 
-          color: fieldConfig.initialCd[0].color,
+          color: currentFieldConfig.initialCd[0].color,
           borderWidth: 2 
         },
         data: boxData
@@ -223,15 +221,15 @@ const initInitialCdChart = () => {
     
     chartInstanceInitial.value.setOption(option);
   } catch (err) {
-    console.error('初始Cd图表初始化失败', err);
-    error.value = new Error(`初始Cd图表初始化失败: ${err.message}`);
+    console.error(t('SoilCdStatistics.chartInitFailed'), err);
+    error.value = new Error(`${t('SoilCdStatistics.chartInitFailed')}: ${err.message}`);
   }
 };
 
 // 初始化【其他指标】图表
 const initOtherIndicatorsChart = () => {
   if (!otherIndicatorsChart.value) {
-    error.value = new Error('其他指标图表容器未找到');
+    error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
     return;
   }
   
@@ -241,11 +239,12 @@ const initOtherIndicatorsChart = () => {
     }
     
     chartInstanceOther.value = echarts.init(otherIndicatorsChart.value);
-    const xAxisData = fieldConfig.otherIndicators.map(ind => ind.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.otherIndicators.map(ind => ind.name);
     const boxData = buildBoxplotData(otherIndicatorsStats.value);
     
     const option = {
-      title: { text: '其他通量Cd指标分布对比', left: 'center' },
+      title: { text: t('SoilCdStatistics.otherFluxIndicators'), left: 'center' },
       tooltip: {
         trigger: "item",
         formatter: (params) => {
@@ -269,7 +268,7 @@ const initOtherIndicatorsChart = () => {
       series: [{
         type: "boxplot",
         itemStyle: { 
-          color: (params) => fieldConfig.otherIndicators[params.dataIndex].color,
+          color: (params) => currentFieldConfig.otherIndicators[params.dataIndex].color,
           borderWidth: 2 
         },
         data: boxData
@@ -278,11 +277,12 @@ const initOtherIndicatorsChart = () => {
     
     chartInstanceOther.value.setOption(option);
   } catch (err) {
-    console.error('其他指标图表初始化失败', err);
-    error.value = new Error(`其他指标图表初始化失败: ${err.message}`);
+    console.error(t('SoilCdStatistics.chartInitFailed'), err);
+    error.value = new Error(`${t('SoilCdStatistics.chartInitFailed')}: ${err.message}`);
   }
 };
 
+
 // Tooltip格式化
 const formatTooltip = (stat) => {
   if (!stat || stat.min === null) {
@@ -291,12 +291,11 @@ const formatTooltip = (stat) => {
   
   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>样本数:<span style="color:#909399;">${stat.count}</span></div>
+      <div>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
     </div>`;
 };
 
@@ -306,11 +305,11 @@ const initCharts = async () => {
     isLoading.value = true;
     error.value = null;
     
-    console.log('开始初始化图表...');
+    // console.log('开始初始化图表...');
     
     // 获取数据
     const data = await fetchData();
-    console.log('获取到的数据:', data);
+    // console.log('获取到的数据:', data);
     
     if (!data || (Array.isArray(data) && data.length === 0)) {
       throw new Error('未获取到有效数据');
@@ -327,7 +326,7 @@ const initCharts = async () => {
     initOtherIndicatorsChart();
     
     isLoading.value = false;
-    console.log('图表初始化完成');
+    // console.log('图表初始化完成');
     
   } catch (err) {
     isLoading.value = false;
@@ -336,6 +335,7 @@ const initCharts = async () => {
   }
 };
 
+
 // 组件生命周期
 onMounted(() => {
   initCharts();
@@ -347,6 +347,12 @@ onMounted(() => {
   
   window.addEventListener('resize', handleResize);
 });
+
+// 监听语言变化
+watch(locale, () => {
+  // 语言切换后重新初始化所有图表
+  initCharts();
+});
 </script>
 
 <style scoped>

+ 279 - 168
src/locales/en.json

@@ -1,6 +1,14 @@
-{"role": {
-    "admin": "管理员",
-    "user": "用户"
+{
+  "role": {
+    "admin": "Admin",
+    "user": "User"
+  },
+  "Header": {
+    "title": "Intelligent Expert System for Soil Acidification Prediction",
+    "welcome": "welcome",
+    "loginsucc": "Login successful",
+    "username": "UserName",
+    "logout": "Log Out"
   },
   "login": {
     "userTitle": "User Login",
@@ -9,6 +17,14 @@
     "loginSuccess": "Login successful",
     "loginFailed": "Login failed, please check your username or password"
   },
+  "logout": {
+    "confirmMessage": "Are you sure you want to logout?",
+    "confirmTitle": "Confirmation",
+    "confirmButton": "Confirm",
+    "cancelButton": "Cancel",
+    "logoutSuccess": "Logout successful",
+    "logoutFailed": "Logout failed, please try again"
+  },
   "register": {
     "title": "User Registration",
     "registerButton": "Register",
@@ -17,180 +33,275 @@
     "registerFailed": "Registration failed",
     "autoLoginPrompt": "Registration successful, redirecting to login page..."
   },
-  "validation": {
+  "validation": {"account": "Account",
+    "password": "Password",
+    "confirmPassword": "Confirm Password",
     "usernameRequired": "Please enter your username",
     "passwordRequired": "Please enter your password",
     "passwordLength": "Password must be between 3 and 16 characters",
-    "confirmPasswordRequired": "Please confirm your password",
+    "confirmPasswordRequired": "Confirm password",
     "passwordMismatch": "The two passwords you entered do not match"
   },
-  "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"
+    "swIntroduce": "Software Introduction",
+    "mapcalulate": "Soil Acidification Plot-Level Calculation",
+    "reversionmodel": "Soil Re-acidification Model",
+    "reducemodel": "Soil Acid Reduction Model",
+    "dataStatistics": "Data Statistics",
+    "softwareIntroduction": "Software Introduction",
+    "projectOverview": "Project Overview",
+    "researchFindings": "Research Findings",
+    "teamInfo": "Team Information",
+    "soilAcidPlotPrediction": "Soil Acidification Plot-Level Prediction",
+    "shaoguanAcidMap": "Shaoguan Soil Acidification Map",
+    "nanxiongAcidMap": "Nanxiong Soil Acidification Map",
+    "soilPH Prediction": "Soil pH Prediction",
+    "soilAcidReductionPrediction": "Soil Acid Reflux Prediction",
+    "acidReductionModelDisplay": "Acid Reflux Model Display",
+    "soilAcidNeutralizationPrediction": "Soil Acid Neutralization Prediction",
+    "acidNeutralizationModelDisplay": "Acid Neutralization Model Display",
+    "detectionStatistics": "Detection Statistics",
+    "soilCadmiumStatistics": "Soil Cadmium Content Statistics",
+    "cropRiskAssessment": "Crop Risk Assessment System",
+    "soilAcidificationStatistics": "Soil Acidification Statistics"
   },
-  "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"
+  "SoilacidificationStatistics": {
+    "Title": "Soil acidification data statistics"
   },
-  "SoilAcidificationControl": {
-    "Title": "Soil acidification control"
+  "AcidModelMap": {
+    "ShaoguanmapTitle": "Shaoguan Fine-Graded Plot-Level Acidification Prediction",
+    "NanxiongmapTitle": "Nanxiong Fine-Graded Plot-Level Acidification Prediction",
+    "loadingTip": "Map loading...",
+    "errorTitle": "Map loading failed",
+    "errorDesc": "Please check GeoServer service and configuration",
+    "retryButton": "Retry Loading",
+    "loadingFeatureInfo": "Loading plot information...",
+    "featureInfoError": "Failed to get plot information",
+    "noData": "No data",
+    "villageLabel": "Village:",
+    "landTypeLabel": "Land Type:",
+    "unknown": "Unknown",
+    "acidInversionPrediction": "Reflux Prediction",
+    "acidReductionPrediction": "Reduction Prediction",
+    "acidReductionParams": "Acid Reduction Parameters",
+    "currentPH": "CurrentpH",
+    "targetPH": "TargetpH",
+    "nitrate": "Nitrate",
+    "ammonium": "Ammonium",
+    "ferricOxide": "FerricOxide",
+    "cancel": "Cancel",
+    "confirm": "Start Prediction",
+    "acidInversionParams": "Acid Reflux Parameters",
+    "predictionResult": "Prediction Result",
+    "acidReductionResult": "Acid Reduction Result",
+    "acidInversionResult": "Acid Reflux Result",
+    "predicting": "Predicting...",
+    "resultUnit": "Apply per mu of land surface 20cm",
+    "ton": "tons",
+    "deltaPH": "ΔpH",
+    "rerun": "Rerun Prediction",
+    "close": "Close",
+    "clickValidLand": "Please click on a valid plot",
+    "phLoading": "Loading...",
+    "noDataText": "Not available",
+    "inputError": "Invalid input parameters, please check and try again",
+    "missingLandType": "Plot land type not obtained, cannot predict",
+    "fetchPHError": "Failed to get current soil pH value, please try again",
+    "fetchBaseParamsError": "Failed to get plot base parameters, using default values",
+    "inversionPredictError": "Paddy field acid reflux prediction failed, please check parameters or try again",
+    "generalPredictError": "Prediction request failed, please try again later",
+    "mapInitError": "Map initialization failed"
+  },
+  "PhPrediction": {
+    "title": "pH Prediction Model",
+    "organicMatter": "Organic Matter (g/kg)",
+    "chloride": "Chloride Ion (g/kg)",
+    "cationExchangeCapacity": "Cation Exchange Capacity (cmol/kg)",
+    "hydrogenIonConcentration": "H+ Concentration (cmol/kg)",
+    "ammoniumNitrogen": "Ammonium Nitrogen (mg/kg)",
+    "freeAlumina": "Free Alumina (g/kg)",
+    "alumina": "Alumina (g/kg)",
+    "freeIronOxide": "Free Iron Oxide (g/kg)",
+    "amorphousIron": "Amorphous Iron (g/Kg)",
+    "initialPH": "Initial pH Value",
+    "predictButton": "Predict pH Curve",
+    "resultTitle": "pH Prediction Result",
+    "closeButton": "Close",
+    "organicMatterPlaceholder": "Enter organic matter 0~35(g/kg)",
+    "chloridePlaceholder": "Enter chloride ion 0~10(g/kg)",
+    "cecPlaceholder": "Enter CEC 0~20(cmol/kg)",
+    "hPlaceholder": "Enter H+ concentration 0~1(cmol/kg)",
+    "hnPlaceholder": "Enter ammonium nitrogen 0~30(mg/kg)",
+    "al3Placeholder": "Enter free alumina 0~2(g/kg)",
+    "aloxPlaceholder": "Enter alumina 0~2(g/kg)",
+    "feoxPlaceholder": "Enter free iron oxide 0~3(g/kg)",
+    "amfePlaceholder": "Enter amorphous iron 0~1(g/Kg)",
+    "initphPlaceholder": "Enter initial pH 0~14",
+    "validationRange": "Value should be between {min} and {max}",
+    "validationError": "Invalid input value, please check your input",
+    "requestFailed": "Request failed: "
+  },
+  "Calculation": {
+    "refluxTitle": "Acid Reflux Model",
+    "neutralizationTitle": "Acid Reduction Model",
+    "initialPH": "Initial Soil pH",
+    "targetPH": "Target Soil pH",
+    "exchangeableHydrogen": "Exchangeable Hydrogen (cmol/kg)",
+    "exchangeableAluminum": "Exchangeable Aluminum (cmol/kg)",
+    "soilOrganicMatter": "Soil Organic Matter (g/kg)",
+    "nitrate": "Nitrate (mg/kg)",
+    "ammoniumSalt": "Ammonium Salt (mg/kg)",
+    "cationExchangeCapacity": "Cation Exchange Capacity (cmol/kg)",
+    "calculateButton": "Calculate",
+    "resultTitle": "Calculation Result",
+    "closeButton": "Close",
+    "reflux": {
+      "exchangeableHydrogenPlaceholder": "Enter exchangeable hydrogen 0~5(cmol/kg)",
+      "exchangeableAluminumPlaceholder": "Enter exchangeable aluminum 0~10(cmol/kg)",
+      "soilOrganicMatterPlaceholder": "Enter soil organic matter 0~35(g/kg)",
+      "nitratePlaceholder": "Enter nitrate 0~70(mg/kg)",
+      "ammoniumSaltPlaceholder": "Enter ammonium salt 0~20(mg/kg)",
+      "cationExchangeCapacityPlaceholder": "Enter CEC 0~20(cmol/kg)"
+    },
+    "neutralization": {
+      "initialPHPlaceholder": "Enter initial soil pH 3~6",
+      "targetPHPlaceholder": "Enter target soil pH 5~7",
+      "exchangeableHydrogenPlaceholder": "Enter exchangeable hydrogen 0~4 (cmol/kg)",
+      "exchangeableAluminumPlaceholder": "Enter exchangeable aluminum 0~8 (cmol/kg)",
+      "soilOrganicMatterPlaceholder": "Enter soil organic matter 0~35 (g/kg)",
+      "nitratePlaceholder": "Enter nitrate 10~70 (mg/kg)",
+      "ammoniumSaltPlaceholder": "Enter ammonium salt 0~20 (mg/kg)",
+      "cationExchangeCapacityPlaceholder": "Enter CEC 0~15 (cmol/kg)"
+    },
+    "validationRange": "Value should be between {min} and {max} and valid number",
+    "validationError": "Invalid input value, please check your input",
+    "invalidResult": "Invalid prediction result obtained",
+    "requestFailed": "Request failed, status code: ",
+    "noResponse": "Request sent successfully, but no response received",
+    "requestError": "Error during request: ",
+    "resultUnit": "Apply per mu of land surface (20cm)",
+    "quicklime": "tons of quicklime"
+  },
+  "ModelIteration": {
+    "scatterPlotTitle": "Scatter Plot",
+    "modelPerformanceAnalysis": "Model Performance Analysis",
+    "dataIncreaseCurve": "Data Increase Curve",
+    "learningCurve": "Learning Curve",
+    "loading": "Loading...",
+    "randomForest": "Random Forest",
+    "xgboost": "XGBoost",
+    "gradientBoosting": "Gradient Boosting",
+    "trueValues": "True Values",
+    "predictedValues": "Predicted Values",
+    "modelIteration": "Model Iteration",
+    "score": "Score (R²)",
+    "generations": "Generations",
+    "trendline": "Trendline",
+    "trueVsPredicted": "True vs Predicted"
   },
   "DetectionStatistics": {
-    "Title": "Detection information statistics"
-  },
-  "FarmlandPollutionStatistics": {
-    "Title": "Soil cadmium content statistics"
-  },
-  "LandClutivatesStatistics": {
-    "Title": "Cultivated land quality assessment"
-  },
-  "SoilacidificationStatistics": {
-    "Title": "Soil acidification data statistics"
+    "irrigationWaterHeavyMetal": "Irrigation Water Heavy Metal Concentration Statistics",
+    "atmosphericHeavyMetal": "Atmospheric Heavy Metal Concentration Statistics",
+    "crossSectionCadmium": "Cadmium concentration statistics by region",
+    "atmosphericPollutionEnterprise": "Particulate Emission Statistics by Region from Atmospheric Pollution Enterprises",
+    "distributionDescription": "Display distribution characteristics of heavy metal concentrations (min, Q1, median, Q3, max)",
+    "sampleSource": "Sample Source: ",
+    "sampleDataCount": " data points",
+    "dataLoading": "Loading data...",
+    "loadingFailed": "Loading failed: ",
+    "noValidData": "No valid data",
+    "noDataToShow": "No valid data to display",
+    "heavyMetalType": "Heavy Metal Type",
+    "concentrationDistribution": "Heavy Metal Concentration Distribution",
+    "unitUgL": "μg/L",
+    "unitMgKg": "mg/kg",
+    "unitMgL": "mg/L",
+    "unitTonPerYear": "t/a",
+    "minValue": "Minimum",
+    "q1Value": "Q1",
+    "medianValue": "Median",
+    "q3Value": "Q3",
+    "maxValue": "Maximum",
+    "maxMethod": "Maximum",
+    "avgMethod": "Average",
+    "cadmiumConcentration": "Cadmium Concentration",
+    "particulateEmission": "Particulate Emission",
+    "monitoringPoints": "Monitoring Points",
+    "points": "",
+    "highestValue": "Maximum",
+    "averageValue": "Average",
+    "chromium": "Cr",
+    "arsenic": "As",
+    "cadmium": "Cd",
+    "mercury": "Hg",
+    "lead": "Pb"
+  },
+  "SoilCdStatistics": {
+    "effectiveCdAnalysis": "Effective Cd Statistical Analysis",
+    "fluxCdAnalysis": "Flux Cd Statistical Analysis",
+    "cropCdAnalysis": "Crop Cd Statistical Analysis",
+    "sampleCount": "Sample Count",
+    "pollutionIndicators": "Pollution Indicators",
+    "mainNutrients": "Main Nutrient Elements",
+    "otherProperties": "Other Physicochemical Properties",
+    "initialCdDistribution": "Initial Cd Distribution Boxplot",
+    "otherFluxIndicators": "Other Flux Cd Indicators Distribution Boxplot",
+    "cropCdMainIndicators": "Crop Cd Main Indicators",
+    "contentComparison": "Content Comparison",
+    "nutrientDistribution": "Nutrient Element Distribution",
+    "propertiesDistribution": "Physicochemical Properties Distribution",
+    "unitMgKg": "mg/kg",
+    "unitPercent": "%",
+    "unitGHa": "g/ha",
+    "unitGHaYear": "g/ha/a",
+    "averageValue": "Average",
+    "totalCadmium": "Total Cd",
+    "availablePotassium": "Available K",
+    "availablePhosphorus": "Available P",
+    "totalManganese": "Total Mn",
+    "totalNitrogen": "Total N",
+    "totalPhosphorus": "Total P",
+    "totalPotassium": "Total K",
+    "totalSulfur": "Total S",
+    "totalIron": "Total Fe",
+    "totalCalcium": "Total Ca",
+    "totalMagnesium": "Total Mg",
+    "totalAluminum": "Total Al",
+    "siltContent": "Silt Content",
+    "sandContent": "Sand Content",
+    "gravelContent": "Gravel Content",
+    "exchangeablePotassium": "Exchangeable Available K",
+    "initialCadmiumTotal": "Initial Soil Total Cadmium",
+    "atmosphericDepositionInput": "Atmospheric Deposition Input Cd",
+    "irrigationWaterInput": "Irrigation Water Input Cd",
+    "agriculturalInput": "Agricultural Input Cd",
+    "undergroundLeaching": "Underground Leaching Cd",
+    "surfaceRunoff": "Surface Runoff Cd",
+    "grainRemoval": "Grain Removal Cd",
+    "retry": "Retry",
+    "reloadInit": "Reinitialize",
+    "chartInitFailed": "Chart initialization failed",
+    "dataLoadFailed": "Data loading failed"
+  },
+  "LandCultivatedStatistics": {
+    "title": "Cultivated Land Quality Assessment by Region Classification Statistics",
+    "priorityProtection": "Priority Protection",
+    "strictControl": "Strict Control",
+    "safeUtilization": "Safe Utilization",
+    "allRegions": "All Regions",
+    "quantity": "Quantity"
+  },
+  "AcidificationDataStatistics": {
+    "refluxTitle": "Acidification Aggravation Delta_pH_105day Bar Chart",
+    "reductionTitle": "Acidification Mitigation Q_delta_pH Bar Chart",
+    "id": "ID",
+    "deltaPH": "Delta_pH_105day",
+    "qDeltaPH": "Q_delta_pH",
+    "dataLoadFailed": "Data loading failed",
+    "noValidData": "No valid data",
+    "chartInitFailed": "Chart initialization failed",
+    "requestError": "Request error, please try again later",
+    "networkError": "No response received, please check network connection",
+    "apiError": "API returned failure status"
   }
 }

+ 50 - 0
src/locales/index.js

@@ -0,0 +1,50 @@
+// 导入语言包
+import zhCN from './zh-CN.json';
+import enUS from './en-US.json';
+
+// 定义语言映射
+const langMap = {
+  'zh-CN': zhCN,
+  'en-US': enUS
+};
+
+// 默认语言(优先从本地存储读取,无则用浏览器默认)
+let currentLang = localStorage.getItem('app-lang') || (navigator.language || 'zh-CN').toLowerCase();
+// 兼容浏览器语言缩写(如en -> en-US)
+currentLang = currentLang === 'zh' ? 'zh-CN' : currentLang === 'en' ? 'en-US' : currentLang;
+
+// 当前语言包数据
+let currentLangData = langMap[currentLang] || zhCN;
+
+// 语言切换回调函数(用于更新页面)
+let langChangeCallback = null;
+
+// 语言管理对象
+const i18n = {
+  // 获取当前语言
+  getLang() {
+    return currentLang;
+  },
+
+  // 切换语言
+  setLang(lang) {
+    if (!langMap[lang]) return; // 语言包不存在则不处理
+    currentLang = lang;
+    currentLangData = langMap[lang];
+    localStorage.setItem('app-lang', lang); // 持久化
+    // 触发回调更新页面
+    if (langChangeCallback) langChangeCallback();
+  },
+
+  // 获取翻译文本(支持嵌套路径,如'common.title')
+  t(key) {
+    return key.split('.').reduce((obj, k) => obj?.[k] || key, currentLangData);
+  },
+
+  // 注册语言切换回调
+  onLangChange(callback) {
+    langChangeCallback = callback;
+  }
+};
+
+export default i18n;

+ 275 - 163
src/locales/zh.json

@@ -1,4 +1,11 @@
 {
+  "Header": {
+    "title": "土壤酸化智能预测专家系统",
+    "welcome": "欢迎",
+    "loginsucc": "登录成功",
+    "username": "用户名",
+    "logout": "退出登录"
+  },
   "login": {
     "userTitle": "用户登录",
     "loginButton": "登录",
@@ -6,6 +13,14 @@
     "loginSuccess": "登录成功",
     "loginFailed": "登录失败,请检查用户名或密码"
   },
+  "logout": {
+    "confirmMessage": "确定要退出登录吗?",
+    "confirmTitle": "提示",
+    "confirmButton": "确定",
+    "cancelButton": "取消",
+    "logoutSuccess": "退出登录成功",
+    "logoutFailed": "退出失败,请重试"
+  },
   "register": {
     "title": "用户注册",
     "registerButton": "注册",
@@ -15,182 +30,279 @@
     "autoLoginPrompt": "注册成功,正在跳转到登录页面..."
   },
   "validation": {
-    "usernameRequired": "请输入用户名",
+    "account": "账户",
+    "password": "密码",
+    "confirmPassword": "确认密码",
+    "usernameRequired": "请输入账号",
     "passwordRequired": "请输入密码",
     "passwordLength": "密码长度为 3 到 16 个字符",
     "confirmPasswordRequired": "请确认密码",
     "passwordMismatch": "两次输入的密码不一致"
   },
-  "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": "输入总通量"
+    "swIntroduce": "软件简介",
+    "mapcalulate": "土壤酸化地块级计算",
+    "reversionmodel": "土壤反酸模型",
+    "reducemodel": "土壤降酸模型",
+    "dataStatistics": "数据统计",
+    "softwareIntroduction": "软件简介",
+    "projectOverview": "项目简介",
+    "researchFindings": "研究成果",
+    "teamInfo": "团队信息",
+    "soilAcidPlotPrediction": "土壤酸化地块级预测",
+    "shaoguanAcidMap": "韶关土壤酸化地图",
+    "nanxiongAcidMap": "南雄土壤酸化地图",
+    "soilPH Prediction": "土壤 pH 预测",
+    "soilAcidReductionPrediction": "土壤反酸预测",
+    "acidReductionModelDisplay": "反酸模型显示",
+    "soilAcidNeutralizationPrediction": "土壤降酸预测",
+    "acidNeutralizationModelDisplay": "降酸模型显示",
+    "detectionStatistics": "检测信息统计",
+    "soilCadmiumStatistics": "土壤镉含量统计",
+    "cropRiskAssessment": "作物风险评估系统",
+    "soilAcidificationStatistics": "土壤酸化统计"
   },
-  "totalOutputFlux": {
-    "Title": "输出总通量"
-  },
-  "netFlux": {
-    "Title": "土壤镉净通量"
-  },
-  "currentYearConcentration": {
-    "Title": "土壤镉当年浓度"
-  },
-  "TotalCadmiumPrediction": {
-    "Title": "土壤镉的总含量预测"
-  },
-  "EffectiveCadmiumPrediction": {
-    "Title": "土壤镉有效态含量预测"
-  },
-  "CropCadmiumPrediction": {
-    "Title": "水稻镉含量预测"
-  },
-  "FutureCadmiumPrediction": {
-    "Title": "土壤镉未来含量预测"
-  },
-  "cropRiskAssessment": {
-    "Title": "水稻镉污染风险"
-  },
-  "farmlandQualityAssessment": {
-    "Title": "韶关"
-  },
-  "acidModel": {
-    "Title": "土壤反酸",
-    "CalculationTitle": "土壤反酸预测",
-    "SoilAcidReductionIterativeEvolutionTitle": "反酸模型迭代可视化"
-  },
-  "neutralizationModel": {
-    "Title": "土壤降酸",
-    "AcidNeutralizationModelTitle": "土壤降酸预测",
-    "SoilAcidificationIterativeEvolutionTitle": "土壤降酸迭代可视化"
-  },
-  "TraditionalFarmingRisk": {
-    "Title": "传统耕种习惯风险趋势"
-  },
-  "HeavyMetalCadmiumControl": {
-    "Title": "重金属镉污染治理"
+  "SoilacidificationStatistics": {
+    "Title": "土壤酸化数据统计"
   },
-  "SoilAcidificationControl": {
-    "Title": "土壤酸化治理"
+  "AcidModelMap": {
+    "ShaoguanmapTitle": "韶关市细粒度地块级酸化预测",
+    "NanxiongmapTitle": "南雄市细粒度地块级酸化预测",
+    "loadingTip": "地图加载中...",
+    "errorTitle": "地图加载失败",
+    "errorDesc": "请检查 GeoServer 服务和配置",
+    "retryButton": "重试加载",
+    "loadingFeatureInfo": "加载地块信息中...",
+    "featureInfoError": "获取地块信息失败",
+    "noData": "无地块信息",
+    "villageLabel": "所属村:",
+    "landTypeLabel": "用地类型:",
+    "unknown": "未知",
+    "acidInversionPrediction": "反酸预测",
+    "acidReductionPrediction": "降酸预测",
+    "acidReductionParams": "降酸预测参数",
+    "currentPH": "当前pH",
+    "targetPH": "目标pH",
+    "nitrate": "硝酸盐",
+    "ammonium": "铵盐",
+    "ferricOxide": "氧化铁",
+    "cancel": "取消",
+    "confirm": "开始预测",
+    "acidInversionParams": "反酸预测参数",
+    "predictionResult": "预测结果",
+    "acidReductionResult": "降酸预测结果",
+    "acidInversionResult": "反酸预测结果",
+    "predicting": "预测中...",
+    "resultUnit": "每亩地土壤表层 20cm 撒",
+    "ton": "吨",
+    "deltaPH": "ΔpH",
+    "rerun": "重新预测",
+    "close": "关闭",
+    "clickValidLand": "请点击有效地块",
+    "phLoading": "加载中...",
+    "noDataText": "未获取",
+    "inputError": "输入参数不合法,请检查后重试",
+    "missingLandType": "未获取到地块用地类型,无法进行预测",
+    "fetchPHError": "获取土壤当前 pH 值失败,请重试",
+    "fetchBaseParamsError": "获取地块基础参数失败,使用默认值",
+    "inversionPredictError": "水田反酸预测失败,请检查参数或重试",
+    "generalPredictError": "预测请求失败,请稍后重试",
+    "mapInitError": "地图初始化失败"
+  },
+  "PhPrediction": {
+    "title": "pH 预测模型",
+    "organicMatter": "有机质 (g/kg)",
+    "chloride": "氯离子 (g/kg)",
+    "cationExchangeCapacity": "阳离子交换量 (cmol/kg)",
+    "hydrogenIonConcentration": "H+浓度 (cmol/kg)",
+    "ammoniumNitrogen": "铵态氮 (mg/kg)",
+    "freeAlumina": "游离氧化铝 (g/kg)",
+    "alumina": "氧化铝 (g/kg)",
+    "freeIronOxide": "游离铁氧化物 (g/kg)",
+    "amorphousIron": "无定形铁 (g/Kg)",
+    "initialPH": "初始 pH 值",
+    "predictButton": "预测 pH 曲线",
+    "resultTitle": "pH 预测结果",
+    "closeButton": "关闭",
+    "organicMatterPlaceholder": "请输入有机质 0~35(g/kg)",
+    "chloridePlaceholder": "请输入氯离子 0~10(g/kg)",
+    "cecPlaceholder": "请输入阳离子交换量 0~20(cmol/kg)",
+    "hPlaceholder": "请输入 H+浓度 0~1(cmol/kg)",
+    "hnPlaceholder": "请输入铵态氮 0~30(mg/kg)",
+    "al3Placeholder": "请输入游离氧化铝 0~2(g/kg)",
+    "aloxPlaceholder": "请输入氧化铝 0~2(g/kg)",
+    "feoxPlaceholder": "请输入游离铁氧化物 0~3(g/kg)",
+    "amfePlaceholder": "请输入无定形铁 0~1(g/Kg)",
+    "initphPlaceholder": "请输入初始 pH 值 0~14",
+    "validationRange": "输入值应在 {min} 到 {max} 之间",
+    "validationError": "输入值不符合要求,请检查输入",
+    "requestFailed": "请求失败:"
+  },
+  "Calculation": {
+    "refluxTitle": "反酸模型",
+    "neutralizationTitle": "降酸模型",
+    "initialPH": "土壤初始 pH",
+    "targetPH": "土壤目标 pH",
+    "exchangeableHydrogen": "交换性氢 (cmol/kg)",
+    "exchangeableAluminum": "交换性铝 (cmol/kg)",
+    "soilOrganicMatter": "土壤有机质 (g/kg)",
+    "nitrate": "硝酸盐 (mg/kg)",
+    "ammoniumSalt": "铵盐 (mg/kg)",
+    "cationExchangeCapacity": "阳离子交换量 (cmol/kg)",
+    "calculateButton": "计算",
+    "resultTitle": "计算结果",
+    "closeButton": "关闭",
+    "reflux": {
+      "exchangeableHydrogenPlaceholder": "请输入交换性氢 0~5(cmol/kg)",
+      "exchangeableAluminumPlaceholder": "请输入交换性铝 0~10(cmol/kg)",
+      "soilOrganicMatterPlaceholder": "请输入土壤有机质 0~35(g/kg)",
+      "nitratePlaceholder": "请输入硝酸盐 0~70(mg/kg)",
+      "ammoniumSaltPlaceholder": "请输入铵盐 0~20(mg/kg)",
+      "cationExchangeCapacityPlaceholder": "请输入阳离子交换量 0~20(cmol/kg)"
+    },
+    "neutralization": {
+      "initialPHPlaceholder": "请输入土壤初始 3~6 pH",
+      "targetPHPlaceholder": "请输入土壤目标 5~7 pH",
+      "exchangeableHydrogenPlaceholder": "请输入交换性氢 0~4 (cmol/kg)",
+      "exchangeableAluminumPlaceholder": "请输入交换性铝 0~8 (cmol/kg)",
+      "soilOrganicMatterPlaceholder": "请输入土壤有机质 0~35 (g/kg)",
+      "nitratePlaceholder": "请输入硝酸盐 10~70 (mg/kg)",
+      "ammoniumSaltPlaceholder": "请输入铵盐 0~20 (mg/kg)",
+      "cationExchangeCapacityPlaceholder": "请输入阳离子交换量 0~15 (cmol/kg)"
+    },
+    "validationRange": "输入值应在 {min} 到 {max} 之间且为有效数字",
+    "validationError": "输入值不符合要求,请检查输入",
+    "invalidResult": "未获取到有效的预测结果",
+    "requestFailed": "请求失败,状态码:",
+    "noResponse": "请求发送成功,但没有收到响应",
+    "requestError": "请求过程中发生错误:",
+    "resultUnit": "每亩地土壤表层 (20cm)撒",
+    "quicklime": "吨生石灰"
+  },
+  "ModelIteration": {
+    "scatterPlotTitle": "散点图",
+    "modelPerformanceAnalysis": "模型性能分析",
+    "dataIncreaseCurve": "数据增长曲线",
+    "learningCurve": "学习曲线",
+    "loading": "加载中...",
+    "randomForest": "随机森林",
+    "xgboost": "XGBoost",
+    "gradientBoosting": "梯度提升",
+    "trueValues": "真实值",
+    "predictedValues": "预测值",
+    "modelIteration": "模型迭代",
+    "score": "Score (R²)",
+    "generations": "代",
+    "trendline": "趋势线",
+    "trueVsPredicted": "True vs Predicted"
   },
   "DetectionStatistics": {
-    "Title": "检测信息统计"
-  },
-  "FarmlandPollutionStatistics": {
-    "Title": "土壤镉含量统计"
-  },
-  "LandClutivatesStatistics": {
-    "Title": "耕地质量评估统计"
-  },
-  "SoilacidificationStatistics": {
-    "Title": "土壤酸化数据统计"
+    "irrigationWaterHeavyMetal": "灌溉水重金属浓度统计箱线图",
+    "atmosphericHeavyMetal": "大气重金属浓度统计箱线图",
+    "crossSectionCadmium": "断面采样点各地区镉浓度统计",
+    "atmosphericPollutionEnterprise": "大气污染企业各地区颗粒物排放量统计",
+    "distributionDescription": "展示各重金属浓度的分布特征(最小值、四分位数、中位数、最大值)",
+    "sampleSource": "样本来源:",
+    "sampleDataCount": "个数据",
+    "dataLoading": "数据加载中...",
+    "loadingFailed": "加载失败:",
+    "noValidData": "无有效数据",
+    "noDataToShow": "无有效数据可展示",
+    "heavyMetalType": "重金属类型",
+    "concentrationDistribution": "重金属浓度分布",
+    "unitUgL": "μg/L",
+    "unitMgKg": "mg/kg",
+    "unitMgL": "mg/L",
+    "unitTonPerYear": "t/a",
+    "minValue": "最小值",
+    "q1Value": "下四分位",
+    "medianValue": "中位数",
+    "q3Value": "上四分位",
+    "maxValue": "最大值",
+    "maxMethod": "最大值",
+    "avgMethod": "平均值",
+    "cadmiumConcentration": "镉浓度",
+    "particulateEmission": "颗粒物排放量",
+    "monitoringPoints": "监测点数量",
+    "points": "个",
+    "highestValue": "最高值",
+    "averageValue": "平均值",
+    "chromium": "铬 (Cr)",
+    "arsenic": "砷 (As)",
+    "cadmium": "镉 (Cd)",
+    "mercury": "汞 (Hg)",
+    "lead": "铅 (Pb)"
+  },
+  "SoilCdStatistics": {
+    "effectiveCdAnalysis": "有效态 Cd 数据统计分析",
+    "fluxCdAnalysis": "通量 Cd 数据统计分析",
+    "cropCdAnalysis": "作物态 Cd 数据统计分析",
+    "sampleCount": "样本数量",
+    "pollutionIndicators": "污染指标",
+    "mainNutrients": "主要养分元素",
+    "otherProperties": "其他理化性质",
+    "initialCdDistribution": "初始 Cd(Initial_Cd)分布箱线图",
+    "otherFluxIndicators": "其他通量 Cd 指标分布箱线图",
+    "cropCdMainIndicators": "作物态 Cd 主要指标",
+    "contentComparison": "含量对比",
+    "nutrientDistribution": "养分元素分布",
+    "propertiesDistribution": "理化性质分布",
+    "unitMgKg": "mg/kg",
+    "unitPercent": "%",
+    "unitGHa": "g/ha",
+    "unitGHaYear": "g/ha/a",
+    "averageValue": "平均值",
+    "totalCadmium": "总镉",
+    "availablePotassium": "速效钾",
+    "availablePhosphorus": "有效磷",
+    "totalManganese": "全锰",
+    "totalNitrogen": "全氮",
+    "totalPhosphorus": "全磷",
+    "totalPotassium": "全钾",
+    "totalSulfur": "全硫",
+    "totalIron": "全铁",
+    "totalCalcium": "全钙",
+    "totalMagnesium": "全镁",
+    "totalAluminum": "全铝",
+    "siltContent": "粉粒组分质量占比",
+    "sandContent": "砂粒组分质量占比",
+    "gravelContent": "石砾组分质量占比",
+    "exchangeablePotassium": "交换性速效钾",
+    "initialCadmiumTotal": "土壤初始 Cd 总量",
+    "atmosphericDepositionInput": "大气沉降输入 Cd",
+    "irrigationWaterInput": "灌溉水输入 Cd",
+    "agriculturalInput": "农业投入输入 Cd",
+    "undergroundLeaching": "地下渗漏 Cd",
+    "surfaceRunoff": "地表径流 Cd",
+    "grainRemoval": "籽粒移除 Cd",
+    "retry": "重试",
+    "reloadInit": "重新尝试初始化",
+    "chartInitFailed": "图表初始化失败",
+    "dataLoadFailed": "数据加载失败"
+  },
+  "LandCultivatedStatistics": {
+    "title": "耕地质量评估按区域分类数据统计",
+    "priorityProtection": "优先保护类",
+    "strictControl": "严格管控类",
+    "safeUtilization": "安全利用类",
+    "allRegions": "全部县",
+    "quantity": "数量"
+  },
+  "AcidificationDataStatistics": {
+    "refluxTitle": "酸化加剧 Delta_pH_105day 柱状图",
+    "reductionTitle": "酸化缓解 Q_delta_pH 柱状图",
+    "id": "ID",
+    "deltaPH": "Delta_pH_105day",
+    "qDeltaPH": "Q_delta_pH",
+    "dataLoadFailed": "数据加载失败",
+    "noValidData": "无有效数据",
+    "chartInitFailed": "图表初始化失败",
+    "requestError": "请求发生错误,请稍后重试",
+    "networkError": "未收到响应,请检查网络连接",
+    "apiError": "API 返回失败状态"
   }
 }

+ 19 - 16
src/views/Admin/userManagement/UserRegistration.vue

@@ -9,21 +9,21 @@
     >
       <!-- 输入账号 -->
       <div class="input-frame">
-        <el-form-item label="账号:" prop="name">
+        <el-form-item :label="$t('validation.username') + ':'" prop="name">
           <el-input v-model="registerForm.name" />
         </el-form-item>
       </div>
 
       <!-- 输入密码 -->
       <div class="input-frame">
-        <el-form-item label="密码:" prop="password">
+        <el-form-item :label="$t('validation.password') + ':'" prop="password">
           <el-input type="password" v-model="registerForm.password" />
         </el-form-item>
       </div>
 
       <!-- 确认密码 -->
       <div class="input-frame">
-        <el-form-item label="确认密码:" prop="confirmPassword">
+        <el-form-item :label="$t('validation.confirmPassword') + ':'" prop="confirmPassword">
           <el-input type="password" v-model="registerForm.confirmPassword" />
         </el-form-item>
       </div>
@@ -37,7 +37,7 @@
             :loading="loading"
             class="register-button"
           >
-            注册
+            {{ $t('register.registerButton') }}
           </el-button>
         </div>
       </el-form-item>
@@ -49,7 +49,9 @@
 import { reactive, ref } from "vue";
 import { ElForm, ElMessage } from "element-plus";
 import { register } from "@/API/users";
+import { useI18n } from 'vue-i18n';
 
+const { t } = useI18n();
 const loading = ref(false);
 
 // 注册表单数据
@@ -58,16 +60,18 @@ const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
 
 // 表单校验规则
 const registerRules = {
-  name: [{ required: true, message: "请输入账号", trigger: "blur" }],
+  name: [
+    { required: true, message: t('validation.usernameRequired'), trigger: "blur" }
+  ],
   password: [
-    { required: true, message: "请输入密码", trigger: "blur" },
-    { min: 3, max: 16, message: "密码长度应为3-16位", trigger: "blur" }
+    { required: true, message: t('validation.passwordRequired'), trigger: "blur" },
+    { min: 3, max: 16, message: t('validation.passwordLength'), trigger: "blur" }
   ],
   confirmPassword: [
-    { required: true, message: "请确认密码", trigger: "blur" },
+    { required: true, message: t('validation.confirmPasswordRequired'), trigger: "blur" },
     {
       validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
-        if (value !== registerForm.password) callback(new Error("两次密码输入不一致"));
+        if (value !== registerForm.password) callback(new Error(t('validation.passwordMismatch')));
         else callback();
       },
       trigger: "blur"
@@ -89,15 +93,15 @@ const onRegister = async () => {
     });
 
     if (res.data?.message) {
-      ElMessage.success(res.data.message);
+      ElMessage.success(t('register.registerSuccess'));
       registerForm.name = "";
       registerForm.password = "";
       registerForm.confirmPassword = "";
     } else {
-      ElMessage.error(res.data?.message || "注册失败");
+      ElMessage.error(res.data?.message || t('register.registerFailed'));
     }
   } catch (error: any) {
-    ElMessage.error(error?.response?.data?.detail || "注册异常");
+    ElMessage.error(error?.response?.data?.detail || t('register.registerFailed'));
   } finally {
     loading.value = false;
   }
@@ -138,8 +142,8 @@ const onRegister = async () => {
 
 .register-button {
   width: 300px;
-  max-width: 3000px; /* 最大宽度 */
-  height: 56px;     /* 高度加大 */
+  max-width: 3000px;
+  height: 56px;
   font-size: 20px;
   border-radius: 15px;
   background: linear-gradient(to right, #8df9f0, #26b046);
@@ -154,5 +158,4 @@ const onRegister = async () => {
   display: flex;
   justify-content: center;
 }
-</style>
-
+</style>

+ 28 - 25
src/views/User/acidModel/Calculation.vue

@@ -2,7 +2,7 @@
   <el-card class="box-card">
     <template #header>
       <div class="card-header">
-        <span>反酸模型</span>
+        <span>{{ $t('Calculation.refluxTitle') }}</span>
       </div>
     </template>
 
@@ -12,61 +12,61 @@
       label-width="240px"
       label-position="left"
     >
-      <el-form-item label="交换性氢(cmol/kg)" prop="H" :error="errorMessages.H" required>
+      <el-form-item :label="$t('Calculation.exchangeableHydrogen')" prop="H" :error="errorMessages.H" required>
         <el-input
           v-model="form.H"
           size="large"
-          placeholder="请输入交换性氢0~5(cmol/kg)"
+          :placeholder="$t('Calculation.reflux.exchangeableHydrogenPlaceholder')"
           @input="handleInput('H', $event, 0, 5)"
         ></el-input>
       </el-form-item>
-      <el-form-item label="交换性铝(cmol/kg)" prop="Al" :error="errorMessages.Al" required>
+      <el-form-item :label="$t('Calculation.exchangeableAluminum')" prop="Al" :error="errorMessages.Al" required>
         <el-input
           v-model="form.Al"
           size="large"
-          placeholder="请输入交换性铝0~10(cmol/kg)"
+          :placeholder="$t('Calculation.reflux.exchangeableAluminumPlaceholder')"
           @input="handleInput('Al', $event, 0, 10)"
         ></el-input>
       </el-form-item>
-      <el-form-item label="土壤有机质(g/kg)" prop="OM" :error="errorMessages.OM" required>
+      <el-form-item :label="$t('Calculation.soilOrganicMatter')" prop="OM" :error="errorMessages.OM" required>
         <el-input
           v-model="form.OM"
           size="large"
-          placeholder="请输入土壤有机质0~35(g/kg)"
+          :placeholder="$t('Calculation.reflux.soilOrganicMatterPlaceholder')"
           @input="handleInput('OM', $event, 0, 35)"
         ></el-input>
       </el-form-item>
-      <el-form-item label="硝酸盐(mg/kg)" prop="NO3" :error="errorMessages.NO3" required>
+      <el-form-item :label="$t('Calculation.nitrate')" prop="NO3" :error="errorMessages.NO3" required>
         <el-input
           v-model="form.NO3"
           size="large"
-          placeholder="请输入硝酸盐0~70(mg/kg)"
+          :placeholder="$t('Calculation.reflux.nitratePlaceholder')"
           @input="handleInput('NO3', $event, 0, 70)"
         ></el-input>
       </el-form-item>
-      <el-form-item label="铵盐(mg/kg)" prop="NH4" :error="errorMessages.NH4" required>
+      <el-form-item :label="$t('Calculation.ammoniumSalt')" prop="NH4" :error="errorMessages.NH4" required>
         <el-input
           v-model="form.NH4"
           size="large"
-          placeholder="请输入铵盐0~20(mg/kg)"
+          :placeholder="$t('Calculation.reflux.ammoniumSaltPlaceholder')"
           @input="handleInput('NH4', $event, 0, 20)"
         ></el-input>
       </el-form-item>
-      <el-form-item label="阳离子交换量(cmol/kg)" prop="CEC" :error="errorMessages.CEC" required>
+      <el-form-item :label="$t('Calculation.cationExchangeCapacity')" prop="CEC" :error="errorMessages.CEC" required>
         <el-input
           v-model="form.CEC"
           size="large"
-          placeholder="请输入阳离子交换量0~20(cmol/kg)"
+          :placeholder="$t('Calculation.reflux.cationExchangeCapacityPlaceholder')"
           @input="handleInput('CEC', $event, 0, 20)"
         ></el-input>
       </el-form-item>
 
-      <el-button type="primary" @click="onSubmit" class="onSubmit">计算</el-button>
+      <el-button type="primary" @click="onSubmit" class="onSubmit">{{ $t('Calculation.calculateButton') }}</el-button>
 
-      <el-dialog v-model="dialogVisible" @close="onDialogClose" :close-on-click-modal="false" width="500px" align-center title="计算结果">
+      <el-dialog v-model="dialogVisible" @close="onDialogClose" :close-on-click-modal="false" width="500px" align-center :title="$t('Calculation.resultTitle')">
         <span class="dialog-class">ΔpH: {{ result }}</span>
         <template #footer>
-          <el-button @click="dialogVisible = false">关闭</el-button>
+          <el-button @click="dialogVisible = false">{{ $t('Calculation.closeButton') }}</el-button>
         </template>
       </el-dialog>
     </el-form>
@@ -76,7 +76,10 @@
 <script setup lang="ts">
 import { reactive, ref, nextTick, onMounted } from "vue";
 import { ElMessage } from "element-plus";
-import { api5000 } from "../../../utils/request"; // 使用api5000
+import { api5000 } from "../../../utils/request";
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 // 表单接口 - 根据文档修改
 interface Form {
@@ -111,7 +114,7 @@ const errorMessages = reactive<Record<string, string>>({
 });
 
 // 当前选中模型
-const selectedModelId = ref<number>(35); // 根据文档修改为35
+const selectedModelId = ref<number>(35); // 根据文档修改为 35
 const selectedModelName = ref<string>("反酸预测模型");
 
 // 输入校验
@@ -127,7 +130,7 @@ const handleInput = (field: keyof Form, event: Event, min: number, max: number)
 
   const numValue = parseFloat(value);
   errorMessages[field] = (isNaN(numValue) || numValue < min || numValue > max)
-    ? `输入值应在 ${min} 到 ${max} 之间且为有效数字`
+    ? t('Calculation.validationRange', { min, max })
     : "";
 };
 
@@ -167,14 +170,14 @@ const onSubmit = async () => {
     const value = form[field];
     if (value === null || !validateInput(value.toString(), min, max)) {
       isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
+      errorMessages[field] = t('Calculation.validationRange', { min, max });
     } else {
       errorMessages[field] = "";
     }
   }
 
   if (!isValid) {
-    ElMessage.error("输入值不符合要求,请检查输入");
+    ElMessage.error(t('Calculation.validationError'));
     return;
   }
 
@@ -201,16 +204,16 @@ const onSubmit = async () => {
       result.value = parseFloat(response.data.result[0].toFixed(2));
       dialogVisible.value = true;
     } else {
-      ElMessage.error("未获取到有效的预测结果");
+      ElMessage.error(t('Calculation.invalidResult'));
     }
   } catch (error: any) {
     console.error("请求失败:", error);
     if (error.response) {
-      ElMessage.error(`请求失败,状态码: ${error.response.status}`);
+      ElMessage.error(`${t('Calculation.requestFailed')}${error.response.status}`);
     } else if (error.request) {
-      ElMessage.error("请求发送成功,但没有收到响应");
+      ElMessage.error(t('Calculation.noResponse'));
     } else {
-      ElMessage.error("请求过程中发生错误: " + error.message);
+      ElMessage.error(`${t('Calculation.requestError')}${error.message}`);
     }
   }
 };

+ 15 - 12
src/views/User/acidModel/ModelIterationVisualization.vue

@@ -3,6 +3,9 @@ import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
 import VueEcharts from 'vue-echarts';
 import 'echarts';
 import { api5000 } from '../../../utils/request';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n()
 
 interface HistoryDataItem {
   dataset_id: number;
@@ -83,9 +86,9 @@ const selectedModelType = ref('rf'); // 默认选择随机森林
 
 // 模型类型选项
 const modelTypeOptions = [
-  { label: '随机森林', value: 'rf' },
-  { label: 'XGBoost', value: 'xgbr' },
-  { label: '梯度提升', value: 'gbst' },
+  { label: t('ModelIteration.randomForest'), value: 'rf' },
+  { label: t('ModelIteration.xgboost'), value: 'xgbr' },
+  { label: t('ModelIteration.gradientBoosting'), value: 'gbst' },
 ];
 
 // 计算数据范围的函数
@@ -137,7 +140,7 @@ const fetchLineData = async () => {
         containLabel: true
       },
       xAxis: {
-        name: '模型迭代',
+        name: t('ModelIteration.modelIteration'),
         type: 'category',
         boundaryGap: false,
         data: timestamps.map((_, index) => `${index + 1}代`)
@@ -315,28 +318,28 @@ onUnmounted(() => {
   <div class="container">
     <template v-if="showInitScatterChart">
       <!-- 散点图 -->
-      <h2 class="chart-header">散点图</h2>
+      <h2 class="chart-header">{{ $t('ModelIteration.scatterPlotTitle') }}</h2>
       <div class="chart-container">
         <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
       </div>
     </template>
 
-    <h2 class="chart-header">模型性能分析</h2>
+    <h2 class="chart-header">{{ $t('ModelIteration.modelPerformanceAnalysis') }}</h2>
     <!-- 模型性能分析部分 -->
     <div class="analysis-section">
        <!-- 数据增长曲线模块 -->
       <div class="chart-module">
-        <h2 class="chart-header">数据增长曲线</h2>
+        <h2 class="chart-header">{{ $t('ModelIteration.dataIncreaseCurve') }}</h2>
         <div class="image-chart-item">
           <div class="image-container">
-            <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" alt="数据增长曲线" >
-            <div v-else class="image-placeholder">加载中...</div>
+            <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" :alt="$t('ModelIteration.dataIncreaseCurve')" >
+            <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
           </div>
         </div>
       </div>
       <!-- 学习曲线模块 -->
       <div class="chart-module">
-        <h2 class="chart-header">学习曲线</h2>
+        <h2 class="chart-header">{{ $t('ModelIteration.learningCurve') }}</h2>
         <div class="model-selector-wrapper">
           <select v-model="selectedModelType" class="model-select">
             <option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
@@ -346,8 +349,8 @@ onUnmounted(() => {
         </div>
         <div class="image-chart-item">
           <div class="image-container">
-            <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" alt="学习曲线" >
-            <div v-else class="image-placeholder">加载中...</div>
+            <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" :alt="$t('ModelIteration.learningCurve')" >
+            <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
           </div>
         </div>
       </div>

+ 86 - 83
src/views/User/acidModel/nanxiong_acidmodelmap.vue

@@ -2,19 +2,19 @@
   <el-card class="map-card">
     <div class="title">
       <div class="section-icon">🗺️</div>
-      <p class="map-title">南雄市细粒度地块级酸化预测</p>
+      <p class="map-title">{{ $t('AcidModelMap.NanxiongmapTitle') }}</p>
     </div>
 
     <div id="map-container" class="map-container">
       <div v-if="mapLoading" class="loading">
         <div class="loading-spinner"></div>
-        <span>地图加载中...</span>
+        <span>{{ $t('AcidModelMap.loadingTip') }}</span>
       </div>
       <div v-if="mapError" class="error-tip">
-        <el-alert title="地图加载失败" type="error" show-icon>
+        <el-alert :title="$t('AcidModelMap.errorTitle')" type="error" show-icon>
           <template #description>
-            <p>请检查GeoServer服务和配置</p>
-            <el-button @click="reloadMap" type="primary">重试加载</el-button>
+            <p>{{ $t('AcidModelMap.errorDesc') }}</p>
+            <el-button @click="reloadMap" type="primary">{{ $t('AcidModelMap.retryButton') }}</el-button>
           </template>
         </el-alert>
       </div>
@@ -34,25 +34,25 @@
           <!-- 地块信息 -->
           <div v-if="featureInfo.loading" class="loading-info">
             <div class="loading-spinner small"></div>
-            <span>加载地块信息中...</span>
+            <span>{{ $t('AcidModelMap.loadingFeatureInfo') }}</span>
           </div>
-          <div v-else-if="featureInfo.error" class="error-info">获取地块信息失败</div>
+          <div v-else-if="featureInfo.error" class="error-info">{{ $t('AcidModelMap.featureInfoError') }}</div>
           <div v-else-if="featureInfo.data" class="feature-info">
             <div class="info-item">
-              <label>所属村:</label>
-              <span>{{ featureInfo.data.village || '未知' }}</span>
+              <label>{{ $t('AcidModelMap.villageLabel') }}</label>
+              <span>{{ featureInfo.data.village || $t('AcidModelMap.unknown') }}</span>
             </div>
             <div class="info-item">
-              <label>用地类型:</label>
-              <span>{{ featureInfo.data.landType || '未知' }}</span>
+              <label>{{ $t('AcidModelMap.landTypeLabel') }}</label>
+              <span>{{ featureInfo.data.landType || $t('AcidModelMap.unknown') }}</span>
             </div>
             <div class="info-item">
-              <label>当前pH:</label>
-              <span v-if="phLoading">加载中...</span>
-              <span v-else>{{ currentPH?.toFixed(2) || '未获取' }}</span>
+              <label>{{ $t('AcidModelMap.currentPH') }}:</label>
+              <span v-if="phLoading">{{ $t('AcidModelMap.phLoading') }}</span>
+              <span v-else>{{ currentPH?.toFixed(2) || $t('AcidModelMap.noDataText') }}</span>
             </div>
           </div>
-          <div v-else class="no-data">无地块信息</div>
+          <div v-else class="no-data">{{ $t('AcidModelMap.noData') }}</div>
           <button @click="closePopup" class="close-btn">×</button>
         </div>
 
@@ -64,14 +64,14 @@
               type="success" 
               @click="startAcidInversionPrediction"
             >
-              反酸预测
+              {{ $t('AcidModelMap.acidInversionPrediction') }}
             </el-button>
             <el-button 
               size="small" 
               type="primary" 
               @click="startAcidReductionPrediction"
             >
-              降酸预测
+              {{ $t('AcidModelMap.acidReductionPrediction') }}
             </el-button>
           </div>
 
@@ -79,7 +79,7 @@
           <div v-if="showAcidReductionInput || (showPredictionResult && currentPredictionType === 'reduction')" class="prediction-card">
             <!-- 降酸参数输入(未预测时显示) -->
             <div v-if="showAcidReductionInput && !showPredictionResult">
-              <div class="section-title">降酸预测参数</div>
+              <div class="section-title">{{ $t('AcidModelMap.acidReductionParams') }}</div>
               <el-form 
                 :model="acidReductionParams" 
                 :rules="acidReductionRules" 
@@ -88,14 +88,14 @@
                 size="small"
               >
                 <div class="params-row">
-                  <el-form-item label="当前pH" class="form-item-compact readonly-item">
+                  <el-form-item :label="$t('AcidModelMap.currentPH')" class="form-item-compact readonly-item">
                     <div class="current-ph-value">
-                      <span v-if="phLoading" class="loading-text">加载中...</span>
+                      <span v-if="phLoading" class="loading-text">{{ $t('AcidModelMap.phLoading') }}</span>
                       <span v-else-if="currentPH !== null">{{ currentPH.toFixed(2) }}</span>
-                      <span v-else class="no-data-text">未获取</span>
+                      <span v-else class="no-data-text">{{ $t('AcidModelMap.noDataText') }}</span>
                     </div>
                   </el-form-item>
-                  <el-form-item label="目标pH" prop="target_pH" class="form-item-compact">
+                  <el-form-item :label="$t('AcidModelMap.targetPH')" prop="target_pH" class="form-item-compact">
                     <el-input
                       v-model.number="acidReductionParams.target_pH"
                       type="number"
@@ -107,30 +107,30 @@
                 </div>
               </el-form>
               <div class="input-buttons">
-                <el-button size="small" @click="cancelAcidReduction" :disabled="phLoading">取消</el-button>
+                <el-button size="small" @click="cancelAcidReduction" :disabled="phLoading">{{ $t('AcidModelMap.cancel') }}</el-button>
                 <el-button size="small" type="primary" @click="confirmAcidReduction" :loading="predictionLoading || phLoading">
-                  开始预测
+                  {{ $t('AcidModelMap.confirm') }}
                 </el-button>
               </div>
             </div>
 
             <!-- 降酸结果(预测后替换参数区显示) -->
             <div v-if="showPredictionResult && currentPredictionType === 'reduction'" class="result-section">
-              <div class="section-title">降酸预测结果</div>
+              <div class="section-title">{{ $t('AcidModelMap.acidReductionResult') }}</div>
               <div v-if="predictionLoading" class="loading-info">
                 <div class="loading-spinner small"></div>
-                <span>预测中...</span>
+                <span>{{ $t('AcidModelMap.predicting') }}</span>
               </div>
               <div v-else-if="predictionError" class="error-info">
                 {{ predictionError }}
               </div>
               <div v-else-if="predictionResult" class="result-content">
                 <div class="result-item">
-                  <span class="prediction-value reduction">每亩地土壤表层20cm撒{{ formatPredictionValue(predictionResult.prediction_reduce)}} 吨</span>
+                  <span class="prediction-value reduction">{{ $t('AcidModelMap.resultUnit') }}{{ formatPredictionValue(predictionResult.prediction_reduce)}} {{ $t('AcidModelMap.ton') }}</span>
                 </div>
                 <div class="result-buttons">
-                  <el-button size="small" @click="resetPrediction">重新预测</el-button>
-                  <el-button size="small" type="primary" @click="closePopup">关闭</el-button>
+                  <el-button size="small" @click="resetPrediction">{{ $t('AcidModelMap.rerun') }}</el-button>
+                  <el-button size="small" type="primary" @click="closePopup">{{ $t('AcidModelMap.close') }}</el-button>
                 </div>
               </div>
             </div>
@@ -140,40 +140,40 @@
           <div v-if="showAcidInversionInput || (showPredictionResult && currentPredictionType === 'inversion')" class="prediction-card">
             <!-- 反酸参数输入(未预测时显示) -->
             <div v-if="showAcidInversionInput && !showPredictionResult">
-              <div class="section-title">反酸预测参数</div>
+              <div class="section-title">{{ $t('AcidModelMap.acidInversionParams') }}</div>
               <div class="params-row">
-                <el-form-item label="当前pH" class="form-item-compact readonly-item">
+                <el-form-item :label="$t('AcidModelMap.currentPH')" class="form-item-compact readonly-item">
                   <div class="current-ph-value">
-                    <span v-if="phLoading">加载中...</span>
-                    <span v-else>{{ currentPH?.toFixed(2) || '未获取' }}</span>
+                    <span v-if="phLoading">{{ $t('AcidModelMap.phLoading') }}</span>
+                    <span v-else>{{ currentPH?.toFixed(2) || $t('AcidModelMap.noDataText') }}</span>
                   </div>
                 </el-form-item>
               </div>
               <div class="input-buttons" style="margin-top: 16px;">
-                <el-button size="small" @click="cancelAcidInversion" :disabled="phLoading">取消</el-button>
+                <el-button size="small" @click="cancelAcidInversion" :disabled="phLoading">{{ $t('AcidModelMap.cancel') }}</el-button>
                 <el-button size="small" type="primary" @click="confirmAcidInversion" :loading="predictionLoading || phLoading">
-                  开始预测
+                  {{ $t('AcidModelMap.confirm') }}
                 </el-button>
               </div>
             </div>
 
-            <!-- 反酸结果(预测后替换参数区显示,和降酸样式1:1) -->
+            <!-- 反酸结果(预测后替换参数区显示,和降酸样式 1:1) -->
             <div v-if="showPredictionResult && currentPredictionType === 'inversion'" class="result-section">
-              <div class="section-title">反酸预测结果</div>
+              <div class="section-title">{{ $t('AcidModelMap.acidInversionResult') }}</div>
               <div v-if="predictionLoading" class="loading-info">
                 <div class="loading-spinner small"></div>
-                <span>预测中...</span>
+                <span>{{ $t('AcidModelMap.predicting') }}</span>
               </div>
               <div v-else-if="predictionError" class="error-info">
                 {{ predictionError }}
               </div>
               <div v-else-if="predictionResult" class="result-content">
                 <div class="result-item">
-                  <span class="prediction-value inversion">ΔpH{{ formatPredictionValue(predictionResult.prediction_reflux)}} </span>
+                  <span class="prediction-value inversion">{{ $t('AcidModelMap.deltaPH') }}{{ formatPredictionValue(predictionResult.prediction_reflux)}} </span>
                 </div>
                 <div class="result-buttons">
-                  <el-button size="small" @click="resetPrediction">重新预测</el-button>
-                  <el-button size="small" type="primary" @click="closePopup">关闭</el-button>
+                  <el-button size="small" @click="resetPrediction">{{ $t('AcidModelMap.rerun') }}</el-button>
+                  <el-button size="small" type="primary" @click="closePopup">{{ $t('AcidModelMap.close') }}</el-button>
                 </div>
               </div>
             </div>
@@ -190,14 +190,17 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, nextTick, onMounted, onUnmounted, computed } from "vue";
+import { reactive, ref, nextTick, computed, onMounted, onUnmounted } from "vue";
 import { ElMessage } from "element-plus";
 import type { FormInstance } from "element-plus";
 import { api8000 } from "@/utils/request";
 import { api5000 } from "../../../utils/request";
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 // 新增:反酸预测相关状态(区分降酸/反酸的显示)
-const phLoading = ref(false); // 控制当前pH的加载状态(点击预测后先加载pH)
+const phLoading = ref(false); // 控制当前pH的加载状态(点击预测后先加载 pH)
 
 // 反酸预测显示状态(补充缺失的变量)
 const showAcidInversionInput = ref(false);
@@ -252,7 +255,7 @@ const currentClickCoords = reactive({
 
 const currentPredictionType = ref<'reduction' | 'inversion' | null>(null);
 
-// 降酸预测参数输入相关(仅保留target_pH,删除NO3/NH4)
+// 降酸预测参数输入相关(仅保留 target_pH,删除 NO3/NH4)
 const acidReductionFormRef = ref<FormInstance | null>(null);
 interface AcidReductionParams {
   target_pH?: number;
@@ -273,15 +276,15 @@ const isPaddyField = computed(() => {
   return featureInfo.data?.landType === LandTypeEnum.PADDY_FIELD;
 });
 
-// 降酸预测校验规则(仅保留target_pH)
+// 降酸预测校验规则(仅保留 target_pH)
 const acidReductionRules = reactive({
   target_pH: [
-    { required: true, message: '请输入目标pH值', trigger: 'blur' },
-    { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+    { required: true, message: t('AcidModelMap.targetPH'), trigger: 'blur' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'blur' },
     { 
       validator: (_rule: any, value: number, callback: any) => {
         if (value < 0 || value > 14) {
-          callback(new Error('值范围在0-14之间'));
+          callback(new Error(t('validation.passwordLength')));
         } else {
           callback();
         }
@@ -296,21 +299,21 @@ const acidReductionRules = reactive({
 const formatPredictionValue = (value: any): string => {
   console.log('预测值原始数据:', value, '类型:', typeof value);
   
-  if (value === null || value === undefined) return '无数据';
+  if (value === null || value === undefined) return t('AcidModelMap.noData');
   
   if (Array.isArray(value)) {
-    if (value.length === 0) return '无数据';
+    if (value.length === 0) return t('AcidModelMap.noData');
     const num = Number(value[0]);
-    return isNaN(num) ? '无效数据' : num.toFixed(4);
+    return isNaN(num) ? t('AcidModelMap.noData') : num.toFixed(4);
   }
   
   if (typeof value === 'object') {
     console.warn('预测值是对象类型:', value);
-    return '数据格式错误';
+    return t('AcidModelMap.noData');
   }
   
   const num = Number(value);
-  return isNaN(num) ? '无效数据' : num.toFixed(4);
+  return isNaN(num) ? t('AcidModelMap.noData') : num.toFixed(4);
 };
 
 // 修改:在原地放大地块
@@ -358,10 +361,10 @@ const drawHighlightFeature = (geoJsonFeature: any) => {
     return scaledFeature;
   };
 
-  // 创建放大1倍的地块
+  // 创建放大 1 倍的地块
   const scaledFeature = scaleFeatureCoordinates(geoJsonFeature, 1);
 
-  // 使用放大后的GeoJSON创建高亮图层
+  // 使用放大后的 GeoJSON 创建高亮图层
   highlightLayer.value = L.geoJSON(scaledFeature, {
     style: {
       color: '#000',
@@ -387,7 +390,7 @@ const drawHighlightFeature = (geoJsonFeature: any) => {
   
   // 调整地图视图以更好地显示放大的地块
   map.value.flyToBounds(scaledBounds, {
-    padding: [50, 50], // 上下左右各50像素的内边距
+    padding: [50, 50], // 上下左右各 50 像素的内边距
     duration: 0.3,     // 动画时长
     maxZoom: 16        // 最大缩放级别限制
   });
@@ -467,7 +470,7 @@ const getFeatureInfo = async (_latlng: any, point: any): Promise<boolean> => {
 // 新增:获取水田地块基础参数(CEC/OM)
 const fetchPaddyFieldBaseParams = async (lng: number, lat: number) => {
   try {
-    // 调用nearest_point接口(仅传必要参数)
+    // 调用 nearest_point 接口(仅传必要参数)
     const response = await api8000.get('/api/vector/nearest-with-predictions', {
       params: {
         target_lon: lng.toString(),
@@ -484,16 +487,16 @@ const fetchPaddyFieldBaseParams = async (lng: number, lat: number) => {
     };
   } catch (error) {
     console.error('获取水田基础参数失败:', error);
-    ElMessage.error('获取地块基础参数失败,使用默认值');
+    ElMessage.error(t('AcidModelMap.fetchBaseParamsError'));
     // 接口调用失败时返回默认值
     return { CEC: 7.14, OM: 22.12 };
   }
 };
 
-// 新增:缓存已获取的原始pH值
+// 新增:缓存已获取的原始 pH 
 const originalPH = ref<number | null>(null);
 
-// 新增:单独获取当前pH的方法
+// 新增:单独获取当前pH 的方法
 const fetchCurrentPH = async () => {
   if (originalPH.value !== null) {
     currentPH.value = originalPH.value;
@@ -518,8 +521,8 @@ const fetchCurrentPH = async () => {
     currentPH.value = phValue;
     return phValue !== null;
   } catch (error) {
-    console.error('获取当前pH失败:', error);
-    ElMessage.error('获取土壤当前pH值失败,请重试');
+    console.error('获取当前pH 失败:', error);
+    ElMessage.error(t('AcidModelMap.fetchPHError'));
     currentPH.value = null;
     return false;
   } finally {
@@ -533,7 +536,7 @@ const startAcidReductionPrediction = async () => {
   const fetchSuccess = await fetchCurrentPH();
   if (!fetchSuccess) return;
 
-  // pH获取成功,显示降酸输入区
+  // pH 获取成功,显示降酸输入区
   currentPredictionType.value = 'reduction';
   showAcidReductionInput.value = true;
   showPredictionResult.value = false;
@@ -542,14 +545,14 @@ const startAcidReductionPrediction = async () => {
 // 反酸预测:点击后先加载pH,再显示反酸区域
 const startAcidInversionPrediction = async () => {
   if (!featureInfo.data?.landType) {
-    ElMessage.error('请先选择有效的地块(需包含用地类型)');
+    ElMessage.error(t('AcidModelMap.missingLandType'));
     return;
   }
   // 先获取当前pH
   const fetchSuccess = await fetchCurrentPH();
   if (!fetchSuccess) return;
 
-  // pH获取成功,显示反酸区域(无输入参数)
+  // pH 获取成功,显示反酸区域(无输入参数)
   currentPredictionType.value = 'inversion';
   showAcidInversionInput.value = true; 
   showAcidReductionInput.value = false;
@@ -572,14 +575,14 @@ const cancelAcidInversion = () => {
 const confirmAcidReduction = async () => {
   if (!acidReductionFormRef.value) return;
   if (currentPH.value === null) {
-    ElMessage.error('请先获取当前pH值');
+    ElMessage.error(t('AcidModelMap.fetchPHError'));
     return;
   }
 
   try {
     await acidReductionFormRef.value.validate();
     
-    // 调用预测接口(仅传target_pH,NO3/NH4用固定值)
+    // 调用预测接口(仅传 target_pH,NO3/NH4 用固定值)
     await callPredictionAPI(
       currentClickCoords.lng,
       currentClickCoords.lat,
@@ -588,18 +591,18 @@ const confirmAcidReduction = async () => {
       }
     );
   } catch (error) {
-    ElMessage.error('输入参数不合法,请检查后重试');
+    ElMessage.error(t('AcidModelMap.inputError'));
   }
 };
 
 // 确认反酸预测(无表单校验,直接调用)
 const confirmAcidInversion = async () => {
   if (currentPH.value === null) {
-    ElMessage.error('请先获取当前pH值');
+    ElMessage.error(t('AcidModelMap.fetchPHError'));
     return;
   }
   if (!featureInfo.data?.landType) {
-    ElMessage.error('未获取到地块用地类型,无法进行预测');
+    ElMessage.error(t('AcidModelMap.missingLandType'));
     return;
   }
 
@@ -610,7 +613,7 @@ const confirmAcidInversion = async () => {
       currentClickCoords.lat
     );
   } catch (error) {
-    ElMessage.error('预测请求失败,请稍后重试');
+    ElMessage.error(t('AcidModelMap.generalPredictError'));
   }
 };
 
@@ -635,7 +638,7 @@ const callPredictionAPI = async (
       
       // 2. 合并:基础参数 + 南雄固定参数
       const requestData = {
-        model_id: 36, // 水田反酸接口固定model_id
+        model_id: 36, // 水田反酸接口固定 model_id
         parameters: {
           CEC: baseParams.CEC,
           NH4: nanxiongFixedParams.NH4,
@@ -646,7 +649,7 @@ const callPredictionAPI = async (
         }
       };
 
-      // 3. 调用水田反酸POST接口
+      // 3. 调用水田反酸 POST 接口
       const response = await api5000.post('/predict', requestData);
       
       // 4. 处理返回结果
@@ -675,7 +678,7 @@ const callPredictionAPI = async (
       const response = await api8000.get(apiUrl, { params: requestParams });
       predictionResult.value = response.data;
 
-      // 同步缓存pH值
+      // 同步缓存 pH 
       const newPH = predictionResult.value.nearest_point?.ph !== undefined 
         ? Number(predictionResult.value.nearest_point.ph) 
         : originalPH.value;
@@ -692,8 +695,8 @@ const callPredictionAPI = async (
     console.error('调用预测接口失败:', error);
     // 友好的错误提示
     predictionError.value = currentPredictionType.value === 'inversion' && isPaddyField.value
-      ? '水田反酸预测失败,请检查参数或重试'
-      : '预测请求失败,请稍后重试';
+      ? t('AcidModelMap.inversionPredictError')
+      : t('AcidModelMap.generalPredictError');
   } finally {
     predictionLoading.value = false;
   }
@@ -768,7 +771,7 @@ const handleMapClick = async (e: any) => {
       showConnectionLine.value = false;
 
       ElMessage({
-        message: '请点击有效地块',
+        message: t('AcidModelMap.clickValidLand'),
         type: 'info',
         offset: 250,
         duration: 2000,
@@ -800,7 +803,7 @@ const closePopup = () => {
   showConnectionLine.value = false;
   resetPrediction();
   featureInfo.data = null;
-  // 清空pH缓存
+  // 清空 pH 缓存
   originalPH.value = null;
   currentPH.value = null;
   // 清除高亮图层
@@ -860,7 +863,7 @@ const onDrag = (event: MouseEvent) => {
   // 计算拖拽后的像素位置
   const newX = event.clientX - dragStartPos.x;
   const newY = event.clientY - dragStartPos.y;
-  // 转成百分比(限制0-95,避免弹窗超出可视区)
+  // 转成百分比(限制 0-95,避免弹窗超出可视区)
   popupPosition.x = Math.max(0, Math.min(95, (newX / viewportWidth) * 100));
   popupPosition.y = Math.max(0, Math.min(95, (newY / viewportHeight) * 100));
   updateConnectionLine();
@@ -918,7 +921,7 @@ const initMap = async () => {
   mapError.value = false;
 
   try {
-    // 动态导入Leaflet
+    // 动态导入 Leaflet
     if (!L) {
       L = await import('leaflet');
       await import('leaflet/dist/leaflet.css');
@@ -945,14 +948,14 @@ const initMap = async () => {
       zoom: 16
     });
 
-    // WMS配置
+    // WMS 配置
     const GEOSERVER_CONFIG = {
       url: "/geoserver/wms",
       workspace: "acidmap",
       layerGroup: "xiafencun", 
     };
 
-    // WMS图层配置
+    // WMS 图层配置
     const wmsLayer = L.tileLayer.wms(GEOSERVER_CONFIG.url, {
       layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
       format: "image/png",
@@ -993,7 +996,7 @@ const initMap = async () => {
     mapError.value = true;
     mapLoading.value = false;
     
-    let errorMessage = '地图初始化失败';
+    let errorMessage = t('AcidModelMap.mapInitError');
     if (error instanceof Error) {
       errorMessage += ': ' + error.message;
     }

+ 32 - 32
src/views/User/acidModel/pHPrediction.vue

@@ -2,7 +2,7 @@
   <el-card class="box-card">
     <template #header>
       <div class="card-header">
-        <span>pH预测模型</span>
+        <span>{{ $t('PhPrediction.title') }}</span>
       </div>
     </template>
 
@@ -12,103 +12,103 @@
       label-width="240px"
       label-position="left"
     >
-      <el-form-item label="有机质(g/kg)" prop="OM" :error="errorMessages.OM" required>
+      <el-form-item :label="$t('PhPrediction.organicMatter')" prop="OM" :error="errorMessages.OM" required>
         <el-input
           v-model="form.OM"
           size="large"
-          placeholder="请输入有机质0~35(g/kg)"
+          :placeholder="$t('PhPrediction.organicMatterPlaceholder')"
           @input="handleInput('OM', $event, 0, 35)"
         ></el-input>
       </el-form-item>
       
-      <el-form-item label="氯离子(g/kg)" prop="CL" :error="errorMessages.CL" required>
+      <el-form-item :label="$t('PhPrediction.chloride')" prop="CL" :error="errorMessages.CL" required>
         <el-input
           v-model="form.CL"
           size="large"
-          placeholder="请输入氯离子0~10(g/kg)"
+          :placeholder="$t('PhPrediction.chloridePlaceholder')"
           @input="handleInput('CL', $event, 0, 10)"
         ></el-input>
       </el-form-item>
 
-      <el-form-item label="阳离子交换量(cmol/kg)" prop="CEC" :error="errorMessages.CEC" required>
+      <el-form-item :label="$t('PhPrediction.cationExchangeCapacity')" prop="CEC" :error="errorMessages.CEC" required>
         <el-input
           v-model="form.CEC"
           size="large"
-          placeholder="请输入阳离子交换量0~20(cmol/kg)"
+          :placeholder="$t('PhPrediction.cecPlaceholder')"
           @input="handleInput('CEC', $event, 0, 20)"
         ></el-input>
       </el-form-item>
 
-      <el-form-item label="H+浓度(cmol/kg)" prop="H" :error="errorMessages.H" required>
+      <el-form-item :label="$t('PhPrediction.hydrogenIonConcentration')" prop="H" :error="errorMessages.H" required>
         <el-input
           v-model="form.H"
           size="large"
-          placeholder="请输入H+浓度0~1(cmol/kg)"
+          :placeholder="$t('PhPrediction.hPlaceholder')"
           @input="handleInput('H', $event, 0, 1)"
         ></el-input>
       </el-form-item>
 
-      <el-form-item label="铵态氮(mg/kg)" prop="HN" :error="errorMessages.HN" required>
+      <el-form-item :label="$t('PhPrediction.ammoniumNitrogen')" prop="HN" :error="errorMessages.HN" required>
         <el-input
           v-model="form.HN"
           size="large"
-          placeholder="请输入铵态氮0~30(mg/kg)"
+          :placeholder="$t('PhPrediction.hnPlaceholder')"
           @input="handleInput('HN', $event, 0, 30)"
         ></el-input>
       </el-form-item>
 
-      <el-form-item label="游离氧化铝(g/kg)" prop="Al3" :error="errorMessages.Al3" required>
+      <el-form-item :label="$t('PhPrediction.freeAlumina')" prop="Al3" :error="errorMessages.Al3" required>
         <el-input
           v-model="form.Al3"
           size="large"
-          placeholder="请输入游离氧化铝0~2(g/kg)"
+          :placeholder="$t('PhPrediction.al3Placeholder')"
           @input="handleInput('Al3', $event, 0, 2)"
         ></el-input>
       </el-form-item>
 
-       <el-form-item label="氧化铝(g/kg)" prop="AlOx" :error="errorMessages.AlOx" required>
+       <el-form-item :label="$t('PhPrediction.alumina')" prop="AlOx" :error="errorMessages.AlOx" required>
         <el-input
           v-model="form.AlOx"
           size="large"
-          placeholder="请输入氧化铝0~2(g/kg)"
+          :placeholder="$t('PhPrediction.aloxPlaceholder')"
           @input="handleInput('AlOx', $event, 0, 2)"
         ></el-input>
       </el-form-item>
 
-      <el-form-item label="游离铁氧化物(g/kg)" prop="FeOx" :error="errorMessages.FeOx" required>
+      <el-form-item :label="$t('PhPrediction.freeIronOxide')" prop="FeOx" :error="errorMessages.FeOx" required>
         <el-input
           v-model="form.FeOx"
           size="large"
-          placeholder="请输入游离铁氧化物0~3(g/kg)"
+          :placeholder="$t('PhPrediction.feoxPlaceholder')"
           @input="handleInput('FeOx', $event, 0, 3)"
         ></el-input>
       </el-form-item>
 
-      <el-form-item label="无定形铁(g/Kg)" prop="AmFe" :error="errorMessages.AmFe" required>
+      <el-form-item :label="$t('PhPrediction.amorphousIron')" prop="AmFe" :error="errorMessages.AmFe" required>
         <el-input
           v-model="form.AmFe"
           size="large"
-          placeholder="请输入无定形铁0~1(g/Kg)"
+          :placeholder="$t('PhPrediction.amfePlaceholder')"
           @input="handleInput('AmFe', $event, 0, 1)"
         ></el-input>
       </el-form-item>
 
-      <el-form-item label="初始pH值" prop="initpH" :error="errorMessages.initpH" required>
+      <el-form-item :label="$t('PhPrediction.initialPH')" prop="initpH" :error="errorMessages.initpH" required>
         <el-input
           v-model="form.initpH"
           size="large"
-          placeholder="请输入初始pH值0~14"
+          :placeholder="$t('PhPrediction.initphPlaceholder')"
           @input="handleInput('initpH', $event, 0, 14)"
         ></el-input>
       </el-form-item>
 
-      <el-button type="primary" @click="onSubmit" class="onSubmit">预测pH曲线</el-button>
-      <el-dialog v-model="dialogVisible" @close="onDialogClose" width="800px" align-center title="pH预测结果">
+      <el-button type="primary" @click="onSubmit" class="onSubmit">{{ $t('PhPrediction.predictButton') }}</el-button>
+      <el-dialog v-model="dialogVisible" @close="onDialogClose" width="800px" align-center :title="$t('PhPrediction.resultTitle')">
         <div class="image-container">
-            <img v-if="imageSrc" :src="imageSrc" alt="预测图片" class="full-image"/>
+            <img v-if="imageSrc" :src="imageSrc" :alt="$t('PhPrediction.resultTitle')" class="full-image"/>
         </div>
         <template #footer>
-          <el-button @click="dialogVisible = false">关闭</el-button>
+          <el-button @click="dialogVisible = false">{{ $t('PhPrediction.closeButton') }}</el-button>
         </template>
       </el-dialog>
     </el-form>
@@ -119,6 +119,9 @@
 import { reactive, ref } from "vue";
 import { ElMessage } from "element-plus";
 import { api5000 } from "../../../utils/request";
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 interface Form {
   OM: number | null;
@@ -173,7 +176,7 @@ const handleInput = (field: keyof Form, event: Event, min: number, max: number)
 
   const numValue = parseFloat(value);
   errorMessages[field] = (isNaN(numValue) || numValue < min || numValue > max)
-    ? `输入值应在 ${min} 到 ${max} 之间`
+    ? t('PhPrediction.validationRange', { min, max })
     : "";
 };
 
@@ -202,14 +205,14 @@ const onSubmit = async () => {
     const value = form[field];
     if (value === null || !validateInput(value.toString(), min, max)) {
       isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间`;
+      errorMessages[field] = t('PhPrediction.validationRange', { min, max });
     } else {
       errorMessages[field] = "";
     }
   }
 
   if (!isValid) {
-    ElMessage.error("输入值不符合要求,请检查输入");
+    ElMessage.error(t('PhPrediction.validationError'));
     return;
   }
 
@@ -246,18 +249,15 @@ const onSubmit = async () => {
     imageSrc.value = URL.createObjectURL(blob);
     dialogVisible.value = true;
   } catch (error) {
-    ElMessage.error(`请求失败: ${error}`);
+    ElMessage.error(`${t('PhPrediction.requestFailed')}${error}`);
   }
 };
-
 const onDialogClose = () => {
   dialogVisible.value = false;
   imageSrc.value = null;
   Object.keys(form).forEach(key => form[key as keyof Form] = null);
   Object.keys(errorMessages).forEach(key => errorMessages[key] = "");
 };
-
-
 </script>
 
 <style scoped>

+ 129 - 127
src/views/User/acidModel/shaoguan_acidmodelmap.vue

@@ -2,19 +2,19 @@
   <el-card class="map-card">
     <div class="title">
       <div class="section-icon">🗺️</div>
-      <p class="map-title">韶关市细粒度地块级酸化预测</p>
+      <p class="map-title">{{ $t('AcidModelMap.ShaoguanmapTitle') }}</p>
     </div>
 
     <div id="map-container" class="map-container">
       <div v-if="mapLoading" class="loading">
         <div class="loading-spinner"></div>
-        <span>地图加载中...</span>
+        <span>{{ $t('AcidModelMap.loadingTip') }}</span>
       </div>
       <div v-if="mapError" class="error-tip">
-        <el-alert title="地图加载失败" type="error" show-icon>
+        <el-alert :title="$t('AcidModelMap.errorTitle')" type="error" show-icon>
           <template #description>
-            <p>请检查GeoServer服务和配置</p>
-            <el-button @click="reloadMap" type="primary">重试加载</el-button>
+            <p>{{ $t('AcidModelMap.errorDesc') }}</p>
+            <el-button @click="reloadMap" type="primary">{{ $t('AcidModelMap.retryButton') }}</el-button>
           </template>
         </el-alert>
       </div>
@@ -34,20 +34,20 @@
           <!-- 地块信息 -->
           <div v-if="featureInfo.loading" class="loading-info">
             <div class="loading-spinner small"></div>
-            <span>加载地块信息中...</span>
+            <span>{{ $t('AcidModelMap.loadingFeatureInfo') }}</span>
           </div>
-          <div v-else-if="featureInfo.error" class="error-info">获取地块信息失败</div>
+          <div v-else-if="featureInfo.error" class="error-info">{{ $t('AcidModelMap.featureInfoError') }}</div>
           <div v-else-if="featureInfo.data" class="feature-info">
             <div class="info-item">
-              <label>所属村:</label>
-              <span>{{ featureInfo.data.village || '未知' }}</span>
+              <label>{{ $t('AcidModelMap.villageLabel') }}</label>
+              <span>{{ featureInfo.data.village || $t('AcidModelMap.unknown') }}</span>
             </div>
             <div class="info-item">
-              <label>用地类型:</label>
-              <span>{{ featureInfo.data.landType || '未知' }}</span>
+              <label>{{ $t('AcidModelMap.landTypeLabel') }}</label>
+              <span>{{ featureInfo.data.landType || $t('AcidModelMap.unknown') }}</span>
             </div>
           </div>
-          <div v-else class="no-data">无地块信息</div>
+          <div v-else class="no-data">{{ $t('AcidModelMap.noData') }}</div>
           <button @click="closePopup" class="close-btn">×</button>
         </div>
 
@@ -59,20 +59,20 @@
               type="success" 
              @click="startAcidInversionPrediction"
             >
-              反酸预测
+              {{ $t('AcidModelMap.acidInversionPrediction') }}
             </el-button>
             <el-button 
               size="small" 
               type="primary" 
               @click="startAcidReductionPrediction"
             >
-              降酸预测
+              {{ $t('AcidModelMap.acidReductionPrediction') }}
             </el-button>
           </div>
 
           <!-- 降酸预测参数输入 -->
           <div v-if="showAcidReductionInput" class="acid-reduction-input">
-           <div class="section-title">降酸预测参数</div>
+           <div class="section-title">{{ $t('AcidModelMap.acidReductionParams') }}</div>
   
              <!-- 关键修复:el-form 绑定 rules 和 ref -->
              <el-form 
@@ -84,14 +84,14 @@
               >
             <!-- 下面的 params-row 放在 el-form 内部 -->
                 <div class="params-row">
-                  <el-form-item label="当前pH" class="form-item-compact readonly-item">
+                  <el-form-item :label="$t('AcidModelMap.currentPH')" class="form-item-compact readonly-item">
                   <div class="current-ph-value">
-                     <span v-if="phLoading" class="loading-text">加载中...</span>
+                     <span v-if="phLoading" class="loading-text">{{ $t('AcidModelMap.phLoading') }}</span>
                      <span v-else-if="currentPH !== null">{{ currentPH.toFixed(2) }}</span>
-                     <span v-else class="no-data-text">未获取</span>
+                     <span v-else class="no-data-text">{{ $t('AcidModelMap.noDataText') }}</span>
                   </div>
                   </el-form-item>
-                  <el-form-item label="目标pH" prop="target_pH" class="form-item-compact">
+                  <el-form-item :label="$t('AcidModelMap.targetPH')" prop="target_pH" class="form-item-compact">
                     <el-input
                       v-model.number="acidReductionParams.target_pH"
                       type="number"
@@ -103,7 +103,7 @@
               </div>
 
     <div class="params-row">
-      <el-form-item label="硝酸盐" prop="NO3" class="form-item-compact">
+      <el-form-item :label="$t('AcidModelMap.nitrate')" prop="NO3" class="form-item-compact">
         
         <el-input
           v-model.number="acidReductionParams.NO3"
@@ -114,7 +114,7 @@
           :disabled="phLoading"
         />
       </el-form-item>
-      <el-form-item label="铵盐" prop="NH4" class="form-item-compact">
+      <el-form-item :label="$t('AcidModelMap.ammonium')" prop="NH4" class="form-item-compact">
         
         <el-input
           v-model.number="acidReductionParams.NH4"
@@ -129,16 +129,16 @@
   </el-form>
 
   <div class="input-buttons">
-    <el-button size="small" @click="cancelAcidReduction" :disabled="phLoading">取消</el-button>
+    <el-button size="small" @click="cancelAcidReduction" :disabled="phLoading">{{ $t('AcidModelMap.cancel') }}</el-button>
     <el-button size="small" type="primary" @click="confirmAcidReduction" :loading="predictionLoading || phLoading">
-      开始预测
+      {{ $t('AcidModelMap.confirm') }}
     </el-button>
   </div>
           </div>
 
          <!-- 反酸预测参数输入 -->
 <div v-if="showAcidInversionInput" class="acid-inversion-input" style="width: 100%; padding: 8px 0;">
-  <div class="section-title">反酸预测参数</div>
+  <div class="section-title">{{ $t('AcidModelMap.acidInversionParams') }}</div>
   <el-form 
     :model="acidInversionParams" 
     :rules="currentAcidInversionRules"  
@@ -147,22 +147,22 @@
     size="small"
     class="inversion-form" 
   >
-    <!-- 1. 当前pH:独占一行(非水田) / 2×2第一格(水田) -->
+    <!-- 1. 当前 pH:独占一行(非水田)/ 2×2 第一格(水田) -->
     <el-form-item 
-      label="当前pH" 
+      :label="$t('AcidModelMap.currentPH')" 
       prop="currentPH" 
       class="form-item-grid" 
       :class="{ 'full-width': !isPaddyField }"
     >
       <div class="current-ph-value">
-        <span v-if="phLoading" class="loading-text">加载中...</span>
+        <span v-if="phLoading" class="loading-text">{{ $t('AcidModelMap.phLoading') }}</span>
         <span v-else-if="currentPH !== null">{{ currentPH.toFixed(2) }}</span>
-        <span v-else class="no-data-text">未获取</span>
+        <span v-else class="no-data-text">{{ $t('AcidModelMap.noDataText') }}</span>
       </div>
     </el-form-item>
 
-    <!-- 2. NO3:非水田第二行左 / 水田2×2第二格 -->
-    <el-form-item label="硝酸盐" prop="NO3" class="form-item-grid">
+    <!-- 2. NO3:非水田第二行左 / 水田 2×2 第二格 -->
+    <el-form-item :label="$t('AcidModelMap.nitrate')" prop="NO3" class="form-item-grid">
       <el-input
         v-model.number="acidInversionParams.NO3"
         type="number"
@@ -174,8 +174,8 @@
       />
     </el-form-item>
 
-    <!-- 3. NH4:非水田第二行右 / 水田2×2第三格 -->
-    <el-form-item label="铵盐" prop="NH4" class="form-item-grid">
+    <!-- 3. NH4:非水田第二行右 / 水田 2×2 第三格 -->
+    <el-form-item :label="$t('AcidModelMap.ammonium')" prop="NH4" class="form-item-grid">
       <el-input
         v-model.number="acidInversionParams.NH4"
         type="number"
@@ -187,10 +187,10 @@
       />
     </el-form-item>
 
-    <!-- 4. FeO:仅水田显示,2×2第四格 -->
+    <!-- 4. FeO:仅水田显示,2×2 第四格 -->
     <el-form-item 
       v-if="isPaddyField" 
-      label="氧化铁" 
+      :label="$t('AcidModelMap.ferricOxide')" 
       prop="FeO" 
       class="form-item-grid"
     >
@@ -207,9 +207,9 @@
   </el-form>
 
   <div class="input-buttons" style="margin-top: 16px;">
-    <el-button size="small" @click="cancelAcidInversion" :disabled="phLoading">取消</el-button>
+    <el-button size="small" @click="cancelAcidInversion" :disabled="phLoading">{{ $t('AcidModelMap.cancel') }}</el-button>
     <el-button size="small" type="primary" @click="confirmAcidInversion" :loading="predictionLoading || phLoading">
-      开始预测
+      {{ $t('AcidModelMap.confirm') }}
     </el-button>
   </div>
 </div>
@@ -219,7 +219,7 @@
             <div class="section-title">{{ predictionTitle }}</div>
             <div v-if="predictionLoading" class="loading-info">
               <div class="loading-spinner small"></div>
-              <span>预测中...</span>
+              <span>{{ $t('AcidModelMap.predicting') }}</span>
             </div>
             <div v-else-if="predictionError" class="error-info">
               {{ predictionError }}
@@ -227,15 +227,15 @@
             <div v-else-if="predictionResult" class="prediction-result">
               <!--降酸结果-->
               <div class="result-item" v-if="currentPredictionType === 'reduction' && predictionResult.prediction_reduce !== undefined">
-                <span class="prediction-value reduction">每亩地土壤表层20cm撒{{ formatPredictionValue(predictionResult.prediction_reduce)}} 吨</span>
+                <span class="prediction-value reduction">{{ $t('AcidModelMap.resultUnit') }}{{ formatPredictionValue(predictionResult.prediction_reduce)}} {{ $t('AcidModelMap.ton') }}</span>
               </div>
               <!-- 反酸预测结果 -->
               <div class="result-item" v-if="currentPredictionType === 'inversion' && predictionResult.prediction_reflux !== undefined">
-                <span class="prediction-value inversion">ΔpH{{ formatPredictionValue(predictionResult.prediction_reflux)}} </span>
+                <span class="prediction-value inversion">{{ $t('AcidModelMap.deltaPH') }}{{ formatPredictionValue(predictionResult.prediction_reflux)}} </span>
               </div>
               <div class="result-buttons">
-                <el-button size="small" @click="resetPrediction">重新预测</el-button>
-                <el-button size="small" type="primary" @click="closePopup">关闭</el-button>
+                <el-button size="small" @click="resetPrediction">{{ $t('AcidModelMap.rerun') }}</el-button>
+                <el-button size="small" type="primary" @click="closePopup">{{ $t('AcidModelMap.close') }}</el-button>
               </div>
             </div>
           </div>
@@ -251,15 +251,18 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, nextTick, onMounted, onUnmounted, computed } from "vue";
+import { reactive, ref, nextTick, computed, onMounted, onUnmounted } from "vue";
 import { ElMessage } from "element-plus";
 import type { FormInstance } from "element-plus";
 import { api8000 } from "@/utils/request";
 import { api5000 } from "../../../utils/request";
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 // 新增:反酸预测相关状态(区分降酸/反酸的显示)
 const showInversionPrediction = ref(false); // 控制反酸预测区域显示
-const phLoading = ref(false); // 控制当前pH的加载状态(点击预测后先加载pH)
+const phLoading = ref(false); // 控制当前 pH 的加载状态(点击预测后先加载 pH)
 
 // 地图状态
 const mapLoading = ref(true);
@@ -272,7 +275,6 @@ const highlightLayer = ref<any>(null);
 // 新增:控制连接线显示的标志
 const showConnectionLine = ref(false);
 
-
 // 预测状态
 const predictionLoading = ref(false);
 const predictionError = ref('');
@@ -284,7 +286,7 @@ const currentPH = ref<number | null>(null);
 let L: any = null;
 
 const defaultParams = reactive({
-  NO3: 34.05, // 随便填一个合理默认值(后端接口要求传,但不影响pH返回)
+  NO3: 34.05, // 随便填一个合理默认值(后端接口要求传,但不影响 pH 返回)
   NH4: 7.87   // 同上,只要是合法数字即可
 });
 
@@ -334,14 +336,14 @@ const LandTypeEnum = {
   PADDY_FIELD: '水田'      // 水田
 } as const;
 
-// 扩展反酸预测参数接口(新增FeO字段)
+// 扩展反酸预测参数接口(新增 FeO 字段)
 interface AcidInversionParams {
   NO3?: number;
   NH4?: number;
-  FeO?: number; // 水田新增参数:氧化铁
+  FeO?: number; // 水田专用参数:氧化铁
 }
 
-// 初始化反酸参数(包含FeO默认值)
+// 初始化反酸参数(包含 FeO 默认值)
 const acidInversionParams = reactive<AcidInversionParams>({
   NO3: undefined,
   NH4: undefined,
@@ -351,28 +353,28 @@ const acidInversionParams = reactive<AcidInversionParams>({
 // 新增水田专用表单验证规则
 const acidInversionPaddyRules = reactive({
   NO3: [
-    { required: true, message: '请输入NO3', trigger: 'change' },
-    { type: 'number', message: '请输入有效数字', trigger: 'change' },
+    { required: true, message: t('AcidModelMap.nitrate'), trigger: 'change' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'change' },
     { validator: (_rule: any, value: number, callback: any) => {
-        if (value < 0) callback(new Error('值不能为负数'));
+        if (value < 0) callback(new Error(t('validation.passwordLength')));
         else callback();
       }, trigger: 'change' 
     }
   ],
   NH4: [
-    { required: true, message: '请输入NH4', trigger: 'change' },
-    { type: 'number', message: '请输入有效数字', trigger: 'change' },
+    { required: true, message: t('AcidModelMap.ammonium'), trigger: 'change' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'change' },
     { validator: (_rule: any, value: number, callback: any) => {
-        if (value < 0) callback(new Error('值不能为负数'));
+        if (value < 0) callback(new Error(t('validation.passwordLength')));
         else callback();
       }, trigger: 'change' 
     }
   ],
   FeO: [
-    { required: true, message: '请输入FeO', trigger: 'change' },
-    { type: 'number', message: '请输入有效数字', trigger: 'change' },
+    { required: true, message: t('AcidModelMap.ferricOxide'), trigger: 'change' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'change' },
     { validator: (_rule: any, value: number, callback: any) => {
-        if (value < 0) callback(new Error('值不能为负数'));
+        if (value < 0) callback(new Error(t('validation.passwordLength')));
         else callback();
       }, trigger: 'change' 
     }
@@ -389,7 +391,7 @@ const currentAcidInversionRules = computed(() => {
   if (isPaddyField.value) {
     return acidInversionPaddyRules;
   }
-  // 非水田使用原有规则(仅NO3、NH4)
+  // 非水田使用原有规则(仅 NO3、NH4)
   return acidInversionRules;
 });
 
@@ -399,12 +401,12 @@ const showAcidInversionInput = ref(false);
 // 添加反酸预测的表单验证规则
 const acidInversionRules = reactive({
   NO3: [
-    { required: true, message: '请输入NO3', trigger: 'change' },
-    { type: 'number', message: '请输入有效数字', trigger: 'change' },
+    { required: true, message: t('AcidModelMap.nitrate'), trigger: 'change' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'change' },
     { 
       validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
-          callback(new Error('值不能为负数'));
+          callback(new Error(t('validation.passwordLength')));
         } else {
           callback();
         }
@@ -413,12 +415,12 @@ const acidInversionRules = reactive({
     }
   ],
   NH4: [
-    { required: true, message: '请输入NH4', trigger: 'change' },
-    { type: 'number', message: '请输入有效数字', trigger: 'change' },
+    { required: true, message: t('AcidModelMap.ammonium'), trigger: 'change' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'change' },
     { 
       validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
-          callback(new Error('值不能为负数'));
+          callback(new Error(t('validation.passwordLength')));
         } else {
           callback();
         }
@@ -434,20 +436,20 @@ const acidInversionFormRef = ref<FormInstance | null>(null);
 
 // 预测标题
 const predictionTitle = computed(() => {
-  if (currentPredictionType.value === 'reduction') return '降酸预测结果';
-  if (currentPredictionType.value === 'inversion') return '反酸预测结果';
-  return '预测结果';
+  if (currentPredictionType.value === 'reduction') return t('AcidModelMap.acidReductionResult');
+  if (currentPredictionType.value === 'inversion') return t('AcidModelMap.acidInversionResult');
+  return t('AcidModelMap.predictionResult');
 });
 
 // 输入校验规则
 const acidReductionRules = reactive({
   target_pH: [
-    { required: true, message: '请输入目标pH值', trigger: 'blur' },
-    { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+    { required: true, message: t('AcidModelMap.targetPH'), trigger: 'blur' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'blur' },
     { 
       validator: (_rule: any, value: number, callback: any) => {
         if (value < 0 || value > 14) {
-          callback(new Error('值范围在0-14之间'));
+          callback(new Error(t('validation.passwordLength')));
         } else {
           callback();
         }
@@ -456,12 +458,12 @@ const acidReductionRules = reactive({
     }
   ],
   NO3: [
-    { required: true, message: '请输入NO3', trigger: 'blur' },
-    { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+    { required: true, message: t('AcidModelMap.nitrate'), trigger: 'blur' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'blur' },
     { 
       validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
-          callback(new Error('值不能为负数'));
+          callback(new Error(t('validation.passwordLength')));
         } else {
           callback();
         }
@@ -469,13 +471,13 @@ const acidReductionRules = reactive({
       trigger: 'blur' 
     }
   ],
-  NH4: [ // 修复:之前是nh4,和prop不一致
-    { required: true, message: '请输入NH4', trigger: 'blur' },
-    { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+  NH4: [
+    { required: true, message: t('AcidModelMap.ammonium'), trigger: 'blur' },
+    { type: 'number', message: t('validation.passwordLength'), trigger: 'blur' },
     { 
       validator: (_rule: any, value: number, callback: any) => {
         if (value < 0) {
-          callback(new Error('值不能为负数'));
+          callback(new Error(t('validation.passwordLength')));
         } else {
           callback();
         }
@@ -489,21 +491,21 @@ const acidReductionRules = reactive({
 const formatPredictionValue = (value: any): string => {
   console.log('预测值原始数据:', value, '类型:', typeof value);
   
-  if (value === null || value === undefined) return '无数据';
+  if (value === null || value === undefined) return t('AcidModelMap.noData');
   
   if (Array.isArray(value)) {
-    if (value.length === 0) return '无数据';
+    if (value.length === 0) return t('AcidModelMap.noData');
     const num = Number(value[0]);
-    return isNaN(num) ? '无效数据' : num.toFixed(4);
+    return isNaN(num) ? t('AcidModelMap.noData') : num.toFixed(4);
   }
   
   if (typeof value === 'object') {
     console.warn('预测值是对象类型:', value);
-    return '数据格式错误';
+    return t('AcidModelMap.noData');
   }
   
   const num = Number(value);
-  return isNaN(num) ? '无效数据' : num.toFixed(4);
+  return isNaN(num) ? t('AcidModelMap.noData') : num.toFixed(4);
 };
 
 // 修改:在原地放大地块
@@ -556,10 +558,10 @@ const drawHighlightFeature = (geoJsonFeature: any) => {
     return scaledFeature;
   };
 
-  // 创建放大1倍的地块
+  // 创建放大 1 倍的地块
   const scaledFeature = scaleFeatureCoordinates(geoJsonFeature, 1);
 
-  // 使用放大后的GeoJSON创建高亮图层
+  // 使用放大后的 GeoJSON 创建高亮图层
   highlightLayer.value = L.geoJSON(scaledFeature, {
     style: {
       color: '#000',
@@ -586,8 +588,8 @@ const drawHighlightFeature = (geoJsonFeature: any) => {
   // 调整地图视图以更好地显示放大的地块
   // 添加一些内边距,让地块不会紧贴地图边缘
   map.value.flyToBounds(scaledBounds, {
-    padding: [50, 50], // 上下左右各50像素的内边距
-    duration: 0.3,       // 1秒动画
+    padding: [50, 50], // 上下左右各 50 像素的内边距
+    duration: 0.3,       // 1 秒动画
     maxZoom: 16        // 最大缩放级别限制,避免放得太大
   });
 
@@ -663,10 +665,11 @@ const getFeatureInfo = async (_latlng: any, point: any): Promise<boolean> => {
     featureInfo.loading = false;
   }
 };
+
 // 新增:获取水田地块基础参数(CEC/Al/OM)
 const fetchPaddyFieldBaseParams = async (lng: number, lat: number) => {
   try {
-    // 调用nearest_point接口(和获取currentPH的接口一致)
+    // 调用 nearest_point 接口(和获取 currentPH 的接口一致)
     const response = await api8000.get('/api/vector/nearest-with-predictions', {
       params: {
         target_lon: lng.toString(),
@@ -678,24 +681,24 @@ const fetchPaddyFieldBaseParams = async (lng: number, lat: number) => {
     });
 
     const nearestPoint = response.data.nearest_point || {};
-    // 返回接口中的基础参数(根据实际接口返回字段调整key)
+    // 返回接口中的基础参数(根据实际接口返回字段调整 key)
     return {
-      CEC: nearestPoint.CEC !==  Number(nearestPoint.CEC) , 
-      Al: nearestPoint.Al !== Number(nearestPoint.Al) ,
+      CEC: nearestPoint.CEC !==  Number(nearestPoint.CEC), 
+      Al: nearestPoint.Al !== Number(nearestPoint.Al),
       OM: nearestPoint.OM !==  Number(nearestPoint.OM)
     };
   } catch (error) {
     console.error('获取水田基础参数失败:', error);
-    ElMessage.error('获取地块基础参数失败,使用默认值');
+    ElMessage.error(t('AcidModelMap.fetchBaseParamsError'));
     // 接口调用失败时返回默认值
     return { CEC: 7.14, Al: 4.0, OM: 22.12 };
   }
 };
 
-// 新增:缓存已获取的原始pH值(避免预测接口覆盖)
+// 新增:缓存已获取的原始 pH 值(避免预测接口覆盖)
 const originalPH = ref<number | null>(null);
 
-// 新增:单独获取当前pH的方法(点击预测后先加载pH)
+// 新增:单独获取当前 pH 的方法(点击预测后先加载 pH)
 const fetchCurrentPH = async () => {
   if (originalPH.value !== null) {
     currentPH.value = originalPH.value; // 优先用缓存值
@@ -718,12 +721,12 @@ const fetchCurrentPH = async () => {
       ? Number(response.data.nearest_point.ph) 
       : null;
       
-    originalPH.value = phValue; // 缓存原始pH
+    originalPH.value = phValue; // 缓存原始 pH
     currentPH.value = phValue;
     return phValue !== null;
   } catch (error) {
-    console.error('获取当前pH失败:', error);
-    ElMessage.error('获取土壤当前pH值失败,请重试');
+    console.error('获取当前 pH 失败:', error);
+    ElMessage.error(t('AcidModelMap.fetchPHError'));
     currentPH.value = null;
     return false;
   } finally {
@@ -731,30 +734,30 @@ const fetchCurrentPH = async () => {
   }
 };
 
-//  降酸预测:点击后先加载pH,再显示输入区
+//  降酸预测:点击后先加载 pH,再显示输入区
 const startAcidReductionPrediction = async () => {
-  // 先获取当前pH
+  // 先获取当前 pH
   const fetchSuccess = await fetchCurrentPH();
   if (!fetchSuccess) return;
 
-  // pH获取成功,显示降酸输入区
+  // pH 获取成功,显示降酸输入区
   currentPredictionType.value = 'reduction';
   showAcidReductionInput.value = true;
   showInversionPrediction.value = false;
   showPredictionResult.value = false;
 };
 
-// 反酸预测:点击后先加载pH,再显示反酸区域
+// 反酸预测:点击后先加载 pH,再显示反酸区域
 const startAcidInversionPrediction = async () => {
   if (!featureInfo.data?.landType) {
-    ElMessage.error('请先选择有效的地块(需包含用地类型)');
+    ElMessage.error(t('AcidModelMap.missingLandType'));
     return;
   }
-  // 先获取当前pH
+  // 先获取当前 pH
   const fetchSuccess = await fetchCurrentPH();
   if (!fetchSuccess) return;
 
-  // pH获取成功,显示反酸区域(无输入参数)
+  // pH 获取成功,显示反酸区域(无输入参数)
   currentPredictionType.value = 'inversion';
   showAcidInversionInput.value = true;
   showAcidReductionInput.value = false;
@@ -782,7 +785,7 @@ const cancelAcidInversion = () => {
 const confirmAcidReduction = async () => {
   if (!acidReductionFormRef.value) return;
   if (currentPH.value === null) {
-    ElMessage.error('请先获取当前pH值');
+    ElMessage.error(t('AcidModelMap.fetchPHError'));
     return;
   }
 
@@ -800,7 +803,7 @@ const confirmAcidReduction = async () => {
       }
     );
   } catch (error) {
-    ElMessage.error('输入参数不合法,请检查后重试');
+    ElMessage.error(t('AcidModelMap.inputError'));
   }
 };
 
@@ -808,19 +811,19 @@ const confirmAcidReduction = async () => {
 const confirmAcidInversion = async () => {
   if (!acidInversionFormRef.value) return;
   if (currentPH.value === null) {
-    ElMessage.error('请先获取当前pH值');
+    ElMessage.error(t('AcidModelMap.fetchPHError'));
     return;
   }
   // 校验用地类型是否存在
   if (!featureInfo.data?.landType) {
-    ElMessage.error('未获取到地块用地类型,无法进行预测');
+    ElMessage.error(t('AcidModelMap.missingLandType'));
     return;
   }
 
   try {
     await acidInversionFormRef.value.validate();
     
-    // 调用预测接口 - 反酸预测需要传递NO3和NH4参数
+    // 调用预测接口 - 反酸预测需要传递 NO3  NH4 参数
     await callPredictionAPI(
       currentClickCoords.lng,
       currentClickCoords.lat,
@@ -831,7 +834,7 @@ const confirmAcidInversion = async () => {
       }
     );
   } catch (error) {
-    ElMessage.error('输入参数不合法,请检查后重试');
+    ElMessage.error(t('AcidModelMap.inputError'));
   }
 };
 
@@ -857,26 +860,26 @@ const callPredictionAPI = async (
       // 1. 先获取地块基础参数(CEC/Al/OM)
       const baseParams = await fetchPaddyFieldBaseParams(lng, lat);
       
-      // 2. 合并:基础参数(接口读取) + 用户输入参数(表单)
+      // 2. 合并:基础参数(接口读取)+ 用户输入参数(表单)
       const requestData = {
-        model_id: 36, // 水田反酸接口固定model_id
+        model_id: 36, // 水田反酸接口固定 model_id
         parameters: {
-          CEC: baseParams.CEC, // 从nearest_point读取
+          CEC: baseParams.CEC, // 从 nearest_point 读取
           NH4: params?.NH4,
           NO3: params?.NO3,
-          Al: baseParams.Al,   // 从nearest_point读取
+          Al: baseParams.Al,   // 从 nearest_point 读取
           FeO: params?.FeO,
-          OM: baseParams.OM,   // 从nearest_point读取
+          OM: baseParams.OM,   // 从 nearest_point 读取
         }
       };
 
-      // 3. 调用水田反酸POST接口(替换为你的实际接口路径)
+      // 3. 调用水田反酸 POST 接口(替换为你的实际接口路径)
       const response = await api5000.post('/predict', requestData);
       
       // 4. 处理返回结果(根据实际接口返回字段调整)
       // 假设接口返回 { prediction_reflux: 0.5 } 这类反酸结果
       predictionResult.value = {
-        prediction_reflux: response.data.result?.[0], // 取result数组的第一个元素
+        prediction_reflux: response.data.result?.[0], // 取 result 数组的第一个元素
         nearest_point: { ph: originalPH.value }
       };
       currentPH.value = originalPH.value;
@@ -922,8 +925,8 @@ const callPredictionAPI = async (
     console.error('调用预测接口失败:', error);
     // 友好的错误提示
     predictionError.value = currentPredictionType.value === 'inversion' && isPaddyField.value
-      ? '水田反酸预测失败,请检查参数或重试'
-      : '预测请求失败,请稍后重试';
+      ? t('AcidModelMap.inversionPredictError')
+      : t('AcidModelMap.generalPredictError');
   } finally {
     predictionLoading.value = false;
   }
@@ -999,9 +1002,9 @@ const handleMapClick = async (e: any) => {
       showConnectionLine.value=false;
 
       ElMessage({
-       message: '请点击有效地块',
+       message: t('AcidModelMap.clickValidLand'),
        type: 'info',
-       offset: 250, // 距离顶部偏移(默认20)
+       offset: 250, // 距离顶部偏移(默认 20)
        duration: 2000, // 显示时长
        customClass: 'custom-land-message', // 自定义样式类名
       });
@@ -1031,7 +1034,7 @@ const closePopup = () => {
   showConnectionLine.value=false;
   resetPrediction();
   featureInfo.data = null;
-  // 清空pH缓存
+  // 清空 pH 缓存
   originalPH.value = null;
   currentPH.value = null;
   // 清除高亮图层
@@ -1091,7 +1094,7 @@ const onDrag = (event: MouseEvent) => {
   // 计算拖拽后的像素位置
   const newX = event.clientX - dragStartPos.x;
   const newY = event.clientY - dragStartPos.y;
-  // 转成百分比(限制0-95,避免弹窗超出可视区)
+  // 转成百分比(限制 0-95,避免弹窗超出可视区)
   popupPosition.x = Math.max(0, Math.min(95, (newX / viewportWidth) * 100));
   popupPosition.y = Math.max(0, Math.min(95, (newY / viewportHeight) * 100));
   updateConnectionLine();
@@ -1149,7 +1152,7 @@ const initMap = async () => {
   mapError.value = false;
 
   try {
-    // 动态导入Leaflet
+    // 动态导入 Leaflet
     if (!L) {
       L = await import('leaflet');
       await import('leaflet/dist/leaflet.css');
@@ -1176,14 +1179,14 @@ const initMap = async () => {
       zoom: 10
     });
 
-    // WMS配置
+    // WMS 配置
     const GEOSERVER_CONFIG = {
       url: "/geoserver/wms",
       workspace: "acidmap",
       layerGroup: "mapwithboundary", 
     };
 
-    // WMS图层配置
+    // WMS 图层配置
     const wmsLayer = L.tileLayer.wms(GEOSERVER_CONFIG.url, {
       layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
       format: "image/png",
@@ -1224,7 +1227,7 @@ const initMap = async () => {
     mapError.value = true;
     mapLoading.value = false;
     
-    let errorMessage = '地图初始化失败';
+    let errorMessage = t('AcidModelMap.mapInitError');
     if (error instanceof Error) {
       errorMessage += ': ' + error.message;
     }
@@ -1653,7 +1656,6 @@ const handleWindowResize = () => {
 }
 
 :deep(.form-item-compact .el-form-item__label) {
-  width: 60px !important; /* 调整标签宽度 */
   text-align: right;
   padding-right: 8px;
 }

+ 54 - 41
src/views/User/dataStatistics/LandCultivatedStatistics.vue

@@ -2,59 +2,67 @@
   <div class="container">
     <div class="chart-wrapper" ref="chartRef"></div>  
   </div>
-    
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, onMounted, onUnmounted, watch } from 'vue'
 import * as echarts from 'echarts'
-import { api8000 } from '@/utils/request' // 导入 api8000 实例
+import { api8000 } from '@/utils/request'
+import { useI18n } from 'vue-i18n'
 
 const chartRef = ref(null)
+const { t } = useI18n()
+let myChart = null
 
-onMounted(async () => {
+const renderChart = async () => {
   try {
-    // 1. 使用 api8000 实例请求数据
     const res = await api8000.get('/api/unit-grouping/areas/statistics')
     const apiData = res.data
 
-    // 2. 处理数据
     const regions = Object.keys(apiData.area_statistics)
-    const categories = ['优先保护类', '严格管控类', '安全利用类']
     
-    // 计算全部县的汇总数据
+    const categories = [
+      t('LandCultivatedStatistics.priorityProtection'),
+      t('LandCultivatedStatistics.strictControl'),
+      t('LandCultivatedStatistics.safeUtilization')
+    ]
+    
     const totalData = {}
-    categories.forEach(category => {
-      totalData[category] = regions.reduce((sum, region) => {
-        return sum + apiData.area_statistics[region][category]
+    categories.forEach((category, index) => {
+      const key = ['优先保护类', '严格管控类', '安全利用类'][index]
+      totalData[key] = regions.reduce((sum, region) => {
+        return sum + apiData.area_statistics[region][key]
       }, 0)
     })
     
-    // 添加"全部县"到地区列表
-    const allRegions = [...regions, '全部县']
+    const allRegions = [...regions, t('LandCultivatedStatistics.allRegions')]
     
-    // 构造ECharts系列数据,包含全部县汇总
-    const seriesData = categories.map(category => ({
-      name: category,
-      type: 'bar',
-      data: [
-        ...regions.map(region => apiData.area_statistics[region][category]),
-        totalData[category]  // 添加汇总数据
-      ]
-    }))
+    const seriesData = categories.map((category, index) => {
+      const key = ['优先保护类', '严格管控类', '安全利用类'][index]
+      return ({
+        name: category,
+        type: 'bar',
+        data: [
+          ...regions.map(region => apiData.area_statistics[region][key]),
+          totalData[key]
+        ]
+      })
+    })
 
-    // 3. 渲染图表
     const chartDom = chartRef.value
     if (!chartDom) {
       console.error('图表容器未找到')
       return
     }
     
-    const myChart = echarts.init(chartDom)
+    if (myChart) {
+      myChart.dispose()
+    }
+    myChart = echarts.init(chartDom)
 
     const option = {
       title: { 
-        text: '耕地质量评估按区域分类数据统计',
+        text: t('LandCultivatedStatistics.title'),
         left: 'center',
         textStyle: {
           fontSize: 16,
@@ -87,7 +95,7 @@ onMounted(async () => {
           interval: 0,
           rotate: 30,
           formatter: (value) => {
-            return value === '全部县' ? `{total|${value}}` : value
+            return value === t('LandCultivatedStatistics.allRegions') ? `{total|${value}}` : value
           },
           rich: {
             total: {
@@ -99,7 +107,7 @@ onMounted(async () => {
       },
       yAxis: { 
         type: 'value', 
-        name: '数量', 
+        name: t('LandCultivatedStatistics.quantity'), 
         nameTextStyle: { fontSize: 14 },
         axisLabel: { fontSize: 12 },
         nameLocation: 'end'
@@ -112,33 +120,38 @@ onMounted(async () => {
         top: '15%',
         containLabel: true 
       },
-      color: ['#5470c6', '#91cc75', '#fac858'] // 自定义颜色
+      color: ['#5470c6', '#91cc75', '#fac858']
     }
 
     myChart.setOption(option)
     
-    // 监听窗口变化自适应
-    const handleResize = () => myChart.resize()
-    window.addEventListener('resize', handleResize)
-    
-    // 组件卸载时清除事件监听
-    onUnmounted(() => {
-      window.removeEventListener('resize', handleResize)
-      myChart.dispose() // 销毁图表实例
-    })
-
   } catch (error) {
     console.error('接口请求失败:', error)
-    // 可以在这里添加错误处理UI
   }
+}
+
+onMounted(async () => {
+  await renderChart()
+  
+  const handleResize = () => myChart && myChart.resize()
+  window.addEventListener('resize', handleResize)
+  
+  onUnmounted(() => {
+    window.removeEventListener('resize', handleResize)
+    if (myChart) {
+      myChart.dispose()
+    }
+  })
+})
+
+watch(() => t('LandCultivatedStatistics.title'), () => {
+  renderChart()
 })
 </script>
 
 <style scoped>
-
 .container {
   padding: 20px;
-  /* 添加70%透明度的渐变背景 */
   background: linear-gradient(
     135deg, 
     rgba(230, 247, 255, 0.7) 0%, 

+ 37 - 34
src/views/User/neutralizationModel/AcidNeutralizationModel.vue

@@ -2,7 +2,7 @@
   <el-card class="box-card">
     <template #header>
       <div class="card-header">
-        <span>降酸模型</span>
+        <span>{{ $t('Calculation.neutralizationTitle') }}</span>
       </div>
     </template>
 
@@ -13,7 +13,7 @@
       label-position="left"
     >
       <el-form-item
-        label="土壤初始 pH"
+        :label="$t('Calculation.initialPH')"
         prop="init_pH"
         :error="errorMessages.init_pH"
         required
@@ -21,7 +21,7 @@
         <el-input
           v-model="form.init_pH"
           size="large"
-          placeholder="请输入土壤初始 3~6 pH"
+          :placeholder="$t('Calculation.neutralization.initialPHPlaceholder')"
           ref="inputRefs.init_pH"
           @input="handleInput('init_pH', $event, 3, 6)"
         >
@@ -29,7 +29,7 @@
       </el-form-item>
 
       <el-form-item
-        label="土壤目标 pH"
+        :label="$t('Calculation.targetPH')"
         prop="target_pH"
         :error="errorMessages.target_pH"
         required
@@ -37,7 +37,7 @@
         <el-input
           v-model="form.target_pH"
           size="large"
-          placeholder="请输入土壤目标 5~7 pH"
+          :placeholder="$t('Calculation.neutralization.targetPHPlaceholder')"
           ref="inputRefs.target_pH"
           @input="handleInput('target_pH', $event, 5, 7)"
         >
@@ -45,7 +45,7 @@
       </el-form-item>
 
       <el-form-item
-        label="交换性氢(cmol/kg)"
+        :label="$t('Calculation.exchangeableHydrogen')"
         prop="H"
         :error="errorMessages.H"
         required
@@ -53,7 +53,7 @@
         <el-input
           v-model="form.H"
           size="large"
-          placeholder="请输入交换性氢 0~4 (cmol/kg)"
+          :placeholder="$t('Calculation.neutralization.exchangeableHydrogenPlaceholder')"
           ref="inputRefs.H"
           @input="handleInput('H', $event, 0, 4)"
         >
@@ -61,7 +61,7 @@
       </el-form-item>
 
       <el-form-item
-        label="交换性铝(cmol/kg)"
+        :label="$t('Calculation.exchangeableAluminum')"
         prop="Al"
         :error="errorMessages.Al"
         required
@@ -69,7 +69,7 @@
         <el-input
           v-model="form.Al"
           size="large"
-          placeholder="请输入交换性铝 0~8 (cmol/kg)"
+          :placeholder="$t('Calculation.neutralization.exchangeableAluminumPlaceholder')"
           ref="inputRefs.Al"
           @input="handleInput('Al', $event, 0, 8)"
         >
@@ -77,7 +77,7 @@
       </el-form-item>
 
       <el-form-item
-        label="土壤有机质(g/kg)"
+        :label="$t('Calculation.soilOrganicMatter')"
         prop="OM"
         :error="errorMessages.OM"
         required
@@ -85,7 +85,7 @@
         <el-input
           v-model="form.OM"
           size="large"
-          placeholder="请输入土壤有机质 0~35 (g/kg)"
+          :placeholder="$t('Calculation.neutralization.soilOrganicMatterPlaceholder')"
           ref="inputRefs.OM"
           @input="handleInput('OM', $event, 0, 35)"
         >
@@ -93,7 +93,7 @@
       </el-form-item>
 
       <el-form-item
-        label="硝酸盐(mg/kg)"
+        :label="$t('Calculation.nitrate')"
         prop="NO3"
         :error="errorMessages.NO3"
         required
@@ -101,7 +101,7 @@
         <el-input
           v-model="form.NO3"
           size="large"
-          placeholder="请输入硝酸盐 10~70 (mg/kg)"
+          :placeholder="$t('Calculation.neutralization.nitratePlaceholder')"
           ref="inputRefs.NO3"
           @input="handleInput('NO3', $event, 10, 70)"
         >
@@ -109,7 +109,7 @@
       </el-form-item>
 
       <el-form-item
-        label="铵盐(mg/kg)"
+        :label="$t('Calculation.ammoniumSalt')"
         prop="NH4"
         :error="errorMessages.NH4"
         required
@@ -117,7 +117,7 @@
         <el-input
           v-model="form.NH4"
           size="large"
-          placeholder="请输入铵盐 0~20 (mg/kg)"
+          :placeholder="$t('Calculation.neutralization.ammoniumSaltPlaceholder')"
           ref="inputRefs.NH4"
           @input="handleInput('NH4', $event, 0, 20)"
         >
@@ -125,7 +125,7 @@
       </el-form-item>
 
       <el-form-item
-        label="阳离子交换量(cmol/kg)"
+        :label="$t('Calculation.cationExchangeCapacity')"
         prop="CEC"
         :error="errorMessages.CEC"
         required
@@ -133,7 +133,7 @@
         <el-input
           v-model="form.CEC"
           size="large"
-          placeholder="请输入阳离子交换量 0~15 (cmol/kg)"
+          :placeholder="$t('Calculation.neutralization.cationExchangeCapacityPlaceholder')"
           ref="inputRefs.CEC"
           @input="handleInput('CEC', $event, 0, 15)"
         >
@@ -141,7 +141,7 @@
       </el-form-item>
 
       <el-button type="primary" @click="onSubmit" class="onSubmit"
-        >计算</el-button
+        >{{ $t('Calculation.calculateButton') }}</el-button
       >
 
       <el-dialog
@@ -151,13 +151,13 @@
         width="500px"
         align-center
         class="dialog-class"
-        title="计算结果"
+        :title="$t('Calculation.resultTitle')"
       >
         <span class="dialog-class"
-          >每亩地土壤表层(20cm)撒 {{ result }}吨生石灰</span
+          >{{ $t('Calculation.resultUnit') }} {{ result }}{{ $t('Calculation.quicklime') }}</span
         ><br />
         <template #footer>
-          <el-button @click="dialogVisible = false">关闭</el-button>
+          <el-button @click="dialogVisible = false">{{ $t('Calculation.closeButton') }}</el-button>
         </template>
       </el-dialog>
     </el-form>
@@ -168,7 +168,10 @@
 import { reactive, ref, nextTick, onMounted } from "vue";
 import { ElMessage } from 'element-plus';
 import axios from 'axios';
-import { api5000 } from "../../../utils/request"; // 使用api5000
+import { api5000 } from "../../../utils/request";
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 // 根据文档修改表单接口
 const form = reactive<{
@@ -205,7 +208,7 @@ const errorMessages = reactive({
   CEC: '',
 });
 
-// 当前选中模型 - 根据文档修改为33
+// 当前选中模型 - 根据文档修改为 33
 const selectedModelId = ref<number>(33);
 const selectedModelName = ref<string>("降酸预测模型");
 
@@ -246,14 +249,14 @@ const handleInput = (field: keyof typeof form, event: Event, min: number, max: n
     const numValue = parseFloat(filteredValue);
     if (isNaN(numValue) || numValue < min || numValue > max) {
       form[field] = null;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
+      errorMessages[field] = t('Calculation.validationRange', { min, max });
     } else {
       form[field] = numValue;
       errorMessages[field] = '';
     }
   } catch (error) {
     console.error(`处理输入时出错:`, error);
-    errorMessages[field] = '输入处理出错,请检查输入';
+    errorMessages[field] = t('Calculation.validationError');
   }
 };
 
@@ -309,20 +312,20 @@ const onSubmit = async () => {
     
     if (value === null) {
       isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
+      errorMessages[field] = t('Calculation.validationRange', { min, max });
       return;
     }
 
     if (!validateInput(value, min, max)) {
       isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
+      errorMessages[field] = t('Calculation.validationRange', { min, max });
     } else {
       errorMessages[field] = '';
     }
   });
 
   if (!isValid) {
-    ElMessage.error('输入值不符合要求,请检查输入');
+    ElMessage.error(t('Calculation.validationError'));
     return;
   }
 
@@ -341,7 +344,7 @@ const onSubmit = async () => {
       CEC: form.CEC,
     }
   };
-  console.log('提交的数据:', data);
+  // console.log('提交的数据:', data);
   try {
     const response = await api5000.post('/predict', data, {
       headers: {
@@ -353,21 +356,21 @@ const onSubmit = async () => {
       result.value = parseFloat((response.data.result / 10).toFixed(2));
     } else {
       console.error('未获取到有效的预测结果');
-      ElMessage.error('未获取到有效的预测结果');
+      ElMessage.error(t('Calculation.invalidResult'));
     }
     dialogVisible.value = true;
   } catch (error: unknown) {
     console.error('请求失败:', error);
     if (axios.isAxiosError(error)) {
       if (error.response) {
-        ElMessage.error(`请求失败,状态码: ${error.response.status}`);
+        ElMessage.error(`${t('Calculation.requestFailed')}${error.response.status}`);
       } else if (error.request) {
-        ElMessage.error('请求发送成功,但没有收到响应');
+        ElMessage.error(t('Calculation.noResponse'));
       } else {
-        ElMessage.error('请求过程中发生错误: ' + error.message);
+        ElMessage.error(`${t('Calculation.requestError')}${(error as Error).message}`);
       }
     } else {
-      ElMessage.error('请求过程中发生错误: ' + (error as Error).message);
+      ElMessage.error(`${t('Calculation.requestError')}${(error as Error).message}`);
     }
   }
 };

+ 15 - 12
src/views/User/neutralizationModel/ModelIterationVisualization.vue

@@ -3,6 +3,9 @@ import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
 import VueEcharts from 'vue-echarts';
 import 'echarts';
 import { api5000 } from '../../../utils/request';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 interface HistoryDataItem {
   dataset_id: number;
@@ -83,9 +86,9 @@ const selectedModelType = ref('rf'); // 默认选择随机森林
 
 // 模型类型选项
 const modelTypeOptions = [
-  { label: '随机森林', value: 'rf' },
-  { label: 'XGBoost', value: 'xgbr' },
-  { label: '梯度提升', value: 'gbst' },
+  { label: t('ModelIteration.randomForest'), value: 'rf' },
+  { label: t('ModelIteration.xgboost'), value: 'xgbr' },
+  { label: t('ModelIteration.gradientBoosting'), value: 'gbst' },
 ];
 
 
@@ -138,7 +141,7 @@ const fetchLineData = async () => {
         containLabel: true
       },
       xAxis: {
-        name: '模型迭代',
+        name: t('ModelIteration.modelIteration'),
         type: 'category',
         boundaryGap: false,
         data: timestamps.map((_, index) => `${index + 1}代`)
@@ -315,28 +318,28 @@ onUnmounted(() => {
   <div class="container">
     <template v-if="showInitScatterChart">
       <!-- 散点图 -->
-      <h2 class="chart-header">散点图</h2>
+      <h2 class="chart-header">{{ $t('ModelIteration.scatterPlotTitle') }}</h2>
       <div class="chart-container">
         <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
       </div>
     </template>
 
-   <h2 class="chart-header">模型性能分析</h2>
+   <h2 class="chart-header">{{ $t('ModelIteration.modelPerformanceAnalysis') }}</h2>
     <!-- 模型性能分析部分 -->
     <div class="analysis-section">
        <!-- 数据增长曲线模块 -->
       <div class="chart-module">
-        <h2 class="chart-header">数据增长曲线</h2>
+        <h2 class="chart-header">{{ $t('ModelIteration.dataIncreaseCurve') }}</h2>
         <div class="image-chart-item">
           <div class="image-container">
-            <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" alt="数据增长曲线" >
-            <div v-else class="image-placeholder">加载中...</div>
+            <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" :alt="$t('ModelIteration.dataIncreaseCurve')" >
+            <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
           </div>
         </div>
       </div>
       <!-- 学习曲线模块 -->
       <div class="chart-module">
-        <h2 class="chart-header">学习曲线</h2>
+        <h2 class="chart-header">{{ $t('ModelIteration.learningCurve') }}</h2>
         <div class="model-selector-wrapper">
           <select v-model="selectedModelType" class="model-select">
             <option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
@@ -346,8 +349,8 @@ onUnmounted(() => {
         </div>
         <div class="image-chart-item">
           <div class="image-container">
-            <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" alt="学习曲线" >
-            <div v-else class="image-placeholder">加载中...</div>
+            <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" :alt="$t('ModelIteration.learningCurve')" >
+            <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
           </div>
         </div>
       </div>

+ 18 - 19
src/views/login/loginView.vue

@@ -17,10 +17,10 @@
           <h2 class="form-title">{{ t("login.userTitle") }}</h2>
         </div>
 
-        <el-form-item label="账号" prop="name">
+        <el-form-item :label="t('Header.username')" prop="name">
           <el-input
             v-model="form.name"
-            placeholder="请输入用户名"
+            :placeholder="t('validation.usernameRequired')"
             autocomplete="username"
           >
             <template #prefix>
@@ -29,12 +29,12 @@
           </el-input>
         </el-form-item>
 
-        <el-form-item label="密码" prop="password">
+        <el-form-item :label="t('Header.logout').replace('退出', '').trim()" prop="password">
           <el-input
             v-model="form.password"
             type="password"
             show-password
-            placeholder="请输入密码"
+            :placeholder="t('validation.passwordRequired')"
             autocomplete="current-password"
           >
             <template #prefix>
@@ -65,9 +65,9 @@
         </el-form-item>
 
         <div class="switch-form">
-          还没有账户
+          {{ t("login.registerLink").split('?')[0] }}
           <span class="link" @click="toggleForm">{{
-            t("login.registerLink")
+            t("login.registerLink").split('?')[1] || t("login.registerLink")
           }}</span>
         </div>
       </el-form>
@@ -84,10 +84,10 @@
           <h2 class="form-title">{{ t("register.title") }}</h2>
         </div>
 
-        <el-form-item label="账号" prop="name" class="form-item-full">
+        <el-form-item :label="t('Header.username')" prop="name" class="form-item-full">
           <el-input
             v-model="registerForm.name"
-            placeholder="建议使用邮箱或手机号"
+            :placeholder="t('validation.usernameRequired')"
           >
             <template #prefix>
               <i class="icon-user"></i>
@@ -95,12 +95,12 @@
           </el-input>
         </el-form-item>
 
-        <el-form-item label="密码" prop="password" class="form-item-full">
+        <el-form-item :label="t('Header.logout').replace('退出', '').trim()" prop="password" class="form-item-full">
           <el-input
             v-model="registerForm.password"
             type="password"
             show-password
-            placeholder="长度 3-16 位"
+            :placeholder="t('validation.passwordLength')"
           >
             <template #prefix>
               <i class="icon-lock"></i>
@@ -109,7 +109,7 @@
         </el-form-item>
 
         <el-form-item
-          label="确认密码"
+          :label="t('validation.confirmPasswordRequired').replace('请', '')"
           prop="confirmPassword"
           class="form-item-full"
         >
@@ -117,7 +117,7 @@
             v-model="registerForm.confirmPassword"
             type="password"
             show-password
-            placeholder="请再次输入密码"
+            :placeholder="t('validation.confirmPasswordRequired')"
           >
             <template #prefix>
               <i class="icon-check"></i>
@@ -147,7 +147,7 @@
         </el-form-item>
 
         <div class="switch-form">
-          已有账户?
+          {{ t("register.backToLoginButton").includes('登录') ? '已有账户?' : 'Have an account?' }}
           <span class="link" @click="toggleForm">{{
             t("register.backToLoginButton")
           }}</span>
@@ -257,12 +257,11 @@ const onSubmit = async () => {
     });
 
     ElMessage.success(t("login.loginSuccess"));
-    // 替换原来的 router.push 行
-const targetRoute = userData.userType === 'admin' 
-  ? 'soilAcidReductionData' 
-  : 'SoilPro';
+    const targetRoute = userData.userType === 'admin' 
+      ? 'soilAcidReductionData' 
+      : 'SoilPro';
 
-await router.push({ name: targetRoute });
+    await router.push({ name: targetRoute });
   } catch (error: any) {
     const errorMsg =
       error.response?.data?.message || error.message || t("login.loginFailed");
@@ -647,4 +646,4 @@ $text-light: #e6dccd;
     transform: translateY(0);
   }
 }
-</style>
+</style>

+ 3 - 2
vite.config.ts

@@ -11,10 +11,10 @@ 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 VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
 import path from 'path'
 
-const  __dirname = path.dirname(fileURLToPath(import.meta.url));
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 // https://vite.dev/config/
 export default defineConfig({
@@ -51,6 +51,7 @@ export default defineConfig({
         },
       },
     },
+    sourcemap: true,
   },
   resolve: {
     alias: {