Parcourir la source

fix: 上传识别,勾画范围限制范围

lxf il y a 2 semaines
Parent
commit
4932bba300

+ 172 - 59
src/components/popup/UploadProgressPopup.vue

@@ -8,12 +8,11 @@
                     <span>{{ t('上传照片') }}</span>
                     <span v-if="!uploadRequired" class="optional">{{ t('(可选)') }}</span>
                 </div>
-                <div class="ai-btn" :style="{ opacity: aiBtnDisabled ? 0.5 : 1 }" @click="handleAIAnalysis">{{ aiBtnText }}</div>
             </div>
-            <upload ref="uploadRef" :maxCount="10" :initImgArr="props.initImgArr" :before-read="beforeReadUpload"
-                :after-read="afterReadUpload" @handleUpload="onUploadChange">
+            <upload ref="uploadRef" :maxCount="10" :initImgArr="props.initImgArr" :enableIdentifyStatus="true"
+                :before-read="beforeReadUpload" :after-read="afterReadUpload" @handleUpload="onUploadChange"
+                @clickPreview="onClickPreview">
             </upload>
-            <div class="upload-result" v-if="aiStr.length">{{ t('AI识别结果:') }}{{ aiStr }}</div>
         </div>
         <slot name="footer"></slot>
         <div class="upload-action-btns">
@@ -21,6 +20,24 @@
             <div class="confirm-btn" @click="handleConfirm">{{ confirmText }}</div>
         </div>
     </popup>
+
+    <popup v-model:show="showIdentifyPreview" class="identify-result-popup" teleport="body" z-index="3000">
+        <div class="identify-preview-wrap">
+            <img class="identify-preview-img" :src="previewImageUrl" alt="" />
+            <div class="identify-result-panel" v-if="previewResult">
+                <div class="result-header">
+                    <span class="accent-bar"></span>
+                    <span class="result-name">{{ t('病虫名称:') }}{{ getIdentifyField(previewResult, ['disease_name', 'name', 'pest_name']) }}</span>
+                </div>
+                <div class="result-tabs">
+                    <span v-for="(tab, index) in resultTabs" :key="tab.key"
+                        class="result-tab" :class="{ active: activeResultTab === index }"
+                        @click="activeResultTab = index">{{ tab.label }}</span>
+                </div>
+                <div class="result-content">{{ currentTabContent }}</div>
+            </div>
+        </div>
+    </popup>
 </template>
 
 <script setup>
@@ -32,7 +49,7 @@ import { ElMessage } from 'element-plus';
 import upload from '@/components/upload.vue';
 import UploadFile from "@/utils/upliadFile";
 import { getFileExt } from "@/utils/util";
