yes-yes-yes-k 1 сар өмнө
parent
commit
f8c223dd4d
33 өөрчлөгдсөн 7982 нэмэгдсэн , 324 устгасан
  1. 559 0
      src/components/atmpollution/airSampleTencentMap.vue
  2. 483 0
      src/components/atmpollution/airsampleChart.vue
  3. 457 0
      src/components/atmpollution/airsampleLine.vue
  4. 479 0
      src/components/atmpollution/atmCompanytencentMap.vue
  5. 252 0
      src/components/atmpollution/atmcompanyline.vue
  6. 252 0
      src/components/atmpollution/atmcompanymap.vue
  7. 378 0
      src/components/atmpollution/atmsamplemap.vue
  8. 391 0
      src/components/atmpollution/heavyMetalEnterprisechart.vue
  9. 68 55
      src/components/detectionStatistics/atmsampleStatistics.vue
  10. 39 35
      src/components/detectionStatistics/irrigationstatistics.vue
  11. 211 0
      src/components/irrpollution/crossSectionSamplelineData.vue
  12. 262 0
      src/components/irrpollution/crossSetionData1.vue
  13. 264 0
      src/components/irrpollution/crossSetionData2.vue
  14. 416 0
      src/components/irrpollution/crossSetionTencentmap.vue
  15. 265 0
      src/components/irrpollution/crosssectionmap.vue
  16. 280 0
      src/components/irrpollution/irrwatermap.vue
  17. 87 0
      src/components/irrpollution/riverwaterassay.vue
  18. 1015 0
      src/components/irrpollution/tencentMapView.vue
  19. 222 0
      src/components/irrpollution/waterassaydata1.vue
  20. 348 0
      src/components/irrpollution/waterassaydata2.vue
  21. 197 0
      src/components/irrpollution/waterassaydata3.vue
  22. 340 0
      src/components/irrpollution/waterassaydata4.vue
  23. 313 0
      src/components/irrpollution/waterdataline.vue
  24. 1 1
      src/components/layout/AppLayout.vue
  25. 74 32
      src/components/soilStatictics/reducedataStatistics.vue
  26. 80 34
      src/components/soilStatictics/refluxcedataStatictics.vue
  27. 66 58
      src/components/soilcdStatistics/cropcdStatictics.vue
  28. 48 34
      src/components/soilcdStatistics/effcdStatistics.vue
  29. 36 31
      src/components/soilcdStatistics/fluxcdStatictics.vue
  30. 21 1
      src/locales/en.json
  31. 21 0
      src/locales/zh.json
  32. 54 41
      src/views/User/dataStatistics/LandCultivatedStatistics.vue
  33. 3 2
      vite.config.ts

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

@@ -0,0 +1,559 @@
+<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>

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

@@ -0,0 +1,483 @@
+<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>

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

@@ -0,0 +1,457 @@
+<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>

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

@@ -0,0 +1,479 @@
+<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>

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

@@ -0,0 +1,252 @@
+<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>

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

@@ -0,0 +1,252 @@
+<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>

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

@@ -0,0 +1,378 @@
+<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>

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

@@ -0,0 +1,391 @@
+<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>

+ 68 - 55
src/components/detectionStatistics/atmsampleStatistics.vue

@@ -27,19 +27,21 @@
 <script>
 import * as echarts from 'echarts'
 import VChart from 'vue-echarts'
