3 次代碼提交 35742d24b5 ... 5deb0a4599

作者 SHA1 備註 提交日期
  Ding 5deb0a4599 Merge branch 'lili' of qw/soilgd12 into master 2 月之前
  yes-yes-yes-k fdcfc2598f 添加南雄页面预测页面 2 月之前
  yes-yes-yes-k 93992558a7 添加南雄页面 2 月之前

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

@@ -236,7 +236,7 @@ const tabs = computed(() => {
 
     icon: "el-icon-data-analysis",
 
-    routes: ["/acidmodelmap"],
+    routes: ["/shaoguan_acidmodelmap","nanxiong_acidmodelmap"],
 
    },
 

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

@@ -40,9 +40,21 @@ export const tabMenuMap: Record<string, MenuItem[]> = {
   ],
   acidmodelmap: [
     {
-      index: "/acidmodelmap",
+      index: "acidmodelmap",
       label: "土壤酸化地块级预测",
       icon: Location,
+      children: [
+        {
+          index: '/shaoguan_acidmodelmap',
+          label: '韶关土壤酸化地图',
+          icon: Location
+        },
+        {
+          index: '/nanxiong_acidmodelmap',
+          label: '南雄土壤酸化地图',
+          icon: Location
+        }
+      ]
     },
     {
       index: "/pHPrediction",

+ 14 - 5
src/router/index.ts

@@ -89,15 +89,24 @@ const routes = [
         meta: { title: "反酸模型显示" },
       },
       {
-        path: "acidmodelmap",
-        name: "acidmodelmap",
+        path: "shaoguan_acidmodelmap",
+        name: "shaoguan_acidmodelmap",
         component: () =>
           import(
-            "@/views/User/acidModel/acidmodelmap.vue"
+            "@/views/User/acidModel/shaoguan_acidmodelmap.vue"
           ),
         meta: { title: "土壤酸化地图" }
       },
-       {
+      {
+        path: "nanxiong_acidmodelmap",
+        name: "nanxiong_acidmodelmap",
+        component: () =>
+          import(
+            "@/views/User/acidModel/nanxiong_acidmodelmap.vue"
+          ),
+        meta: { title: "土壤酸化地图" }
+      },
+      {
         path: "pHPrediction",
         name: "pHPrediction",
         component: () =>
@@ -124,7 +133,7 @@ const routes = [
           ),
         meta: { title: "土壤降酸采样数据" },
       },
-      
+
       {
         path: "DetectionStatistics",
         name: "DetectionStatistics",

+ 1526 - 0
src/views/User/acidModel/nanxiong_acidmodelmap.vue

@@ -0,0 +1,1526 @@
+<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" 
+         ref="popupElement"  
+         class="feature-popup fixed-center"
+         :style="{
+          left: popupPosition.x + '%',
+          top: popupPosition.y + '%',
+          translate:'none'
+         }">
+      <div class="popup-content">
+        <div class="popup-header" @mousedown="startDrag">
+          <!-- 地块信息 -->
+          <div v-if="featureInfo.loading" class="loading-info">
+            <div class="loading-spinner small"></div>
+            <span>加载地块信息中...</span>
+          </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 class="info-item">
+              <label>当前pH:</label>
+              <span v-if="phLoading">加载中...</span>
+              <span v-else>{{ currentPH?.toFixed(2) || '未获取' }}</span>
+            </div>
+          </div>
+          <div v-else class="no-data">无地块信息</div>
+          <button @click="closePopup" class="close-btn">×</button>
+        </div>
+
+        <div class="popup-body">
+          <!-- 操作按钮(仅在无参数输入、无结果时显示) -->
+          <div class="action-buttons" v-if="!showAcidReductionInput && !showAcidInversionInput && !showPredictionResult">
+            <el-button 
+              size="small" 
+              type="success" 
+              @click="startAcidInversionPrediction"
+            >
+              反酸预测
+            </el-button>
+            <el-button 
+              size="small" 
+              type="primary" 
+              @click="startAcidReductionPrediction"
+            >
+              降酸预测
+            </el-button>
+          </div>
+
+          <!-- 降酸参数 + 结果(同区域展示,无下方弹出) -->
+          <div v-if="showAcidReductionInput || (showPredictionResult && currentPredictionType === 'reduction')" class="prediction-card">
+            <!-- 降酸参数输入(未预测时显示) -->
+            <div v-if="showAcidReductionInput && !showPredictionResult">
+              <div class="section-title">降酸预测参数</div>
+              <el-form 
+                :model="acidReductionParams" 
+                :rules="acidReductionRules" 
+                ref="acidReductionFormRef"  
+                label-width="80px" 
+                size="small"
+              >
+                <div class="params-row">
+                  <el-form-item label="当前pH" class="form-item-compact readonly-item">
+                    <div class="current-ph-value">
+                      <span v-if="phLoading" class="loading-text">加载中...</span>
+                      <span v-else-if="currentPH !== null">{{ currentPH.toFixed(2) }}</span>
+                      <span v-else class="no-data-text">未获取</span>
+                    </div>
+                  </el-form-item>
+                  <el-form-item label="目标pH" prop="target_pH" class="form-item-compact">
+                    <el-input
+                      v-model.number="acidReductionParams.target_pH"
+                      type="number"
+                      step="0.01"
+                      placeholder="0-14"
+                      :disabled="phLoading"
+                    />
+                  </el-form-item>
+                </div>
+              </el-form>
+              <div class="input-buttons">
+                <el-button size="small" @click="cancelAcidReduction" :disabled="phLoading">取消</el-button>
+                <el-button size="small" type="primary" @click="confirmAcidReduction" :loading="predictionLoading || phLoading">
+                  开始预测
+                </el-button>
+              </div>
+            </div>
+
+            <!-- 降酸结果(预测后替换参数区显示) -->
+            <div v-if="showPredictionResult && currentPredictionType === 'reduction'" class="result-section">
+              <div class="section-title">降酸预测结果</div>
+              <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="result-content">
+                <div class="result-item">
+                  <span class="prediction-value reduction">每亩地土壤表层20cm撒{{ formatPredictionValue(predictionResult.prediction_reduce)}} 吨</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 v-if="showAcidInversionInput || (showPredictionResult && currentPredictionType === 'inversion')" class="prediction-card">
+            <!-- 反酸参数输入(未预测时显示) -->
+            <div v-if="showAcidInversionInput && !showPredictionResult">
+              <div class="section-title">反酸预测参数</div>
+              <div class="params-row">
+                <el-form-item label="当前pH" class="form-item-compact readonly-item">
+                  <div class="current-ph-value">
+                    <span v-if="phLoading">加载中...</span>
+                    <span v-else>{{ currentPH?.toFixed(2) || '未获取' }}</span>
+                  </div>
+                </el-form-item>
+              </div>
+              <div class="input-buttons" style="margin-top: 16px;">
+                <el-button size="small" @click="cancelAcidInversion" :disabled="phLoading">取消</el-button>
+                <el-button size="small" type="primary" @click="confirmAcidInversion" :loading="predictionLoading || phLoading">
+                  开始预测
+                </el-button>
+              </div>
+            </div>
+
+            <!-- 反酸结果(预测后替换参数区显示,和降酸样式1:1) -->
+            <div v-if="showPredictionResult && currentPredictionType === 'inversion'" class="result-section">
+              <div class="section-title">反酸预测结果</div>
+              <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="result-content">
+                <div class="result-item">
+                  <span class="prediction-value inversion">ΔpH{{ formatPredictionValue(predictionResult.prediction_reflux)}} </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>
+    <!--连接线容器-->
+    <div v-if="showPopup && showConnectionLine"
+         class="connection-line"
+         :style="connectionLine.style">
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, nextTick, onMounted, onUnmounted, computed } from "vue";
+import { ElMessage } from "element-plus";
+import type { FormInstance } from "element-plus";
+import { api8000 } from "@/utils/request";
+import { api5000 } from "../../../utils/request";
+
+// 新增:反酸预测相关状态(区分降酸/反酸的显示)
+const phLoading = ref(false); // 控制当前pH的加载状态(点击预测后先加载pH)
+
+// 反酸预测显示状态(补充缺失的变量)
+const showAcidInversionInput = ref(false);
+
+// 地图状态
+const mapLoading = ref(true);
+const mapError = ref(false);
+const map = ref<any>(null);
+const showPopup = ref(false);
+
+// 新增:高亮图层(存储当前选中地块的高亮图层)
+const highlightLayer = ref<any>(null);
+// 新增:控制连接线显示的标志
+const showConnectionLine = ref(false);
+
+// 预测状态
+const predictionLoading = ref(false);
+const predictionError = ref('');
+const predictionResult = ref<any>(null);
+const showPredictionResult = ref(false);
+const showAcidReductionInput = ref(false);
+const currentPH = ref<number | null>(null);
+
+let L: any = null;
+
+// 南雄固定参数(全程使用,无用户输入)
+const nanxiongFixedParams = reactive({
+  NO3: 33.80, 
+  NH4: 4.20,
+  Al: 4.92,
+  FeO: 43.12  
+});
+
+// 地块信息状态
+const featureInfo = reactive({
+  loading: false,
+  error: false,
+  data: null as any
+});
+
+// 新增:地块中心点坐标(用于连接线起点)
+const featureCenter = reactive({
+  lng: 0,
+  lat: 0
+});
+
+// 当前坐标
+const currentClickCoords = reactive({
+  lng: 0,
+  lat: 0
+});
+
+const currentPredictionType = ref<'reduction' | 'inversion' | null>(null);
+
+// 降酸预测参数输入相关(仅保留target_pH,删除NO3/NH4)
+const acidReductionFormRef = ref<FormInstance | null>(null);
+interface AcidReductionParams {
+  target_pH?: number;
+}
+const acidReductionParams = reactive<AcidReductionParams>({
+  target_pH: undefined
+});
+
+// 用地类型枚举
+const LandTypeEnum = {
+  DRY_LAND: '旱地',        // 旱地
+  PADDY_FIELD: '水田'      // 水田
+} as const;
+
+
+// 计算属性:判断当前是否为水田
+const isPaddyField = computed(() => {
+  return featureInfo.data?.landType === LandTypeEnum.PADDY_FIELD;
+});
+
+// 降酸预测校验规则(仅保留target_pH)
+const acidReductionRules = reactive({
+  target_pH: [
+    { 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' 
+    }
+  ]
+});
+
+
+// 格式化预测值
+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 drawHighlightFeature = (geoJsonFeature: any) => {
+  if (!map.value || !geoJsonFeature || !geoJsonFeature.geometry) return;
+
+  // 清除旧的高亮图层
+  if (highlightLayer.value) {
+    map.value.removeLayer(highlightLayer.value);
+    highlightLayer.value = null;
+  }
+
+  // 计算放大后的坐标(原地放大)
+  const scaleFeatureCoordinates = (feature: any, scale: number) => {
+    const scaledFeature = JSON.parse(JSON.stringify(feature));
+    
+    const scaleCoordinates = (coords: any): any => {
+      if (Array.isArray(coords[0]) && Array.isArray(coords[0][0]) && Array.isArray(coords[0][0][0])) {
+        return coords.map((polygon: any) => 
+          polygon.map((ring: any) => scaleCoordinates(ring))
+        );
+      } else if (Array.isArray(coords[0]) && Array.isArray(coords[0][0])) {
+        return coords.map((ring: any) => scaleCoordinates(ring));
+      } else if (Array.isArray(coords[0])) {
+        return coords.map((point: any) => {
+          const lng = point[0];
+          const lat = point[1];
+          
+          const centerLng = coords[0][0];
+          const centerLat = coords[0][1];
+          
+          const deltaLng = lng - centerLng;
+          const deltaLat = lat - centerLat;
+          
+          return [
+            centerLng + deltaLng * scale,
+            centerLat + deltaLat * scale
+          ];
+        });
+      }
+      return coords;
+    };
+
+    scaledFeature.geometry.coordinates = scaleCoordinates(feature.geometry.coordinates);
+    return scaledFeature;
+  };
+
+  // 创建放大1倍的地块
+  const scaledFeature = scaleFeatureCoordinates(geoJsonFeature, 1);
+
+  // 使用放大后的GeoJSON创建高亮图层
+  highlightLayer.value = L.geoJSON(scaledFeature, {
+    style: {
+      color: '#000',
+      weight: 4,
+      fillColor: '#ff4d4f',
+      fillOpacity: 1,
+      opacity: 1
+    }
+  }).addTo(map.value);
+
+  // 计算地块的中心点
+  const bounds = highlightLayer.value.getBounds();
+  const center = bounds.getCenter();
+  featureCenter.lng = center.lng;
+  featureCenter.lat = center.lat;
+
+  // 确保高亮图层在最上层
+  highlightLayer.value.bringToFront();
+  // 先隐藏连接线
+  showConnectionLine.value = false;
+  // 计算放大后地块的边界
+  const scaledBounds = highlightLayer.value.getBounds();
+  
+  // 调整地图视图以更好地显示放大的地块
+  map.value.flyToBounds(scaledBounds, {
+    padding: [50, 50], // 上下左右各50像素的内边距
+    duration: 0.3,     // 动画时长
+    maxZoom: 16        // 最大缩放级别限制
+  });
+
+  updateConnectionLine();
+};
+
+// 获取地块信息
+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: "xiafencun", 
+    };
+
+    const bounds = map.value.getBounds();
+    const size = map.value.getSize();
+    
+    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 feature = data.features[0];
+      const properties = feature.properties;
+      const hasValidData = properties.QSDWMC || properties.DLMC;
+      
+      if (hasValidData) {
+        featureInfo.data = {
+          village: properties.QSDWMC,
+          landType: properties.DLMC,
+        };
+        // 绘制高亮地块
+        drawHighlightFeature(feature);
+        return true;
+      }
+    }
+    
+    return false;
+    
+  } catch (error) {
+    console.error('获取地块信息失败:', error);
+    featureInfo.error = true;
+    return false;
+  } finally {
+    featureInfo.loading = false;
+  }
+};
+
+// 新增:获取水田地块基础参数(CEC/OM)
+const fetchPaddyFieldBaseParams = async (lng: number, lat: number) => {
+  try {
+    // 调用nearest_point接口(仅传必要参数)
+    const response = await api8000.get('/api/vector/nearest-with-predictions', {
+      params: {
+        target_lon: lng.toString(),
+        target_lat: lat.toString(),
+        city:'nanxiong'
+      }
+    });
+
+    const nearestPoint = response.data.nearest_point || {};
+    // 修复取值逻辑:存在则转数字,否则用默认值
+    return {
+      CEC: nearestPoint.cec !== undefined ? Number(nearestPoint.cec) : 7.14,
+      OM: nearestPoint.om !== undefined ? Number(nearestPoint.om) : 22.12
+    };
+  } catch (error) {
+    console.error('获取水田基础参数失败:', error);
+    ElMessage.error('获取地块基础参数失败,使用默认值');
+    // 接口调用失败时返回默认值
+    return { CEC: 7.14, OM: 22.12 };
+  }
+};
+
+// 新增:缓存已获取的原始pH值
+const originalPH = ref<number | null>(null);
+
+// 新增:单独获取当前pH的方法
+const fetchCurrentPH = async () => {
+  if (originalPH.value !== null) {
+    currentPH.value = originalPH.value;
+    return true; 
+  }
+  if (phLoading.value) return false;
+
+  phLoading.value = true;
+  try {
+    const params = {
+      target_lon: currentClickCoords.lng.toString(),
+      target_lat: currentClickCoords.lat.toString(),
+      city:'nanxiong'
+    };
+
+    const response = await api8000.get('/api/vector/nearest-with-predictions', { params });
+    const phValue = response.data.nearest_point?.ph !== undefined 
+      ? Number(response.data.nearest_point.ph) 
+      : null;
+      
+    originalPH.value = phValue;
+    currentPH.value = phValue;
+    return phValue !== null;
+  } catch (error) {
+    console.error('获取当前pH失败:', error);
+    ElMessage.error('获取土壤当前pH值失败,请重试');
+    currentPH.value = null;
+    return false;
+  } finally {
+    phLoading.value = false;
+  }
+};
+
+// 降酸预测:点击后先加载pH,再显示输入区
+const startAcidReductionPrediction = async () => {
+  // 先获取当前pH
+  const fetchSuccess = await fetchCurrentPH();
+  if (!fetchSuccess) return;
+
+  // pH获取成功,显示降酸输入区
+  currentPredictionType.value = 'reduction';
+  showAcidReductionInput.value = true;
+  showPredictionResult.value = false;
+};
+
+// 反酸预测:点击后先加载pH,再显示反酸区域
+const startAcidInversionPrediction = async () => {
+  if (!featureInfo.data?.landType) {
+    ElMessage.error('请先选择有效的地块(需包含用地类型)');
+    return;
+  }
+  // 先获取当前pH
+  const fetchSuccess = await fetchCurrentPH();
+  if (!fetchSuccess) return;
+
+  // pH获取成功,显示反酸区域(无输入参数)
+  currentPredictionType.value = 'inversion';
+  showAcidInversionInput.value = true; 
+  showAcidReductionInput.value = false;
+  showPredictionResult.value = false;
+};
+
+// 取消降酸预测
+const cancelAcidReduction = () => {
+  showAcidReductionInput.value = false;
+  currentPredictionType.value = null;
+};
+
+// 取消反酸预测
+const cancelAcidInversion = () => {
+  showAcidInversionInput.value = false; 
+  currentPredictionType.value = null;
+};
+
+// 确认降酸预测
+const confirmAcidReduction = async () => {
+  if (!acidReductionFormRef.value) return;
+  if (currentPH.value === null) {
+    ElMessage.error('请先获取当前pH值');
+    return;
+  }
+
+  try {
+    await acidReductionFormRef.value.validate();
+    
+    // 调用预测接口(仅传target_pH,NO3/NH4用固定值)
+    await callPredictionAPI(
+      currentClickCoords.lng,
+      currentClickCoords.lat,
+      {
+        target_pH: acidReductionParams.target_pH!
+      }
+    );
+  } catch (error) {
+    ElMessage.error('输入参数不合法,请检查后重试');
+  }
+};
+
+// 确认反酸预测(无表单校验,直接调用)
+const confirmAcidInversion = async () => {
+  if (currentPH.value === null) {
+    ElMessage.error('请先获取当前pH值');
+    return;
+  }
+  if (!featureInfo.data?.landType) {
+    ElMessage.error('未获取到地块用地类型,无法进行预测');
+    return;
+  }
+
+  try {
+    // 直接调用预测接口(无用户输入参数)
+    await callPredictionAPI(
+      currentClickCoords.lng,
+      currentClickCoords.lat
+    );
+  } catch (error) {
+    ElMessage.error('预测请求失败,请稍后重试');
+  }
+};
+
+// 调用预测接口
+const callPredictionAPI = async (
+  lng: number, 
+  lat: number, 
+  params?: {
+    target_pH?: number;
+  }
+) => {
+  predictionLoading.value = true;
+  predictionError.value = '';
+  predictionResult.value = null;
+
+  try {
+    // 区分预测类型处理
+    if (currentPredictionType.value === 'inversion' && isPaddyField.value) {
+      // ========== 水田反酸预测逻辑 ==========
+      // 1. 先获取地块基础参数(CEC/OM)
+      const baseParams = await fetchPaddyFieldBaseParams(lng, lat);
+      
+      // 2. 合并:基础参数 + 南雄固定参数
+      const requestData = {
+        model_id: 36, // 水田反酸接口固定model_id
+        parameters: {
+          CEC: baseParams.CEC,
+          NH4: nanxiongFixedParams.NH4,
+          NO3: nanxiongFixedParams.NO3,
+          Al: nanxiongFixedParams.Al,
+          FeO: nanxiongFixedParams.FeO,
+          OM: baseParams.OM
+        }
+      };
+
+      // 3. 调用水田反酸POST接口
+      const response = await api5000.post('/predict', requestData);
+      
+      // 4. 处理返回结果
+      predictionResult.value = {
+        prediction_reflux: response.data.result?.[0],
+        nearest_point: { ph: originalPH.value }
+      };
+      currentPH.value = originalPH.value;
+
+    } else {
+      // ========== 其他预测类型(降酸/非水田反酸) ==========
+      const requestParams: Record<string, string | number> = {
+        target_lon: lng,
+        target_lat: lat,
+        city:'nanxiong',
+      };
+
+      if (currentPredictionType.value === 'reduction') {
+        requestParams.prediction_type = 'reduce';
+        if (params?.target_pH) requestParams.target_pH = params.target_pH;
+      } else if (currentPredictionType.value === 'inversion') {
+        requestParams.prediction_type = 'reflux';
+      }
+
+      const apiUrl = '/api/vector/nearest-with-predictions';
+      const response = await api8000.get(apiUrl, { params: requestParams });
+      predictionResult.value = response.data;
+
+      // 同步缓存pH值
+      const newPH = predictionResult.value.nearest_point?.ph !== undefined 
+        ? Number(predictionResult.value.nearest_point.ph) 
+        : originalPH.value;
+      currentPH.value = newPH;
+      originalPH.value = newPH;
+    }
+
+    // 通用结果处理
+    showPredictionResult.value = true;
+    showAcidReductionInput.value = false;
+
+  } catch (error) {
+    currentPH.value = null;
+    console.error('调用预测接口失败:', error);
+    // 友好的错误提示
+    predictionError.value = currentPredictionType.value === 'inversion' && isPaddyField.value
+      ? '水田反酸预测失败,请检查参数或重试'
+      : '预测请求失败,请稍后重试';
+  } finally {
+    predictionLoading.value = false;
+  }
+};
+
+// 重置预测
+const resetPrediction = () => {
+  showPredictionResult.value = false;
+  predictionResult.value = null;
+  currentPredictionType.value = null;
+  showAcidReductionInput.value = false;
+  currentPH.value = originalPH.value;
+  // 重置降酸参数
+  acidReductionParams.target_pH = undefined;
+};
+
+// 地图点击事件处理
+const handleMapClick = async (e: any) => {
+  const lng = e.latlng.lng;
+  const lat = e.latlng.lat;
+  
+  // 更新当前坐标
+  currentClickCoords.lng = lng;
+  currentClickCoords.lat = lat;
+
+  // 重置地块中心点
+  featureCenter.lng = 0;
+  featureCenter.lat = 0;
+
+  // 计算点击点的屏幕坐标
+  const containerPoint = map.value.latLngToContainerPoint(e.latlng);
+  const mapRect = document.getElementById('map-container')!.getBoundingClientRect();
+  
+  clickPoint.x = mapRect.left + containerPoint.x;
+  clickPoint.y = mapRect.top + containerPoint.y;
+  
+  // 重置预测状态
+  resetPrediction();
+  originalPH.value = null;
+  currentPH.value = null;
+  featureInfo.data = null;
+  
+  // 先清除旧的高亮图层
+  if (highlightLayer.value) {
+    map.value.removeLayer(highlightLayer.value);
+    highlightLayer.value = null;
+  }
+  
+  // 获取地块信息
+  try {
+    const hasFeature = await getFeatureInfo(e.latlng, containerPoint);
+    
+    if (hasFeature) {
+      // 设置弹窗初始位置
+      popupPosition.x = 35;
+      popupPosition.y = 35;
+  
+      showPopup.value = true;
+      showConnectionLine.value = false;
+
+      await fetchCurrentPH();
+      // 使用点击点作为连接线起点
+      featureCenter.lng = lng;
+      featureCenter.lat = lat;
+      // 直接显示连接线
+      showConnectionLine.value = true;
+      nextTick(() => {
+        updateConnectionLine();
+      });
+    } else {
+      showPopup.value = false;
+      showConnectionLine.value = false;
+
+      ElMessage({
+        message: '请点击有效地块',
+        type: 'info',
+        offset: 250,
+        duration: 2000,
+        customClass: 'custom-land-message',
+      });
+    }
+  } catch (error) {
+    console.error('处理地块信息失败:', error);
+    // 清除高亮
+    if (highlightLayer.value) {
+      map.value.removeLayer(highlightLayer.value);
+      highlightLayer.value = null;
+    }
+    showPopup.value = false;
+    showConnectionLine.value = false;
+  }
+
+  // 使用点击点作为连接线起点
+  featureCenter.lng = lng;
+  featureCenter.lat = lat;
+  nextTick(() => {
+    updateConnectionLine();
+  });
+};
+
+// 关闭弹窗(清除高亮图层)
+const closePopup = () => {
+  showPopup.value = false;
+  showConnectionLine.value = false;
+  resetPrediction();
+  featureInfo.data = null;
+  // 清空pH缓存
+  originalPH.value = null;
+  currentPH.value = null;
+  // 清除高亮图层
+  if (highlightLayer.value && map.value) {
+    map.value.removeLayer(highlightLayer.value);
+    highlightLayer.value = null;
+  }
+};
+
+// 新增:弹窗拖动相关状态
+const popupElement = ref<HTMLElement>();
+const popupPosition = reactive({
+  x: 35,
+  y: 35
+});
+
+// 新增:连接线相关状态
+const connectionLine = reactive({
+  show: false,
+  style: {}
+});
+
+// 新增:点击坐标(用于连接线起点)
+const clickPoint = reactive({
+  x: 0,
+  y: 0
+});
+
+// 手动拖拽功能
+const isDragging = ref(false);
+const dragStartPos = reactive({ x: 0, y: 0 });
+
+const startDrag = (event: MouseEvent) => {
+  if (!popupElement.value) return;
+  
+  isDragging.value = true;
+  // 初始值:百分比转像素
+  const viewportWidth = window.innerWidth;
+  const viewportHeight = window.innerHeight;
+  const popupLeft = (popupPosition.x / 100) * viewportWidth;
+  const popupTop = (popupPosition.y / 100) * viewportHeight;
+  
+  dragStartPos.x = event.clientX - popupLeft;
+  dragStartPos.y = event.clientY - popupTop;
+  
+  document.addEventListener('mousemove', onDrag);
+  document.addEventListener('mouseup', stopDrag);
+  event.preventDefault();
+};
+
+const onDrag = (event: MouseEvent) => {
+  if (!isDragging.value) return;
+  
+  // 拖拽后转成百分比
+  const viewportWidth = window.innerWidth;
+  const viewportHeight = window.innerHeight;
+  // 计算拖拽后的像素位置
+  const newX = event.clientX - dragStartPos.x;
+  const newY = event.clientY - dragStartPos.y;
+  // 转成百分比(限制0-95,避免弹窗超出可视区)
+  popupPosition.x = Math.max(0, Math.min(95, (newX / viewportWidth) * 100));
+  popupPosition.y = Math.max(0, Math.min(95, (newY / viewportHeight) * 100));
+  updateConnectionLine();
+};
+
+const stopDrag = () => {
+  isDragging.value = false;
+  document.removeEventListener('mousemove', onDrag);
+  document.removeEventListener('mouseup', stopDrag);
+  nextTick(() => {
+    updateConnectionLine();
+  });
+};
+
+// 修改更新连接线方法,使用经纬度坐标
+const updateConnectionLine = () => {
+  if (!map.value || !popupElement.value || !featureCenter.lng || !featureCenter.lat) return;
+  
+  // 将地块中心点的经纬度转换为屏幕坐标
+  const centerLatLng = L.latLng(featureCenter.lat, featureCenter.lng);
+  const centerPoint = map.value.latLngToContainerPoint(centerLatLng);
+  
+  const mapRect = document.getElementById('map-container')!.getBoundingClientRect();
+  const startX = mapRect.left + centerPoint.x;
+  const startY = mapRect.top + centerPoint.y;
+  
+  // 获取弹窗中心点
+  const popupRect = popupElement.value.getBoundingClientRect();
+  // 视口总宽度/高度
+  const viewportWidth = window.innerWidth;
+  const viewportHeight = window.innerHeight;
+  // 弹窗左上角像素坐标(百分比转像素)
+  const popupLeft = (popupPosition.x / 100) * viewportWidth;
+  const popupTop = (popupPosition.y / 100) * viewportHeight;
+  // 弹窗中心点像素坐标
+  const endX = popupLeft + popupRect.width / 2;
+  const endY = popupTop + popupRect.height / 2;
+  
+  // 计算线的长度和角度
+  const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
+  const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI;
+  
+  connectionLine.style = {
+    left: startX + 'px',
+    top: startY + 'px',
+    width: length + 'px',
+    transform: `rotate(${angle}deg)`,
+    transformOrigin: '0 0',
+    '--arrow-angle': `${angle}deg`
+  };
+};
+
+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.24266, 114.39792],
+      zoom: 16
+    });
+
+    // WMS配置
+    const GEOSERVER_CONFIG = {
+      url: "/geoserver/wms",
+      workspace: "acidmap",
+      layerGroup: "xiafencun", 
+    };
+
+    // 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",
+      crs: L.CRS.EPSG4326,
+      attribution: "Data from GeoServer"
+    });
+
+    // 添加图层到地图
+    wmsLayer.addTo(map.value);
+
+    // 绑定点击事件
+    map.value.on('click', handleMapClick);
+
+    // 添加地图移动和缩放事件监听,实时更新连接线
+    map.value.on('moveend zoomend', () => {
+      if (showPopup.value && showConnectionLine.value) {
+        updateConnectionLine();
+      }
+    });
+
+    // 监听地图飞行动画完成事件
+    map.value.on('moveend', () => {
+      // 地图动画完成后显示连接线
+      if (showPopup.value && !showConnectionLine.value && featureCenter.lng && featureCenter.lat) {
+        showConnectionLine.value = true;
+        nextTick(() => {
+          updateConnectionLine();
+        });
+      }
+    });
+
+    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 (map.value) {
+    map.value.remove();
+  }
+  // 清除高亮图层
+  if (highlightLayer.value && map.value) {
+    map.value.removeLayer(highlightLayer.value);
+  }
+
+  // 移除拖拽事件监听
+  document.removeEventListener('mousemove', onDrag);
+  document.removeEventListener('mouseup', stopDrag);
+
+  window.removeEventListener('resize', handleWindowResize);
+});
+
+onMounted(() => {
+  nextTick(() => {
+    initMap();
+  });
+  // 监听窗口缩放
+  window.addEventListener('resize', handleWindowResize);
+});
+
+// 窗口缩放时重新计算连接线
+const handleWindowResize = () => {
+  if (showPopup.value && showConnectionLine.value) {
+    nextTick(() => {
+      updateConnectionLine();
+    });
+  }
+};
+</script>
+
+<style scoped>
+.feature-popup {
+  position: fixed;
+  z-index: 1000;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  width: 280px; /* 增大最小宽度 */
+  min-width: 280px; /* 强制最小宽度 */
+  min-height: 150px; /* 防止弹窗塌陷 */
+}
+
+.feature-popup:active {
+  cursor: grabbing;
+}
+
+.popup-content {
+  padding: 0;
+}
+
+.popup-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 15px 0 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;
+}
+
+.close-btn:hover {
+  color: #606266;
+}
+
+.popup-body {
+  padding: 10px;
+  max-height: 500px;
+  overflow-y: auto;
+}
+
+.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;
+}
+
+.custom-land-message .el-icon-info {
+  color: red !important;
+}
+
+.inversion-form {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr); /* 默认2列 */
+  gap: 12px 8px; /* 行间距12px,列间距8px */
+  width: 100%;
+}
+
+/* 网格项通用样式 */
+.form-item-grid {
+  margin-bottom: 0 !important;
+}
+
+/* 非水田时:当前pH独占一行 */
+.full-width {
+  grid-column: span 2; /* 跨2列 */
+}
+
+/* 适配小屏幕,防止布局错乱 */
+@media (max-width: 300px) {
+  .inversion-form {
+    grid-template-columns: 1fr; /* 小屏幕改为1列 */
+  }
+  .full-width {
+    grid-column: span 1;
+  }
+}
+
+.error-info {
+  color: #f56c6c;
+}
+
+.feature-info {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+}
+
+.info-item label {
+  font-weight: 600;
+  color: #606266;
+  font-size: 14px;
+}
+
+.info-item span {
+  color: #303133;
+  font-size: 14px;
+  text-align: right;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 8px;
+  justify-content: center;
+  margin: 12px 0;
+  padding-top: 12px;
+}
+
+.acid-reduction-input {
+  margin-top: 2px;
+  padding-top: 2px;
+}
+
+.input-buttons {
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+  margin-top: 12px;
+}
+
+.prediction-section {
+  margin-top: 2px;
+  padding-top: 2px;
+}
+
+.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;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.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 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;
+}
+
+/* 表单样式调整 */
+:deep(.el-form-item) {
+  margin-bottom: 12px;
+}
+
+:deep(.el-form-item__label) {
+  font-size: 12px;
+  line-height: 28px;
+}
+
+:deep(.el-input) {
+  font-size: 12px;
+}
+
+
+.popup-content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+/* 参数行布局 */
+.params-row {
+  display: flex;
+  gap: 0px;
+  margin-bottom: 12px;
+  align-items: center;
+  width: 100%;
+}
+
+/* 只读参数项样式 */
+.param-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding: 0 4px;
+}
+
+.param-item label {
+  font-size: 12px;
+  color: #606266;
+  margin-bottom: 4px;
+  font-weight: 600;
+}
+
+.param-item span {
+  font-size: 12px;
+  color: #303133;
+  padding: 6px 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  min-height: 28px;
+  display: flex;
+  align-items: center;
+}
+
+.current-ph-value {
+  width: 100%;
+  height: 32px;
+  line-height: 32px;
+  padding: 0 12px;
+  background: #f5f7fa;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 12px;
+  color: #606266;
+  display: flex;
+  align-items: center;
+}
+
+/* 只读项的特殊样式 */
+.readonly-item :deep(.el-form-item__label) {
+  color: #909399;
+}
+
+.readonly-param span {
+  background: #f0f2f5;
+  color: #909399;
+}
+
+.loading-text, .no-data-text {
+  color: #c0c4cc !important;
+  font-style: italic;
+}
+
+/* 紧凑的表单项样式 */
+.form-item-compact {
+  flex: 1;
+  margin-bottom: 0 !important;
+}
+
+:deep(.form-item-compact .el-form-item__content) {
+  margin-left: 0 !important;
+}
+
+:deep(.form-item-compact .el-form-item__label) {
+  width: 60px !important; /* 调整标签宽度 */
+  text-align: right;
+  padding-right: 8px;
+}
+
+:deep(.form-item-compact .el-input) {
+  width: 100%;
+}
+/* 确保 Tooltip 能正常显示 */
+:deep(.el-tooltip__trigger) {
+  width: 100%;
+}
+
+/* 确保 Tooltip 有足够的 z-index */
+:deep(.el-popper) {
+  z-index: 10000 !important;
+}
+
+:deep(.el-input-number .el-input__inner)::-webkit-outer-spin-button,
+:deep(.el-input-number .el-input__inner)::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+
+:deep(.el-input .el-input__inner[type="number"])::-webkit-outer-spin-button,
+:deep(.el-input .el-input__inner[type="number"])::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+
+.prediction-value.reduction {
+  color: #e6a23c; /* 降酸结果用橙色 */
+}
+
+.prediction-value.inversion {
+  color: #48bb78; /* 反酸结果用绿色 */
+}
+
+/* 新增:高亮图层的z-index确保在最上层 */
+:deep(.leaflet-geojson) {
+  z-index: 999 !important;
+}
+
+/* 连接线样式 */
+.connection-line {
+  position: fixed;
+  height: 1px;
+  background: transparent;
+  background-image: linear-gradient(to right, #409eff 50%, transparent 50%);
+  background-size: 10px 1px;
+  border: none;
+  z-index: 999;
+  pointer-events: none;
+  --arrow-angle: 0deg;
+}
+
+.connection-line::before {
+  content: '';
+  position: absolute;
+  left: -5px; /* 箭头在连接线起点左侧,对准地块 */
+  top: 50%;
+  /* 跟随连接线角度旋转,保持垂直居中 */
+  transform: translateY(-50%) rotate(var(--arrow-angle));
+  /* 三角形箭头:右向箭头(指向地块) */
+  width: 0;
+  height: 0;
+  border-style: solid;
+  border-width: 4px 8px 4px 0; /* 箭头尺寸:高4px*2,长8px */
+  border-color: transparent #409eff transparent transparent; /* 箭头颜色与连接线一致 */
+  transform-origin: center center;
+  z-index: 1;
+}
+/* 新增FeO输入项样式(和其他参数统一) */
+:deep(.form-item-compact.feo-item .el-input) {
+  width: 100%;
+}
+.feo-placeholder {
+  color: #c0c4cc;
+  font-size: 11px;
+}
+</style>
+
+<style>
+/* 强制覆盖“请点击有效地块”提示的图标颜色 */
+.custom-land-message.el-message--info .el-icon-info {
+  color: red !important;
+}
+
+/* 可选:同时修改提示文字颜色 */
+.custom-land-message .el-message__content {
+  color: #ff0000 !important;
+  font-size: 14px !important;
+}
+
+/* 可选:修改提示框整体样式 */
+.custom-land-message {
+  top: 250px !important; /* 和你设置的 offset 一致 */
+  background: #fef0f0 !important;
+  border: 1px solid #fbc4c4 !important;
+  border-radius: 8px !important;
+}
+</style>

+ 8 - 5
src/views/User/acidModel/acidmodelmap.vue → src/views/User/acidModel/shaoguan_acidmodelmap.vue

@@ -2,7 +2,7 @@
   <el-card class="map-card">
     <div class="title">
       <div class="section-icon">🗺️</div>
-      <p class="map-title">细粒度地块级酸化预测</p>
+      <p class="map-title">韶关市细粒度地块级酸化预测</p>
     </div>
 
     <div id="map-container" class="map-container">
@@ -671,6 +671,7 @@ const fetchPaddyFieldBaseParams = async (lng: number, lat: number) => {
       params: {
         target_lon: lng.toString(),
         target_lat: lat.toString(),
+        city:'shaoguan',
         NO3: defaultParams.NO3.toString(), // 传默认值不影响接口返回基础参数
         NH4: defaultParams.NH4.toString()
       }
@@ -679,9 +680,9 @@ const fetchPaddyFieldBaseParams = async (lng: number, lat: number) => {
     const nearestPoint = response.data.nearest_point || {};
     // 返回接口中的基础参数(根据实际接口返回字段调整key)
     return {
-      CEC: nearestPoint.CEC !== undefined ? Number(nearestPoint.CEC) : 7.14, // 兜底默认值
-      Al: nearestPoint.Al !== undefined ? Number(nearestPoint.Al) : 4.0,
-      OM: nearestPoint.OM !== undefined ? Number(nearestPoint.OM) : 22.12
+      CEC: nearestPoint.CEC !==  Number(nearestPoint.CEC) , 
+      Al: nearestPoint.Al !== Number(nearestPoint.Al) ,
+      OM: nearestPoint.OM !==  Number(nearestPoint.OM)
     };
   } catch (error) {
     console.error('获取水田基础参数失败:', error);
@@ -707,6 +708,7 @@ const fetchCurrentPH = async () => {
     const params = {
       target_lon: currentClickCoords.lng.toString(),
       target_lat: currentClickCoords.lat.toString(),
+      city:'shaoguan',
       NO3: defaultParams.NO3.toString(),
       NH4: defaultParams.NH4.toString()
     };
@@ -883,7 +885,8 @@ const callPredictionAPI = async (
       // ========== 其他预测类型(降酸/非水田反酸) ==========
       const requestParams: Record<string, string | number> = {
         target_lon: lng,
-        target_lat: lat
+        target_lat: lat,
+        city:'shaoguan'
       };
 
       if (currentPredictionType.value === 'reduction') {