浏览代码

添加反酸降酸地图

yes-yes-yes-k 1 月之前
父节点
当前提交
fc8ba158cf

+ 1 - 0
src/App.vue

@@ -1,6 +1,7 @@
 <script setup lang='ts'>
 import { RouterView } from "vue-router"
 import request from './utils/request';
+import Acidmodelmap from "./views/User/acidModel/acidmodelmap.vue";
 
 </script>
 

二进制
src/assets/bg/bg2.jpg


二进制
src/assets/bg/bg3.jpg


二进制
src/assets/bg/bg4.jpg


二进制
src/assets/bg/bg5.jpg


二进制
src/assets/bg/bg6.jpg


二进制
src/assets/bg/bg7.jpg


二进制
src/assets/bg/bg8.jpg


二进制
src/assets/bg/bg9.jpg


二进制
src/assets/favicon (2).ico


二进制
src/assets/favicon(3).ico


二进制
src/assets/favicon(4).ico


二进制
src/assets/favicon(5).ico


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

@@ -42,7 +42,7 @@ import { reactive, computed, inject, toRefs, watch } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import { menuItems } from './menuItems';
-
+console.log('当前使用的侧边栏组件:AppAside.vue') // AppAside.vue 中添加
 const props = defineProps({
   activeTab: {
     type: String,

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

@@ -42,7 +42,7 @@ import { inject, reactive, watch, toRefs, computed } from "vue";
 import { useRouter, useRoute, type RouteLocationAsPathGeneric, type RouteLocationAsRelativeGeneric } from "vue-router";
 import { ElMessage } from "element-plus";
 import { tabMenuMap } from "./menuItems2";
-
+console.log('当前使用的侧边栏组件:AppAsideForTab2.vue') // AppAsideForTab2.vue 中添加
 const props = defineProps({
   activeTab: {
     type: String,

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

@@ -345,7 +345,7 @@ const tabs = computed(() => {
         name: "soilAcidificationPrediction",
         label: "土壤酸化预测",
         icon: "el-icon-magic-stick",
-        routes: ["/Calculation", "/AcidNeutralizationModel"],
+        routes: ["/Calculation", "/AcidNeutralizationModel","/acidmodelmap"],
       },
       // {
       //   name: "scenarioSimulation",

+ 9 - 1
src/components/layout/menuItems.ts

@@ -16,8 +16,10 @@ import {
   Collection,
   MagicStick,
   HelpFilled,
-  Coin
+  Coin,
+  Loading
 } from '@element-plus/icons-vue';
+import { icon } from 'leaflet';
 
 export interface MenuItem {
   index: string;
@@ -305,6 +307,12 @@ export const menuItems: MenuItem[] = [
     icon: DataLine,
     tab: 'farmlandQualityAssessment'
   },
+  {
+    index: '/acidmodelmap',
+    label: '土壤酸化地图展示',
+    icon: Location,
+    tab: 'soilAcidificationPrediction',
+  },
   {
     index: '/acidModel',
     label: 'acidModel.Title',//<!--i18n:acidModel.Title-->土壤反酸

+ 9 - 0
src/router/index.ts

@@ -384,6 +384,15 @@ const routes = [
           ), // 修复路径
         meta: { title: "韶关" },
       },
+      {
+        path: "acidmodelmap",
+        name: "acidmodelmap",
+        component: () =>
+          import(
+            "@/views/User/acidModel/acidmodelmap.vue"
+          ),
+        meta: { title: "土壤酸化地图" }
+      },
 
       {
         path: "SoilAcidReboundPrediction",

+ 2 - 388
src/views/User/acidModel/Calculation.vue

@@ -1,46 +1,5 @@
 <template>
-  <div class="container">
-  <!-- 地图卡片 -->
-    <el-card class="map-card">
-      <div class="title">
-        <div class="section-icon">🗺️</div>
-        <p class="map-title">乐昌市三种用地类型分布地图</p>
-      </div>
-      <div id="map-container" class="map-container">
-        <div v-if="mapLoading" class="loading">
-          <div class="loading-spinner"></div>
-          <span>地图加载中...</span>
-        </div>
-        
-        <div v-if="mapError" class="error-tip">
-          <el-alert title="地图加载失败" type="error" show-icon>
-            <template #description>
-              <p>请检查GeoJSON文件路径和格式</p>
-              <el-button @click="reloadMap" type="primary">重试加载</el-button>
-            </template>
-          </el-alert>
-        </div>
-        
-        <!-- 地图信息面板
-        <div v-if="!mapLoading && !mapError" class="map-info">
-          
-          <div class="legend">
-            <div class="legend-item">
-              <span class="color-box" style="background-color: #4CAF50;"></span>
-              <span>耕地</span>
-            </div>
-            <div class="legend-item">
-              <span class="color-box" style="background-color: #8BC34A;"></span>
-              <span>林地</span>
-            </div>
-            <div class="legend-item">
-              <span class="color-box" style="background-color: #FF9800;"></span>
-              <span>建设用地</span>
-            </div>
-          </div>
-        </div> -->
-      </div>
-    </el-card>
+  <div class="container">    
 
     <!-- 表单卡片 -->
     <el-card class="form-card">
@@ -165,7 +124,6 @@
       </el-form>
     </el-card>
     
-    
   </div>
 </template>
 
@@ -207,191 +165,7 @@ const errorMessages = reactive<Record<string, string>>({
   Al3_plus: "",
 });
 
-// 地图相关状态
-const mapLoading = ref(true);
-const mapError = ref(false);
-const map = ref<any>(null);
-let L: any = null;
-
-// 修复:简化地图初始化
-const initMap = async () => {
-  mapLoading.value = true;
-  mapError.value = false;
-
-  try {
-    // 动态导入Leaflet
-    if (!L) {
-      L = await import('leaflet');
-      await import('leaflet/dist/leaflet.css');
-      
-      // 修复Leaflet图标问题
-      delete (L.Icon.Default.prototype as any)._getIconUrl;
-      L.Icon.Default.mergeOptions({
-        iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
-        iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
-        shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
-      });
-    }
-
-    // 清除现有地图
-    if (map.value) {
-      map.value.remove();
-      map.value = null;
-    }
 
-    // 创建地图实例 - 最简单的配置
-    map.value = L.map('map-container', {
-      zoomControl: true,
-      attributionControl: false,
-    });
-
-    // 设置初始视图
-    map.value.setView([25.13, 113.35]);
-    
-    // 直接加载GeoJSON
-    await loadGeoJSON();
-    // 地图初始化完成后,添加点击事件监听
-    map.value.on('click', function(e: LeafletMouseEvent) {
-      const lng = e.latlng.lng.toFixed(6);
-      const lat = e.latlng.lat.toFixed(6);
-      let popupContent = '<div style="padding: 10px; max-width: 300px;">';
-      popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>经度:</strong> ${lng}</p>`;
-      popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>纬度:</strong> ${lat}</p>`;
-      popupContent += '</div>';
-      L.popup().setLatLng(e.latlng).setContent(popupContent).openOn(map.value);
-    });
-
-    mapLoading.value = false;
-
-  } catch (error) {
-    console.error('地图初始化失败:', error);
-    mapError.value = true;
-    mapLoading.value = false;
-    
-    // 修复:安全地处理错误信息
-    let errorMessage = '地图初始化失败';
-    if (error instanceof Error) {
-      errorMessage += ': ' + error.message;
-    }
-    ElMessage.error(errorMessage);
-  }
-};
-
-// 修复:增强GeoJSON加载逻辑
-const loadGeoJSON = async () => {
-  if (!map.value) {
-    console.error('地图未初始化');
-    return;
-  }
-
-  try {
-    // 使用您实际的文件名韶关市各区县边界图
-    const response = await fetch('/data/乐乐5_5_0_0.geojson');
-    if (!response.ok) throw new Error('文件加载失败');
-    
-    const geoData = await response.json();
-    
-    /* 调试:打印GeoJSON信息
-    console.log('GeoJSON数据详情:', {
-      类型: geoData.type,
-      要素数量: geoData.features ? geoData.features.length : '无features属性'
-    });
-    
-    if (geoData.features) {
-      geoData.features.forEach((feature: any, index: number) => {
-        console.log(`要素 ${index}:`, {
-          类型: feature.geometry ? feature.geometry.type : '无几何信息',
-          属性: feature.properties || '无属性'
-        });
-      });
-    }*/
-
-    // 清除现有图层
-    map.value.eachLayer((layer: any) => {
-      map.value.removeLayer(layer);
-    });
-    
-    // 检查是否有有效的几何数据
-    if (!geoData.features || geoData.features.length === 0) {
-      throw new Error('GeoJSON文件不包含有效的地理要素');
-    }
-
-    // 添加GeoJSON到地图
-    const geoJsonLayer = L.geoJSON(geoData, {
-      style: (feature: any) => {
-        const properties = feature?.properties || {};
-        let color = '#3388ff';
-        
-        
-        return {
-          color: color,
-          weight: 2,
-          opacity: 0.8,
-          fillOpacity: 0.5,
-          fillColor: color
-        };
-      },
-      onEachFeature: (feature: any, layer: any) => {
-        if (feature.properties) {
-          /*const props = feature.properties;
-          let popupContent = '<div style="padding: 10px; max-width: 300px;">';
-          popupContent += `<h4 style="margin: 0 0 10px 0;">${props.ZLDWMC || '区域'}</h4>`;
-          
-          popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>经度:</strong> </p>`;
-          popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>纬度:</strong> </p>`;
-
-
-          popupContent += '</div>';
-          
-          layer.bindPopup(popupContent);*/
-          
-          // 添加交互效果
-          layer.on('mouseover', function() {
-            layer.setStyle({
-              weight: 4,
-              fillOpacity: 0.7
-            });
-          });
-          
-          layer.on('mouseout', function() {
-            layer.setStyle({
-              weight: 2,
-              fillOpacity: 0.5
-            });
-          });
-        }
-      }
-    });
-
-    geoJsonLayer.addTo(map.value);
-    
-    // 调整视图到GeoJSON范围
-    const bounds = geoJsonLayer.getBounds();
-    if (bounds && bounds.isValid()) {
-      map.value.fitBounds(bounds, { padding: [20, 20] });
-      map.value.setZoom(10.5);
-    }
-    
-    mapLoading.value = false;
-    
-  } catch (error) {
-    console.error('加载GeoJSON失败:', error);
-    mapError.value = true;
-    mapLoading.value = false;
-    
-    // 修复:安全地处理错误信息
-    let errorMessage = 'GeoJSON文件加载失败';
-    if (error instanceof Error) {
-      errorMessage += ': ' + error.message;
-    }
-    ElMessage.error(errorMessage);
-  }
-};
-
-// 重新加载地图
-const reloadMap = () => {
-  initMap();
-};
 
 // 表单处理方法
 const handleInput = (
@@ -520,9 +294,7 @@ const onDialogClose = () => {
   });
 };
 
-onMounted(() => {
-  setTimeout(initMap, 100);
-});
+
 </script>
 
 <style scoped>
@@ -543,159 +315,6 @@ onMounted(() => {
   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
 }
 
-.map-card {
-  width: 850px;
-  flex: 1;
-  min-height: 600px;
-  margin: 0 auto;
-}
-
-.map-container {
-  height: 550px;
-  width: 100%;
-  position: relative;
-  border-radius: 4px;
-  overflow: hidden;
-  background: #f0f2f5;
-  border: 1px solid #dcdfe6;
-}
-
-.loading {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  text-align: center;
-  z-index: 1000;
-  background: rgba(255, 255, 255, 0.95);
-  padding: 20px;
-  border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
-}
-
-.loading-spinner {
-  width: 30px;
-  height: 30px;
-  border: 3px solid #f3f3f3;
-  border-top: 3px solid #409eff;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-  margin: 0 auto 10px;
-}
-
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-.loading span {
-  margin-left: 8px;
-  color: #606266;
-}
-
-.error-tip {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  width: 80%;
-  z-index: 1000;
-}
-
-.map-info {
-  position: absolute;
-  top: 10px;
-  left: 10px;
-  background: rgba(255, 255, 255, 0.95);
-  padding: 12px 15px;
-  border-radius: 6px;
-  z-index: 1000;
-  font-size: 12px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-  border: 1px solid #ebeef5;
-  min-width: 200px;
-}
-
-.map-info p {
-  margin: 0 0 8px 0;
-  font-weight: 500;
-  color: #303133;
-}
-
-.title {
-  display: flex; /* 启用Flex布局 */
-  align-items: center; /* 垂直居中对齐(核心) */
-  gap: 10px; /* 图标和文字之间的间距(可调整) */
-  margin-bottom: 15px; /* 整体与下方地图的间距 */
-}
-
-.map-title {
-  color: #1a365d;
-  font-size: 1.6rem;
-  font-weight: 600;
-}
-
-.section-icon {
-  font-size: 2.2rem;
-  color: #3a9fd3;
-}
-
-.legend {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-}
-
-.legend-item {
-  display: flex;
-  align-items: center;
-}
-
-.color-box {
-  width: 12px;
-  height: 12px;
-  margin-right: 8px;
-  border: 1px solid #ddd;
-  border-radius: 2px;
-}
-
-.box-card {
-  max-width: 850px;
-  margin: 0 auto;
-  padding: 20px;
-  background-color: #f0f5ff;
-  border-radius: 10px;
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-}
-
-.card-header {
-  text-align: center;
-  color: #333;
-  margin-bottom: 30px;
-  font-size: 25px;
-}
-:deep(.el-tooltip__popper.is-light) {
-  background-color: white;
-  border-color: #dcdfe6;
-  color: #606266;
-}
-
-/* 增强Leaflet样式 */
-:deep(.leaflet-popup-content-wrapper) {
-  border-radius: 8px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-:deep(.leaflet-popup-content) {
-  margin: 8px 12px;
-  line-height: 1.4;
-}
-
-:deep(.leaflet-container) {
-  font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
-  font-size: 12px;
-}
-
 .el-form-item {
   margin-bottom: 20px;
 }
@@ -736,10 +355,5 @@ onMounted(() => {
   .el-form {
     --el-form-label-width: 100px;
   }
-  .map-info {
-    font-size: 10px;
-    padding: 8px 10px;
-    min-width: 150px;
-  }
 }
 </style>

+ 889 - 0
src/views/User/acidModel/acidmodelmap.vue

@@ -0,0 +1,889 @@
+<template>
+  <el-card class="map-card">
+    <div class="title">
+      <div class="section-icon">🗺️</div>
+      <p class="map-title">乐昌市三种用地类型分布地图</p>
+    </div>
+
+    <div id="map-container" class="map-container">
+      <div v-if="mapLoading" class="loading">
+        <div class="loading-spinner"></div>
+        <span>地图加载中...</span>
+      </div>
+      <div v-if="mapError" class="error-tip">
+        <el-alert title="地图加载失败" type="error" show-icon>
+          <template #description>
+            <p>请检查GeoServer服务和配置</p>
+            <el-button @click="reloadMap" type="primary">重试加载</el-button>
+          </template>
+        </el-alert>
+      </div>
+    </div>
+  
+
+  <!-- 信息弹窗 -->
+      <div v-if="showPopup" :style="popupStyle" class="feature-popup">
+        <div class="popup-content">
+          <div class="popup-header">
+            <h4>地块信息</h4>
+            <button @click="closePopup" class="close-btn">×</button>
+          </div>
+          <div class="popup-body">
+            <div v-if="featureInfo.loading" class="loading-info">加载中...</div>
+            <div v-else-if="featureInfo.error" class="error-info">获取信息失败</div>
+            <div v-else-if="featureInfo.data" class="feature-info">
+              <div class="info-item">
+                <label>所属村:</label>
+                <span>{{ featureInfo.data.village || '未知' }}</span>
+              </div>
+              <div class="info-item">
+                <label>用地类型:</label>
+                <span>{{ featureInfo.data.landType || '未知' }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    
+    <!-- 预测结果弹窗 -->
+    <div v-if="showPredictionPopup" :style="predictionPopupStyle" class="prediction-popup">
+      <div class="popup-content">
+        <div class="popup-header">
+          <h4>土壤预测结果</h4>
+          <button @click="closePredictionPopup" class="close-btn">×</button>
+        </div>
+        <div class="popup-body">
+          <div v-if="predictionLoading" class="loading-info">
+            <div class="loading-spinner small"></div>
+            <span>预测中...</span>
+          </div>
+          <div v-else-if="predictionError" class="error-info">
+            {{ predictionError }}
+          </div>
+          <div v-else-if="predictionResult" class="prediction-result">
+            <div class="result-item">
+              <label>最近点位信息:</label>
+              <div class="point-info">
+                <div>经度: {{ predictionResult.nearest_point?.lon }}</div>
+                <div>纬度: {{ predictionResult.nearest_point?.lan }}</div>
+              </div>
+            </div>
+            <div class="result-item" v-if="currentPredictionType === 'reduction' && predictionResult.prediction_model33 !== undefined">
+              <label>降酸预测结果:</label>
+              <span class="prediction-value reduction">每亩地土壤表层20cm撒{{ formatPredictionValue(predictionResult.prediction_model33)}} 吨</span>
+            </div>
+            <!--<div class="result-item" v-if="currentPredictionType === 'inversion' && predictionResult.prediction_model24 !== undefined">
+              <label>反酸预测结果:</label>
+              <span class="prediction-value inversion">ph值{{ formatPredictionValue(predictionResult.prediction_model24) }} </span>
+            <div v-if="predictionResult.warnings" class="warnings">
+              <div v-for="(warning, index) in filteredWarnings" :key="index" class="warning-item">
+                {{ warning }}
+              </div>
+            </div>
+            </div>-->
+            
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 新增:降酸预测参数输入弹窗 -->
+    <el-dialog
+      v-model="showAcidReductionInput"
+      title="降酸预测参数输入"
+      width="350px"
+      :close-on-click-modal="false"
+      class="acid-reduction-dialog"
+    >
+      <el-form :model="acidReductionParams" :rules="acidReductionRules" ref="acidReductionFormRef" label-width="100px">
+        <el-form-item label="targetPH" prop="targetPH">
+          <el-input
+            v-model.number="acidReductionParams.targetPH"
+            type="number"
+            step="0.01"
+            placeholder="请输入目标pH相关参数(0-14)"
+          />
+        </el-form-item>
+        <!--平均为34.04685185-->
+        <el-form-item label="NO3" prop="no3">
+          <el-input
+            v-model.number="acidReductionParams.no3"
+            type="number"
+            step="0.01"
+            min="0"
+            placeholder="请输入NO3特征值(非负)"
+          />
+        </el-form-item>
+        <!--平均为7.872182305-->
+        <el-form-item label="CEC" prop="cec">
+          <el-input
+            v-model.number="acidReductionParams.cec"
+            type="number"
+            step="0.01"
+            min="0"
+            placeholder="请输入CEC特征值(非负)"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showAcidReductionInput = false">取消</el-button>
+        <el-button type="primary" @click="confirmAcidReduction">确认预测</el-button>
+      </template>
+    </el-dialog>
+
+
+
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import {reactive, ref, nextTick, onMounted ,onUnmounted, computed} from "vue";
+import { ElMessage } from "element-plus";
+import type { LeafletMouseEvent } from "leaflet";
+import type { FormInstance } from "element-plus";
+
+// 地图状态
+const mapLoading = ref(true);
+const mapError = ref(false);
+const map = ref<any>(null);
+const showPopup =ref(false);
+const popupStyle = reactive({
+  left:'0px',
+  top:'0px'
+})
+// 预测弹窗状态
+const showPredictionPopup = ref(false);
+const predictionPopupStyle = reactive({
+  left: '0px',
+  top: '0px'
+});
+const predictionLoading = ref(false);
+const predictionError = ref('');
+const predictionResult = ref<any>(null);
+
+const disableMouseEvents = ref(false);
+
+
+let L:any = null;
+
+// 地块信息状态
+const featureInfo = reactive({
+  loading: false,
+  error: false,
+  data: null as any
+});
+
+// 当前点击的坐标
+const currentClickCoords = reactive({
+  lng: 0,
+  lat: 0
+});
+
+const currentPredictionType = ref<'reduction' | 'inversion' | null>(null);
+// 新增:降酸预测参数输入相关
+const showAcidReductionInput = ref(false); // 输入弹窗显示状态
+const acidReductionFormRef = ref<FormInstance | null>(null); // 表单引用,用于校验
+// 输入参数(对应后端需要的Q_delete_pH、NO3、CEC)
+const acidReductionParams = reactive({
+  targetPH: 0, // 默认值(可调整)
+  no3: 0,         // 默认值
+  cec: 0          // 默认值
+});
+
+// 过滤警告信息,排除模型24的错误
+const filteredWarnings = computed(() => {
+  if (!predictionResult.value?.warnings) return [];
+  
+  return predictionResult.value.warnings.filter((warning: string) => {
+    // 过滤掉模型24相关的错误信息
+    return !warning.includes('模型24');
+  });
+});
+
+// 输入校验规则
+const acidReductionRules = reactive({
+  targetPH: [
+    { required: true, message: '请输入目标ph值', trigger: 'blur' },
+    { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+    { 
+      validator: (rule: any, value: number, callback: any) => {
+        if (value < 0 || value > 14) {
+          callback(new Error('值范围在0-14之间'));
+        } else {
+          callback();
+        }
+      }, 
+      trigger: 'blur' 
+    }
+  ],
+  no3: [
+    { required: true, message: '请输入NO3', trigger: 'blur' },
+    { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+    { 
+      validator: (rule: any, value: number, callback: any) => {
+        if (value < 0) {
+          callback(new Error('值不能为负数'));
+        } else {
+          callback();
+        }
+      }, 
+      trigger: 'blur' 
+    }
+  ],
+  cec: [
+    { required: true, message: '请输入CEC', trigger: 'blur' },
+    { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+    { 
+      validator: (rule: any, value: number, callback: any) => {
+        if (value < 0) {
+          callback(new Error('值不能为负数'));
+        } else {
+          callback();
+        }
+      }, 
+      trigger: 'blur' 
+    }
+  ]
+});
+const predictionTitle = computed(() => {
+  if (currentPredictionType.value === 'reduction') return '降酸预测结果';
+  if (currentPredictionType.value === 'inversion') return '反酸预测结果';
+  return '土壤预测结果';
+});
+
+
+// 防抖计时器
+let hoverTimer: any = null;
+
+// 格式化数字显示
+const formatNumber = (value: any): string => {
+  if (value === null || value === undefined) return '无数据';
+  const num = Number(value);
+  return isNaN(num) ? '无效数据' : num.toFixed(6);
+};
+
+// 格式化预测值
+const formatPredictionValue = (value: any): string => {
+  console.log('预测值原始数据:', value, '类型:', typeof value);
+  
+  if (value === null || value === undefined) return '无数据';
+  
+  // 如果是数组,取第一个元素
+  if (Array.isArray(value)) {
+    if (value.length === 0) return '无数据';
+    const num = Number(value[0]);
+    return isNaN(num) ? '无效数据' : num.toFixed(4);
+  }
+  
+  // 如果是对象,尝试转换为数字
+  if (typeof value === 'object') {
+    console.warn('预测值是对象类型:', value);
+    return '数据格式错误';
+  }
+  
+  // 直接转换为数字
+  const num = Number(value);
+  return isNaN(num) ? '无效数据' : num.toFixed(4);
+};
+
+
+// 获取地块信息的函数
+const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
+  if (!map.value) return false;
+  
+  featureInfo.loading = true;
+  featureInfo.error = false;
+  featureInfo.data = null;
+
+  try {
+    const GEOSERVER_CONFIG = {
+      url: "/geoserver/wms",
+      workspace: "acidmap",
+      layerGroup: "mapwithboundary", 
+    };
+
+    const bounds = map.value.getBounds();
+    const size = map.value.getSize();
+    
+    // 使用URLSearchParams构建查询参数
+    const params = new URLSearchParams();
+    params.append('service', 'WMS');
+    params.append('version', '1.1.1');
+    params.append('request', 'GetFeatureInfo');
+    params.append('layers', `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`);
+    params.append('query_layers', `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`);
+    params.append('info_format', 'application/json');
+    params.append('feature_count', '10');
+    params.append('x', Math.round(point.x).toString());
+    params.append('y', Math.round(point.y).toString());
+    params.append('width', size.x.toString());
+    params.append('height', size.y.toString());
+    params.append('srs', 'EPSG:4326');
+    params.append('bbox', `${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}`);
+
+    const url = `${GEOSERVER_CONFIG.url}?${params.toString()}`;
+    
+    const response = await fetch(url);
+    
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+    
+    const data = await response.json();
+    
+    // 检查是否有要素返回
+    if (data.features && data.features.length > 0) {
+      const properties = data.features[0].properties;
+      
+      // 检查是否有有效的地块信息(根据您的字段调整)
+      const hasValidData = properties.QSDWMC || properties.DLMC;
+      
+      if (hasValidData) {
+        featureInfo.data = {
+          village: properties.QSDWMC,
+          landType: properties.DLMC,
+        };
+        return true; // 找到有效地块
+      }
+    }
+    
+    return false; // 没有找到有效地块
+    
+  } catch (error) {
+    console.error('获取地块信息失败:', error);
+    featureInfo.error = true;
+    return false;
+  } finally {
+    featureInfo.loading = false;
+  }
+};
+
+// 调用8000后端预测接口
+const callPredictionAPI = async (lng: number, lat: number,acidReductionParams?: {
+    targetPH: number;
+    no3: number;
+    cec: number;
+  } ) => {
+  predictionLoading.value = true;
+  predictionError.value = '';
+  predictionResult.value = null;
+
+  disableMouseEvents.value = true;
+
+  try {
+    // 构建URL参数
+    const urlParams = new URLSearchParams();
+    urlParams.append('target_lon', lng.toString());
+    urlParams.append('target_lan', lat.toString());
+    // 如果是降酸预测,添加3个新增参数
+    if (acidReductionParams) {
+      urlParams.append('target_pH', acidReductionParams.targetPH.toString());
+      urlParams.append('NO3', acidReductionParams.no3.toString());
+      urlParams.append('CEC', acidReductionParams.cec.toString());
+    }
+
+    const response = await fetch(
+      `http://localhost:8000/api/vector/nearest-with-predictions?${urlParams.toString()}`
+    );
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+
+    const data = await response.json();
+    predictionResult.value = data;
+
+    //console.log('完整的预测结果:', data); // 调试用,查看返回的数据结构
+    //console.log('模型24预测结果:', data.prediction_model24, '类型:', typeof data.prediction_model24);
+
+    // 显示预测弹窗
+    showPredictionPopup.value = true;
+
+  } catch (error) {
+    console.error('调用预测接口失败:', error);
+    predictionError.value = `预测失败: ${error instanceof Error ? error.message : '未知错误'}`;
+    ElMessage.error('预测请求失败,请检查后端服务');
+
+    disableMouseEvents.value = false;
+  } finally {
+    predictionLoading.value = false;
+  }
+};
+
+
+// 降酸预测按钮点击
+const handleAcidReductionPrediction = (lng: number, lat: number) => {
+  currentPredictionType.value = 'reduction';
+  currentClickCoords.lng = lng;
+  currentClickCoords.lat = lat;
+  showAcidReductionInput.value = true;
+};
+
+// 新增:确认降酸预测(输入完成后调用)
+const confirmAcidReduction = async () => {
+console.log(acidReductionParams); 
+  // 表单校验
+  if (!acidReductionFormRef.value) return;
+  try {
+    await acidReductionFormRef.value.validate(); // 校验输入是否合法
+    // 校验通过,调用接口(传递输入的3个参数)
+    await callPredictionAPI(
+      currentClickCoords.lng,
+      currentClickCoords.lat,
+      {
+        targetPH: acidReductionParams.targetPH,
+        no3: acidReductionParams.no3,
+        cec: acidReductionParams.cec
+      }
+    );
+    // 关闭输入弹窗
+    showAcidReductionInput.value = false;
+  } catch (error) {
+    // 校验失败,不调用接口
+    ElMessage.error('输入参数不合法,请检查后重试');
+    console.error('表单校验失败:', error);
+  }
+};
+
+// 反酸预测按钮点击
+const handleAcidInversionPrediction = (lng: number, lat: number) => {
+  currentPredictionType.value = 'inversion';
+  callPredictionAPI(lng, lat);
+};
+
+
+// 鼠标移动事件处理
+const handleMouseMove = (e: any) => {
+
+  // 如果禁用了鼠标事件,直接返回
+  if (disableMouseEvents.value) {
+    return;
+  }
+
+  if (hoverTimer) {
+    clearTimeout(hoverTimer);
+  }
+  
+  // 设置防抖,避免频繁请求
+  hoverTimer = setTimeout(async () => {
+    const containerPoint = map.value.latLngToContainerPoint(e.latlng);
+
+    // 先隐藏弹窗
+    showPopup.value = false;
+    featureInfo.data = null;
+    
+    try {
+      // 尝试获取地块信息
+      const hasFeature = await getFeatureInfo(e.latlng, containerPoint);
+      
+      // 只有在有地块信息时才显示弹窗
+      if (hasFeature) {
+        // 更新弹窗位置
+        popupStyle.left = `${containerPoint.x + 10}px`;
+        popupStyle.top = `${containerPoint.y + 10}px`;
+        
+        // 显示弹窗
+        showPopup.value = true;
+      }
+    } catch (error) {
+      console.error('处理地块信息失败:', error);
+      // 出错时不显示弹窗
+      showPopup.value = false;
+    }
+  }, 200);
+};
+
+// 鼠标离开地图事件
+const handleMouseOut = () => {
+  if (hoverTimer) {
+    clearTimeout(hoverTimer);
+  }
+  showPopup.value = false;
+};
+
+// 关闭弹窗
+const closePopup = () => {
+  showPopup.value = false;
+};
+
+// 关闭预测弹窗
+const closePredictionPopup = () => {
+  showPredictionPopup.value = false;
+  predictionResult.value = null;
+  currentPredictionType.value = null;
+  
+  // 重新启用鼠标事件
+  disableMouseEvents.value = false;
+};
+
+const initMap = async () => {
+  mapLoading.value = true;
+  mapError.value = false;
+
+  try {
+    // 动态导入Leaflet(保持不变)
+    if (!L) {
+      L = await import('leaflet');
+      await import('leaflet/dist/leaflet.css');
+      
+      delete (L.Icon.Default.prototype as any)._getIconUrl;
+      L.Icon.Default.mergeOptions({
+        iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+        iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+        shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+      });
+    }
+
+    // 清除现有地图
+    if (map.value) {
+      map.value.remove();
+      map.value = null;
+    }
+
+    // 创建地图实例
+    map.value = L.map('map-container', {
+      zoomControl: true,
+      attributionControl: false,
+      center: [25.202903, 113.25383], // 设置初始中心点
+      zoom: 10 // 设置初始缩放级别
+    });
+
+    // WMS配置
+    const GEOSERVER_CONFIG = {
+      url: "/geoserver/wms",
+      workspace: "acidmap",
+      layerGroup: "mapwithboundary", 
+    };
+
+    // WMS图层配置
+    const wmsLayer = L.tileLayer.wms(GEOSERVER_CONFIG.url, {
+      layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
+      format: "image/png",
+      transparent: true,
+      version: "1.1.1", // 使用1.1.1版本更稳定
+      crs: L.CRS.EPSG4326, // 明确指定坐标系
+      attribution: "Data from GeoServer"
+    });
+
+    // 添加图层到地图
+    wmsLayer.addTo(map.value);
+
+    // 绑定鼠标事件
+    map.value.on('mousemove', handleMouseMove);
+    map.value.on('mouseout', handleMouseOut);
+
+
+    // 地图点击事件 - 修改后的版本
+    map.value.on('click', function (e: LeafletMouseEvent) {
+      const lng = e.latlng.lng;
+      const lat = e.latlng.lat;
+      
+      // 保存当前点击坐标
+      currentClickCoords.lng = lng;
+      currentClickCoords.lat = lat;
+
+      const containerPoint = map.value.latLngToContainerPoint(e.latlng);
+      
+      // 设置预测弹窗位置
+      predictionPopupStyle.left = `${containerPoint.x + 10}px`;
+      predictionPopupStyle.top = `${containerPoint.y + 10}px`;
+
+      let popupContent = '<div style="padding: 10px; max-width: 300px;">';
+      popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>经度:</strong> ${lng.toFixed(6)}</p>`;
+      popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>纬度:</strong> ${lat.toFixed(6)}</p>`;
+      popupContent += '<div style="margin-top: 8px; display: flex; gap: 8px; justify-content: center;">';
+      popupContent += `<button onclick="window.handleAcidReductionPrediction(${lng}, ${lat})" style="padding: 6px 12px; cursor: pointer; background: #409eff; color: white; border: none; border-radius: 4px;">降酸预测</button>`;
+      popupContent += `<button onclick="window.handleAcidInversionPrediction(${lng}, ${lat})" style="padding: 6px 12px; cursor: pointer; background: #67c23a; color: white; border: none; border-radius: 4px;">反酸预测</button>`;
+      popupContent += '</div>';
+      popupContent += '</div>';
+      
+      L.popup()
+        .setLatLng(e.latlng)
+        .setContent(popupContent)
+        .openOn(map.value);
+    });
+
+    mapLoading.value = false;
+
+  } catch (error) {
+    console.error('地图初始化失败:', error);
+    mapError.value = true;
+    mapLoading.value = false;
+    
+    let errorMessage = '地图初始化失败';
+    if (error instanceof Error) {
+      errorMessage += ': ' + error.message;
+    }
+    ElMessage.error(errorMessage);
+  }
+};
+
+const reloadMap = () => {
+  initMap();
+};
+
+// 组件卸载时清理
+onUnmounted(() => {
+  if (hoverTimer) {
+    clearTimeout(hoverTimer);
+  }
+
+  // 清理window上的方法
+  delete (window as any).handleAcidReductionPrediction;
+  delete (window as any).handleAcidInversionPrediction;
+});
+
+onMounted(() => {
+  // 将预测方法挂载到window对象
+  (window as any).handleAcidReductionPrediction = handleAcidReductionPrediction;
+  (window as any).handleAcidInversionPrediction = handleAcidInversionPrediction;
+  nextTick(() => {
+    initMap(); // DOM渲染完成后初始化,避免容器尺寸问题
+  });
+});
+</script>
+
+<style scoped>
+.feature-popup,
+.prediction-popup {
+  position: absolute;
+  z-index: 1000;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  min-width: 200px;
+  max-width: 300px;
+  pointer-events: none;
+}
+
+.prediction-popup {
+  max-width: 350px;
+  pointer-events: auto;
+}
+
+.popup-content {
+  padding: 0;
+}
+
+.popup-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 15px;
+  border-bottom: 1px solid #ebeef5;
+  background: #f5f7fa;
+  border-radius: 8px 8px 0 0;
+}
+
+.popup-header h4 {
+  margin: 0;
+  font-size: 14px;
+  color: #303133;
+}
+
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 18px;
+  cursor: pointer;
+  color: #909399;
+  padding: 0;
+  width: 20px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  pointer-events: auto;
+}
+
+.close-btn:hover {
+  color: #606266;
+}
+
+.popup-body {
+  padding: 15px;
+}
+
+.loading-info, .error-info, .no-data {
+  text-align: center;
+  color: #909399;
+  font-size: 14px;
+}
+
+.error-info {
+  color: #f56c6c;
+}
+
+.feature-info {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.info-item label {
+  font-weight: 600;
+  color: #606266;
+  font-size: 12px;
+}
+
+.info-item span {
+  color: #303133;
+  font-size: 12px;
+  text-align: right;
+}
+.map-card {
+  width: 850px;
+  flex: 1;
+  min-height: 600px;
+  margin: 0 auto;
+}
+
+.map-container {
+  height: 550px; /* 必须设置明确高度 */
+  width: 100%;
+  position: relative;
+  border-radius: 4px;
+  overflow: hidden;
+  background: #f0f2f5;
+  border: 1px solid #dcdfe6;
+}
+
+.loading {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  text-align: center;
+  z-index: 1000;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.loading-spinner {
+  width: 30px;
+  height: 30px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin: 0 auto 10px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading span {
+  margin-left: 8px;
+  color: #606266;
+}
+
+.error-tip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 80%;
+  z-index: 1000;
+}
+
+.title {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 15px;
+}
+
+.map-title {
+  color: #1a365d;
+  font-size: 1.6rem;
+  font-weight: 600;
+}
+
+.section-icon {
+  font-size: 2.2rem;
+  color: #3a9fd3;
+}
+
+.prediction-result {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.result-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.result-item label {
+  font-weight: 600;
+  color: #606266;
+  font-size: 12px;
+}
+
+.point-info {
+  font-size: 11px;
+  color: #666;
+  background: #f8f9fa;
+  padding: 8px;
+  border-radius: 4px;
+  line-height: 1.4;
+}
+
+.prediction-value {
+  font-weight: bold;
+  color: #409eff;
+  font-size: 14px;
+}
+
+.warnings {
+  margin-top: 8px;
+  padding: 8px;
+  background: #fff6f6;
+  border: 1px solid #fbc4c4;
+  border-radius: 4px;
+}
+
+.warning-item {
+  font-size: 11px;
+  color: #f56c6c;
+  line-height: 1.3;
+}
+
+.el-form-item {
+  margin-bottom: 16px;
+}
+.el-input {
+  width: 100%;
+}
+.feature-popup,
+.prediction-popup {
+  position: absolute;
+  z-index: 1000;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  min-width: 200px;
+  max-width: 300px;
+  pointer-events: none;
+}
+
+.prediction-popup {
+  max-width: 350px;
+  pointer-events: auto;
+}
+::v-deep .acid-reduction-dialog{
+  --el-dialog-margin-top:40vh ;
+}
+</style>