Ver código fonte

faet:对接病虫识别功能接口

wangsisi 5 dias atrás
pai
commit
9c0e233259

+ 10 - 0
src/api/modules/record.js

@@ -21,4 +21,14 @@ module.exports = {
         url: config.base_new_url + "write_farm_record",
         type: "post",
     },
+    //识别病虫害
+    batchPestIdentify: {
+        url: config.base_new_url + "batch_pest_identify",
+        type: "post",
+    },
+    //分区格网生成
+    generateGrid: {
+        url: config.base_new_url + "generate_terrain_grids",
+        type: "get",
+    },
 }

BIN
src/assets/img/common/sd-1.jpg


BIN
src/assets/img/common/sd-1.png


BIN
src/assets/img/common/sd-2.jpg


BIN
src/assets/img/common/sd-2.png


BIN
src/assets/img/common/sd-3.jpg


BIN
src/assets/img/common/sd-3.png


BIN
src/assets/img/common/sd-4.jpg


BIN
src/assets/img/common/sd-4.png


BIN
src/assets/img/common/sd-5.jpg


BIN
src/assets/img/common/sd-5.png


BIN
src/assets/img/common/sd-6.jpg


BIN
src/assets/img/common/sd-6.png


+ 53 - 10
src/components/popup/UploadProgressPopup.vue

@@ -8,12 +8,12 @@
                     <span>{{ t('上传照片') }}</span>
                     <span v-if="!uploadRequired" class="optional">{{ t('(可选)') }}</span>
                 </div>
-                <div class="ai-btn">{{ t('AI 智能分析') }}</div>
+                <div class="ai-btn" :style="{ opacity: aiBtnDisabled ? 0.5 : 1 }" @click="handleAIAnalysis">{{ aiBtnText }}</div>
             </div>
-            <upload ref="uploadRef" :maxCount="10" :initImgArr="displayImgArr" :before-read="beforeReadUpload"
-                :after-read="afterReadUpload">
+            <upload ref="uploadRef" :maxCount="10" :initImgArr="props.initImgArr" :before-read="beforeReadUpload"
+                :after-read="afterReadUpload" @handleUpload="onUploadChange">
             </upload>
-            <!-- <div class="upload-result">{{ t('AI识别结果:该病为该病为该病为该病为病为该病为病为该病为') }}</div> -->
+            <div class="upload-result" v-if="aiStr.length">{{ t('AI识别结果:') }}{{ aiStr }}</div>
         </div>
         <slot name="footer"></slot>
         <div class="upload-action-btns">
@@ -62,24 +62,37 @@ const emit = defineEmits(['update:show', 'cancel', 'confirm', 'reset']);
 
 const uploadRef = ref(null);
 const popupInnerImgArr = ref([]);
+/** 用户已上传/删除过图片后,以 popupInnerImgArr 为准,不再回退 props.initImgArr */
+const imgTouched = ref(false);
 const popupInnerLoading = ref(false);
 const uploadFileObj = new UploadFile();
 const miniUserId = localStorage.getItem("MINI_USER_ID");
 
 const loading = computed(() => props.popupImageUploadLoading || popupInnerLoading.value);
 
