Kaynağa Gözat

弹窗修改

yes-yes-yes-k 1 ay önce
ebeveyn
işleme
b94ad6cdc7
1 değiştirilmiş dosya ile 354 ekleme ve 374 silme
  1. 354 374
      src/views/User/acidModel/acidmodelmap.vue

+ 354 - 374
src/views/User/acidModel/acidmodelmap.vue

@@ -19,120 +19,133 @@
         </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 v-if="showPopup"  class="feature-popup fixed-center">
       <div class="popup-content">
         <div class="popup-header">
-          <h4>土壤预测结果</h4>
-          <button @click="closePredictionPopup" class="close-btn">×</button>
+          <h4>地块信息</h4>
+          <button @click="closePopup" class="close-btn">×</button>
         </div>
         <div class="popup-body">
-          <div v-if="predictionLoading" class="loading-info">
+          <!-- 坐标信息 -->
+          <div class="coordinate-info">
+            <div class="info-item">
+              <label>经度:</label>
+              <span>{{ currentClickCoords.lng.toFixed(6) }}</span>
+            </div>
+            <div class="info-item">
+              <label>纬度:</label>
+              <span>{{ currentClickCoords.lat.toFixed(6) }}</span>
+            </div>
+          </div>
+
+          <!-- 地块信息 -->
+          <div v-if="featureInfo.loading" class="loading-info">
             <div class="loading-spinner small"></div>
-            <span>预测中...</span>
+            <span>加载地块信息中...</span>
           </div>
-          <div v-else-if="predictionError" class="error-info">
-            {{ predictionError }}
+          <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 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 v-else class="no-data">无地块信息</div>
+
+          <!-- 操作按钮 -->
+          <div class="action-buttons" v-if="!showPredictionResult && !showAcidReductionInput">
+            <el-button 
+              size="small" 
+              type="primary" 
+              @click="startAcidReductionPrediction"
+            >
+              降酸预测
+            </el-button>
+            <el-button 
+              size="small" 
+              type="success" 
+              @click="handleAcidInversionPrediction"
+            >
+              反酸预测
+            </el-button>
+          </div>
+
+          <!-- 降酸预测参数输入 -->
+          <div v-if="showAcidReductionInput" class="acid-reduction-input">
+            <div class="section-title">降酸预测参数</div>
+            <el-form :model="acidReductionParams" :rules="acidReductionRules" ref="acidReductionFormRef" label-width="80px" size="small">
+              <el-form-item label="目标pH" prop="targetPH">
+                <el-input
+                  v-model.number="acidReductionParams.targetPH"
+                  type="number"
+                  step="0.01"
+                  placeholder="0-14"
+                />
+              </el-form-item>
+              <el-form-item label="NO3" prop="no3">
+                <el-input
+                  v-model.number="acidReductionParams.no3"
+                  type="number"
+                  step="0.01"
+                  min="0"
+                  placeholder="非负数"
+                />
+              </el-form-item>
+              <el-form-item label="CEC" prop="cec">
+                <el-input
+                  v-model.number="acidReductionParams.cec"
+                  type="number"
+                  step="0.01"
+                  min="0"
+                  placeholder="非负数"
+                />
+              </el-form-item>
+            </el-form>
+            <div class="input-buttons">
+              <el-button size="small" @click="cancelAcidReduction">取消</el-button>
+              <el-button size="small" type="primary" @click="confirmAcidReduction" :loading="predictionLoading">
+                开始预测
+              </el-button>
+            </div>
+          </div>
+
+          <!-- 预测结果展示 -->
+          <div v-if="showPredictionResult" class="prediction-section">
+            <div class="section-title">{{ predictionTitle }}</div>
+            <div v-if="predictionLoading" class="loading-info">
+              <div class="loading-spinner small"></div>
+              <span>预测中...</span>
             </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 v-else-if="predictionError" class="error-info">
+              {{ predictionError }}
             </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 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-buttons">
+                <el-button size="small" @click="resetPrediction">重新预测</el-button>
+                <el-button size="small" type="primary" @click="closePopup">关闭</el-button>
               </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>
 
@@ -146,23 +159,15 @@ 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 showPopup = ref(false);
+
+
+// 预测状态
 const predictionLoading = ref(false);
 const predictionError = ref('');
 const predictionResult = ref<any>(null);
-
-const disableMouseEvents = ref(false);
-
+const showPredictionResult = ref(false);
+const showAcidReductionInput = ref(false);
 
 let L:any = null;
 
