Browse Source

Merge branch 'agriculture' of http://www.sysuimars.cn:3000/feiniao/feiniao-farm-h5 into agriculture

lxf 1 tuần trước cách đây
mục cha
commit
eccdc6608e

BIN
src/assets/img/agricultural/gaojin.png


BIN
src/assets/img/agricultural/patrol-guide.png


BIN
src/assets/img/agricultural/start.png


BIN
src/assets/img/agricultural/title-bg.png


+ 13 - 0
src/components/pageComponents/PhenologyTrackTimelineItem.vue

@@ -1,4 +1,5 @@
 <template>
+    <div v-if="yearText" class="track-year">{{ yearText }}</div>
     <template v-if="groupedList.length">
         <div class="phenology-track-item" v-for="group in groupedList" :key="group.date">
             <div class="track-axis">
@@ -41,6 +42,13 @@ const dataLoaded = ref(false);
 
 const sourceList = computed(() => (Array.isArray(props.list) ? props.list : imageList.value));
 
+const yearText = computed(() => {
+    if (!Array.isArray(props.list)) return "";
+    const item = props.list.find((entry) => entry?.date || entry?.latest_time);
+    const match = String(item?.date || item?.latest_time || "").match(/(\d{4})/);
+    return match ? t("agriFile.yearLabel", { year: match[1] }) : "";
+});
+
 function formatDate(value) {
     if (!value) return "";
     const text = String(value);
@@ -124,6 +132,11 @@ defineExpose({
 </script>
 
 <style scoped lang="scss">
+.track-year {
+    margin-bottom: 12px;
+    color: rgba(0, 0, 0, 0.4);
+}
+
 .track-empty {
     padding: 24px 0;
     text-align: center;

+ 30 - 4
src/i18n/messages.js

@@ -146,8 +146,8 @@ export default {
             farmLocation: "位置位置位置位置",
             moreFarms: "更多农场",
             patrolRecord: "巡园记录",
-            patrolGrowthTitle: "长势跟踪巡园要点 ({zone})",
-            patrolAbnormalTitle: "异常态势巡园要点 ({zone})",
+            patrolGrowthTitle: "长势跟踪巡园要点",
+            patrolAbnormalTitle: "异常态势巡园要点",
             patrolSubject: "某某主题",
             patrolIssue: "互动问题互动问题互动问题互动问题互",
             forward: "转发",
@@ -156,7 +156,20 @@ export default {
             interactReason: "互动原因互动原因",
             agriAssessment: "农情研判",
             agriAssessmentDesc: "互动原因互动原因互动原因互动原因互气象风险等互动原因互动原因互动原因互动原因互气象风险等",
+            interactQuestion: "互动问题",
+            interactQuestionText: "具体问题具体问题具体问题具体问题具体?",
+            ratioPlaceholder: "请输入比例",
+            submit: "提交",
+            referenceImage: "参考图片",
+            patrolGuide: "巡园向导",
+            growthAbnormal: "长势异常",
+            rootAbnormal: "根系异常类",
             uploadPhoto: "上传照片",
+            pestInteract: "病虫害互动",
+            selectAbnormalPlot: "请选择发生异常的地块",
+            confirmPlot: "确认地块",
+            pleaseSelectPlot: "请先选择地块",
+            cancel: "取消",
             agriAlbum: "农情相册",
             diagnosisReport: "诊断报告",
             reportUpdate: "更新",
@@ -520,8 +533,8 @@ export default {
             farmLocation: "Location",
             moreFarms: "More farms",
             patrolRecord: "Patrol records",
-            patrolGrowthTitle: "Growth tracking patrol ({zone})",
-            patrolAbnormalTitle: "Abnormality patrol ({zone})",
+            patrolGrowthTitle: "Growth tracking patrol",
+            patrolAbnormalTitle: "Abnormality patrol",
             patrolSubject: "Subject",
             patrolIssue: "Interaction questions interaction questions",
             forward: "Forward",
@@ -530,7 +543,20 @@ export default {
             interactReason: "Interaction reason",
             agriAssessment: "Agri assessment",
             agriAssessmentDesc: "Interaction reason interaction reason meteorological risk, etc.",
+            interactQuestion: "Interactive question",
+            interactQuestionText: "Specific question specific question specific question?",
+            ratioPlaceholder: "Enter ratio",
+            submit: "Submit",
+            referenceImage: "Reference image",
+            patrolGuide: "Patrol guide",
+            growthAbnormal: "Growth abnormality",
+            rootAbnormal: "Root abnormality",
             uploadPhoto: "Upload photo",
+            pestInteract: "Pest interaction",
+            selectAbnormalPlot: "Please select the plot where the anomaly occurred",
+            confirmPlot: "Confirm plot",
+            pleaseSelectPlot: "Please select a plot first",
+            cancel: "Cancel",
             agriAlbum: "Agri album",
             diagnosisReport: "Diagnosis report",
             reportUpdate: "Update",

+ 7 - 0
src/router/globalRoutes.js

@@ -205,4 +205,11 @@ export default [
         meta: { keepAlive: true },
         component: () => import("@/views/old_mini/agri_file/pages/growthTrack.vue"),
     },
+    // 病虫害互动选地块
+    {
+        path: "/pest_interact",
+        name: "PestInteract",
+        meta: { keepAlive: true },
+        component: () => import("@/views/old_mini/agri_file/pages/pestInteract.vue"),
+    },
 ];

+ 35 - 0
src/views/old_mini/agri_file/components/albumUploadPopup.vue

@@ -23,6 +23,20 @@
                 </div>
             </uploader>
 
+            <div class="album-upload-popup__date">
+                <div class="album-upload-popup__date-label">上传日期</div>
+                <el-date-picker
+                    v-model="uploadDate"
+                    type="date"
+                    format="YYYY.MM.DD"
+                    value-format="YYYY.MM.DD"
+                    placeholder="请选择上传日期"
+                    :editable="false"
+                    style="width: 100%"
+                    size="large"
+                />
+            </div>
+
             <el-input
                 v-model="description"
                 class="album-upload-popup__desc"
@@ -79,8 +93,16 @@ const showValue = ref(false);
 const fileList = ref([]);
 const uploadedKeys = ref([]);
 const description = ref("");
+const uploadDate = ref("");
 const confirming = ref(false);
 
+function formatDate(date = new Date()) {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, "0");
+    const day = String(date.getDate()).padStart(2, "0");
+    return `${year}.${month}.${day}`;
+}
+
 watch(
     () => props.show,
     (val) => {
@@ -100,6 +122,7 @@ function resetForm() {
     fileList.value = [];
     uploadedKeys.value = [];
     description.value = "";
+    uploadDate.value = formatDate();
     confirming.value = false;
 }
 
@@ -162,6 +185,7 @@ const handleConfirm = () => {
     emit("confirm", {
         images: [...uploadedKeys.value],
         description: description.value.trim(),
+        date: uploadDate.value,
     });
     // 正式上传接口未定时,先关闭弹窗由父组件处理
     confirming.value = false;
@@ -245,6 +269,17 @@ const handleClosed = () => {
         font-weight: 300;
     }
 
+    &__date {
+        margin-bottom: 12px;
+
+        &-label {
+            margin-bottom: 8px;
+            font-size: 14px;
+            line-height: 20px;
+            color: #333;
+        }
+    }
+
     &__desc {
         border: 1px solid rgba(220, 220, 220, 1);
         margin-bottom: 16px;

+ 349 - 377
src/views/old_mini/agri_file/index.vue

@@ -9,108 +9,106 @@
             </div>
         </div>
         <div class="content-panel">
-        <div v-show="activeNav === 'patrol'">
-            <div
-                v-for="item in patrolList"
-                :key="item.id"
-                class="tracking-item"
-                :class="`tracking-item--${item.theme}`"
-                @click="goGrowthTrack(item)"
-            >
-                <div class="tracking-item__header">
-                    <div class="tracking-item__header-row">
-                        <div class="tracking-item__header-left">
-                            <span class="tracking-item__level">
-                                <el-icon class="tracking-item__level-icon"><Bell /></el-icon>
-                                {{ item.level }}
-                            </span>
-                            <span class="tracking-item__title van-ellipsis">{{ item.title }}</span>
-                        </div>
-                        <div class="tracking-item__share" @click.stop>
-                            <el-icon><Right /></el-icon>
-                            <span>{{ t("agriFile.forward") }}</span>
+            <div v-show="activeNav === 'patrol'">
+                <div v-for="item in patrolList" :key="item.id" class="tracking-item"
+                    :class="`tracking-item--${item.theme}`" @click="goGrowthTrack(item)">
+                    <div class="tracking-item__header">
+                        <div class="tracking-item__header-row">
+                            <div class="tracking-item__header-left">
+                                <span class="tracking-item__level">
+                                    <el-icon><BellFilled /></el-icon>
+                                    {{ item.level }}
+                                </span>
+                                <span class="tracking-item__title van-ellipsis">
+                                    {{ item.title }}<span v-if="item.zone" class="tracking-item__zone"> ({{ item.zone }})</span>
+                                </span>
+                            </div>
+                            <div class="tracking-item__share" @click.stop>
+                                <Icon size="14" color="#2199F8" name="share" />
+                                <span>{{ t("agriFile.forward") }}</span>
+                            </div>
                         </div>
                     </div>
-                </div>
-                <div class="tracking-item__body">
-                    <div class="tracking-item__action">
-                        <div class="tracking-item__action-main">
-                            <div class="tracking-item__icon">
-                                <img :src="item.icon" alt="" />
-                                <span class="tracking-item__reason">{{ item.subject }}</span>
+                    <div class="tracking-item__body">
+                        <div class="tracking-item__action">
+                            <div class="tracking-item__action-main">
+                                <div class="tracking-item__icon">
+                                    <img :src="item.icon" alt="" />
+                                    <span class="tracking-item__reason">{{ item.subject }}</span>
+                                </div>
+                                <div class="tracking-item__issue">{{ item.issue }}</div>
+                            </div>
+                            <div class="tracking-item__btn" @click.stop="openUploadPopup">
+                                <el-icon>
+                                    <Plus />
+                                </el-icon>
+                                <span>{{ t("agriFile.uploadPhoto") }}</span>
                             </div>
-                            <div class="tracking-item__issue">{{ item.issue }}</div>
-                        </div>
-                        <div class="tracking-item__btn" @click.stop="openUploadPopup">
-                            <el-icon><Plus /></el-icon>
-                            <span>{{ t("agriFile.uploadPhoto") }}</span>
                         </div>
                     </div>
                 </div>
-            </div>
-            <div class="crop-archive">
-                <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div>
-                <div class="crop-archive__year">{{ t("agriFile.yearLabel", { year: 2025 }) }}</div>
-                <PhenologyTrackTimelineItem :list="cropArchiveList" />
-            </div>
-        </div>
-        <div v-show="activeNav === 'album'">
-            <div class="map-card" @click="goAlbumMap">
-                <div ref="mapContainer" class="map-container"></div>
-                <div class="add-zone-btn" @click.stop="goAddZone">
-                    <el-icon>
-                        <Plus />
-                    </el-icon>
-                    <span>{{ t("agriFile.addZone") }}</span>
-                </div>
-                <div v-for="item in zoneList" :key="item.id" :ref="(el) => setMarkerEl(item.id, el)"
-                    class="zone-marker">
-                    <div class="zone-marker__label">{{ item.name }}</div>
-                    <div class="zone-marker__thumb">
-                        <img :src="item.cover" alt="" />
-                        <span class="zone-marker__badge">{{ item.count }}</span>
-                    </div>
-                    <div class="zone-marker__arrow"></div>
+                <div class="crop-archive">
+                    <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div>
+                    <PhenologyTrackTimelineItem :list="cropArchiveList" />
                 </div>
             </div>
-            <div class="album-grid">
-                <div class="album-add">
-                    <el-icon class="album-add__icon">
-                        <Plus />
-                    </el-icon>
-                    <span>{{ t("agriFile.addAlbum") }}</span>
+            <div v-show="activeNav === 'album'">
+                <div class="map-card" @click="goAlbumMap">
+                    <div ref="mapContainer" class="map-container"></div>
+                    <div class="add-zone-btn" @click.stop="goAddZone">
+                        <el-icon>
+                            <Plus />
+                        </el-icon>
+                        <span>{{ t("agriFile.addZone") }}</span>
+                    </div>
+                    <div v-for="item in zoneList" :key="item.id" :ref="(el) => setMarkerEl(item.id, el)"
+                        class="zone-marker">
+                        <div class="zone-marker__label">{{ item.name }}</div>
+                        <div class="zone-marker__thumb">
+                            <img :src="item.cover" alt="" />
+                            <span class="zone-marker__badge">{{ item.count }}</span>
+                        </div>
+                        <div class="zone-marker__arrow"></div>
+                    </div>
                 </div>
-                <div v-for="item in albumList" :key="item.id" class="album-card">
-                    <div class="album-card__cover">
-                        <img :src="item.cover" alt="" />
-                        <span class="album-card__count">{{ t("agriFile.photoCount", { count: item.count }) }}</span>
-                        <div class="album-card__add">
-                            <el-icon>
-                                <Plus />
-                            </el-icon>
+                <div class="album-grid">
+                    <div class="album-add">
+                        <el-icon class="album-add__icon">
+                            <Plus />
+                        </el-icon>
+                        <span>{{ t("agriFile.addAlbum") }}</span>
+                    </div>
+                    <div v-for="item in albumList" :key="item.id" class="album-card">
+                        <div class="album-card__cover">
+                            <img :src="item.cover" alt="" />
+                            <span class="album-card__count">{{ t("agriFile.photoCount", { count: item.count }) }}</span>
+                            <div class="album-card__add">
+                                <el-icon>
+                                    <Plus />
+                                </el-icon>
+                            </div>
                         </div>
+                        <div class="album-card__name">{{ item.name }}</div>
                     </div>
-                    <div class="album-card__name">{{ item.name }}</div>
                 </div>
             </div>
-        </div>
-        <div v-show="activeNav === 'report'">
-            <div v-for="item in reportList" :key="item.id" class="report-card">
-                <div class="report-card__cover">
-                    <img :src="item.cover" alt="" />
-                    <span class="report-card__date">{{ item.date }}</span>
-                    <span class="report-card__status">
-                        <i class="report-card__dot"></i>
-                        {{ t("agriFile.reportUpdate") }}
-                    </span>
-                </div>
-                <div class="report-card__body">
-                    <div class="report-card__title">{{ item.title }}</div>
-                    <div class="report-card__desc">{{ item.desc }}</div>
+            <div v-show="activeNav === 'report'">
+                <div v-for="item in reportList" :key="item.id" class="report-card">
+                    <div class="report-card__cover">
+                        <img :src="item.cover" alt="" />
+                        <span class="report-card__date">{{ item.date }}</span>
+                        <span class="report-card__status">
+                            <i class="report-card__dot"></i>
+                            {{ t("agriFile.reportUpdate") }}
+                        </span>
+                    </div>
+                    <div class="report-card__body">
+                        <div class="report-card__title">{{ item.title }}</div>
+                        <div class="report-card__desc">{{ item.desc }}</div>
+                    </div>
                 </div>
             </div>
         </div>
-        </div>
         <album-upload-popup v-model:show="showUploadPopup" />
     </div>
 </template>
@@ -119,9 +117,9 @@
 import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { useStore } from "vuex";
 import { useRouter } from "vue-router";
-import { Switch, Plus, Bell, Right } from "@element-plus/icons-vue";
 import Overlay from "ol/Overlay";
 import { useI18n } from "@/i18n";
+import { Icon } from 'vant';
 import FileMap from "./fileMap";
 import * as util from "@/common/ol_common.js";
 import farmHeader from "@/components/farmHeader.vue";
@@ -184,21 +182,23 @@ const showUploadPopup = ref(false);
 const patrolList = ref([
     {
         id: 1,
+        type: "growth",
         theme: "blue",
         level: "",
         title: "",
         subject: "",
         issue: "",
-        icon: require("@/assets/img/report/yc-icon.png"),
+        icon: require("@/assets/img/report/wh-icon.png"),
     },
     {
         id: 2,
+        type: "abnormal",
         theme: "blue",
         level: "",
         title: "",
         subject: "",
         issue: "",
-        icon: require("@/assets/img/report/bh-icon.png"),
+        icon: require("@/assets/img/report/wh-icon.png"),
     },
 ]);
 
@@ -276,7 +276,8 @@ const fillMockLabels = () => {
     patrolList.value = patrolList.value.map((item, index) => ({
         ...item,
         level: t("agriFile.riskLevel2"),
-        title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle", { zone: albumName }),
+        title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
+        zone: albumName,
         subject: t("agriFile.patrolSubject"),
         issue: t("agriFile.patrolIssue"),
     }));
@@ -298,7 +299,7 @@ const goAlbumMap = () => {
 const goGrowthTrack = (item) => {
     router.push({
         path: "/growth_track",
-        query: { id: item?.id },
+        query: { id: item?.id, type: item?.type || "growth" },
     });
 };
 
@@ -339,7 +340,7 @@ watch(activeNav, (key) => {
         display: flex;
         justify-content: space-around;
         padding: 10px 10px;
-        margin-bottom: 0;
+        margin-bottom: 10px;
 
         .nav-card {
             position: relative;
@@ -461,352 +462,323 @@ watch(activeNav, (key) => {
                 border-top: 6px solid #fff;
             }
         }
-    }
-
-    .add-zone-btn {
-        position: absolute;
-        right: 5px;
-        bottom: 10px;
-        z-index: 5;
-        display: flex;
-        align-items: center;
-        gap: 2px;
-        padding: 5px 10px;
-        border-radius: 4px;
-        background: #fff;
-        color: #2199F8;
-        font-weight: 500;
-    }
-
-    .album-grid {
-        display: grid;
-        grid-template-columns: 1fr 1fr;
-        gap: 16px;
-        margin-top: 16px;
-        box-sizing: border-box;
 
-        .album-add {
-            align-self: start;
+        .add-zone-btn {
+            position: absolute;
+            right: 5px;
+            bottom: 10px;
+            z-index: 5;
             display: flex;
-            flex-direction: column;
             align-items: center;
-            justify-content: center;
-            width: 100%;
-            aspect-ratio: 1;
-            border-radius: 5px;
-            background: #F5F5F7;
-            color: #969494;
-            font-size: 16px;
+            gap: 2px;
+            padding: 5px 10px;
+            border-radius: 4px;
+            background: #fff;
+            color: #2199F8;
+            font-weight: 500;
+        }
 
-            &__icon {
-                margin-bottom: 6px;
-                font-size: 20px;
+        .album-grid {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 16px;
+            margin-top: 16px;
+            box-sizing: border-box;
+
+            .album-add {
+                align-self: start;
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+                justify-content: center;
+                width: 100%;
+                aspect-ratio: 1;
+                border-radius: 5px;
+                background: #F5F5F7;
+                color: #969494;
+                font-size: 16px;
+
+                &__icon {
+                    margin-bottom: 6px;
+                    font-size: 20px;
+                }
+            }
+
+            .album-card {
+                width: 100%;
+
+                &__cover {
+                    position: relative;
+                    width: 100%;
+                    aspect-ratio: 1;
+
+                    img {
+                        width: 100%;
+                        height: 100%;
+                        object-fit: cover;
+                        border-radius: 6px;
+                    }
+                }
+
+                &__count {
+                    position: absolute;
+                    top: 1px;
+                    right: 0;
+                    padding: 1px 5px;
+                    border-radius: 0 5px 0 5px;
+                    background: rgba(0, 0, 0, 0.4);
+                    backdrop-filter: blur(4px);
+                    color: #fff;
+                }
+
+                &__add {
+                    position: absolute;
+                    right: 8px;
+                    bottom: 8px;
+                    z-index: 1;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    width: 44px;
+                    height: 44px;
+                    border-radius: 50%;
+                    background: #2199F8;
+                    color: #fff;
+                    font-size: 20px;
+                    box-shadow: 0 2px 8px rgba(33, 153, 248, 0.35);
+                }
+
+                &__name {
+                    margin-top: 10px;
+                    color: #1F1F1F;
+                }
             }
         }
 
-        .album-card {
-            width: 100%;
+        .report-card {
+            margin-bottom: 10px;
+            border-radius: 8px;
+            background: #fff;
+
+            &:last-child {
+                margin-bottom: 0;
+            }
 
             &__cover {
                 position: relative;
                 width: 100%;
-                aspect-ratio: 1;
+                height: 158px;
+
                 img {
+                    display: block;
                     width: 100%;
                     height: 100%;
                     object-fit: cover;
-                    border-radius: 6px;
+                    border-radius: 5px;
                 }
             }
 
-            &__count {
+            &__date {
                 position: absolute;
-                top: 1px;
-                right: 0;
-                padding: 1px 5px;
-                border-radius: 0 5px 0 5px;
-                background: rgba(0, 0, 0, 0.4);
+                top: 0;
+                padding: 5px 12px;
+                border-radius: 4px;
+                background: rgba(0, 0, 0, 0.45);
                 backdrop-filter: blur(4px);
                 color: #fff;
+                font-size: 16px;
+                border-radius: 5px 0 5px 0;
             }
 
-            &__add {
+            &__status {
                 position: absolute;
-                right: 8px;
-                bottom: 8px;
-                z-index: 1;
+                top: 10px;
+                right: 0;
                 display: flex;
                 align-items: center;
-                justify-content: center;
-                width: 44px;
-                height: 44px;
-                border-radius: 50%;
-                background: #2199F8;
+                gap: 4px;
+                padding: 2px 13px;
+                border-radius: 20px 0 0 20px;
+                background: #FF5454;
                 color: #fff;
-                font-size: 20px;
-                box-shadow: 0 2px 8px rgba(33, 153, 248, 0.35);
+                font-size: 12px;
             }
 
-            &__name {
-                margin-top: 10px;
-                color: #1F1F1F;
+            &__dot {
+                width: 6px;
+                height: 6px;
+                border-radius: 50%;
+                background: #fff;
             }
-        }
-    }
-
-    .report-card {
-        overflow: hidden;
-        margin-bottom: 12px;
-        border-radius: 12px;
-        background: #fff;
-
-        &:last-child {
-            margin-bottom: 0;
-        }
-
-        &__cover {
-            position: relative;
-            width: 100%;
-            height: 148px;
 
-            img {
-                display: block;
-                width: 100%;
-                height: 100%;
-                object-fit: cover;
+            &__body {
+                padding: 10px;
+                margin-top: 4px;
             }
-        }
 
-        &__date {
-            position: absolute;
-            top: 8px;
-            left: 8px;
-            padding: 2px 8px;
-            border-radius: 4px;
-            background: rgba(0, 0, 0, 0.45);
-            color: #fff;
-            font-size: 12px;
-            line-height: 18px;
-        }
-
-        &__status {
-            position: absolute;
-            top: 8px;
-            right: 8px;
-            display: flex;
-            align-items: center;
-            gap: 4px;
-            padding: 2px 8px 2px 6px;
-            border-radius: 20px;
-            background: #FF4D4F;
-            color: #fff;
-            font-size: 12px;
-            line-height: 18px;
-        }
-
-        &__dot {
-            width: 6px;
-            height: 6px;
-            border-radius: 50%;
-            background: #fff;
-        }
-
-        &__body {
-            padding: 10px 12px 12px;
-        }
-
-        &__title {
-            color: #1F1F1F;
-            font-size: 16px;
-            font-weight: 600;
-            line-height: 22px;
-        }
+            &__title {
+                font-size: 16px;
+                font-weight: 500;
+            }
 
-        &__desc {
-            margin-top: 6px;
-            color: rgba(0, 0, 0, 0.45);
-            font-size: 13px;
-            line-height: 20px;
+            &__desc {
+                margin-top: 4px;
+                color: #9E9B9B;
+            }
         }
-    }
 
-    .tracking-item {
-        width: 100%;
-        margin-bottom: 12px;
-        border: 1px solid #fff;
-        border-radius: 10px;
-        box-shadow: 0 4px 4px 0 var(--tracking-shadow);
-        cursor: pointer;
+        .tracking-item {
+            width: 100%;
+            margin-bottom: 16px;
+            box-shadow: 0px 4px 4px 0px rgba(33, 153, 248, 0.1);
 
-        &:last-of-type {
-            margin-bottom: 0;
-        }
 
-        &--blue {
-            --tracking-header-bg: linear-gradient(90deg, #5BB8FF 0%, #2199F8 100%);
-            --tracking-primary: #2199f8;
-            --tracking-action-border: rgba(33, 153, 248, 0.2);
-            --tracking-shadow: #2199f81a;
-        }
+            &:last-of-type {
+                margin-bottom: 0;
+            }
 
-        &__header {
-            position: relative;
-            padding: 10px 10px 18px;
-            border-radius: 10px 10px 0 0;
-            background: var(--tracking-header-bg);
-        }
+            &__header {
+                position: relative;
+                padding: 10px 10px 20px;
+                border-radius: 10px 10px 0 0;
+                background: linear-gradient(180deg, #86CAFF 0%, #2199F8 48.5%);
+            }
 
-        &__header-row {
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-            gap: 8px;
-        }
+            &__header-row {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+            }
 
-        &__header-left {
-            display: flex;
-            align-items: center;
-            min-width: 0;
-            gap: 6px;
-        }
+            &__header-left {
+                display: flex;
+                align-items: center;
+                gap: 5px;
+            }
 
-        &__level {
-            display: flex;
-            align-items: center;
-            flex-shrink: 0;
-            gap: 2px;
-            padding: 1px 8px;
-            border-radius: 20px;
-            background: #fff;
-            color: #FF6A6A;
-            font-size: 12px;
-        }
+            &__level {
+                display: flex;
+                align-items: center;
+                gap: 4px;
+                padding: 1px 4px;
+                border-radius: 2px;
+                background: #fff;
+                color: #FF6A6A;
+                font-size: 12px;
+            }
 
-        &__level-icon {
-            font-size: 12px;
-        }
+            &__title {
+                color: #fff;
+                font-size: 16px;
+                font-weight: 500;
+            }
 
-        &__title {
-            min-width: 0;
-            color: #fff;
-            font-size: 15px;
-            font-weight: 500;
-        }
+            &__zone {
+                font-size: 12px;
+            }
 
-        &__share {
-            display: flex;
-            align-items: center;
-            flex-shrink: 0;
-            gap: 2px;
-            padding: 3px 10px;
-            border-radius: 16px;
-            background: #fff;
-            color: #2199F8;
-            font-size: 12px;
-        }
+            &__share {
+                display: flex;
+                align-items: center;
+                gap: 4px;
+                padding: 2px 10px;
+                border-radius: 25px;
+                background: #fff;
+                color: #2199F8;
+                font-size: 12px;
+            }
 
-        &__body {
-            position: relative;
-            padding: 14px 10px 10px;
-            margin-top: -8px;
-            border-radius: 10px;
-            background: #fff;
+            &__body {
+                position: relative;
+                padding: 10px;
+                margin-top: -8px;
+                border-radius: 10px;
+                background: #fff;
 
-            &::before {
-                content: "";
-                position: absolute;
-                top: -7px;
-                left: 50%;
-                width: 0;
-                height: 0;
-                border-left: 7px solid transparent;
-                border-right: 7px solid transparent;
-                border-bottom: 7px solid #fff;
-                transform: translateX(-50%);
+                &::before {
+                    content: "";
+                    position: absolute;
+                    top: -7px;
+                    left: 21%;
+                    width: 0;
+                    height: 0;
+                    border-left: 7px solid transparent;
+                    border-right: 7px solid transparent;
+                    border-bottom: 7px solid #fff;
+                    transform: translateX(-50%);
+                }
             }
-        }
 
-        &__action {
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-            gap: 8px;
-        }
+            &__action {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                border: 1px solid rgba(33, 153, 248, 0.2);
+                border-radius: 6px;
+                padding: 8px;
+            }
 
-        &__action-main {
-            min-width: 0;
-            flex: 1;
-        }
+            &__icon {
+                display: flex;
+                align-items: center;
+                gap: 5px;
 
-        &__icon {
-            display: flex;
-            align-items: center;
-            gap: 4px;
+                img {
+                    width: 18px;
+                    height: 16px;
+                }
+            }
 
-            img {
-                width: 18px;
-                height: 16px;
+            &__reason {
+                color: #2199f8;
+                font-weight: 500;
             }
-        }
 
-        &__reason {
-            color: var(--tracking-primary);
-            font-weight: 500;
-        }
+            &__issue {
+                margin-top: 2px;
+                color: #2199f8;
+                font-size: 12px;
+            }
 
-        &__issue {
-            margin-top: 4px;
-            color: var(--tracking-primary);
-            font-size: 12px;
-            line-height: 17px;
+            &__btn {
+                display: flex;
+                align-items: center;
+                gap: 2px;
+                padding: 7px 10px;
+                border-radius: 5px;
+                background: #2199f8;
+                color: #fff;
+                font-size: 12px;
+            }
         }
 
-        &__btn {
-            display: flex;
-            align-items: center;
-            flex-shrink: 0;
-            gap: 2px;
-            padding: 8px 10px;
-            border-radius: 6px;
-            background: var(--tracking-primary);
-            color: #fff;
-            font-size: 13px;
-        }
-    }
+        .crop-archive {
+            padding: 10px;
+            border-radius: 10px;
+            background: #fff;
 
-    .crop-archive {
-        margin-top: 16px;
-        padding: 16px 12px;
-        border-radius: 12px;
-        background: #fff;
+            &__title {
+                position: relative;
+                display: inline-block;
+                z-index: 1;
+                font-size: 16px;
+                font-weight: 500;
+                margin-bottom: 5px;
 
-        &__title {
-            position: relative;
-            display: inline-block;
-            z-index: 1;
-            color: #1F1F1F;
-            font-size: 16px;
-            font-weight: 600;
-            line-height: 22px;
-
-            &::after {
-                content: "";
-                position: absolute;
-                left: 0;
-                bottom: 2px;
-                z-index: -1;
-                width: 2em;
-                height: 6px;
-                border-radius: 2px;
-                background: rgba(91, 184, 255, 0.55);
+                &::after {
+                    content: "";
+                    position: absolute;
+                    left: 0;
+                    bottom: 2px;
+                    z-index: -1;
+                    width: 90px;
+                    height: 6px;
+                    border-radius: 25px;
+                    background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
+                    opacity: 0.4; 
+                }
             }
         }
-
-        &__year {
-            margin: 4px 0 12px;
-            color: rgba(0, 0, 0, 0.45);
-            font-size: 12px;
-        }
     }
 }
 </style>

+ 368 - 89
src/views/old_mini/agri_file/pages/growthTrack.vue

@@ -1,156 +1,435 @@
 <template>
     <div class="growth-track-page">
         <custom-header :name="t('agriFile.workName')" />
-        <div class="growth-hero">
-            <div class="growth-hero__top">
-                <div class="growth-hero__main">
-                    <div class="growth-hero__title-row">
-                        <span class="growth-hero__title">{{ theme }}</span>
-                        <span class="growth-hero__tag growth-hero__tag--level">
-                            <el-icon class="growth-hero__bell">
-                                <Bell />
-                            </el-icon>
-                            {{ level }}
-                        </span>
-                        <span class="growth-hero__tag growth-hero__tag--zone">{{ zoneName }}</span>
+        <div class="growth-track-body">
+            <div class="growth-hero">
+                <div class="growth-hero__top">
+                    <div class="growth-hero__main">
+                        <div class="growth-hero__title-row">
+                            <span class="growth-hero__title">{{ theme }}</span>
+                            <span class="growth-hero__tag growth-hero__tag--level">
+                                <el-icon>
+                                    <BellFilled />
+                                </el-icon>
+                                {{ level }}
+                            </span>
+                            <span class="growth-hero__tag growth-hero__tag--zone">{{ zoneName }}</span>
+                        </div>
+                        <div class="growth-hero__reason">{{ reason }}</div>
+                    </div>
+                    <div class="growth-hero__share">
+                        <Icon name="share" />
+                        <span>{{ t("agriFile.forward") }}</span>
+                    </div>
+                </div>
+            </div>
+            <template v-if="!isAbnormal">
+                <div class="growth-content">
+                    <div class="growth-content__title">{{ t("agriFile.agriAssessment") }}</div>
+                    <div class="growth-content__desc">{{ t("agriFile.agriAssessmentDesc") }}</div>
+                </div>
+                <div class="interact-card">
+                    <div class="interact-card__head">
+                        <div class="interact-card__tab">
+                            <img class="interact-card__star" src="@/assets/img/agricultural/start.png" alt="" />
+                            <span>{{ t("agriFile.interactQuestion") }}</span>
+                        </div>
+                    </div>
+                    <div class="interact-card__qa">
+                        <div class="interact-card__question">{{ t("agriFile.interactQuestionText") }}</div>
+                        <div class="interact-card__row">
+                            <el-input v-model="ratio" class="interact-card__input" type="number" inputmode="decimal"
+                                :placeholder="t('agriFile.ratioPlaceholder')">
+                                <template #suffix>%</template>
+                            </el-input>
+                            <div class="interact-card__submit" @click="handleSubmit">{{ t("agriFile.submit") }}</div>
+                        </div>
+                    </div>
+                    <div class="interact-card__ref">
+                        <span class="interact-card__ref-label">{{ t("agriFile.referenceImage") }}</span>
+                        <img class="interact-card__ref-img" src="@/assets/img/agricultural/patrol-guide.png" alt="" />
                     </div>
-                    <div class="growth-hero__reason">{{ reason }}</div>
                 </div>
-                <div class="growth-hero__share">
-                    <el-icon>
-                        <Right />
-                    </el-icon>
-                    <span>{{ t("agriFile.forward") }}</span>
+                <div class="guide-card">
+                    <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
+                    <img class="guide-card__img" src="@/assets/img/agricultural/patrol-guide.png" alt="" />
+                </div>
+            </template>
+            <div v-else>
+                <div class="situation-card__head" v-for="item in situationList" :key="item.nameKey">
+                    <div class="situation-card__name">
+                        <img class="situation-card__icon" src="@/assets/img/agricultural/gaojin.png" alt="" />
+                        <span>{{ t(item.nameKey) }}</span>
+                    </div>
+                    <div class="situation-card__assess">
+                        <div class="situation-card__assess-title">{{ t("agriFile.agriAssessment") }}</div>
+                        <div class="situation-card__assess-desc">{{ t("agriFile.agriAssessmentDesc") }}</div>
+                    </div>
+                    <div class="situation-card__guide">
+                        <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
+                        <div class="situation-card__guide-box">
+                            <img src="@/assets/img/agricultural/patrol-guide.png" alt="" />
+                        </div>
+                    </div>
                 </div>
             </div>
         </div>
-        <div class="growth-content">
-            <div class="growth-content__title">{{ t("agriFile.agriAssessment") }}</div>
-            <div class="growth-content__desc">{{ t("agriFile.agriAssessmentDesc") }}</div>
+        <div class="guide-card__upload" @click="handleUploadClick">
+            <el-icon><Plus /></el-icon>
+            <span>{{ t("agriFile.uploadPhoto") }}</span>
         </div>
+        <album-upload-popup v-model:show="showUploadPopup" />
     </div>
 </template>
 
 <script setup>
-import { computed } from "vue";
-import { Bell, Right } from "@element-plus/icons-vue";
+import { computed, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { Icon } from 'vant';
+import { ElMessage } from "element-plus";
 import customHeader from "@/components/customHeader.vue";
+import albumUploadPopup from "../components/albumUploadPopup.vue";
 import { useI18n } from "@/i18n";
 
 const { t } = useI18n();
+const route = useRoute();
+const router = useRouter();
 
+const isAbnormal = computed(() => route.query.type === "abnormal");
 const theme = computed(() => t("agriFile.interactTheme"));
 const level = computed(() => t("agriFile.riskLevel2"));
 const reason = computed(() => t("agriFile.interactReason"));
 const zoneName = computed(() => t("agriFile.zoneOne"));
+const ratio = ref("");
+const showUploadPopup = ref(false);
+const situationList = [
+    { nameKey: "agriFile.growthAbnormal" },
+    { nameKey: "agriFile.rootAbnormal" },
+];
+
+const handleSubmit = () => {
+    if (ratio.value === "" || ratio.value == null) {
+        ElMessage.warning(t("agriFile.ratioPlaceholder"));
+        return;
+    }
+    ElMessage.success(t("agriFile.submit"));
+};
+
+const handleUploadClick = () => {
+    if (isAbnormal.value) {
+        router.push("/pest_interact");
+        return;
+    }
+    showUploadPopup.value = true;
+};
 </script>
 
 <style lang="scss" scoped>
 .growth-track-page {
+    display: flex;
+    flex-direction: column;
     width: 100%;
-    min-height: 100vh;
+    height: 100vh;
+    overflow: hidden;
     background: #f5f5f5;
 
-    .growth-hero {
-        flex-shrink: 0;
-        padding: 14px 12px 28px;
-        background: #2199f8;
+    .growth-track-body {
+        flex: 1;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
 
-        &__top {
-            display: flex;
-            align-items: flex-start;
-            justify-content: space-between;
-            gap: 10px;
+        .growth-hero {
+            padding: 18px 10px 38px;
+            background: #2199f8;
+            color: #fff;
+
+            &__top {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+            }
+
+            &__title-row {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+            }
+
+            &__title {
+                font-size: 28px;
+                font-family: "PangMenZhengDao";
+                margin-top: -5px;
+            }
+
+            &__tag {
+                display: inline-flex;
+                align-items: center;
+                gap: 4px;
+                padding: 1px 4px;
+                border-radius: 2px;
+                background: #fff;
+                font-size: 12px;
+            }
+
+            &__tag--level {
+                color: #FF6A6A;
+            }
+
+            &__tag--zone {
+                color: #2199f8;
+            }
+
+
+            &__reason {
+                margin-top: 10px;
+                padding: 4px 10px;
+                border-radius: 4px;
+                background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%);
+            }
+
+            &__share {
+                display: flex;
+                align-items: center;
+                gap: 4px;
+                padding: 4px 14px;
+                border-radius: 25px;
+                background: rgba(19, 125, 209, 0.5);
+                color: #fff;
+            }
         }
+    }
 
-        &__main {
-            min-width: 0;
-            flex: 1;
+    .growth-content {
+        position: relative;
+        z-index: 1;
+        margin: -24px 10px 12px;
+        padding: 10px;
+        border-radius: 8px;
+        background: #fff;
+
+        &__title {
+            color: #4F4F4F;
+            font-size: 16px;
+            font-family: "PangMenZhengDao";
         }
 
-        &__title-row {
-            display: flex;
-            flex-wrap: wrap;
-            align-items: center;
-            gap: 6px;
+        &__desc {
+            margin-top: 4px;
+            color: #9C9C9C;
+            font-size: 14px;
         }
+    }
 
-        &__title {
-            color: #fff;
-            font-size: 20px;
-            font-weight: 600;
-            line-height: 28px;
+    .interact-card {
+        margin: 0 10px 10px;
+        overflow: hidden;
+        border: 1px solid rgba(33, 153, 248, 0.5);
+        border-radius: 12px;
+        background: #fff;
+
+        &__head {
+            position: relative;
+            background: linear-gradient(180deg, #e5f4ff 0%, #ffffff 72%);
         }
 
-        &__tag {
+        &__tab {
             display: inline-flex;
             align-items: center;
-            flex-shrink: 0;
-            gap: 2px;
-            height: 20px;
-            padding: 0 8px;
-            border-radius: 20px;
-            background: #fff;
-            font-size: 12px;
-            line-height: 20px;
+            gap: 5px;
+            height: 32px;
+            padding: 0 32px 10px 8px;
+            color: #fff;
+            font-size: 16px;
+            font-family: "PangMenZhengDao";
+            background: url("@/assets/img/agricultural/title-bg.png") no-repeat center / 100% 100%;
+
+            span {
+                margin-top: -3px;
+            }
         }
 
-        &__tag--level {
-            color: #ff6a6a;
+        &__star {
+            width: 16px;
+            height: 14px;
         }
 
-        &__tag--zone {
-            color: #2199f8;
+        &__qa {
+            margin: 10px;
+            padding: 10px;
+            border-radius: 8px;
+            background: rgba(33, 153, 248, 0.1);
+
         }
 
-        &__bell {
-            font-size: 12px;
+        &__question {
+            font-size: 14px;
+            font-weight: 500;
         }
 
-        &__reason {
-            margin-top: 10px;
-            padding: 6px 8px;
+        &__row {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            margin-top: 8px;
+        }
+
+        &__input {
+            flex: 1;
+        }
+
+        &__submit {
+            padding: 6px 21px;
             border-radius: 4px;
-            background: rgba(255, 255, 255, 0.16);
+            background: #2199f8;
             color: #fff;
-            font-size: 13px;
-            line-height: 20px;
+            text-align: center;
         }
 
-        &__share {
-            display: flex;
-            align-items: center;
-            flex-shrink: 0;
-            gap: 2px;
-            margin-top: 4px;
-            padding: 4px 10px;
-            border-radius: 16px;
-            background: rgba(13, 92, 173, 0.35);
+        &__ref {
+            position: relative;
+            margin: 10px;
+            border-radius: 8px;
+        }
+
+        &__ref-label {
+            position: absolute;
+            z-index: 1;
+            padding: 3px 11px;
+            border-radius: 8px 0 8px 0;
+            background: rgba(0, 0, 0, 0.43);
+            backdrop-filter: blur(5px);
             color: #fff;
             font-size: 12px;
         }
+
+        &__ref-img {
+            display: block;
+            width: 100%;
+        }
     }
 
-    .growth-content {
+    .guide-card__upload {
+        position: fixed;
+        right: 7px;
+        bottom: 94px;
+        z-index: 10;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        width: 58px;
+        height: 58px;
+        border-radius: 50%;
+        background: #2199f8;
+        color: #fff;
+        box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.1);
+        font-size: 15px;
+    }
+
+    .guide-card__upload span {
+        margin-top: 3px;
+        font-size: 10px;
+    }
+
+    .guide-title {
         position: relative;
         z-index: 1;
-        margin: -16px 10px 12px;
-        padding: 14px 12px 16px;
-        border-radius: 12px;
+        display: inline-block;
+        font-size: 16px;
+        font-weight: 500;
+
+        &::after {
+            content: "";
+            position: absolute;
+            left: 0;
+            bottom: 2px;
+            z-index: -1;
+            width: 90px;
+            height: 6px;
+            border-radius: 25px;
+            background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
+            opacity: 0.4;
+        }
+    }
+
+    .guide-card {
+        position: relative;
+        margin: 10px;
+        padding: 10px;
+        border-radius: 8px;
         background: #fff;
 
-        &__title {
-            color: #1f1f1f;
+        &__img {
+            display: block;
+            width: 100%;
+            margin-top: 8px;
+        }
+    }
+
+    .situation-card {
+        &__head {
+            position: relative;
+            z-index: 1;
+            margin: 0 10px 12px;
+            padding: 12px;
+            border-radius: 10px;
+            background: #fff;
+
+            &:first-child {
+                margin-top: -24px;
+            }
+        }
+
+        &__name {
+            display: flex;
+            align-items: stretch;
+            gap: 10px;
+            padding-bottom: 10px;
+            border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+            color: #2199f8;
             font-size: 16px;
-            font-weight: 600;
-            line-height: 22px;
+            font-weight: 500;
         }
 
-        &__desc {
-            margin-top: 8px;
-            color: rgba(0, 0, 0, 0.45);
-            font-size: 13px;
-            line-height: 20px;
+        &__icon {
+            width: 18px;
+            height: 18px;
+        }
+
+        &__assess {
+            margin-top: 10px;
+            padding: 10px;
+            border-radius: 8px;
+            background: #F9F9F9;
+        }
+
+        &__assess-title {
+            color: #4F4F4F;
+            font-size: 16px;
+            margin-top: -3px;
+            font-family: "PangMenZhengDao";
+        }
+
+        &__assess-desc {
+            margin-top: 4px;
+            color: #9C9C9C;
+        }
+
+        &__guide {
+            margin-top: 10px;
+            padding: 10px;
+            border: 1px solid rgba(33, 153, 248, 0.2);
+            border-radius: 8px;
+            background: linear-gradient(360deg, #FFFFFF 90.33%, #ABD8FC 116.61%, #2199F8 148.55%);
+        }
+
+        &__guide-box {
+            margin-top: 12px;
+
+            img {
+                display: block;
+                width: 100%;
+            }
         }
     }
 }

+ 350 - 0
src/views/old_mini/agri_file/pages/pestInteract.vue

@@ -0,0 +1,350 @@
+<template>
+    <div class="pest-interact-page">
+        <custom-header :name="t('agriFile.pestInteract')" />
+        <div class="pest-interact-content">
+            <div class="map-container" ref="mapContainer"></div>
+            <div class="search-bar">
+                <div class="search-bar__city">
+                    <span class="search-bar__city-name van-ellipsis">{{ cityName }}</span>
+                    <el-icon class="search-bar__city-icon">
+                        <ArrowDown />
+                    </el-icon>
+                </div>
+                <location-search class="search-bar__search" :user-location="userLocation"
+                    @change="handleLocationChange" />
+            </div>
+            <div class="map-tip">{{ t("agriFile.selectAbnormalPlot") }}</div>
+            <div class="locate-btn" @click="handleLocate">
+                <img class="locate-btn__icon" src="@/assets/img/map/map-icon.png" alt="" />
+            </div>
+        </div>
+        <div class="custom-bottom-fixed-btns">
+            <div class="bottom-btn secondary-btn" @click="handleCancel">{{ t("agriFile.cancel") }}</div>
+            <div class="bottom-btn primary-btn" @click="handleConfirm">{{ t("agriFile.confirmPlot") }}</div>
+        </div>
+        <album-upload-popup v-model:show="showUploadPopup" />
+    </div>
+</template>
+
+<script setup>
+import { nextTick, onActivated, ref } from "vue";
+import { useRouter } from "vue-router";
+import { useStore } from "vuex";
+import { ElMessage } from "element-plus";
+import { ArrowDown } from "@element-plus/icons-vue";
+import customHeader from "@/components/customHeader.vue";
+import locationSearch from "@/components/pageComponents/locationSearch.vue";
+import albumUploadPopup from "../components/albumUploadPopup.vue";
+import FileMap from "../fileMap";
+import * as util from "@/common/ol_common.js";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+const router = useRouter();
+const store = useStore();
+const mapContainer = ref(null);
+const fileMap = new FileMap();
+const selectedZoneId = ref(null);
+const showUploadPopup = ref(false);
+let clickBound = false;
+
+const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
+const cityName = ref("");
+const userLocation = ref(
+    store.state.home.miniUserLocation ||
+    localStorage.getItem("MINI_USER_LOCATION") ||
+    "113.61702297075017,23.584863449735067"
+);
+
+const MOCK_ZONES = [
+    { id: 1, nameKey: "agriFile.legendZone", dlng: -0.00115, dlat: 0.00055, delta: 0.00155 },
+];
+
+function squarePolygonWkt(lng, lat, delta = 0.0014) {
+    const ring = [
+        [lng - delta, lat + delta * 0.7],
+        [lng + delta * 0.35, lat + delta],
+        [lng + delta, lat - delta * 0.2],
+        [lng + delta * 0.15, lat - delta],
+        [lng - delta * 0.85, lat - delta * 0.45],
+        [lng - delta, lat + delta * 0.7],
+    ];
+    return `POLYGON((${ring.map((point) => point.join(" ")).join(", ")}))`;
+}
+
+function getFarmData() {
+    try {
+        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+    } catch {
+        return {};
+    }
+}
+
+function isPointWkt(value) {
+    return typeof value === "string" && /^POINT\s*\(/i.test(value.trim());
+}
+
+function wkbHexToPointWkt(hex) {
+    const text = String(hex || "").trim();
+    if (!/^[0-9a-fA-F]+$/.test(text) || text.length < 42) return null;
+    const bytes = new Uint8Array(text.length / 2);
+    for (let i = 0; i < bytes.length; i++) {
+        bytes[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
+    }
+    const view = new DataView(bytes.buffer);
+    const little = view.getUint8(0) === 1;
+    let type = view.getUint32(1, little);
+    const hasSrid = (type & 0x20000000) !== 0;
+    type &= 0xff;
+    if (type !== 1) return null;
+    let offset = 5;
+    if (hasSrid) offset += 4;
+    if (offset + 16 > bytes.length) return null;
+    const lng = view.getFloat64(offset, little);
+    const lat = view.getFloat64(offset + 8, little);
+    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
+    return `POINT(${lng} ${lat})`;
+}
+
+function toPointWkt(value) {
+    if (!value || typeof value !== "string") return null;
+    if (isPointWkt(value)) return value.trim();
+    return wkbHexToPointWkt(value);
+}
+
+function getFarmMapLocation() {
+    const farmData = getFarmData();
+    const candidates = [farmData.wkt, farmData.geom_wkt, farmData.farm_location];
+    for (const item of candidates) {
+        const pointWkt = toPointWkt(item);
+        if (pointWkt) return pointWkt;
+    }
+    return DEFAULT_MAP_LOCATION;
+}
+
+function getDefaultMapLocation() {
+    return (
+        toPointWkt(localStorage.getItem("MINI_USER_LOCATION_POINT")) ||
+        toPointWkt(store.state.home.miniUserLocationPoint) ||
+        getFarmMapLocation()
+    );
+}
+
+function getDefaultMapCoordinate() {
+    return util.wktCastGeom(getDefaultMapLocation()).getFirstCoordinate();
+}
+
+function loadPlotLayers() {
+    const farmData = getFarmData();
+    cityName.value = farmData.city || farmData.farm_city || t("agriFile.defaultCity");
+    const [lng, lat] = getDefaultMapCoordinate();
+    const labels = MOCK_ZONES.map((item) => ({
+        name: t(item.nameKey),
+        longitude: lng + item.dlng,
+        latitude: lat + item.dlat,
+    }));
+    const zoneRecords = MOCK_ZONES.map((item) => ({
+        id: item.id,
+        zone_name: "",
+        polygon: squarePolygonWkt(lng + item.dlng, lat + item.dlat, item.delta),
+    }));
+    fileMap.setRecordPolygons(zoneRecords, "album");
+    fileMap.setAlbumMarkers({ labels, photos: [] });
+    fileMap.recordPolygonLayer.source.getFeatures().forEach((feature, index) => {
+        feature.set("zone_id", MOCK_ZONES[index]?.id);
+    });
+    selectedZoneId.value = MOCK_ZONES[0]?.id ?? null;
+}
+
+function onMapClick(evt) {
+    const feature = fileMap.kmap?.map?.forEachFeatureAtPixel(evt.pixel, (hit) => {
+        if (hit.get("zone_id")) return hit;
+        return undefined;
+    });
+    if (!feature) return;
+    selectedZoneId.value = feature.get("zone_id");
+}
+
+function bindMapClick() {
+    if (clickBound || !fileMap.kmap?.map) return;
+    clickBound = true;
+    fileMap.kmap.map.on("singleclick", onMapClick);
+}
+
+const initMap = async () => {
+    await nextTick();
+    if (!mapContainer.value) return;
+    fileMap.initMap(getDefaultMapLocation(), mapContainer.value);
+    loadPlotLayers();
+    fileMap.kmap?.map?.updateSize?.();
+    bindMapClick();
+};
+
+const handleLocationChange = (payload) => {
+    if (!payload?.coordinateArray || !fileMap.kmap) return;
+    fileMap.kmap.getView().animate({
+        center: payload.coordinateArray,
+        zoom: 16,
+        duration: 0,
+    });
+    if (payload.city) cityName.value = payload.city;
+};
+
+const handleLocate = () => {
+    if (!fileMap.kmap) return;
+    fileMap.kmap.getView().animate({
+        center: getDefaultMapCoordinate(),
+        zoom: 16,
+        duration: 0,
+    });
+};
+
+const handleCancel = () => {
+    router.back();
+};
+
+const handleConfirm = () => {
+    if (!selectedZoneId.value) {
+        ElMessage.warning(t("agriFile.pleaseSelectPlot"));
+        return;
+    }
+    sessionStorage.setItem("SELECTED_ABNORMAL_PLOT", String(selectedZoneId.value));
+    showUploadPopup.value = true;
+};
+
+onActivated(() => {
+    initMap();
+});
+</script>
+
+<style lang="scss" scoped>
+.pest-interact-page {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    background: #fff;
+}
+
+.pest-interact-content {
+    position: relative;
+    flex: 1;
+    min-height: 0;
+    overflow: hidden;
+
+    .map-container {
+        width: 100%;
+        height: 100%;
+    }
+
+    .search-bar {
+        position: absolute;
+        top: 12px;
+        left: 12px;
+        right: 12px;
+        z-index: 2;
+        display: flex;
+        align-items: center;
+        height: 40px;
+        padding: 0 4px 0 12px;
+        border-radius: 20px;
+        background: rgba(0, 0, 0, 0.45);
+        box-sizing: border-box;
+
+        &__city {
+            display: flex;
+            align-items: center;
+            flex-shrink: 0;
+            max-width: 92px;
+            color: #fff;
+            font-size: 14px;
+        }
+
+        &__city-name {
+            max-width: 72px;
+        }
+
+        &__city-icon {
+            margin-left: 2px;
+            font-size: 12px;
+        }
+
+        &__search {
+            flex: 1;
+            min-width: 0;
+            margin-left: 8px;
+
+            :deep(.el-select__wrapper) {
+                background: transparent;
+                box-shadow: none;
+                border: none;
+                min-height: 40px;
+                padding-left: 0;
+            }
+
+            :deep(.el-select__placeholder),
+            :deep(.el-select__input) {
+                color: rgba(255, 255, 255, 0.7);
+            }
+
+            :deep(.el-icon) {
+                color: rgba(255, 255, 255, 0.85);
+            }
+        }
+    }
+
+    .map-tip {
+        position: absolute;
+        top: 64px;
+        left: 50%;
+        z-index: 2;
+        transform: translateX(-50%);
+        padding: 8px 30px;
+        border-radius: 25px;
+        background: rgba(0, 0, 0, 0.5);
+        color: #7CC5FF;
+        font-size: 12px;
+        white-space: nowrap;
+    }
+
+    .locate-btn {
+        position: absolute;
+        right: 12px;
+        bottom: 24px;
+        z-index: 2;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 36px;
+        height: 36px;
+        border-radius: 8px;
+        background: #fff;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+        &__icon {
+            width: 16px;
+            height: 18px;
+        }
+    }
+}
+
+.custom-bottom-fixed-btns {
+    position: relative;
+    justify-content: space-between;
+
+    .bottom-btn {
+        padding: 10px 20px;
+        border-radius: 25px;
+    }
+
+    .secondary-btn {
+        color: #666;
+        border: 1px solid rgba(153, 153, 153, 0.5);
+    }
+
+    .primary-btn {
+        background: #2199f8;
+    }
+}
+</style>