Explorar el Código

feat(user/introduction): add crop Cd content statistics and second map

新增了第二个土壤镉含量地图页面,实现Cd数据加载、统计计算,添加分布柱状图和统计面板,调整了首个地图的中心和缩放级别,优化了地图容器样式。
yes-yes-yes-k hace 1 semana
padre
commit
3a1116648d
Se han modificado 1 ficheros con 265 adiciones y 14 borrados
  1. 265 14
      src/views/User/introduction/TotalIntroduction.vue

+ 265 - 14
src/views/User/introduction/TotalIntroduction.vue

@@ -8,9 +8,13 @@
    import { api8000 } from '@/utils/request'
 
    const samplePointsData = ref([])
+   const cdSampleData = ref([])  // 第二个地图的 CD 数据
    const mapRef = ref(null)
+   const mapRef2 = ref(null)  // 第二个地图
    let map = null
+   let map2 = null  // 第二个地图实例
    let wmsLayer = null
+   let wmsLayer2 = null  // 第二个地图的 WMS 图层
    let geoJsonLayer = null
 
 
@@ -39,12 +43,30 @@
      range3: 0,
    })
 
+   //  第二个地图的 CD 含量统计
+   const cdStatistics = ref({
+     totalBlocks: 0,
+     avgCD: 0,
+     safeCount: 0,      // 安全区间 (0.0-0.2 mg/kg)
+     warningCount: 0,   // 预警区间 (0.2-0.3 mg/kg)
+     exceedCount: 0,    // 超标区间 (≥0.3 mg/kg)
+     maxCD: 0,
+     minCD: 0
+   })
+
+   //  CD 含量分布个数计算
+   const cdDistribution = ref({
+     safe: 0,      // 安全
+     warning: 0,   // 预警
+     exceed: 0     // 超标
+   })
+
    const selectedPoint = ref(null)
 
   //  地图配置
    const CONFIG = {
-    center:[25.202903, 113.25383],
-    zoom:11,
+    center:[25.222903, 113.25383],
+    zoom:10,  // 调小缩放级别,显示更大范围
     //  获取所有点的 API 地址
     getPoint:'/api/vector/export/all?table_name=le_data_block_map&format=geojson',
     geoserver:{
@@ -119,6 +141,24 @@ function getPHComment(avgPH) {
 
   }
 
+  async function initMap2(){
+    await nextTick()
+
+    if(!mapRef2.value) return 
+    map2 = L.map(mapRef2.value).setView(CONFIG.center,CONFIG.zoom)
+
+    // 第二个地图展示 CropCd_block_map_with_boundary 图层
+    wmsLayer2 = L.tileLayer.wms(CONFIG.geoserver.wmsUrl, {
+      layers: `${CONFIG.geoserver.workspace}:CropCd_block_map_with_boundary`,
+      format: 'image/png',
+      transparent: true,
+      version: '1.1.0',
+      srs:'EPSG:4326',
+      attribution: '© GeoServer - Crop CD'
+    }).addTo(map2)
+
+  }
+
   async function switchMap(mapType) {
     if (!map || !wmsLayer) return
     
@@ -301,6 +341,104 @@ function getPHComment(avgPH) {
   }
 }
 
+// 加载第二个地图的 CD 含量统计数据
+async function loadCDStatistics() {
+  try {
+    // 等待数据加载完成
+    if (cdSampleData.value.length === 0) return;
+    
+    let cdCount = 0
+    let avgCD = 0
+    let safeCount = 0      // 0.0-0.2 mg/kg
+    let warningCount = 0   // 0.2-0.3 mg/kg
+    let exceedCount = 0    // ≥0.3 mg/kg
+    let maxCD = -Infinity
+    let minCD = Infinity
+
+    cdSampleData.value.forEach(feature => {
+      // 假设 CD 含量字段为 CropCd_mea
+      const cdValue = feature.properties.CropCd_mea
+      const numericCd = typeof cdValue === 'string' ? parseFloat(cdValue) : cdValue
+      
+      if (numericCd !== null && numericCd !== undefined && !isNaN(numericCd) && numericCd >= 0) {
+        cdCount++
+        const delta = numericCd - avgCD;
+        avgCD = avgCD + delta / cdCount;
+        maxCD = Math.max(maxCD, numericCd)
+        minCD = Math.min(minCD, numericCd)
+        
+        // 根据规则分类
+        if (numericCd < 0.2) {
+          safeCount++
+        }
+        else if (numericCd < 0.3) {
+          warningCount++
+        }
+        else {
+          exceedCount++
+        }
+      }
+    });
+
+    cdStatistics.value = {
+      totalBlocks: cdSampleData.value.length,
+      avgCD: cdCount > 0 ? parseFloat(avgCD.toFixed(3)) : 0,
+      safeCount,
+      warningCount,
+      exceedCount,
+      maxCD: maxCD === -Infinity ? 0 : parseFloat(maxCD.toFixed(3)),
+      minCD: minCD === Infinity ? 0 : parseFloat(minCD.toFixed(3))
+    };
+    
+    // 计算 CD 含量分布
+    cdDistribution.value = {
+      safe: safeCount,
+      warning: warningCount,
+      exceed: exceedCount
+    };
+
+    // console.log('✅ CD 统计数据加载完成:', cdStatistics.value);
+  } catch (err) {
+    console.error('加载 CD 统计数据失败:', err);
+  }
+}
+
+// 加载第二个地图的 CD 数据
+async function loadCDData() {
+  try {
+    const url = `${CONFIG.geoserver.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${CONFIG.geoserver.workspace}:Crop_cd_block_map&outputFormat=application/json`
+    
+    const response = await fetch(url);
+    
+    if (!response.ok) {
+      const errorText = await response.text();
+      console.error('❌ WFS 响应错误:', errorText);
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+    
+    const text = await response.text();
+    
+    // 尝试解析 JSON
+    let geoJsonData;
+    try {
+      geoJsonData = JSON.parse(text);
+    } catch (parseError) {
+      console.error('❌ JSON 解析失败,响应内容:', text);
+      throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
+    }
+    
+
+    // 保存 CD 数据
+    cdSampleData.value = geoJsonData.features;
+    
+    // 计算 CD 统计信息
+    loadCDStatistics();
+    
+  } catch (err) {
+    console.error('❌ 加载 CD 数据失败:', err);
+  }
+}
+
 // 修改为只获取数据用于统计,不渲染到地图
   async function fetchDataForStatistics() {
     try {
@@ -470,12 +608,22 @@ onUnmounted(()=>{
 
   onMounted(async()=>{
     await initMap()
+    await initMap2()  // 初始化第二个地图
     await fetchDataForStatistics()
     await loadStatistics()
+    await loadCDData()  // 加载 CD 数据
+    // loadCDStatistics() 会在 loadCDData 完成后自动调用
   })
 
   onUnmounted(()=>{
-    if(map) map.remove()
+    if(map) {
+      map.remove()
+      map = null
+    }
+    if(map2) {
+      map2.remove()
+      map2 = null
+    }
   })
 
 </script>
@@ -483,6 +631,7 @@ onUnmounted(()=>{
 <template>
   <div class="map-container">
     <div class="ph-map" ref="mapRef"></div>
+    <div class="ph-map" ref="mapRef2"></div>
 
     <!-- 计算刷新按钮 -->
      <div class="compute">
@@ -553,7 +702,51 @@ onUnmounted(()=>{
       </div>
     </div>
 
-    <!-- ph分布 -->
+    <!-- CD 含量统计 -->
+    <div class="statistics-panel" style="top: 550px;">
+      <h4>🌾 作物 Cd 含量统计</h4>
+      
+      <div class="stat-row">
+        <span class="stat-label">地块总数:</span>
+        <span class="stat-value">{{ cdStatistics.totalBlocks }} 个</span>
+      </div>
+      
+      <div class="stat-row highlight">
+        <span class="stat-label">平均 Cd 含量:</span>
+        <span class="stat-value" :class="cdStatistics.avgCD > 0 ? 'success' : 'danger'">
+          {{ cdStatistics.avgCD > 0 ? cdStatistics.avgCD + ' mg/kg' : '-' }}
+        </span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">安全 (<0.2 mg/kg):</span>
+        <span class="stat-value success">{{ cdStatistics.safeCount }} 个</span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">预警 (0.2-0.3 mg/kg):</span>
+        <span class="stat-value warning">{{ cdStatistics.warningCount }} 个</span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">超标 (≥0.3 mg/kg):</span>
+        <span class="stat-value danger">{{ cdStatistics.exceedCount }} 个</span>
+      </div>
+      
+      <div class="stat-divider"></div>
+      
+      <div class="stat-row small">
+        <span class="stat-label">最高 Cd:</span>
+        <span class="stat-value danger">{{ cdStatistics.maxCD > 0 ? cdStatistics.maxCD + ' mg/kg' : '-' }}</span>
+      </div>
+      
+      <div class="stat-row small">
+        <span class="stat-label">最低 Cd:</span>
+        <span class="stat-value success">{{ cdStatistics.minCD > 0 ? cdStatistics.minCD + ' mg/kg' : '-' }}</span>
+      </div>
+    </div>
+
+    <!-- ph 分布 -->
     <div class="distribution-chart">
       <h4>📈 pH 分布</h4>
       <div class="bar-chart">
@@ -581,6 +774,43 @@ onUnmounted(()=>{
       </div>
     </div>
 
+    <!-- CD 含量分布 -->
+    <div class="distribution-chart" style="top: 550px;">
+      <h4>🌾 作物 Cd 含量分布</h4>
+      <div class="bar-chart">
+        <div class="bar-item">
+          <div class="bar-label">安全 (<0.2)</div>
+          <div class="bar-container">
+            <div 
+              class="bar-fill success"
+              :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.safe / cdStatistics.totalBlocks) * 100 : 0}%` }"
+            ></div>
+            <span class="bar-value">{{ cdDistribution.safe }}</span>
+          </div>
+        </div>
+        <div class="bar-item">
+          <div class="bar-label">预警 (0.2-0.3)</div>
+          <div class="bar-container">
+            <div 
+              class="bar-fill warning"
+              :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.warning / cdStatistics.totalBlocks) * 100 : 0}%` }"
+            ></div>
+            <span class="bar-value">{{ cdDistribution.warning }}</span>
+          </div>
+        </div>
+        <div class="bar-item">
+          <div class="bar-label">超标 (≥0.3)</div>
+          <div class="bar-container">
+            <div 
+              class="bar-fill danger"
+              :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.exceed / cdStatistics.totalBlocks) * 100 : 0}%` }"
+            ></div>
+            <span class="bar-value">{{ cdDistribution.exceed }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
     <div class="stat-comment" v-if="statistics.avgPH > 0">
         <div class="comment-title">📝 综合评估:</div>
         <div class="comment-text" :class="getPHLevelClass(statistics.avgPH)">
@@ -609,6 +839,23 @@ onUnmounted(()=>{
       </div>
     </div>
 
+    <!-- CD 含量图例 -->
+    <div class="legend" style="top: 580px;">
+      <h4>🌾 Cd 含量图例</h4>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #22c55e;"></span>
+        <span>0.0 - 0.2 mg/kg (安全)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #eab308;"></span>
+        <span>0.2-0.3 mg/kg (预警)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #ef4444;"></span>
+        <span>≥ 0.3 mg/kg (超标)</span>
+      </div>
+    </div>
+
     <!-- 采样点详情 -->
     <div class="point-detail-modal" v-if="selectedPoint">
       <div class="detail-content">
@@ -652,10 +899,15 @@ onUnmounted(()=>{
 
 .ph-map{
   width: 600px;
-  min-height: 500px;
-  border-radius: 16px; /* 圆角大小,可根据需要调整,比如 10px、20px */
-  border: 3px solid #1092d8; /* 蓝色边框,宽度和颜色可自定义 */
-  overflow: hidden; /* 关键:防止地图内容溢出圆角区域 */
+  height: 500px;  /* 固定高度 */
+  border-radius: 16px;
+  border: 3px solid #1092d8;
+  overflow: hidden;
+  margin-bottom: 20px;  /* 两个地图之间的间距 */
+}
+
+.ph-map:first-child {
+  margin-bottom: 50px;  /* 第一个地图下方留空 */
 }
 
 /* ✅ 统计面板样式 */
@@ -971,15 +1223,14 @@ onUnmounted(()=>{
   padding: 15px;
   border-radius: 8px;
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-  z-index: 1000;
-  min-width: 180px;
+  min-width: 10px;
   backdrop-filter: blur(4px);
   z-index: 0;
 }
 
 .legend h4 {
   margin: 0 0 10px 0;
-  font-size: 14px;
+  font-size: 10px;
   color: #333;
   border-bottom: 2px solid #1092d8;
   padding-bottom: 6px;
@@ -990,7 +1241,7 @@ onUnmounted(()=>{
   align-items: center;
   gap: 8px;
   margin-bottom: 6px;
-  font-size: 12px;
+  font-size: 8px;
   color: #555;
 }
 
@@ -999,8 +1250,8 @@ onUnmounted(()=>{
 }
 
 .legend-color {
-  width: 20px;
-  height: 20px;
+  width: 10px;
+  height: 10px;
   border-radius: 4px;
   border: 1px solid #ddd;
   flex-shrink: 0;