-import { api8000 } from '@/utils/request' // 导入 api8000 实例
-import { ref, onMounted, computed } from 'vue'
+import { api8000 } from '@/utils/request'
+import { ref, onMounted, computed, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
 
 export default {
   components: { VChart },
   setup() {
     const { t } = useI18n()
+    const { locale } = useI18n()
 
     // -------- 核心配置 --------
-    // 新接口地址(直接返回箱线图所需统计数据)
-    const apiUrl = ref('/api/vector/stats/Atmo_sample_data') // 修改为相对路径
-    const heavyMetals = [
+    const apiUrl = ref('/api/vector/stats/Atmo_sample_data')
+    
+    // 获取重金属配置的函数(每次调用都会使用最新的翻译)
+    const getHeavyMetals = () => [
       { key: 'Cr_particulate', name: t('DetectionStatistics.chromium'), color: '#FF9800' },
       { key: 'As_particulate', name: t('DetectionStatistics.arsenic'), color: '#4CAF50' },
       { key: 'Cd_particulate', name: t('DetectionStatistics.cadmium'), color: '#9C27B0' },
@@ -47,20 +49,19 @@ export default {
       { key: 'Pb_particulate', name: t('DetectionStatistics.lead'), color: '#F44336' },
     ]
 
-
     // -------- 状态管理 --------
-    const chartOption = ref({})   // ECharts 配置
-    const isLoading = ref(true)   // 加载状态
-    const error = ref(null)       // 错误信息
-    const statsByIndex = ref([])  // 与x轴对齐的统计结果(用于tooltip)
+    const chartOption = ref({})
+    const isLoading = ref(true)
+    const error = ref(null)
+    const statsByIndex = ref([])
+    const apiDataCache = ref(null) // 缓存接口数据
+
     const totalPoints = computed(() => {
-      // 从统计数据中获取样本总数(假设所有金属样本数相同)
       return statsByIndex.value.length > 0 
         ? statsByIndex.value[0].count || 0 
         : 0;
     })
 
-
     // -------- 工具函数 --------
     const log = (message, metalName = '') => {
       console.log(
@@ -71,8 +72,7 @@ export default {
     }
 
     const buildBoxplotData = (stats) => {
-      const xAxisData = heavyMetals.map(m => m.name)
-      // 生成箱线图所需格式:[[min, q1, median, q3, max], ...]
+      const xAxisData = stats.map(s => s.name)
       const data = stats.map(s => 
         s.min === null 
           ? [null, null, null, null, null] 
@@ -81,8 +81,10 @@ export default {
       return { xAxisData, data }
     }
 
-    const initChart = (xAxisData, data, stats) => {
-      statsByIndex.value = stats // 缓存统计数据用于tooltip
+    const initChart = (stats) => {
+      statsByIndex.value = stats
+      
+      const { xAxisData, data } = buildBoxplotData(stats)
       
       chartOption.value = {
         tooltip: {
@@ -121,9 +123,9 @@ export default {
         series: [{
           name: t('DetectionStatistics.concentrationDistribution'),
           type: 'boxplot',
-          data, // 直接使用接口返回的统计数据
+          data,
           itemStyle: {
-            color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
+            color: (p) => (getHeavyMetals()[p.dataIndex]?.color || '#1890ff'),
             borderWidth: 2
           },
           emphasis: {
@@ -134,48 +136,51 @@ export default {
       }
     }
 
+    // -------- 处理数据并初始化图表 --------
+    const processDataAndInitChart = () => {
+      if (!apiDataCache.value) return
+      
+      const heavyMetals = getHeavyMetals()
+      const stats = heavyMetals.map(metal => {
+        const metalStats = apiDataCache.value[metal.key]
+        if (!metalStats) {
+          log(`警告:接口缺少${metal.name}的统计数据`)
+          return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
+        }
+        
+        const requiredFields = ['min', 'q1', 'median', 'q3', 'max']
+        const hasValidData = requiredFields.every(field => 
+          metalStats[field] !== undefined && !isNaN(metalStats[field])
+        )
+
+        if (!hasValidData) {
+          log(`警告:${metal.name}的统计数据不完整`)
+          return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
+        }
+
+        return {
+          ...metal,
+          min: Number(metalStats.min),
+          q1: Number(metalStats.q1),
+          median: Number(metalStats.median),
+          q3: Number(metalStats.q3),
+          max: Number(metalStats.max),
+          count: metalStats.count ? Number(metalStats.count) : 0
+        }
+      })
+
+      initChart(stats)
+      isLoading.value = false
+    }
+
     // -------- 接口请求 --------
     onMounted(async () => {
       try {
         log('发起新接口请求,获取统计数据...')
-        // 使用 api8000 替代 axios
         const response = await api8000.get(apiUrl.value)
-        const apiData = response.data.data
-
-        // 从接口数据中提取每个重金属的统计量
-        const stats = heavyMetals.map(metal => {
-          const metalStats = apiData[metal.key]
-          if (!metalStats) {
-            log(`警告:接口缺少${metal.name}的统计数据`)
-            return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
-          }
-          
-          // 验证必要的统计字段
-          const requiredFields = ['min', 'q1', 'median', 'q3', 'max']
-          const hasValidData = requiredFields.every(field => 
-            metalStats[field] !== undefined && !isNaN(metalStats[field])
-          )
-
-          if (!hasValidData) {
-            log(`警告:${metal.name}的统计数据不完整`)
-            return { ...metal, min: null, q1: null, median: null, q3: null, max: null, count: 0 }
-          }
-
-          return {
-            ...metal,
-            min: Number(metalStats.min),
-            q1: Number(metalStats.q1),
-            median: Number(metalStats.median),
-            q3: Number(metalStats.q3),
-            max: Number(metalStats.max),
-            count: metalStats.count ? Number(metalStats.count) : 0
-          }
-        })
-
-        // 构建图表数据并初始化图表
-        const { xAxisData, data } = buildBoxplotData(stats)
-        initChart(xAxisData, data, stats)
-        isLoading.value = false
+        apiDataCache.value = response.data.data
+        
+        processDataAndInitChart()
 
       } catch (err) {
         error.value = err
@@ -184,6 +189,14 @@ export default {
       }
     })
 
+    // 监听语言变化,重新初始化图表
+    watch(locale, () => {
+      console.log('语言切换,重新初始化图表')
+      if (apiDataCache.value) {
+        processDataAndInitChart()
+      }
+    })
+
     return {
       chartOption,
       isLoading,

+ 39 - 35
src/components/detectionStatistics/irrigationstatistics.vue

@@ -27,35 +27,35 @@
 <script>
 import * as echarts from 'echarts'
 import VChart from 'vue-echarts'
-import { api8000 } from '@/utils/request' // 导入 api8000 实例
-import { ref, onMounted, computed } from 'vue'
+import { api8000 } from '@/utils/request'
+import { ref, onMounted, computed, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
 
 export default {
   components: { VChart },
   setup() {
-     const { t } = useI18n()
-
+    const { t } = useI18n()
+    const { locale } = useI18n()
 
     // -------- 基本状态 --------
-    const apiUrl = ref('/api/vector/stats/water_sampling_data') // 修改为相对路径
+    const apiUrl = ref('/api/vector/stats/water_sampling_data')
     const apiTimestamp = ref(null)
-    const statsData = ref({}); 
+    const statsData = ref({})
     const chartOption = ref({})
     const isLoading = ref(true)
     const error = ref(null)
     
-    // 样本数统计(从预统计数据中获取)
+    // 样本数统计
     const totalPoints = computed(() => {
-      const firstMetalKey = heavyMetals[0]?.key;
-      return statsData.value[firstMetalKey]?.count || 0;
+      const firstMetalKey = getHeavyMetals()[0]?.key
+      return statsData.value[firstMetalKey]?.count || 0
     })
 
-    // 缓存每个品类的统计量(与 x 轴顺序一致)
+    // 缓存每个品类的统计量
     const statsByIndex = ref([])
 
-    // -------- 配置:金属字段 --------
-    const heavyMetals = [
+    // -------- 配置:金属字段 - 改为函数形式 --------
+    const getHeavyMetals = () => [
       { key: 'cr_concentration', name: t('DetectionStatistics.chromium'), color: '#FF9800' },
       { key: 'as_concentration', name: t('DetectionStatistics.arsenic'), color: '#4CAF50' },
       { key: 'cd_concentration', name: t('DetectionStatistics.cadmium'), color: '#9C27B0' },
@@ -65,11 +65,11 @@ export default {
 
     // -------- 构建箱线数据 --------
     const buildBoxplotData = () => {
-      const xAxisData = heavyMetals.map(m => m.name);
+      const heavyMetals = getHeavyMetals()
+      const xAxisData = heavyMetals.map(m => m.name)
 
-      // 缓存每个重金属的统计量(用于tooltip)
       statsByIndex.value = heavyMetals.map(metal => {
-        const stat = statsData.value[metal.key] || {};
+        const stat = statsData.value[metal.key] || {}
         return {
           key: metal.key,
           name: metal.name,
@@ -79,23 +79,22 @@ export default {
           q3: stat.q3,
           max: stat.max,
           color: metal.color
-        };
-      });
+        }
+      })
 
-      // 构建ECharts箱线图数据
       const data = statsByIndex.value.map(s => {
         if (s.min === undefined || s.min === null) {
-          return [null, null, null, null, null];
+          return [null, null, null, null, null]
         }
-        return [s.min, s.q1, s.median, s.q3, s.max];
-      });
+        return [s.min, s.q1, s.median, s.q3, s.max]
+      })
 
-      return { xAxisData, data };
-    };
+      return { xAxisData, data }
+    }
 
     // -------- 初始化图表 --------
     const initChart = () => {
-      const { xAxisData, data } = buildBoxplotData();
+      const { xAxisData, data } = buildBoxplotData()
 
       chartOption.value = {
         tooltip: {
@@ -121,7 +120,7 @@ export default {
           name: t('DetectionStatistics.heavyMetalType'),
           nameLocation: 'middle',
           nameGap: 30,
-          axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold' ,fontSize :11}
+          axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold', fontSize: 11 }
         },
         yAxis: {
           type: 'value',
@@ -129,7 +128,7 @@ export default {
           nameTextStyle: { fontSize: 12 }, 
           nameLocation: 'end',
           nameGap: 8,
-          axisLabel: { color: '#555', fontWeight: 'bold',fontSize:11 },
+          axisLabel: { color: '#555', fontWeight: 'bold', fontSize: 11 },
           splitLine: { lineStyle: { color: '#f0f0f0' } }
         },
         series: [{
@@ -137,7 +136,7 @@ export default {
           type: 'boxplot',
           data,
           itemStyle: {
-            color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
+            color: (p) => (getHeavyMetals()[p.dataIndex]?.color || '#1890ff'),
             borderWidth: 2
           },
           emphasis: {
@@ -153,18 +152,23 @@ export default {
     // -------- 拉取接口并绘图 --------
     onMounted(async () => {
       try {
-        // 使用api8000替代axios
-        const response = await api8000.get(apiUrl.value);
-        statsData.value = response.data.data; 
-        apiTimestamp.value = new Date().toLocaleString();
-        initChart();
+        const response = await api8000.get(apiUrl.value)
+        statsData.value = response.data.data
+        apiTimestamp.value = new Date().toLocaleString()
+        initChart()
       } catch (err) {
-        error.value = err;
-        isLoading.value = false;
-        console.error('接口请求失败:', err);
+        error.value = err
+        isLoading.value = false
+        console.error('接口请求失败:', err)
       }
     })
 
+    // 监听语言变化,重新初始化图表
+    watch(locale, () => {
+      console.log('语言切换,重新初始化图表')
+      initChart()
+    })
+
     return {
       apiUrl,
       apiTimestamp,

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

@@ -0,0 +1,211 @@
+<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>

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

@@ -0,0 +1,262 @@
+<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>

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

@@ -0,0 +1,264 @@
+<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>

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

@@ -0,0 +1,416 @@
+<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>

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

@@ -0,0 +1,265 @@
+<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>

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

@@ -0,0 +1,280 @@
+<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>

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

@@ -0,0 +1,87 @@
+<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>

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

@@ -0,0 +1,1015 @@
+<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>

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

@@ -0,0 +1,222 @@
+<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>

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

@@ -0,0 +1,348 @@
+<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>

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

@@ -0,0 +1,197 @@
+<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>

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

@@ -0,0 +1,340 @@
+<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>

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

@@ -0,0 +1,313 @@
+<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>

+ 1 - 1
src/components/layout/AppLayout.vue

@@ -403,7 +403,7 @@ const handleLogout = async () => {
   } catch (error) {
     // 区分用户取消操作和真正的错误
     if (error === 'cancel' || error?.toString().includes('cancel')) {
-      console.log("用户取消退出登录");
+      // console.log("用户取消退出登录");
     } else {
       ElMessage.error("退出失败,请重试");
       console.error("退出登录错误:", error);

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

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

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

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

+ 66 - 58
src/components/soilcdStatistics/cropcdStatictics.vue

@@ -51,6 +51,7 @@ import { api8000 } from '@/utils/request'; // 导入 api8000 实例import {api80
 import { useI18n } from 'vue-i18n';
 
 const { t } = useI18n();
+const {locale} = useI18n()
 
 
 // 图表实例引用
@@ -78,8 +79,8 @@ const pollutionStats = ref([]);
 const nutrientStats = ref([]);
 const extraStats = ref([]);
 
-// 字段配置
-const fieldConfig = {
+// 字段配置 - 改为函数形式
+const getFieldConfig = () => ({
   pollution: [
     { 
       key: '002_0002IDW', 
@@ -104,20 +105,20 @@ const fieldConfig = {
     }
   ],
   nutrient: [
-    { 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}
+    { 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: 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},
+    { 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 },
   ]
-};
+});
 
 
 // 数据请求 - 增强错误处理和调试
@@ -125,13 +126,13 @@ const fetchData = async () => {
   try {
     isLoading.value = true;
     const apiUrl = '/api/vector/stats/CropCd_input_data';
-    console.log('正在请求数据:', apiUrl);
+    // console.log('正在请求数据:', apiUrl);
     
     const response = await api8000.get(apiUrl);
-    console.log('API响应:', response);
+    // console.log('API响应:', response);
     
     // 调试:输出响应结构
-    console.log('响应数据:', response.data);
+    // console.log('响应数据:', response.data);
     
     // 处理不同的响应格式
     let processedData;
@@ -141,7 +142,7 @@ const fetchData = async () => {
       throw new Error('无法解析API返回的数据结构');
     }
     
-    console.log('处理后的数据:', processedData);
+    // console.log('处理后的数据:', processedData);
     return processedData;
   } catch (err) {
     console.error('数据请求失败:', err);
@@ -157,17 +158,15 @@ const buildBoxplotData = (statsArray) => {
   });
 };
 
-// 初始化作物态Cd指标图表
+// 初始化作物态 Cd 指标图表
 const initPollutionChart = () => {
   nextTick(() => {
-    // 若图表实例已存在,先销毁避免内存泄漏
     if (chartInstanceCd) chartInstanceCd.dispose();
-    // 校验 DOM 存在性(防止 ref 未关联到有效 DOM)
     if (!cdBarChart.value) return;
-    // 初始化 ECharts 实例
     chartInstanceCd = echarts.init(cdBarChart.value);
     
-    const xAxisData = fieldConfig.pollution.map(f => f.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.pollution.map(f => f.name);
     const barData = pollutionStats.value.map(stat => stat.avg || 0);
     
     chartInstanceCd.setOption({
@@ -180,8 +179,8 @@ const initPollutionChart = () => {
       xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 12, rotate: 30 } },
       yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
       series: [{
-        name:t('SoilCdStatistics.averageValue'), type: "bar",
-        itemStyle: { color: (p) => fieldConfig.pollution[p.dataIndex].color },
+        name: t('SoilCdStatistics.averageValue'), type: "bar",
+        itemStyle: { color: (p) => currentFieldConfig.pollution[p.dataIndex].color },
         data: barData
       }]
     });
@@ -195,7 +194,8 @@ const initNutrientChart = () => {
     if (!nutrientBoxChart.value) return;
     chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
     
-    const xAxisData = fieldConfig.nutrient.map(f => f.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.nutrient.map(f => f.name);
     const boxData = buildBoxplotData(nutrientStats.value);
     
     chartInstanceNutrient.setOption({
@@ -204,7 +204,7 @@ const initNutrientChart = () => {
         trigger: "item",
         formatter: (params) => {
           const stat = nutrientStats.value[params.dataIndex];
-          const fieldConfigItem = fieldConfig.nutrient.find(f => f.key === stat.key);
+          const fieldConfigItem = currentFieldConfig.nutrient.find(f => f.key === stat.key);
           return formatTooltip(stat, fieldConfigItem?.unit);
         }
       },
@@ -232,30 +232,32 @@ const initNutrientChart = () => {
 
 // 初始化其他理化性质图表
 const initExtraChart = () => {
-  const xAxisData = fieldConfig.extra.map(f => f.name);
+  const currentFieldConfig = getFieldConfig();
+  const xAxisData = currentFieldConfig.extra.map(f => f.name);
   const boxData = buildBoxplotData(extraStats.value);
   
-    nextTick(() => {
-      if (chartInstanceExtra) chartInstanceExtra.dispose();
-      chartInstanceExtra = echarts.init(extraBoxChart.value);
-      chartInstanceExtra.setOption({
-         title: { text: t('SoilCdStatistics.propertiesDistribution'), left: 'center', textStyle: { fontSize: 14 } },
-         tooltip: {
-           trigger: "item",
-           formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
-        },
-         grid: { top: 40, right: 15, bottom: 40, left: 40 },
-         xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11} },
-        yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
-        series: [{
-          name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
-           itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
-           data: boxData
-        }]
-        });
-      });
+  nextTick(() => {
+    if (chartInstanceExtra) chartInstanceExtra.dispose();
+    chartInstanceExtra = echarts.init(extraBoxChart.value);
+    chartInstanceExtra.setOption({
+      title: { text: t('SoilCdStatistics.propertiesDistribution'), left: 'center', textStyle: { fontSize: 14 } },
+      tooltip: {
+        trigger: "item",
+        formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
+      },
+      grid: { top: 40, right: 15, bottom: 40, left: 40 },
+      xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11 } },
+      yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
+      series: [{
+        name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
+        itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
+        data: boxData
+      }]
+    });
+  });
 };
 
+
 // 格式化 Tooltip
 const formatTooltip = (stat, unit = '') => {
   if (!stat || !stat.min) {
@@ -271,17 +273,19 @@ const formatTooltip = (stat, unit = '') => {
     </div>`;
 };
 
+
 // 初始化图表主流程
 const initCharts = async () => {
   try {
     isLoading.value = true;
     error.value = null;
     
-    const statsData = await fetchData(); // 新接口返回的统计数据
+    const statsData = await fetchData();
+    const currentFieldConfig = getFieldConfig();
     
-    // -------- 1. 处理「作物态Cd指标」统计 --------
-    pollutionStats.value = fieldConfig.pollution.map(field => {
-      const fieldStats = statsData[field.key]; // 从接口数据中取对应字段的统计
+    // -------- 1. 处理「作物态 Cd 指标」统计 --------
+    pollutionStats.value = currentFieldConfig.pollution.map(field => {
+      const fieldStats = statsData[field.key];
       if (!fieldStats) {
         return { 
           key: field.key, 
@@ -294,7 +298,6 @@ const initCharts = async () => {
           avg: null 
         };
       }
-      // (可选)单位转换:若接口返回原始值,需按fieldConfig的convert规则转换
       let min = fieldStats.min;
       let q1 = fieldStats.q1;
       let median = fieldStats.median;
@@ -322,7 +325,7 @@ const initCharts = async () => {
     });
 
     // -------- 2. 处理「主要养分元素」统计 --------
-    nutrientStats.value = fieldConfig.nutrient.map(field => {
+    nutrientStats.value = currentFieldConfig.nutrient.map(field => {
       const fieldStats = statsData[field.key];
       if (!fieldStats) {
         return { 
@@ -336,7 +339,6 @@ const initCharts = async () => {
           avg: null 
         };
       }
-      // (可选)单位转换
       let min = fieldStats.min;
       let q1 = fieldStats.q1;
       let median = fieldStats.median;
@@ -364,7 +366,7 @@ const initCharts = async () => {
     });
 
     // -------- 3. 处理「其他理化性质」统计 --------
-    extraStats.value = fieldConfig.extra.map(field => {
+    extraStats.value = currentFieldConfig.extra.map(field => {
       const fieldStats = statsData[field.key];
       if (!fieldStats) {
         return { 
@@ -378,7 +380,6 @@ const initCharts = async () => {
           avg: null 
         };
       }
-      // (可选)单位转换
       let min = fieldStats.min;
       let q1 = fieldStats.q1;
       let median = fieldStats.median;
@@ -405,13 +406,13 @@ const initCharts = async () => {
       };
     });
 
-    // -------- 更新「样本数量」等汇总统计 --------
-    const firstFieldKey = fieldConfig.pollution[0]?.key;
+    // -------- 更新汇总统计 --------
+    const firstFieldKey = currentFieldConfig.pollution[0]?.key;
     stats.value = {
       cd002Avg: pollutionStats.value.find(s => s.key === '002_0002IDW')?.avg || 0,
       cd02Avg: pollutionStats.value.find(s => s.key === '02_002IDW')?.avg || 0,
       cd2Avg: pollutionStats.value.find(s => s.key === '2_02IDW')?.avg || 0,
-      samples: statsData[firstFieldKey]?.count || 0 // 从接口的count字段取样本数
+      samples: statsData[firstFieldKey]?.count || 0
     };
 
     // 初始化图表
@@ -427,6 +428,7 @@ const initCharts = async () => {
   }
 };
 
+
 // 组件挂载
 onMounted(() => {
   initCharts();
@@ -445,6 +447,12 @@ onMounted(() => {
       .forEach(inst => inst && inst.dispose());
   };
 });
+
+// 监听语言变化
+watch(locale, () => {
+  console.log('语言切换,重新初始化图表');
+  initCharts();
+});
 </script>
 
 <style>

+ 48 - 34
src/components/soilcdStatistics/effcdStatistics.vue

@@ -50,6 +50,7 @@ import { api8000 } from '@/utils/request'; // 导入 api8000 实例
 import { useI18n } from 'vue-i18n';
 
 const { t } = useI18n();
+const { locale } = useI18n();
 
 // 图表实例引用
 const cdBarChart = ref(null);
@@ -75,45 +76,45 @@ const pollutionStats = ref([]);       // 污染指标统计(总镉/有效态
 const nutrientStats = ref([]);        // 养分元素统计
 const extraStats = ref([]);           // 其他理化性质统计
 
-// 字段配置
-const fieldConfig = {
+// 获取字段配置的函数(每次调用都会使用最新的翻译)
+const getFieldConfig = () => ({
   pollution: [
-    { 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},
+    { 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: 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}
+    { 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: 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}
+    { key: 'TFe_IDW', name: t('SoilCdStatistics.totalIron'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TCa_IDW', name: t('SoilCdStatistics.totalCalcium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TMg_IDW', name: t('SoilCdStatistics.totalMagnesium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
+    { key: 'TAl_IDW', name: t('SoilCdStatistics.totalAluminum'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false }
   ]
-};
+});
 
 const fetchData = async () => {
   try {
     isLoading.value = true;
     const apiUrl = '/api/vector/stats/EffCd_input_data';
-    console.log('正在请求数据:', apiUrl);
+    // console.log('正在请求数据:', apiUrl);
     
     const response = await api8000.get(apiUrl);
-    console.log('API响应:', response);
+    // console.log('API响应:', response);
     
     // 调试:输出响应结构
-    console.log('响应数据:', response.data);
+    // console.log('响应数据:', response.data);
     
     // 处理不同的响应格式
     let processedData;
@@ -123,7 +124,7 @@ const fetchData = async () => {
       throw new Error('无法解析API返回的数据结构');
     }
     
-    console.log('处理后的数据:', processedData);
+    // console.log('处理后的数据:', processedData);
     return processedData;
   } catch (err) {
     console.error('数据请求失败:', err);
@@ -181,18 +182,20 @@ const calculateFieldStats = (statsData, fieldKey, fieldName, fieldConfigItem) =>
 
 // 批量计算所有字段的统计量(按图表类型缓存)
 const calculateAllStats = (statsData) => {
+  const currentFieldConfig = getFieldConfig(); // 获取最新的字段配置
+
   // 1. 污染指标统计(与x轴顺序一致)
-  pollutionStats.value = fieldConfig.pollution.map(field => 
+  pollutionStats.value = currentFieldConfig.pollution.map(field => 
     calculateFieldStats(statsData, field.key, field.name, field)
   );
   
   // 2. 养分元素统计(与x轴顺序一致)
-  nutrientStats.value = fieldConfig.nutrient.map(field => 
+  nutrientStats.value = currentFieldConfig.nutrient.map(field => 
     calculateFieldStats(statsData, field.key, field.name, field)
   );
   
   // 3. 其他理化性质统计(与x轴顺序一致)
-  extraStats.value = fieldConfig.extra.map(field => 
+  extraStats.value = currentFieldConfig.extra.map(field => 
     calculateFieldStats(statsData, field.key, field.name, field)
   );
   
@@ -219,8 +222,9 @@ const initPollutionChart = () => {
   if (chartInstanceCd) chartInstanceCd.dispose();
   chartInstanceCd = echarts.init(cdBarChart.value);
   
+  const currentFieldConfig = getFieldConfig();
   // 提取x轴标签和数据
-  const xAxisData = fieldConfig.pollution.map(f => f.name);
+  const xAxisData = currentFieldConfig.pollution.map(f => f.name);
   const barData = pollutionStats.value.map(stat => stat.mean);
   
   chartInstanceCd.setOption({
@@ -248,7 +252,7 @@ const initPollutionChart = () => {
     },
     series: [{
       name: t('SoilCdStatistics.averageValue'), type: "bar",
-      itemStyle: {color: (params) => fieldConfig.pollution[params.dataIndex].color },
+      itemStyle: {color: (params) => currentFieldConfig.pollution[params.dataIndex].color },
       data: barData
     }]
   });
@@ -259,7 +263,9 @@ const initNutrientChart = () => {
   if (chartInstanceNutrient) chartInstanceNutrient.dispose();
   chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
   
-  const xAxisData = fieldConfig.nutrient.map(f => f.name);
+  const currentFieldConfig = getFieldConfig();
+
+  const xAxisData = currentFieldConfig.nutrient.map(f => f.name);
   const boxData = buildBoxplotData(nutrientStats.value);
   
   chartInstanceNutrient.setOption({
@@ -287,7 +293,7 @@ const initNutrientChart = () => {
     },
     series: [{
       name: t('SoilCdStatistics.mainNutrients'), type: "boxplot",
-      itemStyle: { color: (params) => fieldConfig.nutrient[params.dataIndex].color },
+      itemStyle: { color: (params) => currentFieldConfig.nutrient[params.dataIndex].color },
       data: boxData
     }]
   });
@@ -295,7 +301,9 @@ const initNutrientChart = () => {
 
 // 初始化其他理化性质图表(箱线图)
 const initExtraChart = () => {
-  const xAxisData = fieldConfig.extra.map(f => f.name);
+  const currentFieldConfig = getFieldConfig();
+
+  const xAxisData = currentFieldConfig.extra.map(f => f.name);
   const boxData = buildBoxplotData(extraStats.value);
   
       nextTick(() => {
@@ -312,7 +320,7 @@ const initExtraChart = () => {
           yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
           series: [{
             name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
-            itemStyle: { color: (params) => fieldConfig.extra[params.dataIndex].color },
+            itemStyle: { color: (params) => currentFieldConfig.extra[params.dataIndex].color },
             data: boxData
           }]
         });
@@ -358,6 +366,12 @@ const initCharts = async () => {
   }
 };
 
+// 监听语言变化
+watch(locale, () => {
+  // 语言切换后重新初始化所有图表
+  initCharts();
+});
+
 // 组件挂载
 onMounted(() => {
   initCharts();

+ 36 - 31
src/components/soilcdStatistics/fluxcdStatictics.vue

@@ -54,6 +54,7 @@ import * as echarts from 'echarts';
 import { api8000 } from '@/utils/request'; // 导入 api8000 实例
 import { useI18n } from 'vue-i18n';
 
+const {locale} = useI18n()
 const { t } = useI18n();
 
 // 图表容器 & 实例
@@ -72,7 +73,7 @@ const initialCdStats = ref([]);
 const otherIndicatorsStats = ref([]);
 
 // 字段配置
-const fieldConfig = {
+const getFieldConfig = () => ({
   initialCd: [
     { key: 'Initial_Cd', name: t('SoilCdStatistics.initialCadmiumTotal'), color: '#5470c6' }
   ],
@@ -84,19 +85,19 @@ const fieldConfig = {
     { key: 'DB_Cd', name: t('SoilCdStatistics.surfaceRunoff'), color: '#38b2ac' },
     { key: 'ZL_Cd', name: t('SoilCdStatistics.grainRemoval'), color: '#4169e1' },
   ]
-};
+});
 
 const fetchData = async () => {
   try {
     isLoading.value = true;
     const apiUrl = '/api/vector/stats/FluxCd_input_data';
-    console.log('正在请求数据:', apiUrl);
+    // console.log('正在请求数据:', apiUrl);
     
     const response = await api8000.get(apiUrl);
-    console.log('API响应:', response);
+    // console.log('API响应:', response);
     
     // 调试:输出响应结构
-    console.log('响应数据:', response.data);
+    // console.log('响应数据:', response.data);
     
     // 处理不同的响应格式
     let processedData;
@@ -106,7 +107,7 @@ const fetchData = async () => {
       throw new Error('无法解析API返回的数据结构');
     }
     
-    console.log('处理后的数据:', processedData);
+    // console.log('处理后的数据:', processedData);
     return processedData;
   } catch (err) {
     console.error('数据请求失败:', err);
@@ -145,29 +146,24 @@ const calculateFieldStats = (statsData, fieldKey, fieldName) => {
 
 // 计算所有统计数据
 const calculateAllStats = (data) => {
-  console.log('计算统计数据,输入数据:', data);
+  const currentFieldConfig = getFieldConfig();
   
-  // 初始Cd统计
-  initialCdStats.value = fieldConfig.initialCd.map(indicator => 
+  // 初始 Cd 统计
+  initialCdStats.value = currentFieldConfig.initialCd.map(indicator => 
     calculateFieldStats(data, indicator.key, indicator.name)
   );
   
   // 其他指标统计
-  otherIndicatorsStats.value = fieldConfig.otherIndicators.map(indicator => 
+  otherIndicatorsStats.value = currentFieldConfig.otherIndicators.map(indicator => 
     calculateFieldStats(data, indicator.key, indicator.name)
   );
   
   // 更新样本数
   const firstStat = initialCdStats.value[0] || otherIndicatorsStats.value[0];
   stats.value.samples = firstStat?.count || 0;
-  
-  console.log('计算后的统计数据:', {
-    initialCdStats: initialCdStats.value,
-    otherIndicatorsStats: otherIndicatorsStats.value,
-    sampleCount: stats.value.samples
-  });
 };
 
+
 // 构建箱线图数据
 const buildBoxplotData = (statsArray) => {
   return statsArray.map(stat => {
@@ -178,7 +174,7 @@ const buildBoxplotData = (statsArray) => {
   });
 };
 
-// 初始化【初始Cd】图表
+// 初始化【初始 Cd】图表
 const initInitialCdChart = () => {
   if (!initialCdChart.value) {
     error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
@@ -191,11 +187,12 @@ const initInitialCdChart = () => {
     }
     
     chartInstanceInitial.value = echarts.init(initialCdChart.value);
-    const xAxisData = fieldConfig.initialCd.map(ind => ind.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.initialCd.map(ind => ind.name);
     const boxData = buildBoxplotData(initialCdStats.value);
     
     const option = {
-      title: { text:  t('SoilCdStatistics.initialCdDistribution'), left: 'center' },
+      title: { text: t('SoilCdStatistics.initialCdDistribution'), left: 'center' },
       tooltip: {
         trigger: "item",
         formatter: (params) => {
@@ -215,7 +212,7 @@ const initInitialCdChart = () => {
       series: [{
         type: "boxplot",
         itemStyle: { 
-          color: fieldConfig.initialCd[0].color,
+          color: currentFieldConfig.initialCd[0].color,
           borderWidth: 2 
         },
         data: boxData
@@ -242,7 +239,8 @@ const initOtherIndicatorsChart = () => {
     }
     
     chartInstanceOther.value = echarts.init(otherIndicatorsChart.value);
-    const xAxisData = fieldConfig.otherIndicators.map(ind => ind.name);
+    const currentFieldConfig = getFieldConfig();
+    const xAxisData = currentFieldConfig.otherIndicators.map(ind => ind.name);
     const boxData = buildBoxplotData(otherIndicatorsStats.value);
     
     const option = {
@@ -270,7 +268,7 @@ const initOtherIndicatorsChart = () => {
       series: [{
         type: "boxplot",
         itemStyle: { 
-          color: (params) => fieldConfig.otherIndicators[params.dataIndex].color,
+          color: (params) => currentFieldConfig.otherIndicators[params.dataIndex].color,
           borderWidth: 2 
         },
         data: boxData
@@ -284,6 +282,7 @@ const initOtherIndicatorsChart = () => {
   }
 };
 
+
 // Tooltip格式化
 const formatTooltip = (stat) => {
   if (!stat || stat.min === null) {
@@ -292,12 +291,11 @@ const formatTooltip = (stat) => {
   
   return `<div style="font-weight:bold">${stat.name}</div>
     <div style="margin-top:8px">
-      <div>最小值:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
-      <div>下四分位:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
-      <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
-      <div>上四分位:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
-      <div>最大值:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
-      <div>样本数:<span style="color:#909399;">${stat.count}</span></div>
+      <div>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
+      <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
     </div>`;
 };
 
@@ -307,11 +305,11 @@ const initCharts = async () => {
     isLoading.value = true;
     error.value = null;
     
-    console.log('开始初始化图表...');
+    // console.log('开始初始化图表...');
     
     // 获取数据
     const data = await fetchData();
-    console.log('获取到的数据:', data);
+    // console.log('获取到的数据:', data);
     
     if (!data || (Array.isArray(data) && data.length === 0)) {
       throw new Error('未获取到有效数据');
@@ -328,7 +326,7 @@ const initCharts = async () => {
     initOtherIndicatorsChart();
     
     isLoading.value = false;
-    console.log('图表初始化完成');
+    // console.log('图表初始化完成');
     
   } catch (err) {
     isLoading.value = false;
@@ -337,6 +335,7 @@ const initCharts = async () => {
   }
 };
 
+
 // 组件生命周期
 onMounted(() => {
   initCharts();
@@ -348,6 +347,12 @@ onMounted(() => {
   
   window.addEventListener('resize', handleResize);
 });
+
+// 监听语言变化
+watch(locale, () => {
+  // 语言切换后重新初始化所有图表
+  initCharts();
+});
 </script>
 
 <style scoped>

+ 21 - 1
src/locales/en.json

@@ -272,6 +272,26 @@
     "reloadInit": "Reinitialize",
     "chartInitFailed": "Chart initialization failed",
     "dataLoadFailed": "Data loading failed"
+  },
+  "LandCultivatedStatistics": {
+    "title": "Cultivated Land Quality Assessment by Region Classification Statistics",
+    "priorityProtection": "Priority Protection",
+    "strictControl": "Strict Control",
+    "safeUtilization": "Safe Utilization",
+    "allRegions": "All Regions",
+    "quantity": "Quantity"
+  },
+  "AcidificationDataStatistics": {
+    "refluxTitle": "Acidification Aggravation Delta_pH_105day Bar Chart",
+    "reductionTitle": "Acidification Mitigation Q_delta_pH Bar Chart",
+    "id": "ID",
+    "deltaPH": "Delta_pH_105day",
+    "qDeltaPH": "Q_delta_pH",
+    "dataLoadFailed": "Data loading failed",
+    "noValidData": "No valid data",
+    "chartInitFailed": "Chart initialization failed",
+    "requestError": "Request error, please try again later",
+    "networkError": "No response received, please check network connection",
+    "apiError": "API returned failure status"
   }
-  
 }

+ 21 - 0
src/locales/zh.json

@@ -272,5 +272,26 @@
     "reloadInit": "重新尝试初始化",
     "chartInitFailed": "图表初始化失败",
     "dataLoadFailed": "数据加载失败"
+  },
+  "LandCultivatedStatistics": {
+    "title": "耕地质量评估按区域分类数据统计",
+    "priorityProtection": "优先保护类",
+    "strictControl": "严格管控类",
+    "safeUtilization": "安全利用类",
+    "allRegions": "全部县",
+    "quantity": "数量"
+  },
+  "AcidificationDataStatistics": {
+    "refluxTitle": "酸化加剧 Delta_pH_105day 柱状图",
+    "reductionTitle": "酸化缓解 Q_delta_pH 柱状图",
+    "id": "ID",
+    "deltaPH": "Delta_pH_105day",
+    "qDeltaPH": "Q_delta_pH",
+    "dataLoadFailed": "数据加载失败",
+    "noValidData": "无有效数据",
+    "chartInitFailed": "图表初始化失败",
+    "requestError": "请求发生错误,请稍后重试",
+    "networkError": "未收到响应,请检查网络连接",
+    "apiError": "API 返回失败状态"
   }
 }

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

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

+ 3 - 2
vite.config.ts

@@ -11,10 +11,10 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 import Icons from 'unplugin-icons/vite'
 import IconsResolver from 'unplugin-icons/resolver'
 
-import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 
+import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
 import path from 'path'
 
-const  __dirname = path.dirname(fileURLToPath(import.meta.url));
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 // https://vite.dev/config/
 export default defineConfig({
@@ -51,6 +51,7 @@ export default defineConfig({
         },
       },
     },
+    sourcemap: true,
   },
   resolve: {
     alias: {