Selaa lähdekoodia

fix: 首页画范围,分区相册

lxf 12 tuntia sitten
vanhempi
commit
2438d49904

+ 3 - 0
src/components/farmHeader.vue

@@ -133,6 +133,9 @@ defineExpose({ loadFarmInfo });
         gap: 6px;
         min-width: 0;
         flex: 1;
+        :deep(.el-avatar) {
+            flex: none;
+        }
     }
 
     .farm-header__text {

+ 9 - 2
src/components/popup/restorePlantingPopup.vue

@@ -95,7 +95,7 @@
                     :class="{ disabled: submitting }"
                     @click="handleConfirm"
                 >
-                    <span v-if="submitting">
+                    <span v-if="submitting" class="restore-planting-popup__btn-loading">
                         <el-icon><Loading /></el-icon>
                     </span>
                     {{ questionType === "time" ? t("agriFile.confirmNow") : t("agriFile.submitInfo") }}
@@ -276,7 +276,7 @@ const applyPhenologyByKey = (key) => {
     const name = item.period_name || "";
     phenologyHighlight.value = name;
     phenologyQuestion.value = `${PHENOLOGY_PREFIX}${name} ?`;
-    phenologyImage.value = resolveImageUrl(item.url) || DEFAULT_PHENOLOGY_IMAGE;
+    phenologyImage.value = resolveImageUrl(item.pheno_pic_url) || DEFAULT_PHENOLOGY_IMAGE;
     selectedPeriodCode.value = "";
 };
 
@@ -666,6 +666,13 @@ defineExpose({
                 opacity: 0.6;
                 pointer-events: none;
             }
+
+            
+
+            &-btn-loading {
+                line-height: 16px;
+                margin-right: 4px;
+            }
         }
     }
 }

+ 63 - 0
src/utils/selectedFarm.js

@@ -0,0 +1,63 @@
+/** 与 farmHeader / allGarden 一致的选中农场缓存读写 */
+
+function 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;
+    }
+    if (farm.wkt && /^POINT\s*\(/i.test(String(farm.wkt).trim())) {
+        return farm.wkt;
+    }
+    return farm.geom_wkt || farm.farm_location || farm.wkt || "";
+}
+
+export function normalizeSelectedFarm(farm = {}) {
+    const id = farm.farm_id ?? farm.id;
+    const name = farm.farm_name || farm.name || "";
+    const wkt = resolveFarmPointWkt(farm);
+    // 各入口字段不统一:列表用 variety_name,切换页写 categoryLabel
+    const cropLabel =
+        farm.categoryLabel || farm.variety_name || farm.crop_name || farm.farm_variety || "";
+    return {
+        ...farm,
+        id,
+        farm_id: id,
+        name,
+        farm_name: name,
+        wkt,
+        rawAddress: farm.farm_address || farm.city || farm.rawAddress || "",
+        variety_name: farm.variety_name || cropLabel,
+        categoryLabel: farm.categoryLabel || cropLabel,
+    };
+}
+
+/** 写入当前选中农场;无有效 id 时返回 false */
+export function saveSelectedFarm(farm) {
+    const normalized = normalizeSelectedFarm(farm);
+    if (normalized.id == null || normalized.id === "") return false;
+    localStorage.setItem("selectedFarmId", String(normalized.id));
+    localStorage.setItem("selectedFarmName", normalized.name || "");
+    localStorage.setItem("selectedFarmPoint", normalized.wkt || "");
+    localStorage.setItem("selectedFarmData", JSON.stringify(normalized));
+    return true;
+}
+
+/**
+ * 新增农场成功后:请求 getFarms 列表,选中最后一项。
+ */
+export async function selectNewlyCreatedFarm({ userId } = {}) {
+    const uid = userId || localStorage.getItem("MINI_USER_ID");
+    if (!uid) return false;
+
+    try {
+        const res = await VE_API.questionnaire.getFarms({ id: uid });
+        const list = Array.isArray(res?.data) ? res.data : [];
+        const last = list.length ? list[list.length - 1] : null;
+        if (last) return saveSelectedFarm(last);
+    } catch (e) {
+        console.warn("[selectNewlyCreatedFarm] getFarms failed", e);
+    }
+    return false;
+}

+ 23 - 11
src/views/old_mini/agri_file/components/albumPanel.vue

@@ -120,14 +120,25 @@ function getSelectedFarmId() {
     return id != null && id !== "" ? Number(id) : null;
 }
 