-import {base_img_url2} from '@/api/config';
+import { base_img_url2 } from '@/api/config';
 
 const props = defineProps({
     show: {
@@ -70,15 +87,33 @@ const miniUserId = localStorage.getItem("MINI_USER_ID");
 
 const loading = computed(() => props.popupImageUploadLoading || popupInnerLoading.value);
 
-const aiStr = ref('');
-/** 已点击过 AI 分析,防止重复请求 */
-const aiAnalyzeLocked = ref(false);
-const aiBtnDisabled = computed(
-    () => popupInnerImgArr.value.length === 0 || aiAnalyzeLocked.value,
-);
-const aiBtnText = computed(() =>
-    aiAnalyzeLocked.value ? t('识别中') : t('AI 智能分析'),
-);
+const showIdentifyPreview = ref(false);
+const previewImageUrl = ref('');
+const previewResult = ref(null);
+const activeResultTab = ref(0);
+
+const resultTabs = computed(() => [
+    { key: 'name', label: t('病虫的名称'), fields: ['disease_name', 'name', 'pest_name'] },
+    { key: 'phenotype', label: t('病虫的表型'), fields: ['phenotype', 'phenotypic_characteristics'] },
+    { key: 'harm', label: t('危害'), fields: ['harm', 'damage', 'kepu', 'harm_desc'] },
+    { key: 'peak', label: t('高发时期'), fields: ['peak_period', 'high_incidence_period', 'outbreak_period'] },
+]);
+
+const currentTabContent = computed(() => {
+    const tab = resultTabs.value[activeResultTab.value];
+    if (!tab || !previewResult.value) return '-';
+    return getIdentifyField(previewResult.value, tab.fields);
+});
+
+function getIdentifyField(result, fields) {
+    for (const field of fields) {
+        const value = result?.[field];
+        if (value !== undefined && value !== null && value !== '') {
+            return value;
+        }
+    }
+    return '-';
+}
 
 function getConfirmImgArr() {
     if (imgTouched.value || popupInnerImgArr.value.length) {
@@ -87,7 +122,7 @@ function getConfirmImgArr() {
     return [...(props.initImgArr || [])];
 }
 
-/** 与 upload 组件内 imgArr 同步(含删除),用于 AI 按钮透明度等 */
+/** 与 upload 组件内 imgArr 同步(含删除),用于确认提交等 */
 function onUploadChange({ imgArr }) {
     imgTouched.value = true;
     popupInnerImgArr.value = [...(imgArr || [])];
@@ -114,8 +149,12 @@ const afterReadUpload = async (data) => {
             if (resFilename) {
                 file.status = "done";
                 file.message = "";
+                file.resFilename = resFilename;
+                file.url = base_img_url2 + resFilename;
                 imgTouched.value = true;
                 popupInnerImgArr.value.push(resFilename);
+                file.identifyStatus = 'identifying';
+                identifySingleImage(file, resFilename);
             } else {
                 file.status = "failed";
                 file.message = "上传失败";
@@ -128,6 +167,40 @@ const afterReadUpload = async (data) => {
     }
 };
 
+async function identifySingleImage(file, resFilename) {
+    let farmData = {};
+    try {
+        farmData = JSON.parse(localStorage.getItem('selectedFarmData') || '{}');
+    } catch {
+        farmData = {};
+    }
+    const params = {
+        image_urls: [base_img_url2 + resFilename],
+        crop_type: farmData.farm_variety,
+    };
+    try {
+        const res = await VE_API.record.batchPestIdentify(params);
+        if (res.code === 200 && res.results?.length) {
+            file.identifyStatus = 'done';
+            file.identifyResult = res.results[0];
+        } else {
+            file.identifyStatus = 'failed';
+            ElMessage.error(t('AI识别失败,请稍后再试!'));
+        }
+    } catch {
+        file.identifyStatus = 'failed';
+        ElMessage.error(t('AI识别失败,请稍后再试!'));
+    }
+}
+
+function onClickPreview(file) {
+    if (file.identifyStatus !== 'done' || !file.identifyResult) return;
+    previewImageUrl.value = file.url || base_img_url2 + file.resFilename;
+    previewResult.value = file.identifyResult;
+    activeResultTab.value = 0;
+    showIdentifyPreview.value = true;
+}
+
 function handleConfirm() {
     const imgArr = getConfirmImgArr();
     if (props.uploadRequired && !imgArr.length) {
@@ -141,35 +214,11 @@ function uploadReset() {
     popupInnerImgArr.value = [];
     imgTouched.value = false;
     popupInnerLoading.value = false;
+    showIdentifyPreview.value = false;
+    previewImageUrl.value = '';
+    previewResult.value = null;
+    activeResultTab.value = 0;
     uploadRef.value?.uploadReset?.();
-    aiStr.value = '';
-    aiAnalyzeLocked.value = false;
-}
-
-function handleAIAnalysis() {
-    if (aiAnalyzeLocked.value) return;
-    const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
-    const imgArr = getConfirmImgArr();
-    if (imgArr.length === 0) return;
-    aiAnalyzeLocked.value = true;
-    const params = {
-        image_urls: imgArr,
-        crop_type: farmData.farm_variety,
-    };
-    VE_API.record.batchPestIdentify(params)
-        .then((res) => {
-            if (res.code === 200 && res.results?.length) {
-                aiStr.value = res.results.map((item) => item.disease_name).join(',');
-            } else {
-                ElMessage.error(t('AI识别失败,请稍后再试!'));
-            }
-        })
-        .catch(() => {
-            ElMessage.error(t('AI识别失败,请稍后再试!'));
-        })
-        .finally(() => {
-            aiAnalyzeLocked.value = false;
-        });
 }
 
 watch(
@@ -212,22 +261,6 @@ defineExpose({
                     color: rgba(18, 18, 18, 0.2);
                 }
             }
-
-            .ai-btn {
-                padding: 5px 10px;
-                border-radius: 4px;
-                background: rgba(33, 153, 248, 0.1);
-                color: #2199F8;
-                border: 1px solid #2199F8;
-            }
-        }
-
-        .upload-result {
-            color: #646464;
-            padding: 6px 10px;
-            background: rgba(161, 161, 161, 0.1);
-            border-radius: 5px;
-            margin-top: 12px;
         }
     }
 
@@ -257,4 +290,84 @@ defineExpose({
         }
     }
 }
+
+.identify-result-popup {
+    width: 100%;
+    max-width: 100%;
+    border-radius: 0;
+    background: none;
+
+    .identify-preview-wrap {
+        position: relative;
+        width: 100%;
+
+        .identify-preview-img {
+            display: block;
+            width: 100%;
+            max-height: 75vh;
+            object-fit: contain;
+            background: #000;
+        }
+
+        .identify-result-panel {
+            position: absolute;
+            left: 12px;
+            right: 12px;
+            bottom: 12px;
+            padding: 10px 12px;
+            background: rgba(61, 61, 61, 0.92);
+            border-radius: 8px;
+            backdrop-filter: blur(4px);
+
+            .result-header {
+                display: flex;
+                align-items: center;
+                gap: 6px;
+                margin-bottom: 8px;
+
+                .accent-bar {
+                    width: 3px;
+                    height: 14px;
+                    border-radius: 2px;
+                    background: #f0d09c;
+                    flex-shrink: 0;
+                }
+
+                .result-name {
+                    color: #f0d09c;
+                    font-size: 14px;
+                    font-weight: 500;
+                    line-height: 20px;
+                }
+            }
+
+            .result-tabs {
+                display: flex;
+                flex-wrap: wrap;
+                gap: 8px 12px;
+                margin-bottom: 8px;
+
+                .result-tab {
+                    color: rgba(255, 255, 255, 0.75);
+                    font-size: 12px;
+                    line-height: 18px;
+                    cursor: pointer;
+
+                    &.active {
+                        color: #fff;
+                        font-weight: 500;
+                    }
+                }
+            }
+
+            .result-content {
+                color: #fff;
+                font-size: 13px;
+                line-height: 20px;
+                white-space: pre-wrap;
+                word-break: break-all;
+            }
+        }
+    }
+}
 </style>

+ 79 - 4
src/components/upload.vue

@@ -11,9 +11,25 @@
     </div>
     <div class="upload-content">
       <img v-if="exampleImg" @click="showExample(null)" class="example" src="@/assets/img/home/example-4.png" alt="" />
-      <uploader class="uploader" :class="{ 'uploader-list': exampleImg }" v-model="fileList"
-        :multiple="props.maxCount > 1" :max-count="props.maxCount" :before-read="onBeforeRead" :after-read="onAfterRead"
-        @delete="deleteImg">
+      <uploader class="uploader" :class="{ 'uploader-list': exampleImg, 'uploader-identify': enableIdentifyStatus }"
+        v-model="fileList" :multiple="props.maxCount > 1" :max-count="props.maxCount" :before-read="onBeforeRead"
+        :after-read="onAfterRead" :preview-full-image="!enableIdentifyStatus" @delete="deleteImg"
+        @click-preview="onClickPreview">
+        <template v-if="enableIdentifyStatus" #preview-cover="previewItem">
+          <div v-if="previewItem?.identifyStatus" class="identify-status-bar">
+            <template v-if="previewItem.identifyStatus === 'identifying'">
+              <span class="identify-status-text">{{ $t('正在识别中..') }}</span>
+            </template>
+            <template v-else-if="previewItem.identifyStatus === 'done'">
+              <el-icon class="identify-done-icon" :size="14"><CircleCheck /></el-icon>
+              <span class="identify-status-text">{{ $t('识别完成') }}</span>
+              <el-icon class="identify-arrow-icon" :size="12"><ArrowRight /></el-icon>
+            </template>
+            <template v-else-if="previewItem.identifyStatus === 'failed'">
+              <span class="identify-status-text">{{ $t('识别失败') }}</span>
+            </template>
+          </div>
+        </template>
         <template v-if="exampleImg">
           <slot v-if="!fileList.length"></slot>
           <img class="plus" v-else src="@/assets/img/home/plus.png" alt="">
@@ -43,6 +59,7 @@ import eventBus from "@/api/eventBus";
 import { base_img_url2 } from "@/api/config";
 import { getFileExt } from "@/utils/util";
 import { ElMessage } from "element-plus";
+import { CircleCheck, ArrowRight } from "@element-plus/icons-vue";
 import UploadFile from "@/utils/upliadFile";
 import 'vant/lib/uploader/style';
 import { useStore } from "vuex";
@@ -82,13 +99,18 @@ const props = defineProps({
     type: Function,
     default: null,
   },
+  /** 上传后展示 AI 识别状态条 */
+  enableIdentifyStatus: {
+    type: Boolean,
+    default: false,
+  },
 })
 
 
 const store = useStore();
 const miniUserId = store.state.home.miniUserId;
 
-const emit = defineEmits(['handleUpload'])
+const emit = defineEmits(['handleUpload', 'clickPreview'])
 
 //上传照片
 const fileList = ref([]);
@@ -117,6 +139,10 @@ const onAfterRead = async (files) => {
   return defaultAfterRead(files);
 };
 
+const onClickPreview = (file, detail) => {
+  emit('clickPreview', file, detail);
+};
+
 const defaultAfterRead = async (files) => {
   if (!Array.isArray(files)) {
     files = [files];
@@ -285,6 +311,24 @@ onMounted(() => {
         --van-padding-xs: 6px;
       }
     }
+
+    &.uploader-identify {
+      ::v-deep {
+        .van-uploader__preview {
+          overflow: hidden;
+          border-radius: 8px;
+        }
+
+        .van-uploader__preview-cover {
+          position: absolute;
+          left: 0;
+          right: 0;
+          bottom: 0;
+          top: auto;
+          height: auto;
+        }
+      }
+    }
   }
 
   .uploader-list {
@@ -311,6 +355,37 @@ onMounted(() => {
     top: 0;
     left: 0;
   }
+
+  .identify-status-bar {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    gap: 4px;
+    width: 100%;
+    padding: 4px 6px;
+    box-sizing: border-box;
+    background: rgba(0, 0, 0, 0.55);
+    color: #fff;
+    font-size: 11px;
+    line-height: 16px;
+
+    .identify-status-text {
+      flex: 1;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .identify-done-icon {
+      color: #52c41a;
+      flex-shrink: 0;
+    }
+
+    .identify-arrow-icon {
+      flex-shrink: 0;
+      margin-left: auto;
+    }
+  }
 }
 
 .example-popup {

+ 186 - 5
src/views/old_mini/recordDetails/map/mapManage.js

@@ -19,8 +19,10 @@ import Select from "ol/interaction/Select";
 import { singleClick } from "ol/events/condition";
 import { reactive } from "vue";
 import WKT from "ol/format/WKT.js";
+import GeoJSON from "ol/format/GeoJSON";
 import * as proj from "ol/proj";
 import { getArea } from "ol/sphere.js";
+import * as turf from "@turf/turf";
 
 const VIEWPORT_INTERACTION_TYPES = [
   DragPan,
@@ -64,6 +66,12 @@ class MapManage {
         });
       },
     });
+    this.boundaryLayer = new KMap.VectorLayer("drawBoundaryLayer", 1050, {
+      style: () => this.createBoundaryStyle(),
+    });
+    this.boundaryGeometry = null;
+    this.constrainedDrawing = false;
+    this.constrainedDrawingReady = false;
     this.gridLayer = new KMap.VectorLayer("terrainGridLayer", 1100, {
       style: (feature) => {
         const selected = !!feature.get("selected");
@@ -97,24 +105,172 @@ class MapManage {
     });
   }
 
+  createBoundaryStyle() {
+    return new Style({
+      fill: new Fill({
+        color: "rgba(124, 124, 124, 0.12)",
+      }),
+      stroke: new Stroke({
+        color: "rgba(255, 255, 255, 0.85)",
+        width: 2,
+        lineDash: [8, 4],
+      }),
+    });
+  }
+
+  createDrawnPestStyle() {
+    return new Style({
+      fill: new Fill({
+        color: "rgba(100, 0, 0, 0.45)",
+      }),
+      stroke: new Stroke({
+        color: "#E03131",
+        width: 1.8,
+      }),
+    });
+  }
+
   initMap(location, target, options = {}) {
-    const { editable = true } = options;
+    const { editable = true, constrainedDrawing = false } = options;
     this.editable = editable;
+    this.constrainedDrawing = constrainedDrawing;
+    this.constrainedDrawingReady = false;
+    this.boundaryGeometry = null;
     let level = 16;
     let coordinate = util.wktCastGeom(location).getFirstCoordinate();
     this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22);
     let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png";
     this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2);
     // this.kmap.addLayer(this.clickPointLayer.layer);
+    this.kmap.addLayer(this.boundaryLayer.layer);
     this.kmap.addLayer(this.gridLayer.layer);
 
     if (this.editable) {
       this.kmap.initDraw(() => {});
       this.kmap.modifyDraw();
       this.setRegionDrawingActive(false);
+    } else if (this.constrainedDrawing) {
+      this.setupConstrainedDrawing();
+      this.setConstrainedDrawingActive(false);
     }
   }
 
+  setupConstrainedDrawing() {
+    if (!this.kmap || this.constrainedDrawingReady) return;
+    this.constrainedDrawingReady = true;
+    this.kmap.initDraw((e) => {
+      if (e.feature) {
+        this.clipFeatureToBoundary(e.feature);
+      }
+    });
+    this.kmap.modifyDraw((e) => {
+      e.features.forEach((feature) => this.clipFeatureToBoundary(feature));
+    });
+  }
+
+  setConstrainedDrawingActive(active) {
+    if (!this.kmap) return;
+    if (this.kmap.draw) {
+      this.kmap.draw.setActive(active);
+    }
+    if (this.kmap.modify) {
+      this.kmap.modify.setActive(active);
+    }
+  }
+
+  enableConstrainedDrawing() {
+    if (!this.boundaryGeometry) return;
+    this.setupConstrainedDrawing();
+    this.setConstrainedDrawingActive(true);
+  }
+
+  intersectWithBoundary(geometry) {
+    if (!this.boundaryGeometry || !geometry || !this.kmap) return null;
+    const geoJson = new GeoJSON();
+    const projection = this.kmap.map.getView().getProjection();
+    const opts = {
+      dataProjection: "EPSG:4326",
+      featureProjection: projection,
+    };
+    try {
+      const drawGeo = geoJson.writeGeometryObject(geometry, opts);
+      const boundaryGeo = geoJson.writeGeometryObject(this.boundaryGeometry, opts);
+      const drawnFeature = turf.feature(drawGeo);
+      const boundaryFeature = turf.feature(boundaryGeo);
+      let result = null;
+      try {
+        result = turf.intersect(turf.featureCollection([drawnFeature, boundaryFeature]));
+      } catch {
+        result = turf.intersect(drawnFeature, boundaryFeature);
+      }
+      if (!result?.geometry) return null;
+      return geoJson.readGeometry(result.geometry, opts);
+    } catch {
+      return null;
+    }
+  }
+
+  clipFeatureToBoundary(feature) {
+    if (!feature || !this.boundaryGeometry || !this.kmap?.polygonLayer?.source) return;
+    const geometry = feature.getGeometry();
+    if (!geometry) return;
+    const clipped = this.intersectWithBoundary(geometry);
+    if (!clipped) {
+      this.kmap.polygonLayer.source.removeFeature(feature);
+      return;
+    }
+    feature.setGeometry(clipped);
+    feature.setStyle(this.createDrawnPestStyle());
+  }
+
+  setBoundaryWkt(wkt) {
+    if (!this.kmap || !this.boundaryLayer?.source || !wkt) return;
+    this.boundaryLayer.source.clear();
+    const mapProjection = this.kmap.map.getView().getProjection();
+    const geometry = this.wktFormat.readGeometry(String(wkt).trim(), {
+      dataProjection: "EPSG:4326",
+      featureProjection: mapProjection,
+    });
+    this.boundaryGeometry = geometry.clone();
+    const feature = new Feature({ geometry });
+    feature.set("isBoundary", true);
+    this.boundaryLayer.addFeature(feature);
+    this.fitBoundaryView();
+  }
+
+  clearBoundaryLayer() {
+    this.boundaryLayer?.source?.clear();
+    this.boundaryGeometry = null;
+  }
+
+  fitBoundaryView() {
+    if (!this.kmap || !this.boundaryLayer?.source) return;
+    const extent = this.boundaryLayer.source.getExtent();
+    if (!extent || extent.some((v) => !Number.isFinite(v))) return;
+    this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] });
+  }
+
+  setDrawnAreaGeometry(geometryArr) {
+    if (!this.kmap?.polygonLayer?.source || !Array.isArray(geometryArr)) return;
+    this.kmap.polygonLayer.source.clear();
+    const mapProjection = this.kmap.map.getView().getProjection();
+    geometryArr.forEach((item) => {
+      try {
+        const geometry = this.wktFormat.readGeometry(String(item).trim(), {
+          dataProjection: "EPSG:4326",
+          featureProjection: mapProjection,
+        });
+        const feature = new Feature({ geometry });
+        this.clipFeatureToBoundary(feature);
+        if (feature.getGeometry()) {
+          this.kmap.polygonLayer.source.addFeature(feature);
+        }
+      } catch {
+        /* 单条解析失败则跳过 */
+      }
+    });
+  }
+
   /**
    * 是否允许平移/缩放、勾画与编辑;为 false 时同时隐藏中心点位图标
    */
@@ -176,8 +332,9 @@ class MapManage {
     if (!this.kmap) return null;
     const center = this.kmap.getView().getCenter();
     const wkt = this.generateSquareWktByMu(center, mu);
-    this.setAreaGeometry([wkt]);
+    this.setBoundaryWkt(wkt);
     this.setMapPoint(center);
+    this.enableConstrainedDrawing();
     return wkt;
   }
 
@@ -190,7 +347,8 @@ class MapManage {
     });
     this.setMapPoint(center);
     const wkt = this.generateSquareWktByMu(center, mu);
-    this.setAreaGeometry([wkt]);
+    this.setBoundaryWkt(wkt);
+    this.enableConstrainedDrawing();
     return wkt;
   }
 
@@ -220,6 +378,11 @@ class MapManage {
     this.clearGridLayer();
   }
 
+  clearAllLayers() {
+    this.clearLayer();
+    this.clearBoundaryLayer();
+  }
+
   clearGridLayer() {
     this.unbindGridClick();
     this.gridLayer?.source?.clear();
@@ -435,6 +598,14 @@ class MapManage {
     this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] });
   }
 
