Bladeren bron

添加地图合一部分统计图

yes-yes-yes-k 4 dagen geleden
bovenliggende
commit
1ce6ec3837
1 gewijzigde bestanden met toevoegingen van 570 en 78 verwijderingen
  1. 570 78
      src/views/User/introduction/TotalIntroduction.vue

+ 570 - 78
src/views/User/introduction/TotalIntroduction.vue

@@ -1,5 +1,5 @@
 <script setup>
-   import { ref, watch, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
+   import { ref, watch, onMounted, onBeforeUnmount, onUnmounted ,computed,nextTick} from 'vue'
    import L from 'leaflet'
    import 'leaflet/dist/leaflet.css'
    import { api8000 } from '@/utils/request'
@@ -8,6 +8,8 @@
    const mapRef = ref(null)
    let map = null
    let wmsLayer = null
+   let geoJsonLayer = null
+
 
    const statistics = ref({
      totalBlocks: 0,
@@ -19,9 +21,18 @@
      minPH: 0
    })
 
+  //  
+   const phDistribution = ref({
+     range1: 0,
+     range2: 0,
+     range3: 0,
+   })
+
+   const selectedPoint = ref(null)
+
    const CONFIG = {
     center:[25.202903, 113.25383],
-    zoom:9.5,
+    zoom:11,
     getPoint:'/api/vector/export/all?table_name=le_soil_data&format=geojson',
     geoserver:{
       url:'/geoserver',
@@ -35,11 +46,13 @@
 
    // 获取 pH 等级对应的 CSS 类
 function getPHLevelClass(ph) {
-  if (!ph || ph === 0) return ''
-  if (ph <= 5.2) return 'danger'
-  if (ph < 6.0) return 'warning'
-  return 'success'
-}
+    // ✅ 先转换为数字
+    const numericPh = typeof ph === 'string' ? parseFloat(ph) : ph
+    if (!numericPh || numericPh === 0) return ''
+    if (numericPh <= 5.2) return 'danger'
+    if (numericPh < 6.0) return 'warning'
+    return 'success'
+  }
 
 
 // 获取综合评语
@@ -50,6 +63,20 @@ function getPHComment(avgPH) {
   return '🟢 土壤酸碱度适宜,状态良好'
 }
 
+  
+  function getPHRangeLabel(range) {
+  const labels = {
+    range1: '< 5.2',
+    range2: '5.2 - 6.0',
+    range3: ' >6.0 ',
+  }
+  return labels[range] || range
+  }
+
+  // 添加分页加载支持
+   const batchSize = 1000;
+   let allFeatures = [];
+
   async function initMap(){
     await nextTick()
 
@@ -67,90 +94,187 @@ function getPHComment(avgPH) {
 
   }
 
-  async function loadLeSoilData() {
+  function createPointIcon(feature) {
+    const ph = feature.properties.ph || feature.properties.value
+    let color = '#22c55e'
+    if (ph <= 5.2) color = '#ef4444'
+    else if (ph < 6.0) color = '#f59e0b'
+
+    return L.divIcon({
+      className: 'custom-marker',
+      html: `<div style="
+        background-color: ${color};
+        width: 12px;
+        height: 12px;
+        border-radius: 50%;
+        border: 2px solid white;
+        box-shadow: 0 2px 4px rgba(0,0,0,0.3);
+      "></div>`,
+      iconSize: [12, 12],
+      iconAnchor: [6, 6]
+    })
+  }
+
+  function parsePHValue(phValue) {
+    if (!phValue && phValue !== 0) return null
+    const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
+    return !isNaN(numericPh) && numericPh > 0 ? numericPh : null
+  }
+
+
+  async function loadStatistics() {
   try {
-    const response = await fetch(
-      `${CONFIG.geoserver.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${CONFIG.geoserver.workspace}:${CONFIG.geoserver.dataLayer}&outputFormat=application/json`
-    );
-    
-    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
-    
-    const geoJsonData = await response.json();
-    
-    console.log('✅ le_soil_data 数据:', geoJsonData.features);
-
-    if (geoJsonData.features && geoJsonData.features.length > 0) {
-      // 处理数据...
-      geoJsonData.features.forEach(feature => {
-        const ph = feature.properties.ph || feature.properties.value;
-        if (ph && ph > 0) {
-          // console.log('📌 采样点:', feature.properties.TXZQMC, 'pH:', ph);
-        }
-      });
-    } else {
-      console.warn('⚠️ 没有找到 le_soil_data 数据');
-    }
+    // 等待数据加载完成
+    if (samplePointsData.value.length === 0) return;
+
+    let phCount = 0
+    let avgPH = 0
+    let strongAcidCount = 0
+    let mildAcidCount = 0
+    let normalCount = 0
+    let maxPH = -Infinity
+    let minPH = Infinity
+
+    samplePointsData.value.forEach(feature => {
+      const numericPh = parsePHValue(feature.properties.ph || feature.properties.value)
+      
+      if (numericPh && numericPh > 0 && !isNaN(numericPh)) {
+        phCount++
+        const delta = numericPh - avgPH;
+        avgPH = avgPH + delta / phCount;
+        maxPH = Math.max(maxPH, numericPh)
+        minPH = Math.min(minPH, numericPh)
+        
+        if (numericPh <= 5.2) strongAcidCount++
+        else if (numericPh < 6.0) mildAcidCount++
+        else normalCount++
+      }
+    });
+
+    statistics.value = {
+      totalBlocks: samplePointsData.value.length,
+      avgPH:  phCount > 0 ? parseFloat(avgPH.toFixed(2)) : 0,  // ✅ 确保是数字
+      strongAcidCount,
+      mildAcidCount,
+      normalCount,
+      maxPH: maxPH === -Infinity ? 0 : parseFloat(maxPH.toFixed(2)),
+      minPH: minPH === Infinity ? 0 : parseFloat(minPH.toFixed(2))
+    };
+
+    console.log('✅ 统计数据加载完成:', statistics.value);
   } catch (err) {
-    console.error('❌ 加载 le_soil_data 失败:', err);
+    console.error('加载统计数据失败:', err);
   }
 }
 
-  async function loadStatistics() {
+// 修改为只获取数据用于统计,不渲染到地图
+  async function fetchDataForStatistics() {
     try {
       const response = await fetch(
         `${CONFIG.geoserver.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${CONFIG.geoserver.workspace}:${CONFIG.geoserver.dataLayer}&outputFormat=application/json`
       );
+      
+      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+      
       const geoJsonData = await response.json();
+      
+      console.log('✅ 获取到数据:', geoJsonData.features.length, '条记录');
 
-      if (geoJsonData.features && geoJsonData.features.length > 0) {
-        let phCount = 0
-        let avgPH = 0
-        let strongAcidCount = 0
-        let mildAcidCount = 0
-        let normalCount = 0
-        let maxPH = -Infinity
-        let minPH = Infinity
+      // 保存数据用于统计和交互
+      allFeatures = geoJsonData.features;
+      samplePointsData.value = geoJsonData.features;
+      
+      // 计算统计信息
+      calculatePHDistribution(allFeatures);
+      loadStatistics();
+      
+      // 添加点击事件监听(不显示标记点)
+      map.on('click', function(e) {
+        const latlng = e.latlng;
+        // 查找最近的采样点
+        findNearestPoint(latlng);
+      });
+      
+    } catch (err) {
+      console.error('❌ 加载统计数据失败:', err);
+    }
+  }
 
+  // 计算 pH 分布
+  function calculatePHDistribution(features) {
+    const distribution = {
+      range1: 0,
+      range2: 0,
+      range3: 0
+    }
+
+    features.forEach(feature => {
+      const phValue = feature.properties.ph || feature.properties.value
+      const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
+      if (numericPh && numericPh > 0) {
+        if (numericPh <= 5.2) distribution.range1++
+        else if (numericPh <= 6) distribution.range2++
+        else  distribution.range3++
+      }
+    })
+
+    phDistribution.value = distribution
+  }
+
+  // 查找最近的采样点
+  function findNearestPoint(latlng) {
+    let nearestPoint = null;
+    let minDistance = Infinity;
+    
+    allFeatures.forEach(feature => {
+      const coords = feature.geometry.coordinates;
+      if (coords && coords.length >= 2) {
+        const pointLat = coords[1];
+        const pointLng = coords[0];
+        const distance = Math.sqrt(
+          Math.pow(pointLat - latlng.lat, 2) + 
+          Math.pow(pointLng - latlng.lng, 2)
+        );
         
-        geoJsonData.features.forEach(feature => {
-          const ph = feature.properties.ph || feature.properties.value
-          if (ph && ph > 0) {
-            phCount++
-
-            // ✅ 使用在线算法计算平均值,避免大数累加
-            const delta = ph - avgPH;
-            avgPH = avgPH + delta / phCount;
-
-
-            maxPH = Math.max(maxPH, ph)
-            minPH = Math.min(minPH, ph)
-            
-            if (ph <= 5.2) strongAcidCount++
-            else if (ph < 6.0) mildAcidCount++
-            else normalCount++
-          }
-        });
-
-        statistics.value = {
-          totalBlocks: geoJsonData.features.length,
-          avgPH:avgPH.toFixed(2),
-          strongAcidCount,
-          mildAcidCount,
-          normalCount,
-          maxPH: maxPH === -Infinity ? 0 : parseFloat(maxPH.toFixed(2)),
-          minPH: minPH === Infinity ? 0 : parseFloat(minPH.toFixed(2))
-        };
-
-        console.log('✅ 统计数据加载完成:', statistics.value);
+        if (distance < minDistance && distance < 0.01) { // 10 米范围内
+          minDistance = distance;
+          nearestPoint = feature;
+        }
       }
-    } catch (err) {
-      console.error('加载统计数据失败:', err);
+    });
+    
+    if (nearestPoint) {
+      const ph = parsePHValue(nearestPoint.properties.ph || nearestPoint.properties.value);
+      
+      selectedPoint.value = {
+        ph: ph,
+        properties: nearestPoint.properties
+      };
     }
   }
 
+
+onUnmounted(()=>{
+    if(map) {
+      map.remove()
+      map = null
+    }
+    if(wmsLayer) {
+      wmsLayer = null
+    }
+    if(markersLayer) {
+      markersLayer = null
+    }
+    if(geoJsonLayer) { // 原 markersLayer 改为 geoJsonLayer(代码中实际定义的是 geoJsonLayer)
+      geoJsonLayer = null
+    }
+    samplePointsData.value = []
+    selectedPoint.value = null
+  })
+
   onMounted(async()=>{
     await initMap()
-    await loadLeSoilData()
+    await fetchDataForStatistics()
     await loadStatistics()
   })
 
@@ -164,6 +288,12 @@ function getPHComment(avgPH) {
   <div class="map-container">
     <div class="ph-map" ref="mapRef"></div>
 
+    <!-- 计算刷新按钮 -->
+     <div class="compute">
+      <button class="combtn">实施降酸措施一周期后</button>
+      <button class="combtn">反酸一周期后</button>
+     </div>
+    <!-- pH 统计 -->
     <div class="statistics-panel">
       <h4>📊 乐昌县土壤 pH 统计</h4>
       
@@ -174,7 +304,7 @@ function getPHComment(avgPH) {
       
       <div class="stat-row highlight">
         <span class="stat-label">平均 pH 值:</span>
-        <span class="stat-value" :class="getPHLevelClass(statistics.avgPH)">
+        <span class="stat-value" :class="getPHLevelClass(parseFloat(statistics.avgPH))">
           {{ statistics.avgPH || '-' }}
         </span>
       </div>
@@ -214,8 +344,35 @@ function getPHComment(avgPH) {
       </div>
     </div>
 
+    <!-- ph分布 -->
+    <div class="distribution-chart">
+      <h4>📈 pH 分布</h4>
+      <div class="bar-chart">
+        <div 
+          v-for="(value, key) in phDistribution" 
+          :key="key"
+          class="bar-item"
+        >
+          <div class="bar-label">{{ getPHRangeLabel(key) }}</div>
+          <div class="bar-container">
+            <div 
+              class="bar-fill" 
+              :style="{ width: `${(value / statistics.totalBlocks) * 100}%` }"
+              :class="
+              {
+               danger: key === 'range1',
+               warning: key === 'range2',
+               success: key === 'range3'
+              }
+              "
+            ></div>
+            <span class="bar-value">{{ value }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
     
-    
+    <!-- 图例 -->
     <div class="legend">
       <h4>pH 值图例</h4>
       <div class="legend-item">
@@ -235,6 +392,35 @@ function getPHComment(avgPH) {
         <span>无数据</span>
       </div>
     </div>
+
+    <!-- 采样点详情 -->
+    <div class="point-detail-modal" v-if="selectedPoint">
+      <div class="detail-content">
+        <div class="detail-header">
+          <h4>📍 采样点详情</h4>
+          <button @click="selectedPoint = null" class="close-btn">×</button>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">pH 值:</span>
+          <span class="detail-value" :class="getPHLevelClass(selectedPoint.ph)">
+            {{ selectedPoint.ph?.toFixed(2) || '-' }}
+          </span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">酸化程度:</span>
+          <span :class="['detail-value', getPHLevelClass(selectedPoint.ph)]">
+            {{ selectedPoint.ph <= 5.2 ? '强酸性' : selectedPoint.ph < 6.0 ? '弱酸性' : '正常' }}
+          </span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">建议:</span>
+          <span class="detail-suggestion">
+            {{ selectedPoint.ph <= 5.2 ? '立即治理,施用石灰改良' : selectedPoint.ph < 6.0 ? '注意保持,适量施用有机肥' : '继续保持当前管理措施' }}
+          </span>
+        </div>
+      </div>
+    </div>
+
    </div>
 </template>
 
@@ -249,26 +435,65 @@ function getPHComment(avgPH) {
 }
 
 .ph-map{
-  width: 60%;
+  width: 65%;
   height: 95%;
   min-height: 500px;
+  border-radius: 16px; /* 圆角大小,可根据需要调整,比如 10px、20px */
+  border: 3px solid #1092d8; /* 蓝色边框,宽度和颜色可自定义 */
+  overflow: hidden; /* 关键:防止地图内容溢出圆角区域 */
 }
 
 /* ✅ 统计面板样式 */
 .statistics-panel {
   position: absolute;
-  top: 20px;
+  top: 100px;
   right: 10px;
   background: rgba(255, 255, 255, 0.95);
   padding: 20px;
   border-radius: 12px;
   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
   z-index: 1000;
-  min-width: 280px;
+  min-width: 350px;
   backdrop-filter: blur(10px);
   border: 2px solid rgba(16, 146, 216, 0.2);
 }
 
+.compute {
+  position: absolute;
+  display: flex;
+  gap: 15px;
+  top:5px;
+  right: 20px;
+  padding: 20px;
+  z-index: 1000;
+}
+
+.combtn {
+  padding: 15px 20px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #fff;
+  background: linear-gradient(135deg, #1092d8 0%, #0d7bb8 100%);
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  box-shadow: 0 4px 12px rgba(16, 146, 216, 0.3);
+  transition: all 0.3s ease;
+  white-space: nowrap;
+}
+
+.combtn:hover {
+  background: linear-gradient(135deg, #0d7bb8 0%, #0a6598 100%);
+  box-shadow: 0 6px 16px rgba(16, 146, 216, 0.4);
+  transform: translateY(-2px);
+}
+
+.combtn:active {
+  transform: translateY(0);
+  box-shadow: 0 2px 8px rgba(16, 146, 216, 0.3);
+}
+
+
 .statistics-panel h4 {
   margin: 0 0 15px 0;
   font-size: 18px;
@@ -284,6 +509,7 @@ function getPHComment(avgPH) {
   align-items: center;
   margin-bottom: 12px;
   font-size: 14px;
+  line-height: 1.4; /* 新增行高,提升可读性 */
 }
 
 .stat-row.highlight {
@@ -304,9 +530,10 @@ function getPHComment(avgPH) {
 }
 
 .stat-value {
-  font-weight: bold;
+  font-weight: 600;
   font-size: 16px;
   color: #333;
+  letter-spacing: 0.5px; /* 新增字间距 */
 }
 
 .stat-value.danger {
@@ -361,6 +588,158 @@ function getPHComment(avgPH) {
   color: #22c55e;
 }
 
+.distribution-chart {
+  position: absolute;
+  top:600px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 350px;
+  backdrop-filter: blur(10px);
+}
+
+.distribution-chart h4 {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 8px;
+}
+
+.bar-chart {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.bar-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.bar-label {
+  font-size: 11px;
+  color: #666;
+  min-width: 60px;
+}
+
+.bar-container {
+  flex: 1;
+  position: relative;
+  height: 20px;
+  background: #f0f0f0;
+  border-radius: 4px;
+  overflow: hidden;
+  border: 1px solid #e5e7eb;
+}
+
+.bar-fill {
+  height: 100%;
+  transition: width 0.3s ease-in-out; /* 缓动动画更丝滑 */
+  border-radius: 3px; /* 圆角匹配容器 */
+}
+
+.bar-fill.danger {
+  background: #ef4444;
+}
+
+.bar-fill.warning {
+  background: #f59e0b;
+}
+
+.bar-fill.success {
+  background: #22c55e;
+}
+
+.bar-value {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 11px;
+  font-weight: bold;
+  color: black; 
+  text-shadow: 0 1px 2px rgba(0,0,0,0.2); /* 新增文字阴影 */
+}
+
+.alert-panel {
+  position: absolute;
+  top: 20px;
+  left: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 250px;
+  max-height: 300px;
+  overflow-y: auto;
+  backdrop-filter: blur(10px);
+}
+
+.alert-panel h4 {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  color: #ef4444;
+  border-bottom: 2px solid #ef4444;
+  padding-bottom: 8px;
+}
+
+.alert-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.alert-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px;
+  background: rgba(239, 68, 68, 0.05);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.alert-item:hover {
+  background: rgba(239, 68, 68, 0.15);
+  transform: translateX(4px);
+}
+
+.alert-rank {
+  background: #ef4444;
+  color: white;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.alert-name {
+  flex: 1;
+  font-size: 13px;
+  color: #333;
+}
+
+.alert-ph {
+  font-size: 12px;
+  font-weight: bold;
+}
+
+.alert-ph.danger {
+  color: #ef4444;
+}
+
 /* 图例样式 */
 .legend {
   position: absolute;
@@ -405,6 +784,81 @@ function getPHComment(avgPH) {
   flex-shrink: 0;
 }
 
+.point-detail-modal {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: white;
+  padding: 20px;
+  border-radius: 12px;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+  z-index: 2000;
+  min-width: 300px;
+  border: 1px solid rgba(16, 146, 216, 0.2); /* 新增边框 */
+  animation: fadeIn 0.3s ease; /* 新增淡入动画 */
+}
+
+/* 新增弹窗动画 */
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translate(-50%, -45%);
+  }
+  to {
+    opacity: 1;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.detail-content {
+  position: relative;
+}
+
+.detail-content h4 {
+  margin: 0 0 15px 0;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 10px;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  padding: 8px;
+  background: #f8f9fa;
+  border-radius: 6px;
+}
+
+.detail-label {
+  font-weight: 500;
+  color: #666;
+}
+
+.detail-value {
+  font-weight: bold;
+  color: #333;
+}
+
+.detail-value.danger {
+  color: #ef4444;
+}
+
+.detail-value.warning {
+  color: #f59e0b;
+}
+
+.detail-value.success {
+  color: #22c55e;
+}
+
+.detail-suggestion {
+  color: #1092d8;
+  font-size: 13px;
+  line-height: 1.5;
+}
+
 /* 自定义 Tooltip 样式 */
 :deep(.custom-tooltip) {
   background: rgba(255, 255, 255, 0.95);
@@ -415,4 +869,42 @@ function getPHComment(avgPH) {
   font-family: 'Microsoft YaHei', sans-serif;
   backdrop-filter: blur(4px);
 }
+
+.custom-marker {
+  background: transparent !important;
+  border: none !important;
+}
+
+.detail-content {
+  position: relative;
+}
+
+.detail-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+}
+
+
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 20px;
+  cursor: pointer;
+  color: #909399;
+  padding: 0;
+  width: 20px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%; /* 新增圆形背景 */
+  transition: all 0.2s; /* 新增过渡 */
+}
+
+.close-btn:hover {
+  color: #ef4444; /* 改为红色更醒目 */
+  background: #fef2f2; /* 新增背景色 */
+}
 </style>