-/** 弹窗内上传优先展示,否则回显父组件传入的 initImgArr */
-const displayImgArr = computed(() =>
-    popupInnerImgArr.value.length ? popupInnerImgArr.value : props.initImgArr,
+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 智能分析'),
 );
 
 function getConfirmImgArr() {
-    if (popupInnerImgArr.value.length) {
-        return [...popupInnerImgArr.value.map(item => base_img_url2 + item)];
+    if (imgTouched.value || popupInnerImgArr.value.length) {
+        return popupInnerImgArr.value.map((item) => base_img_url2 + item);
     }
     return [...(props.initImgArr || [])];
 }
 
+/** 与 upload 组件内 imgArr 同步(含删除),用于 AI 按钮透明度等 */
+function onUploadChange({ imgArr }) {
+    imgTouched.value = true;
+    popupInnerImgArr.value = [...(imgArr || [])];
+}
+
 const beforeReadUpload = () => {
     popupInnerLoading.value = false;
     return true;
@@ -101,6 +114,7 @@ const afterReadUpload = async (data) => {
             if (resFilename) {
                 file.status = "done";
                 file.message = "";
+                imgTouched.value = true;
                 popupInnerImgArr.value.push(resFilename);
             } else {
                 file.status = "failed";
@@ -110,6 +124,7 @@ const afterReadUpload = async (data) => {
         }
     } finally {
         popupInnerLoading.value = false;
+        uploadRef.value?.syncImgArr?.(popupInnerImgArr.value);
     }
 };
 
@@ -124,8 +139,37 @@ function handleConfirm() {
 
 function uploadReset() {
     popupInnerImgArr.value = [];
+    imgTouched.value = false;
     popupInnerLoading.value = false;
     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(
@@ -175,7 +219,6 @@ defineExpose({
                 background: rgba(33, 153, 248, 0.1);
                 color: #2199F8;
                 border: 1px solid #2199F8;
-                opacity: 0.5;
             }
         }
 

+ 8 - 1
src/components/upload.vue

@@ -197,9 +197,16 @@ function uploadReset() {
   imgArr.value = []
 }
 
+function syncImgArr(arr) {
+  imgArr.value = [...(arr || [])]
+  fileArr.value = imgArr.value.map((item) =>
+    props.fullPath ? base_img_url2 + item : item,
+  )
+}
 
 defineExpose({
-  uploadReset
+  uploadReset,
+  syncImgArr,
 })
 
 onMounted(() => {

+ 15 - 0
src/utils/boldKeywords.js

@@ -0,0 +1,15 @@
+/**
+ * 将文本中指定关键词包裹为加粗样式(配合 v-html 与 .text-bold 使用)
+ * @param {string} text
+ * @param {string[]} [keywords]
+ * @returns {string}
+ */
+export function boldKeywordsInText(text, keywords = []) {
+    if (!text) return "";
+    if (!keywords?.length) return text;
+    return keywords.reduce(
+        (result, keyword) =>
+            keyword ? result.split(keyword).join(`<span class="text-bold">${keyword}</span>`) : result,
+        text,
+    );
+}

+ 48 - 38
src/views/old_mini/agri_record/index.vue

@@ -12,42 +12,44 @@
             <garden-list ref="gardenListRef" :garden-id="selectedGardenId" @loaded="handleGardenLoaded"
                 @selectGarden="handleGardenSelected" />
         </div>
-        <!-- 作物档案:外层可滚动,滚动位置单独缓存以便路由/Tab 返回后恢复 -->
-        <div ref="archivesScrollAreaRef" class="archives-time-line" v-show="activeGardenTab === 'current'">
-            <div class="trend-monitor-list">
-                <div class="trend-monitor-card" @click="handleTrendMonitorCardClick(item)"
-                    v-for="(item, index) in trendMonitorMockList" :key="index">
-                    <div class="card-header">
-                        <div class="header-left">
-                            <span class="title">{{ item?.first_work?.work_name }}</span>
-                            <span class="level-tag" v-if="item?.risk_level">{{ t('agriRecord.riskLevel', { level: item.risk_level }) }}</span>
+        <!-- 作物档案:趋势监测与 Tab 固定顶部,仅农事列表区域滚动 -->
+        <div class="archives-time-line" v-show="activeGardenTab === 'current'">
+            <div class="archives-time-line-fixed">
+                <div class="trend-monitor-list">
+                    <div class="trend-monitor-card" @click="handleTrendMonitorCardClick(item)"
+                        v-for="(item, index) in trendMonitorMockList" :key="index">
+                        <div class="card-header">
+                            <div class="header-left">
+                                <span class="title">{{ item?.first_work?.work_name }}</span>
+                                <span class="level-tag" v-if="item?.risk_level">{{ t('agriRecord.riskLevel', { level: item.risk_level }) }}</span>
+                            </div>
+                            <div class="status-tag" v-if="item?.status === 0">{{ t('agriRecord.pendingRecord') }}</div>
                         </div>
-                        <div class="status-tag" v-if="item?.status === 0">{{ t('agriRecord.pendingRecord') }}</div>
-                    </div>
-                    <div class="card-row">
-                        <div class="reason-text">{{ item?.first_work?.work_reason_short }}</div>
-                        <div class="question-tag">{{ item?.first_work?.interaction_issue }}</div>
-                    </div>
-                    <div class="record-list">
-                        <div class="record-item" v-for="(record, recordIndex) in item.list?.slice(0, 3)" :key="recordIndex">
-                            <span v-if="record?.category_name">({{ record.time }}){{ record.category_name }}</span>
-                            <span v-else>{{ record }}</span>
+                        <div class="card-row">
+                            <div class="reason-text">{{ item?.first_work?.work_reason_short }}</div>
+                            <div class="question-tag">{{ item?.first_work?.interaction_issue }}</div>
+                        </div>
+                        <div class="record-list">
+                            <div class="record-item" v-for="(record, recordIndex) in item.list?.slice(0, 3)" :key="recordIndex">
+                                <span v-if="record?.category_name">({{ record.time }}){{ record.category_name }}</span>
+                                <span v-else>{{ record }}</span>
+                            </div>
                         </div>
                     </div>
                 </div>
-            </div>
-            <div class="farm-work-tabs">
-                <div
-                    v-for="tab in farmWorkTabs"
-                    :key="tab.key"
-                    class="farm-work-tabs__item"
-                    :class="{ 'farm-work-tabs__item--active': activeFarmWorkTab === tab.key }"
-                    @click="handleFarmWorkTabClick(tab.key)"
-                >
-                    {{ t(tab.labelKey) }}
+                <div class="farm-work-tabs">
+                    <div
+                        v-for="tab in farmWorkTabs"
+                        :key="tab.key"
+                        class="farm-work-tabs__item"
+                        :class="{ 'farm-work-tabs__item--active': activeFarmWorkTab === tab.key }"
+                        @click="handleFarmWorkTabClick(tab.key)"
+                    >
+                        {{ t(tab.labelKey) }}
+                    </div>
                 </div>
             </div>
-            <div class="archives-time-line-content">
+            <div ref="archivesScrollAreaRef" class="archives-time-line-content">
                 <archives-farm-time-line :farmId="gardenId" :activeFarmWorkTab="activeFarmWorkTab"></archives-farm-time-line>
             </div>
         </div>
@@ -347,15 +349,18 @@ const handleTrendMonitorCardClick = (item) => {
 
     .archives-time-line {
         position: relative;
-        height: calc(100% - 140px);
+        height: 100%;
         padding: 12px;
+        padding-top: 120px;
         display: flex;
         flex-direction: column;
         min-height: 0;
-        // padding-top: 150px;
-        padding-top: 120px;
-        overflow-y: auto;
-        -webkit-overflow-scrolling: touch;
+        overflow: hidden;
+        box-sizing: border-box;
+
+        .archives-time-line-fixed {
+            flex-shrink: 0;
+        }
 
         .farm-work-tabs {
             display: flex;
@@ -379,14 +384,19 @@ const handleTrendMonitorCardClick = (item) => {
 
         .archives-time-line-content {
             margin-top: 10px;
-            flex: none;
+            flex: 1;
             min-height: 0;
+            overflow-y: auto;
+            -webkit-overflow-scrolling: touch;
             background: #fff;
             border-radius: 8px;
             padding: 10px;
             box-sizing: border-box;
-            display: flex;
-            flex-direction: column;
+
+            :deep(.timeline-container) {
+                height: auto;
+                overflow: visible;
+            }
         }
     }
 }

+ 82 - 6
src/views/old_mini/growth_report/historyRiskReport.vue

@@ -57,9 +57,16 @@
                 <div class="report-box">
                     <div class="box-title">土壤改良分析</div>
                     <div class="box-text">
-                        <div class="part-text" v-if="currentFarmVariety == 1">当前农场土壤为壤土,酸性至微酸性(pH
-                            6.5),有机质含量(6.25%),肥力良好。在后续的种植管理中,需保持土壤肥力,合理施肥,必要时需加入生石灰中和酸性,避免土壤酸化加剧。</div>
-                        <div class="part-text" v-else>当前农场土壤为壤土,酸性至微酸性(pH 5.9),有机质含量(5.52%),肥力良好,但酸性偏低。在后续的种植管理中,需保持土壤肥力,合理施肥,需加入生石灰中和酸性,降低土壤酸化问题。</div>
+                        <div
+                            class="part-text"
+                            v-if="currentFarmVariety == 1"
+                            v-html="boldKeywordsInText(SOIL_ANALYSIS_LYCHEE, SOIL_ANALYSIS_LYCHEE_BOLD_KEYWORDS)"
+                        ></div>
+                        <div
+                            class="part-text"
+                            v-else
+                            v-html="boldKeywordsInText(SOIL_ANALYSIS_RICE, SOIL_ANALYSIS_RICE_BOLD_KEYWORDS)"
+                        ></div>
                     </div>
                 </div>
 
@@ -67,15 +74,27 @@
                     <div class="box-title">耕作习惯分析</div>
                     <div class="box-text">
                         <div class="part-text" v-if="currentFarmVariety == 1">当前农场耕作管理制度完善,未出现频繁翻耕等问题。在后续的种植管理中,需继续维持合理的农事规划和农事操作,保持土壤活力。</div>
-                        <div class="part-text" v-else>当前农场耕作管理制度完善,未出现频繁翻耕等问题。在后续的种植管理中,需继续维持合理的农事规划和农事操作,保持土壤活力。但由于农场长期种植单一作物,导致土壤养分失衡,因此建议在施肥时多补充有机质和微量元素。同时,建议在水稻休耕时期轮种紫云英等绿肥作物,改善土壤问题。</div>
+                        <div
+                            class="part-text"
+                            v-else
+                            v-html="boldKeywordsInText(FARMING_HABIT_RICE, FARMING_HABIT_RICE_BOLD_KEYWORDS)"
+                        ></div>
                     </div>
                 </div>
 
                 <div class="report-box">
                     <div class="box-title">设施投入建议</div>
                     <div class="box-text">
-                        <div class="part-text" v-if="currentFarmVariety == 1">考虑到当前农场历史高发风险为高温干旱风险,高发概率达到80%,建议提前规划好喷灌/滴灌系统,以维持农场的日常灌溉,降低高温干旱风险的影响。</div>
-                        <div class="part-text" v-else>考虑到当前农场历史高发风险为低温冻害和暴雨涝渍风险,历史出现概率均超过90%,建议提前规划好排水沟渠以及灌溉系统,以便灵活排水灌水,合理控制田间温度和水层深度。</div>
+                        <div
+                            class="part-text"
+                            v-if="currentFarmVariety == 1"
+                            v-html="boldKeywordsInText(FACILITY_ADVICE_LYCHEE, FACILITY_ADVICE_LYCHEE_BOLD_KEYWORDS)"
+                        ></div>
+                        <div
+                            class="part-text"
+                            v-else
+                            v-html="boldKeywordsInText(FACILITY_ADVICE_RICE, FACILITY_ADVICE_RICE_BOLD_KEYWORDS)"
+                        ></div>
                     </div>
                 </div>
             </div>
@@ -87,6 +106,54 @@
 import { ref, computed } from "vue";
 import { useRouter ,useRoute} from "vue-router";
 import customHeader from "@/components/customHeader.vue";
+import { boldKeywordsInText } from "@/utils/boldKeywords";
+
+const SOIL_ANALYSIS_LYCHEE =
+    "当前农场土壤为壤土,酸性至微酸性(pH 6.5),有机质含量(6.25%),肥力良好。在后续的种植管理中,需保持土壤肥力,合理施肥,必要时需加入生石灰中和酸性,避免土壤酸化加剧。";
+
+const SOIL_ANALYSIS_LYCHEE_BOLD_KEYWORDS = [
+    "壤土",
+    "酸性至微酸性(pH 6.5)",
+    "有机质含量(6.25%)",
+    "必要时需加入生石灰中和酸性",
+];
+
+const SOIL_ANALYSIS_RICE =
+    "当前农场土壤为壤土,酸性至微酸性(pH 5.9),有机质含量(5.52%),肥力良好,但酸性偏低。在后续的种植管理中,需保持土壤肥力,合理施肥,需加入生石灰中和酸性,降低土壤酸化问题。";
+
+const SOIL_ANALYSIS_RICE_BOLD_KEYWORDS = [
+    "壤土",
+    "酸性至微酸性(pH 5.9)",
+    "有机质含量(5.52%)",
+    "需加入生石灰中和酸性",
+];
+
+const FARMING_HABIT_RICE =
+    "当前农场耕作管理制度完善,未出现频繁翻耕等问题。在后续的种植管理中,需继续维持合理的农事规划和农事操作,保持土壤活力。但由于农场长期种植单一作物,导致土壤养分失衡,因此建议在施肥时多补充有机质和微量元素。同时,建议在水稻休耕时期轮种紫云英等绿肥作物,改善土壤问题。";
+
+const FARMING_HABIT_RICE_BOLD_KEYWORDS = [
+    "长期种植单一作物",
+    "多补充有机质和微量元素",
+    "轮种紫云英等绿肥作物",
+];
+
+const FACILITY_ADVICE_LYCHEE =
+    "考虑到当前农场历史高发风险为高温干旱风险,高发概率达到80%,建议提前规划好喷灌/滴灌系统,以维持农场的日常灌溉,降低高温干旱风险的影响。";
+
+const FACILITY_ADVICE_LYCHEE_BOLD_KEYWORDS = [
+    "高发风险为高温干旱风险",
+    "80%",
+    "规划好喷灌/滴灌系统",
+];
+
+const FACILITY_ADVICE_RICE =
+    "考虑到当前农场历史高发风险为低温冻害和暴雨涝渍风险,历史出现概率均超过90%,建议提前规划好排水沟渠以及灌溉系统,以便灵活排水灌水,合理控制田间温度和水层深度。";
+
+const FACILITY_ADVICE_RICE_BOLD_KEYWORDS = [
+    "高发风险为低温冻害和暴雨涝渍风险",
+    "90%",
+    "规划好排水沟渠以及灌溉系统",
+];
 
 const router = useRouter();
 const loading = ref(false);
@@ -280,6 +347,12 @@ const handleGoBack = () => {
                     margin-bottom: 8px;
                 }
 
+                .part-text {
+                    :deep(.text-bold) {
+                        font-weight: 700;
+                    }
+                }
+
                 .types-info {
                     background: rgba(33, 153, 248, 0.1);
                     color: #000000;
@@ -328,6 +401,9 @@ const handleGoBack = () => {
 
                     .part-text {
                         padding-top: 6px;
+                        :deep(.text-bold) {
+                            font-weight: bold;
+                        }
                     }
 
                     .part-top {

+ 21 - 9
src/views/old_mini/growth_report/index.vue

@@ -95,12 +95,12 @@
                                 <img src="@/assets/img/common/tp-6.png" alt="">
                             </div>
                             <div class="tp-img" v-else>
-                                <img src="@/assets/img/common/sd-1.png" alt="">
-                                <img src="@/assets/img/common/sd-2.png" alt="">
-                                <img src="@/assets/img/common/sd-3.png" alt="">
-                                <img src="@/assets/img/common/sd-4.png" alt="">
-                                <img src="@/assets/img/common/sd-5.png" alt="">
-                                <img src="@/assets/img/common/sd-6.png" alt="">
+                                <img src="@/assets/img/common/sd-1.jpg" alt="">
+                                <img src="@/assets/img/common/sd-2.jpg" alt="">
+                                <img src="@/assets/img/common/sd-3.jpg" alt="">
+                                <img src="@/assets/img/common/sd-4.jpg" alt="">
+                                <img src="@/assets/img/common/sd-5.jpg" alt="">
+                                <img src="@/assets/img/common/sd-6.jpg" alt="">
                             </div>
                         </div>
                         <div class="warning-part">
@@ -118,7 +118,7 @@
 
                             <div class="report-part" v-for="(part, partI) in riskList" :key="partI">
                                 <div class="part-title">{{ part.title }}</div>
-                                <div class="part-text">{{ part.description }}</div>
+                                <div class="part-text" v-html="boldKeywordsInText(part.description, part.boldKeywords)"></div>
                             </div>
                         </div>
                     </div>
@@ -136,7 +136,7 @@
                                         <div class="text-link">{{ t('查看农事') }}</div>
                                     </div> -->
                                 </div>
-                                <div class="part-text">{{ part.description }}</div>
+                                <div class="part-text" v-html="boldKeywordsInText(part.description, part.boldKeywords)"></div>
                             </div>
                         </div>
                     </div>
@@ -153,7 +153,7 @@
                                         <div class="text-link">{{ t('查看互动') }}</div>
                                     </div> -->
                                 </div>
-                                <div class="part-text">{{ part.description }}</div>
+                                <div class="part-text" v-html="boldKeywordsInText(part.description, part.boldKeywords)"></div>
                             </div>
                         </div>
                     </div>
@@ -247,6 +247,7 @@ import agriExecutePopup from "@/components/popup/agriExecutePopup.vue";
 import gardenList from "@/components/gardenList.vue";
 import adjustPopup from "./adjustPopup.vue";
 import { useI18n } from "@/i18n";
+import { boldKeywordsInText } from "@/utils/boldKeywords";
 
 const { t, toggleLocale: dispatchToggleLocale } = useI18n();
 const store = useStore();
@@ -270,6 +271,10 @@ const riskList = computed(() => [
             currentFarmVariety.value == 1
                 ? t("growthReport.risk.pest.desc")
                 : t("growthReport.risk.pest.descRice"),
+        boldKeywords:
+            currentFarmVariety.value == 1
+                ? ["高温干旱", "中等水平", "果皮灼伤"]
+                : ["低等水平", "对水分会更加敏感"],
     },
     // {
     //     title: t("growthReport.risk.rain.title"),
@@ -287,6 +292,8 @@ const adviceList = computed(() => [
             currentFarmVariety.value == 1
                 ? t("growthReport.advice.foliar.desc")
                 : t("growthReport.advice.foliar.descRice"),
+        boldKeywords:
+            currentFarmVariety.value == 1 ? ["需及时喷撒清水"] : ["建议喷灌清水"],
     },
     // {
     //     title: t("growthReport.advice.pestControl.title"),
@@ -302,12 +309,14 @@ const patrolList = computed(() => {
             description: isLychee
                 ? t("growthReport.patrol.process.desc")
                 : t("growthReport.patrol.process.descRice"),
+            boldKeywords: isLychee ? ["5%", "果实转色期"] : ["60%", "分蘖末期"],
         },
         {
             title: t("growthReport.patrol.growth.title"),
             description: isLychee
                 ? t("growthReport.patrol.growth.desc")
                 : t("growthReport.patrol.growth.descRice"),
+            boldKeywords: isLychee ? ["10%", "抽生新梢"] : ["10%", "干旱缺素"],
         },
     ];
     // {
@@ -1182,6 +1191,9 @@ onUnmounted(() => {
                     }
                     .part-text {
                         padding-top: 6px;
+                        :deep(.text-bold) {
+                            font-weight: bold;
+                        }
                     }
                     .part-top {
                         display: flex;

+ 76 - 1
src/views/old_mini/recordDetails/map/mapManage.js

@@ -3,7 +3,9 @@ import * as util from "@/common/ol_common.js";
 import config from "@/api/config.js";
 import Style from "ol/style/Style";
 import Icon from "ol/style/Icon";
-import { Point } from "ol/geom";
+import Fill from "ol/style/Fill";
+import Stroke from "ol/style/Stroke";
+import { Point, Polygon } from "ol/geom";
 import Feature from "ol/Feature";
 import DragPan from "ol/interaction/DragPan";
 import MouseWheelZoom from "ol/interaction/MouseWheelZoom";
@@ -60,6 +62,20 @@ class MapManage {
         });
       },
     });
+    this.gridLayer = new KMap.VectorLayer("terrainGridLayer", 900, {
+      style: () => {
+        return new Style({
+          // fill: new Fill({
+          //   color: "rgba(0, 0, 0, 0.25)",
+          // }),
+          stroke: new Stroke({
+            color: "rgba(0, 0, 0, 0.57)",
+            width: 1.2,
+          }),
+        });
+      },
+    });
+    this.wktFormat = new WKT();
   }
 
   initMap(location, target) {
@@ -69,6 +85,7 @@ class MapManage {
     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.gridLayer.layer);
 
     this.kmap.initDraw(() => {});
     this.kmap.modifyDraw();
@@ -124,6 +141,64 @@ class MapManage {
       this.kmap.draw.abortDrawing();
     }
     this.kmap.polygonLayer.source.clear();
+    this.clearGridLayer();
+  }
+
+  clearGridLayer() {
+    this.gridLayer?.source?.clear();
+  }
+
+  /**
+   * MULTIPOINT WKT 顶点转 Polygon(接口网格为多点围成的面)
+   */
+  multipointWktToPolygon(multipointWkt) {
+    if (!this.kmap || !multipointWkt) return null;
+    const projection = this.kmap.map.getView().getProjection();
+    let geom;
+    try {
+      geom = this.wktFormat.readGeometry(String(multipointWkt).trim(), {
+        dataProjection: "EPSG:4326",
+        featureProjection: projection,
+      });
+    } catch {
+      return null;
+    }
+    if (geom.getType() !== "MultiPoint") return null;
+    const coords = geom.getCoordinates();
+    if (!coords || coords.length < 3) return null;
+    const ring = coords.map((c) => [...c]);
+    const first = ring[0];
+    const last = ring[ring.length - 1];
+    if (first[0] !== last[0] || first[1] !== last[1]) {
+      ring.push([...first]);
+    }
+    return new Polygon([ring]);
+  }
+
+  /**
+   * 渲染地形网格(generateGrid 接口返回)
+   * @param {{ id: number, geometry: string, area_m2?: number }[]} gridItems
+   */
+  setTerrainGrids(gridItems) {
+    if (!this.kmap || !this.gridLayer?.source) return;
+    this.clearGridLayer();
+    if (!Array.isArray(gridItems) || !gridItems.length) return;
+
+    gridItems.forEach((item) => {
+      const polygon = this.multipointWktToPolygon(item?.geometry);
+      if (!polygon) return;
+      const feature = new Feature({ geometry: polygon });
+      feature.set("gridId", item.id);
+      feature.set("area_m2", item.area_m2);
+      this.gridLayer.addFeature(feature);
+    });
+  }
+
+  fitGridView() {
+    if (!this.kmap || !this.gridLayer?.source) return;
+    const extent = this.gridLayer.source.getExtent();
+    if (!extent || extent.some((v) => !Number.isFinite(v))) return;
+    this.kmap.getView().fit(extent, { duration: 500, padding: [80, 80, 80, 80] });
   }
 
   destroyMap() {

+ 10 - 0
src/views/old_mini/recordDetails/mapManage.vue

@@ -197,6 +197,16 @@ const handleConfirmDraw = () => {
             ElMessage.warning("请先勾画地块");
             return;
         }
+        // VE_API.record.generateGrid({
+        //     multipolygon: payload.geometryArr[0],
+        // }).then(res => {
+        //     if (res.code === 200 && Array.isArray(res.data) && res.data.length) {
+        //         mapManage.setTerrainGrids(res.data);
+        //         mapManage.fitGridView();
+        //     } else if (res.code === 200) {
+        //         ElMessage.warning("未生成网格数据");
+        //     }
+        // });
         // console.log("地块数据", payload);
         // console.log("WKT 列表", payload.geometryArr);
         // console.log("合计亩数", payload.mianji);