Quellcode durchsuchen

中英文切换大界面1.0

yes-yes-yes-k vor 3 Monaten
Ursprung
Commit
c7e3312287
37 geänderte Dateien mit 980 neuen und 7991 gelöschten Zeilen
  1. 0 559
      src/components/atmpollution/airSampleTencentMap.vue
  2. 0 483
      src/components/atmpollution/airsampleChart.vue
  3. 0 457
      src/components/atmpollution/airsampleLine.vue
  4. 0 479
      src/components/atmpollution/atmCompanytencentMap.vue
  5. 0 252
      src/components/atmpollution/atmcompanyline.vue
  6. 0 252
      src/components/atmpollution/atmcompanymap.vue
  7. 0 378
      src/components/atmpollution/atmsamplemap.vue
  8. 0 391
      src/components/atmpollution/heavyMetalEnterprisechart.vue
  9. 13 12
      src/components/detectionStatistics/atmcompanyStatics.vue
  10. 22 18
      src/components/detectionStatistics/atmsampleStatistics.vue
  11. 14 14
      src/components/detectionStatistics/crosscetionStatistics.vue
  12. 22 18
      src/components/detectionStatistics/irrigationstatistics.vue
  13. 0 211
      src/components/irrpollution/crossSectionSamplelineData.vue
  14. 0 262
      src/components/irrpollution/crossSetionData1.vue
  15. 0 264
      src/components/irrpollution/crossSetionData2.vue
  16. 0 416
      src/components/irrpollution/crossSetionTencentmap.vue
  17. 0 265
      src/components/irrpollution/crosssectionmap.vue
  18. 0 280
      src/components/irrpollution/irrwatermap.vue
  19. 0 87
      src/components/irrpollution/riverwaterassay.vue
  20. 0 1015
      src/components/irrpollution/tencentMapView.vue
  21. 0 222
      src/components/irrpollution/waterassaydata1.vue
  22. 0 348
      src/components/irrpollution/waterassaydata2.vue
  23. 0 197
      src/components/irrpollution/waterassaydata3.vue
  24. 0 340
      src/components/irrpollution/waterassaydata4.vue
  25. 0 313
      src/components/irrpollution/waterdataline.vue
  26. 46 44
      src/components/soilcdStatistics/cropcdStatictics.vue
  27. 50 45
      src/components/soilcdStatistics/effcdStatistics.vue
  28. 29 28
      src/components/soilcdStatistics/fluxcdStatictics.vue
  29. 219 5
      src/locales/en.json
  30. 223 11
      src/locales/zh.json
  31. 28 25
      src/views/User/acidModel/Calculation.vue
  32. 15 12
      src/views/User/acidModel/ModelIterationVisualization.vue
  33. 86 83
      src/views/User/acidModel/nanxiong_acidmodelmap.vue
  34. 32 32
      src/views/User/acidModel/pHPrediction.vue
  35. 129 127
      src/views/User/acidModel/shaoguan_acidmodelmap.vue
  36. 37 34
      src/views/User/neutralizationModel/AcidNeutralizationModel.vue
  37. 15 12
      src/views/User/neutralizationModel/ModelIterationVisualization.vue

+ 0 - 559
src/components/atmpollution/airSampleTencentMap.vue