@@ -173,37 +178,41 @@ const featureInfo = reactive({
   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 acidReductionFormRef = ref<FormInstance | null>(null);
 const acidReductionParams = reactive({
-  targetPH: 0, // 默认值(可调整)
-  no3: 0,         // 默认值
-  cec: 0          // 默认值
+  targetPH: 7.0, // 设置一个合理的默认值
+  no3: 34.05,    // 设置一个合理的默认值
+  cec: 7.87      // 设置一个合理的默认值
 });
 
-// 过滤警告信息,排除模型24的错误
+// 过滤警告信息
 const filteredWarnings = computed(() => {
   if (!predictionResult.value?.warnings) return [];
-  
   return predictionResult.value.warnings.filter((warning: string) => {
-    // 过滤掉模型24相关的错误信息
     return !warning.includes('模型24');
   });
 });
 
+// 预测标题
+const predictionTitle = computed(() => {
+  if (currentPredictionType.value === 'reduction') return '降酸预测结果';
+  if (currentPredictionType.value === 'inversion') return '反酸预测结果';
+  return '预测结果';
+});
+
 // 输入校验规则
 const acidReductionRules = reactive({
   targetPH: [
-    { required: true, message: '请输入目标ph值', trigger: 'blur' },
+    { required: true, message: '请输入目标pH值', trigger: 'blur' },
     { type: 'number', message: '请输入有效数字', trigger: 'blur' },
     { 
       validator: (rule: any, value: number, callback: any) => {
@@ -245,22 +254,6 @@ const acidReductionRules = reactive({
     }
   ]
 });
-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 => {
@@ -268,26 +261,22 @@ const formatPredictionValue = (value: any): string => {
   
   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;
   
@@ -305,7 +294,6 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
     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');
@@ -331,11 +319,8 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
     
     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) {
@@ -343,11 +328,11 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
           village: properties.QSDWMC,
           landType: properties.DLMC,
         };
-        return true; // 找到有效地块
+        return true;
       }
     }
     
-    return false; // 没有找到有效地块
+    return false;
     
   } catch (error) {
     console.error('获取地块信息失败:', error);
@@ -358,30 +343,30 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
   }
 };
 
-// 调用8000后端预测接口
-const callPredictionAPI = async (lng: number, lat: number,acidReductionParams?: {
+// 调用预测接口
+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());
     }
 
+    console.log('调用预测接口,参数:', urlParams.toString());
+
     const response = await fetch(
       `http://localhost:8000/api/vector/nearest-with-predictions?${urlParams.toString()}`
     );
@@ -392,41 +377,40 @@ const callPredictionAPI = async (lng: number, lat: number,acidReductionParams?:
 
     const data = await response.json();
     predictionResult.value = data;
+    console.log('预测结果:', data);
 
-    //console.log('完整的预测结果:', data); // 调试用,查看返回的数据结构
-    //console.log('模型24预测结果:', data.prediction_model24, '类型:', typeof data.prediction_model24);
-
-    // 显示预测弹窗
-    showPredictionPopup.value = true;
+    // 显示预测结果
+    showPredictionResult.value = true;
+    showAcidReductionInput.value = false;
 
   } catch (error) {
     console.error('调用预测接口失败:', error);
     predictionError.value = `预测失败: ${error instanceof Error ? error.message : '未知错误'}`;
-    ElMessage.error('预测请求失败,请检查后端服务');
-
-    disableMouseEvents.value = false;
+    ElMessage.error('预测请求失败,请检查后端服务是否启动');
   } finally {
     predictionLoading.value = false;
   }
 };
 
-
-// 降酸预测按钮点击
-const handleAcidReductionPrediction = (lng: number, lat: number) => {
+// 开始降酸预测(显示参数输入)
+const startAcidReductionPrediction = () => {
   currentPredictionType.value = 'reduction';
-  currentClickCoords.lng = lng;
-  currentClickCoords.lat = lat;
   showAcidReductionInput.value = true;
 };
 
-// 新增:确认降酸预测(输入完成后调用)
+// 取消降酸预测
+const cancelAcidReduction = () => {
+  showAcidReductionInput.value = false;
+  currentPredictionType.value = null;
+};
+
+// 确认降酸预测
 const confirmAcidReduction = async () => {
-console.log(acidReductionParams); 
-  // 表单校验
   if (!acidReductionFormRef.value) return;
+  
   try {
-    await acidReductionFormRef.value.validate(); // 校验输入是否合法
-    // 校验通过,调用接口(传递输入的3个参数)
+    await acidReductionFormRef.value.validate();
+    
     await callPredictionAPI(
       currentClickCoords.lng,
       currentClickCoords.lat,
@@ -436,84 +420,54 @@ console.log(acidReductionParams);
         cec: acidReductionParams.cec
       }
     );
-    // 关闭输入弹窗
-    showAcidReductionInput.value = false;
+    
   } catch (error) {
-    // 校验失败,不调用接口
     ElMessage.error('输入参数不合法,请检查后重试');
     console.error('表单校验失败:', error);
   }
 };
 
 // 反酸预测按钮点击
-const handleAcidInversionPrediction = (lng: number, lat: number) => {
+const handleAcidInversionPrediction = () => {
   currentPredictionType.value = 'inversion';
-  callPredictionAPI(lng, lat);
+  callPredictionAPI(currentClickCoords.lng, currentClickCoords.lat);
 };
 
+// 重置预测
+const resetPrediction = () => {
+  showPredictionResult.value = false;
+  predictionResult.value = null;
+  currentPredictionType.value = null;
+  showAcidReductionInput.value = false;
+};
 
-// 鼠标移动事件处理
-const handleMouseMove = (e: any) => {
-
-  // 如果禁用了鼠标事件,直接返回
-  if (disableMouseEvents.value) {
-    return;
-  }
+// 地图点击事件处理
+const handleMapClick = async (e: any) => {
+  const lng = e.latlng.lng;
+  const lat = e.latlng.lat;
+  
+  // 更新当前坐标
+  currentClickCoords.lng = lng;
+  currentClickCoords.lat = lat;
 
-  if (hoverTimer) {
-    clearTimeout(hoverTimer);
-  }
+  showPopup.value = true;
+  
+  // 重置预测状态
+  resetPrediction();
   
-  // 设置防抖,避免频繁请求
-  hoverTimer = setTimeout(async () => {
+  // 获取地块信息
+  try {
     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);
+    await getFeatureInfo(e.latlng, containerPoint);
+  } catch (error) {
+    console.error('处理地块信息失败:', error);
   }
-  showPopup.value = false;
 };
 
 // 关闭弹窗
 const closePopup = () => {
   showPopup.value = false;
-};
-
-// 关闭预测弹窗
-const closePredictionPopup = () => {
-  showPredictionPopup.value = false;
-  predictionResult.value = null;
-  currentPredictionType.value = null;
-  
-  // 重新启用鼠标事件
-  disableMouseEvents.value = false;
+  resetPrediction();
 };
 
 const initMap = async () => {
@@ -521,7 +475,7 @@ const initMap = async () => {
   mapError.value = false;
 
   try {
-    // 动态导入Leaflet(保持不变)
+    // 动态导入Leaflet
     if (!L) {
       L = await import('leaflet');
       await import('leaflet/dist/leaflet.css');
@@ -544,8 +498,8 @@ const initMap = async () => {
     map.value = L.map('map-container', {
       zoomControl: true,
       attributionControl: false,
-      center: [25.202903, 113.25383], // 设置初始中心点
-      zoom: 10 // 设置初始缩放级别
+      center: [25.202903, 113.25383],
+      zoom: 10
     });
 
     // WMS配置
@@ -560,48 +514,16 @@ const initMap = async () => {
       layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
       format: "image/png",
       transparent: true,
-      version: "1.1.1", // 使用1.1.1版本更稳定
-      crs: L.CRS.EPSG4326, // 明确指定坐标系
+      version: "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);
-    });
+    // 绑定点击事件
+    map.value.on('click', handleMapClick);
 
     mapLoading.value = false;
 
@@ -624,41 +546,30 @@ const reloadMap = () => {
 
 // 组件卸载时清理
 onUnmounted(() => {
-  if (hoverTimer) {
-    clearTimeout(hoverTimer);
+  if (map.value) {
+    map.value.remove();
   }
-
-  // 清理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渲染完成后初始化,避免容器尺寸问题
+    initMap();
   });
 });
 </script>
 
 <style scoped>
-.feature-popup,
-.prediction-popup {
-  position: absolute;
+.feature-popup {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%,-50%);
   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;
+  min-width: 320px;
+  max-width: 380px;
 }
 
 .popup-content {
@@ -669,7 +580,7 @@ onMounted(() => {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  padding: 12px 15px;
+  padding: 10px 15px;
   border-bottom: 1px solid #ebeef5;
   background: #f5f7fa;
   border-radius: 8px 8px 0 0;
@@ -693,7 +604,6 @@ onMounted(() => {
   display: flex;
   align-items: center;
   justify-content: center;
-  pointer-events: auto;
 }
 
 .close-btn:hover {
@@ -702,12 +612,25 @@ onMounted(() => {
 
 .popup-body {
   padding: 15px;
+  max-height: 500px;
+  overflow-y: auto;
+}
+
+.coordinate-info {
+  margin-bottom: 12px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #f0f0f0;
 }
 
 .loading-info, .error-info, .no-data {
   text-align: center;
   color: #909399;
   font-size: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  margin: 10px 0;
 }
 
 .error-info {
@@ -718,6 +641,7 @@ onMounted(() => {
   display: flex;
   flex-direction: column;
   gap: 8px;
+  margin-bottom: 12px;
 }
 
 .info-item {
@@ -729,14 +653,127 @@ onMounted(() => {
 .info-item label {
   font-weight: 600;
   color: #606266;
-  font-size: 12px;
+  font-size: 14px;
 }
 
 .info-item span {
   color: #303133;
-  font-size: 12px;
+  font-size: 14px;
   text-align: right;
 }
+
+.action-buttons {
+  display: flex;
+  gap: 8px;
+  justify-content: center;
+  margin: 12px 0;
+  padding-top: 12px;
+  border-top: 1px solid #f0f0f0;
+}
+
+.acid-reduction-input {
+  margin-top: 12px;
+  padding-top: 12px;
+  border-top: 1px solid #f0f0f0;
+}
+
+.input-buttons {
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+  margin-top: 12px;
+}
+
+.prediction-section {
+  margin-top: 12px;
+  padding-top: 12px;
+  border-top: 1px solid #f0f0f0;
+}
+
+.section-title {
+  font-weight: 600;
+  color: #303133;
+  font-size: 13px;
+  margin-bottom: 8px;
+}
+
+.prediction-result {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.result-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.result-item label {
+  font-weight: 600;
+  color: #606266;
+  font-size: 14px;
+}
+
+.point-info {
+  font-size: 11px;
+  color: #666;
+  background: #f8f9fa;
+  padding: 6px;
+  border-radius: 4px;
+  line-height: 1.4;
+}
+
+.prediction-value {
+  font-weight: bold;
+  font-size: 13px;
+}
+
+.prediction-value.reduction {
+  color: #e6a23c;
+}
+
+.warnings {
+  margin-top: 8px;
+  padding: 6px;
+  background: #fff6f6;
+  border: 1px solid #fbc4c4;
+  border-radius: 4px;
+}
+
+.warning-item {
+  font-size: 11px;
+  color: #f56c6c;
+  line-height: 1.3;
+}
+
+.result-buttons {
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+  margin-top: 12px;
+}
+
+.loading-spinner {
+  width: 20px;
+  height: 20px;
+  border: 2px solid #f3f3f3;
+  border-top: 2px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+.loading-spinner.small {
+  width: 16px;
+  height: 16px;
+  border-width: 1.5px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
 .map-card {
   width: 850px;
   flex: 1;
@@ -745,7 +782,7 @@ onMounted(() => {
 }
 
 .map-container {
-  height: 550px; /* 必须设置明确高度 */
+  height: 550px;
   width: 100%;
   position: relative;
   border-radius: 4px;
@@ -767,21 +804,6 @@ onMounted(() => {
   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;
@@ -814,76 +836,34 @@ onMounted(() => {
   color: #3a9fd3;
 }
 
-.prediction-result {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.result-item {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
+/* 表单样式调整 */
+:deep(.el-form-item) {
+  margin-bottom: 12px;
 }
 
-.result-item label {
-  font-weight: 600;
-  color: #606266;
+:deep(.el-form-item__label) {
   font-size: 12px;
+  line-height: 28px;
 }
 
-.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;
+:deep(.el-input) {
+  font-size: 12px;
 }
 
-.warning-item {
-  font-size: 11px;
-  color: #f56c6c;
-  line-height: 1.3;
+:deep(.el-input__inner) {
+  height: 28px;
+  line-height: 28px;
 }
 
-.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;
+.popup-body {
+  padding: 15px;
+  overflow-y: auto;
+  max-height: calc(80vh - 60px); /* 减去头部高度 */
 }
 
-.prediction-popup {
-  max-width: 350px;
-  pointer-events: auto;
-}
-::v-deep .acid-reduction-dialog{
-  --el-dialog-margin-top:40vh ;
+.popup-content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
 }
 </style>