+  getBoundaryWkt() {
+    if (!this.boundaryGeometry || !this.kmap) return "";
+    return this.wktFormat.writeGeometry(this.boundaryGeometry, {
+      dataProjection: "EPSG:4326",
+      featureProjection: this.kmap.map.getView().getProjection(),
+    });
+  }
+
   getDisplayAreaWkt() {
     if (!this.kmap?.polygonLayer?.source) return "";
     const projection = this.kmap.map.getView().getProjection();
@@ -492,12 +663,15 @@ class MapManage {
 
   destroyMap() {
     this.unbindGridClick();
-    this.clearLayer();
+    this.clearAllLayers();
     if (this.kmap && typeof this.kmap.destroy === "function") {
       this.kmap.destroy();
     }
     this.kmap = null;
     this.regionDrawingActive = false;
+    this.constrainedDrawing = false;
+    this.constrainedDrawingReady = false;
+    this.boundaryGeometry = null;
     this.selectedGridIds?.clear();
   }
 
@@ -505,6 +679,9 @@ class MapManage {
    * 地图上全部已勾画地块:WKT 列表、每块亩数、合计亩数(亩换算与互动勾画页一致)
    */
   getAreaGeometry() {
+    if (!this.kmap) {
+      return { geometryArr: [], mianji: "0.00", parcels: [] };
+    }
     const features = this.kmap.getLayerFeatures();
     const format = new WKT();
     const projection = this.kmap.map.getView().getProjection();
@@ -550,7 +727,11 @@ class MapManage {
       }
       this.kmap.polygonLayer.source.addFeature(feature);
     });
-    this.fitView();
+    if (this.boundaryGeometry) {
+      this.fitBoundaryView();
+    } else {
+      this.fitView();
+    }
   }
 
   fitView(){

+ 29 - 64
src/views/old_mini/recordDetails/mapManage.vue

@@ -163,40 +163,22 @@ const confirmPayload = ref({
 
 function buildMapConfirmPayload(overrides = {}) {
     const selectedGrids = mapManage.getSelectedGrids();
+    const drawnPayload = mapManage.getAreaGeometry();
     const hasGridLayer = Boolean(mapManage.kmap && mapManage.gridLayer?.source);
+    const hasDrawnAreas = drawnPayload.geometryArr.length > 0;
     return {
         zone_name: confirmPayload.value.zone_name,
-        coordinates: selectedGrids.gridIds.length
-            ? mapManage.mergeGeometryWkts(selectedGrids.geometryArr)
-            : confirmPayload.value.coordinates,
-        gridMultipolygon: mapManage.getDisplayAreaWkt() || confirmPayload.value.gridMultipolygon,
+        coordinates: hasDrawnAreas
+            ? mapManage.mergeGeometryWkts(drawnPayload.geometryArr)
+            : (selectedGrids.gridIds.length
+                ? mapManage.mergeGeometryWkts(selectedGrids.geometryArr)
+                : confirmPayload.value.coordinates),
+        gridMultipolygon: mapManage.getBoundaryWkt() || confirmPayload.value.gridMultipolygon,
         selectedGridIds: hasGridLayer ? selectedGrids.gridIds : confirmPayload.value.selectedGridIds,
         ...overrides,
     };
 }
 
-function loadPestTerrainGrids(gridMultipolygon, selectedGridIds = []) {
-    const wkt = String(gridMultipolygon || '').trim();
-    if (!wkt) return;
-
-    const baseWktArr = normalizeMapCoordinates(wkt);
-    if (baseWktArr.length) {
-        mapManage.setAreaGeometry(baseWktArr);
-    }
-
-    VE_API.record.generateGrid({ multipolygon: wkt }).then((res) => {
-        if (res.code === 200 && Array.isArray(res.data) && res.data.length) {
-            mapManage.setTerrainGrids(res.data);
-            if (selectedGridIds.length) {
-                mapManage.restoreSelectedGrids(selectedGridIds);
-            }
-            mapManage.fitGridView();
-        } else if (res.code === 200) {
-            ElMessage.warning("未生成网格数据");
-        }
-    });
-}
-
 const handleRegionNameConfirm = (regionName) => {
     confirmPayload.value.zone_name = regionName;
     store.commit('recordDetails/SET_MAP_CONFIRM_PAYLOAD', buildMapConfirmPayload({
@@ -210,19 +192,6 @@ const onStartRegionDrawing = () => {
     mapManage.enableRegionDrawing();
 };
 
-const handleLocationChange = (location) => {
-    if (recordType.value === 'pest') {
-        const wkt = mapManage.setCenterAndSquare(location.coordinateArray, 60);
-        if (wkt) {
-            confirmPayload.value.coordinates = [wkt];
-            confirmPayload.value.gridMultipolygon = wkt;
-            confirmPayload.value.selectedGridIds = [];
-            loadPestTerrainGrids(wkt);
-        }
-        return;
-    }
-    mapManage.setMapPosition(location.coordinateArray);
-};
 
 const handleMapIconClick = () => {
     mapManage.setMapPosition(getFarmMapCoordinate());
@@ -242,25 +211,14 @@ const handleClearDraw = () => {
 
 const handleConfirmDraw = () => {
     if (recordType.value === 'pest') {
-        const selectedGrids = mapManage.getSelectedGrids();
-        if (!selectedGrids.gridIds.length) {
-            ElMessage.warning("至少要选中一个区域");
+        const payload = mapManage.getAreaGeometry();
+        if (!payload.geometryArr.length) {
+            ElMessage.warning("请先勾画地块");
             return;
         }
-        const mergedGeometry = mapManage.mergeGeometryWkts(selectedGrids.geometryArr);
-        confirmPayload.value.coordinates = mergedGeometry;
+        confirmPayload.value.coordinates = mapManage.mergeGeometryWkts(payload.geometryArr);
+        confirmPayload.value.gridMultipolygon = mapManage.getBoundaryWkt() || confirmPayload.value.gridMultipolygon;
         showRegionNamePopup.value = true;
-        // const payload = mapManage.getAreaGeometry();
-        // if (!payload.geometryArr.length) {
-        //     ElMessage.warning("请先勾画地块");
-        //     return;
-        // }
-        // // console.log("地块数据", payload);
-        // // console.log("WKT 列表", payload.geometryArr);
-        // // console.log("合计亩数", payload.mianji);
-        // // console.log("分块明细", payload.parcels);
-        // confirmPayload.value.coordinates = payload.geometryArr;
-        // showRegionNamePopup.value = true;
     } else {
         uploadProgressPopupRef.value?.uploadReset?.();
         showUploadProgressPopup.value = true;
@@ -272,6 +230,7 @@ function initMapManageView() {
     mapManage.destroyMap();
     mapManage.initMap(getFarmMapLocation(), mapContainer.value, {
         editable: recordType.value !== 'pest',
+        constrainedDrawing: recordType.value === 'pest',
     });
 
     const stored = store.state.recordDetails.mapConfirmPayload;
@@ -301,22 +260,28 @@ function initMapManageView() {
     if (recordType.value === 'pest') {
         mapManage.enableMapInteraction();
         if (confirmPayload.value.gridMultipolygon) {
-            loadPestTerrainGrids(
-                confirmPayload.value.gridMultipolygon,
-                confirmPayload.value.selectedGridIds,
-            );
+            mapManage.setBoundaryWkt(confirmPayload.value.gridMultipolygon);
+            mapManage.enableConstrainedDrawing();
+            const coords = normalizeMapCoordinates(confirmPayload.value.coordinates);
+            if (coords.length) {
+                mapManage.setDrawnAreaGeometry(coords);
+            }
         } else if (!confirmPayload.value.coordinates) {
             const wkt = mapManage.setDefaultSquareAtCenter(60);
             if (wkt) {
-                confirmPayload.value.coordinates = [wkt];
                 confirmPayload.value.gridMultipolygon = wkt;
-                loadPestTerrainGrids(wkt);
             }
         } else {
             const coords = normalizeMapCoordinates(confirmPayload.value.coordinates);
-            if (coords.length === 1) {
-                confirmPayload.value.gridMultipolygon = coords[0];
-                loadPestTerrainGrids(coords[0], confirmPayload.value.selectedGridIds);
+            if (coords.length === 1 && !confirmPayload.value.gridMultipolygon) {
+                mapManage.setBoundaryWkt(coords[0]);
+                mapManage.enableConstrainedDrawing();
+            } else if (coords.length) {
+                const wkt = mapManage.setDefaultSquareAtCenter(60);
+                if (wkt) {
+                    confirmPayload.value.gridMultipolygon = wkt;
+                    mapManage.setDrawnAreaGeometry(coords);
+                }
             }
         }
     }