@@ -1,559 +0,0 @@
-<template>
-  <div class="map-page">
-    <div ref="mapContainer" class="map-container"></div>
-    <!-- 错误提示 -->
-    <div v-if="error" class="error-message">{{ error }}</div>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
-import axios from 'axios'
-const isMapReady = ref(false)
-const mapContainer = ref(null)
-const error = ref(null)
-const TMap = ref(null);
-let activeTempMarker = ref(null)
-let infoWindow = ref(null)
-let map = null
-let markersLayer = null
-let soilTypeVectorLayer = null; // 土壤类型多边形图层
-let overlay = null
-const state = reactive({
-  showOverlay: false,
-  showSoilTypes: true,
-  showSurveyData: true,
-  shoeWaterSystem: true,
-  excelData: [], // 用于存储从接口获取的数据
-  lastTapTime: 0
-})
-
-const tMapConfig = reactive({
-  key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
-  geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
-})
-
-// 加载SDK的代码保持不变...
-const loadSDK = () => {
-  return new Promise((resolve, reject) => {
-    if (window.TMap?.service?.Geocoder) {
-      console.log('SDK已缓存,直接使用');
-      TMap.value = window.TMap
-      return resolve(window.TMap)
-    }
-
-    const script = document.createElement('script')
-    script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`
-    window.initTMap = () => {
-      if (!window.TMap?.service?.Geocoder) {
-        console.error('SDK加载后仍无效');
-        reject(new Error('地图SDK加载失败'))
-        return
-      }
-      console.log('SDK动态加载完毕');
-      TMap.value = window.TMap
-      resolve(window.TMap)
-    }
-
-    script.onerror = (err) => {
-      console.error('SDK加载报错', err);
-      reject(`地图资源加载失败: ${err.message}`)
-      document.head.removeChild(script)
-    }
-
-    document.head.appendChild(script)
-  })
-}
-
-// 初始化地图 - 保持大部分不变,增加数据加载
-const initMap = async () => {
-  try {
-    await loadSDK()
-    console.log('开始创建地图实例');
-    
-    map = new TMap.value.Map(mapContainer.value, {
-      center: new TMap.value.LatLng(24.9, 113.9),//前大往下,后大往左
-      zoom: 10,
-      minZoom: 9.25,
-      maxZoom: 11,
-      renderOptions: {
-        antialias: true
-      },
-    })
-    console.log('地图实例创建成功');
-    
-    // 创建标记点向量图层
-    markersLayer = new TMap.value.MultiMarker({
-      map: map,
-      zIndex: 1000,
-      collision:false,
-      styles: {
-        default: new TMap.value.MarkerStyle({
-          width: 15, // 图标宽度
-          height: 15, // 图标高度
-          anchor: { x: 12.5, y: 12.5 }, // 居中定位
-          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCI+PGNpcmNsZSBjeD0iMTUiIGN5PSIxNSIgcj0iMTAiIGZpbGw9InJlZCIvPjwvc3ZnPg=='
-        })
-      }
-    });
-    
-    // 绑定标记点击事件
-    markersLayer.on('click', handleMarkerClick);
-    
-    // 创建土壤类型多边形图层
-    soilTypeVectorLayer = new TMap.value.MultiPolygon({
-      map: map,
-      styles: {
-        default: new TMap.value.PolygonStyle({
-          fillColor: '#cccccc',
-          fillOpacity: 0.4,
-          strokeColor: '#333',
-          strokeWidth: 1
-        })
-      }    
-    });  
-   
-    // 先加载数据,再更新标记
-    await fetchData(); // 新增:获取数据
-    if(state.excelData.length > 0) {
-    const first = state.excelData[0];
-    console.log('第一条数据坐标:', 
-      first.纬度 || first.latitude, 
-      first.经度 || first.longitude
-    );
-  }
-    updateMarkers();   // 更新标记点
-    
-    // 标记地图就绪
-    isMapReady.value = true;
-    console.log('地图初始化完成');
-
-  } catch (err) {
-    isMapReady.value = true;
-    console.error('initMap执行异常:', err);
-    error.value = err.message
-  }
-}
-
-// 新增:从接口获取数据
-const fetchData = async () => {
-  try {
-    const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data', {
-      timeout: 100000
-    });
-
-    state.excelData = response.data.filter(item => {
-      // 检查数据完整性
-      if(!item['样品编码'] || !item.纬度 || !item.经度) {
-        console.warn(`数据不完整,已跳过: ${item.样品编码 || '未知编码'}`);
-        return false;
-      }
-      
-      const lat = Number(item.纬度);
-      const lng = Number(item.经度);
-      
-      // 验证数值范围
-      const isValid = !isNaN(lat) && !isNaN(lng) && 
-                     lat >= -90 && lat <= 90 && 
-                     lng >= -180 && lng <= 180;
-      
-      if(!isValid) {
-        console.error(`无效经纬度: ${item.样品编码} (${item.纬度}, ${item.经度})`);
-      }
-      return isValid;
-    });
-    
-    console.log('有效数据记录:', state.excelData.length);
-  } catch (err) {
-    console.error('数据请求失败详情:', err.response?.data || err.message);
-    error.value = `数据加载失败: ${err.message}`;
-    
-  }
-}
-
-// 更新标记点 - 保持不变
-const updateMarkers = () => {
-  const coordCount = new Map();
-  const geometries = state.excelData.map(item => {
-    console.log(`ID: ${item.样品编码}, 坐标: (${item.纬度}, ${item.经度})`); // 替换字段名
-    if (!item.样品编码 || !item.纬度 || !item.经度) {
-      console.error(`无效数据项: ${JSON.stringify(item)}`);
-      return null;
-    }
-    const lat = Number(item.纬度);
-    const lng = Number(item.经度);
-
-    if (isNaN(lat) || isNaN(lng)) {
-      console.error(`坐标值非数字: ${item.样品编码} (${item.纬度}, ${item.经度})`);
-      return null;
-    }
-
-    const coordKey = `${lat}_${lng}`;
-    const count = coordCount.get(coordKey) || 0;
-    coordCount.set(coordKey, count + 1);
-
-    let finalLat = lat;
-    let finalLng = lng;
-    
-    // 重复坐标添加偏移
-    if (count > 0) {
-      const latOffset = count * 0.01;  // 南北方向偏移(约11米)
-      const lngOffset = count * 0.02;
-      finalLat = lat + latOffset;
-      finalLng = lng + lngOffset;
-      
-      console.log(`偏移点 ${item.样品编码}: ${lat},${lng} → ${finalLat},${finalLng}`);
-    }
-
-    const position = new TMap.value.LatLng(finalLat, finalLng);
-
-    return {
-      id: item.样品名称,
-      styleId: 'default',
-      position:position, // 替换字段名
-      properties: {
-        title: item.采样 || `采样点 ${item.样品名称}`, 
-        sampler_id: item.样品编码,
-        originalPosition: { lat, lng }
-      }
-    };
-  })
-  
-  markersLayer.setGeometries(geometries);
-  console.log('成功添加标记点数量:', geometries.length);
-};
-
-// Marker点击事件处理 - 保持不变
-const handleMarkerClick = async (e) => {
-  console.log('点击标记点');
-  
-  const marker = e.geometry;
-  if (!marker) {
-    console.error('未获取到标记点对象');
-    return;
-  }
-
-  // 关闭之前的信息窗口
-  if (infoWindow.value) {
-    infoWindow.value.close();
-    infoWindow.value = null;
-  }
-  
-  // 显示加载中
-  infoWindow.value = new TMap.value.InfoWindow({
-    map: map,
-    position: marker.position,
-    content: '<div style="padding:12px;text-align:center">加载数据中...</div>',
-    //offset: { x: 0, y: -32 }
-  });
-  infoWindow.value.open();
-
-  try {
-    const markerId = marker.id.trim();
-    console.log('点击标记点样品名称:', markerId);
-    
-    // 直接从本地数据查找,无需二次请求
-    const matchedData = state.excelData.find(item => 
-      item.样品名称.trim() === markerId
-    );
-
-    if (!matchedData) {
-      console.error("无法匹配的数据列表:", state.excelData.map(i => i.样品名称));
-      throw new Error(`未找到样品名称为 ${markerId} 的监测数据`);
-    }
-
-    // 创建信息窗口内容
-    const content = `
-      <div class="water-info-window">
-        <h3 class="info-title">${matchedData.采样}</h3>
-        <div class="info-row">
-          <span class="info-label">采样点ID:</span>
-          <span class="info-value">${matchedData.样品名称}</span>
-        </div>
-
-        <div class="info-row">
-          <span class="info-label">样品编号:</span>
-          <span class="info-value">${matchedData.样品编号}</span>
-        </div>
-  
-        <div class="contaminant-grid" style="grid-template-columns: repeat(2, 1fr); gap: 8px;">
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">Cr mg/kg:</span>
-            <span class="contaminant-value">${matchedData['Cr mg/kg']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">Cr ug/m3:</span>
-            <span class="contaminant-value">${matchedData['Cr ug/m3']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">As mg/kg:</span>
-            <span class="contaminant-value">${matchedData['As mg/kg']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">As ug/m3:</span>
-            <span class="contaminant-value">${matchedData['As ug/m3']}</span>
-          </div>
-          
-          <div class="contaminant-item">
-            <span class="contaminant-name">Cd mg/kg:</span>
-            <span class="contaminant-value">${matchedData['Cd mg/kg']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">Cd ug/m3:</span>
-            <span class="contaminant-value">${matchedData['Cd ug/m3']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">Hg mg/kg:</span>
-            <span class="contaminant-value">${matchedData['Hg mg/kg']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">Hg ug/m3:</span>
-            <span class="contaminant-value">${matchedData['Hg ug/m3']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">Pb mg/kg:</span>
-            <span class="contaminant-value">${matchedData['Pb mg/kg']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">Pb ug/m3:</span>
-            <span class="contaminant-value">${matchedData['Pb ug/m3']}</span>
-          </div>
-          
-          <div class="contaminant-item">
-            <span class="contaminant-name">颗粒物的重量 mg:</span>
-            <span class="contaminant-value">${matchedData['颗粒物的重量 mg']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">标准体积 m3:</span>
-            <span class="contaminant-value">${matchedData['标准体积 m3']}</span>
-          </div>
-
-          <div class="contaminant-item">
-            <span class="contaminant-name">颗粒物浓度ug/m3:</span>
-            <span class="contaminant-value">${matchedData['颗粒物浓度ug/m3']}</span>
-          </div>
-
-        </div>
-      </div>
-    `;
-    
-    // 更新信息窗口
-    infoWindow.value.setContent(content);
-    
-  } catch (error) {
-    console.error('API请求失败:', error);
-    
-    // 显示错误信息
-    const errorContent = `
-      <div style="padding:12px;color:red">
-        <h3>${marker.properties.title}</h3>
-        <p>获取数据失败: ${error.message}</p>
-      </div>
-    `;
-    
-    infoWindow.value.setContent(errorContent);
-  }
-}
-
-// 其余函数保持不变...
-const manageTempMarker = {
-  add: (lat, lng, phValue) => {
-    if (activeTempMarker.value) {
-      markersLayer.remove("-999")
-    }
-    
-    // 确保已添加临时样式
-    if (!markersLayer.getStyles().temp) {
-      markersLayer.setStyles({
-        temp: new TMap.value.MarkerStyle({
-          width: 30,
-          height: 30,
-          anchor: { x: 12.5, y: 12.5 },
-          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMkg2Yy0xLjEgMC0yIC45LTIgMnYxNmMwIDEuMS45IDIgMiAyaDEyYzEuMSAwIDIgLS45IDItMnYtNGMwLTEuMS0uOS0yLTItMmgtMnY0aC00di00SDEyVjJ6bTAgMTZINnYtOEgxOFYxOHoiIGZpbGw9IiNGRjAwMDAiLz48L3N2Zz4='
-        })
-      });
-    }
-    
-    const tempMarker = markersLayer.add({
-      id: "-999",
-      position: new TMap.value.LatLng(lat, lng),
-      styleId: 'temp',
-      properties: {
-        title: '克里金插值',
-        phValue: parseFloat(phValue).toFixed(2),
-        isTemp: true
-      }
-    })
-    activeTempMarker.value = tempMarker
-  },
-  remove: () => {
-    if (activeTempMarker.value) {
-      markersLayer.remove("-999")
-      activeTempMarker.value = null
-    }
-  }
-}
-
-onMounted(async () => {
-  console.log('开始执行 onMounted');
-  
-  try {
-    await initMap()
-    console.log('地图初始化完成');
-  } catch (err) {
-    console.error('onMounted执行异常', err);
-    error.value = err.message
-  }
-})
-
-onBeforeUnmount(() => {
-  if (activeTempMarker.value) {
-    manageTempMarker.remove()
-  }
-  if (markersLayer) markersLayer.setMap(null)
-  if (overlay) overlay.setMap(null)
-  if (infoWindow.value) {
-    infoWindow.value.close()
-    infoWindow.value = null
-  }
-  if (soilTypeVectorLayer) soilTypeVectorLayer.setMap(null)
-})
-</script>
-
-<style>
-/* 原有样式保持不变,修改以下部分 */
-.error-message {
-  position: fixed;
-  top: 20px;
-  left: 50%;
-  transform: translateX(-50%);
-  padding: 12px 20px;
-  background-color: #ff4444;
-  color: white;
-  border-radius: 4px;
-  z-index: 9999;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
-  animation: fadein 0.5s, fadeout 0.5s 4.5s;
-}
-
-@keyframes fadein {
-  from { top: 0; opacity: 0; }
-  to { top: 20px; opacity: 1; }
-}
-
-@keyframes fadeout {
-  from { top: 20px; opacity: 1; }
-  to { top: 0; opacity: 0; }
-}
-
-.map-page {
-  position: relative;
-  width: 100vw;
-  height: 100vh;
-}
-
-.map-container {
-  width: 100%;
-  height: 100vh ;
-  min-height: 600px;
-  pointer-events: all;
-}
-
-.contaminants {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 2px;
-}
-
-/* 窗口容器:精准控制尺寸 */
-.water-info-window {
-  max-width: 340px !important;
-  width: 100%;
-  height: auto;
-  padding: 8px;
-  box-sizing: border-box;
-  background: #FFFFFF;
-  border-radius: 8px;
-  box-shadow: 0 3px 12px rgba(0, 32, 71, 0.1);
-  border: 1px solid #e5e7eb;
-  overflow: hidden !important;
-}
-
-/* 标题区样式 */
-.info-title {
-  font-size: 0.9rem;
-  padding: 6px 8px;
-  letter-spacing: 0.2px;
-  border-bottom: 1px solid #f1f2f6;
-  margin: 0 0 8px 0;
-}
-
-/* 基础数据行 */
-.info-row {
-  display: flex;
-  align-items: center;
-  margin: 4px 0;
-  padding-left: 8px;
-}
-
-.info-label {
-  font-size: 0.8rem;
-  padding-right: 6px;
-  flex: 0 0 80px;
-}
-
-.info-value {
-  font-size: 0.8rem;
-  padding: 2px 6px;
-  border-left: 2px solid #e5e7eb;
-}
-
-/* 污染物网格:优化布局使名称和数值在同一行并放大字体 */
-.contaminant-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 4px; /* 减小间距以补偿字体增大 */
-  margin: 4px 0;
-}
-
-/* 污染物项:确保名称和数值在同一行 */
-.contaminant-item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 3px 5px; /* 减小内边距 */
-  background: #f9fafb;
-  border-radius: 4px;
-}
-
-/* 增大字体大小,保持在同一行 */
-.contaminant-name {
-  font-size: 0.8rem; /* 增大字体 */
-  color: #6b7280;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  flex: 1;
-  margin-right: 5px;
-}
-
-.contaminant-value {
-  font-size: 0.8rem; /* 增大字体 */
-  background: #e5e7eb;
-  padding: 2px 6px;
-  border-radius: 3px;
-  min-width: 40px;
-  text-align: center;
-  flex-shrink: 0;
-}
-</style>

+ 0 - 483
src/components/atmpollution/airsampleChart.vue

@@ -1,483 +0,0 @@
-<template>
-  <div class="atmosphere-summary">
-    <!-- 图表容器 -->
-    <div ref="chartRef" class="chart-box"></div>
-    
-    <!-- 状态提示 -->
-    <div v-if="loading" class="status">
-      <div class="spinner"></div>
-      <p>数据加载中...</p>
-    </div>
-    
-    <div v-else-if="error" class="status error">
-      <i class="fa fa-exclamation-circle"></i> {{ error }}
-      <div v-if="errorDetails" class="error-details">
-        <p>错误详情:</p>
-        <pre>{{ errorDetails }}</pre>
-      </div>
-    </div>
-
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
-import * as echarts from 'echarts'
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// 接收计算方式(重量/体积)
-const props = defineProps({
-  calculationMethod: {
-    type: String,
-    required: true,
-    default: 'weight'
-  }
-})
-
-// --------------------------
-// 配置区
-// --------------------------
-const API_URL = `/api/vector/export/all?table_name=Atmo_sample_data`; // 使用相对路径
-
-// 重量指标字段
-const WEIGHT_FIELDS = [
-  'Cr_particulate',
-  'As_particulate',
-  'Cd_particulate',
-  'Hg_particulate',
-  'Pb_particulate'
-];
-
-// 体积字段名
-const VOLUME_FIELD = 'volume'; 
-
-// 自定义颜色(用户指定)
-const COLORS = ['#ff4d4f99', '#1890ff', '#ffd700', '#52c41a88', '#722ed199'];
-// --------------------------
-
-// 响应式数据
-const chartRef = ref(null);
-const loading = ref(true);
-const error = ref('');
-const errorDetails = ref(''); // 存储错误详情
-const showLog = ref(false); // 默认隐藏日志
-const fullLog = ref('');
-let myChart = null;
-
-// 记录日志
-const log = (message) => {
-  const time = new Date().toLocaleTimeString();
-  fullLog.value += `[${time}] ${message}\n`;
-  console.log(`[日志] ${message}`);
-};
-
-
-const fixInvalidJsonValues = (rawData) => {
-  if (typeof rawData !== 'string') {
-    rawData = JSON.stringify(rawData);
-  }
-  
-  const fixedData = rawData
-    .replace(/:\s*NaN\b/g, ': null')
-    .replace(/:\s*"N"\b/g, ': null')
-    .replace(/:\s*"NaN"\b/g, ': null')
-    .replace(/:\s*Infinity\b/g, ': null')
-    .replace(/:\s*-\s*Infinity\b/g, ': null')
-    .replace(/:\s+/g, ': ')
-    .replace(/,\s+/g, ', ');
-  
-  return fixedData;
-};
-
-
-function weightToVolume(weight, volume) {
-  if (weight === undefined || weight === null) {
-    log(`重量值无效: ${weight}`);
-    return 0;
-  }
-  if (volume === undefined || volume === null || volume === 0 || isNaN(volume)) {
-    log(`体积值无效: ${volume}(已自动替换为1)`);
-    volume = 1;
-  }
-  
-  const weightNum = parseFloat(weight);
-  const volumeNum = parseFloat(volume);
-  
-  if (isNaN(weightNum)) {
-    log(`重量无法转换为数字: ${weight}`);
-    return 0;
-  }
-  
-  const ug = weightNum * 1000;
-  return parseFloat((ug / volumeNum).toFixed(2));
-}
-
-function getRegion(location) {
-  if (!location || typeof location !== 'string') {
-    return '未知区县';
-  }
-
-  const regions = [
-    '浈江区', '武江区', '曲江区', '乐昌市', 
-    '南雄市', '始兴县', '仁化县', '翁源县', 
-    '新丰县', '乳源瑶族自治县'
-  ];
-
-  // 精确匹配
-  for (const region of regions) {
-    if (location.includes(region)) {
-      return region;
-    }
-  }
-
-  // 模糊匹配
-  const aliasMap = {
-    '浈江': '浈江区', '武江': '武江区', '曲江': '曲江区',
-    '乐昌': '乐昌市', '南雄': '南雄市', '始兴': '始兴县',
-    '仁化': '仁化县', '翁源': '翁源县', '新丰': '新丰县',
-    '乳源': '乳源瑶族自治县'
-  };
-  
-  for (const [alias, region] of Object.entries(aliasMap)) {
-    if (location.includes(alias)) {
-      return region;
-    }
-  }
-
-  return '未知区县';
-}
-
-
-async function processData() {
-  try {
-    log('开始数据处理');
-    
-    // 使用 api8000 实例发起请求
-    const response = await api8000.get(API_URL, {
-      responseType: 'text', // 确保获取原始文本
-      timeout: 15000
-    });
-    
-    const fixedJson = fixInvalidJsonValues(response.data);
-    const geoData = JSON.parse(fixedJson);
-
-    if (!geoData || !geoData.features || !Array.isArray(geoData.features)) {
-      throw new Error('数据结构错误,缺少features数组');
-    }
-    log(`解析到${geoData.features.length}条数据`);
-
-    // 处理数据
-    const processedItems = geoData.features.map((feature, index) => {
-      const props = feature.properties || {};
-      return {
-        id: index,
-        location: props.sampling_location || '',
-        region: getRegion(props.sampling_location || ''),
-        volume: props[VOLUME_FIELD],
-        weights: WEIGHT_FIELDS.reduce((acc, field) => {
-          acc[field] = props[field];
-          return acc;
-        }, {})
-      };
-    });
-
-    // 统计数据
-    const regionStats = {};
-    const totalStats = {};
-    WEIGHT_FIELDS.forEach(field => {
-      totalStats[field] = { sum: 0, count: 0 };
-      totalStats[`${field}_volume`] = { sum: 0, count: 0 };
-    });
-
-    processedItems.forEach(item => {
-      const { region, volume, weights } = item;
-      if (!regionStats[region]) {
-        regionStats[region] = {};
-        WEIGHT_FIELDS.forEach(field => {
-          regionStats[region][field] = { sum: 0, count: 0 };
-          regionStats[region][`${field}_volume`] = { sum: 0, count: 0 };
-        });
-      }
-
-      WEIGHT_FIELDS.forEach(field => {
-        const weightValue = weights[field];
-        const weightNum = parseFloat(weightValue);
-
-        if (!isNaN(weightNum)) {
-          regionStats[region][field].sum += weightNum;
-          regionStats[region][field].count += 1;
-          totalStats[field].sum += weightNum;
-          totalStats[field].count += 1;
-        }
-
-        const volumeValue = weightToVolume(weightValue, volume);
-        regionStats[region][`${field}_volume`].sum += volumeValue;
-        regionStats[region][`${field}_volume`].count += 1;
-        totalStats[`${field}_volume`].sum += volumeValue;
-        totalStats[`${field}_volume`].count += 1;
-      });
-    });
-
-    // 准备图表数据
-    const chartRegions = Object.keys(regionStats).filter(r => r !== '未知区县');
-    if (chartRegions.length === 0) chartRegions.push('未知区县');
-    chartRegions.push('全市平均');
-
-    // 生成系列数据
-    const series = WEIGHT_FIELDS.map((field, index) => {
-      const metricType = props.calculationMethod === 'volume' 
-        ? `${field}_volume` 
-        : field;
-      
-      const data = chartRegions.map(region => {
-        if (region === '全市平均') {
-          return totalStats[metricType].count > 0 
-            ? (totalStats[metricType].sum / totalStats[metricType].count).toFixed(2)
-            : '0.00';
-        }
-        
-        const stats = regionStats[region][metricType];
-        return stats.count > 0 
-          ? (stats.sum / stats.count).toFixed(2)
-          : '0.00';
-      });
-
-      return {
-        name: field.replace('_particulate', ''), // 图例名称(不带后缀)
-        type: 'bar',
-        data,
-        itemStyle: { 
-          color: COLORS[index % COLORS.length] // 使用用户指定的颜色
-        },
-        label: {
-          show: true,
-          position: 'top',
-          fontSize: 12
-        }
-      };
-    });
-
-    return { regions: chartRegions, series };
-
-  } catch (err) {
-    error.value = '数据处理失败';
-    errorDetails.value = err.message;
-    return null;
-  }
-}
-
-// /​**​
-//  * 初始化图表(带单位显示)
-//  */
-async function initChart() {
-  loading.value = true;
-  error.value = '';
-  errorDetails.value = '';
-  
-  try {
-    await nextTick();
-    if (!chartRef.value) {
-      throw new Error('图表容器未挂载');
-    }
-
-    const chartData = await processData();
-    if (!chartData) return;
-
-    // 确定单位(核心修改:添加单位逻辑)
-    const { unit, titleText } = props.calculationMethod === 'weight' 
-      ? { 
-          unit: 'mg/kg', 
-          titleText: '各区域重金属含量平均值' ,
-        } 
-      : { 
-          unit: 'ug/m³',  // 体积单位为ug/m³,可根据实际需求修改
-          titleText: '各区域重金属含量平均值' ,
-        };
-
-    // 销毁旧图表
-    if (myChart) myChart.dispose();
-    myChart = echarts.init(chartRef.value);
-
-    // 设置图表配置(带单位显示)
-    myChart.setOption({
-      title: { 
-        text: titleText,
-        subtext: `单位: ${unit}`, // 标题显示单位
-        left: 'center',
-        textStyle:{fontSize:20},
-        subtextStyle:{fontSize:18}
-      },
-      tooltip: { //提示框
-        trigger: 'axis',
-        formatter: function(params) {
-          // Tooltip显示单位
-          let res = `${params[0].name}<br/>`;
-          params.forEach(item => {
-            res += `${item.marker} ${item.seriesName}: ${item.value} ${unit}<br/>`;
-          });
-          return res;
-        },
-        textStyle:{fontSize:15}
-      },
-      xAxis: {
-        type: 'category',
-        data: chartData.regions,
-        axisLabel: { fontSize:16 }
-      },
-      yAxis: { 
-        type: 'value',
-        axisLabel: {
-          formatter: `{value} ${unit}` // Y轴显示单位
-          ,fontSize:15
-        }
-      },
-      series: chartData.series.map(series => ({  // 遍历每个系列,添加 label 配置
-       ...series,  // 保留原有配置
-       label: {
-         show: true,  // 显示数值标签
-         position: 'top',  // 标签位置(顶部)
-         fontSize: 15,  // 这里才是柱状图数值的字体大小!
-         color: '#333'  // 可选:设置文字颜色
-       }
-      })),
-      legend: { //图例
-        data: chartData.series.map(s => s.name),
-        top:'10%',
-        right:'5%',
-        textStyle:{fontSize:18}
-      },
-      grid: {
-        left: '1%', right: '2%', bottom: '2%', top: '20%',
-        containLabel: true,
-        axisLabel:{fontSize:18}
-      }
-    }, true);
-
-    // 监听窗口大小
-    const handleResize = () => myChart.resize();
-    window.addEventListener('resize', handleResize);
-    onUnmounted(() => window.removeEventListener('resize', handleResize));
-
-  } catch (err) {
-    error.value = '图表加载失败';
-    errorDetails.value = err.message;
-  } finally {
-    loading.value = false;
-  }
-}
-
-// 监听计算方式变化
-watch(() => props.calculationMethod, initChart);
-
-// 组件挂载后初始化
-onMounted(() => {
-  initChart();
-});
-
-// 组件卸载时清理
-onUnmounted(() => {
-  if (myChart) myChart.dispose();
-});
-</script>
-
-<style scoped>
-.atmosphere-summary {
-  width: 100%;
-  max-width: 1400px;
-  margin: 0 auto;
-  box-sizing: border-box;
-  position: relative;
-}
-
-.chart-box {
-  width: 100%;
-  height: 500px;
-  min-height: 400px;
-  background: #fff;
-  border: 1px solid #e9ecef;
-  border-radius: 8px;
-}
-
-.status {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  padding: 20px;
-  background: rgba(255, 255, 255, 0.9);
-  border-radius: 6px;
-  text-align: center;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-  max-width: 80%;
-}
-
-.error {
-  color: #dc3545;
-  border: 1px solid #f5c6cb;
-  background: #f8d7da;
-}
-
-.error-details {
-  margin-top: 15px;
-  text-align: left;
-  font-size: 14px;
-}
-
-.error-details pre {
-  background: rgba(255, 255, 255, 0.8);
-  padding: 10px;
-  border-radius: 4px;
-  overflow: auto;
-  max-height: 200px;
-  white-space: pre-wrap;
-}
-
-.spinner {
-  width: 40px;
-  height: 40px;
-  margin: 0 auto 15px;
-  border: 4px solid #e9ecef;
-  border-top: 4px solid #007bff;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-.debug-panel {
-  margin-top: 20px;
-  padding: 15px;
-  background: #f8f9fa;
-  border-radius: 6px;
-  font-size: 14px;
-}
-
-.log-toggle {
-  background: #007bff;
-  color: white;
-  border: none;
-  padding: 6px 12px;
-  border-radius: 4px;
-  cursor: pointer;
-  margin-bottom: 10px;
-}
-
-.log-content {
-  max-height: 300px;
-  overflow: auto;
-  background: #fff;
-  padding: 10px;
-  border-radius: 4px;
-  border: 1px solid #e9ecef;
-}
-
-.log-content pre {
-  margin: 0;
-  white-space: pre-wrap;
-  font-family: monospace;
-  font-size: 12px;
-}
-</style>

+ 0 - 457
src/components/atmpollution/airsampleLine.vue

@@ -1,457 +0,0 @@
-<template>
-  <div class="container mx-auto px-4 py-8">
-    <!-- 错误提示(带原始响应预览) -->
-    <div v-if="error" class="status error mb-4">
-      <i class="fa fa-exclamation-circle"></i> {{ error }}
-      <div class="raw-response" v-if="rawResponse">
-        <button @click="showRaw = !showRaw" class="mt-2">
-          {{ showRaw ? '收起原始响应' : '查看原始响应(前1000字符)' }}
-        </button>
-        <pre v-if="showRaw" class="mt-2 bg-gray-50 p-2 text-sm">{{ truncatedRawResponse }}</pre>
-      </div>
-    </div>
-
-    <div class="bg-white rounded-xl shadow-lg overflow-hidden">
-      <!-- 加载状态 -->
-      <div v-if="loading" class="py-20 flex justify-center items-center">
-        <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
-      </div>
-      
-      <!-- 数据展示区:表格 + 地图 + 柱状图 -->
-      <div v-else-if="filteredData.length > 0" class="flex flex-col md:flex-row">
-        <!-- 表格 -->
-        <div class="w-full md:w-1/2 overflow-x-auto">
-          <table class="min-w-full divide-y divide-gray-200">
-            <thead class="bg-white">
-              <tr>
-                <th 
-                  v-for="(col, index) in displayColumns" 
-                  :key="index"
-                  :style="{ width: col.width }"  
-                  class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
-                  @click="sortData(col.label)"
-                >
-                  <div class="flex items-center justify-between">
-                    {{ col.label }}
-                    <span v-if="sortKey === col.label" class="ml-1 text-gray-400">
-                      {{ sortOrder === 'asc' ? '↑' : '↓' }}
-                    </span>
-                  </div>
-                </th>
-              </tr>
-            </thead>
-            <tbody class="bg-white divide-y divide-gray-200">
-              <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex" 
-                  class="hover:bg-white transition-colors duration-150">
-                <td 
-                  v-for="(col, colIndex) in displayColumns" 
-                  :key="colIndex"
-                  :style="{ width: col.width }"  
-                  class="px-6 py-4 whitespace-nowrap text-sm"
-                >
-                  <div class="flex items-center">
-                    <div class="text-gray-900 font-medium">
-                      {{ formatValue(item, col) }}
-                    </div>
-                  </div>
-                </td>
-              </tr>
-            </tbody>
-          </table>
-        </div>
-
-        <!-- 地图 + 柱状图(右侧区域) -->
-        <div class="w-full md:w-1/2 p-4 flex flex-col">
-          <!-- 地图(依赖:@vue-leaflet/vue-leaflet) -->
-          <div class="h-64 mb-4">
-            <LMap 
-              :center="mapCenter" 
-              :zoom="12" 
-              style="width: 100%; height: 100%"
-            >
-              <LTileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
-              <LMarker 
-                v-for="(item, idx) in filteredData" 
-                :key="idx" 
-                :lat-lng="[item.latitude, item.longitude]"
-              >
-                <LPopup>{{ item.sampling_location }}</LPopup>
-              </LMarker>
-            </LMap>
-          </div>
-
-          <!-- 柱状图(依赖:echarts) -->
-          <div class="h-64">
-            <div ref="chart" class="w-full h-full"></div>
-          </div>
-        </div>
-      </div>
-      
-      <!-- 空数据状态 -->
-      <div v-else class="p-8 text-center">
-        <div class="flex flex-col items-center justify-center">
-          <div class="text-gray-400 mb-4">
-            <i class="fa fa-database text-5xl"></i>
-          </div>
-          <h3 class="text-lg font-medium text-gray-900 mb-1">暂无有效数据</h3>
-          <p class="text-gray-500">已过滤全空行</p>
-        </div>
-      </div>
-  
-      <!-- 数据统计 -->
-      <div class="p-4 bg-gray-50 border-t border-gray-200">
-        <div class="flex flex-col md:flex-row justify-between items-center">
-          <div class="text-sm text-gray-500 mb-2 md:mb-0">
-            共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
-import { LMap, LTileLayer, LMarker, LPopup } from '@vue-leaflet/vue-leaflet'; // 地图组件
-import * as echarts from 'echarts'; // 柱状图
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// ========== 接口配置 ==========
-const apiUrl = '/api/vector/export/all?table_name=Atmo_sample_data'; // 使用相对路径
-
-// ========== 响应式数据 ==========
-const props = defineProps({
-  calculationMethod: {
-    type: String,
-    required: true,
-    default: 'weight'
-  }
-});
-
-const waterData = ref([]);
-const loading = ref(true);
-const error = ref('');
-const rawResponse = ref('');  
-const showRaw = ref(false);   
-const sortKey = ref('');      
-const sortOrder = ref('asc'); 
-const mapCenter = ref([23.5, 116.5]); // 地图初始中心(根据实际数据调整)
-const chartInstance = ref(null); // ECharts实例
-const chart = ref(null); // 图表容器引用
-
-// 换算函数(重量→体积)
-function calculateConcentration(heavyMetalWeight, volume) {
-  if (heavyMetalWeight === undefined || volume === undefined || isNaN(heavyMetalWeight) || isNaN(volume) || volume === 0) {
-    return '未知';
-  }
-  const ug = heavyMetalWeight * 1000; 
-  const concentration = ug / volume;
-  return concentration.toFixed(2); // 保留2位小数
-}
-
-function calculateParticleConcentration(particleWeight, volume) {
-  if (particleWeight === undefined || volume === undefined || isNaN(particleWeight) || isNaN(volume) || volume === 0) {
-    return '未知';
-  }
-  const ug = particleWeight * 1000; 
-  const concentration = ug / volume;
-  return concentration.toFixed(2); // 保留2位小数
-}
-
-// 列配置(补充 width,根据内容合理分配宽度)
-const commonColumns = [
-  { key: 'sampling_location', label: '采样位置', type: 'string', width: '180px' },
-  { key: 'sample_name', label: '样品名称', type: 'string', width: '70px' },
-  { key: 'latitude', label: '纬度', type: 'number', width: '60px' }, // 地图依赖字段
-  { key: 'longitude', label: '经度', type: 'number', width: '60px' },// 地图依赖字段
-];
-
-const weightColumns = [
-  { key: 'Cr_particulate', label: 'Cr mg/kg', type: 'number', width: '80px' },
-  { key: 'As_particulate', label: 'As mg/kg', type: 'number', width: '80px' },
-  { key: 'Cd_particulate', label: 'Cd mg/kg', type: 'number', width: '80px' },
-  { key: 'Hg_particulate', label: 'Hg mg/kg', type: 'number', width: '85px' },
-  { key: 'Pb_particulate', label: 'Pb mg/kg', type: 'number', width: '80px' },
-  { key: 'particle_weight', label: '颗粒物重量 mg', type: 'number', width: '140px' },
-];
-
-const volumeColumns = [
-  { key: 'standard_volume', label: '标准体积 m³', type: 'number', width: '120px' },
-  { label: 'Cr ug/m³', getValue: (item) => calculateConcentration(item.Cr_particulate, item.standard_volume), type: 'number', width: '80px' },
-  { label: 'As ug/m³', getValue: (item) => calculateConcentration(item.As_particulate, item.standard_volume), type: 'number', width: '80px' },
-  { label: 'Cd ug/m³', getValue: (item) => calculateConcentration(item.Cd_particulate, item.standard_volume), type: 'number', width: '80px' },
-  { label: 'Hg ug/m³', getValue: (item) => calculateConcentration(item.Hg_particulate, item.standard_volume), type: 'number', width: '80px' },
-  { label: 'Pb ug/m³', getValue: (item) => calculateConcentration(item.Pb_particulate, item.standard_volume), type: 'number', width: '80px' },
-  { label: '颗粒物浓度 ug/m³', getValue: (item) => calculateParticleConcentration(item.particle_weight, item.standard_volume), type: 'number', width: '140px' },
-];
-
-// 动态生成显示列
-const displayColumns = computed(() => {
-  return props.calculationMethod === 'volume' 
-    ? [...commonColumns, ...volumeColumns] 
-    : [...commonColumns, ...weightColumns];
-});
-
-// 数值格式化
-const formatValue = (item, col) => {
-  if (col.getValue) {
-    const val = col.getValue(item);
-    return val === '未知' ? '-' : val;
-  } else {
-    const value = item[col.key];
-    if (value === null || value === undefined || value === '') return '-';
-    if (col.type === 'number') {
-      const num = parseFloat(value);
-      return isNaN(num) ? '-' : num.toFixed(2); // 统一保留2位小数
-    } else {
-      return value;
-    }
-  }
-};
-
-// 过滤全空行(允许0值)
-const filteredData = computed(() => {
-  return waterData.value.filter(item => {
-    return displayColumns.value.some(col => {
-      let val = col.getValue ? col.getValue(item) : item[col.key];
-      if (col.type === 'string') {
-        return val !== null && val !== '' && val !== '-';
-      } else {
-        const num = parseFloat(val);
-        return !isNaN(num); // 允许0值,仅排除非数字
-      }
-    });
-  });
-});
-
-// 排序功能
-const sortedData = computed(() => {
-  if (!sortKey.value) return filteredData.value;
-  
-  const sortCol = displayColumns.value.find(col => col.label === sortKey.value);
-  if (!sortCol) return filteredData.value;
-  
-  return [...filteredData.value].sort((a, b) => {
-    let valA = sortCol.getValue ? sortCol.getValue(a) : a[sortCol.key];
-    let valB = sortCol.getValue ? sortCol.getValue(b) : b[sortCol.key];
-    
-    if (sortCol.type === 'string') {
-      const strA = valA.toString().trim();
-      const strB = valB.toString().trim();
-      return sortOrder.value === 'asc' 
-        ? strA.localeCompare(strB) 
-        : strB.localeCompare(strA);
-    }
-    
-    const numA = parseFloat(valA) || -Infinity;
-    const numB = parseFloat(valB) || -Infinity;
-    if (numA < numB) return sortOrder.value === 'asc' ? -1 : 1;
-    if (numA > numB) return sortOrder.value === 'asc' ? 1 : -1;
-    return 0;
-  });
-});
-
-// 切换排序
-const sortData = (label) => {
-  const targetCol = displayColumns.value.find(col => col.label === label);
-  if (!targetCol) return;
-  
-  if (sortKey.value === label) {
-    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
-  } else {
-    sortKey.value = label;
-    sortOrder.value = 'asc';
-  }
-};
-
-// 截断原始响应(前1000字符)
-const truncatedRawResponse = computed(() => {
-  return rawResponse.value.length > 1000 
-    ? rawResponse.value.slice(0, 1000) + '...' 
-    : rawResponse.value;
-});
-
-// 柱状图数据(动态生成)
-const chartData = computed(() => {
-  const xData = filteredData.value.map(item => item.sample_name);
-  const yData = filteredData.value.map(item => {
-    if (props.calculationMethod === 'volume') {
-      const val = item['Cr ug/m³'];
-      return val !== '-' ? parseFloat(val) : 0;
-    } else {
-      const val = item.Cr_particulate;
-      return val !== '-' ? parseFloat(val) : 0;
-    }
-  });
-
-  return {
-    xAxis: { type: 'category', data: xData },
-    yAxis: { type: 'value' },
-    series: [{ name: 'Cr 浓度', type: 'bar', data: yData }],
-  };
-});
-
-// 初始化柱状图
-const initChart = () => {
-  nextTick(() => {
-    if (chartInstance.value) chartInstance.value.dispose();
-    if (chart.value) {
-      chartInstance.value = echarts.init(chart.value);
-      chartInstance.value.setOption(chartData.value);
-    }
-  });
-};
-
-// 监听数据变化,更新图表
-watch(filteredData, () => {
-  if (chartInstance.value) {
-    chartInstance.value.setOption(chartData.value);
-  }
-});
-
-// 数据请求 & 修复逻辑(含超时控制)
-const fetchData = async () => {
-  try {
-    loading.value = true;
-    error.value = '';
-    rawResponse.value = '';
-
-    // 使用 api8000 实例发起请求
-    const response = await api8000.get(apiUrl, {
-      responseType: 'text', // 确保获取原始文本
-      timeout: 5000 // 5秒超时
-    });
-    
-    rawResponse.value = response.data;
-
-    // 修复 JSON 语法(替换 NaN/Infinity)
-    const fixedText = response.data
-      .replace(/:\s*NaN/g, ': null')
-      .replace(/:\s*Infinity/g, ': null');
-
-    let data;
-    try {
-      data = JSON.parse(fixedText);
-    } catch (parseErr) {
-      error.value = `数据解析失败:${parseErr.message}\n原始响应:${response.data.slice(0, 200)}...`;
-      console.error(parseErr, response.data);
-      loading.value = false;
-      return;
-    }
-
-    // 兼容接口格式
-    let features = [];
-    if (data.type === 'FeatureCollection' && Array.isArray(data.features)) {
-      features = data.features;
-    } else if (Array.isArray(data)) {
-      features = data;
-    } else {
-      throw new Error('接口格式异常,需为 FeatureCollection 或数组');
-    }
-
-    // 提取数据(含经纬度)
-    waterData.value = features.map(feature => 
-      feature.properties ? feature.properties : feature
-    );
-
-    // 更新地图中心(取第一个点的经纬度,无数据则保持默认)
-    if (waterData.value.length > 0) {
-      mapCenter.value = [
-        waterData.value[0].latitude, 
-        waterData.value[0].longitude
-      ];
-    }
-
-    console.log('✅ 数据加载完成,记录数:', waterData.value.length);
-    initChart(); // 初始化图表
-
-  } catch (err) {
-    if (err.code === 'ECONNABORTED') {
-      error.value = '请求超时!请检查网络或接口响应速度';
-    } else {
-      error.value = `数据加载失败:${err.message}`;
-    }
-    console.error(err);
-  } finally {
-    loading.value = false;
-  }
-};
-
-// 组件挂载时加载数据
-onMounted(() => {
-  fetchData();
-});
-
-// 卸载时销毁图表
-onUnmounted(() => {
-  if (chartInstance.value) {
-    chartInstance.value.dispose();
-  }
-});
-</script>
-
-<style scoped>
-/* 错误提示样式 */
-.status.error {
-  color: #dc2626;
-  background: #fee2e2;
-  padding: 12px 16px;
-  border-radius: 6px;
-}
-.status.error button {
-  cursor: pointer;
-  background: #ff4d4f;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  padding: 4px 8px;
-}
-.raw-response pre {
-  white-space: pre-wrap;
-  word-break: break-all;
-  background: #f9fafb;
-  padding: 8px;
-  border-radius: 4px;
-  font-size: 12px;
-}
-
-/* 表格核心样式:固定布局 + 列宽生效 */
-table {
-  border-collapse: collapse;
-  table-layout: fixed; 
-  width: 100%; 
-  background-color: white;        
-}
-
-th, td {
-  border: 1px solid #d1d5db;
-  text-align: center;
-  padding: 10px 6px;   
-  min-width: 60px;     
-  white-space: normal; 
-  overflow: hidden;    
-  text-overflow: ellipsis; 
-  background-color: white;
-}
-
-/* 地图 & 图表容器 */
-.leaflet-container {
-  width: 100%;
-  height: 100%;
-}
-.echarts-container {
-  width: 100%;
-  height: 100%;
-}
-
-/* 响应式优化 */
-@media (max-width: 768px) {
-  th, td {
-    padding: 8px 4px;
-    font-size: 14px;
-  }
-  .flex-col md:flex-row {
-    flex-direction: column;
-  }
-}
-</style>

+ 0 - 479
src/components/atmpollution/atmCompanytencentMap.vue

@@ -1,479 +0,0 @@
-<template>
-  <div class="map-page">
-    <div ref="mapContainer" class="map-container"></div>
-    <!-- 错误提示 -->
-    <div v-if="error" class="error-message">{{ error }}</div>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
-import axios from 'axios'
-const isMapReady = ref(false)
-const mapContainer = ref(null)
-const error = ref(null)
-const TMap = ref(null);
-let activeTempMarker = ref(null)
-let infoWindow = ref(null)
-let map = null
-let markersLayer = null
-let soilTypeVectorLayer = null; // 土壤类型多边形图层
-let overlay = null
-const state = reactive({
-  showOverlay: false,
-  showSoilTypes: true,
-  showSurveyData: true,
-  shoeWaterSystem: true,
-  excelData: [], // 用于存储从接口获取的数据
-  lastTapTime: 0
-})
-let soilTypeLayer = null
-let currentInfoWindow = null;
-let surveyDataLayer = ref(null);
-let selectedPolygon = ref(null); // 补充定义
-
-const tMapConfig = reactive({
-  key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
-  geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
-})
-
-// 加载SDK的代码保持不变...
-const loadSDK = () => {
-  return new Promise((resolve, reject) => {
-    if (window.TMap?.service?.Geocoder) {
-      //console.log('SDK已缓存,直接使用');
-      TMap.value = window.TMap
-      return resolve(window.TMap)
-    }
-
-    const script = document.createElement('script')
-    script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`
-    window.initTMap = () => {
-      if (!window.TMap?.service?.Geocoder) {
-        console.error('SDK加载后仍无效');
-        reject(new Error('地图SDK加载失败'))
-        return
-      }
-      //console.log('SDK动态加载完毕');
-      TMap.value = window.TMap
-      resolve(window.TMap)
-    }
-
-    script.onerror = (err) => {
-      console.error('SDK加载报错', err);
-      reject(`地图资源加载失败: ${err.message}`)
-      document.head.removeChild(script)
-    }
-
-    document.head.appendChild(script)
-  })
-}
-
-// 初始化地图 - 保持大部分不变,增加数据加载
-const initMap = async () => {
-  try {
-    await loadSDK()
-    //console.log('开始创建地图实例');
-    
-    map = new TMap.value.Map(mapContainer.value, {
-      center: new TMap.value.LatLng(24.39, 114),//前大往下,后大往左
-      zoom: 9.25,
-      minZoom: 8,
-      //maxZoom: 11,
-      renderOptions: {
-        antialias: true
-      },
-    })
-    //console.log('地图实例创建成功');
-    
-    // 创建标记点向量图层
-    markersLayer = new TMap.value.MultiMarker({
-      map: map,
-      zIndex: 1000,
-      collision:false,
-      styles: {
-        default: new TMap.value.MarkerStyle({
-          width: 30, // 图标宽度
-          height: 30, // 图标高度
-          anchor: { x: 12.5, y: 12.5 }, // 居中定位
-          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cG9seWdvbiBwb2ludHM9IjEyLDIgMiwyMCAyMiwyMCIgZmlsbD0iIzFFODhFNSIvPjwvc3ZnPg=='
-        })
-      }
-    });
-    
-    // 绑定标记点击事件
-    markersLayer.on('click', handleMarkerClick);
-    
-    // 创建土壤类型多边形图层
-    soilTypeVectorLayer = new TMap.value.MultiPolygon({
-      map: map,
-      styles: {
-        default: new TMap.value.PolygonStyle({
-          fillColor: '#cccccc',
-          fillOpacity: 0.4,
-          strokeColor: '#333',
-          strokeWidth: 1
-        })
-      }    
-    });  
-   
-    // 先加载数据,再更新标记
-    await fetchData(); // 新增:获取数据
-    if(state.excelData.length > 0) {
-    const first = state.excelData[0];
-   // console.log('第一条数据坐标:', 
-     // first.纬度 || first.latitude, 
-      //first.经度 || first.longitude
-    //);
-  }
-    updateMarkers();   // 更新标记点
-    
-    // 标记地图就绪
-    isMapReady.value = true;
-    //console.log('地图初始化完成');
-
-    // 创建样式标签并注入样式
-    const style = document.createElement('style');
-    style.textContent = `
-      .water-info-window {
-        max-width: 80vw !important;
-        width: auto !important;
-        overflow: visible !important;
-      }
-      
-      .info-value {
-        white-space: normal !important;
-        word-wrap: break-word !important;
-        max-width: none !important;
-      }
-    `;
-    document.head.appendChild(style);
-
-  } catch (err) {
-    isMapReady.value = true;
-    console.error('initMap执行异常:', err);
-    error.value = err.message
-  }
-}
-
-// 新增:从接口获取数据
-const fetchData = async () => {
-  try {
-    const response = await axios.get('http://localhost:3000/table/Atmosphere_company_data', {
-      timeout: 100000
-    });
-
-    state.excelData = response.data.filter(item => {
-      if(!item['污染源序号'] || !item.纬度 || !item.经度) { // 替换为新字段
-        console.warn(`数据不完整,已跳过: ${item.污染源序号 || '未知序号'}`);
-        return false;
-      }  
-      const lat = Number(item.纬度);
-      const lng = Number(item.经度);  
-      const isValid = !isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;  
-      if(!isValid) console.error(`无效经纬度: ${item.污染源序号} (${item.纬度}, ${item.经度})`);  
-      return isValid;
-    });
-    
-    //console.log('有效数据记录:', state.excelData.length);
-  } catch (err) {
-    console.error('数据请求失败详情:', err.response?.data || err.message);
-    error.value = `数据加载失败: ${err.message}`;
-    
-  }
-}
-
-// 更新标记点 - 保持不变
-const updateMarkers = () => {
-  const geometries = state.excelData.map(item => {  
-  //console.log(`ID: ${item.污染源序号}, 坐标: (${item.纬度}, ${item.经度})`); // 替换为新字段  
-  if (!item.污染源序号 || !item.纬度 || !item.经度) return null;  
-  const lat = Number(item.纬度);
-  const lng = Number(item.经度);  
-  if (isNaN(lat) || isNaN(lng)) return null;  
-
-  return {
-    id: item.污染源序号, // 标记 ID 设为「污染源序号」
-    styleId: 'default',
-    position: new TMap.value.LatLng(lat, lng), 
-    properties: {
-      title: item.公司 || `污染源 ${item.污染源序号}`, // 标题用「公司」名称
-      sampler_id: item.污染源序号,
-    }
-  };
-  });
-  
-  markersLayer.setGeometries(geometries);
-  //console.log('成功添加标记点数量:', geometries.length);
-};
-
-// Marker点击事件处理 - 保持不变
-const handleMarkerClick = async (e) => {
-  //console.log('点击标记点');
-  
-  const marker = e.geometry;
-  if (!marker) {
-    console.error('未获取到标记点对象');
-    return;
-  }
-
-  // 关闭之前的信息窗口
-  if (infoWindow.value) {
-    infoWindow.value.close();
-    infoWindow.value = null;
-  }
-  
-  // 显示加载中
-  infoWindow.value = new TMap.value.InfoWindow({
-    map: map,
-    position: marker.position,
-    content: '<div style="padding:12px;text-align:center">加载数据中...</div>',
-    //offset: { x: 0, y: -220 },
-    
-  });
-  infoWindow.value.open();
-
-  try {
-    const markerId = marker.id.trim();
-    //console.log('点击标记点样品名称:', markerId);
-    
-    // 直接从本地数据查找,无需二次请求
-    const matchedData = state.excelData.find(item => 
-      item.污染源序号.trim() === markerId
-    );
-
-    if (!matchedData) {
-      console.error("无法匹配的数据列表:", state.excelData.map(i => i.样品名称));
-      throw new Error(`未找到样品名称为 ${markerId} 的监测数据`);
-    }
-
-    // 创建信息窗口内容
-    const content = `
-  <div class="water-info-window">
-    <h3 class="info-title">${matchedData.公司}</h3>
-
-    <div class="info-row">
-      <span class="info-label">污染源序号:</span>
-      <span class="info-value">${matchedData.污染源序号}</span>
-    </div>
-
-    <div class="info-row">
-      <span class="info-label">所属区县:</span>
-      <span class="info-value">${matchedData.所属区县}</span>
-    </div>
-
-    <div class="info-row">
-      <span class="info-label">类型:</span>
-      <span class="info-value">${matchedData.类型}</span>
-    </div>
-
-    <div class="info-row">
-      <span class="info-label">大气颗粒物排放(t/a):</span>
-      <span class="info-value">${matchedData['大气颗粒物排放(t/a)']}</span>
-    </div>
-
-  </div>
-`;
-    
-    // 更新信息窗口
-    infoWindow.value.setContent(content);
-    
-  } catch (error) {
-    console.error('API请求失败:', error);
-    
-    // 显示错误信息
-    const errorContent = `
-      <div style="padding:12px;color:red">
-        <h3>${marker.properties.title}</h3>
-        <p>获取数据失败: ${error.message}</p>
-      </div>
-    `;
-    
-    infoWindow.value.setContent(errorContent);
-  }
-}
-
-// 其余函数保持不变...
-const manageTempMarker = {
-  add: (lat, lng, phValue) => {
-    if (activeTempMarker.value) {
-      markersLayer.remove("-999")
-    }
-    
-    // 确保已添加临时样式
-    if (!markersLayer.getStyles().temp) {
-      markersLayer.setStyles({
-        temp: new TMap.value.MarkerStyle({
-          width: 30,
-          height: 30,
-          anchor: { x: 12.5, y: 12.5 },
-          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cG9seWdvbiBwb2ludHM9IjEyLDIgMiwyMCAyMiwyMCIgZmlsbD0iIzFFODhFNSIvPjwvc3ZnPg=='
-        })
-      });
-    }
-    
-    const tempMarker = markersLayer.add({
-      id: "-999",
-      position: new TMap.value.LatLng(lat, lng),
-      styleId: 'temp',
-      properties: {
-        title: '克里金插值',
-        phValue: parseFloat(phValue).toFixed(2),
-        isTemp: true
-      }
-    })
-    activeTempMarker.value = tempMarker
-  },
-  remove: () => {
-    if (activeTempMarker.value) {
-      markersLayer.remove("-999")
-      activeTempMarker.value = null
-    }
-  }
-}
-
-onMounted(async () => {
-  //console.log('开始执行 onMounted');
-  
-  try {
-    await initMap()
-    //console.log('地图初始化完成');
-  } catch (err) {
-    console.error('onMounted执行异常', err);
-    error.value = err.message
-  }
-})
-
-onBeforeUnmount(() => {
-  if (activeTempMarker.value) {
-    manageTempMarker.remove()
-  }
-  if (markersLayer) markersLayer.setMap(null)
-  if (overlay) overlay.setMap(null)
-  if (infoWindow.value) {
-    infoWindow.value.close()
-    infoWindow.value = null
-  }
-  if (soilTypeVectorLayer) soilTypeVectorLayer.setMap(null)
-})
-</script>
-
-<style>
-/* 原有样式保持不变,新增错误提示样式 */
-.error-message {
-  position: fixed;
-  top: 20px;
-  left: 50%;
-  transform: translateX(-50%);
-  padding: 12px 20px;
-  background-color: #ff4444;
-  color: white;
-  border-radius: 4px;
-  z-index: 9999;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
-  animation: fadein 0.5s, fadeout 0.5s 4.5s;
-}
-
-@keyframes fadein {
-  from { top: 0; opacity: 0; }
-  to { top: 20px; opacity: 1; }
-}
-
-@keyframes fadeout {
-  from { top: 20px; opacity: 1; }
-  to { top: 0; opacity: 0; }
-}
-
-/* 其余样式保持不变 */
-.map-page {
-  position: relative;
-  width: 100vw;
-  height: 100vh;
-}
-
-.map-container {
-  width: 100%;
-  height: 100vh ;
-  min-height: 600px;
-  pointer-events: all;
-}
-
-
-
-.water-info-window {
-  max-width: 80vw; /* 相对视口最大值,更加灵活 */
-  width: auto;
-  padding: 16px;
-  border-radius: 12px;
-  box-shadow: 0 6px 18px rgba(0,0,0,0.15);
-  background: #fff;
-  border: 1px solid #e5e7eb;
-  overflow: visible; /* 配合自动平移,确保内容不被裁剪 */
-}
-
-/* 标题区:增加渐变装饰线 */
-.info-title {
-  font-size: 18px;
-  font-weight: 600;
-  color: #1e3a8a;
-  text-align: center;
-  margin-bottom: 14px;
-  padding-bottom: 12px;
-  border-bottom: 1px solid #e5e7eb;
-  position: relative; /* 为伪元素准备 */
-}
-.info-title::after {
-  content: "";
-  position: absolute;
-  bottom: 0;
-  left: 20px;
-  right: 20px;
-  height: 1px;
-  background: linear-gradient(to right, 
-    rgba(30, 58, 138, 0.1), 
-    rgba(30, 58, 138, 0.2), 
-    rgba(30, 58, 138, 0.1)
-  );
-}
-
-/* 数据行:Grid 布局确保对齐 */
-.info-row {
-  display: grid;
-  grid-template-columns: 180px 1fr; /* 标签180px,值自适应 */
-  gap: 12px; /* 列间距 */
-  align-items: flex-start;
-  margin: 10px 0;
-  margin: 10px 0;/**增加行间距,提升可读性 */
-}
-
-/* 标签:右对齐 + 深灰配色 */
-.info-label {
-  text-align: right;
-  color: #6b7280;
-  font-size: 14px;
-}
-
-/* 数据值:浅灰背景 + 智能换行 */
-.info-value {
-  padding: 6px 10px;
-  background: #f3f4f6;
-  border-radius: 6px;
-  font-size: 14px;
-  white-space: normal;   /* 长文本强制换行 */
-  word-wrap: break-word;
-  overflow: visible;/**不隐藏溢出内容 */
-  text-overflow: clip;
-  min-height: 24px;        /* 确保最小高度,避免内容塌陷 */
-}
-
-/* 响应式适配(小窗口自动压缩) */
-@media (max-width: 480px) {
-  .water-info-window {
-    max-width: 90vw; /* 占满视口宽度 */
-    padding: 10px;
-  }
-  .info-row {
-    grid-template-columns: 100px 1fr; /* 缩小标签宽度 */
-  }
-}
-</style>

+ 0 - 252
src/components/atmpollution/atmcompanyline.vue

@@ -1,252 +0,0 @@
-<template>
-  <div class="region-average-chart">
-    <!-- 错误提示 -->
-    <div v-if="error" class="status error">
-      <i class="fa fa-exclamation-circle"></i> {{ error }}
-      <div class="raw-response" v-if="rawResponse">
-        <button @click="showRaw = !showRaw">
-          {{ showRaw ? '收起原始响应' : '查看原始响应(前1000字符)' }}
-        </button>
-        <pre v-if="showRaw">{{ truncatedRawResponse }}</pre>
-      </div>
-    </div>
-
-    <!-- 加载状态 -->
-    <div v-if="loading" class="loading-state">
-      <div class="spinner"></div>
-      <p>数据加载中...</p>
-    </div>
-
-    <!-- 数据表格 -->
-    <div v-else class="table-container">
-      <table class="data-table">
-        <thead>
-          <tr>
-            <th>污染源序号</th>
-            <th>公司名称</th>
-            <th>公司类型</th>
-            <th>所属区县</th>
-            <th>颗粒物排放(t/a)</th>
-            <th>经度</th>
-            <th>纬度</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="item in tableData" :key="item.id">
-            <td>{{ item.id }}</td>
-            <td>{{ item.company_name }}</td>
-            <td>{{ item.company_type }}</td>
-            <td>{{ item.county }}</td>
-            <td>{{ item.particulate_emission }}</td>
-            <td>{{ item.longitude.toFixed(6) }}</td>
-            <td>{{ item.latitude.toFixed(6) }}</td>
-          </tr>
-          <tr v-if="tableData.length === 0">
-            <td colspan="7" class="empty-state">暂无有效数据</td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, computed } from 'vue';
-import { wgs84togcj02 } from 'coordtransform';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// ========== 接口配置 ==========
-const apiUrl = '/api/vector/export/all?table_name=atmo_company'; // 使用相对路径
-
-// ========== 响应式数据 ==========
-const error = ref('');        // 错误信息
-const loading = ref(true);    // 加载状态
-const tableData = ref([]);    // 表格数据
-const rawResponse = ref('');  // 原始响应文本(调试用)
-const showRaw = ref(false);   // 是否展开原始响应
-
-// 截断原始响应(前1000字符)
-const truncatedRawResponse = computed(() => {
-  return rawResponse.value.length > 1000 
-    ? rawResponse.value.slice(0, 1000) + '...' 
-    : rawResponse.value;
-});
-
-// ========== 数据请求 & 修复逻辑 ==========
-const fetchData = async () => {
-  try {
-    loading.value = true;
-    error.value = '';
-    rawResponse.value = '';
-
-    // 1. 使用 api8000 实例获取原始文本
-    const response = await api8000.get(apiUrl, {
-      responseType: 'text' // 确保获取原始文本
-    });
-    
-    // 2. 获取原始响应文本
-    let rawText = response.data;
-    rawResponse.value = rawText; // 保存原始响应
-
-    // 3. 暴力替换 NaN 为 null(核心修复!)
-    rawText = rawText.replace(/:\s*NaN/g, ': null'); 
-
-    // 4. 解析 JSON(若失败,报错含修复后内容)
-    const geoJSONData = JSON.parse(rawText);
-
-    // 5. 校验 GeoJSON 结构(兜底)
-    if (!geoJSONData.features || !Array.isArray(geoJSONData.features)) {
-      throw new Error('修复后仍无有效 features 数组');
-    }
-
-    // 6. 处理数据(转换经纬度 + 字段兜底)
-    tableData.value = geoJSONData.features
-      .map(feature => feature.properties || {}) // 兜底空 properties
-      .map(props => {
-        // 经纬度转换(按需调整,若不需要可移除)
-        const lng = Number(props.longitude);
-        const lat = Number(props.latitude);
-        if (isNaN(lng) || isNaN(lat)) {
-          console.warn('无效经纬度,跳过该数据:', props);
-          return null;
-        }
-        const [gcjLng, gcjLat] = wgs84togcj02(lng, lat); // 或直接用 lng/lat
-
-        return {
-          id: props.id || '未知',
-          company_name: props.company_name || '未知',
-          company_type: props.company_type || '未知',
-          county: props.county || '未知',
-          particulate_emission: props.particulate_emission !== undefined 
-            ? props.particulate_emission 
-            : '未知',
-          longitude: gcjLng,
-          latitude: gcjLat
-        };
-      })
-      .filter(item => item !== null); // 过滤无效项
-
-  } catch (err) {
-    error.value = `数据加载失败:${err.message}`;
-    console.error('详细错误:', err);
-  } finally {
-    loading.value = false;
-  }
-};
-
-// ========== 生命周期 ==========
-onMounted(() => {
-  fetchData();
-});
-</script>
-
-<style scoped>
-.region-average-chart {
-  width: 100%;
-  margin: 20px 0;
-  padding: 10px;
-}
-
-/* 错误提示 */
-.status.error {
-  color: #dc2626;
-  border: 1px solid #dc2626;
-  padding: 10px;
-  margin-bottom: 10px;
-}
-.status.error button {
-  padding: 2px 6px;
-  cursor: pointer;
-  margin-top: 5px;
-}
-.raw-response pre {
-  white-space: pre-wrap;
-  word-break: break-all;
-  font-size: 12px;
-  margin: 5px 0;
-  padding: 5px;
-  border: 1px solid #ccc;
-}
-
-/* 加载状态 */
-.loading-state {
-  padding: 20px 0;
-  text-align: center;
-}
-.spinner {
-  width: 30px;
-  height: 30px;
-  margin: 0 auto 10px;
-  border: 3px solid #e5e7eb;
-  border-top: 3px solid #333;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-/* 最基础的表格样式 */
-.table-container {
-  width: 100%;
-  max-width: 1200px;
-  margin: 0 auto;
-  overflow-x: auto; /* 防止内容溢出 */
-}
-
-.data-table {
-  border-collapse: collapse; /* 确保边框合并 */
-  border: 1px solid #ccc; /* 表格外边框 */
-}
-
-.data-table th, 
-.data-table td {
-  border: 1px solid #ccc; /* 单元格边框 */
-  padding: 8px 10px;
-  text-align: center;
-  font-size: 14px;
-}
-
-.data-table th {
-  font-weight: bold;
-  background-color: #f5f5f5; /* 表头背景色 */
-}
-
-/* 添加行分隔线效果 */
-.data-table tr:not(:last-child) {
-  border-bottom: 1px solid #e0e0e0; /* 行间分隔线 */
-}
-
-/* 列宽分配 */
-.data-table th:nth-child(1),
-.data-table td:nth-child(1) { width: 10%; }
-.data-table th:nth-child(2),
-.data-table td:nth-child(2) { width: 28%; }
-.data-table th:nth-child(3),
-.data-table td:nth-child(3) { width: 16%; }
-.data-table th:nth-child(4),
-.data-table td:nth-child(4) { width: 10%; }
-.data-table th:nth-child(5),
-.data-table td:nth-child(5) { width: 12%; }
-.data-table th:nth-child(6),
-.data-table td:nth-child(6) { width: 11%; }
-.data-table th:nth-child(7),
-.data-table td:nth-child(7) { width: 11%; }
-
-/* 内容换行处理 */
-.data-table td {
-  word-wrap: break-word; /* 长内容自动换行 */
-  padding: 8px 5px; /* 调整内边距 */
-}
-
-/* 移动端适配 */
-@media (max-width: 768px) {
-  .table-container {
-    padding: 10px 0;
-  }
-  .data-table {
-    font-size: 13px; /* 缩小字体 */
-  }
-}
-</style>

+ 0 - 252
src/components/atmpollution/atmcompanymap.vue

@@ -1,252 +0,0 @@
-<template>
-  <div class="map-wrapper">
-    <div ref="mapContainer" class="map-container"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue';
-import L from 'leaflet';
-import 'leaflet/dist/leaflet.css';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-const mapContainer = ref(null);
-
-// 定义蓝色三角形标记
-const blueTriangle = L.divIcon({
-  className: 'custom-div-icon',
-  html: `<svg width="24" height="24" viewBox="0 0 24 24">
-          <path d="M12 2L2 22h20L12 2z" fill="#0066CC" stroke="#003366" stroke-width="2"/>
-        </svg>`,
-  iconSize: [24, 24],
-  iconAnchor: [12, 24]
-});
-
-onMounted(() => {
-  if (!mapContainer.value) {
-    console.error('❌ 地图容器未找到!');
-    return;
-  }
-
-  const map = L.map(mapContainer.value, {
-    center: [24.7, 114], // 韶关大致中心
-    zoom: 8.5,
-    minZoom: 8,
-  });
-
-
-  // 区县颜色映射(保持不变)
-  const districtColorMap = {
-    "武江区": "#FF6B6B",
-    "浈江区": "#4ECDC4",
-    "曲江区": "#FFD166",
-    "始兴县": "#A0DAA9",
-    "仁化县": "#6A0572",
-    "翁源县": "#1A535C",
-    "乳源瑶族自治县": "#FF9F1C",
-    "新丰县": "#87CEEB",
-    "乐昌市": "#118AB2",
-    "南雄市": "#06D6A0",
-    "韶关市": "#cccccc",
-  };
-
-  // 区县颜色匹配函数(保持不变)
-  function getDistrictColor(name) {
-    if (districtColorMap[name]) return districtColorMap[name];
-    const normalizedName = name.replace(/市|县|区|自治县/g, '');
-    for (const key in districtColorMap) {
-      if (key.includes(normalizedName) || normalizedName.includes(key.replace(/市|县|区|自治县/g, ''))) {
-        return districtColorMap[key];
-      }
-    }
-    return '#cccccc';
-  }
-
-  // 只加载乡镇级GeoJSON,并根据所属区县着色http://localhost:8000/api/vector/export/all?table_name=town&format=geojson
-  fetch('./data/韶关市乡镇划分图5.geojson')
-    .then(res => {
-      if (!res.ok) throw new Error(`乡镇划分图加载失败:${res.status}`);
-      return res.json();
-    })
-    .then(townshipGeojson => {
-      L.geoJSON(townshipGeojson, {
-        style: (feature) => {
-          const countyName = feature.properties.FXZQMC || '';
-          
-          return {
-            fillColor: getDistrictColor(countyName), // 使用所属区县的颜色
-            fillOpacity: 0.7,                       // 填充透明度
-            color: '#333333',                       // 边界线颜色
-            weight: 1,                              // 乡镇边界线粗细(比区县细一些)
-            lineJoin: 'round'                       // 线条拐角圆润
-          };
-        },
-        
-      }).addTo(map);
-      //console.log('✅ 乡镇划分图加载完成,已按所属区县着色');
-    })
-    .catch(err => {
-      console.error('❌ 乡镇划分图加载失败:', err);
-      alert('乡镇划分图加载错误:' + err.message);
-    });
-
-  // 加载大气污染源数据(使用 api8000 实例)
-  api8000.get('/api/vector/export/all?table_name=atmo_company', {
-    responseType: 'text' // 确保获取原始文本
-  })
-    .then(response => {
-      const text = response.data;
-      
-      // 修复 NaN 问题
-      const fixedText = text.replace(
-        /"particulate_emission":\s*NaN/g, 
-        '"particulate_emission": null'
-      );
-      
-      const geoJSONData = JSON.parse(fixedText);
-      
-      //console.log('✅ 接口数据加载完成,共', geoJSONData.features.length, '条记录');
-      
-      let markerCount = 0;
-      geoJSONData.features.forEach((feature, idx) => {
-        try {
-          const props = feature.properties;
-          
-          const lng = parseFloat(props.longitude);
-          const lat = parseFloat(props.latitude);
-          if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
-            console.warn(`❌ 无效坐标(第${idx}条):`, lat, lng);
-            return;
-          }
-
-          const emission = props.particulate_emission !== null ? parseFloat(props.particulate_emission) : null;
-          const formattedEmission = emission === null || isNaN(emission)
-            ? '未知' 
-            : `${emission} t/a`;
-
-          const marker = L.marker([lat, lng], {
-            icon: blueTriangle,
-            zIndexOffset: 1000,
-          }).addTo(map);
-
-          marker.bindPopup(`
-            <div class="popup-container">
-              <h3 class="popup-title">${props.company_name || '未知'}</h3>
-              <div class="popup-divider"></div>
-              <p><strong>污染源序号:</strong> ${props.id || '未知'}</p>
-              <p><strong>企业类型:</strong> ${props.company_type || '未知'}</p>
-              <p><strong>所属区县:</strong> ${props.county || '未知'}</p>
-              <p><strong>大气颗粒物排放:</strong> ${formattedEmission}</p>
-            </div>
-          `);
-
-          markerCount++;
-        } catch (err) {
-          console.error(`❌ 处理第${idx}条数据失败:`, err);
-        }
-      });
-
-      //console.log(`✅ 成功创建 ${markerCount} 个有效标记`);
-    })
-    .catch(err => {
-      console.error('❌ 接口数据加载失败:', err);
-      alert('数据加载错误:' + err.message);
-    });
-
-  map.on('load', () => {
-    setTimeout(() => {
-      map.invalidateSize();
-      //console.log('✅ 地图尺寸已重新计算');
-    }, 300);
-  });
-
-  window.addEventListener('resize', () => {
-    map.invalidateSize();
-  });
-});
-</script>
-
-<style scoped>
-/* 样式保持不变 */
-.map-wrapper {
-  width: 100%;
-  height: 100%;
-  position: relative;
-}
-.map-container {
-  width: 100% !important;
-  height: 100% !important;
-}
-
-/* 弹窗样式 */
-::v-deep .popup-title {
-  text-align: center;
-  font-size: 18px;
-  font-weight: 700;
-  color: #0066CC;
-  margin: 0 0 6px;
-  border-bottom: none;
-  padding-bottom: 8px;
-}
-
-::v-deep .popup-divider {
-  height: 1px;
-  background: #0066CC;
-  margin: 8px 0;
-}
-
-::v-deep .popup-container {
-  min-width: 240px;
-  max-width: 300px;
-  padding: 16px;
-  font-family: "Microsoft YaHei", sans-serif;
-}
-
-::v-deep .popup-container p {
-  margin: 6px 0;
-  font-size: 15px;
-  color: #666;
-  line-height: 1.6;
-}
-
-::v-deep .popup-container strong {
-  color: #0066CC;
-  font-weight: 600;
-}
-
-::v-deep .exceeding {
-  color: #FF3333;
-  font-weight: bold;
-}
-
-/* 美化弹窗 */
-::v-deep .leaflet-popup-content-wrapper {
-  padding: 0 !important;
-  border-radius: 12px !important;
-  box-shadow: 0 6px 16px rgba(0,0,0,0.2) !important;
-}
-
-::v-deep .leaflet-popup-content {
-  margin: 0 !important;
-  width: auto !important;
-}
-
-::v-deep .leaflet-popup-tip {
-  display: none;
-}
-
-/* 图例样式 */
-::v-deep .info {
-  padding: 6px 8px;
-  background: white;
-  background: rgba(255,255,255,0.9);
-  box-shadow: 0 0 15px rgba(0,0,0,0.2);
-  border-radius: 5px;
-}
-
-/* 自定义标记样式 */
-::v-deep .custom-div-icon svg {
-  transition: transform 0.2s;
-  display: block;
-}
-</style>

+ 0 - 378
src/components/atmpollution/atmsamplemap.vue

@@ -1,378 +0,0 @@
-<template>
-  <div class="map-wrapper">
-    <div ref="mapContainer" class="map-container"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, watch } from 'vue'; 
-import L from 'leaflet';
-import 'leaflet/dist/leaflet.css';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-const props = defineProps({
-  calculationMethod: {
-    type: String,
-    required: true,
-    default: 'weight' 
-  }
-});
-
-const mapContainer = ref(null);
-const mapInstance = ref(null); 
-const markers = ref([]); 
-
-// ====================== 核心:重量→体积换算逻辑 ======================
-function calculateConcentration(heavyMetalWeight, volume) {
-  if (heavyMetalWeight === undefined || volume === undefined || isNaN(heavyMetalWeight) || isNaN(volume) || volume === 0) {
-    return '未知';
-  }
-  const ug = heavyMetalWeight * 1000; 
-  const concentration = ug / volume;
-  return concentration; 
-}
-
-function calculateParticleConcentration(particleWeight, volume) {
-  if (particleWeight === undefined || volume === undefined || isNaN(particleWeight) || isNaN(volume) || volume === 0) {
-    return '未知';
-  }
-  const ug = particleWeight * 1000; 
-  const concentration = ug / volume;
-  return concentration;
-}
-
-// ====================== 指标映射(重量直接取数,体积动态计算) ======================
-const metricsMap = {
-  weight: [ 
-    { label: 'Cr mg/kg', key: 'Cr_particulate' },
-    { label: 'As mg/kg', key: 'As_particulate' },
-    { label: 'Cd mg/kg', key: 'Cd_particulate' },
-    { label: 'Hg mg/kg', key: 'Hg_particulate' },
-    { label: 'Pb mg/kg', key: 'Pb_particulate' },
-    { label: '颗粒物重量 mg', key: 'particle_weight' }
-  ],
-  volume: [ 
-    { label: 'Cr ug/m³', getValue: (item) => calculateConcentration(item.Cr_particulate, item.standard_volume) },
-    { label: 'As ug/m³', getValue: (item) => calculateConcentration(item.As_particulate, item.standard_volume) },
-    { label: 'Cd ug/m³', getValue: (item) => calculateConcentration(item.Cd_particulate, item.standard_volume) },
-    { label: 'Hg ug/m³', getValue: (item) => calculateConcentration(item.Hg_particulate, item.standard_volume) },
-    { label: 'Pb ug/m³', getValue: (item) => calculateConcentration(item.Pb_particulate, item.standard_volume) },
-    { label: '颗粒物浓度 ug/m³', getValue: (item) => calculateParticleConcentration(item.particle_weight, item.standard_volume) },
-    { label: '标准体积 m³', key: 'standard_volume' },
-  ]
-};
-
-// ====================== 工具函数 ======================
-function formatValue(value) {
-  if (value === undefined || value === null || value === '' || isNaN(value)) {
-    return '未知';
-  }
-  return parseFloat(value);
-}
-
-function formatLocation(fullLocation) {
-  if (!fullLocation) return '未知位置';
-  const processed = fullLocation.replace(/^广东省韶关市/, '').trim().replace(/^韶关市/, '');
-  return processed || '未知位置';
-}
-
-function generatePopupContent(item, method) {
-  const metrics = metricsMap[method]; 
-  const metricsHtml = metrics.map(metric => {
-    let value;
-    if (method === 'weight') {
-      value = formatValue(item[metric.key]);
-    } else {
-      value = metric.getValue ? metric.getValue(item) : formatValue(item[metric.key]);
-    }
-    return `
-      <div class="data-item">
-        <span class="item-label">${metric.label}:</span>
-        <span class="item-value">${value.toFixed(6)}</span>
-      </div>
-    `;
-  }).join('');
-
-  return `
-    <div class="popup-container">
-      <div class="popup-header">
-        <h3 class="popup-title">${formatLocation(item.sampling_location || '未知采样点')}</h3>
-      </div>
-      <ul class="popup-info-list">
-        <li>
-          <span class="info-label">采样点ID:</span>
-          <span class="info-value">${item.ID || item.sample_code || '未知'}</span>
-        </li>
-        <li>
-          <span class="info-label">样品名称:</span>
-          <span class="info-value">${item.sample_name || '未知'}</span>
-        </li>
-      </ul>
-      <div class="grid-container">
-        <div class="grid-item">${metricsHtml}</div>
-      </div>
-    </div>
-  `;
-}
-
-// ====================== 区县名称归一化(处理后缀差异) ======================
-function normalizeDistrictName(name) {
-  if (!name) return '';
-  return name.replace(/市|县|区|自治县/g, ''); // 移除行政后缀
-}
-
-onMounted(() => {
-  if (!mapContainer.value) {
-    console.error('❌ 地图容器未找到!');
-    return;
-  }
-
-  const map = L.map(mapContainer.value, {
-    center: [24.7, 114], 
-    zoom: 8.5,
-    minZoom: 8.3,
-  });
-  mapInstance.value = map;
-
-  // 区县颜色映射(键名与GeoJSON的FXZQMC精确匹配,或归一化后匹配)
-  const districtColorMap = {
-    "武江区": "#FF6B6B",
-    "浈江区": "#4ECDC4",
-    "曲江区": "#FFD166",
-    "始兴县": "#A0DAA9",
-    "仁化县": "#6A0572",
-    "翁源县": "#1A535C",
-    "乳源瑶族自治县": "#FF9F1C", // 支持精确匹配
-    "新丰县": "#87CEEB",
-    "乐昌市": "#118AB2",
-    "南雄市": "#06D6A0",
-    "乳源": "#FF9F1C" // 可选:支持归一化后匹配(如GeoJSON是“乳源”时)
-  };
-
-  // 加载区县边界(关键修改:匹配FXZQMC字段 + 归一化)http://localhost:8000/api/vector/export/all?table_name=town_boundary&format=geojsonhttp://localhost:8000/api/vector/export/all?table_name=town&format=geojson
-  fetch('./data/韶关市乡镇划分图5.geojson')
-    .then(res => {
-      if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
-      return res.json();
-    })
-    .then(geojson => {
-      //console.log('✅ 区县边界数据加载完成,要素数:', geojson.features.length);
-      
-      L.geoJSON(geojson, {
-        style: (feature) => {
-          const rawDistrictName = feature.properties.FXZQMC || ''; // 从FXZQMC提取
-          const normalizedName = normalizeDistrictName(rawDistrictName);
-          
-          // 优先精确匹配,再归一化匹配
-          const color = districtColorMap[rawDistrictName] || 
-                        districtColorMap[normalizedName] || 
-                        '#cccccc';
-          
-          return {
-            fillColor: color,
-            fillOpacity: 0.7,
-            color: '#333333',
-            weight: 2,
-          };
-        },
-      }).addTo(map);
-
-      // 加载大气数据(使用 api8000 实例)
-      api8000.get('/api/vector/export/all?table_name=Atmo_sample_data', {
-        responseType: 'text' // 确保获取原始文本
-      })
-        .then(response => {
-          const text = response.data;
-          const validJsonText = text.replace(/NaN/g, 'null');
-          
-          try {
-            return JSON.parse(validJsonText);
-          } catch (err) {
-            console.error('❌ JSON 解析失败(原始数据含非法值):', err);
-            throw new Error('数据格式错误,请检查服务端返回');
-          }
-        })
-        .then(geojsonData => {
-          const atmosphereData = geojsonData.features.map(feature => feature.properties);
-          //console.log('✅ 大气数据加载完成,记录数:', atmosphereData.length);
-          markers.value = []; 
-          
-          atmosphereData.forEach((item, idx) => {
-            try {
-              // 提取经纬度(兼容 properties 和 geometry)
-              let lat = item.latitude;
-              let lng = item.longitude;
-
-                if (lat === undefined || lng === undefined) {
-                  if (item.geometry && item.geometry.type === 'Point' && item.geometry.coordinates.length === 2) {
-                    lng = item.geometry.coordinates[0]; 
-                    lat = item.geometry.coordinates[1];
-                  } else {
-                    console.error(`❌ 未找到经纬度字段(第${idx}条)`);
-                    return;
-                  }
-                }
-
-                const cleanLat = String(lat).replace(/[^\d.-]/g, '');
-                const cleanLng = String(lng).replace(/[^\d.-]/g, '');
-                
-                lat = parseFloat(parseFloat(cleanLat).toFixed(6));
-                lng = parseFloat(parseFloat(cleanLng).toFixed(6));
-                
-                const marker = L.circleMarker([lat, lng], {
-                  radius: 3.5,
-                  color: '#FF3333',
-                  fillColor: '#FF3333',
-                  fillOpacity: 0.9,
-                  weight: 1.5,
-                  zIndexOffset: 1000,
-                }).addTo(map);
-
-                marker.bindPopup(generatePopupContent(item, props.calculationMethod));
-                markers.value.push({ marker, item });
-              } catch (err) {
-                console.error(`❌ 处理大气数据失败(第${idx}条):`, err);
-              }
-            });
-        })
-        .catch(err => {
-          console.error('❌ 大气数据加载失败:', err);
-          alert('大气数据接口错误:' + err.message);
-        });
-    })
-    .catch(err => {
-      console.error('❌ 区县边界加载失败:', err);
-      alert('区县边界加载错误:' + err.message);
-    });
-});
-
-// 监听计算方式变化,更新弹窗
-watch(
-  () => props.calculationMethod,
-  (newMethod) => {
-    markers.value.forEach(({ marker, item }) => {
-      marker.bindPopup(generatePopupContent(item, newMethod));
-    });
-    //console.log(`✅ 已切换为${newMethod === 'weight' ? '重量' : '体积'}计算方式,弹窗内容已更新`);
-  }
-);
-</script>
-
-<style scoped>
-.map-wrapper {
-  width: 100%;
-  height: 100%;
-  position: relative;
-}
-.map-container {
-  width: 100% !important;
-  height: 100% !important;
-}
-
-::v-deep .leaflet-popup-content-wrapper {
-  padding: 0 !important;
-  border-radius: 10px !important;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
-}
-
-::v-deep .leaflet-popup-content {
-  margin: 0 !important;
-  width: auto !important;
-  max-width: 300px; 
-}
-
-::v-deep .popup-container {
-  min-width: 280px;
-  padding: 12px;
-  font-family: "Microsoft YaHei", sans-serif;
-}
-
-::v-deep .popup-header {
-  margin-bottom: 10px;
-}
-
-::v-deep .popup-title {
-  text-align: center;
-  font-size: 16px;
-  font-weight: 700;
-  color: #0066CC;
-  margin: 0 0 5px;
-  padding-bottom: 6px;
-  border-bottom: 1.5px solid #0066CC;
-}
-
-::v-deep .popup-info-list {
-  list-style: none;
-  padding: 0;
-  margin: 0 0 10px;
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 6px;
-}
-
-::v-deep .popup-info-list li {
-  display: flex;
-  margin: 0;
-  padding: 3px 6px;
-  background: #f9f9f9;
-  border-radius: 3px;
-}
-
-::v-deep .info-label {
-  flex: 0 0 85px;
-  font-weight: 600;
-  color: #333;
-  font-size: 13px;
-}
-
-::v-deep .info-value {
-  flex: 1;
-  color: #666;
-  font-size: 13px;
-  white-space: nowrap;
-}
-
-::v-deep .grid-container {
-  display: grid;
-  grid-template-columns: 1fr; 
-  gap: 6px;
-}
-
-::v-deep .grid-item {
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-}
-
-::v-deep .data-item {
-  display: flex;
-  justify-content: space-between;
-  padding: 6px 8px;
-  background: #f9f9f9;
-  border-radius: 3px;
-}
-
-::v-deep .item-label {
-  font-weight: 600;
-  color: #555;
-  font-size: 13px;
-}
-
-::v-deep .item-value {
-  color: #000;
-  font-size: 13px;
-}
-
-::v-deep .leaflet-popup-tip {
-  display: none;
-}
-
-::v-deep .leaflet-circle-marker {
-  stroke-width: 1.5px !important;
-}
-
-::v-deep .leaflet-marker-pane .leaflet-circle-marker[fill="#118AB2"]:hover {
-  fill-opacity: 1 !important;
-  stroke-width: 2.5px !important;
-}
-</style>

+ 0 - 391
src/components/atmpollution/heavyMetalEnterprisechart.vue

@@ -1,391 +0,0 @@
-<template>
-  <div class="heavy-metal-chart">
-    <!-- 错误提示(带原始响应预览) -->
-    <div v-if="error" class="status error">
-      <i class="fa fa-exclamation-circle"></i> {{ error }}
-      <div class="raw-response" v-if="rawResponse">
-        <button @click="showRaw = !showRaw" class="raw-btn">
-          {{ showRaw ? '收起原始响应' : '查看原始响应(前1000字符)' }}
-        </button>
-        <pre v-if="showRaw" class="raw-pre">{{ truncatedRawResponse }}</pre>
-      </div>
-    </div>
-
-    <!-- 加载状态 -->
-    <div v-if="loading" class="loading-state">
-      <div class="spinner"></div>
-      <p>数据加载中...</p>
-    </div>
-
-    <!-- 图表容器(使用v-show确保DOM始终存在) -->
-    <div 
-      v-show="!loading && !error" 
-      ref="chartRef" 
-      class="chart-box"
-      :style="{ 
-        height: '100%', 
-        border: '2px solid #1890ff', 
-        position: 'relative' 
-      }"
-    >
-      <!-- 容器状态可视化提示(调试用) -->
-      <div class="container-status" v-if="debugMode">
-        容器状态: {{ containerStatus }}
-        <br>
-        高度: {{ containerHeight }}px
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, computed, onUnmounted, nextTick } from 'vue';
-import * as echarts from 'echarts';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// ========== 核心配置 ==========
-const API_URL = '/api/vector/export/all?table_name=atmo_company'; // 使用相对路径
-const SG_REGIONS = [
-  '浈江区', '武江区', '曲江区', '乐昌市', 
-  '南雄市', '始兴县', '仁化县', '翁源县', 
-  '新丰县', '乳源县'
-];
-const EXCLUDE_FIELDS = [
-  'id', 'company_name', 'company_type', 'longitude', 'latitude'
-];
-const debugMode = true; // 调试模式:显示容器状态
-
-// ========== 响应式数据 ==========
-const chartRef = ref(null);
-const error = ref('');
-const loading = ref(true);
-const rawResponse = ref('');
-const showRaw = ref(false);
-const containerStatus = ref('未初始化');
-const containerHeight = ref(0);
-let myChart = null;
-
-// 截断原始响应
-const truncatedRawResponse = computed(() => {
-  return rawResponse.value.length > 1000 
-    ? rawResponse.value.slice(0, 1000) + '...' 
-    : rawResponse.value;
-});
-
-// ========== 容器状态检查(实时更新) ==========
-const checkContainer = () => {
-  if (!chartRef.value) {
-    containerStatus.value = '未找到容器元素';
-    containerHeight.value = 0;
-    return false;
-  }
-  
-  containerStatus.value = '已找到容器元素';
-  containerHeight.value = chartRef.value.offsetHeight;
-  return true;
-};
-
-// ========== 数据处理逻辑 ==========
-const processData = (features) => {
-  console.log('🔍 开始处理数据,features数量:', features.length);
-  
-  // 提取有效properties
-  const apiData = features
-    .map(feature => feature.properties || {})
-    .filter(props => Object.keys(props).length > 0);
-  console.log('🔍 有效properties数量:', apiData.length);
-
-  if (apiData.length === 0) {
-    throw new Error('无有效数据(properties为空)');
-  }
-
-  // 识别污染物字段
-  const pollutantFields = Object.keys(apiData[0])
-    .filter(key => 
-      !EXCLUDE_FIELDS.includes(key) &&  
-      !isNaN(parseFloat(apiData[0][key]))  
-    );
-  console.log('🔍 识别的污染物字段:', pollutantFields);
-
-  if (pollutantFields.length === 0) {
-    throw new Error('未识别到有效污染物字段,请检查EXCLUDE_FIELDS');
-  }
-
-  // 按区县统计
-  const regionStats = {};
-  const globalStats = {};
-  let totalSamples = 0;
-
-  pollutantFields.forEach(field => {
-    globalStats[field] = { sum: 0, count: 0 };
-  });
-
-  apiData.forEach(item => {
-    const county = item.county || '未知区县';
-    totalSamples++;
-
-    if (!regionStats[county]) {
-      regionStats[county] = {};
-      pollutantFields.forEach(field => {
-        regionStats[county][field] = { sum: 0, count: 0 };
-      });
-    }
-
-    pollutantFields.forEach(field => {
-      const value = parseFloat(item[field]);
-      if (!isNaN(value)) {
-        regionStats[county][field].sum += value;
-        regionStats[county][field].count++;
-        globalStats[field].sum += value;
-        globalStats[field].count++;
-      }
-    });
-  });
-  console.log('🔍 区县统计结果:', regionStats);
-
-  // 构建有效区县
-  const validRegions = SG_REGIONS.filter(region => regionStats[region])
-    .concat('全市平均');
-  console.log('🔍 有效区县列表:', validRegions);
-
-  // 构建图表数据
-  const series = pollutantFields.map((field, index) => ({
-    name: field,
-    type: 'bar',
-    data: validRegions.map(region => {
-      if (region === '全市平均') {
-        return globalStats[field].count 
-          ? (globalStats[field].sum / globalStats[field].count).toFixed(5)
-          : 0;
-      }
-      return regionStats[region][field].count 
-        ? (regionStats[region][field].sum / regionStats[region][field].count).toFixed(5) 
-        : 0;
-    }),
-    itemStyle: { 
-      // 当x轴类目是“全市平均”时,柱子显示红色
-      color: (params) => {
-        // params.dataIndex 对应 regions 数组的索引,最后一个是“全市平均”
-        const isTotal = validRegions[params.dataIndex] === '全市平均';
-        return isTotal ? '#ff0000' : '#1890ff'; // 红色可用 #ff0000 或其他红色值
-      }
-    },
-    label: { show: true, position: 'top', fontSize: 15 }
-  }));
-
-  return { regions: validRegions, series, totalSamples };
-};
-
-// ========== ECharts初始化 ==========
-const initChart = (data) => {
-  // 检查容器状态
-  if (!checkContainer()) {
-    error.value = '图表容器未准备好,请刷新页面重试';
-    return;
-  }
-
-  // 检查容器高度
-  if (containerHeight.value < 100) {
-    error.value = `容器高度异常(${containerHeight.value}px),请检查样式`;
-    return;
-  }
-
-  // 销毁旧实例
-  if (myChart && !myChart.isDisposed()) {
-    myChart.dispose();
-  }
-
-  // 空数据检查
-  if (data.series.length === 0 || data.regions.length === 0) {
-    error.value = '无有效数据用于绘制图表';
-    return;
-  }
-
-  // 初始化图表
-  try {
-    myChart = echarts.init(chartRef.value);
-    myChart.setOption({
-      title: {
-        text: '韶关市各区县企业排放大气颗粒物浓度平均值',
-        left: 'center',
-        subtext: `基于 ${data.totalSamples} 个有效样本`,
-        subtextStyle: { fontSize: 15 }
-      },
-      tooltip: {
-        trigger: 'axis',
-        formatter: (params) => {
-          let content = `${params[0].name}:<br>`;
-          params.forEach(p => {
-            content += `${p.seriesName}: ${p.value} t/a<br>`;
-          });
-          return content;
-        },
-        axisLabel:{fontSize:15}
-      },
-      xAxis: {
-        type: 'category',
-        data: data.regions,
-        axisLabel: { rotate: 30, fontSize: 15 }
-      },
-      yAxis: {
-        type: 'value',
-        name: '颗粒物排放量 (t/a)',
-        nameTextStyle: { fontSize: 15 },
-        axisLabel:{fontSize:15},
-      },
-      
-      series: data.series,
-      grid: { left: '5%', right: '5%', bottom: '5%', containLabel: true }
-    });
-    console.log('✅ 图表初始化成功');
-  } catch (err) {
-    error.value = `图表初始化失败:${err.message}`;
-    console.error('图表初始化错误:', err);
-  }
-};
-
-// ========== 数据请求逻辑 ==========
-const fetchData = async () => {
-  try {
-    loading.value = true;
-    error.value = '';
-    console.log('🚀 开始请求数据:', API_URL);
-
-    // 使用 api8000 实例发起请求
-    const response = await api8000.get(API_URL, {
-      timeout: 20000, // 20秒超时
-      responseType: 'text'
-    });
-    
-    rawResponse.value = response.data;
-    console.log('✅ 数据请求成功,状态码:', response.status);
-
-    // 修复NaN并解析
-    const fixedJson = response.data.replace(/:\s*NaN/g, ': null');
-    const geoJSONData = JSON.parse(fixedJson);
-
-    // 校验数据结构
-    if (!geoJSONData.features || !Array.isArray(geoJSONData.features)) {
-      throw new Error('响应数据缺少features数组');
-    }
-
-    // 处理数据
-    const chartData = processData(geoJSONData.features);
-    console.log('✅ 数据处理完成,准备渲染图表');
-
-    // 等待DOM更新(双重保险)
-    await nextTick();
-    console.log('🔄 DOM更新完成,检查容器:', chartRef.value);
-
-    // 强制延迟确保容器准备好(极端情况处理)
-    setTimeout(() => {
-      initChart(chartData);
-    }, 300);
-
-  } catch (err) {
-    error.value = `数据加载失败:${err.message}`;
-    console.error('❌ 数据请求错误:', err);
-  } finally {
-    loading.value = false;
-  }
-};
-
-// ========== 生命周期 ==========
-onMounted(() => {
-  // 初始检查容器
-  checkContainer();
-  // 开始加载数据
-  fetchData();
-});
-
-// ========== 响应式布局 ==========
-const handleResize = () => {
-  if (myChart) {
-    myChart.resize();
-    console.log('🔄 图表已重绘');
-  }
-};
-onMounted(() => window.addEventListener('resize', handleResize));
-onUnmounted(() => window.removeEventListener('resize', handleResize));
-</script>
-
-<style scoped>
-.heavy-metal-chart {
-  width: 100%;
-  max-width: 1200px;
-  background: #fff;
-  border-radius: 12px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
-  position: relative;
-}
-
-/* 错误提示 */
-.status.error {
-  color: #dc2626;
-  background: #fee2e2;
-  padding: 12px 16px;
-  border-radius: 6px;
-  margin-bottom: 16px;
-}
-.raw-btn {
-  margin: 8px 0;
-  padding: 4px 8px;
-  background: #ff4d4f;
-  color: white;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
-  font-size: 18px;
-}
-.raw-pre {
-  white-space: pre-wrap;
-  word-break: break-all;
-  background: #f9fafb;
-  padding: 8px;
-  border-radius: 4px;
-  font-size: 18px;
-  max-height: 200px;
-  overflow: auto;
-}
-
-/* 加载状态 */
-.loading-state {
-  text-align: center;
-  padding: 60px 0;
-  color: #6b7280;
-}
-.spinner {
-  width: 40px;
-  height: 40px;
-  margin: 0 auto 16px;
-  border: 4px solid #e5e7eb;
-  border-top: 4px solid #3b82f6;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-/* 图表容器 */
-.chart-box {
-  width: 100%;
-  min-height: 500px ; /* 强制最小高度 */
-  border-radius: 8px;
-  overflow: hidden;
-}
-
-/* 容器状态提示 */
-.container-status {
-  position: absolute;
-  top: 10px;
-  left: 10px;
-  background: rgba(255,255,255,0.8);
-  padding: 4px 8px;
-  border-radius: 4px;
-  font-size: 12px;
-  color: #1890ff;
-  z-index: 10;
-}
-</style>

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

+ 22 - 18
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>
@@ -29,21 +29,25 @@ import * as echarts from 'echarts'
 import VChart from 'vue-echarts'
 import { api8000 } from '@/utils/request' // 导入 api8000 实例
 import { ref, onMounted, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
 
 export default {
   components: { VChart },
   setup() {
+    const { t } = 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' },
+      { 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)   // 加载状态
@@ -86,22 +90,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,7 +119,7 @@ export default {
           splitLine: { lineStyle: { color: '#f0f0f0' } }
         },
         series: [{
-          name: '重金属浓度分布',
+          name: t('DetectionStatistics.concentrationDistribution'),
           type: 'boxplot',
           data, // 直接使用接口返回的统计数据
           itemStyle: {

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

+ 22 - 18
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>
@@ -29,10 +29,14 @@ import * as echarts from 'echarts'
 import VChart from 'vue-echarts'
 import { api8000 } from '@/utils/request' // 导入 api8000 实例
 import { ref, onMounted, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
 
 export default {
   components: { VChart },
   setup() {
+     const { t } = useI18n()
+
+
     // -------- 基本状态 --------
     const apiUrl = ref('/api/vector/stats/water_sampling_data') // 修改为相对路径
     const apiTimestamp = ref(null)
@@ -52,11 +56,11 @@ export default {
 
     // -------- 配置:金属字段 --------
     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' }
+      { 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' }
     ]
 
     // -------- 构建箱线数据 --------
@@ -99,22 +103,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: 30,
           axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold' ,fontSize :11}
@@ -129,7 +133,7 @@ export default {
           splitLine: { lineStyle: { color: '#f0f0f0' } }
         },
         series: [{
-          name: '重金属浓度分布',
+          name: t('DetectionStatistics.concentrationDistribution'),
           type: 'boxplot',
           data,
           itemStyle: {

+ 0 - 211
src/components/irrpollution/crossSectionSamplelineData.vue

@@ -1,211 +0,0 @@
-<template>
-  <div class="map-page">
-    <!-- 错误提示 -->
-    <div v-if="error" class="error-message">
-      <i class="fa fa-exclamation-circle"></i> {{ error }}
-    </div>
-    
-    <!-- 加载状态 -->
-    <div v-if="loading" class="loading-state">
-      <div class="spinner"></div>
-      <p>数据加载中...</p>
-    </div>
-
-    <!-- 数据表格容器 -->
-    <div v-else class="table-container">
-      <table class="data-table">
-        <!-- 表头 -->
-        <thead>
-          <tr>
-            <th>断面编号</th>
-            <th>所属河流</th>
-            <th>断面位置</th>
-            <th>所属区县</th>
-            <th>Cd含量(ug/L)</th>
-            <th>经度</th>
-            <th>纬度</th>
-          </tr>
-        </thead>
-        <!-- 表体(遍历数据) -->
-        <tbody>
-          <tr v-for="item in state.excelData" :key="item.id">
-            <td>{{ item.id }}</td>
-            <td>{{ item.river }}</td>
-            <td>{{ item.location }}</td>
-            <td>{{ item.district }}</td>
-            <td>{{ item.cdValue }}</td>
-            <td>{{ item.longitude.toFixed(6) }}</td> <!-- 保留6位小数 -->
-            <td>{{ item.latitude.toFixed(6) }}</td>
-          </tr>
-          <!-- 空数据提示 -->
-          <tr v-if="state.excelData.length === 0">
-            <td colspan="7" class="empty-state">暂无数据</td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted } from 'vue'
-import { wgs84togcj02 } from 'coordtransform';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// 状态管理
-const error = ref(null)
-const loading = ref(true)
-const state = reactive({
-  excelData: [], // 存储解析后的断面数据
-  riverAvgData: [] // 存储按河流分组后的平均数据
-})
-
-// 从接口获取数据并处理
-const fetchData = async () => {
-  try {
-    loading.value = true;
-    error.value = null;
-    
-    // 使用 api8000 实例获取数据
-    const response = await api8000.get('/api/vector/export/all?table_name=cross_section');
-    
-    // 处理数据
-    state.excelData = response.data.features
-      .map(feature => {
-        const props = feature.properties;
-        // 转换经纬度
-        const lng = Number(props.longitude);
-        const lat = Number(props.latitude);
-        
-        if (isNaN(lat) || isNaN(lng)) {
-          console.error('无效经纬度数据:', props);
-          return null;
-        }
-        
-        // WGS84转GCJ02坐标
-        const [gcjLng, gcjLat] = wgs84togcj02(lng, lat);
-        
-        return {
-          id: props.id,
-          river: props.river_name || '未知河流',
-          location: props.position || '未知位置',
-          district: props.county || '未知区县',
-          cdValue: props.cd_concentration !== undefined ? props.cd_concentration : '未知',
-          latitude: gcjLat,
-          longitude: gcjLng
-        };
-      })
-      .filter(item => item !== null); // 过滤无效数据
-      
-  } catch (err) {
-    error.value = `数据加载失败: ${err.message || '未知错误'}`;
-    console.error('数据处理错误:', err);
-  } finally {
-    loading.value = false;
-  }
-}
-
-// 生命周期
-onMounted(async () => {
-  await fetchData();
-})
-
-</script>
-
-<style scoped>
-.map-page {
-  width: 100%;
-  margin: 0 auto 24px;
-  background-color: white;
-  border-radius: 12px;
-  padding: 20px;
-  box-sizing: border-box;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-/* 错误提示样式 */
-.error-message {
-  color: #dc2626;
-  background-color: #fee2e2;
-  padding: 12px 16px;
-  border-radius: 6px;
-  margin-bottom: 16px;
-  display: flex;
-  align-items: center;
-  font-weight: 500;
-}
-
-.error-message i {
-  margin-right: 8px;
-  font-size: 18px;
-}
-
-/* 加载状态样式 */
-.loading-state {
-  text-align: center;
-  padding: 40px 0;
-  color: #6b7280;
-}
-
-.spinner {
-  width: 40px;
-  height: 40px;
-  margin: 0 auto 16px;
-  border: 4px solid #e5e7eb;
-  border-top: 4px solid #3b82f6;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-.data-container {
-  width: 100%;
-  overflow-x: auto;
-  padding: 0;
-  margin: 0;
-}
-
-.data-table {
-  width: 100%;
-  border-collapse: collapse;
-}
-
-.data-table th, .data-table td {
-  padding: 12px 15px;
-  text-align: center;
-  border: 1px solid #e5e7eb;
-  background-color: white;
-}
-
-.data-table th {
-  background-color: white;
-  font-weight: bold;
-  color: #1f2937;
-}
-
-
-.data-table tr:hover {
-  background-color: #f3f4f6;
-}
-
-/* 空数据状态 */
-.empty-state {
-  padding: 40px 0;
-  color: #6b7280;
-  font-style: italic;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
-  .map-page {
-    width: 96%;
-  }
-  
-  .table-container {
-    overflow-x: auto;
-  }
-}
-</style>

+ 0 - 262
src/components/irrpollution/crossSetionData1.vue

@@ -1,262 +0,0 @@
-<template>
-  <div class="chart-page">
-    <!-- 加载状态 -->
-    <div v-if="loading" class="loading-indicator">
-      <div class="spinner"></div>
-      <p>数据加载中...</p>
-    </div>
-    
-    <!-- 错误提示 -->
-    <div v-else-if="error" class="error-message">
-      <i class="fa fa-exclamation-circle"></i> {{ error }}
-    </div>
-    
-    <!-- 图表容器 -->
-    <div v-else ref="chartContainer" class="chart-container"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
-import { wgs84togcj02 } from 'coordtransform';
-import * as echarts from 'echarts'
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// 状态管理
-const error = ref(null)
-const loading = ref(true)
-const chartContainer = ref(null)
-let chart = null
-
-const state = reactive({
-  excelData: [],        // 存储解析后的断面数据
-  riverAvgData: []      // 存储按河流分组后的平均数据
-})
-
-// 从接口获取数据并初始化
-const initData = async () => {
-  try {
-    loading.value = true;
-    error.value = null;
-    
-    // 使用 api8000 实例获取数据
-    const response = await api8000.get('/api/vector/export/all?table_name=cross_section');
-    
-    // 处理每个Feature的properties
-    state.excelData = response.data.features
-      .map(feature => {
-        const props = feature.properties
-        
-        // 处理经纬度(保持原有坐标转换逻辑)
-        const lng = Number(props.longitude)
-        const lat = Number(props.latitude)
-        
-        if (isNaN(lat) || isNaN(lng)) {
-          console.error('无效经纬度数据:', props)
-          return null
-        }
-        
-        // WGS84转GCJ02坐标
-        const [gcjLng, gcjLat] = wgs84togcj02(lng, lat)
-        
-        return {
-          id: props.id,
-          river: props.river_name || '未知河流',
-          location: props.position || '未知位置',
-          district: props.county || '未知区县',
-          cdValue: props.cd_concentration !== undefined ? props.cd_concentration : '未知',
-          latitude: gcjLat,
-          longitude: gcjLng
-        }
-      })
-      .filter(item => item !== null) // 过滤无效数据
-    
-    // 计算河流平均值
-    calculateRiverAvg()
-    
-  } catch (err) {
-    error.value = `数据加载失败:${err.message || '未知错误'}`
-    console.error('数据处理错误:', err)
-  } finally {
-    loading.value = false
-  }
-}
-
-// 按河流分组计算平均值
-const calculateRiverAvg = () => {
-  const riverGroups = {};
-
-  // 分组统计
-  state.excelData.forEach(item => {
-    if (!riverGroups[item.river]) {
-      riverGroups[item.river] = { total: 0, count: 0 }
-    }
-    riverGroups[item.river].total += item.cdValue
-    riverGroups[item.river].count += 1
-  });
-
-  // 计算各组平均值
-  const riverAvg = [];
-  let totalAll = 0;
-  let countAll = 0;
-
-  for (const river in riverGroups) {
-    const avg = riverGroups[river].total / riverGroups[river].count;
-    riverAvg.push({
-      river,
-      avg: parseFloat(avg).toFixed(6)  //保留6位小数
-    });
-    totalAll += riverGroups[river].total;
-    countAll += riverGroups[river].count;
-  }
-
-  // 添加总平均值
-  const totalAvg = {
-    river: '总河流平均',
-    avg: parseFloat((totalAll / countAll)).toFixed(6)
-  };
-  riverAvg.push(totalAvg);
-
-  state.riverAvgData = riverAvg;
-  updateChart(); // 更新图表
-}
-
-// 初始化ECharts实例
-const initChart = () => {
-  if (chartContainer.value) {
-    chart = echarts.init(chartContainer.value)
-    updateChart()
-  }
-}
-
-// 更新图表数据
-const updateChart = () => {
-  if (!chart || state.riverAvgData.length === 0) return;
-
-  // 处理图表数据
-  const rivers = state.riverAvgData.map(item => item.river)
-  const avgs = state.riverAvgData.map(item => item.avg)
-
-  // ECharts配置项
-  const option = {
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: { type: 'shadow' },
-      formatter: '{a} <br/>{b}: {c} ug/L'
-    },
-    grid: {
-      left: '5%',
-      right: '5%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      type: 'category',
-      data: rivers,
-      axisLabel: { 
-        interval: 0, 
-        fontSize: 15,
-        rotate: 30, // 添加旋转以避免标签重叠
-        margin: 15  // 增加间距
-      }
-    },
-    yAxis: {
-      type: 'value',
-      name: 'Cd浓度 (ug/L)',
-      min: 0,
-      nameTextStyle: { fontSize: 15},
-      axisLabel: { formatter: '{value}', fontSize: 15 }
-    },
-    series: [{
-      name: '平均镉浓度',
-      type: 'bar',
-      data: avgs,
-      itemStyle: {
-        color: (params) => 
-          params.dataIndex === rivers.length - 1 ? '#FF4500' : '#1E88E5'
-      },
-      label: {
-        show: true,
-        position: 'top',
-        formatter: '{c}',
-        fontSize: 15
-      },
-      emphasis: { focus: 'series' }
-    }]
-  };
-
-  chart.setOption(option)
-}
-
-// 生命周期钩子
-onMounted(async () => {
-  await initData()   // 先加载数据
-  initChart()        // 再初始化图表
-  
-  // 监听窗口变化
-  window.addEventListener('resize', () => {
-    if (chart) chart.resize()
-  })
-})
-
-onBeforeUnmount(() => {
-  if (chart) chart.dispose() // 销毁图表实例
-})
-</script>
-
-<style scoped>
-.chart-page {
-  width: 100%;
-  margin: 0 auto 24px;
-  border-radius: 12px;
-  padding: 20px;
-  box-sizing: border-box;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-.chart-container {
-  width: 100%; 
-  height:400px;
-  margin: 0 auto;
-  border-radius: 12px;
-}
-
-/* 加载状态样式 */
-.loading-indicator {
-  text-align: center;
-  padding: 40px 0;
-  color: #6b7280;
-}
-
-.spinner {
-  width: 40px;
-  height: 40px;
-  margin: 0 auto 16px;
-  border: 4px solid #e5e7eb;
-  border-top: 4px solid #3b82f6;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-/* 错误提示样式 */
-.error-message {
-  color: #dc2626;
-  background-color: #fee2e2;
-  padding: 12px 16px;
-  border-radius: 6px;
-  margin-bottom: 16px;
-  display: flex;
-  align-items: center;
-  font-weight: 500;
-}
-
-.error-message i {
-  margin-right: 8px;
-  font-size: 18px;
-}
-</style>

+ 0 - 264
src/components/irrpollution/crossSetionData2.vue

@@ -1,264 +0,0 @@
-<template>
-  <!-- 柱状图容器 -->
-  <div class="chart-page">
-    <!-- 加载状态 -->
-    <div v-if="loading" class="loading-indicator">
-      <div class="spinner"></div>
-      <p>数据加载中...</p>
-    </div>
-    
-    <!-- 错误提示 -->
-    <div v-else-if="error" class="error-message">
-      <i class="fa fa-exclamation-circle"></i> {{ error }}
-    </div>
-    
-    <!-- 图表容器 -->
-    <div v-else ref="chartContainer" class="chart-container"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
-import * as echarts from 'echarts'
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// 状态管理
-const error = ref(null)
-const loading = ref(true) // 添加加载状态
-const chartContainer = ref(null)
-let chart = null
-const state = reactive({
-  excelData: [], // 存储解析后的断面数据
-  districtAvgData: [] // 存储按区县分组后的平均数据
-})
-
-// 从接口初始化数据(使用 api8000 实例)
-const initData = async () => {
-  try {
-    loading.value = true;
-    error.value = null;
-    
-    // 使用 api8000 实例获取数据
-    const response = await api8000.get('/api/vector/export/all?table_name=cross_section');
-    
-    // 逐行解析Feature,确保每个样本都被处理
-    state.excelData = response.data.features.map(feature => {
-      const props = feature.properties || {}
-      
-      // 强制转换Cd浓度为数值(处理异常值)
-      let cdValue = parseFloat(props.cd_concentration)
-      if (isNaN(cdValue)) {
-        console.warn('发现无效Cd浓度值,已设为0:', props)
-        cdValue = 0 // 保证参与计算,避免数据缺失
-      }
-      
-      return {
-        id: props.id || '未知ID',       // 兜底处理
-        river: props.river_name || '未知河流',
-        district: props.county || '未知区县',
-        cdValue: cdValue
-      }
-    })
-    
-    calculateDistrictAvg() // 计算区县平均值
-  } catch (err) {
-    error.value = `数据加载失败: ${err.message || '未知错误'}`;
-    console.error('数据加载失败:', err)
-  } finally {
-    loading.value = false;
-  }
-}
-
-// 按区县计算平均浓度(核心逻辑保持不变)
-const calculateDistrictAvg = () => {
-  const districtGroups = {};
-  
-  // 1. 分组统计(总和 + 数量)
-  state.excelData.forEach(item => {
-    const district = item.district;
-    if (!districtGroups[district]) {
-      districtGroups[district] = { total: 0, count: 0 };
-    }
-    districtGroups[district].total += item.cdValue;
-    districtGroups[district].count += 1;
-  });
-
-  // 2. 计算平均值 + 总平均
-  const districtAvg = [];
-  let totalAll = 0;
-  let countAll = 0;
-
-  for (const district in districtGroups) {
-    const avg = districtGroups[district].total / districtGroups[district].count;
-    districtAvg.push({
-      district,
-      avg: parseFloat(avg).toFixed(6)
-    });
-    totalAll += districtGroups[district].total;
-    countAll += districtGroups[district].count;
-  }
-
-  // 添加总平均值(最后一项)
-  districtAvg.push({
-    district: '总平均',
-    avg: parseFloat((totalAll / countAll)).toFixed(6)
-  });
-
-  state.districtAvgData = districtAvg;
-  updateChart(); // 更新图表
-}
-
-// 初始化图表
-const initChart = () => {
-  if (chartContainer.value) {
-    chart = echarts.init(chartContainer.value);
-    updateChart();
-  }
-}
-
-// 更新图表数据
-const updateChart = () => {
-  if (!chart || state.districtAvgData.length === 0) return;
-
-  // 准备图表数据
-  const districts = state.districtAvgData.map(item => item.district); // x轴:区县名
-  const avgs = state.districtAvgData.map(item => item.avg); // y轴:平均浓度
-
-  // 图表配置
-  const option = {
-    tooltip: {
-      trigger: 'axis',
-      formatter: '{b}: {c} μg/L' // 悬停提示:区县名 + 浓度值
-    },
-    grid: {
-      left: '5%',
-      right: '5%',
-      bottom: '15%', // 底部留空间,防止区县名重叠
-      containLabel: true
-    },
-    xAxis: {
-      type: 'category',
-      data: districts,
-      axisLabel: {
-        interval: 0,
-        fontSize: 15,
-        rotate: 30, // 添加旋转以避免标签重叠
-        margin: 15 // 增加间距
-      }
-    },
-    yAxis: {
-      type: 'value',
-      name: 'Cd浓度 (μg/L)',
-      min: 0, // 从0开始更直观
-      nameTextStyle:{
-        fontSize:15,
-      },
-      axisLabel: {
-        formatter: '{value}',
-        fontSize: 15
-      }
-    },
-    series: [
-      {
-        name: '平均浓度',
-        type: 'bar',
-        data: avgs,
-        itemStyle: {
-          // 为"总平均"设置不同颜色(最后一项)
-          color: (params) => params.dataIndex === districts.length - 1 ? '#FF4500' : '#1E88E5'
-        },
-        label: {
-          show: true, // 显示数值标签
-          position: 'top',
-          formatter: '{c}',
-          fontSize: 15
-        },
-        barWidth: '60%'
-      }
-    ],
-    graphic: [
-      {
-        type: 'rect',
-        left: '5%',   // 与 grid.left 对齐
-        right: '5%',  // 与 grid.right 对齐
-        bottom: '5%',// 与 grid.bottom 对齐(位于绘图区域底部)
-        height: 30,   // 圆弧高度
-        // 顶部左右圆角(半径 15,与高度 30 配合形成上凸圆弧)
-        borderRadius: [15, 15, 0, 0], 
-        fill: '#FFFFFF', // 白色填充
-        z: -1          // 层级低于图表元素(如柱子、坐标轴)
-      }
-    ]
-  };
-
-  chart.setOption(option);
-}
-
-// 生命周期管理
-onMounted(async () => {
-  await initData(); // 先加载接口数据
-  initChart();      // 再初始化图表
-  // 监听窗口 resize,自动调整图表大小
-  window.addEventListener('resize', () => chart && chart.resize());
-})
-
-onBeforeUnmount(() => {
-  // 组件销毁时释放图表资源
-  if (chart) chart.dispose();
-})
-</script>
-
-<style scoped>
-.chart-page {
-  width: 100%;
-  margin: 0 auto 24px;
-  background-color: white;
-  border-radius: 12px;
-  padding: 20px;
-  box-sizing: border-box;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-.chart-container {
-  width: 100%;
-  height: 550px; /* 增加高度以容纳旋转的标签 */
-}
-
-/* 加载状态样式 */
-.loading-indicator {
-  text-align: center;
-  padding: 40px 0;
-  color: #6b7280;
-}
-
-.spinner {
-  width: 40px;
-  height: 40px;
-  margin: 0 auto 16px;
-  border: 4px solid #e5e7eb;
-  border-top: 4px solid #3b82f6;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-/* 错误提示样式 */
-.error-message {
-  color: #dc2626;
-  background-color: #fee2e2;
-  padding: 12px 16px;
-  border-radius: 6px;
-  margin-bottom: 16px;
-  display: flex;
-  align-items: center;
-  font-weight: 500;
-}
-
-.error-message i {
-  margin-right: 8px;
-  font-size: 18px;
-}
-</style>

+ 0 - 416
src/components/irrpollution/crossSetionTencentmap.vue

@@ -1,416 +0,0 @@
-<template>
-    <div class="map-page">
-    <div ref="mapContainer" class="map-container"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
-import { wgs84togcj02 } from 'coordtransform';
-const isMapReady = ref(false)
-const TMap = ref(null); // 存储腾讯地图SDK实例
-const mapContainer = ref(null); // 地图容器DOM
-const state = reactive({ excelData: [] }); // 存储处理后的数据
-const infoWindow = ref(null); // 信息窗口实例
-let map = null; // 地图实例
-let markersLayer = null; // 标记图层实例
-
-// 腾讯地图配置
-const tMapConfig = reactive({
-  key: import.meta.env.VITE_TMAP_KEY, // 必须配置环境变量(腾讯地图开发者密钥)
-})
-
-// 加载腾讯地图SDK
-const loadSDK = () => {
-  return new Promise((resolve, reject) => {
-    if (window.TMap) {
-      TMap.value = window.TMap
-      return resolve(window.TMap)
-    }
-    const script = document.createElement('script')
-    script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic&key=${tMapConfig.key}&callback=initTMap`
-    window.initTMap = () => {
-      TMap.value = window.TMap
-      resolve(window.TMap)
-    }
-    script.onerror = (err) => {
-      reject(`地图加载失败: ${err.message}`)
-      document.head.removeChild(script)
-    }
-    document.head.appendChild(script)
-  })
-}
-
-// 初始化断面数据(直接嵌入你的Excel数据)
-const initData = () => {
-  const rawData = [
-    { "断面编号": 0, "所属河流": "浈江", "断面位置": "小古录", "所属区县": "始兴县", "经度": 114.208543, "纬度": 25.059851, "Cd(ug/L)": 0.11 },
-    { "断面编号": 1, "所属河流": "浈江", "断面位置": "长坝", "所属区县": "仁化县", "经度": 113.692874, "纬度": 24.874845, "Cd(ug/L)": 1.116 },
-    { "断面编号": 2, "所属河流": "浈江", "断面位置": "东河桥", "所属区县": "浈江区", "经度": 113.601631, "纬度": 24.80784, "Cd(ug/L)": 3.46 },
-    { "断面编号": 3, "所属河流": "武江", "断面位置": "坪石", "所属区县": "乐昌市", "经度": 113.066281, "纬度": 25.274421, "Cd(ug/L)": 0.98 },
-    { "断面编号": 4, "所属河流": "武江", "断面位置": "乐昌", "所属区县": "乐昌市", "经度": 113.338782, "纬度": 25.129212, "Cd(ug/L)": 0.11 },
-    { "断面编号": 5, "所属河流": "武江", "断面位置": "武江桥", "所属区县": "乐昌市", "经度": 113.349815, "纬度": 25.120278, "Cd(ug/L)": 0.15 },
-    { "断面编号": 6, "所属河流": "北江", "断面位置": "九公里", "所属区县": "浈江区", "经度": 113.580758, "纬度": 24.761299, "Cd(ug/L)": 7.83 },
-    { "断面编号": 7, "所属河流": "北江", "断面位置": "白土", "所属区县": "曲江区", "经度": 113.531284, "纬度": 24.679958, "Cd(ug/L)": 5.94 },
-    { "断面编号": 8, "所属河流": "浈江", "断面位置": "昆仑水站", "所属区县": "南雄市", "经度": 114.3629285, "纬度": 25.10053746, "Cd(ug/L)": 0.517 },
-    { "断面编号": 9, "所属河流": "北江", "断面位置": "白沙", "所属区县": "曲江", "经度": 113.5707136, "纬度": 24.58139261, "Cd(ug/L)": 1.54 },
-    { "断面编号": 10, "所属河流": "浈江", "断面位置": "周田水站", "所属区县": "仁化县", "经度": 113.8293461, "纬度": 24.97851516, "Cd(ug/L)": 0.182 },
-    { "断面编号": 11, "所属河流": "武江", "断面位置": "坪石水站", "所属区县": "乐昌市", "经度": 113.0467854, "纬度": 25.28883459, "Cd(ug/L)": 1.071 }
-  ];
-
-  // 处理坐标(WGS84转GCJ02,腾讯地图用GCJ02)
-  state.excelData = rawData.map(item => {
-    const lng = Number(item.经度);
-    const lat = Number(item.纬度);
-    if (isNaN(lat) || isNaN(lng)) {
-      console.error('无效经纬度:', item);
-      return null;
-    }
-    const [gcjLng, gcjLat] = wgs84togcj02(lng, lat); // 坐标转换
-    return {
-      id: item.断面编号,
-      river: item.所属河流,
-      location: item.断面位置,
-      district: item.所属区县,
-      cdValue: item["Cd(ug/L)"],
-      latitude: gcjLat,
-      longitude: gcjLng,
-    };
-  }).filter(item => item !== null);
-}
-
-// 初始化地图
-const initMap = async () => {
-  try {
-    await loadSDK()
-    // 创建地图实例(中心设为数据区域:粤北)
-    map = new TMap.value.Map(mapContainer.value, {
-      center: new TMap.value.LatLng(25.2, 114), //前大往下,后大往左
-      zoom: 9.8,
-      minZoom: 9.8,
-      maxZoom: 14,
-    })
-    // 创建标记图层
-    markersLayer = new TMap.value.MultiMarker({
-      map: map,
-      styles: {
-        default: new TMap.value.MarkerStyle({
-          width: 30,
-          height: 30,
-          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xMiAyTDIgMjJoMjBMMTIgMnoiIGZpbGw9IiMxRTg4RTUiLz48L3N2Zz4='        
-        })
-      }
-    });
-    // 绑定标记点击事件
-    markersLayer.on('click', handleMarkerClick);
-    // 加载数据并渲染标记
-    initData();
-    updateMarkers();
-    isMapReady.value = true;
-  } catch (err) {
-    console.error('地图初始化失败:', err);
-    error.value = err.message;
-  }
-}
-
-// 更新标记点
-const updateMarkers = () => {
-  const geometries = state.excelData.map(item => ({
-    id: String(item.id), // 统一转字符串,避免类型错误
-    styleId: 'default',
-    position: new TMap.value.LatLng(item.latitude, item.longitude),
-    properties: {
-      title: item.location, // 断面位置作为标题
-    }
-  }));
-  markersLayer.setGeometries(geometries);
-}
-
-// 标记点击事件(直接用本地数据)
-const handleMarkerClick = (e) => {
-  const marker = e.geometry;
-  if (!marker) return;
-  // 查找本地数据
-  const markerId = String(marker.id);
-  const matchedData = state.excelData.find(item => String(item.id) === markerId);
-  if (!matchedData) {
-    console.error('未找到数据:', markerId);
-    return;
-  }
-  // 构建信息窗口内容
-  const content = `
-    <div class="water-info-window">
-      <h3 class="info-title">断面编号:${matchedData.id}</h3>
-      <div class="info-content">
-        <div class="info-row">
-          <span class="info-label">所属河流:</span>
-          <span class="info-value">${matchedData.river}</span>
-        </div>
-
-        <div class="info-row">
-          <span class="info-label">断面位置:</span>
-          <span class="info-value">${matchedData.location}</span> 
-        </div>
-
-        <div class="info-row">
-          <span class="info-label">所属区县:</span>
-          <span class="info-value">${matchedData.district}</span>
-        </div>
-        
-        <div class="info-row">
-          <span class="info-label">Cd含量:</span>
-          <span class="info-value">${matchedData.cdValue} ug/L</span>
-        </div>
-      
-      </div>
-    </div>
-  `;
-  // 关闭之前的信息窗口
-  if (infoWindow.value) {
-    infoWindow.value.close();
-  }
-  // 打开新信息窗口
-  infoWindow.value = new TMap.value.InfoWindow({
-    map: map,
-    position: marker.position,
-    content,
-    offset: { x: 0, y: -32 } // 向上偏移,避免遮挡标记
-  });
-  infoWindow.value.open();
-}
-
-// 生命周期
-onMounted(async () => {
-  try {
-    await loadSDK();
-    await initMap();
-q    //监听窗口大小变化,调整图表
-    window.addEventListener('resize',()=>{
-      if(chart){
-        chart.resize();
-      }
-    })
-  } catch (err) {
-    error.value = err.message;
-  }
-})
-
-onBeforeUnmount(() => {
-  if (markersLayer) markersLayer.setMap(null); // 销毁标记图层
-  if (infoWindow.value) infoWindow.value.close(); // 关闭信息窗口
-})
-</script>
-
-<style scoped>
-.map-page {
-  width: 100%;
-  margin: 0 auto 24px; /* 水平居中,底部间距24px */
-  background-color: white;
-  border-radius: 12px;
-  padding: 20px;
-  box-sizing: border-box;
-}
-
-/* 地图容器样式 */
-.map-container {
-  width: 100%;
-  height: 550px; /* 固定高度,确保地图显示完整 */
-  margin: 1rem auto;
-  border-radius: 12px;
-}
-
-/* 表格容器 */
-.table-page { 
-  width: 100%; 
-  background-color: rgba(255, 255, 255, 0.85);
-  border-radius: 12px;
-  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
-  overflow: hidden;
-  transition: transform 0.3s ease;
-}
-
-.table-page:hover {
-  transform: translateY(-3px);
-}
-
-.table-container {
-  padding: 20px;
-  flex: 1;
-  overflow: auto;
-}
-
-.table-title {
-  text-align: left;
-  margin: 0;
-  padding: 15px 15px 15px 24px;
-  position: relative;
-  font-size: 1.5rem;
-  font-weight: 600;
-  color: #1e88e5;
-  line-height: 1.2;
-  background-color: rgba(30, 136, 229, 0.08);
-  border-bottom: 1px solid rgba(30, 136, 229, 0.15);
-}
-
-/* 蓝色小方块 */
-.table-title::before {
-  content: "";
-  position: absolute;
-  left: 8px;
-  top: 50%;
-  transform: translateY(-50%);
-  width: 8px;
-  height: 8px;
-  background-color: #1e88e5;
-  border-radius: 50%;
-}
-
-.data-table {
-  width: 100%;
-  border-collapse: collapse;
-  min-width: 800px;
-  background-color: rgba(255, 255, 255, 0.7);
-}
-
-.data-table th,.data-table td {
-  padding: 12px 15px;
-  text-align: center;
-  border: 1px solid rgba(229, 231, 235, 0.7);
-}
-
-.data-table th {
-  background-color: rgba(243, 244, 246, 0.7);
-  font-weight: bold;
-  color: #1f2937;
-}
-
-.data-table tr:nth-child(even){
-  background-color: rgba(249, 250, 251, 0.7);
-}
-
-.data-table tr:hover{
-  background-color: rgba(243, 244, 246, 0.7);
-}
-
-/* 信息窗口核心调整 */
-:v-deep(.tmap-infowindow) {
-  padding: 20px !important;
-  min-width: 320px !important;
-  font-size: 1.25rem !important;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.2) !important;
-  border-radius: 8px !important;
-  background: rgba(255, 255, 255, 0.95) !important;
-  backdrop-filter: blur(4px);
-}
-
-.water-info-window {
-  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
-}
-
-.info-title {
-  background: linear-gradient(135deg, rgba(30, 136, 229, 0.85), rgba(21, 101, 192, 0.85));
-  color: white;
-  font-size: 1.15rem;
-  font-weight: 600;
-  padding: 16px 20px;
-  margin: 0;
-  position: relative;
-  letter-spacing: 0.5px;
-  border-radius: 6px 6px 0 0;
-}
-
-.info-title:after {
-  content: "";
-  position: absolute;
-  bottom: 0;
-  left: 20px;
-  right: 20px;
-  height: 1px;
-  background: linear-gradient(to right, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.15));
-}
-
-.info-row {
-  display: flex;
-  margin-bottom: 15px;
-  align-items: center;
-  position: relative;
-  padding-left: 20px;
-}
-
-.info-row:last-child {
-  margin-bottom: 0;
-}
-
-.info-row:before {
-  content: "";
-  position: absolute;
-  left: 0;
-  top: 50%;
-  transform: translateY(-50%);
-  width: 14px;
-  height: 14px;
-  border-radius: 50%;
-  background-color: #e3f2fd;
-  border: 2px solid #90caf9;
-}
-
-.info-row:nth-child(4):before {
-  background-color: #ffebee;
-  border-color: #ffcdd2;
-}
-
-.info-label {
-  flex: 0 0 100px;
-  color: #546e7a;
-  font-size: 0.95rem;
-  font-weight: 500;
-  text-align: right;
-  padding-right: 15px;
-  position: relative;
-}
-
-.info-label:after {
-  content: ":";
-  position: absolute;
-  right: 5px;
-}
-
-.info-value {
-  flex: 1;
-  color: #263238;
-  font-size: 1rem;
-  background: rgba(248, 249, 250, 0.7);
-  padding: 10px 15px;
-  border-radius: 6px;
-  border-left: 3px solid #64b5f6;
-  font-weight: 500;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
-  transition: all 0.3s ease;
-}
-
-.info-row:nth-child(4) .info-value {
-  color: #e53935;
-  border-left-color: #ef9a9a;
-  font-weight: 600;
-  position: relative;
-}
-
-.info-row:nth-child(4) .info-value:after {
-  content: "mg/L";
-  position: absolute;
-  right: 15px;
-  font-size: 0.85rem;
-  font-weight: normal;
-  color: #78909c;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
-  .map-page {
-    width: 96%; /* 小屏幕稍窄,避免边缘拥挤 */
-  }
-  .map-container {
-    height: 300px; /* 小屏幕缩短高度 */
-  }
-}
-</style>

+ 0 - 265
src/components/irrpollution/crosssectionmap.vue

@@ -1,265 +0,0 @@
-<template>
-  <div class="map-wrapper">
-    <div ref="mapContainer" class="map-container"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue';
-import L from 'leaflet';
-import 'leaflet/dist/leaflet.css';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-const mapContainer = ref(null);
-
-// 定义蓝色三角形标记(保持不变)
-const blueTriangle = L.divIcon({
-  className: 'custom-div-icon',
-  html: `<svg width="24" height="24" viewBox="0 0 24 24">
-          <path d="M12 2L2 22h20L12 2z" fill="#0066CC" stroke="#003366" stroke-width="2"/>
-        </svg>`,
-  iconSize: [24, 24],
-  iconAnchor: [12, 24]
-});
-
-onMounted(() => {
-  // 初始化地图(保持不变)
-  if (!mapContainer.value) {
-    console.error('❌ 地图容器未找到!');
-    return;
-  }
-
-  const map = L.map(mapContainer.value, {
-    center: [24.9, 114], // 韶关大致中心  前大往下,后大往左
-    zoom: 8.5,
-    minZoom: 8.3,
-  });
-
-  // 区县颜色映射 + 增强匹配(保持不变)
-  const districtColorMap = {
-    "武江区": "#FF6B6B",
-    "浈江区": "#4ECDC4",
-    "曲江区": "#FFD166",
-    "始兴县": "#A0DAA9",
-    "仁化县": "#6A0572",
-    "翁源县": "#1A535C",
-    "乳源瑶族自治县": "#FF9F1C",
-    "新丰县": "#87CEEB",
-    "乐昌市": "#118AB2",
-    "南雄市": "#06D6A0",
-    "韶关市": "#cccccc",
-  };
-
-  function getDistrictColor(name) {
-    if (districtColorMap[name]) return districtColorMap[name];
-    const normalizedName = name.replace(/市|县|区|自治县/g, '');
-    for (const key in districtColorMap) {
-      if (key.includes(normalizedName) || normalizedName.includes(key.replace(/市|县|区|自治县/g, ''))) {
-        return districtColorMap[key];
-      }
-    }
-    return '#cccccc';
-  }
-
-  // 加载区县边界(保持不变)http://localhost:8000/api/vector/boundary?table_name=counties&field_name=city_name&field_value=%E9%9F%B6%E5%85%B3%E5%B8%82
-  fetch('/data/韶关市各区县边界图.geojson')
-    .then(res => {
-      if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
-      return res.json();
-    })
-    .then(geojson => {
-      L.geoJSON(geojson, {
-        style: (feature) => {
-          const districtName = feature.properties.name || '';
-          const color = getDistrictColor(districtName);
-          return {
-            fillColor: color,
-            fillOpacity: 0.7,
-            color: '#333333',
-            weight: 2,
-          };
-        },
-      }).addTo(map);
-
-      // 加载水系图 + 新增接口数据加载(核心修改)
-      fetch('/data/韶关市河流水系图.geojson')
-        .then(res => {
-          if (!res.ok) throw new Error(`水系图加载失败:${res.status}`);
-          return res.json();
-        })
-        .then(waterGeojson => {
-          L.geoJSON(waterGeojson, {
-            style: {
-              color: '#0066CC',
-              weight: 2,
-              opacity: 0.8,
-            },
-          }).addTo(map);
-
-          // ========================
-          // 从接口加载数据(使用 api8000 实例)
-          // ========================
-          api8000.get('/api/vector/export/all?table_name=cross_section')
-            .then(response => {
-              const geoJSONData = response.data;
-              // 提取GeoJSON的features.properties作为数据项
-              const dataItems = geoJSONData.features.map(feature => feature.properties);
-              console.log('✅ 接口数据加载完成,要素数:', dataItems.length);
-
-              let markerCount = 0;
-              dataItems.forEach((item, idx) => {
-                try {
-                  // 字段映射(接口字段 → 原逻辑字段)
-                  const mappedItem = {
-                    "断面编号": item.id,
-                    "所属河流": item.river_name,
-                    "断面位置": item.position,
-                    "所属区县": item.county,
-                    "经度": item.longitude,
-                    "纬度": item.latitude,
-                    "Cd(ug/L)": item.cd_concentration
-                  };
-
-                  // 经纬度校验(保持原有逻辑)
-                  const lng = parseFloat(mappedItem.经度);
-                  const lat = parseFloat(mappedItem.纬度);
-                  if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
-                    console.warn(`❌ 坐标越界(第${idx}条):`, lat, lng, mappedItem);
-                    return;
-                  }
-
-                  // 创建标记(保持原有样式)
-                  const marker = L.marker([lat, lng], {
-                    icon: blueTriangle,
-                    zIndexOffset: 1000,
-                  }).addTo(map);
-
-                  // 镉含量格式化(保持原有逻辑)
-                  const cdValue = parseFloat(mappedItem["Cd(ug/L)"]);
-                  const formattedCd = isNaN(cdValue) ? '未知' : cdValue + ' μg/L';
-
-                  // 弹窗内容(保持原有结构)
-                  marker.bindPopup(`
-                    <div class="popup-container">
-                      <h3 class="popup-title">所属河流: ${mappedItem.所属河流}</h3>
-                      <div class="popup-divider"></div>
-                      <p><strong>断面编号:</strong> ${mappedItem.断面编号}</p>
-                      <p><strong>断面位置:</strong> ${mappedItem.断面位置}</p>
-                      <p><strong>所属区县:</strong> ${mappedItem.所属区县}</p>
-                      <p><strong>镉(Cd)含量:</strong> ${formattedCd}</p>
-                    </div>
-                  `);
-
-                  // 鼠标交互(保持原有逻辑)
-                  marker.on('mouseover', () => {
-                    marker.getElement().querySelector('svg').style.transform = 'scale(1.2)';
-                  }).on('mouseout', () => {
-                    marker.getElement().querySelector('svg').style.transform = 'scale(1)';
-                  });
-
-                  markerCount++;
-                } catch (err) {
-                  console.error(`❌ 处理第${idx}条数据失败:`, err);
-                }
-              });
-
-              console.log(`✅ 成功创建 ${markerCount} 个标记点`);
-            })
-            .catch(err => {
-              console.error('❌ 采样点数据加载失败:', err);
-              alert('采样点数据加载错误:' + err.message);
-            });
-          // ========================
-        })
-        .catch(err => {
-          console.error('❌ 水系图加载失败:', err);
-          alert('水系图加载错误:' + err.message);
-        });
-    })
-    .catch(err => {
-      console.error('❌ 区县边界加载失败:', err);
-      alert('区县边界加载错误:' + err.message);
-    });
-});
-</script>
-
-<style scoped>
-/* 原有样式保持不变 */
-.map-wrapper {
-  width: 100%;
-  height: 100%;
-  position: relative;
-}
-.map-container {
-  width: 100% !important;
-  height: 100% !important;
-}
-
-::v-deep .popup-title {
-  text-align: center;
-  font-size: 18px;
-  font-weight: 700;
-  color: #0066CC;
-  margin: 0 0 6px;
-  border-bottom: none;
-  padding-bottom: 8px;
-}
-
-::v-deep .popup-divider {
-  height: 1px;
-  background: #0066CC;
-  margin: 8px 0;
-}
-
-::v-deep .popup-container {
-  min-width: 240px;
-  max-width: 300px;
-  padding: 16px;
-  font-family: "Microsoft YaHei", sans-serif;
-}
-
-::v-deep .popup-container p {
-  margin: 6px 0;
-  font-size: 15px;
-  color: #666;
-  line-height: 1.6;
-}
-
-::v-deep .popup-container strong {
-  color: #0066CC;
-  font-weight: 600;
-}
-
-::v-deep .exceeding {
-  color: #FF3333;
-  font-weight: bold;
-}
-
-::v-deep .leaflet-popup-content-wrapper {
-  padding: 0 !important;
-  border-radius: 12px !important;
-  box-shadow: 0 6px 16px rgba(0,0,0,0.2) !important;
-}
-
-::v-deep .leaflet-popup-content {
-  margin: 0 !important;
-  width: auto !important;
-}
-
-::v-deep .leaflet-popup-tip {
-  display: none;
-}
-
-::v-deep .info {
-  padding: 6px 8px;
-  background: white;
-  background: rgba(255,255,255,0.9);
-  box-shadow: 0 0 15px rgba(0,0,0,0.2);
-  border-radius: 5px;
-}
-
-::v-deep .custom-div-icon svg {
-  transition: transform 0.2s;
-  display: block;
-}
-</style>

+ 0 - 280
src/components/irrpollution/irrwatermap.vue

@@ -1,280 +0,0 @@
-<template>
-  <div class="map-wrapper" @click.stop>
-    <div ref="mapContainer" class="map-container"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue';
-import L from 'leaflet';
-import 'leaflet/dist/leaflet.css';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-const mapContainer = ref(null);
-
-onMounted(() => {
-  // 初始化地图(强制确保容器可用)
-  if (!mapContainer.value) {
-    console.error('❌ 地图容器未找到!');
-    return;
-  }
-
-  // 定义位置格式化函数(处理"广东省韶关市"前缀)
-  const formatLocation = (fullLocation) => {
-    if (!fullLocation) return '未知位置'; // 处理空值
-    // 移除前缀并清理空格
-    const processed = fullLocation.replace(/^(广东省)?韶关市/, '').trim();
-    // 处理移除后为空的情况
-    return processed || '未知位置';
-  };
-
-  const map = L.map(mapContainer.value, {
-    center: [25, 114], // 韶关大致中心  前大往下,后大往左
-    zoom: 8.5,
-    minZoom: 8.3,
-  });
-
-  // 区县颜色映射(与GeoJSON的properties.name严格匹配)
-  const districtColorMap = {
-    "武江区": "#FF6B6B",
-    "浈江区": "#4ECDC4",
-    "曲江区": "#FFD166",
-    "始兴县": "#A0DAA9",
-    "仁化县": "#6A0572",
-    "翁源县": "#1A535C",
-    "乳源瑶族自治县": "#FF9F1C",
-    "新丰县": "#87CEEB",
-    "乐昌市": "#118AB2",
-    "南雄市": "#06D6A0",
-  };
-
-  // 加载区县边界(带完整错误处理)http://localhost:8000/api/vector/boundary?table_name=counties&field_name=city_name&field_value=%E9%9F%B6%E5%85%B3%E5%B8%82
-  fetch('/data/韶关市各区县边界图.geojson')
-    .then(res => {
-      if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
-      return res.json();
-    })
-    .then(geojson => {
-      console.log('✅ 区县边界数据加载完成,要素数:', geojson.features.length);
-      
-      L.geoJSON(geojson, {
-        style: (feature) => {
-          const districtName = feature.properties.name; // 匹配GeoJSON的name字段
-          const color = districtColorMap[districtName] || '#cccccc';
-          return {
-            fillColor: color,
-            fillOpacity: 0.7,
-            color: '#333333', // 边界颜色
-            weight: 2,        // 边界宽度
-          };
-        },
-      }).addTo(map);
-
-      // 加载水系图(新增,带样式和错误处理)
-      fetch('/data/韶关市河流水系图.geojson')
-        .then(res => {
-          if (!res.ok) throw new Error(`水系图加载失败:${res.status}`);
-          return res.json();
-        })
-        .then(waterGeojson => {
-          console.log('✅ 水系图数据加载完成,要素数:', waterGeojson.features.length);
-          
-          L.geoJSON(waterGeojson, {
-            style: {
-              color: '#0066CC', // 水系颜色
-              weight: 2,       // 线条宽度
-              opacity: 0.8,    // 透明度
-            },
-          }).addTo(map);
-
-          // ========================
-          // 修复核心:加载采样+检测数据(使用 api8000 实例)
-          // ========================
-          api8000.get('/api/vector/export/all?table_name=water_sampling_data')
-            .then(response => {
-              const geoJSONData = response.data;
-              console.log('✅ 采样数据加载完成,要素数:', geoJSONData.features.length);
-                
-              let markerCount = 0;
-              geoJSONData.features.forEach((feature, idx) => {
-                const props = feature.properties; // 单个要素的完整属性(采样+检测)
-                try {
-                  // 智能提取经纬度字段(支持多种可能的字段名)
-                  const latField = ['latitude', 'lat', 'Latitude', 'Lat'].find(key => props[key] !== undefined);
-                  const lngField = ['longitude', 'lng', 'Longitude', 'Lng'].find(key => props[key] !== undefined);
-                  
-                  if (!latField || !lngField) {
-                    console.error(`❌ 未找到经纬度字段(第${idx}条):`, props);
-                    return;
-                  }
-                  
-                  // 清理并转换经纬度(处理特殊字符和逗号)
-                  const cleanLat = String(props[latField]).replace(/[^\d.-]/g, '');
-                  const cleanLng = String(props[lngField]).replace(/[^\d.-]/g, '');
-                  
-                  // 强制四舍五入到6位小数(避免精度问题)
-                  const lat = parseFloat(parseFloat(cleanLat).toFixed(6));
-                  const lng = parseFloat(parseFloat(cleanLng).toFixed(6));
-                  
-                  // 范围校验(扩大范围10%,兼容边界值)
-                  if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
-                    console.warn(`❌ 坐标超出合理范围(第${idx}条):`, lat, lng, props);
-                    return;
-                  }
-                  
-                  // 创建标记点(使用 L.circleMarker 而非 L.marker)
-                  const marker = L.circleMarker([lat, lng], {
-                    radius: 4,                  // 增大圆点半径,确保可见
-                    color: '#FF3333',           // 边框颜色(红色)
-                    fillColor: '#FF3333',       // 填充颜色(红色)
-                    fillOpacity: 0.9,           // 填充透明度(接近不透明)
-                    weight: 2,                  // 边框宽度(加粗)
-                    zIndexOffset: 1000,         // 提高层级,确保在所有图层之上
-                  }).addTo(map);
-
-                  // 弹窗内容:直接从 props 提取检测数据(匹配实际字段名)
-                  marker.bindPopup(`
-                    <div class="popup-container">
-                      <h3 class="popup-title">${formatLocation(props.sampling_location)}</h3>
-                      <div class="popup-divider"></div> <!-- 分隔线 -->
-                      <table class="popup-table">
-                        <thead>
-                          <tr>
-                            <th>检测项</th>
-                            <th>数值</th>
-                          </tr>
-                        </thead>
-                        <tbody>
-                          <tr><td>Ph</td><td>${props.ph_value || '未知'}</td></tr>
-                          <tr><td>铬(Cr)(ug/L)</td><td>${props.cr_concentration.toFixed(6)|| '未知'}</td></tr>
-                          <tr><td>砷(As)(ug/L)</td><td>${props.as_concentration.toFixed(6) || '未知'}</td></tr>
-                          <tr><td>镉(Cd)(ug/L)</td><td>${props.cd_concentration.toFixed(6) || '未知'}</td></tr>
-                          <tr><td>汞(Hg)(ug/L)</td><td>${props.hg_concentration.toFixed(6) || '未知'}</td></tr>
-                          <tr><td>铅(Pb)(ug/L)</td><td>${props.pb_concentration.toFixed(6) || '未知'}</td></tr>
-                        </tbody>
-                      </table>
-                    </div>
-                  `);
-                  
-                  markerCount++;
-                } catch (err) {
-                  console.error(`❌ 处理采样点失败(第${idx}条):`, err);
-                }
-              });
-
-              console.log(`✅ 成功创建 ${markerCount} 个标记点`);
-            })
-            .catch(err => {
-              console.error('❌ 采样/检测数据加载失败:', err);
-              alert('数据接口错误:' + err.message);
-            });
-          // ========================
-          // 水系图加载完成后的逻辑结束
-          // ========================
-        })
-        .catch(err => {
-          console.error('❌ 水系图加载失败:', err);
-          alert('水系图加载错误:' + err.message);
-        });
-    })
-    .catch(err => {
-      console.error('❌ 区县边界加载失败:', err);
-      alert('区县边界加载错误:' + err.message);
-    });
-});
-</script>
-
-<style scoped>
-.map-wrapper {
-  width:100%;
-  height: 100%;
-  position: relative;
-  z-index: 100;
-}
-.map-container {
-  width: 100% !important;
-  height: 100% !important;
-}
-
-/*  标题和分隔线 */
-::v-deep .popup-title {
-  text-align: center;       /* 居中 */
-  font-size: 16px;          /* 减小字号 */
-  font-weight: 700;         /* 加粗 */
-  color: #0066CC;           /* 蓝色,匹配设计 */
-  margin: 0 0 4px;          /* 间距调整 */
-  border-bottom: 2px solid #0066CC; /* 底部横线 */
-  padding-bottom: 4px;      /* 横线与文字间距 */
-}
-
-::v-deep .popup-divider {
-  height: 1px;              /* 横线高度 */
-  background: #0066CC;      /* 横线颜色 */
-  margin: 6px 0;            /* 上下间距 */
-}
-
-/*  表格样式 */
-::v-deep .popup-table {
-  width: 100%;              /* 占满容器 */
-  border-collapse: collapse;/* 合并边框 */
-  margin-top: 12px;         /* 与段落间距 */
-}
-
-::v-deep .popup-table th,
-::v-deep .popup-table td {
-  border: 1px solid #CCCCCC;/* 单元格边框 */
-  padding: 4px 6px;         /* 内边距 */
-  text-align: center;       /* 内容居中 */
-  font-size: 12px;          /* 字号调整 */
-}
-
-::v-deep .popup-table th {
-  background: #F5F5F5;      /* 表头背景色 */
-  font-weight: 600;         /* 表头加粗 */
-}
-
-/* 美化弹窗(完整层级穿透) */
-::v-deep .leaflet-popup-content-wrapper {
-  padding: 0 !important;
-  border-radius: 12px !important;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
-}
-
-::v-deep .leaflet-popup-content {
-  margin: 0 !important;
-  width: auto !important;
-  max-width: 220px !important;
-}
-
-::v-deep .popup-container {
-  min-width: 180px;
-  max-width: 220px;
-  padding: 10px;
-  font-family: "Microsoft YaHei", sans-serif;
-}
-
-::v-deep .popup-content p {
-  margin: 6px 0;
-  font-size: 15px;
-  color: #666;
-  line-height: 1.6;
-}
-
-::v-deep .popup-content strong {
-  color: #FF3333; /* 与标记点颜色呼应 */
-  font-weight: 600;
-}
-
-/* 可选:隐藏弹窗箭头,更像卡片 */
-::v-deep .leaflet-popup-tip {
-  display: none;
-}
-
-/* 临时调试:确保标记点可见 */
-::v-deep .leaflet-marker-icon {
-  display: none !important; /* 隐藏默认标记图标 */
-}
-::v-deep .leaflet-circle-marker {
-  stroke-width: 2px !important;
-}
-</style>

+ 0 - 87
src/components/irrpollution/riverwaterassay.vue

@@ -1,87 +0,0 @@
-<template>
-  <div class="map-container">
-    <div id="water-system-map"></div>
-  </div>
-</template>
-
-<script setup>
-//水系图的转换
-import 'leaflet/dist/leaflet.css';
-import { onMounted, onUnmounted } from 'vue';
-import L from 'leaflet';
-
-let map; // 声明为全局变量,避免被Vue垃圾回收
-
-onMounted(() => {
-  // 初始化地图容器尺寸
-  const mapContainer = document.getElementById('water-system-map');
-  mapContainer.style.width = '100%';
-  mapContainer.style.height = '600px';
-
-  // 初始化地图(经纬度、缩放级别可根据GeoJSON数据调整)
-  const map = L.map('water-system-map').setView([24.88, 113.62], 9);
-
-
-  // 加载GeoJSON
-  fetch('/data/韶关市河流水系图.geojson')
-    .then(res => {
-      if (!res.ok) {
-        throw new Error('GeoJSON加载失败');
-      }
-      return res.json();
-    })
-    .then(geojson => {
-      // 添加水系样式(可自定义颜色、宽度)
-      L.geoJSON(geojson, {
-        style: {
-          color: '#0066cc',    // 蓝色线条
-          weight: 2,           // 线条宽度
-          opacity: 0.8,        // 透明度
-          lineJoin: 'round'    // 拐角圆润
-        },
-        // 可选:添加鼠标悬停效果
-        onEachFeature(feature, layer) {
-          layer.on('mouseover', function() {
-            this.setStyle({ color: '#ff3300', weight: 3 }); // 悬停变红加粗
-          });
-          layer.on('mouseout', function() {
-            this.setStyle({ color: '#0066cc', weight: 2 }); // 离开恢复
-          });
-        }
-      }).addTo(map);
-    })
-    .catch(err => {
-      console.error('加载GeoJSON失败:', err);
-      alert('水系图加载失败,请检查文件路径');
-    });
-
-  // 监听窗口Resize,适配地图尺寸
-  window.addEventListener('resize', handleResize);
-});
-
-onUnmounted(() => {
-  // 组件销毁时移除事件监听,避免内存泄漏
-  window.removeEventListener('resize', handleResize);
-  if (map) {
-    map.remove();
-    map = null;
-  }
-});
-
-// 窗口Resize处理函数
-function handleResize() {
-  if (map) {
-    map.invalidateSize();
-  }
-}
-</script>
-
-<style scoped>
-.map-container {
-  width: 100%;
-  height: 600px; /* 确保父容器有高度 */
-}
-.leaflet-default-icon-path {
-  background-image: url('https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png');
-}
-</style>

+ 0 - 1015
src/components/irrpollution/tencentMapView.vue

@@ -1,1015 +0,0 @@
-<template>
-  <div class="map-page">
-    <div ref="mapContainer" 
-     class="map-container"
-    ></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
-import axios from 'axios'
-import {wgs84togcj02} from 'coordtransform';
-const farmlandLayer = ref(null);
-const isMapReady = ref(false)
-const mapContainer = ref(null)
-const error = ref(null)
-const TMap = ref(null);
-let districtBoundaryLayer = null;
-let activeTempMarker = ref(null)
-let infoWindow = ref(null)
-let map = null
-let markersLayer = null
-let soilTypeVectorLayer=null;//土壤类型多边形图层
-let waterSystemLayer = null;
-const state = reactive({
-  showOverlay: false,
-  showSoilTypes: true,
-  showSurveyData: true,
-  showWaterSystem:true,
-  excelData: [],//标记点数据
-  lastTapTime: 0
-})
-
-
-const tMapConfig = reactive({
-  key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
-  geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
-})
-
-
-
-let sdkLoaded = false; // 新增:标记 SDK 是否已加载
-const loadSDK = () => {
-  return new Promise((resolve, reject) => {
-    if (sdkLoaded) { // 已加载则直接返回
-      resolve(window.TMap);
-      return;
-    }
-    // 移除旧脚本(避免重复加载)
-    const oldScript = document.querySelector('script[src*="map.qq.com"]');
-    if (oldScript) oldScript.remove();
-    
-    const script = document.createElement('script');
-    script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`;
-    
-    window.initTMap = () => {
-      sdkLoaded = true; // 标记为已加载
-      if (!window.TMap?.service?.Geocoder) {
-        reject(new Error('地图SDK加载失败'));
-        return;
-      }
-      TMap.value = window.TMap;
-      resolve(window.TMap);
-    };
-    
-    script.onerror = (err) => {
-      reject(`地图资源加载失败: ${err.message}`);
-      document.head.removeChild(script);
-    };
-    document.head.appendChild(script);
-  });
-};
-
-const WATER_SAMPLING_API='https://www.soilgd.com:3000/table/Water_sampling_data';
-const fetchWaterSamplingData = async ()=>{
-  try{
-    const response = await axios.get(WATER_SAMPLING_API);
-    return response.data.data || response.data;
-  }catch(err){
-    console.error("接口请求失败:",err);
-    throw new Error(`获取水样数据失败:${err.message || '网络错误'}`)
-  }
-};
-
-const initData =async ()=>{
-  try{
-    const rawData = await fetchWaterSamplingData();
-    if(!Array.isArray(rawData)){
-      throw new Error('接口返回数据格式错误');
-    }
-
-    state.excelData = rawData.map(item=>{
-      const lat=Number(item.latitude);
-      const lng=Number(item.longitude);
-
-      if(isNaN(lat)||isNaN(lng)){
-        console.error('无效经纬度数据',item);
-        return null;
-      }
-
-      return{
-        ...item,
-        latitude:lat,
-        longitude:lng,
-      };
-    }).filter(item=>item !==null)
-    console.log(`成功加载${state.excelData.length}条有效数据`);
-  }catch(err){
-    console.error('数据初始化失败:',err);
-    error.value = err.message;
-    state.excelData=[];
-  }
-}
-
-// 初始化地图
-const initMap = async () => {
-  try {
-    if (map) {
-      map.destroy();
-      map = null;
-    }
-    await loadSDK()
-    //console.log('开始创建地图实例');
-    
-    map = new TMap.value.Map(mapContainer.value, {
-      center: new TMap.value.LatLng(24.25,114.5),//前大往下,后大往左
-      zoom:9,
-      zoomControl:true,
-      renderOptions: {
-        preserveDrawingBuffer: true, // 必须开启以支持截图
-        antialias: true
-      },
-    })
-    //console.log('地图实例创建成功,开始创建markersLayer');
-    //console.log('当前地图样式ID:', map.getMapStyleId());
-     if (markersLayer) {
-    markersLayer.setMap(null);
-    markersLayer = null;
-  }
-    // 创建标记点向量图层
-  markersLayer = new TMap.value.MultiMarker({
-  map: map,
-  zIndex:1000,
-  styles: {
-    default: new TMap.value.MarkerStyle({
-      width: 15, // 图标宽度
-      height: 15, // 图标高度
-      anchor: { x: 12.5, y: 12.5 }, // 居中定位
-      src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCI+PGNpcmNsZSBjeD0iMTUiIGN5PSIxNSIgcj0iMTAiIGZpbGw9InJlZCIvPjwvc3ZnPg=='
-      })
-    }
-  });
-    console.log('markersLayer是否绑定地图:',markersLayer.getMap() === map);
-    
-
-    // 创建土壤类型多边形图层
-    soilTypeVectorLayer = new TMap.value.MultiPolygon({
-      map: map,
-      styles: {
-        default: new TMap.value.PolygonStyle({
-          fillColor: '#cccccc',
-          fillOpacity: 0.4,
-          strokeColor: '#333',
-          strokeWidth: 1
-        })
-      }    
-    });
-    
-  if (typeof handleMarkerClick === 'function' && markersLayer) {
-     markersLayer.on('click', handleMarkerClick); 
-     console.log('[地图] 标记点点击事件绑定成功');
-  }
-    await initData()
-    updateMarkers()
-    // 在updateMarkers()后执行
-   // console.log(markersLayer.getStyles());
-    //console.log(document.querySelector('.tmap-marker img')); 
-    
-
-     // 6. 绑定事件
-     
-    //console.log('地图实例创建完成,开始加载水系图');
-    //加载区县边界
-    await loadDistrictBoundaries();
-    await loadWaterSystemGeoJSON(); // 等待水系图加载完成
-
-    // 标记地图就绪
-    isMapReady.value = true;
-    //console.log('地图初始化完成(含水系图)');
-
-
-    // 新增地图就绪状态监听
-    map.on('idle', () => {
-      isMapReady.value = true;
-      //console.log('地图初始化完成');
-      //console.log('标记点图层初始化:',markersLayer.value);
-    })
-  } catch (err) {
-    isMapReady.value = true;
-    console.error('initMap执行异常:',err);
-    error.value = err.message
-  }
-}
-
-// 加载水系 GeoJSON 并添加到地图
-const loadWaterSystemGeoJSON = async () => {
-  try {
-    const url = `${window.location.origin}/data/韶关市河流水系图.geojson`;
-    console.log('加载水系:', url);
-    
-    const response = await fetch(url);
-    
-    // 检查响应状态
-    if (!response.ok) {
-      const errorText = await response.text();
-      throw new Error(`HTTP错误 ${response.status}: ${errorText.substring(0, 100)}`);
-    }
-    
-    const geojson = await response.json();
-    console.log('成功加载水系GeoJSON:', geojson.features.length, '个要素');
-    
-    // 创建腾讯地图可用的坐标转换函数
-    const processCoordinates = (coords) => {
-      const [gcjLng, gcjLat] = wgs84togcj02(coords[0], coords[1]);
-      return new TMap.value.LatLng(gcjLat, gcjLng);
-    };
-
-    // 销毁旧图层
-    if (waterSystemLayer) {
-      waterSystemLayer.setMap(null);
-      waterSystemLayer = null;
-    }
-
-    // 创建水系图层
-    waterSystemLayer = new TMap.value.MultiPolyline({
-      map: map,
-      styles: {
-        default: new TMap.value.PolylineStyle({
-          color: '#0066cc',
-          width: 2,
-          opacity: 0.8,
-          lineCap: 'round',
-          lineJoin: 'round'
-        })
-      },
-      geometries: geojson.features
-        .filter(feature => {
-          return feature.geometry.type === 'LineString' || 
-                 feature.geometry.type === 'MultiLineString';
-        })
-        .map(feature => {
-          let paths = [];
-          
-          if (feature.geometry.type === 'LineString') {
-            paths = feature.geometry.coordinates.map(processCoordinates);
-          } else {
-            paths = feature.geometry.coordinates.map(line => 
-              line.map(processCoordinates)
-            );
-          }
-          
-          return {
-            id: feature.id || `water_${Date.now()}`,
-            styleId: 'default',
-            paths: paths,
-            properties: feature.properties
-          };
-        })
-    });
-    console.log('水系图层加载完成');
-
-  } catch (err) {
-    console.error('水系加载失败:', err.message);
-    error.value = `水系图加载失败: ${err.message}`;
-  }
-};
-
-
-// 加载区县边界数据
-const loadDistrictBoundaries = async () => {
-  try {
-    const url = '/data/韶关市各区县边界图.geojson';
-    console.log('加载区县边界:', url);
-    
-    const response = await fetch(url);
-    
-    // 打印响应状态和头信息
-    console.log('HTTP状态码:', response.status);
-    console.log('内容类型:', response.headers.get('content-type'));
-    console.log('内容长度:', response.headers.get('content-length'));
-    
-    // 检查响应状态
-    if (!response.ok) {
-      const errorText = await response.text();
-      throw new Error(`HTTP错误 ${response.status}: ${errorText.substring(0, 100)}`);
-    }
-    
-    const geojson = await response.json();
-    console.log('成功加载区县GeoJSON:', geojson.features.length, '个要素');
-
-    // 1. 定义颜色数组(顺序与 geojson.features 中的区县顺序一致)
-    const districtColorMap = {
-      '武江区': '#FF6B6B',
-      '浈江区': '#4ECDC4',
-      '曲江区': '#FFD166',
-      '始兴县': '#A0DAA9',
-      '仁化县': '#6A0572',
-      '翁源县': '#1A535C',
-      '乳源瑶族自治县': '#FF9F1C', // 修正:匹配 GeoJSON 的“乳源瑶族自治县”
-      '新丰县': '#87CEEB', // 新增:为“新丰县”配置颜色(可自定义)
-      '乐昌市': '#118AB2',
-      '南雄市': '#06D6A0'
-    };
-
-    // 2. 处理几何数据:为每个区县分配 styleId(用索引,与颜色数组对应)
-    const geometries = geojson.features.map(feature => {
-      const districtName = feature.properties.name;
-      console.log('GEOJSON中的区县名称',districtName);
-      
-      const color = districtColorMap[districtName] ||'#ccc';
-      // 坐标转换(WGS84 → GCJ02,确保边界在正确位置)
-      let paths = [];
-      if (feature.geometry.type === 'Polygon') {
-        paths = feature.geometry.coordinates.map(ring => 
-          ring.map(coord => {
-            const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
-            return new TMap.value.LatLng(gcjLat, gcjLng);
-          })
-        );
-      } else if (feature.geometry.type === 'MultiPolygon') {
-        paths = feature.geometry.coordinates.map(polygon => 
-          polygon.map(ring => 
-            ring.map(coord => {
-              const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
-              return new TMap.value.LatLng(gcjLat, gcjLng);
-            })
-          )
-        );
-      }
-
-      // 关键:styleId 设为索引(与颜色数组索引对应)
-      return {
-        id: `district-${districtName}`,
-        styleId: `style-${districtName}`,
-        paths:paths
-      };
-    });
-
-    districtColorMap['武江区'] = '#FF0000'; // 强制武江区为红色
-    // 3. 构建样式对象(key 与 styleId 一致)
-    const styles = {};
-    for(const name in districtColorMap){
-      styles[`style-${name}`]=new TMap.value.PolygonStyle({
-        fillColor:districtColorMap[name],
-        fillOpacity:0.7,
-        strokeColor:'#333',
-        strokeWidth:2
-      })
-       console.log(`区县${name}的样式颜色:`, styles[`style-${name}`].getFillColor());
-    }
-
-    // 4. 创建图层并应用样式
-    districtBoundaryLayer = new TMap.value.MultiPolygon({
-      map: map,
-      geometries: geometries,
-      styles:styles
-    });
-    console.log('区县样式对象:', styles);
-    districtBoundaryLayer.setStyles(styles); // 样式生效
-    
-  } catch (err) {
-    console.error('加载区县边界失败:', err.message);
-    error.value = `区县边界加载失败: ${err.message}`;
-  }
-};
-
-
-// 更新标记点,添加Label显示
-const updateMarkers = () => {
-  // 正确的标记点创建方式
-  const geometries = state.excelData.map(item => {
-   // console.log(`'原始ID:'"${item.water_sample_ID}"`);
-    //console.log(`坐标验证:lat=${item.latitude},lng=${item.longitude}`);
-    
-    return {
-      id: item.water_sample_ID,
-      styleId: 'default',
-      position: new TMap.value.LatLng( item.latitude,item.longitude),
-      properties: {
-        title: item.sampling_location,
-        sampler_id:item.water_sample_ID,
-      }
-    };
-  })
-  
-  // 一次性设置所有标记
-  markersLayer.setGeometries(geometries);
-};
-
-const API_BASE_URL = 'https://www.soilgd.com:3000/table/Water_assay_data'; 
-
-// 新增Marker点击事件处理
-const handleMarkerClick = async(e) => {
-  //console.log('点击事件已发生');
-  
-  const marker = e.geometry;
-  const markerId=marker.id.trim();
-
-  if (!marker) {
-    //console.error('未获取到标记点对象');
-    return;
-  }
-
-  // 关闭之前的信息窗口
-  if (infoWindow.value) {
-    infoWindow.value.close();
-    infoWindow.value=null;
-  }
-   // 显示加载中的信息窗口
-  infoWindow.value = new TMap.value.InfoWindow({
-    map: map,
-    position: marker.position,
-    content: '<div style="padding:12px;text-align:center">加载数据中...</div>',
-    // offset: { x: 0, y: -32 }
-  });
-  infoWindow.value.open();
-
-  try {
-    // 调试信息:显示当前点击的标记点ID
-    //console.log('点击标记点ID:', markerId);
-    //console.log('请求URL:', `${API_BASE_URL}?water_sample_ID=eq.${markerId}`);
-    
-    // 调用API获取水质数据 - 使用 markerId 而不是 marker.id
-    const response = await axios.get(API_BASE_URL, {
-      params: {
-        water_sample_ID: `eq.${markerId}`
-      },
-      timeout: 5000
-    });
-    
-    //console.log('API响应数据:', response.data);
-
-    // 关键:手动筛选出 water_sample_ID 匹配的第一条数据
-  const matchedData = response.data.find(item => 
-    item.water_sample_ID.trim() === markerId
-  );
-
-  if (!matchedData) {
-    throw new Error(`未找到采样点 ${markerId} 的监测数据`);
-  }
-
-    // 获取第一条数据
-    const apiData = matchedData;
-    
-    // 调试信息:显示获取到的数据ID
-    //console.log('获取到的水质数据ID:', apiData.water_sample_ID);
-    
-    // 创建信息窗口内容 - 使用 marker.properties.title 确保显示正确位置
-    const content = `
-  <div class="water-info-window">
-    <!-- 标题区 -->
-    <h3 class="info-title">${marker.properties.title}</h3>
-    
-    <!-- 基础信息区 -->
-    <div class="info-row">
-      <span class="info-label">采样点ID:</span>
-      <span class="info-value">${apiData.water_sample_ID}</span>
-    </div>
-    <div class="info-row">
-      <span class="info-label">样本编号:</span>
-      <span class="info-value">${apiData.sample_code || '无'}</span>
-    </div>
-    <div class="info-row">
-      <span class="info-label">pH值:</span>
-      <span class="info-value">${apiData.pH}</span>
-    </div>
-    
-    <!-- 分隔线 -->
-    <div class="divider"></div>
-    
-    <!-- 重金属区 -->
-    <h4 class="contaminant-title">重金属含量 (ug/L)</h4>
-    <div class="contaminant-grid">
-      <div class="contaminant-item">
-        <span class="contaminant-name">Cr:</span>
-        <span class="contaminant-value">${apiData.Cr}</span>
-      </div>
-      <div class="contaminant-item">
-        <span class="contaminant-name">As:</span>
-        <span class="contaminant-value">${apiData.As}</span>
-      </div>
-      <div class="contaminant-item">
-        <span class="contaminant-name">Cd:</span>
-        <span class="contaminant-value">${apiData.Cd}</span>
-      </div>
-      <div class="contaminant-item">
-        <span class="contaminant-name">Hg:</span>
-        <span class="contaminant-value">${apiData.Hg}</span>
-      </div>
-      <div class="contaminant-item">
-        <span class="contaminant-name">Pb:</span>
-        <span class="contaminant-value">${apiData.Pb}</span>
-      </div>
-    </div>
-  </div>
-`;
-    
-    // 更新信息窗口
-    infoWindow.value.setContent(content);
-    
-  } catch (error) {
-    console.error('API请求失败:', error);
-    
-    // 显示错误信息
-    const errorContent = `
-      <div style="padding:12px;color:red">
-        <h3>${marker.properties.title}</h3>
-        <p>获取数据失败: ${error.message}</p>
-        <p>尝试获取的ID: ${markerId}</p>
-      </div>
-    `;
-    
-    infoWindow.value.setContent(errorContent);
-  }
-}
-
-
-
-onMounted(async () => {
-  //console.log('开始执行 onMounted');
-  
-  try {
-    await loadSDK();
-    //console.log('SDK加载完成,开始initData');
-    await initMap()
-    //console.log('initMap执行完毕');
-    
-  } catch (err) {
-    console.error('onMounted执行异常',err);
-    error.value = err.message
-  }
-})
-
-onBeforeUnmount(() => {
-  // 1. 销毁地图实例(先销毁,再置空)
-  if (map) {
-    try {
-      map.destroy(); // 腾讯地图销毁方法
-      console.log('[地图] 地图实例已销毁');
-    } catch (e) {
-      console.error('[地图] 销毁失败:', e);
-    }
-    map = null;
-  }
-
-  // 2. 销毁图层(逐个检查)
-  const layers = [markersLayer, soilTypeVectorLayer, waterSystemLayer];
-  layers.forEach(layer => {
-    if (layer) {
-      try {
-        layer.setMap(null); // 从地图移除
-        if (layer.destroy) layer.destroy(); // 调用图层销毁方法
-      } catch (e) {
-        console.error('[地图] 图层销毁失败:', e);
-      }
-    }
-  });
-
-  // 3. 清理全局变量
-  if (window.initTMap) {
-    delete window.initTMap; // 移除全局回调
-  }
-});
-
-
-onUpdated(() => {
-  try {
-    if (map.value && farmlandLayer.value) {
-      // 更新地图视图
-    }
-  } catch (error) {
-    console.error("地图更新错误:", error);
-  }
-});
-
-</script>
-
-<style scoped>
-.map-page {
-  position: relative;
-  width: 100vw;
-  height: 100vh;
-}
-
-.map-container {
-  width: 100%;
-  height: 100vh ;
-  min-height: 600px;
-  pointer-events: all;
-}
-
-.control-panel {
-  position: fixed;
-  top: 24px;
-  right: 24px;
-  background: rgba(255, 255, 255, 0.95);
-  padding: 16px;
-  border-radius: 12px;
-  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
-  backdrop-filter: blur(8px);
-  border: 1px solid rgba(255, 255, 255, 0.2);
-  z-index: 1000;
-  min-width: 240px;
-  transition: all 0.3s ease;
-}
-
-.control-panel:hover {
-  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
-  transform: translateY(-2px);
-}
-
-.control-panel label {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  padding: 8px 12px;
-  border-radius: 8px;
-  transition: background 0.2s ease;
-  cursor: pointer;
-}
-
-.control-panel label:hover {
-  background: rgba(56, 118, 255, 0.05);
-}
-
-.control-panel input[type="checkbox"] {
-  width: 18px;
-  height: 18px;
-  border: 2px solid #3876ff;
-  border-radius: 4px;
-  appearance: none;
-  cursor: pointer;
-  transition: all 0.2s ease;
-}
-
-.control-panel input[type="checkbox"]:checked {
-  background: #3876ff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E") no-repeat center;
-  background-size: 12px;
-}
-
-.export-controls {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-  margin-top: 16px;
-}
-
-.export-controls button {
-  padding: 10px 16px;
-  font-size: 14px;
-  font-weight: 500;
-  border: none;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: all 0.2s ease;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  background: #3876ff;
-  color: white;
-}
-
-.export-controls button:disabled {
-  background: #e0e0e0;
-  color: #9e9e9e;
-  cursor: not-allowed;
-  opacity: 0.8;
-}
-
-.export-controls button:not(:disabled):hover {
-  background: #2b5dc5;
-  box-shadow: 0 4px 12px rgba(56, 118, 255, 0.3);
-}
-
-/* 新增加载动画 */
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-.loading-spinner {
-  width: 18px;
-  height: 18px;
-  border: 2px solid rgba(255, 255, 255, 0.3);
-  border-top-color: white;
-  border-radius: 50%;
-  animation: spin 0.8s linear infinite;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
-  .control-panel {
-    top: 16px;
-    right: 16px;
-    left: 16px;
-    width: auto;
-    min-width: auto;
-  }
-  
-  .export-controls {
-    flex-direction: row;
-    flex-wrap: wrap;
-  }
-  
-  .export-controls button {
-    flex: 1;
-    justify-content: center;
-  }
-}
-
-.polygon-info {
-  padding: 12px;
-  max-width: 300px;
-  
-  h3 {
-    margin: 0 0 8px;
-    color: #333;
-    font-size: 16px;
-  }
-
-  table {
-    width: 100%;
-    border-collapse: collapse;
-
-    tr {
-      border-bottom: 1px solid #eee;
-    }
-
-    th, td {
-      padding: 6px 4px;
-      text-align: left;
-      font-size: 14px;
-    }
-
-    th {
-      color: #666;
-      white-space: nowrap;
-      padding-right: 8px;
-    }
-  }
-}
-.point-info {
-  padding: 12px;
-  min-width: 200px;
-  
-  h3 {
-    margin: 0 0 8px;
-    font-size: 14px;
-    color: white;
-    padding: 4px 8px;
-    border-radius: 4px;
-    display: inline-block;
-    background: var(--category-color);
-  }
-  
-  p {
-    margin: 6px 0;
-    font-size: 13px;
-    line-height: 1.4;
-    
-    &:last-child {
-      margin-bottom: 0;
-    }
-  }
-}
-
-
-.tooltip {
-  position: absolute;
-  padding: 8px 12px;
-  background: rgba(255, 255, 255, 0.9);
-  border-radius: 6px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
-  z-index: 1001;
-  font-size: 14px;
-  white-space: nowrap;
-  opacity: 0;
-  transform: translateY(10px);
-  visibility: hidden;
-  transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
-  border: 1px solid #e0e0e0;
-}
-
-.tooltip.visible {
-  opacity: 1;
-  transform: translateY(0);
-  visibility: visible;
-}
-
-.tooltip::after {
-  content: "";
-  position: absolute;
-  width: 0;
-  height: 0;
-  border-left: 6px solid transparent;
-  border-right: 6px solid transparent;
-  top: 100%;
-  left: 50%;
-  transform: translateX(-50%);
-  border-top: 6px solid rgba(255, 255, 255, 0.9);
-  border-top-color: inherit;
-}
-
-:deep(.tmap-vector-label) {
-  white-space: nowrap;
-  pointer-events: none; /* 允许点击穿透,不影响地图交互 */
-}
-
-/* 在style标签中添加以下样式 */
-:deep(.tmap-infowindow) {
-  padding: 12px;
-  min-width: 300px;
-  border-radius: 8px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
-  background-color: white;
-}
-
-.db-info {
-  margin-top: 10px;
-  padding: 10px;
-  background-color: #f8f9fa;
-  border-left: 3px solid #4285f4;
-  border-radius: 4px;
-}
-
-.db-info h4 {
-  margin-top: 0;
-  color: #4285f4;
-  font-size: 14px;
-}
-
-.db-info pre {
-  margin: 5px 0 0;
-  font-size: 12px;
-  white-space: pre-wrap;
-  word-break: break-word;
-}
-
-.water-info-window {
-  font-family: 'Segoe UI', Tahoma, sans-serif;
-  background: #fff;
-  border-radius: 4px;
-  padding: 4px;
-  width: 200px;
-  height:auto;
-  border: 1px solid #e2e8f0;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-  font-size: 0.7rem; /* 基础字体大小调整为0.7rem(约11px) */
-}
-
-.info-title {
-  color: #1e40af;
-  font-size: 0.8rem;
-  margin: 0 0 3px 0;
-  padding-bottom: 2px;
-  border-bottom: 1px solid #e0f2fe;
-  font-weight: 600;
-  text-align: center;
-}
-
-.info-content {
-  padding: 2px;
-}
-
-.info-row {
-  display: flex;
-  margin-bottom: 2px;
-  align-items: center;
-}
-
-.info-label {
-  flex: 0 0 60px; /* 标签宽度调整为60px */
-  color: #475569;
-  font-weight: 500;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.info-value {
-  flex: 1;
-  color: #0f172a;
-  padding: 1px 3px;
-  background: #f8fafc;
-  border-radius: 2px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  font-size: 0.7rem;
-}
-
-.contaminant-section {
-  margin-top: 3px;
-  padding-top: 3px;
-  border-top: 1px dotted #e2e8f0;
-}
-
-.contaminant-title {
-  color: #1e40af;
-  margin: 0 0 2px 0;
-  font-size: 0.7rem;
-  font-weight: 500;
-  padding-left: 2px;
-}
-
-/* 污染物改为网格布局,每行3个 */
-.contaminants {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 2px;
-}
-
-.contaminant-item {
-  background: #f8fafc;
-  border-radius: 2px;
-  padding: 2px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  border: 0.5px solid #e2e8f0;
-}
-
-.contaminant-name {
-  color: #3b82f6;
-  font-weight: 500;
-  font-size: 0.7rem;
-  white-space: nowrap;
-  max-width: 100%;
-}
-
-.contaminant-value {
-  color: #0f172a;
-  font-size: 0.8rem;
-  background: #e2e8f0;
-  padding: 1px 2px;
-  border-radius: 2px;
-  margin-top: 1px;
-  min-width: 25px;
-  text-align: center;
-}
-  .assay-info {
-    margin-top: 16px;
-    padding: 8px 12px;
-    background-color: #f5f5f5;
-    border-radius: 6px;
-    font-size: 0.85rem;
-    color: #666;
-    text-align: center;
-  }
-
-  /* 在style标签中添加 */
-.crystal-bubble .bubble {
-  width: 24px;
-  height: 24px;
-  border-radius: 50%;
-  background: radial-gradient(circle at 30% 30%, #00b4ff, #0077cc);
-  box-shadow: 
-    0 0 10px rgba(0, 183, 255, 0.7),
-    inset 0 0 15px rgba(0, 100, 200, 0.5);
-  position: relative;
-  animation: pulse 1.5s infinite;
-}
-
-.crystal-bubble .water-drop {
-  position: absolute;
-  width: 10px;
-  height: 10px;
-  background: rgba(255, 255, 255, 0.85);
-  border-radius: 50%;
-  top: 25%;
-  left: 25%;
-  box-shadow: 
-    0 0 5px #fff,
-    inset 0 0 3px rgba(0, 0, 0, 0.2);
-  transform: rotate(-20deg);
-}
-
-@keyframes pulse {
-  0% { transform: scale(1); opacity: 0.8; }
-  50% { transform: scale(1.1); opacity: 1; }
-  100% { transform: scale(1); opacity: 0.8; }
-}
-
-/* 区县边界样式 */
-.district-boundary {
-  stroke: #333;
-  stroke-width: 1px;
-  fill-opacity: 0.6;
-  transition: fill-opacity 0.3s;
-}
-
-.district-boundary:hover {
-  fill-opacity: 0.8;
-  stroke-width: 2px;
-}
-
-.district-label {
-  font-size: 14px;
-  font-weight: bold;
-  text-anchor: middle;
-  pointer-events: none;
-  fill: #333;
-  text-shadow: 0 0 3px white, 0 0 3px white, 0 0 3px white;
-}
-</style>

+ 0 - 222
src/components/irrpollution/waterassaydata1.vue

@@ -1,222 +0,0 @@
-<template>
-  <div class="boxplot-container">
-    <div ref="chartRef" style="width: 100%; height: 500px;"></div>
-  </div>
-</template>
-<!--各种重金属的箱图-->
-<script setup lang="ts">
-import * as echarts from 'echarts';
-import { ref, onMounted, onUnmounted } from 'vue';
-import axios from 'axios';
-
-// 明确定义数据类型
-interface HeavyMetalData {
-  sampleId: string;
-  Cr: number | null;
-  As: number | null;
-  Cd: number | null;
-  Hg: number | null;
-  Pb: number | null;
-}
-
-const METALS = ['Cr', 'As', 'Cd', 'Hg', 'Pb'] as const;
-type MetalType = typeof METALS[number];
-
-const METAL_LABELS: Record<MetalType, string> = {
-  Cr: '铬(Cr)',
-  As: '砷(As)',
-  Cd: '镉(Cd)',
-  Hg: '汞(Hg)',
-  Pb: '铅(Pb)'
-};
-
-// 图表变量
-const chartRef = ref<HTMLElement | null>(null);
-const chartInstance = ref<echarts.ECharts | null>(null);
-const metalData = ref<HeavyMetalData[]>([]);
-let resizeHandler: (() => void) | null = null; // 用于存储resize处理函数
-
-// 数据清洗函数
-const cleanData = (rawValue: any): number | null => {
-  if (typeof rawValue === 'string') {
-    const num = parseFloat(rawValue);
-    return isNaN(num) || num < 0 ? null : num;
-  }
-  return typeof rawValue === 'number' && rawValue >= 0 ? rawValue : null;
-};
-
-// 修复后的四分位数计算算法
-const calculateBoxplotStats = (values: number[]): [number, number, number, number, number] | null => {
-  if (values.length < 5) return null; // 至少需要5个数据点才能生成有效的箱线图
-  
-  // 升序排序
-  const sorted = [...values].sort((a, b) => a - b);
-  const n = sorted.length;
-
-  // 正确的分位位置计算
-  const quantile = (p: number) => {
-    const pos = (n + 1) * p;
-    const lowerIndex = Math.max(0, Math.min(n - 1, Math.floor(pos) - 1));
-    const fraction = pos - Math.floor(pos);
-    
-    if (lowerIndex >= n - 1) return sorted[n - 1];
-    return sorted[lowerIndex] + fraction * (sorted[lowerIndex + 1] - sorted[lowerIndex]);
-  };
-
-  return [
-    sorted[0],               // 最小值
-    quantile(0.25),          // Q1
-    quantile(0.5),           // 中位数
-    quantile(0.75),          // Q3
-    sorted[n - 1]            // 最大值
-  ];
-};
-
-// 渲染图表
-const renderBoxplot = () => {
-  if (!chartRef.value || metalData.value.length === 0) return;
-  
-  // 移除旧的resize监听器
-  if (resizeHandler) {
-    window.removeEventListener('resize', resizeHandler);
-  }
-
-  // 分组收集每种金属的有效数值
-  const metalValues = Object.fromEntries(
-    METALS.map(metal => [
-      metal, 
-      metalData.value
-        .map(item => item[metal])
-        .filter((val): val is number => val !== null)
-    ])
-  ) as Record<MetalType, number[]>;
-
-  // 准备箱线图数据
-  const validBoxplotData: ([number, number, number, number, number] | null)[] = 
-    METALS.map(metal => calculateBoxplotStats(metalValues[metal]));
-
-  // ECharts配置
-  const option: echarts.EChartsOption = {
-    backgroundColor: '#FFFFFF',
-    title: {
-      text: '重金属浓度分布箱线图',
-      left: 'center',
-      textStyle: { color: '#333', fontSize: 16 }
-    },
-    tooltip: {
-      trigger: 'item',
-      formatter: (params: any) => {
-        const metalIndex = params.dataIndex;
-        const metal = METALS[metalIndex];
-        const stats = validBoxplotData[metalIndex];
-        
-        // 处理空数据情况(修复图片中的null错误)
-        if (stats === null || stats[0] === null) {
-          return `<span style="color:#ff0000">${METAL_LABELS[metal]}数据不足,无法生成统计值</span>`;
-        }
-        
-        // 类型安全解构(确保所有值都是number类型)
-        const [min, q1, median, q3, max] = stats;
-        
-        return `
-          <b>${METAL_LABELS[metal]}</b><br/>
-          最小值: ${min.toFixed(4)} mg/L<br/>
-          下四分位: ${q1.toFixed(4)} mg/L<br/>
-          中位数: ${median.toFixed(4)} mg/L<br/>
-          上四分位: ${q3.toFixed(4)} mg/L<br/>
-          最大值: ${max.toFixed(4)} mg/L
-        `;
-      }
-    },
-    xAxis: {
-      type: 'category',
-      data: METALS.map(metal => METAL_LABELS[metal]),
-      axisLabel: { color: '#333', interval: 0 }
-    },
-    yAxis: {
-      type: 'value',
-      name: '浓度(mg/L)',
-      nameTextStyle: { color: '#333' },
-      axisLabel: { 
-        color: '#333',
-        formatter: (value: number) => value.toFixed(4)
-      }
-    },
-    series: [{
-      type: 'boxplot',
-      // 过滤无效数据(解决ts 2322错误)
-      data: validBoxplotData.filter(arr => arr !== null) as [number, number, number, number, number][],
-      itemStyle: {
-        color: '#4285F4',
-        borderWidth: 1.5
-      },
-      emphasis: {
-        itemStyle: {
-          borderColor: '#333',
-          borderWidth: 2
-        }
-      }
-    }]
-  };
-
-  // 初始化图表
-  if (chartInstance.value) {
-    chartInstance.value.dispose();
-  }
-  chartInstance.value = echarts.init(chartRef.value);
-  chartInstance.value.setOption(option);
-  
-  // 响应式处理
-  resizeHandler = () => chartInstance.value?.resize();
-  window.addEventListener('resize', resizeHandler);
-};
-
-// 数据加载
-const loadData = async () => {
-  try {
-    const response = await axios.get<any[]>(
-      'https://www.soilgd.com:3000/table/Water_assay_data',
-      { timeout: 5000 }
-    );
-    
-    // 数据转换与过滤
-    metalData.value = response.data
-      .map(item => ({
-        sampleId: String(item.sampleId),
-        Cr: cleanData(item.Cr),
-        As: cleanData(item.As),
-        Cd: cleanData(item.Cd),
-        Hg: cleanData(item.Hg),
-        Pb: cleanData(item.Pb)
-      }))
-      // 修复:允许部分有效数据
-      .filter(item => METALS.some(metal => item[metal] !== null));
-    
-    renderBoxplot();
-  } catch (error) {
-    console.error('数据加载失败:', error);
-    alert('数据加载错误,请查看控制台日志');
-  }
-};
-
-onMounted(() => loadData());
-onUnmounted(() => {
-  // 清理资源
-  if (resizeHandler) {
-    window.removeEventListener('resize', resizeHandler);
-  }
-  chartInstance.value?.dispose();
-});
-</script>
-
-<style scoped>
-.boxplot-container {
-  width: 100%;
-  max-width: 1000px;
-  margin: 20px auto;
-  padding: 20px;
-  background: white;
-  border-radius: 8px;
-  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
-}
-</style>

+ 0 - 348
src/components/irrpollution/waterassaydata2.vue

@@ -1,348 +0,0 @@
-<template>
-  <div class="region-average-chart">
-    <div ref="chartRef" class="chart-box"></div>
-    <div v-if="loading" class="status">数据加载中...</div>
-    <div v-else-if="error" class="status error">{{ error }}</div>
-  </div>
-</template>
-<!--各地区的重金属平均值得柱状图-->
-<script setup>
-import { ref, onMounted, onUnmounted } from 'vue';
-import * as echarts from 'echarts';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// ========== 接口配置(使用 api8000 实例) ==========
-const TABLE_NAME = 'water_sampling_data';
-
-// ========== 配置项(调整字段适配新接口) ==========
-// 排除的非重金属字段(适配新接口字段名)
-const EXCLUDE_FIELDS = [
-  'id', 'sample_code', 'assayer_id', 'assay_time', 
-  'assay_instrument_model', 'sample_number', 'ph_value',
-  'latitude', 'longitude', 'sampling_location', 'sampling_time' // 新增采样相关非检测字段
-];
-const COLORS = ['#ff4d4f99', '#1890ff', '#ffd700',  '#52c41a88', '#722ed199' ];
-
-// 韶关市下属行政区划白名单(保持不变)
-const SG_REGIONS = [
-  '浈江区', '武江区', '曲江区', '乐昌市', 
-  '南雄市', '始兴县', '仁化县', '翁源县', 
-  '新丰县', '乳源瑶族自治县'
-];
-
-// ========== 响应式数据 ==========
-const chartRef = ref(null);
-const loading = ref(true);
-const error = ref('');
-let myChart = null;
-
-// ========== 地区提取函数(保持不变) ==========
-const extractRegion = (location) => {
-  if (!location || typeof location !== 'string') return null;
-
-  // 1. 精确匹配官方区县名称
-  const officialMatch = SG_REGIONS.find(region => 
-    location.includes(region)
-  );
-  if (officialMatch) return officialMatch;
-
-  // 2. 处理嵌套格式(如"韶关市-浈江区")
-  const nestedMatch = location.match(/(韶关市)([^市]+?[区市县])/);
-  if (nestedMatch && nestedMatch[2]) {
-    const region = nestedMatch[2].replace("韶关市", "").trim();
-    const validRegion = SG_REGIONS.find(r => r.includes(region));
-    if (validRegion) return validRegion;
-  }
-
-  // 3. 特殊格式处理(如"韶关市浈江区")
-  const shortMatch = location.match(/韶关市([区市县][^市]{2,5})/);
-  if (shortMatch && shortMatch[1]) return shortMatch[1];
-
-  // 4. 修正常见拼写错误
-  if (location.includes('乐昌')) return '乐昌市';
-  if (location.includes('乳源')) return '乳源瑶族自治县';
-
-  console.warn(`⚠️ 未识别地区: ${location}`);
-  return '未知区县';
-};
-
-// ========== 数据处理流程(适配新接口单数据源) ==========
-const processData = (allData) => {
-  // 1. 构建采样点ID到区县的映射(sample_number对应新接口的水样ID)
-  const regionMap = new Map();
-  allData.forEach(item => {
-    const region = extractRegion(item.sampling_location || '');
-    if (region && region !== '未知区县' && item.sample_number) {
-      regionMap.set(item.sample_number, region);
-    }
-  });
-
-  // 2. 关联重金属数据与区县(单条数据已包含所有信息)
-  const mergedData = allData.map(item => ({
-    ...item,
-    // 通过sample_number关联区县
-    region: regionMap.get(item.sample_number) || '未知区县'
-  }));
-
-  // 3. 识别重金属字段(新接口字段如cr_concentration、as_concentration等)
-  const metals = Object.keys(mergedData[0] || {})
-    .filter(key => 
-      !EXCLUDE_FIELDS.includes(key) &&  // 排除非重金属字段
-      !isNaN(parseFloat(mergedData[0][key])) &&  // 确保是数值
-      key.includes('concentration')  // 新接口重金属字段含concentration
-    );
-
-  // 4. 按区县分组统计
-  const regionGroups = {};
-  const cityWideAverages = {}; // 全市平均值
-  const uniqueSampleIds = new Set();
-  
-  // 初始化统计计数器
-  metals.forEach(metal => {
-    cityWideAverages[metal] = { sum: 0, count: 0 };
-  });
-  
-  mergedData.forEach(item => {
-    const region = item.region;
-    if (item.sample_number) {
-      uniqueSampleIds.add(item.sample_number);
-    }
-
-    // 初始化区县分组
-    if (!regionGroups[region]) {
-      regionGroups[region] = {};
-      metals.forEach(metal => {
-        regionGroups[region][metal] = { sum: 0, count: 0 };
-      });
-    }
-
-    // 统计各重金属含量
-    metals.forEach(metal => {
-      const val = parseFloat(item[metal]);
-      if (!isNaN(val)) {
-        // 区县统计
-        regionGroups[region][metal].sum += val;
-        regionGroups[region][metal].count++;
-        
-        // 全市统计
-        cityWideAverages[metal].sum += val;
-        cityWideAverages[metal].count++;
-      }
-    });
-  });
-  
-  const totalSamples = uniqueSampleIds.size;
-
-  // 5. 按官方顺序排序区县
-  const regions = SG_REGIONS.filter(region => regionGroups[region]);
-  
-  // 6. 添加"全市平均"作为最后一个类别
-  regions.push("全市平均");
-
-  // 7. 构建ECharts数据(处理重金属字段名显示)
-  const series = metals.map((metal, idx) => {
-    // 格式化重金属名称(如cr_concentration → Cr)
-    const prefix = metal.split('_')[0]; // 先获取前缀
-    const metalName = prefix 
-    ? prefix[0].toUpperCase() + prefix.slice(1) // 首字母大写 + 剩余字符
-    : ''; // 处理空字符串情况 
-    
-    // 计算全市平均值
-    const cityWideAvg = cityWideAverages[metal].count 
-      ? (cityWideAverages[metal].sum / cityWideAverages[metal].count).toFixed(2) 
-      : 0;
-    
-    return {
-      name: metalName, // 显示简化名称(如Cr、As)
-      type: 'bar',
-      data: regions.map(region => {
-        if (region === "全市平均") {
-          return cityWideAvg;
-        }
-        const group = regionGroups[region][metal];
-        return group.count ? (group.sum / group.count).toFixed(2) : 0;
-      }),
-      itemStyle: { 
-        color: COLORS[idx % COLORS.length],
-      },
-      label: {
-        show: true,
-        position: 'top',
-        fontSize: 15,
-        color: '#333',
-      }
-    };
-  });
-
-  return { regions, series, totalSamples };
-};
-
-// ========== ECharts 初始化(保持不变) ==========
-const initChart = ({ regions, series, totalSamples }) => {
-  if (!chartRef.value) return;
-  if (myChart) myChart.dispose();
-
-  myChart = echarts.init(chartRef.value);
-  const option = {
-    title: { 
-      text: '各地区重金属含量平均值',
-      left: 'center',
-      subtext: `数据来源: ${totalSamples}个有效检测样本`,
-      subtextStyle: {
-        fontSize: 15
-      }
-    },
-    tooltip: { 
-      trigger: 'axis',
-      formatter: params => {
-        const regionName = params[0].name;
-        const isCityWide = regionName === "全市平均";
-        
-        let content = `${isCityWide ? "全市平均值" : regionName}:`;
-        if (isCityWide) {
-          content += `<br><span style="color: #666;">(基于${totalSamples}个样本计算)</span>`;
-        }
-        
-        return content + params.map(p => `<br>${p.seriesName}: ${p.value} ug/L`).join('');
-      },
-      textStyle: {
-        fontSize: 15
-      }
-    },
-    xAxis: {
-      type: 'category',
-      data: regions,
-      axisLabel: { 
-        rotate: 45,
-        formatter: val => val.replace('韶关市', ''),
-        fontSize: 15
-      }
-    },
-    yAxis: { 
-      type: 'value', 
-      name: '浓度(ug/L)',
-      nameTextStyle: {
-        fontSize: 15,
-      },
-      axisLabel: {
-        fontSize: 15,
-      }
-    },
-    dataZoom: [{
-      type: 'inside',
-      start: 0,
-      end: 100
-    }],
-    series,
-    legend: { //图例
-      data: series.map(s => s.name), 
-      top:'10%',
-      right:'5%',
-      textStyle: {
-        fontSize: 15
-      }
-    },
-    grid: { //整个图的位置
-      left: '3%', 
-      right: '3%', 
-      bottom: 0, 
-      containLabel: true 
-    },
-  };
-
-  myChart.setOption(option);
-};
-
-// ========== 生命周期钩子(使用 api8000 实例) ==========
-onMounted(async () => {
-  try {
-    loading.value = true;
-    error.value = '';
-    
-    // 使用 api8000 实例获取数据
-    const response = await api8000.get(`/api/vector/export/all?table_name=${TABLE_NAME}`);
-    
-    // 处理可能的字符串响应
-    let data = response.data;
-    if (typeof data === 'string') {
-      try {
-        // 替换 NaN 为 null
-        const cleanedData = data.replace(/\bNaN\b/g, 'null');
-        data = JSON.parse(cleanedData);
-      } catch (parseErr) {
-        throw new Error('接口返回的是字符串,但 JSON 解析失败');
-      }
-    }
-    
-    // 处理对象中的 NaN 值
-    if (typeof data === 'object' && data !== null) {
-      const replaceNaN = (obj) => {
-        for (const key in obj) {
-          if (typeof obj[key] === 'object' && obj[key] !== null) {
-            replaceNaN(obj[key]);
-          } else if (typeof obj[key] === 'number' && isNaN(obj[key])) {
-            obj[key] = null;
-          } else if (obj[key] === 'NaN') {
-            obj[key] = null;
-          }
-        }
-      };
-      replaceNaN(data);
-    }
-    
-    // 接口返回格式判断(GeoJSON或直接数组)
-    const allData = data.features 
-      ? data.features.map(f => f.properties) 
-      : data;
-    
-    // 处理数据并初始化图表
-    initChart(processData(allData));
-  } catch (err) {
-    error.value = '数据加载失败: ' + (err.message || '未知错误');
-    console.error('接口错误:', err);
-  } finally {
-    loading.value = false;
-  }
-});
-
-// 响应式布局(保持不变)
-const resizeHandler = () => myChart && myChart.resize();
-onMounted(() => window.addEventListener('resize', resizeHandler));
-onUnmounted(() => {
-  window.removeEventListener('resize', resizeHandler);
-  if (myChart) myChart.dispose();
-});
-</script>
-
-<style scoped>
-.region-average-chart {
-  width: 100%;
-  height: 100%;
-  max-width: 1200px;
-  margin: 0 auto;
-  position: relative;
-}
-.chart-box {
-  width: 100%;
-  height: 100%;
-  min-height: 400px;
-  background-color: white;
-  border-radius: 8px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-}
-.status {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  padding: 15px;
-  background: rgba(255,255,255,0.8);
-  border-radius: 4px;
-  font-size: 16px;
-  z-index: 10;
-}
-.error { 
-  color: #ff4d4f;
-  font-weight: bold;
-}
-</style>

+ 0 - 197
src/components/irrpollution/waterassaydata3.vue

@@ -1,197 +0,0 @@
-<template>
-  <div class="heavy-metal-radar">
-    <h2 class="chart-title">重金属指标雷达图分析</h2>
-    <canvas ref="chartRef" class="chart-box"></canvas>
-    <div v-if="loading" class="status">数据加载中...</div>
-    <div v-else-if="error" class="status error">{{ error }}</div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, onUnmounted } from 'vue';
-import Chart from 'chart.js/auto';
-import axios from 'axios';
-
-// ========== 接口配置(和柱状图对齐) ==========
-const ASSAY_API = 'https://www.soilgd.com:3000/table/Water_assay_data'; // 复用柱状图的接口
-
-// ========== 配置项(模仿柱状图) ==========
-const EXCLUDE_FIELDS = [
-  'water_assay_ID', 'sample_code', 'assayer_ID', 'assay_time', 
-  'assay_instrument_model', 'water_sample_ID', 'pH'
-];
-const COLORS = ['#165DFF', '#36CFC9', '#722ED1']; // 雷达图三色
-
-// ========== 响应式数据 ==========
-const chartRef = ref(null);
-const loading = ref(true);
-const error = ref('');
-let radarChart = null;
-
-// ========== 数据处理:提取重金属指标 + 统计计算 ==========
-const processRadarData = (assayData) => {
-  // 1. 提取重金属字段(排除指定字段,且为数值类型)
-  const metals = Object.keys(assayData[0] || {})
-    .filter(key => !EXCLUDE_FIELDS.includes(key) && !isNaN(parseFloat(assayData[0][key])));
-
-  // 2. 计算每个重金属的统计值(均值、中位数、标准差)
-  const stats = metals.map(metal => {
-    const values = assayData.map(item => parseFloat(item[metal])).filter(v => !isNaN(v));
-    return {
-      mean: calculateMean(values),
-      median: calculateMedian(values),
-      std: calculateStdDev(values)
-    };
-  });
-
-  return { metals, stats };
-};
-
-// ========== 统计工具函数 ==========
-const calculateMean = (values) => {
-  if (values.length === 0) return 0;
-  return values.reduce((sum, val) => sum + val, 0) / values.length;
-};
-
-const calculateMedian = (values) => {
-  if (values.length === 0) return 0;
-  const sorted = [...values].sort((a, b) => a - b);
-  const mid = Math.floor(sorted.length / 2);
-  return sorted.length % 2 === 0 
-    ? (sorted[mid - 1] + sorted[mid]) / 2 
-    : sorted[mid];
-};
-
-const calculateStdDev = (values) => {
-  if (values.length <= 1) return 0;
-  const mean = calculateMean(values);
-  const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
-  return Math.sqrt(variance);
-};
-
-// ========== 初始化雷达图(Chart.js) ==========
-const initRadarChart = ({ metals, stats }) => {
-  if (!chartRef.value) return;
-  if (radarChart) radarChart.destroy();
-
-  const ctx = chartRef.value.getContext('2d');
-  radarChart = new Chart(ctx, {
-    type: 'radar',
-    data: {
-      labels: metals,
-      datasets: [
-        {
-          label: '均值',
-          data: stats.map(s => s.mean.toFixed(2)),
-          borderColor: COLORS[0],
-          backgroundColor: 'rgba(22, 93, 255, 0.1)',
-          pointRadius: 4,
-          borderWidth: 2
-        },
-        {
-          label: '中位数',
-          data: stats.map(s => s.median.toFixed(2)),
-          borderColor: COLORS[1],
-          backgroundColor: 'rgba(54, 207, 201, 0.1)',
-          pointRadius: 4,
-          borderWidth: 2
-        },
-        {
-          label: '标准差',
-          data: stats.map(s => s.std.toFixed(2)),
-          borderColor: COLORS[2],
-          backgroundColor: 'rgba(114, 46, 209, 0.1)',
-          pointRadius: 4,
-          borderWidth: 2
-        }
-      ]
-    },
-    options: {
-      responsive: true,
-      maintainAspectRatio: false,
-      scales: {
-        r: {
-          beginAtZero: true,
-          ticks: { display: false },
-          pointLabels: { font: { size: 12, weight: 'bold' } },
-          grid: { color: 'rgba(0,0,0,0.05)' },
-          angleLines: { color: 'rgba(0,0,0,0.1)' }
-        }
-      },
-      plugins: {
-        legend: { position: 'bottom' },
-        tooltip: {
-          callbacks: {
-            label: (ctx) => `${ctx.dataset.label}: ${ctx.raw} mg/L`
-          }
-        }
-      }
-    }
-  });
-};
-
-// ========== 生命周期钩子(和柱状图对齐) ==========
-onMounted(async () => {
-  try {
-    // 【关键】和柱状图一样,axios 请求 **不携带凭证**(withCredentials: false,默认就是false)
-    const assayRes = await axios.get(ASSAY_API, { timeout: 10000, withCredentials:false });
-    const processed = processRadarData(assayRes.data);
-    
-    if (processed.metals.length === 0) {
-      throw new Error('未检测到有效重金属指标');
-    }
-    
-    initRadarChart(processed);
-  } catch (err) {
-    error.value = '数据加载失败: ' + (err.message || '未知错误');
-    console.error('接口错误:', err);
-  } finally {
-    loading.value = false;
-  }
-});
-
-// 响应式resize(模仿柱状图)
-const resizeHandler = () => radarChart && radarChart.resize();
-onMounted(() => window.addEventListener('resize', resizeHandler));
-onUnmounted(() => window.removeEventListener('resize', resizeHandler));
-</script>
-
-<style scoped>
-.heavy-metal-radar {
-  width: 100%;
-  max-width: 800px;
-  margin: 20px auto;
-  position: relative;
-  padding-top: 0;
-  background-color: white;
-  border-radius: 8px;
-}
-.chart-box {
-  width: 100%;
-  min-height: 350px;
-  max-height: 600px;
-  height: auto;
-  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
-}
-.chart-title {
-  text-align: center;   /* 水平居中 */
-  font-size: 18px;      /* 字体大小 */
-  font-weight: 600;     /* 加粗 */
-  color: #333;          /* 字体颜色 */
-  margin: 10px 0;     /* 底部间距,避免和图表贴紧 */
-}
-.status {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  padding: 15px;
-  background: rgba(255,255,255,0.8);
-  border-radius: 4px;
-  z-index: 10;
-}
-.error { 
-  color: #ff4d4f;
-  font-weight: bold;
-}
-</style>

+ 0 - 340
src/components/irrpollution/waterassaydata4.vue

@@ -1,340 +0,0 @@
-<template>
-  <div class="region-average-chart">
-    <!-- 重金属选择器 -->
-    <div class="metal-selector" v-if="!loading && !error && metals.length > 0">
-      <label for="metal-select">选择重金属:</label>
-      <select id="metal-select" v-model="selectedMetal">
-        <option v-for="metal in metals" :key="metal" :value="metal">{{ metal }}</option>
-      </select>
-    </div>
-    
-    <!-- 图表容器 -->
-    <div ref="chartRef" class="chart-box"></div>
-    
-    <!-- 状态信息 -->
-    <div v-if="loading" class="status">数据加载中...</div>
-    <div v-else-if="error" class="status error">{{ error }}</div>
-    <div v-else-if="metals.length === 0" class="status">没有有效的重金属数据</div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, onUnmounted, watch } from 'vue';
-import * as echarts from 'echarts';
-import axios from 'axios';
-
-// ========== 接口配置 ==========
-const SAMPLING_API = 'https://www.soilgd.com:3000/table/Water_sampling_data';
-const ASSAY_API = 'https://www.soilgd.com:3000/table/Water_assay_data';
-
-// ========== 配置项 ==========
-const EXCLUDE_FIELDS = [
-  'water_assay_ID', 'sample_code', 'assayer_ID', 'assay_time', 
-  'assay_instrument_model', 'water_sample_ID', 'pH'
-];
-const COLORS = [
-  '#FF6B6B', '#4ECDC4', '#FFD166', '#6A4C93', '#1982C4',
-  '#FF9F1C', '#2EC4B6', '#E71D36', '#3A86FF', '#FF006E'
-];
-
-// 韶关市下属行政区划
-const SG_REGIONS = [
-  '浈江区', '武江区', '曲江区', '乐昌市', 
-  '南雄市', '始兴县', '仁化县', '翁源县', 
-  '新丰县', '乳源瑶族自治县'
-];
-
-// ========== 响应式数据 ==========
-const chartRef = ref(null);
-const loading = ref(true);
-const error = ref('');
-// 修复 selectedMetal 未定义问题:提前声明变量
-const selectedMetal = ref(''); 
-const metals = ref([]);
-const chartData = ref(null);
-let myChart = null;
-
-// ========== 地区提取函数 ==========
-const extractRegion = (location) => {
-  if (!location || typeof location !== 'string') return null;
-
-  // 1. 精确匹配官方区县名称
-  const officialMatch = SG_REGIONS.find(region => 
-    location.includes(region)
-  );
-  if (officialMatch) return officialMatch;
-
-  // 2. 处理嵌套格式(如"韶关市-浈江区")
-  const nestedMatch = location.match(/(韶关市)([^市]+?[区市县])/);
-  if (nestedMatch && nestedMatch[2]) {
-    const region = nestedMatch[2].replace("韶关市", "").trim();
-    // 验证是否为合法区县
-    const validRegion = SG_REGIONS.find(r => r.includes(region));
-    if (validRegion) return validRegion;
-  }
-
-  // 3. 特殊格式处理(如"韶关市浈江区")
-  const shortMatch = location.match(/韶关市([区市县][^市]{2,5})/);
-  if (shortMatch && shortMatch[1]) return shortMatch[1];
-
-  // 4. 修正常见拼写错误
-  if (location.includes('乐昌')) return '乐昌市';
-  if (location.includes('乳源')) return '乳源瑶族自治县';
-
-  console.warn(`⚠️ 未识别地区: ${location}`);
-  return '未知区县';
-};
-
-// ========== 数据处理流程 ==========
-const processMergedData = (samplingData, assayData) => {
-  // 1. 构建采样点ID到区县的映射
-  const regionMap = new Map();
-  const uniqueSampleIds = new Set();
-  
-  samplingData.forEach(item => {
-    const region = extractRegion(item.sampling_location || '');
-    if (region && region !== '未知区县') {
-      regionMap.set(item.water_sample_ID, region);
-    }
-  });
-
-  // 2. 关联重金属数据与区县
-  const mergedData = assayData.map(item => ({
-    ...item,
-    region: regionMap.get(item.water_sample_ID) || '未知区县'
-  }));
-
-  // 3. 识别重金属字段
-  const detectedMetals = Object.keys(mergedData[0] || {})
-    .filter(key => !EXCLUDE_FIELDS.includes(key) && !isNaN(parseFloat(mergedData[0][key])));
-  
-  metals.value = detectedMetals;
-  if (detectedMetals.length > 0 && !selectedMetal.value) {
-    selectedMetal.value = detectedMetals[0];
-  }
-
-  // 4. 按区县分组统计
-  const regionGroups = {};
-  
-  mergedData.forEach(item => {
-    const region = item.region;
-    const sampleId = item.water_sample_ID;
-    
-    if (sampleId) uniqueSampleIds.add(sampleId);
-    
-    // 初始化区县数据
-    if (!regionGroups[region]) {
-      regionGroups[region] = {};
-      detectedMetals.forEach(metal => {
-        regionGroups[region][metal] = { sum: 0, count: 0 };
-      });
-    }
-
-    // 统计重金属数据
-    detectedMetals.forEach(metal => {
-      const val = parseFloat(item[metal]);
-      if (!isNaN(val)) {
-        regionGroups[region][metal].sum += val;
-        regionGroups[region][metal].count++;
-      }
-    });
-  });
-
-  // 5. 构建扇形图数据
-  const pieSeriesData = detectedMetals.map(metal => {
-    // 该重金属在各区县的平均浓度总和
-    let totalAverage = 0;
-    
-    // 收集各区县该重金属的平均值
-    const regionAverages = SG_REGIONS.map(region => {
-      if (!regionGroups[region]) return null;
-      
-      const group = regionGroups[region][metal];
-      const avg = group.count ? group.sum / group.count : 0;
-      totalAverage += avg;
-      
-      return { 
-        name: region,
-        value: avg
-      };
-    }).filter(Boolean);
-
-    // 计算占比
-    const seriesData = regionAverages.map(item => ({
-      name: item.name,
-      value: totalAverage > 0 ? (item.value / totalAverage) * 100 : 0,
-      rawValue: item.value // 保留原始浓度值用于显示
-    }));
-
-    return {
-      metal,
-      seriesData
-    };
-  });
-
-  return {
-    regions: SG_REGIONS,
-    pieSeriesData,
-    totalSamples: uniqueSampleIds.size
-  };
-};
-
-// ========== 初始化/更新图表 ==========
-const initChart = () => {
-  if (!chartRef.value || !selectedMetal.value || !chartData.value) return;
-  
-  if (myChart) myChart.dispose();
-  myChart = echarts.init(chartRef.value);
-
-  // 获取当前重金属的数据
-  const currentMetalData = chartData.value.pieSeriesData.find(
-    item => item.metal === selectedMetal.value
-  );
-
-  if (!currentMetalData) return;
-
-  const option = {
-    title: { 
-      text: `韶关市${selectedMetal.value}平均浓度区域占比`, 
-      left: 'center',
-      subtext: `数据来源: ${chartData.value.totalSamples}个有效检测样本`
-    },
-    tooltip: {
-      trigger: 'item',
-      formatter: function(params) {
-        return `${params.name}<br/>
-                ${selectedMetal.value}: ${params.data.rawValue.toFixed(4)} mg/L<br/>
-                占比: ${params.percent}%`;
-      }
-    },
-    legend: {
-      orient: 'vertical',
-      right: 10,
-      top: 'center',
-      data: currentMetalData.seriesData.map(item => item.name)
-    },
-    series: [
-      {
-        name: selectedMetal.value,
-        type: 'pie',
-        radius: ['35%', '65%'],
-        center: ['45%', '50%'],
-        avoidLabelOverlap: false,
-        itemStyle: {
-          borderRadius: 10,
-          borderColor: '#fff',
-          borderWidth: 2
-        },
-        label: {
-          show: false,
-          position: 'center'
-        },
-        emphasis: {
-          label: {
-            show: true,
-            fontSize: '16',
-            fontWeight: 'bold',
-            formatter: '{b}\n{c}%'
-          }
-        },
-        labelLine: {
-          show: false
-        },
-        data: currentMetalData.seriesData
-      }
-    ],
-    color: COLORS
-  };
-
-  myChart.setOption(option);
-};
-
-// ========== 生命周期钩子 ==========
-onMounted(async () => {
-  try {
-    // 修复请求超时问题:将超时时间延长至10秒
-    const [samplingRes, assayRes] = await Promise.all([
-      axios.get(SAMPLING_API, { timeout: 10000 }), // 10秒超时
-      axios.get(ASSAY_API, { timeout: 10000 })
-    ]);
-    
-    chartData.value = processMergedData(samplingRes.data, assayRes.data);
-    initChart();
-  } catch (err) {
-    // 处理超时错误
-    if (err.code === 'ECONNABORTED') {
-      error.value = '请求超时:服务器响应时间超过10秒';
-    } else {
-      error.value = '数据加载失败: ' + (err.message || '未知错误');
-    }
-    console.error('接口错误:', err);
-  } finally {
-    loading.value = false;
-  }
-});
-
-// 监听重金属选择变化
-watch(selectedMetal, (newVal) => {
-  if (newVal && myChart && chartData.value) {
-    initChart();
-  }
-});
-
-// 响应式布局
-const resizeHandler = () => myChart && myChart.resize();
-onMounted(() => window.addEventListener('resize', resizeHandler));
-onUnmounted(() => window.removeEventListener('resize', resizeHandler));
-</script>
-
-<style scoped>
-.region-average-chart {
-  width: 100%;
-  max-width: 1200px;
-  margin: 20px auto;
-  position: relative;
-}
-.chart-box {
-  width: 100%;
-  height: 600px;
-  min-height: 400px;
-  background-color: white;
-  border-radius: 8px;
-  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
-}
-.status {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  padding: 15px;
-  background: rgba(255,255,255,0.8);
-  border-radius: 4px;
-  text-align: center;
-}
-.error { 
-  color: #ff4d4f;
-  font-weight: bold;
-}
-.metal-selector {
-  margin-bottom: 15px;
-  text-align: center;
-  padding: 10px;
-}
-.metal-selector label {
-  margin-right: 10px;
-  font-weight: bold;
-}
-.metal-selector select {
-  padding: 8px 15px;
-  border-radius: 4px;
-  border: 1px solid #ddd;
-  background-color: #f8f8f8;
-  font-size: 14px;
-  min-width: 150px;
-  cursor: pointer;
-  transition: border 0.3s;
-}
-.metal-selector select:hover {
-  border-color: #1890ff;
-}
-</style>

+ 0 - 313
src/components/irrpollution/waterdataline.vue

@@ -1,313 +0,0 @@
-<template>
-  <div class="line-page">
-    <div class="container mx-auto px-4 py-8">
-    <div class="bg-white rounded-xl shadow-lg overflow-hidden">
-      
-      <!-- 加载状态 -->
-      <div v-if="loading" class="py-20 flex justify-center items-center">
-        <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
-      </div>
-      
-      <!-- 错误状态 -->
-      <div v-else-if="error" class="p-8 bg-red-50 border-l-4 border-red-400 text-red-700">
-        <div class="flex">
-          <div class="flex-shrink-0">
-            <i class="fa fa-exclamation-triangle text-red-500 text-xl"></i>
-          </div>
-          <div class="ml-3">
-            <h3 class="text-sm font-medium text-red-800">加载失败</h3>
-            <div class="mt-2 text-sm text-red-700">
-              <p>{{ error }}</p>
-            </div>
-            <div class="mt-4">
-              <button @click="fetchData" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150 ease-in-out">
-                <i class="fa fa-refresh mr-2"></i>重试
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-      
-      <!-- 数据表格 -->
-      <div v-else-if="filteredData.length > 0" class="overflow-x-auto">
-        <table class="min-w-full divide-y divide-gray-200">
-          <thead class="bg-white">
-            <tr>
-              <th 
-                v-for="(col, index) in displayColumns" 
-                :key="index"
-                class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
-                @click="sortData(col.key)"
-              >
-                <div class="flex items-center justify-between whitespace-nowrap overflow-hidden">
-                  <span class="truncate" :title="col.label">{{ col.label }}</span>
-                  <span v-if="sortKey === col.key" class="ml-1 text-gray-400 flex-shrink-0">
-                    {{ sortOrder === 'asc' ? '↑' : '↓' }}
-                  </span>
-                </div>
-              </th>
-            </tr>
-          </thead>
-          <tbody class="bg-white divide-y divide-gray-200">
-            <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex" 
-                class="hover:bg-gray-50 transition-colors duration-150">
-              <td 
-                v-for="(col, colIndex) in displayColumns" 
-                :key="colIndex"
-                class="px-6 py-4 whitespace-nowrap text-sm"
-              >
-                <div class="flex items-center">
-                  <div class="text-gray-900 font-medium">
-                    <!-- 格式化金属含量字段为小数点后六位 -->
-                    <template v-if="concentrationFields.includes(col.key) && item[col.key] !== null">
-                      {{ formatConcentration(item[col.key]) }}
-                    </template>
-                    <template v-else>
-                      {{ item[col.key] !== null ? item[col.key] : '-' }}
-                    </template>
-                  </div>
-                </div>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-      
-      <!-- 空数据状态 -->
-      <div v-else class="p-8 text-center">
-        <div class="flex flex-col items-center justify-center">
-          <div class="text-gray-400 mb-4">
-            <i class="fa fa-database text-5xl"></i>
-          </div>
-          <h3 class="text-lg font-medium text-gray-900 mb-1">暂无有效数据</h3>
-          <p class="text-gray-500">已过滤全空行</p>
-        </div>
-      </div>
-  
-      <!-- 数据统计 -->
-      <div class="p-4 bg-white border-t border-gray-200">
-        <div class="flex flex-col md:flex-row justify-between items-center">
-          <div class="text-sm text-gray-500 mb-2 md:mb-0">
-            共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted } from 'vue';
-import { api8000 } from '@/utils/request'; // 导入 api8000 实例
-
-// 适配接口的字段映射
-const displayColumns = ref([
-  { key: 'sample_number', label: '水样ID' },
-  { key:'sampling_location',label:'地理位置'},
-  { key: 'ph_value', label: 'PH值' },
-  { key: 'cr_concentration', label: '铬含量(ug/L)' },
-  { key: 'as_concentration', label: '砷含量(ug/L)' },
-  { key: 'cd_concentration', label: '镉含量(ug/L)' },
-  { key: 'hg_concentration', label: '汞含量(ug/L)' },
-  { key: 'pb_concentration', label: '铅含量(ug/L)' },
-]);
-
-// 需要格式化为小数点后六位的字段
-const concentrationFields = [
-  'cr_concentration',
-  'as_concentration',
-  'cd_concentration',
-  'hg_concentration',
-  'pb_concentration'
-];
-
-// 状态管理
-const waterData = ref([]);
-const loading = ref(true);
-const error = ref(null);
-const sortKey = ref('');
-const sortOrder = ref('asc');
-
-// 格式化金属含量为小数点后六位
-const formatConcentration = (value) => {
-  const num = Number(value);
-  return isNaN(num) ? value : num.toFixed(6);
-};
-
-// 接口请求
-const fetchData = async () => {
-  try {
-    loading.value = true;
-    error.value = null;
-    
-    // 使用 api8000 实例获取数据
-    const response = await api8000.get('/api/vector/export/all?table_name=water_sampling_data');
-    
-    // 处理可能的字符串响应
-    let data = response.data;
-    if (typeof data === 'string') {
-      try {
-        // 替换 NaN 为 null
-        const cleanedData = data.replace(/\bNaN\b/g, 'null');
-        data = JSON.parse(cleanedData);
-      } catch (parseErr) {
-        throw new Error('接口返回的是字符串,但 JSON 解析失败');
-      }
-    }
-    
-    // 处理对象中的 NaN 值
-    if (typeof data === 'object' && data !== null) {
-      const replaceNaN = (obj) => {
-        for (const key in obj) {
-          if (typeof obj[key] === 'object' && obj[key] !== null) {
-            replaceNaN(obj[key]);
-          } else if (typeof obj[key] === 'number' && isNaN(obj[key])) {
-            obj[key] = null;
-          } else if (obj[key] === 'NaN') {
-            obj[key] = null;
-          }
-        }
-      };
-      replaceNaN(data);
-    }
-    
-    // 接口返回格式判断(GeoJSON或直接数组)
-    const rawData = data.features 
-      ? data.features.map(f => f.properties) 
-      : data;
-    
-    waterData.value = rawData;
-  } catch (err) {
-    error.value = err.message || '无法连接到服务器,请检查接口是否可用';
-    console.error('数据加载失败:', err);
-  } finally {
-    loading.value = false;
-  }
-};
-
-// 过滤全空行
-const filteredData = computed(() => {
-  return waterData.value.filter(item => {
-    return displayColumns.value.some(col => item[col.key] !== null && item[col.key] !== '-');
-  });
-});
-
-// 排序功能
-const sortedData = computed(() => {
-  if (!sortKey.value) return filteredData.value;
-  
-  return [...filteredData.value].sort((a, b) => {
-    const valA = a[sortKey.value];
-    const valB = b[sortKey.value];
-    
-    // 处理 null 值
-    if (valA === null && valB === null) return 0;
-    if (valA === null) return sortOrder.value === 'asc' ? 1 : -1;
-    if (valB === null) return sortOrder.value === 'asc' ? -1 : 1;
-    
-    // 数值比较
-    if (typeof valA === 'number' && typeof valB === 'number') {
-      return sortOrder.value === 'asc' ? valA - valB : valB - valA;
-    }
-    
-    // 字符串比较
-    if (typeof valA === 'string' && typeof valB === 'string') {
-      return sortOrder.value === 'asc' 
-        ? valA.localeCompare(valB) 
-        : valB.localeCompare(valA);
-    }
-    
-    return 0;
-  });
-});
-
-// 切换排序
-const sortData = (key) => {
-  if (sortKey.value === key) {
-    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
-  } else {
-    sortKey.value = key;
-    sortOrder.value = 'asc';
-  }
-};
-
-// 组件挂载
-onMounted(() => {
-  fetchData();
-});
-</script>
-
-<style scoped>
-.line-page {
-    width: 100%;
-  margin: 0 auto 24px;
-  background-color: white;
-  border-radius: 12px;
-  padding: 20px;
-  box-sizing: border-box;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-} 
-/* 布局 */
-.container {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 32px 16px;
-}
-.overflow-x-auto { overflow-x: auto; }
-
-/* 卡片 */
-.bg-white { background-color: #fff; }
-.rounded-xl { border-radius: 1rem; }
-.shadow-lg { 
-  box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 
-             0 4px 6px -4px rgba(0,0,0,0.1); 
-}
-
-/* 文字 */
-.text-center { text-align: center; }
-.text-lg { font-size: 1rem; }
-.font-bold { font-weight: 700; }
-.text-gray-800 { color: #111827; }
-
-/* 动画 */
-.animate-spin {
-  animation: spin 1s linear infinite;
-}
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}
-
-/* 表格 */
-table { 
-  width: 100%; 
-  border-collapse: collapse;
-  background-color: white;
-}
-th, td {
-  border: 1px solid #d1d5db;
-  text-align: center;
-  padding: 12px 8px;
-  font-size: 14px;
-  background-color: white;
-}
-.px-6 { padding: 0 1.5rem; }
-.py-4 { padding: 1rem 0; }
-.hover\:bg-gray-50:hover { background-color: #f9fafb; }
-
-/* 表头样式优化 - 确保不换行 */
-th .truncate {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  max-width: 150px; /* 可根据实际调整 */
-  display: inline-block;
-}
-
-/* 响应式 */
-@media (max-width: 640px) {
-  .container { padding: 32px 8px; }
-  .px-6 { padding: 0 0.75rem; }
-}
-</style>

+ 46 - 44
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,9 @@
 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();
 
 
 // 图表实例引用
@@ -76,44 +78,44 @@ const pollutionStats = ref([]);
 const nutrientStats = ref([]);
 const extraStats = ref([]);
 
-// 字段配置(根据接口返回的作物态Cd数据结构定义)
+// 字段配置
 const fieldConfig = {
- pollution: [
+  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},
   ]
 };
 
@@ -172,13 +174,13 @@ const initPollutionChart = () => {
       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",
+        name:t('SoilCdStatistics.averageValue'), type: "bar",
         itemStyle: { color: (p) => fieldConfig.pollution[p.dataIndex].color },
         data: barData
       }]
@@ -197,7 +199,7 @@ const initNutrientChart = () => {
     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) => {
@@ -219,7 +221,7 @@ const initNutrientChart = () => {
         axisLabel: { fontSize: 11 } 
       },
       series: [{
-        name: '养分元素', 
+        name: t('SoilCdStatistics.mainNutrients'), 
         type: "boxplot",
         itemStyle: { color: '#ee6666', borderColor: '#fac858' },
         data: boxData
@@ -237,7 +239,7 @@ const initExtraChart = () => {
       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])
@@ -246,7 +248,7 @@ const initExtraChart = () => {
          xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11} },
         yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
         series: [{
-          name: '理化性质', type: "boxplot",
+          name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
            itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
            data: boxData
         }]
@@ -254,18 +256,18 @@ const initExtraChart = () => {
       });
 };
 
-// 格式化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>`;
 };
 

+ 50 - 45
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,9 @@
 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 cdBarChart = ref(null);
@@ -72,31 +75,31 @@ const pollutionStats = ref([]);       // 污染指标统计(总镉/有效态
 const nutrientStats = ref([]);        // 养分元素统计
 const extraStats = ref([]);           // 其他理化性质统计
 
-// 字段配置(参考灌溉水的重金属配置方式)
+// 字段配置
 const fieldConfig = {
   pollution: [
-    { key: 'TCd_IDW', name: '总镉', color: '#5470c6' ,unit:'mg/kg' , convert: false },
-     { key: 'AvaK_IDW', name: '速效钾', color: '#fac858',unit:'mg/kg', convert: false },
-    { key: 'AvaP_IDW', name: '有效磷', color: '#fac858' ,unit:'mg/kg' , convert: false},
-    { key: 'TMn_IDW', name: '全锰', color: '#73c0de' ,unit:'mg/kg' , convert: false},
-    { key: 'TN_IDW', name: '全氮', color: '#fac858' ,unit:'mg/kg' , convert: true, conversionFactor: 1000},
-    { key: 'TP_IDW', name: '全磷', color: '#fac858' ,unit:'mg/kg', convert: true, conversionFactor: 1000},
-    { key: 'TK_IDW', name: '全钾', color: '#fac858' ,unit:'mg/kg', convert: true, conversionFactor: 1000},
+    { 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}
   ]
 };
 
@@ -221,10 +224,10 @@ const initPollutionChart = () => {
   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,12 +242,12 @@ 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",
+      name: t('SoilCdStatistics.averageValue'), type: "bar",
       itemStyle: {color: (params) => fieldConfig.pollution[params.dataIndex].color },
       data: barData
     }]
@@ -260,7 +263,7 @@ const initNutrientChart = () => {
   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,12 +281,12 @@ const initNutrientChart = () => {
     },
     yAxis: { 
       type: "value", 
-      name: '含量(mg/kg)', 
+      name: t('SoilCdStatistics.unitMgKg'), 
       nameTextStyle: { fontSize: 12 }, 
       axisLabel: { fontSize: 11 } 
     },
     series: [{
-      name: '养分元素', type: "boxplot",
+      name: t('SoilCdStatistics.mainNutrients'), type: "boxplot",
       itemStyle: { color: (params) => fieldConfig.nutrient[params.dataIndex].color },
       data: boxData
     }]
@@ -299,7 +302,7 @@ const initExtraChart = () => {
         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,7 +311,7 @@ const initExtraChart = () => {
           xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11 } },
           yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
           series: [{
-            name: '理化性质', type: "boxplot",
+            name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
             itemStyle: { color: (params) => fieldConfig.extra[params.dataIndex].color },
             data: boxData
           }]
@@ -316,18 +319,20 @@ const initExtraChart = () => {
   });
 };
 
-// 格式化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>`;
 };
 

+ 29 - 28
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,9 @@
 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 initialCdChart = ref(null);
@@ -73,15 +74,15 @@ const otherIndicatorsStats = ref([]);
 // 字段配置
 const fieldConfig = {
   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' },
   ]
 };
 
@@ -180,7 +181,7 @@ const buildBoxplotData = (statsArray) => {
 // 初始化【初始Cd】图表
 const initInitialCdChart = () => {
   if (!initialCdChart.value) {
-    error.value = new Error('初始Cd图表容器未找到');
+    error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
     return;
   }
   
@@ -194,7 +195,7 @@ const initInitialCdChart = () => {
     const boxData = buildBoxplotData(initialCdStats.value);
     
     const option = {
-      title: { text: '初始Cd分布箱线图', left: 'center' },
+      title: { text:  t('SoilCdStatistics.initialCdDistribution'), left: 'center' },
       tooltip: {
         trigger: "item",
         formatter: (params) => {
@@ -223,15 +224,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;
   }
   
@@ -245,7 +246,7 @@ const initOtherIndicatorsChart = () => {
     const boxData = buildBoxplotData(otherIndicatorsStats.value);
     
     const option = {
-      title: { text: '其他通量Cd指标分布对比', left: 'center' },
+      title: { text: t('SoilCdStatistics.otherFluxIndicators'), left: 'center' },
       tooltip: {
         trigger: "item",
         formatter: (params) => {
@@ -278,8 +279,8 @@ 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}`);
   }
 };
 

+ 219 - 5
src/locales/en.json

@@ -38,26 +38,240 @@
     "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"
+  },
+  "SoilacidificationStatistics": {
+    "Title": "Soil acidification data statistics"
+  },
+  "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": {
+    "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 Cadmium",
+    "availablePotassium": "Available Potassium",
+    "availablePhosphorus": "Available Phosphorus",
+    "totalManganese": "Total Manganese",
+    "totalNitrogen": "Total Nitrogen",
+    "totalPhosphorus": "Total Phosphorus",
+    "totalPotassium": "Total Potassium",
+    "totalSulfur": "Total Sulfur",
+    "totalIron": "Total Iron",
+    "totalCalcium": "Total Calcium",
+    "totalMagnesium": "Total Magnesium",
+    "totalAluminum": "Total Aluminum",
+    "siltContent": "Silt Content",
+    "sandContent": "Sand Content",
+    "gravelContent": "Gravel Content",
+    "exchangeablePotassium": "Exchangeable Available Potassium",
+    "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"
   }
+  
 }

+ 223 - 11
src/locales/zh.json

@@ -29,36 +29,248 @@
     "passwordMismatch": "两次输入的密码不一致"
   },
   "Menu": {
-    "dataManagement":"数据管理",
+    "dataManagement": "数据管理",
     "infoManagement": "信息管理",
     "modelManagement": "模型管理及配置",
     "userManagement": "用户管理",
-    
-    "swIntroduce":"软件简介",
-    "mapcalulate":"土壤酸化地块级计算",
-    "reversionmodel":"土壤反酸模型",
-    "reducemodel":"土壤降酸模型",
+    "swIntroduce": "软件简介",
+    "mapcalulate": "土壤酸化地块级计算",
+    "reversionmodel": "土壤反酸模型",
+    "reducemodel": "土壤降酸模型",
     "dataStatistics": "数据统计",
-    
     "softwareIntroduction": "软件简介",
     "projectOverview": "项目简介",
     "researchFindings": "研究成果",
     "teamInfo": "团队信息",
-    
     "soilAcidPlotPrediction": "土壤酸化地块级预测",
     "shaoguanAcidMap": "韶关土壤酸化地图",
     "nanxiongAcidMap": "南雄土壤酸化地图",
     "soilPH Prediction": "土壤 pH 预测",
-    
     "soilAcidReductionPrediction": "土壤反酸预测",
     "acidReductionModelDisplay": "反酸模型显示",
-    
     "soilAcidNeutralizationPrediction": "土壤降酸预测",
     "acidNeutralizationModelDisplay": "降酸模型显示",
-    
     "detectionStatistics": "检测信息统计",
     "soilCadmiumStatistics": "土壤镉含量统计",
     "cropRiskAssessment": "作物风险评估系统",
     "soilAcidificationStatistics": "土壤酸化统计"
+  },
+  "SoilacidificationStatistics": {
+    "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": {
+    "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": "数据加载失败"
   }
 }

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

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