-function parsePointCoordinates(pointWkt) {
-    if (!pointWkt || !/^POINT\s*\(/i.test(String(pointWkt).trim())) {
-        return null;
-    }
+/** 从分区 polygon/geom 取内部中心点,用于气泡定位 */
+function parsePolygonCenter(polygonWkt) {
+    if (!polygonWkt) return null;
+    const text = String(polygonWkt).trim();
+    if (!text || !/^(MULTI)?POLYGON\s*\(/i.test(text)) return null;
     try {
-        const coord = util.wktCastGeom(pointWkt).getFirstCoordinate();
-        if (!coord || coord.length < 2) return null;
-        return { longitude: coord[0], latitude: coord[1] };
+        const geom = util.wktCastGeom(text);
+        if (typeof geom.getInteriorPoint === "function") {
+            const coord = geom.getInteriorPoint().getCoordinates();
+            if (coord?.length >= 2) {
+                return { longitude: coord[0], latitude: coord[1] };
+            }
+        }
+        const extent = geom.getExtent();
+        if (!extent || !Number.isFinite(extent[0])) return null;
+        return {
+            longitude: (extent[0] + extent[2]) / 2,
+            latitude: (extent[1] + extent[3]) / 2,
+        };
     } catch {
         return null;
     }
@@ -142,7 +153,9 @@ function resolveZoneCoverUrl(path) {
 }
 
 function mapZoneItem(zone) {
-    const coords = parsePointCoordinates(zone?.point);
+    const polygon = zone.polygon || zone.geom || "";
+    const geom = zone.geom || zone.polygon || "";
+    const coords = parsePolygonCenter(polygon || geom);
     return {
         id: zone.zone_id,
         zoneId: zone.zone_id,
@@ -152,9 +165,8 @@ function mapZoneItem(zone) {
         cover: resolveZoneCoverUrl(zone.cover_url),
         longitude: coords?.longitude,
         latitude: coords?.latitude,
-        point: zone.point,
-        polygon: zone.polygon || zone.geom || "",
-        geom: zone.geom || zone.polygon || "",
+        polygon,
+        geom,
     };
 }
 

+ 139 - 17
src/views/old_mini/agri_file/components/patrolPanel.vue

@@ -1,5 +1,25 @@
 <template>
     <div class="patrol-panel">
+        <!-- 品种切换 -->
+        <div class="variety-bar">
+            <div class="variety-bar__tags">
+                <div
+                    v-for="item in zoneList"
+                    :key="item.zone_id"
+                    class="variety-bar__tag"
+                    :class="{ active: item.zone_id === selectedZoneId }"
+                    @click="handleSelectZone(item)"
+                >
+                    {{ item.zone_name }}
+                </div>
+            </div>
+            <div class="variety-bar__add" @click="goAddVariety">
+                <el-icon>
+                    <Plus />
+                </el-icon>
+                <span>新增品种</span>
+            </div>
+        </div>
         <div
             v-for="item in patrolList"
             :key="item.id"
@@ -63,6 +83,7 @@ import { onActivated, onMounted, ref } from "vue";
 import { useRouter } from "vue-router";
 import { Icon } from "vant";
 import { ElMessage } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
 import { useI18n } from "@/i18n";
 import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
 import albumUploadPopup from "./albumUploadPopup.vue";
@@ -75,6 +96,9 @@ const router = useRouter();
 const PATROL_ICON = require("@/assets/img/report/wh-icon.png");
 
 // ---------- 页面状态 ----------
+const zoneList = ref([]);
+const selectedZoneId = ref(null);
+const selectedZoneName = ref("");
 const patrolList = ref([
     {
         id: 1,
@@ -145,9 +169,14 @@ const getSelectedFarm = () => {
     }
 };
 
-const getSelectedZoneId = () => {
+const getSelectedFarmId = () => {
+    const stored = localStorage.getItem("selectedFarmId");
+    if (stored != null && stored !== "") {
+        return Number(stored);
+    }
     const farm = getSelectedFarm();
-    return farm.zone_id ?? farm.zoneId ?? null;
+    const id = farm.farm_id ?? farm.id;
+    return id != null && id !== "" ? Number(id) : null;
 };
 
 /** 清空第一条上的感知任务残留,回退为 mock 文案 */
@@ -159,7 +188,7 @@ const clearFirstPatrolInteract = () => {
         id: 1,
         level: t("agriFile.riskLevel2"),
         title: t("agriFile.patrolGrowthTitle"),
-        zone: t("agriFile.zoneOne"),
+        zone: selectedZoneName.value || t("agriFile.zoneOne"),
         subject: t("agriFile.patrolSubject"),
         issue: t("agriFile.patrolIssue"),
         desc: "",
@@ -170,8 +199,34 @@ const clearFirstPatrolInteract = () => {
 };
 
 // ---------- 业务逻辑:加载 ----------
+const applySelectedZone = (zone) => {
+    selectedZoneId.value = zone?.zone_id ?? null;
+    selectedZoneName.value = zone?.zone_name || "";
+};
+
+const fetchZones = async () => {
+    const farmId = getSelectedFarmId();
+    if (farmId == null) {
+        zoneList.value = [];
+        applySelectedZone(null);
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.getZones({ farm_id: farmId });
+        const zones = Array.isArray(res?.data?.zones) ? res.data.zones : [];
+        zoneList.value = zones;
+        // 优先沿用当前选中;否则默认第一个
+        const matched = zones.find((z) => z.zone_id === selectedZoneId.value);
+        applySelectedZone(matched || zones[0] || null);
+    } catch (e) {
+        console.warn("[patrolPanel] getZones failed", e);
+        zoneList.value = [];
+        applySelectedZone(null);
+    }
+};
+
 const fetchPatrolInteractTask = async () => {
-    const zoneId = getSelectedZoneId();
+    const zoneId = selectedZoneId.value;
     if (zoneId == null || zoneId === "") {
         clearFirstPatrolInteract();
         return;
@@ -200,16 +255,16 @@ const fetchPatrolInteractTask = async () => {
 
 const fetchCropArchive = async () => {
     const farm = getSelectedFarm();
-    const params = {
-        farm_id: farm.farm_id,
-        zone_id: farm.zone_id,
-        crop_code: farm.category_code,
-    };
-    if (!params) {
+    const zoneId = selectedZoneId.value;
+    if (zoneId == null || zoneId === "") {
         cropArchiveList.value = [];
         return;
     }
-    const { zone_name: zoneName, ...query } = params;
+    const query = {
+        farm_id: farm.farm_id ?? farm.id,
+        zone_id: zoneId,
+        crop_code: farm.category_code,
+    };
     try {
         const res = await VE_API.entry.getPhenoRecord(query);
         if (res?.code !== 200) {
@@ -217,7 +272,7 @@ const fetchCropArchive = async () => {
             return;
         }
         const list = Array.isArray(res.data) ? res.data : [];
-        const fallbackZone = zoneName || t("agriFile.zoneOne");
+        const fallbackZone = selectedZoneName.value || t("agriFile.zoneOne");
         cropArchiveList.value = list
             .map((item, index) => mapPhenoRecordToArchive(item, index, fallbackZone))
             .sort((a, b) => String(b.date).localeCompare(String(a.date)));
@@ -228,7 +283,7 @@ const fetchCropArchive = async () => {
 };
 
 const fillMockLabels = () => {
-    const albumName = t("agriFile.zoneOne");
+    const albumName = selectedZoneName.value || t("agriFile.zoneOne");
     patrolList.value = patrolList.value.map((item, index) => {
         // 第一条由外部感知任务接口填充,不覆盖
         if (index === 0 && item.interactCode) return item;
@@ -243,15 +298,37 @@ const fillMockLabels = () => {
     });
 };
 
-const loadPatrolData = () => {
+/** 按当前选中品种刷新巡园卡片与作物档案 */
+const reloadBySelectedZone = () => {
+    clearFirstPatrolInteract();
+    fillMockLabels();
+    fetchPatrolInteractTask();
+    fetchCropArchive();
+};
+
+const loadPatrolData = async () => {
     // 先清掉上一农场感知任务,避免 fillMockLabels 因 interactCode 保留旧数据
     clearFirstPatrolInteract();
+    await fetchZones();
     fillMockLabels();
     fetchPatrolInteractTask();
     fetchCropArchive();
 };
 
 // ---------- 事件处理 ----------
+const handleSelectZone = (zone) => {
+    if (!zone || zone.zone_id === selectedZoneId.value) return;
+    applySelectedZone(zone);
+    reloadBySelectedZone();
+};
+
+const goAddVariety = () => {
+    router.push({
+        path: "/add_zone",
+        query: { type: "region", isSuccessPopup: "0" },
+    });
+};
+
 const goGrowthTrack = (item) => {
     router.push({
         path: "/growth_track",
@@ -284,7 +361,7 @@ const handleShare = (item) => {
 };
 
 const handleUploadClick = (item) => {
-    uploadZoneId.value = item?.zoneId ?? item?.id ?? getSelectedZoneId() ?? "";
+    uploadZoneId.value = item?.zoneId ?? item?.id ?? selectedZoneId.value ?? "";
     showUploadPopup.value = true;
 };
 
@@ -299,9 +376,9 @@ const handleUploadConfirm = async ({ images, date } = {}) => {
         const res = await VE_API.questionnaire.uploadImages({
             cloud_url,
             farm_id: farm.farm_id ?? farm.id ?? "",
-            zone_id: uploadZoneId.value || getSelectedZoneId() || "",
+            zone_id: uploadZoneId.value || selectedZoneId.value || "",
             category_code: farm.category_code || "",
-            zone_name: farm.variety_name || "",
+            zone_name: selectedZoneName.value || farm.variety_name || "",
             create_time: date,
         });
         if (res?.code === 200) {
@@ -324,11 +401,56 @@ onActivated(loadPatrolData);
 
 defineExpose({
     fetchPatrolInteractTask,
+    fetchCropArchive,
+    loadPatrolData,
 });
 </script>
 
 <style lang="scss" scoped>
 .patrol-panel {
+    .variety-bar {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 8px;
+        margin-bottom: 10px;
+
+        &__tags {
+            display: flex;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 6px;
+            flex: 1;
+            min-width: 0;
+        }
+
+        &__tag {
+            padding: 4px 16px;
+            border-radius: 4px;
+            background: #fff;
+            color: #4e5969;
+            font-size: 14px;
+
+            &.active {
+                background: #2199f8;
+                color: #fff;
+            }
+        }
+
+        &__add {
+            flex: none;
+            display: flex;
+            align-items: center;
+            gap: 2px;
+            padding: 5px 10px;
+            border-radius: 4px;
+            border: 1px solid #2199f8;
+            background: rgba(33, 153, 248, 0.1);
+            color: #2199f8;
+            font-size: 14px;
+        }
+    }
+
     .tracking-item {
         width: 100%;
         margin-bottom: 16px;

+ 2 - 2
src/views/old_mini/agri_file/index.vue

@@ -255,9 +255,9 @@ const handleComplete = () => {
     showSuccessPopup.value = false;
 };
 
-/** 恢复种植提交成功后刷新巡园感知任务 */
+/** 恢复种植提交成功后刷新巡园卡片与作物档案 */
 const handleRestorePlantingSuccess = () => {
-    patrolPanelRef.value?.fetchPatrolInteractTask?.();
+    patrolPanelRef.value?.loadPatrolData?.();
 };
 
 const handleNavClick = (item) => {

+ 3 - 2
src/views/old_mini/agri_file/pages/addZone.vue

@@ -7,7 +7,7 @@
                 <location-search class="search-bar__search" :user-location="userLocation"
                     @change="handleLocationChange" />
             </div>
-            <div class="map-tip normal-text" v-show="isFarmDrawMode">请勾画 <span class="map-tip__highlight">桂味</span>
+            <div class="map-tip normal-text" v-show="isFarmDrawMode">请勾画 <span class="map-tip__highlight">{{ getFarmType }}</span>
                 的种植区域</div>
             <div class="map-tip" v-show="!isFarmDrawMode">{{ t("agriFile.selectZoneHint") }}</div>
             <div class="locate-btn" @click="handleLocate">
@@ -358,7 +358,8 @@ const handleFarmNameConfirm = async (regionName) => {
         polygon,
     };
     sessionStorage.setItem("FARM_RANGE_GEOMETRY", JSON.stringify(geometryData));
-
+// console.log("isSuccessPopupParam", isSuccessPopupParam.value);
+console.log("isSuccessPopupParam", isSuccessPopupParam.value, route.query);
     if (isSuccessPopupParam.value) {
         showDrawSuccessPopup.value = true;
         return;

+ 38 - 14
src/views/old_mini/agri_file/pages/albumMap.vue

@@ -77,14 +77,25 @@ function getSelectedFarmId() {
     }
 }
 
-function parsePointCoordinates(pointWkt) {
-    if (!pointWkt || !/^POINT\s*\(/i.test(String(pointWkt).trim())) {
-        return null;
-    }
+/** 从分区 polygon/geom 取内部中心点,用于气泡定位 */
+function parsePolygonCenter(polygonWkt) {
+    if (!polygonWkt) return null;
+    const text = String(polygonWkt).trim();
+    if (!text || !/^(MULTI)?POLYGON\s*\(/i.test(text)) return null;
     try {
-        const coord = util.wktCastGeom(pointWkt).getFirstCoordinate();
-        if (!coord || coord.length < 2) return null;
-        return { longitude: coord[0], latitude: coord[1] };
+        const geom = util.wktCastGeom(text);
+        if (typeof geom.getInteriorPoint === "function") {
+            const coord = geom.getInteriorPoint().getCoordinates();
+            if (coord?.length >= 2) {
+                return { longitude: coord[0], latitude: coord[1] };
+            }
+        }
+        const extent = geom.getExtent();
+        if (!extent || !Number.isFinite(extent[0])) return null;
+        return {
+            longitude: (extent[0] + extent[2]) / 2,
+            latitude: (extent[1] + extent[3]) / 2,
+        };
     } catch {
         return null;
     }
@@ -99,7 +110,9 @@ function resolveZoneCoverUrl(path) {
 }
 
 function mapZoneItem(zone) {
-    const coords = parsePointCoordinates(zone?.point);
+    const polygon = zone.polygon || zone.geom || "";
+    const geom = zone.geom || zone.polygon || "";
+    const coords = parsePolygonCenter(polygon || geom);
     return {
         id: zone.zone_id,
         zoneId: zone.zone_id,
@@ -109,7 +122,8 @@ function mapZoneItem(zone) {
         cover: resolveZoneCoverUrl(zone.cover_url),
         longitude: coords?.longitude,
         latitude: coords?.latitude,
-        point: zone.point,
+        polygon,
+        geom,
     };
 }
 
@@ -162,13 +176,23 @@ const bindZoneOverlays = () => {
     });
 };
 
+/** 用分区 polygon 画范围并 fit;无 polygon 时退回中心点 */
 const fitZonesOnMap = () => {
     const records = zoneList.value
-        .filter((z) => z.longitude != null && z.latitude != null)
-        .map((z) => ({
-            zone_name: "",
-            polygon: `POINT(${z.longitude} ${z.latitude})`,
-        }));
+        .map((z) => {
+            const polygon =
+                (typeof z.polygon === "string" && z.polygon.trim()) ||
+                (typeof z.geom === "string" && z.geom.trim()) ||
+                (z.longitude != null && z.latitude != null
+                    ? `POINT(${z.longitude} ${z.latitude})`
+                    : "");
+            if (!polygon) return null;
+            return {
+                zone_name: "",
+                polygon,
+            };
+        })
+        .filter(Boolean);
     if (!records.length) return;
     fileMap.setRecordPolygons(records, "album");
 };

+ 5 - 2
src/views/old_mini/create_farm/index.vue

@@ -112,6 +112,7 @@ import wx from "weixin-js-sdk";
 import { Popup } from "vant";
 import { ElMessage } from "element-plus";
 import { transformFromGCJToWGS, transformFromWGSToGCJ } from "@/utils/WSCoordinate.js";
+import { selectNewlyCreatedFarm } from "@/utils/selectedFarm";
 
 /** 与 selectCrop.vue 写入的 sessionStorage 键一致 */
 const SESSION_SELECT_CROP_KEY = "selectedCrop";
@@ -483,8 +484,10 @@ const submitForm = (formEl) => {
 
             isSubmitting.value = true;
             VE_API.farm.createFarm(queryParams)
-                .then(({ code }) => {
-                    if (code === 200) {
+                .then(async (res) => {
+                    if (res?.code === 200) {
+                        // 创建成功后拉列表,选中最后一项(新建农场)
+                        await selectNewlyCreatedFarm();
                         showPlantingReportPopup.value = true;
                         sessionStorage.removeItem(SESSION_SELECT_CROP_KEY);
                     }

+ 3 - 0
src/views/old_mini/entry_information/components/manualFarmingService.vue

@@ -98,6 +98,7 @@ import { ElMessage } from "element-plus";
 import { Search } from "@element-plus/icons-vue";
 import { Popup } from "vant";
 import tipPopup from "@/components/popup/tipPopup.vue";
+import { selectNewlyCreatedFarm } from "@/utils/selectedFarm";
 
 const TASK_KEY = "ENTRY_SELECTED_FARM_TASKS";
 const TASK_VIEW_KEY = "ENTRY_MANUAL_VIEW";
@@ -443,6 +444,8 @@ const handleSubmit = async () => {
         const res = Array.isArray(raw) ? raw[0] : raw;
         if (res?.code === 200) {
             pendingReportParams.value = buildReportParams();
+            // 新增成功后拉列表,选中最后一项
+            await selectNewlyCreatedFarm();
             clearEntrySession();
             showSuccessPopup.value = true;
         } else {

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

@@ -87,6 +87,7 @@ import { ElMessage } from "element-plus";
 import { Search } from "@element-plus/icons-vue";
 import addMachinePopup from "./addMachinePopup.vue";
 import tipPopup from "@/components/popup/tipPopup.vue";
+import { selectNewlyCreatedFarm } from "@/utils/selectedFarm";
 
 // ---------------------------------------------------------------------------
 // Props / Emits
@@ -621,6 +622,8 @@ async function submitEntry(includeEquipment = true) {
         const res = Array.isArray(raw) ? raw[0] : raw;
         if (res?.code === 200) {
             pendingReportParams.value = buildReportParams();
+            // 新增成功后拉列表,选中最后一项
+            await selectNewlyCreatedFarm();
             clearEntrySession();
             showSuccessPopup.value = true;
         } else {

+ 33 - 2
src/views/old_mini/growth_report/components/CropAlertPanel.vue

@@ -158,10 +158,36 @@ const guideVisible = ref(false);
 /** 防止切换农场时旧请求回写覆盖新数据 */
 let reloadToken = 0;
 
+/** 展示名:优先当前分区 zone_name,其次 props / 农场缓存(新建农场尚无分区时用) */
+const resolveCropNameDisplay = () => {
+    const zone = zoneList.value[activeIndex.value];
+    if (zone?.variety_name) return zone.variety_name;
+    const farm = getSelectedFarm();
+    return (
+        farm.variety_name ||
+        farm.crop_name ||
+        farm.farm_variety ||
+        ""
+    );
+};
+
+const syncCropNameDisplay = () => {
+    currentCropName.value = resolveCropNameDisplay();
+};
+
 watch(
     () => props.cropName,
-    (name) => {
-        if (name) currentCropName.value = name;
+    () => {
+        syncCropNameDisplay();
+    }
+);
+
+watch(
+    () => props.farmId,
+    (id, prev) => {
+        if (String(id ?? "") === String(prev ?? "")) return;
+        syncCropNameDisplay();
+        nextTick(() => reloadFarmAlerts());
     }
 );
 
@@ -192,10 +218,12 @@ const applySelectedZone = (index) => {
     if (!zone) {
         activeIndex.value = 0;
         selectedZoneId.value = null;
+        syncCropNameDisplay();
         return;
     }
     activeIndex.value = index;
     selectedZoneId.value = zone.zone_id ?? null;
+    syncCropNameDisplay();
 };
 
 const handleSelectCrop = (index) => {
@@ -359,6 +387,7 @@ const reloadFarmAlerts = async () => {
     activeIndex.value = 0;
     zoneList.value = [];
     cropOptions.value = [];
+    syncCropNameDisplay();
     await fetchZones(token);
     if (token !== reloadToken) return;
     await fetchAlertCards(token);
@@ -469,7 +498,9 @@ onMounted(() => {
 
 /** 进入 / 从切换农场返回:nextTick 等父组件 syncSelectedFarmId 落盘后再拉 */
 onActivated(() => {
+    syncCropNameDisplay();
     nextTick(() => {
+        syncCropNameDisplay();
         reloadFarmAlerts();
     });
 });

+ 84 - 1
src/views/old_mini/growth_report/growthReportMap.js

@@ -5,8 +5,9 @@ import Style from "ol/style/Style";
 import Icon from "ol/style/Icon";
 import Fill from "ol/style/Fill";
 import Stroke from "ol/style/Stroke";
-import { Point } from 'ol/geom';
+import { Point, Polygon } from "ol/geom";
 import Feature from "ol/Feature";
+import Overlay from "ol/Overlay";
 import WKT from "ol/format/WKT.js";
 import { reactive } from "vue";
 
@@ -14,6 +15,9 @@ export let mapLocation = reactive({
   data: null,
 });
 
+/** 无范围时中心示意方框边长(米) */
+const NO_RANGE_SIDE_METERS = 280;
+
 /**
  * @description 地图层对象
  */
@@ -22,6 +26,7 @@ class IndexMap {
     let that = this;
     let vectorStyle = new KMap.VectorStyle();
     this.vectorStyle = vectorStyle;
+    this.noRangeTipOverlay = null;
 
     // 位置图标
     this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
@@ -34,6 +39,19 @@ class IndexMap {
         });
       },
     });
+
+    // 无勾画范围:中心点示意正方形
+    this.noRangeGuideLayer = new KMap.VectorLayer("noRangeGuideLayer", 9998, {
+      style: () =>
+        new Style({
+          fill: new Fill({ color: "rgba(0, 0, 0, 0.28)" }),
+          stroke: new Stroke({
+            color: "rgba(255, 255, 255, 0.95)",
+            width: 1.5,
+            lineDash: [6, 4],
+          }),
+        }),
+    });
   }
 
   initMap(location, target) {
@@ -42,6 +60,7 @@ class IndexMap {
     this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22);
     let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png";
     this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2);
+    this.kmap.addLayer(this.noRangeGuideLayer.layer);
     this.kmap.addLayer(this.clickPointLayer.layer);
     this.setMapPoint(coordinate)
     // this.addMapSingerClick()
@@ -61,6 +80,70 @@ class IndexMap {
     let point = new Feature(new Point(coordinate))
     this.clickPointLayer.addFeature(point)
     mapLocation.data = coordinate
+    // 示意框跟着中心点走
+    if (this.noRangeGuideLayer?.source?.getFeatures?.()?.length) {
+      this.drawNoRangeSquare(coordinate);
+      this.updateNoRangeTipPosition(coordinate);
+    }
+  }
+
+  /** 以中心点生成正方形环坐标(EPSG:4326) */
+  buildSquareRing(center, sideLengthMeters = NO_RANGE_SIDE_METERS) {
+    const lng = Number(center[0]);
+    const lat = Number(center[1]);
+    const half = sideLengthMeters / 2;
+    const latDelta = half / 111000;
+    const lngDelta = half / (111000 * Math.cos((lat * Math.PI) / 180));
+    return [
+      [lng - lngDelta, lat + latDelta],
+      [lng + lngDelta, lat + latDelta],
+      [lng + lngDelta, lat - latDelta],
+      [lng - lngDelta, lat - latDelta],
+      [lng - lngDelta, lat + latDelta],
+    ];
+  }
+
+  drawNoRangeSquare(coordinate) {
+    if (!this.noRangeGuideLayer?.source || !coordinate?.length) return;
+    this.noRangeGuideLayer.source.clear();
+    const ring = this.buildSquareRing(coordinate);
+    const feature = new Feature(new Polygon([ring]));
+    this.noRangeGuideLayer.addFeature(feature);
+  }
+
+  updateNoRangeTipPosition(coordinate) {
+    if (!this.noRangeTipOverlay || !coordinate?.length) return;
+    const ring = this.buildSquareRing(coordinate);
+    // 方框底边中点略偏下,提示贴在方框下方
+    const tipCoord = [(ring[2][0] + ring[3][0]) / 2, ring[2][1] - (ring[0][1] - ring[2][1]) * 0.08];
+    this.noRangeTipOverlay.setPosition(tipCoord);
+  }
+
+  /** 无范围引导:中心示意方框 + 下方提示 Overlay */
+  showNoRangeGuide(coordinate, tipEl) {
+    if (!this.kmap?.map || !coordinate?.length) return;
+    this.drawNoRangeSquare(coordinate);
+    if (tipEl) {
+      if (!this.noRangeTipOverlay) {
+        this.noRangeTipOverlay = new Overlay({
+          element: tipEl,
+          positioning: "top-center",
+          offset: [0, 10],
+          stopEvent: true,
+        });
+        this.kmap.map.addOverlay(this.noRangeTipOverlay);
+      } else if (this.noRangeTipOverlay.getElement() !== tipEl) {
+        this.noRangeTipOverlay.setElement(tipEl);
+      }
+      this.updateNoRangeTipPosition(coordinate);
+    }
+  }
+
+  clearNoRangeGuide() {
+    this.noRangeGuideLayer?.source?.clear?.();
+    if (this.noRangeTipOverlay) {
+      this.noRangeTipOverlay.setPosition(undefined);
+    }
   }
 
   /** 当前地图点位(优先标记点,其次视图中心) */

+ 94 - 2
src/views/old_mini/growth_report/index.vue

@@ -2,6 +2,16 @@
     <div class="growth-report-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
         <div class="report-content">
             <div class="map-container" ref="mapContainer"></div>
+            <!-- Overlay 宿主:无范围时挂到地图方框下方 -->
+            <div
+                ref="noRangeTipRef"
+                class="no-range-tip"
+                :style="{ display: noFarmRange ? 'inline-flex' : 'none' }"
+                @click.stop="goAddZone('farm')"
+            >
+                <span>勾画区域,获地块级长势报告</span>
+                <el-icon class="no-range-tip__arrow"><ArrowRight /></el-icon>
+            </div>
         </div>
 
         <!-- 浮窗:长势互动 -->
@@ -111,7 +121,7 @@ import { computed, nextTick, onActivated, onMounted, ref } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { useStore } from "vuex";
 import { ElMessage } from "element-plus";
-import { Close, LocationFilled } from "@element-plus/icons-vue";
+import { ArrowRight, Close, LocationFilled } from "@element-plus/icons-vue";
 import { convertPointToArray } from "@/utils/index";
 import GrowthReportMap from "./growthReportMap.js";
 import CropAlertPanel from "./components/CropAlertPanel.vue";
@@ -138,14 +148,22 @@ const store = useStore();
 const tabBarHeight = computed(() => store.state.home.tabBarHeight);
 
 const mapContainer = ref(null);
+const noRangeTipRef = ref(null);
 const growthReportMap = new GrowthReportMap();
 const locationName = ref("");
+/** status≠1 时展示中心勾画引导 */
+const noFarmRange = ref(false);
 const selectedFarmId = ref(localStorage.getItem("selectedFarmId") || "");
 const currentCropName = computed(() => {
     // 依赖 selectedFarmId,切换农场后重新读缓存
     void selectedFarmId.value;
     const farm = getSelectedFarm();
-    return farm.categoryLabel || farm.variety_name || farm.crop_name || "";
+    return (
+        farm.variety_name ||
+        farm.crop_name ||
+        farm.farm_variety ||
+        ""
+    );
 });
 const activeMenuKey = ref("hot-drought-2");
 const panelHeight = ref(280);
@@ -184,6 +202,54 @@ const syncSelectedFarmId = () => {
     );
 };
 
+/** status=1 有范围;否则展示勾画引导 */
+async function fetchFarmRangeStatus() {
+    const farm = getSelectedFarm();
+    const farmId = selectedFarmId.value || farm.farm_id || farm.id;
+    const zoneId = getSelectedZoneId();
+    if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
+        noFarmRange.value = true;
+        await syncNoRangeGuide();
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.checkZoneRange({
+            farm_id: farmId,
+            zone_id: zoneId,
+        });
+        noFarmRange.value = Number(res?.data?.status) !== 1;
+    } catch (e) {
+        console.warn("[growth_report] checkZoneRange failed", e);
+        noFarmRange.value = true;
+    }
+    await syncNoRangeGuide();
+}
+
+/** 无范围时在地图中心点画示意方框 + 下方提示 */
+async function syncNoRangeGuide() {
+    await nextTick();
+    if (!growthReportMap.kmap) return;
+    if (!noFarmRange.value) {
+        growthReportMap.clearNoRangeGuide();
+        return;
+    }
+    const coordinate =
+        growthReportMap.getCurrentCoordinate() ||
+        convertPointToArray(currentMapPoint.value)?.map(Number);
+    if (!coordinate?.length) return;
+    growthReportMap.showNoRangeGuide(coordinate, noRangeTipRef.value);
+}
+
+const goAddZone = (type) => {
+    router.push({
+        path: "/add_zone",
+        query: {
+            type,
+            isSuccessPopup: type === "farm" ? "1" : "0",
+        },
+    });
+};
+
 const showInteractCard = ref(false);
 const interactQuestionText = ref("");
 const interactOptions = ref([]);
@@ -319,11 +385,13 @@ const initMap = async () => {
         if (coordinate?.length === 2) {
             growthReportMap.setMapPosition(coordinate);
         }
+        await syncNoRangeGuide();
         return;
     }
     const location = currentMapPoint.value || store.state.home.miniUserLocationPoint || DEFAULT_MAP_POINT;
     currentMapPoint.value = location;
     growthReportMap.initMap(location, mapContainer.value);
+    await syncNoRangeGuide();
 };
 
 function applySelectedLocation() {
@@ -509,6 +577,7 @@ onMounted(() => {
     syncMapFromSelectedFarm();
     getLocationName();
     initMap();
+    fetchFarmRangeStatus();
     fetchInteractTask();
     tryOpenInviteEntryPopup();
 });
@@ -517,6 +586,7 @@ onActivated(() => {
     const applied = applySelectedLocation();
     if (!applied) syncMapFromSelectedFarm();
     initMap();
+    fetchFarmRangeStatus();
     fetchInteractTask();
     tryOpenInviteEntryPopup();
 });
@@ -851,5 +921,27 @@ onActivated(() => {
             height: 100%;
         }
     }
+
+    .no-range-tip {
+        display: none;
+        align-items: center;
+        gap: 6px;
+        max-width: calc(100vw - 48px);
+        height: 36px;
+        padding: 0 14px;
+        border-radius: 18px;
+        background: rgba(40, 48, 36, 0.72);
+        backdrop-filter: blur(4px);
+        color: #fff;
+        font-size: 14px;
+        white-space: nowrap;
+        box-sizing: border-box;
+        cursor: pointer;
+
+        &__arrow {
+            font-size: 14px;
+            flex-shrink: 0;
+        }
+    }
 }
 </style>