Selaa lähdekoodia

fix: 弹窗,地图,样式

lxf 6 päivää sitten
vanhempi
commit
699cda584d

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


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


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


+ 62 - 7
src/components/farmHeader.vue

@@ -30,16 +30,71 @@ const farmName = ref("");
 const farmLocation = ref("");
 const circleUrl = ref(defaultAvatar);
 
-const loadFarmInfo = () => {
+const applyFarmInfo = (farm = {}) => {
+    farmName.value = farm.farm_name || farm.name || t("agriFile.farmName");
+    farmLocation.value = farm.farm_address || farm.rawAddress || farm.city || t("agriFile.farmLocation");
+    circleUrl.value = farm.cover || farm.image || farm.avatar || defaultAvatar;
+};
+
+const hasSelectedFarm = (farm) => {
+    if (!farm || typeof farm !== "object") return false;
+    return !!(farm.farm_id || farm.id || farm.farm_name || farm.name);
+};
+
+const resolveFarmPointWkt = (farm) => {
+    if (farm.geom_wkt && /^POINT\s*\(/i.test(String(farm.geom_wkt).trim())) {
+        return farm.geom_wkt;
+    }
+    if (farm.farm_location && /^POINT\s*\(/i.test(String(farm.farm_location).trim())) {
+        return farm.farm_location;
+    }
+    return farm.geom_wkt || farm.wkt || "";
+};
+
+const normalizeFarm = (farm) => {
+    const id = farm.farm_id ?? farm.id;
+    return {
+        ...farm,
+        id,
+        name: farm.farm_name || farm.name,
+        wkt: resolveFarmPointWkt(farm),
+        rawAddress: farm.farm_address || farm.city || "",
+    };
+};
+
+const saveSelectedFarm = (farm) => {
+    localStorage.setItem("selectedFarmId", farm.id);
+    localStorage.setItem("selectedFarmName", farm.name || "");
+    localStorage.setItem("selectedFarmPoint", farm.wkt || "");
+    localStorage.setItem("selectedFarmData", JSON.stringify(farm));
+};
+
+const loadFirstFarmAsSelected = async () => {
+    try {
+        const { data } = await VE_API.farm.getFarmList();
+        const first = (data || [])[0];
+        if (!first) {
+            applyFarmInfo();
+            return;
+        }
+        const farm = normalizeFarm(first);
+        saveSelectedFarm(farm);
+        applyFarmInfo(farm);
+    } catch {
+        applyFarmInfo();
+    }
+};
+
+const loadFarmInfo = async () => {
     try {
         const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
-        farmName.value = farm.farm_name || farm.name || t("agriFile.farmName");
-        farmLocation.value = farm.farm_address || farm.rawAddress || farm.city || t("agriFile.farmLocation");
-        circleUrl.value = farm.cover || farm.image || farm.avatar || defaultAvatar;
+        if (hasSelectedFarm(farm)) {
+            applyFarmInfo(farm);
+            return;
+        }
+        await loadFirstFarmAsSelected();
     } catch {
-        farmName.value = t("agriFile.farmName");
-        farmLocation.value = t("agriFile.farmLocation");
-        circleUrl.value = defaultAvatar;
+        await loadFirstFarmAsSelected();
     }
 };
 

+ 100 - 0
src/components/popup/surveyAskPopup.vue

@@ -0,0 +1,100 @@
+<template>
+    <Popup
+        v-model:show="showValue"
+        round
+        position="center"
+        teleport="body"
+        :z-index="20000"
+        class="survey-ask-popup"
+        :close-on-click-overlay="true"
+    >
+        <img class="survey-ask-popup__badge" src="@/assets/img/agricultural/ask-bg.png" alt="" />
+        <img class="survey-ask-popup__illust" src="@/assets/img/agricultural/ask.png" alt="" />
+        <div class="survey-ask-popup__title">{{ t("workExecute.fillSurvey") }}</div>
+        <div class="survey-ask-popup__desc">{{ t("workExecute.fillSurveyDesc") }}</div>
+        <div class="survey-ask-popup__btn" @click="handleFill">{{ t("workExecute.fillNow") }}</div>
+    </Popup>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { Popup } from "vant";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+
+const props = defineProps({
+    show: {
+        type: Boolean,
+        default: false,
+    },
+});
+
+const emit = defineEmits(["update:show", "confirm"]);
+
+const showValue = computed({
+    get: () => props.show,
+    set: (val) => emit("update:show", val),
+});
+
+const handleFill = () => {
+    emit("confirm");
+    showValue.value = false;
+};
+</script>
+
+<style lang="scss">
+.survey-ask-popup.van-popup {
+    width: 300px;
+    padding: 28px 20px 24px;
+    text-align: center;
+    overflow: visible;
+    background: #fff;
+    box-sizing: border-box;
+}
+
+.survey-ask-popup__badge {
+    position: absolute;
+    top: -18px;
+    left: -10px;
+    width: 128px;
+    height: auto;
+    pointer-events: none;
+    z-index: 1;
+}
+
+.survey-ask-popup__illust {
+    display: block;
+    width: 148px;
+    height: auto;
+    margin: 12px auto 18px;
+}
+
+.survey-ask-popup__title {
+    font-size: 24px;
+    color: #000;
+    line-height: 28px;
+    font-family: "PangMenZhengDao";
+}
+
+.survey-ask-popup__desc {
+    margin-top: 6px;
+    font-size: 22px;
+    color: #000;
+    line-height: 28px;
+    font-family: "PangMenZhengDao";
+}
+
+.survey-ask-popup__btn {
+    height: 40px;
+    line-height: 40px;
+    border-radius: 25px;
+    background: #2199f8;
+    color: #fff;
+    font-size: 16px;
+    box-sizing: border-box;
+    padding: 0 30px;
+    width: fit-content;
+    margin: 20px auto 0;
+    }
+</style>

+ 34 - 0
src/i18n/messages.js

@@ -199,8 +199,11 @@ export default {
             reportDesc: "主题主题主题主题主题主题主题主题主题主题主题主题主题主题主题主题",
             reportDate: "8月30日",
             addZone: "新增分区",
+            drawFarmArea: "勾画区域",
             addAlbum: "新增相册",
             selectZoneHint: "请勾选您的分区区域",
+            drawFarmAreaHint: "请勾画农场区域",
+            drawFarmAreaSuccess: "勾画农场区域成功",
             searchLocation: "搜索位置",
             defaultCity: "武汉市",
             cancelSelect: "取消勾选",
@@ -339,6 +342,20 @@ export default {
             viewDetail: "查看详情",
             forwardTip: "转发功能即将开放",
             exclusiveFarmCalendar: "专属农事日历",
+            fillSurvey: "填写调查问卷",
+            fillSurveyDesc: "更方便为您分配农场任务",
+            fillNow: "立即填写",
+            surveyEntryTitle: "录入信息",
+            surveyEntryHint: "请勾选您希望由专业服务团队",
+            surveyEntryHint2: "帮您服务的农事类别",
+            surveySearchPlaceholder: "请输入农事类别",
+            surveySearch: "搜索",
+            surveySelectTitle: "选择农事类别",
+            surveySelectHint: "可多选",
+            surveySubmit: "提交信息",
+            surveyPleaseSelect: "请至少选择一个农事类别",
+            surveySubmitSuccess: "提交成功",
+            surveyNoMatch: "暂无匹配类别",
             redispatch: "重新派发",
             remindAccept: "提醒接受",
             modifyInfo: "修改信息",
@@ -611,8 +628,11 @@ export default {
             reportDesc: "Theme theme theme theme theme theme theme theme theme theme theme theme",
             reportDate: "Aug 30",
             addZone: "Add zone",
+            drawFarmArea: "Draw area",
             addAlbum: "Add album",
             selectZoneHint: "Please select your zone area",
+            drawFarmAreaHint: "Please draw the farm area",
+            drawFarmAreaSuccess: "Farm area drawn successfully",
             searchLocation: "Search location",
             defaultCity: "Wuhan",
             cancelSelect: "Clear selection",
@@ -751,6 +771,20 @@ export default {
             viewDetail: "View details",
             forwardTip: "Forward feature coming soon",
             exclusiveFarmCalendar: "exclusive farm calendar",
+            fillSurvey: "Fill out the survey",
+            fillSurveyDesc: "Helps us assign farm tasks more easily",
+            fillNow: "Fill in now",
+            surveyEntryTitle: "Enter information",
+            surveyEntryHint: "Select the farm work categories",
+            surveyEntryHint2: "you want professional teams to handle",
+            surveySearchPlaceholder: "Enter farm work category",
+            surveySearch: "Search",
+            surveySelectTitle: "Select farm work categories",
+            surveySelectHint: "Multi-select",
+            surveySubmit: "Submit",
+            surveyPleaseSelect: "Please select at least one category",
+            surveySubmitSuccess: "Submitted successfully",
+            surveyNoMatch: "No matching categories",
             redispatch: "Redispatch",
             remindAccept: "Remind to accept",
             modifyInfo: "Edit info",

+ 7 - 0
src/router/globalRoutes.js

@@ -116,6 +116,13 @@ export default [
         meta: { showTabbar: true, keepAlive: true },
         component: () => import("@/views/old_mini/work_execute/index.vue"),
     },
+    // 调查问卷 - 选择农事类别
+    {
+        path: "/survey_entry",
+        name: "SurveyEntry",
+        meta: { keepAlive: false },
+        component: () => import("@/views/old_mini/work_execute/surveyEntry.vue"),
+    },
     // 农场信息
     {
         path: "/farm_info",

+ 66 - 4
src/views/old_mini/agri_file/index.vue

@@ -55,7 +55,13 @@
             <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">
+                    <div v-if="noFarmRange" class="map-mask" @click.stop="goAddZone('farm')">
+                        <div class="mask-content">
+                            <div class="mask-content__title">勾画农场区域,关联作物照片</div>
+                            <div class="mask-content__btn">去勾选</div>
+                        </div>
+                    </div>
+                    <div v-else class="add-zone-btn" @click.stop="goAddZone">
                         <el-icon>
                             <Plus />
                         </el-icon>
@@ -151,6 +157,8 @@ const fileMap = new FileMap();
 const markerEls = {};
 const mapOverlays = [];
 
+const noFarmRange = ref(true);
+
 const navCards = [
     {
         key: "patrol",
@@ -193,7 +201,7 @@ const showUploadPopup = ref(false);
 const showCompleteFarmPopup = ref(false);
 const showDiagnosisReportPopup = ref(false);
 // 后续由接口控制是否展示恢复种植服务弹窗
-const showRestorePlantingPopup = ref(false);
+const showRestorePlantingPopup = ref(true);
 const patrolList = ref([
     {
         id: 1,
@@ -273,6 +281,11 @@ const initAlbumMap = async () => {
     const center = getFarmCenter();
     fileMap.initMap(`POINT(${center[0]} ${center[1]})`, mapContainer.value);
     await nextTick();
+    if (noFarmRange.value) {
+        zoneList.value = [];
+        albumList.value = [];
+        return;
+    }
     bindZoneOverlays();
     fileMap.kmap?.map?.updateSize?.();
 };
@@ -303,8 +316,15 @@ const fillMockLabels = () => {
     }));
 };
 
-const goAddZone = () => {
-    router.push("/add_zone");
+const goAddZone = (type) => {
+    router.push(
+        {
+            path: "/add_zone",
+            query: {
+                type,
+            },
+        }
+    );
 };
 
 const goAlbumMap = () => {
@@ -452,6 +472,48 @@ watch(activeNav, (key) => {
             clip-path: inset(0px round 8px);
         }
 
+        .map-mask {
+            position: absolute;
+            top: 0;
+            left: 0;
+            z-index: 5;
+            width: 100%;
+            height: 100%;
+            background: rgba(0, 0, 0, 0.5);
+            border-radius: 8px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            pointer-events: auto;
+            cursor: pointer;
+
+            .mask-content {
+                background: rgba(255, 255, 255, 0.2);
+                backdrop-filter: blur(4px);
+                padding: 8px 10px;
+                border-radius: 4px;
+                width: fit-content;
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+                justify-content: center;
+                gap: 12px;
+                color: #fff;
+                pointer-events: none;
+                &__title {
+                    font-size: 18px;
+                    font-family: "PangMenZhengDao";
+                }
+                &__btn {
+                    padding: 0 24px;
+                    height: 32px;
+                    line-height: 32px;
+                    border-radius: 24px;
+                    background: #2199F8;
+                }
+            }
+        }
+
         .zone-marker {
             display: flex;
             flex-direction: column;

+ 32 - 5
src/views/old_mini/agri_file/pages/addZone.vue

@@ -1,13 +1,13 @@
 <template>
     <div class="add-zone-page">
-        <custom-header :name="t('agriFile.addZone')" />
+        <custom-header :name="isFarmDrawMode ? t('agriFile.drawFarmArea') : t('agriFile.addZone')" />
         <div class="add-zone-content">
             <div class="map-container" ref="mapContainer"></div>
             <div class="search-bar">
                 <location-search class="search-bar__search" :user-location="userLocation"
                     @change="handleLocationChange" />
             </div>
-            <div class="map-tip">{{ t("agriFile.selectZoneHint") }}</div>
+            <div class="map-tip">{{ isFarmDrawMode ? t("agriFile.drawFarmAreaHint") : t("agriFile.selectZoneHint") }}</div>
             <div class="locate-btn" @click="handleLocate">
                 <img class="locate-btn__icon" src="@/assets/img/map/map-icon.png" alt="" />
             </div>
@@ -21,25 +21,29 @@
 </template>
 
 <script setup>
-import { nextTick, onActivated, onDeactivated, ref } from "vue";
-import { useRouter } from "vue-router";
+import { computed, nextTick, onActivated, onDeactivated, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
 import { useStore } from "vuex";
 import { ElMessage } from "element-plus";
 import customHeader from "@/components/customHeader.vue";
 import locationSearch from "@/components/pageComponents/locationSearch.vue";
 import RegionNamePopup from "@/views/old_mini/recordDetails/components/RegionNamePopup.vue";
 import MapManage from "@/views/old_mini/recordDetails/map/mapManage.js";
+import { POINT_ICON } from "@/views/old_mini/entry_information/map/selectLocationMap.js";
 import * as util from "@/common/ol_common.js";
 import { useI18n } from "@/i18n";
 
 const { t } = useI18n();
+const route = useRoute();
 const router = useRouter();
+const isFarmDrawMode = computed(() => route.query.type === "farm");
 const store = useStore();
 const mapContainer = ref(null);
 const mapManage = new MapManage();
 const showRegionNamePopup = ref(false);
 
 const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
+const FARM_DRAW_LOCATION = "POINT(113.31812938867188 23.002427029296875)";
 const ZONE_STYLE = {
     fill: "rgba(33, 153, 248, 0.35)",
     fillSelected: "rgba(33, 153, 248, 0.45)",
@@ -114,14 +118,26 @@ function getDefaultMapCoordinate() {
     return util.wktCastGeom(getDefaultMapLocation()).getFirstCoordinate();
 }
 
+function getFarmDrawCoordinate() {
+    return util.wktCastGeom(FARM_DRAW_LOCATION).getFirstCoordinate();
+}
+
+function getMapLocation() {
+    if (isFarmDrawMode.value) return FARM_DRAW_LOCATION;
+    return getDefaultMapLocation();
+}
+
 function initMapView() {
     if (!mapContainer.value) return;
     mapManage.destroyMap();
-    mapManage.initMap(getDefaultMapLocation(), mapContainer.value, {
+    mapManage.initMap(getMapLocation(), mapContainer.value, {
         editable: true,
         zoneStyle: ZONE_STYLE,
     });
     mapManage.enableRegionDrawing();
+    if (isFarmDrawMode.value) {
+        mapManage.showCenterMarker(getFarmDrawCoordinate(), POINT_ICON.resident);
+    }
 }
 
 const handleLocationChange = (payload) => {
@@ -130,6 +146,12 @@ const handleLocationChange = (payload) => {
 };
 
 const handleLocate = () => {
+    if (isFarmDrawMode.value) {
+        const coordinate = getFarmDrawCoordinate();
+        mapManage.setMapPosition(coordinate);
+        mapManage.showCenterMarker(coordinate, POINT_ICON.resident);
+        return;
+    }
     mapManage.setMapPosition(getDefaultMapCoordinate());
 };
 
@@ -143,6 +165,11 @@ const handleConfirmDraw = () => {
         ElMessage.warning(t("agriFile.pleaseDrawZone"));
         return;
     }
+    if (isFarmDrawMode.value) {
+        ElMessage.success(t("agriFile.drawFarmAreaSuccess"));
+        router.back();
+        return;
+    }
     showRegionNamePopup.value = true;
 };
 

+ 1 - 28
src/views/old_mini/agri_file/pages/albumMap.vue

@@ -4,10 +4,6 @@
         <div class="album-map-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"
@@ -35,7 +31,7 @@
 import { nextTick, onActivated, ref } from "vue";
 import { useRouter } from "vue-router";
 import { useStore } from "vuex";
-import { ArrowDown, Plus } from "@element-plus/icons-vue";
+import { Plus } from "@element-plus/icons-vue";
 import customHeader from "@/components/customHeader.vue";
 import locationSearch from "@/components/pageComponents/locationSearch.vue";
 import FileMap from "../fileMap";
@@ -51,7 +47,6 @@ const fileMap = new FileMap();
 const defaultCover = require("@/assets/img/home/banner.png");
 const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
 
-const cityName = ref("");
 const userLocation = ref(
     store.state.home.miniUserLocation ||
         localStorage.getItem("MINI_USER_LOCATION") ||
@@ -149,8 +144,6 @@ function getDefaultMapCoordinate() {
 }
 
 const loadAlbumLayers = () => {
-    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),
@@ -186,7 +179,6 @@ const handleLocationChange = (payload) => {
         zoom: 16,
         duration: 0,
     });
-    if (payload.city) cityName.value = payload.city;
 };
 
 const handleLocate = () => {
@@ -243,28 +235,9 @@ onActivated(() => {
     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;

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

@@ -4,12 +4,6 @@
         <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>
@@ -31,7 +25,6 @@ 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";
@@ -49,7 +42,6 @@ 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") ||
@@ -135,8 +127,6 @@ function getDefaultMapCoordinate() {
 }
 
 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),
@@ -187,7 +177,6 @@ const handleLocationChange = (payload) => {
         zoom: 16,
         duration: 0,
     });
-    if (payload.city) cityName.value = payload.city;
 };
 
 const handleLocate = () => {
@@ -252,28 +241,9 @@ onActivated(() => {
         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;

+ 1 - 8
src/views/old_mini/agri_file/pages/regionAlbums.vue

@@ -327,14 +327,7 @@ onActivated(() => {
         );
 
         &.is-light {
-            // background: linear-gradient(
-            //     180deg,
-            //     rgba(255, 255, 255, 0.08) 0%,
-            //     rgba(0, 0, 0, 0.06) 100%
-            // );
-
-            background: linear-gradient(180deg, #d2ebff 0%, #c2dcef 70%, #8ca1b2 100%)
-
+            background: linear-gradient(180deg, #d2ebff 0%, #c2dcef 70%, #8ca1b2 100%);
         }
     }
 

+ 1 - 11
src/views/old_mini/entry_information/components/baInformation.vue

@@ -51,10 +51,8 @@
         </div>
 
         <div class="custom-bottom-fixed-btns">
-            <div class="bottom-btn primary-btn" @click="handleNext">下一步 (1/3)</div>
+            <div class="bottom-btn primary-btn" @click="handleNext">下一步 (1/4)</div>
         </div>
-
-        <invite-entry-popup ref="invitePopupRef" />
     </div>
 </template>
 
@@ -63,11 +61,9 @@ import { nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref } from
 import { useRouter } from "vue-router";
 import { useStore } from "vuex";
 import SelectLocationMap from "../map/selectLocationMap.js";
-import inviteEntryPopup from "./inviteEntryPopup.vue";
 
 const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
 const FORM_KEY = "ENTRY_BA_FORM";
-const INVITE_POPUP_KEY = "ENTRY_INVITE_POPUP_SHOWN";
 const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
 
 const emit = defineEmits(["next"]);
@@ -76,7 +72,6 @@ const store = useStore();
 
 const formRef = ref(null);
 const previewMapRef = ref(null);
-const invitePopupRef = ref(null);
 let previewMap = null;
 const form = reactive({
     name: "",
@@ -202,11 +197,6 @@ const handleNext = async () => {
 onMounted(() => {
     restoreFormDraft();
     syncLocationFromSession();
-    // 首次进入本页弹出邀请录入弹窗
-    if (!sessionStorage.getItem(INVITE_POPUP_KEY)) {
-        sessionStorage.setItem(INVITE_POPUP_KEY, "1");
-        nextTick(() => invitePopupRef.value?.open());
-    }
 });
 
 onActivated(() => {

+ 3 - 1
src/views/old_mini/entry_information/components/inviteEntryPopup.vue

@@ -22,9 +22,10 @@
 
 <script setup>
 import { ref } from "vue";
-import { useRoute } from "vue-router";
+import { useRouter } from "vue-router";
 import { Popup } from "vant";
 
+const router = useRouter();
 const show = ref(false);
 
 const open = () => {
@@ -37,6 +38,7 @@ const close = () => {
 
 const handleConfirm = () => {
     close();
+    router.push("/entry_information");
 };
 
 defineExpose({ open, close });

+ 1 - 1
src/views/old_mini/entry_information/components/selectCategory.vue

@@ -58,7 +58,7 @@
 
         <div class="custom-bottom-fixed-btns">
             <div class="bottom-btn secondary-btn" @click="emit('prev')">上一步</div>
-            <div class="bottom-btn primary-btn" @click="handleNext">下一步 (2/3)</div>
+            <div class="bottom-btn primary-btn" @click="handleNext">下一步 (2/4)</div>
         </div>
     </div>
 </template>

+ 522 - 0
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -0,0 +1,522 @@
+<template>
+    <div class="select-equipment">
+        <div class="select-equipment__content">
+            <div class="page-header">
+                <div class="page-title">请填写您的农场设备</div>
+                <div class="page-subtitle">完善设备信息,让农机调度更精准</div>
+            </div>
+
+            <div class="toolbar">
+                <div class="search-bar">
+                    <el-icon class="search-icon"><Search /></el-icon>
+                    <input
+                        v-model="searchKeyword"
+                        class="search-input"
+                        type="text"
+                        placeholder="请输入农机名称"
+                        @keyup.enter="handleSearch"
+                    />
+                    <div class="search-btn" @click="handleSearch">搜索</div>
+                </div>
+                <div class="add-btn" @click="handleAddMachine">+ 添加农机</div>
+            </div>
+
+            <div class="equipment-list" v-loading="loading">
+                <div v-for="group in displayGroups" :key="group.name" class="equipment-card">
+                    <div class="section-title">
+                        <span class="title-icon"></span>
+                        <span>{{ group.name }}</span>
+                    </div>
+                    <div class="tag-group">
+                        <div
+                            v-for="item in group.items"
+                            :key="item.id"
+                            class="tag-item"
+                            :class="{ selected: selectedIds.has(String(item.id)) }"
+                            @click="toggleSelect(item)"
+                        >
+                            <span class="text">{{ item.name }}</span>
+                        </div>
+                    </div>
+                </div>
+                <div v-if="!loading && !displayGroups.length" class="empty-tip">暂无匹配农机</div>
+            </div>
+        </div>
+
+        <div class="custom-bottom-fixed-btns">
+            <div class="btns-l">
+                <div class="bottom-btn secondary-btn" @click="emit('prev')">上一步</div>
+                <div class="bottom-btn secondary-btn" :class="{ disabled: submitting }" @click="handleSkip">
+                    跳过
+                </div>
+            </div>
+            <div class="bottom-btn primary-btn" :class="{ disabled: submitting }" @click="handleSubmit">
+                {{ submitting ? "提交中..." : "提交信息" }}
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { Search } from "@element-plus/icons-vue";
+
+const emit = defineEmits(["prev", "confirm"]);
+
+const EQUIPMENT_KEY = "ENTRY_SELECTED_EQUIPMENT";
+const SELECTED_LIST_KEY = "ENTRY_SELECTED_VARIETY_LIST";
+const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
+const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
+const EDIT_UID_KEY = "ENTRY_VARIETY_EDIT_UID";
+const STEP_KEY = "ENTRY_INFORMATION_STEP";
+
+const loading = ref(false);
+const submitting = ref(false);
+const searchKeyword = ref("");
+const appliedKeyword = ref("");
+const selectedIds = reactive(new Set());
+const equipmentGroups = ref([]);
+
+const DEFAULT_GROUPS = [
+    {
+        name: "水肥类",
+        items: [
+            { id: 101, name: "滴灌系统" },
+            { id: 102, name: "喷灌设备" },
+            { id: 103, name: "水肥一体机" },
+            { id: 104, name: "水泵机组" },
+            { id: 105, name: "施肥机" },
+            { id: 106, name: "过滤设备" },
+        ],
+    },
+    {
+        name: "类别一",
+        items: [
+            { id: 201, name: "拖拉机" },
+            { id: 202, name: "旋耕机" },
+            { id: 203, name: "开沟机" },
+            { id: 204, name: "植保机" },
+            { id: 205, name: "无人机" },
+            { id: 206, name: "运输车" },
+        ],
+    },
+    {
+        name: "类别二",
+        items: [
+            { id: 301, name: "修剪机" },
+            { id: 302, name: "采收机" },
+            { id: 303, name: "割草机" },
+            { id: 304, name: "粉碎机" },
+            { id: 305, name: "发电机" },
+            { id: 306, name: "其他设备" },
+        ],
+    },
+];
+
+const displayGroups = computed(() => {
+    const keyword = (appliedKeyword.value || "").trim();
+    if (!keyword) return equipmentGroups.value;
+    return equipmentGroups.value
+        .map((group) => ({
+            ...group,
+            items: group.items.filter((item) => item.name.includes(keyword)),
+        }))
+        .filter((group) => group.items.length);
+});
+
+const allEquipmentItems = computed(() =>
+    equipmentGroups.value.flatMap((group) => group.items)
+);
+
+function readJson(key) {
+    try {
+        const raw = sessionStorage.getItem(key);
+        return raw ? JSON.parse(raw) : null;
+    } catch {
+        return null;
+    }
+}
+
+function restoreSelection() {
+    const cached = readJson(EQUIPMENT_KEY);
+    if (!Array.isArray(cached)) return;
+    cached.forEach((item) => {
+        if (item?.id != null) selectedIds.add(String(item.id));
+    });
+}
+
+function saveSelectionDraft() {
+    const selected = allEquipmentItems.value.filter((item) =>
+        selectedIds.has(String(item.id))
+    );
+    sessionStorage.setItem(EQUIPMENT_KEY, JSON.stringify(selected));
+}
+
+function fetchEquipmentList() {
+    loading.value = true;
+    // 暂无农机列表接口,使用本地默认数据
+    equipmentGroups.value = DEFAULT_GROUPS;
+    loading.value = false;
+}
+
+const handleSearch = () => {
+    appliedKeyword.value = searchKeyword.value;
+};
+
+const handleAddMachine = () => {
+    ElMessage.info("添加农机功能即将开放");
+};
+
+const toggleSelect = (item) => {
+    const id = String(item.id);
+    if (selectedIds.has(id)) {
+        selectedIds.delete(id);
+    } else {
+        selectedIds.add(id);
+    }
+    saveSelectionDraft();
+};
+
+function getVarietyDraftList() {
+    const draft = readJson(SELECTED_LIST_KEY);
+    if (Array.isArray(draft)) return draft;
+    if (Array.isArray(draft?.list)) return draft.list;
+    return [];
+}
+
+function getPhenophaseLabel(item) {
+    return item.phenophase || item.phenologyName || String(item.phenologyId || "");
+}
+
+function buildPlotPayload(includeEquipment = true) {
+    const baForm = readJson("ENTRY_BA_FORM") || {};
+    const varietyList = getVarietyDraftList();
+    const equipmentList = includeEquipment
+        ? allEquipmentItems.value.filter((item) => selectedIds.has(String(item.id)))
+        : [];
+
+    return {
+        user_name: baForm.name,
+        tel: baForm.phone,
+        crops: varietyList.map((item) => ({
+            crop_type: item.categoryName,
+            crop_id: Number(item.categoryId),
+            variety_list: [Number(item.id)],
+            phenophase: getPhenophaseLabel(item),
+            start_time: item.startTime,
+            plant_area: Number(item.area),
+            point: item.location,
+        })),
+        farm_machines: equipmentList.map((item) => ({
+            id: item.id,
+            name: item.name,
+        })),
+    };
+}
+
+function clearEntrySession() {
+    sessionStorage.removeItem(SELECTED_LIST_KEY);
+    sessionStorage.removeItem(CATEGORY_SESSION_KEY);
+    sessionStorage.removeItem("ENTRY_BA_FORM");
+    sessionStorage.removeItem(LOCATION_KEY);
+    sessionStorage.removeItem(EDIT_UID_KEY);
+    sessionStorage.removeItem(EQUIPMENT_KEY);
+    sessionStorage.removeItem(STEP_KEY);
+}
+
+async function submitEntry(includeEquipment = true) {
+    if (submitting.value) return;
+    const varietyList = getVarietyDraftList();
+
+    if (!varietyList.length) {
+        ElMessage.warning("请先完善种植品种信息");
+        return;
+    }
+    const baForm = readJson("ENTRY_BA_FORM");
+    if (!baForm?.name || !baForm?.phone) {
+        ElMessage.warning("请先完善个人信息");
+        return;
+    }
+
+    submitting.value = true;
+    try {
+        const params = buildPlotPayload(includeEquipment);
+        console.log("entry submit params", params);
+        // const res = await VE_API.entry.addPlotInfo(params);
+        // if (res.code === 200) {
+        //     ElMessage.success(res.msg || "提交成功");
+        //     clearEntrySession();
+        //     emit("confirm", params);
+        // } else {
+        //     ElMessage.error(res.msg || "提交失败,请稍后再试");
+        // }
+        ElMessage.success("提交成功");
+        clearEntrySession();
+        emit("confirm", params);
+    } catch (error) {
+        console.error("entry submit failed", error);
+        ElMessage.error("提交失败");
+    } finally {
+        submitting.value = false;
+    }
+}
+
+const handleSkip = () => {
+    submitEntry(false);
+};
+
+const handleSubmit = () => {
+    submitEntry(true);
+};
+
+onMounted(() => {
+    restoreSelection();
+    fetchEquipmentList();
+});
+</script>
+
+<style lang="scss" scoped>
+.select-equipment {
+    flex: 1;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+
+    &__content {
+        flex: 1;
+        min-height: 0;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        padding: 8px 16px 80px;
+    }
+
+    .page-header {
+        padding: 8px 4px 16px;
+
+        .page-title {
+            font-size: 26px;
+            color: #005599;
+            font-family: "PangMenZhengDao";
+            line-height: 36px;
+        }
+
+        .page-subtitle {
+            margin-top: 4px;
+            font-size: 14px;
+            color: rgba(46, 46, 46, 0.4);
+            line-height: 20px;
+        }
+    }
+
+    .toolbar {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        margin-bottom: 12px;
+    }
+
+    .search-bar {
+        flex: 1;
+        min-width: 0;
+        display: flex;
+        align-items: center;
+        height: 36px;
+        padding: 0 12px;
+        background: rgba(255, 255, 255, 0.5);
+        border: 1px solid rgba(33, 153, 248, 0.5);
+        border-radius: 6px;
+        box-sizing: border-box;
+
+        .search-icon {
+            color: rgba(0, 0, 0, 0.35);
+            font-size: 16px;
+            margin-right: 6px;
+            flex-shrink: 0;
+        }
+
+        .search-input {
+            flex: 1;
+            min-width: 0;
+            border: none;
+            outline: none;
+            background: transparent;
+            font-size: 14px;
+            color: #333;
+
+            &::placeholder {
+                color: rgba(0, 0, 0, 0.3);
+            }
+        }
+
+        .search-btn {
+            flex-shrink: 0;
+            padding-left: 10px;
+            color: #2199f8;
+            font-size: 14px;
+            cursor: pointer;
+        }
+    }
+
+    .add-btn {
+        flex-shrink: 0;
+        height: 36px;
+        padding: 0 12px;
+        border-radius: 6px;
+        background: #2199f8;
+        color: #fff;
+        font-size: 14px;
+        line-height: 36px;
+        white-space: nowrap;
+        cursor: pointer;
+    }
+
+    .equipment-card {
+        background: #fff;
+        border-radius: 12px;
+        padding: 14px 12px 16px;
+        margin-bottom: 12px;
+    }
+
+    .section-title {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 4px;
+        font-size: 16px;
+        font-weight: 600;
+        color: #1a1a1a;
+
+        .title-icon {
+            position: relative;
+            width: 14px;
+            height: 14px;
+            flex-shrink: 0;
+
+            &::before,
+            &::after {
+                content: "";
+                position: absolute;
+                width: 10px;
+                height: 10px;
+                border-radius: 50%;
+            }
+
+            &::before {
+                left: 0;
+                top: 2px;
+                background: #2199f8;
+                opacity: 0.85;
+            }
+
+            &::after {
+                right: 0;
+                top: 0;
+                background: #7ec8ff;
+                opacity: 0.9;
+            }
+        }
+    }
+
+    .tag-group {
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        gap: 0 8px;
+        font-size: 14px;
+
+        .tag-item {
+            margin-top: 10px;
+            position: relative;
+            border-radius: 6px;
+            box-sizing: border-box;
+            height: 36px;
+            text-align: center;
+            line-height: 36px;
+            cursor: pointer;
+            color: #000;
+            background: #f3f4f6;
+            border: 1px solid transparent;
+
+            .text {
+                display: inline-block;
+                max-width: 100%;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                vertical-align: top;
+                padding: 0 6px;
+            }
+
+            &.selected {
+                border: 1px solid #2199f8;
+                background: #e8f5ff;
+                color: #2199f8;
+
+                &::after {
+                    content: "";
+                    position: absolute;
+                    z-index: 9;
+                    top: -1px;
+                    right: -1px;
+                    width: 18px;
+                    height: 14px;
+                    background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
+                }
+            }
+        }
+    }
+
+    .empty-tip {
+        padding: 40px 0;
+        text-align: center;
+        color: rgba(0, 0, 0, 0.35);
+        font-size: 14px;
+    }
+
+    .custom-bottom-fixed-btns {
+        display: flex;
+        justify-content: space-between;
+        align-items: baseline;
+        gap: 10px;
+        padding: 12px 12px 0;
+        height: 80px;
+        background: #fff;
+        box-sizing: border-box;
+        box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
+        .btns-l {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+        }
+
+        .bottom-btn {
+            padding: 0 30px;
+            height: 40px;
+            line-height: 40px;
+            box-sizing: border-box;
+            font-size: 14px;
+            border-radius: 25px;
+            text-align: center;
+            white-space: nowrap;
+
+            &.disabled {
+                opacity: 0.6;
+                pointer-events: none;
+            }
+        }
+
+        .secondary-btn {
+            padding: 0 26px;
+            color: #2199f8;
+            background: #fff;
+            border: 1px solid #2199f8;
+        }
+
+        .primary-btn {
+            background: #2199f8;
+            color: #fff;
+        }
+    }
+}
+</style>

+ 10 - 58
src/views/old_mini/entry_information/components/selectVariety.vue

@@ -188,9 +188,7 @@
 
         <div class="custom-bottom-fixed-btns">
             <div class="bottom-btn secondary-btn" @click="handlePrev">上一步</div>
-            <div class="bottom-btn primary-btn" :class="{ disabled: submitting }" @click="handleConfirm">
-                {{ submitting ? "提交中..." : "确定信息" }}
-            </div>
+            <div class="bottom-btn primary-btn" @click="handleConfirm">下一步 (3/4)</div>
         </div>
     </div>
 </template>
@@ -203,7 +201,7 @@ import { useRouter } from "vue-router";
 import { useStore } from "vuex";
 import SelectLocationMap, { POINT_ICON } from "../map/selectLocationMap.js";
 
-const emit = defineEmits(["prev", "confirm"]);
+const emit = defineEmits(["prev", "next"]);
 const router = useRouter();
 const store = useStore();
 
@@ -671,12 +669,6 @@ const getBaForm = () => {
     }
 };
 
-const getPhenophaseLabel = (item) => {
-    const options = getPhenologyOptions(item.categoryId);
-    const found = options.find((opt) => String(opt.id) === String(item.phenologyId));
-    return found?.name || String(item.phenologyId || "");
-};
-
 const validateSubmitData = () => {
     if (!categoryTabs.value.length) {
         ElMessage.warning("请先选择种植品类");
@@ -722,35 +714,7 @@ const validateSubmitData = () => {
     return true;
 };
 
-const buildPlotPayload = () => {
-    const baForm = getBaForm() || {};
-    return {
-        user_name: baForm.name,
-        tel: baForm.phone,
-        crops: selectedList.value.map((item) => ({
-            crop_type: item.categoryName,
-            crop_id: Number(item.categoryId),
-            variety_list: [Number(item.id)],
-            phenophase: getPhenophaseLabel(item),
-            start_time: item.startTime,
-            plant_area: Number(item.area),
-            point: item.location,
-        })),
-    };
-};
-
-const clearEntrySession = () => {
-    sessionStorage.removeItem(SELECTED_LIST_KEY);
-    sessionStorage.removeItem(CATEGORY_SESSION_KEY);
-    sessionStorage.removeItem("ENTRY_BA_FORM");
-    sessionStorage.removeItem(LOCATION_KEY);
-    sessionStorage.removeItem(EDIT_UID_KEY);
-    sessionStorage.removeItem("ENTRY_INFORMATION_STEP");
-};
-
-const submitting = ref(false);
-
-const handleConfirm = async () => {
+const handleConfirm = () => {
     const idx = categoryTabs.value.findIndex(
         (tab) => String(tab.id) === String(activeCategoryId.value)
     );
@@ -760,25 +724,13 @@ const handleConfirm = async () => {
         return;
     }
     if (!validateSubmitData()) return;
-    if (submitting.value) return;
-
-    submitting.value = true;
-    try {
-        const params = buildPlotPayload();
-        console.log('params', params);
-        // const res = await VE_API.entry.addPlotInfo(params);
-        // if (res.code === 200) {
-        //     ElMessage.success(res.msg || "提交成功");
-        //     clearEntrySession();
-        //     emit("confirm", params);
-        // } else {
-        //     ElMessage.error(res.msg || "提交失败,请稍后再试");
-        // }
-    } catch {
-        ElMessage.error("提交失败,请稍后再试");
-    } finally {
-        submitting.value = false;
-    }
+    selectedList.value.forEach((item) => {
+        const options = getPhenologyOptions(item.categoryId);
+        const found = options.find((opt) => String(opt.id) === String(item.phenologyId));
+        item.phenophase = found?.name || String(item.phenologyId || "");
+    });
+    saveSelectedListDraft();
+    emit("next");
 };
 </script>
 

+ 6 - 4
src/views/old_mini/entry_information/index.vue

@@ -4,7 +4,8 @@
         <div class="entry-information-body">
             <ba-information v-if="currentStep === 1" @next="handleNext" />
             <select-category v-else-if="currentStep === 2" @prev="handlePrev" @next="handleNext" />
-            <select-variety v-else-if="currentStep === 3" @prev="handlePrev" @confirm="handleConfirm" />
+            <select-variety v-else-if="currentStep === 3" @prev="handlePrev" @next="handleNext" />
+            <select-equipment v-else-if="currentStep === 4" @prev="handlePrev" @confirm="handleConfirm" />
         </div>
     </div>
 </template>
@@ -16,6 +17,7 @@ import customHeader from "@/components/customHeader.vue";
 import baInformation from "./components/baInformation.vue";
 import selectCategory from "./components/selectCategory.vue";
 import selectVariety from "./components/selectVariety.vue";
+import selectEquipment from "./components/selectEquipment.vue";
 
 const STEP_KEY = "ENTRY_INFORMATION_STEP";
 
@@ -23,7 +25,7 @@ const router = useRouter();
 
 function readStep() {
     const step = Number(sessionStorage.getItem(STEP_KEY));
-    return step >= 1 && step <= 3 ? step : 1;
+    return step >= 1 && step <= 4 ? step : 1;
 }
 
 const currentStep = ref(readStep());
@@ -37,7 +39,7 @@ watch(
 );
 
 const handleNext = () => {
-    if (currentStep.value < 3) {
+    if (currentStep.value < 4) {
         currentStep.value += 1;
     }
 };
@@ -49,7 +51,7 @@ const handlePrev = () => {
 };
 
 const handleConfirm = () => {
-    // router.replace("/growth_report");
+    router.replace("/growth_report");
 };
 </script>
 

+ 77 - 3
src/views/old_mini/growth_report/index.vue

@@ -73,19 +73,24 @@
             @height-change="handlePanelHeightChange"
         />
     </div>
+
+    <!-- 录入信息邀请弹窗:分享链接带 showInviteEntry 时展示 -->
+    <invite-entry-popup ref="invitePopupRef" />
 </template>
 
 <script setup>
 import { computed, nextTick, onActivated, onMounted, ref } from "vue";
-import { useRouter } from "vue-router";
+import { useRoute, useRouter } from "vue-router";
 import { useStore } from "vuex";
 import { LocationFilled } from "@element-plus/icons-vue";
 import { convertPointToArray } from "@/utils/index";
 import GrowthReportMap from "./growthReportMap.js";
 import CropAlertPanel from "./components/CropAlertPanel.vue";
+import inviteEntryPopup from "@/views/old_mini/entry_information/components/inviteEntryPopup.vue";
 import wx from "weixin-js-sdk";
 
 const router = useRouter();
+const route = useRoute();
 
 const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
 const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
@@ -116,6 +121,7 @@ const currentCropName = ref("水稻");
 const activeMenuKey = ref("hot-drought-2");
 const panelHeight = ref(280);
 const inviteBottom = computed(() => panelHeight.value);
+const invitePopupRef = ref(null);
 
 const stressMenuItems = [
     { key: "stress", line1: "胁迫", line2: "胁迫", icon: menuIcon },
@@ -222,8 +228,8 @@ const handleInvite = (item) => {
     const query = {
         askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
         shareText: "邀请您完善地块种植信息",
-        targetUrl: `entry_information`,
-        paramsPage: JSON.stringify({ inviteName }),
+        targetUrl: `growth_report`,
+        paramsPage: JSON.stringify({ inviteName, showInviteEntry: 1 }),
         imageUrl: 'https://birdseye-img.sysuimars.com/temp/field.png',
     };
     wx.miniProgram.navigateTo({
@@ -231,6 +237,72 @@ const handleInvite = (item) => {
     });
 }
 
+function isTruthyFlag(value) {
+    return value === 1 || value === "1" || value === true || value === "true";
+}
+
+function parseMaybeJson(value) {
+    if (!value) return null;
+    if (typeof value === "object") return value;
+    try {
+        return JSON.parse(value);
+    } catch {
+        return null;
+    }
+}
+
+function shouldShowInviteEntryPopup() {
+    if (isTruthyFlag(route.query.showInviteEntry)) return true;
+
+    const miniJson = parseMaybeJson(route.query.miniJson);
+    if (miniJson) {
+        if (isTruthyFlag(miniJson.showInviteEntry)) return true;
+        const nested = parseMaybeJson(miniJson.paramsPage);
+        if (isTruthyFlag(nested?.showInviteEntry)) return true;
+    }
+
+    const paramsPage = parseMaybeJson(route.query.paramsPage);
+    if (isTruthyFlag(paramsPage?.showInviteEntry)) return true;
+
+    return false;
+}
+
+function clearInviteEntryQuery() {
+    if (!shouldShowInviteEntryPopup()) return;
+    const newQuery = { ...(route.query || {}) };
+    delete newQuery.showInviteEntry;
+    if (newQuery.paramsPage) {
+        const paramsPage = parseMaybeJson(newQuery.paramsPage);
+        if (paramsPage && typeof paramsPage === "object") {
+            delete paramsPage.showInviteEntry;
+            newQuery.paramsPage = JSON.stringify(paramsPage);
+        }
+    }
+    if (newQuery.miniJson) {
+        const miniJson = parseMaybeJson(newQuery.miniJson);
+        if (miniJson && typeof miniJson === "object") {
+            delete miniJson.showInviteEntry;
+            if (miniJson.paramsPage) {
+                const nested = parseMaybeJson(miniJson.paramsPage);
+                if (nested && typeof nested === "object") {
+                    delete nested.showInviteEntry;
+                    miniJson.paramsPage = JSON.stringify(nested);
+                }
+            }
+            newQuery.miniJson = JSON.stringify(miniJson);
+        }
+    }
+    router.replace({ path: route.path, query: newQuery });
+}
+
+function tryOpenInviteEntryPopup() {
+    if (!shouldShowInviteEntryPopup()) return;
+    nextTick(() => {
+        invitePopupRef.value?.open();
+        clearInviteEntryQuery();
+    });
+}
+
 const handleSwitchCategory = (cropName) => {
     if (!cropName) return;
     currentCropName.value = cropName;
@@ -256,11 +328,13 @@ const handlePanelHeightChange = (height) => {
 onMounted(() => {
     getLocationName();
     initMap();
+    tryOpenInviteEntryPopup();
 });
 onActivated(() => {
     const applied = applySelectedLocation();
     if (!applied) getLocationName();
     initMap();
+    tryOpenInviteEntryPopup();
 });
 </script>
 

+ 21 - 0
src/views/old_mini/recordDetails/map/mapManage.js

@@ -455,6 +455,26 @@ class MapManage {
     // this.clickPointLayer.addFeature(point);
   }
 
+  showCenterMarker(coordinate, iconSrc) {
+    if (!this.kmap || !coordinate) return;
+    if (!this.centerMarkerLayer) {
+      const src = iconSrc || require("@/assets/img/home/garden-point.png");
+      this.centerMarkerLayer = new KMap.VectorLayer("centerMarkerLayer", 9998, {
+        style: () =>
+          new Style({
+            image: new Icon({
+              src,
+              scale: 0.45,
+              anchor: [0.5, 1],
+            }),
+          }),
+      });
+      this.kmap.addLayer(this.centerMarkerLayer.layer);
+    }
+    this.centerMarkerLayer.source.clear();
+    this.centerMarkerLayer.addFeature(new Feature(new Point(coordinate)));
+  }
+
   setMapPosition(center) {
     this.kmap.getView().animate({
       center,
@@ -761,6 +781,7 @@ class MapManage {
   destroyMap() {
     this.unbindGridClick();
     this.clearAllLayers();
+    this.centerMarkerLayer = null;
     if (this.kmap && typeof this.kmap.destroy === "function") {
       this.kmap.destroy();
     }

+ 22 - 0
src/views/old_mini/work_execute/index.vue

@@ -47,6 +47,7 @@
             </div>
         </div>
         <farm-calendar-popup v-model:show="showFarmCalendarPopup" @confirm="goCompleteFarmInfo" />
+        <survey-ask-popup v-model:show="showSurveyAskPopup" @confirm="handleSurveyConfirm" />
     </div>
 </template>
 
@@ -59,6 +60,7 @@ import IndexMap from "./index";
 import customCalendar from "./components/calendar.vue";
 import farmHeader from "@/components/farmHeader.vue";
 import farmCalendarPopup from "@/components/popup/farmCalendarPopup.vue";
+import surveyAskPopup from "@/components/popup/surveyAskPopup.vue";
 import { useI18n } from "@/i18n";
 
 const { t } = useI18n();
@@ -68,6 +70,8 @@ const indexMap = new IndexMap();
 const mapContainer = ref(null);
 const calendarRef = ref(null);
 const showFarmCalendarPopup = ref(false);
+/** 控制调查问卷弹窗显示 */
+const showSurveyAskPopup = ref(true);
 const tabBarHeight = computed(() => store.state.home.tabBarHeight);
 
 const formatDateYMD = (date = new Date()) =>
@@ -144,6 +148,11 @@ const goCompleteFarmInfo = () => {
     router.push("/entry_information");
 };
 
+const handleSurveyConfirm = () => {
+    showSurveyAskPopup.value = false;
+    router.push("/survey_entry");
+};
+
 const mapPoint = ref(null);
 
 const getMapMarkerList = () => taskList.value.filter((item) => item.wkt);
@@ -155,11 +164,24 @@ onMounted(() => {
             indexMap.initMap(mapPoint.value, mapContainer.value, true);
             indexMap.initData(getMapMarkerList(), "farmName", "wkt");
         }
+        // keepAlive 热更新后可能未重新 setup,这里再同步一次显示状态
+        if (showSurveyAskPopup.value) {
+            showSurveyAskPopup.value = false;
+            nextTick(() => {
+                showSurveyAskPopup.value = true;
+            });
+        }
     });
 });
 
 onActivated(() => {
     nextTick(() => {
+        if (showSurveyAskPopup.value) {
+            showSurveyAskPopup.value = false;
+            nextTick(() => {
+                showSurveyAskPopup.value = true;
+            });
+        }
         if (!indexMap.kmap) {
             if (mapContainer.value) {
                 mapPoint.value = store.state.home.miniUserLocationPoint || "POINT(113.614209 23.585836)";

+ 360 - 0
src/views/old_mini/work_execute/surveyEntry.vue

@@ -0,0 +1,360 @@
+<template>
+    <div class="survey-entry-page">
+        <custom-header :name="t('workExecute.surveyEntryTitle')" bgColor="#fff" />
+
+        <div class="survey-entry-body">
+            <div class="hero">
+                <div class="hero-l">
+                    <div class="hero__text">{{ t("workExecute.surveyEntryHint") }}</div>
+                    <div class="hero__text">{{ t("workExecute.surveyEntryHint2") }}</div>
+                </div>
+                <img class="hero__illust" src="@/assets/img/agricultural/enter.png" alt="" />
+            </div>
+
+            <div class="search-bar">
+                <el-icon class="search-bar__icon"><Search /></el-icon>
+                <input
+                    v-model="searchKeyword"
+                    class="search-bar__input"
+                    type="text"
+                    :placeholder="t('workExecute.surveySearchPlaceholder')"
+                    @keyup.enter="handleSearch"
+                />
+                <div class="search-bar__btn" @click="handleSearch">{{ t("workExecute.surveySearch") }}</div>
+            </div>
+
+            <div class="category-panel">
+                <div class="category-panel__title">{{ t("workExecute.surveySelectTitle") }}<span class="category-panel__hint">({{ t("workExecute.surveySelectHint") }})</span></div>
+                <div class="category-grid" v-loading="loading">
+                    <div
+                        v-for="item in displayList"
+                        :key="item.id"
+                        class="category-tag"
+                        :class="{ selected: selectedIds.has(String(item.id)) }"
+                        @click="toggleSelect(item)"
+                    >
+                        <span class="category-tag__text van-ellipsis">{{ item.name }}</span>
+                    </div>
+                    <div v-if="!loading && !displayList.length" class="empty-tip">
+                        {{ t("workExecute.surveyNoMatch") }}
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="survey-entry-footer">
+            <div class="submit-btn" @click="handleSubmit">{{ t("workExecute.surveySubmit") }}</div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { Search } from "@element-plus/icons-vue";
+import customHeader from "@/components/customHeader.vue";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+const router = useRouter();
+
+const SESSION_KEY = "SURVEY_SELECTED_WORK_TYPES";
+
+const loading = ref(false);
+const categoryList = ref([]);
+const searchKeyword = ref("");
+const appliedKeyword = ref("");
+const selectedIds = reactive(new Set());
+
+const DEFAULT_CATEGORIES = [
+    "施肥管理",
+    "病虫害防治",
+    "灌溉补水",
+    "修剪整枝",
+    "疏花疏果",
+    "土壤改良",
+    "除草清园",
+    "采收作业",
+    "套袋护果",
+    "授粉辅助",
+    "果园巡查",
+    "气象防护",
+    "营养诊断",
+    "物候监测",
+    "农机作业",
+    "药肥配送",
+    "无人机植保",
+    "产后处理",
+    "仓储管理",
+    "其他农事",
+    "标准施肥",
+    "标准防治",
+    "标准调节",
+    "机动农事",
+    "预警响应",
+    "恢复农事",
+    "异常处置",
+].map((name, index) => ({ id: index + 1, name }));
+
+const displayList = computed(() => {
+    const keyword = (appliedKeyword.value || "").trim();
+    if (!keyword) return categoryList.value;
+    return categoryList.value.filter((item) => item.name.includes(keyword));
+});
+
+const normalizeList = (list) => {
+    if (!Array.isArray(list)) return [];
+    return list
+        .map((item, index) => ({
+            id: item.id ?? item.type ?? item.code ?? index + 1,
+            name: item.name || item.type_name || item.label || "",
+        }))
+        .filter((item) => item.name);
+};
+
+const restoreSelection = () => {
+    try {
+        const raw = sessionStorage.getItem(SESSION_KEY);
+        if (!raw) return;
+        const ids = JSON.parse(raw).map((item) => String(item.id));
+        ids.forEach((id) => selectedIds.add(id));
+    } catch {
+        // ignore
+    }
+};
+
+const fetchCategories = async () => {
+    loading.value = true;
+    try {
+        categoryList.value = DEFAULT_CATEGORIES;
+        // const res = await VE_API.z_farm_work_record.getFarmWorkTypeList();
+        // const list = normalizeList(res?.data);
+        // categoryList.value = list.length ? list : DEFAULT_CATEGORIES;
+    } catch {
+        categoryList.value = DEFAULT_CATEGORIES;
+    } finally {
+        loading.value = false;
+    }
+};
+
+const handleSearch = () => {
+    appliedKeyword.value = searchKeyword.value;
+};
+
+const toggleSelect = (item) => {
+    const id = String(item.id);
+    if (selectedIds.has(id)) {
+        selectedIds.delete(id);
+    } else {
+        selectedIds.add(id);
+    }
+};
+
+const handleSubmit = () => {
+    const selected = categoryList.value.filter((item) => selectedIds.has(String(item.id)));
+    if (!selected.length) {
+        ElMessage.warning(t("workExecute.surveyPleaseSelect"));
+        return;
+    }
+    sessionStorage.setItem(SESSION_KEY, JSON.stringify(selected));
+    ElMessage.success(t("workExecute.surveySubmitSuccess"));
+    router.back();
+};
+
+onMounted(() => {
+    restoreSelection();
+    fetchCategories();
+});
+</script>
+
+<style lang="scss" scoped>
+.survey-entry-page {
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+    // background: linear-gradient(90deg, #57B5FF 0%, #BBE1FF 11.5%, #F6F6F6 100%);
+    background: linear-gradient(270deg, rgba(218, 195, 255, 0.59) 0%, #E6F2FF 0.01%, #8FC5FE 100%);
+
+    // background: linear-gradient(180deg, #d7ecff 0%, #eaf5ff 42%, #f5f9fc 100%);
+    overflow: hidden;
+}
+
+.survey-entry-body {
+    padding: 0 10px 10px 10px;
+    flex: 1;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.hero {
+    padding: 20px 0px;
+    position: relative;
+
+    &__text {
+        flex: 1;
+        min-width: 0;
+        font-size: 19px;
+        line-height: 24px;
+        color: #005599;
+        font-family: "PangMenZhengDao";
+    }
+
+    &__illust {
+        position: absolute;
+        top: 4px;
+        right: 0px;
+        width: 106px;
+        height: auto;
+        flex-shrink: 0;
+    }
+}
+
+.search-bar {
+    display: flex;
+    align-items: center;
+    height: 36px;
+    margin-bottom: 10px;
+    padding: 0 12px;
+    border-radius: 10px;
+    background: rgba(255, 255, 255, 0.5);
+    backdrop-filter: blur(4px);
+    border: 1px solid rgba(33, 153, 248, 0.5);
+    // box-shadow: 0 2px 8px rgba(33, 153, 248, 0.12);
+    box-sizing: border-box;
+
+    &__icon {
+        color: rgba(0, 0, 0, 0.35);
+        font-size: 16px;
+        margin-right: 6px;
+        flex-shrink: 0;
+    }
+
+    &__input {
+        flex: 1;
+        min-width: 0;
+        border: none;
+        outline: none;
+        background: transparent;
+        font-size: 14px;
+        color: #333;
+
+        &::placeholder {
+            color: rgba(0, 0, 0, 0.3);
+        }
+    }
+
+    &__btn {
+        flex-shrink: 0;
+        padding-left: 10px;
+        color: #2199f8;
+        font-size: 14px;
+        cursor: pointer;
+    }
+}
+
+.category-panel {
+    flex: 1;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    padding: 12px 12px 0;
+    border-radius: 10px;
+    background: #fff;
+    overflow: hidden;
+
+    &__title {
+        padding-bottom: 10px;
+        margin-bottom: 10px;
+        font-size: 16px;
+        font-weight: 500;
+        color: #222222;
+        border-bottom: 0.2px solid rgba(0, 0, 0, 0.1);
+    }
+
+    &__hint {
+        font-size: 14px;
+        color: rgba(34, 34, 34, 0.5);
+    }
+}
+
+.category-grid {
+    flex: 1;
+    min-height: 0;
+    overflow-y: auto;
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 10px;
+    align-content: start;
+    padding-bottom: 16px;
+}
+
+.category-tag {
+    position: relative;
+    height: 40px;
+    padding: 0 8px;
+    border-radius: 8px;
+    background: #f3f4f6;
+    border: 1px solid transparent;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-sizing: border-box;
+    cursor: pointer;
+
+    &__text {
+        max-width: 100%;
+        font-size: 14px;
+        color: #1a1a1a;
+        text-align: center;
+    }
+
+    &.selected {
+        background: #fff;
+        border-color: #2199f8;
+
+        .category-tag__text {
+            color: #2199f8;
+        }
+
+        &::after {
+            content: "";
+            position: absolute;
+            top: -1px;
+            right: -1px;
+            width: 18px;
+            height: 14px;
+            background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
+        }
+    }
+}
+
+.empty-tip {
+    grid-column: 1 / -1;
+    padding: 48px 0;
+    text-align: center;
+    color: rgba(0, 0, 0, 0.35);
+    font-size: 14px;
+}
+
+.survey-entry-footer {
+    flex-shrink: 0;
+    padding: 10px 16px 24px;
+    background: #fff;
+    box-shadow: 2px 2px 4.5px 0px rgba(0, 0, 0, 0.4);
+
+    .submit-btn {
+        margin: 0 auto;
+        width: fit-content;
+        padding: 0 30px;
+        height: 40px;
+        line-height: 40px;
+        text-align: center;
+        border-radius: 25px;
+        background: #2199f8;
+        color: #fff;
+    }
+}
+</style>