Browse Source

fix: 新增农场

lxf 2 days ago
parent
commit
21e4f20d25

+ 23 - 11
src/components/allGarden.vue

@@ -52,11 +52,11 @@
 
             <div class="farm-list">
                 <div
-                    v-for="(item, index) in filteredFarmList"
-                    :key="index"
+                    v-for="item in filteredFarmList"
+                    :key="item.id"
                     class="farm-card"
-                    @click.stop="handleEnterFarm(item, index)"
-                    :class="{ 'is-current': currentIndex === index }"
+                    @click.stop="handleEnterFarm(item)"
+                    :class="{ 'is-current': item.isCurrent }"
                 >
                     <div class="farm-card__header">
                         <div class="farm-card__main">
@@ -69,7 +69,7 @@
                             </div>
                         </div>
                         <div
-                            v-if="currentIndex === index"
+                            v-if="item.isCurrent"
                             class="action-btn action-btn-current"
                         >
                             {{ t("garden.currentFarm") }}
@@ -77,7 +77,7 @@
                         <div
                             v-else
                             class="action-btn action-btn-enter"
-                            @click.stop="handleEnterFarm(item, index)"
+                            @click.stop="handleEnterFarm(item)"
                         >
                             {{ t("garden.enterFarm") }}
                         </div>
@@ -186,8 +186,22 @@ function resolveFarmPointWkt(farm) {
     return farm.geom_wkt || "";
 }
 
+function getSelectedFarmId() {
+    const stored = localStorage.getItem("selectedFarmId");
+    if (stored != null && stored !== "") {
+        return Number(stored);
+    }
+    try {
+        const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+        const id = farm.farm_id ?? farm.id;
+        return id != null && id !== "" ? Number(id) : null;
+    } catch {
+        return null;
+    }
+}
+
 function normalizeFarmList(data) {
-    const selectedId = localStorage.getItem("selectedFarmId");
+    const selectedId = getSelectedFarmId();
     return (data || []).map((farm) => {
         const id = farm.farm_id;
         return {
@@ -201,7 +215,7 @@ function normalizeFarmList(data) {
             phenologyText: farm.period_name || "",
             taskName: farm.fw_name || "",
             taskStatus: farm.work_status_name || farm.work_status || "",
-            isCurrent: selectedId != null && Number(id) === Number(selectedId),
+            isCurrent: selectedId != null && Number(id) === selectedId,
         };
     });
 }
@@ -259,9 +273,7 @@ function getFarmList() {
         });
 }
 
-const currentIndex = ref(null);
-function handleEnterFarm(item, index) {
-    currentIndex.value = index;
+function handleEnterFarm(item) {
     saveSelectedFarm(item);
     ElMessage.success(t("garden.enterFarmSuccess"));
     router.back();

+ 182 - 18
src/views/old_mini/agri_file/components/addZoneInfoPopup.vue

@@ -93,8 +93,12 @@
                 />
             </div>
 
-            <div class="add-zone-info-popup__confirm" @click="handleConfirm">
-                {{ t("agriFile.confirmArea") }}
+            <div
+                class="add-zone-info-popup__confirm"
+                :class="{ disabled: submitting }"
+                @click="handleConfirm"
+            >
+                {{ submitting ? "提交中..." : t("agriFile.confirmArea") }}
             </div>
         </div>
     </popup>
@@ -124,6 +128,11 @@ const props = defineProps({
         type: [String, Number],
         default: "",
     },
+    /** 选点页确认的种植点位 WKT,如 POINT(lng lat) */
+    pointLocation: {
+        type: String,
+        default: "",
+    },
 });
 
 const emit = defineEmits(["update:show", "confirm"]);
@@ -144,6 +153,7 @@ const cropMeta = reactive({
 
 const categoryLoading = ref(false);
 const varietyLoading = ref(false);
+const submitting = ref(false);
 const categoryOptions = ref([]);
 const varietyOptions = ref([]);
 const phenologyOptions = ref([]);
@@ -174,9 +184,80 @@ const resolveCountyCode = () => {
     return DEFAULT_COUNTY_CODE;
 };
 
+function getSelectedFarm() {
+    try {
+        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+    } catch {
+        return {};
+    }
+}
+
+/** 与 farmHeader / allGarden 保持一致的选中农场缓存 */
+function saveSelectedFarm(farm) {
+    const id = farm?.farm_id ?? farm?.id;
+    if (id == null || id === "") return;
+    const name = farm.farm_name || farm.name || "";
+    const wkt =
+        farm.wkt || farm.geom_wkt || farm.farm_location || farm.point_location || "";
+    const normalized = {
+        ...farm,
+        id,
+        farm_id: id,
+        name,
+        farm_name: name,
+        wkt,
+    };
+    localStorage.setItem("selectedFarmId", String(id));
+    localStorage.setItem("selectedFarmName", name);
+    localStorage.setItem("selectedFarmPoint", wkt);
+    localStorage.setItem("selectedFarmData", JSON.stringify(normalized));
+}
+
+/** 用创建结果刷新「当前选中农场」缓存 */
+function applyCreatedFarmToCache({ created, payload, extras }) {
+    const prev = getSelectedFarm();
+    const createdData = created && typeof created === "object" ? created : {};
+    const id =
+        createdData.farm_id ??
+        createdData.id ??
+        payload.farm_id ??
+        prev.farm_id ??
+        prev.id;
+    if (id == null || id === "") return;
+
+    saveSelectedFarm({
+        ...prev,
+        ...createdData,
+        farm_id: id,
+        id,
+        farm_name:
+            createdData.farm_name ||
+            createdData.name ||
+            extras.zoneName ||
+            prev.farm_name ||
+            prev.name ||
+            "",
+        name:
+            createdData.farm_name ||
+            createdData.name ||
+            extras.zoneName ||
+            prev.name ||
+            prev.farm_name ||
+            "",
+        farm_location: payload.point_location || prev.farm_location || prev.wkt || "",
+        geom_wkt: payload.point_location || prev.geom_wkt || prev.wkt || "",
+        wkt: payload.point_location || prev.wkt || prev.geom_wkt || "",
+        variety_name: extras.varietyName || prev.variety_name || "",
+        period_name: extras.phenologyName || prev.period_name || "",
+        category_code: payload.crop_code || prev.category_code || "",
+        zone_id: createdData.zone_id ?? createdData.zoneId ?? prev.zone_id,
+        zone_name: extras.zoneName || prev.zone_name || "",
+    });
+}
+
 function getDefaultPoint() {
     try {
-        const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+        const farm = getSelectedFarm();
         const wkt = farm.wkt || farm.geom_wkt || farm.farm_location;
         if (typeof wkt === "string" && /^POINT\s*\(/i.test(wkt.trim())) return wkt.trim();
     } catch {
@@ -234,15 +315,45 @@ function applyInitialInteract(data) {
     if (!stillExists) form.phenologyId = "";
 }
 
+/** 补齐 crop_code / crop_group,与 selectVariety 一致 */
+async function ensureCropMeta(cropId) {
+    const category = categoryOptions.value.find(
+        (item) => String(item.id) === String(cropId)
+    );
+    if (category?.code && !cropMeta.crop_code) {
+        cropMeta.crop_code = category.code;
+    }
+    if ((category?.firstCrop || category?.code) && !cropMeta.crop_group) {
+        cropMeta.crop_group = category.firstCrop || "";
+    }
+    if (cropMeta.crop_code || cropId == null || cropId === "") return;
+
+    try {
+        const detailRes = await VE_API.entry.getMachines({ crop_id: cropId });
+        const detail = detailRes?.data || {};
+        if (detailRes?.code === 200) {
+            if (detail.crop_code) cropMeta.crop_code = detail.crop_code;
+            if (detail.crop_group) {
+                cropMeta.crop_group = cropMeta.crop_group || detail.crop_group;
+            }
+        }
+    } catch {
+        // ignore
+    }
+}
+
 async function fetchInitialInteractOptionsWithoutZone() {
     const category = categoryOptions.value.find(
         (item) => String(item.id) === String(form.categoryId)
     );
     if (!category) return;
 
+    await ensureCropMeta(form.categoryId);
+
+    const farm = getSelectedFarm();
     const params = {
-        crop_big_type: cropMeta.crop_group || category.firstCrop || "",
-        crop_code: cropMeta.crop_code || category.code || "",
+        crop_big_type: farm.crop_big_type || farm.crop_group || "",
+        crop_code: farm.category_code || "",
         crop_maturing: "P0",
         location: getDefaultPoint(),
         date: form.plantDate || new Date().toISOString().split("T")[0],
@@ -355,6 +466,7 @@ const fetchVarietyOptions = async (cropId) => {
             varietyOptions.value = res.data.varieties;
             cropMeta.crop_code = res.data.crop_code || "";
             cropMeta.crop_group = res.data.crop_group || "";
+            await ensureCropMeta(cropId);
             await fetchInitialInteractOptionsWithoutZone();
         } else {
             varietyOptions.value = [];
@@ -426,7 +538,8 @@ const handleVarietyChange = (variety) => {
     applyAutoZoneName(name);
 };
 
-const handleConfirm = () => {
+const handleConfirm = async () => {
+    if (submitting.value) return;
     if (!form.categoryId) {
         ElMessage.warning(t("agriFile.pleaseSelectCategory"));
         return;
@@ -449,6 +562,18 @@ const handleConfirm = () => {
         return;
     }
 
+    const farm = getSelectedFarm();
+    const farmId = farm.farm_id ?? farm.id;
+    const pointLocation = props.pointLocation || getDefaultPoint();
+    if (farmId == null || farmId === "") {
+        ElMessage.warning("请先选择农场");
+        return;
+    }
+    if (!pointLocation) {
+        ElMessage.warning("请先选择种植点位");
+        return;
+    }
+
     const category = categoryOptions.value.find(
         (item) => String(item.id) === String(form.categoryId)
     );
@@ -458,19 +583,53 @@ const handleConfirm = () => {
     const phenology = phenologyOptions.value.find(
         (item) => String(item.id) === String(form.phenologyId)
     );
-
-    emit("confirm", {
+    const varietyName = variety?.name || form.varietyId || "";
+    const phenophaseCode = phenology?.code || String(form.phenologyId || "");
+
+    const payload = {
+        farm_id: farmId,
+        point_location: pointLocation,
+        crop_code: farm.category_code || "",
+        variety_list: varietyName,
+        start_time: form.plantDate,
         zone_name: zoneName,
-        category_id: form.categoryId,
-        category_name: category?.name || "",
-        variety_id: form.varietyId,
-        variety_name: variety?.name || form.varietyId || "",
-        phenology_id: form.phenologyId,
-        phenology_name: phenology?.name || "",
-        phenology_code: phenology?.code || String(form.phenologyId || ""),
-        plant_date: form.plantDate,
-    });
-    emit("update:show", false);
+        phenophase_code: phenophaseCode,
+    };
+
+    submitting.value = true;
+    try {
+        const res = await VE_API.questionnaire.createZone(payload);
+        if (res?.code === 200) {
+            ElMessage.success(res.msg || t("agriFile.createZoneSuccess"));
+            applyCreatedFarmToCache({
+                created: res?.data,
+                payload,
+                extras: {
+                    zoneName,
+                    varietyName,
+                    phenologyName: phenology?.name || "",
+                },
+            });
+            emit("confirm", {
+                ...payload,
+                category_id: form.categoryId,
+                category_name: category?.name || "",
+                variety_id: form.varietyId,
+                variety_name: varietyName,
+                phenology_id: form.phenologyId,
+                phenology_name: phenology?.name || "",
+                phenology_code: phenophaseCode,
+                plant_date: form.plantDate,
+            });
+            emit("update:show", false);
+            return;
+        }
+        ElMessage.error(res?.msg || "创建分区失败,请稍后再试");
+    } catch {
+        ElMessage.error("创建分区失败,请稍后再试");
+    } finally {
+        submitting.value = false;
+    }
 };
 </script>
 
@@ -554,6 +713,11 @@ const handleConfirm = () => {
         color: #fff;
         font-size: 16px;
         text-align: center;
+
+        &.disabled {
+            opacity: 0.6;
+            pointer-events: none;
+        }
     }
 }
 </style>

+ 17 - 3
src/views/old_mini/agri_file/components/albumPanel.vue

@@ -23,7 +23,7 @@
                 <div class="zone-marker__label">{{ item.name }}</div>
                 <div class="zone-marker__thumb">
                     <img :src="item.cover" alt="" />
-                    <span class="zone-marker__badge">{{ item.count }}</span>
+                    <span v-if="item.count" class="zone-marker__badge">{{ item.count }}</span>
                 </div>
                 <div class="zone-marker__arrow"></div>
             </div>
@@ -87,13 +87,14 @@ const { t } = useI18n();
 const router = useRouter();
 
 // ---------- 常量 ----------
-// const defaultCover = require("@/assets/img/home/banner.png");
-const defaultCover = "https://birdseye-img-ali-cdn.sysuimars.com/birdseye-look-mini/25862/1751598966682.png";
+const defaultCover = require("@/assets/img/agricultural/photo.png");
+// const defaultCover = "https://birdseye-img-ali-cdn.sysuimars.com/birdseye-look-mini/25862/1751598966682.png";
 const DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
 
 // ---------- 页面状态 ----------
 const mapContainer = ref(null);
 const fileMap = new FileMap();
+fileMap.fitPadding = [68, 50, 0, 50];
 const markerEls = {};
 const mapOverlays = [];
 const noFarmRange = ref(false);
@@ -200,6 +201,18 @@ const bindZoneOverlays = () => {
     });
 };
 
+/** 用区域点位写入矢量层并 fit,才会走到实例上的 fitPadding */
+const fitZonesOnMap = () => {
+    const records = zoneList.value
+        .filter((z) => z.longitude != null && z.latitude != null)
+        .map((z) => ({
+            zone_name: "",
+            polygon: `POINT(${z.longitude} ${z.latitude})`,
+        }));
+    if (!records.length) return;
+    fileMap.setRecordPolygons(records, "album");
+};
+
 async function fetchZoneList() {
     const farmId = getSelectedFarmId();
     if (!farmId) {
@@ -234,6 +247,7 @@ const initAlbumMap = async () => {
     await nextTick();
     bindZoneOverlays();
     fileMap.kmap?.map?.updateSize?.();
+    fitZonesOnMap();
 };
 
 const getFarmMapCenter = () => {

+ 19 - 15
src/views/old_mini/agri_file/fileMap.js

@@ -169,17 +169,21 @@ function createPhotoMarkerStyle(img, count) {
     ctx.closePath();
     ctx.fillStyle = "#ffffff";
     ctx.fill();
-
-    const bx = thumbX + thumb - badgeW + 4;
-    const by = thumbY - 6;
-    roundRectPath(ctx, bx, by, badgeW, badgeH, 8);
-    ctx.fillStyle = "#ffffff";
-    ctx.fill();
-    ctx.fillStyle = "#1F1F1F";
-    ctx.font = "10px sans-serif";
-    ctx.textAlign = "center";
-    ctx.textBaseline = "middle";
-    ctx.fillText(countText, bx + badgeW / 2, by + badgeH / 2 + 0.5);
+    console.log('count, ', count, countText);
+
+    // count 为 0 / 空时不绘制数量角标
+    if (Number(count) > 0) {
+        const bx = thumbX + thumb - badgeW + 4;
+        const by = thumbY - 6;
+        roundRectPath(ctx, bx, by, badgeW, badgeH, 8);
+        ctx.fillStyle = "#ffffff";
+        ctx.fill();
+        ctx.fillStyle = "#1F1F1F";
+        ctx.font = "10px sans-serif";
+        ctx.textAlign = "center";
+        ctx.textBaseline = "middle";
+        ctx.fillText(countText, bx + badgeW / 2, by + badgeH / 2 + 0.5);
+    }
 
     return new Style({
         image: iconFromCanvas(canvas, dpr, [0.5, 1]),
@@ -498,11 +502,11 @@ class FileMap {
             size,
             duration: 0,
             padding,
-            maxZoom: 19,
+            // maxZoom: 22,
         });
-        if ((view.getZoom() ?? 0) < 16) {
-            view.setZoom(16);
-        }
+        // if ((view.getZoom() ?? 0) < 16) {
+        //     view.setZoom(16);
+        // }
     }
 }
 

+ 201 - 103
src/views/old_mini/agri_file/pages/albumMap.vue

@@ -13,26 +13,48 @@
             <div class="locate-btn" @click="handleLocate">
                 <img class="locate-btn__icon" src="@/assets/img/map/map-icon.png" alt="" />
             </div>
+            <div
+                v-for="item in zoneList"
+                :key="item.id"
+                :ref="(el) => setMarkerEl(item.id, el)"
+                class="zone-marker"
+            >
+                <div class="zone-marker__label">{{ item.name }}</div>
+                <div class="zone-marker__thumb">
+                    <img :src="item.cover" alt="" />
+                    <span v-if="item.count" class="zone-marker__badge">{{ item.count }}</span>
+                </div>
+                <div class="zone-marker__arrow"></div>
+            </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import { nextTick, onActivated, ref } from "vue";
+import { nextTick, onActivated, onBeforeUnmount, onMounted, ref } from "vue";
 import { useStore } from "vuex";
+import Overlay from "ol/Overlay";
 import customHeader from "@/components/customHeader.vue";
 import locationSearch from "@/components/pageComponents/locationSearch.vue";
 import FileMap from "../fileMap";
 import * as util from "@/common/ol_common.js";
+import { base_img_url2 } from "@/api/config";
 import { useI18n } from "@/i18n";
 
 const { t } = useI18n();
 const store = useStore();
+
+// ---------- 常量 ----------
+const defaultCover = require("@/assets/img/agricultural/photo.png");
+const DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
+
+// ---------- 页面状态 ----------
 const mapContainer = ref(null);
 const fileMap = new FileMap();
-
-const defaultCover = require("@/assets/img/home/banner.png");
-const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
+fileMap.fitPadding = [68, 50, 50, 50];
+const markerEls = {};
+const mapOverlays = [];
+const zoneList = ref([]);
 
 const userLocation = ref(
     store.state.home.miniUserLocation ||
@@ -40,125 +62,146 @@ const userLocation = ref(
         "113.61702297075017,23.584863449735067"
 );
 
-const MOCK_ZONES = [
-    { id: 1, nameKey: "agriFile.zoneOne", dlng: -0.00115, dlat: 0.00055, delta: 0.00155 },
-    { id: 2, nameKey: "agriFile.zoneTwo", dlng: 0.00105, dlat: -0.00095, delta: 0.00128 },
-];
-
-const MOCK_PHOTOS = [
-    { id: 1, dlng: -0.00185, dlat: 0.00115 },
-    { id: 2, dlng: 0.00155, dlat: 0.00075 },
-    { id: 3, dlng: -0.00055, dlat: -0.00135 },
-    { id: 4, dlng: 0.00135, dlat: -0.00075 },
-    { id: 5, dlng: -0.00235, dlat: -0.00015 },
-    { id: 6, dlng: 0.00015, dlat: 0.00155 },
-    { id: 7, dlng: 0.00255, dlat: -0.00135 },
-    { id: 8, dlng: -0.00015, dlat: -0.00195 },
-];
-
-function squarePolygonWkt(lng, lat, delta = 0.0014) {
-    const ring = [
-        [lng - delta, lat + delta * 0.7],
-        [lng + delta * 0.35, lat + delta],
-        [lng + delta, lat - delta * 0.2],
-        [lng + delta * 0.15, lat - delta],
-        [lng - delta * 0.85, lat - delta * 0.45],
-        [lng - delta, lat + delta * 0.7],
-    ];
-    return `POLYGON((${ring.map((point) => point.join(" ")).join(", ")}))`;
+// ---------- 工具函数 ----------
+function getSelectedFarmId() {
+    const stored = localStorage.getItem("selectedFarmId");
+    if (stored != null && stored !== "") {
+        return Number(stored);
+    }
+    try {
+        const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+        const id = farm.farm_id ?? farm.id;
+        return id != null && id !== "" ? Number(id) : null;
+    } catch {
+        return null;
+    }
 }
 
-function getFarmData() {
+function parsePointCoordinates(pointWkt) {
+    if (!pointWkt || !/^POINT\s*\(/i.test(String(pointWkt).trim())) {
+        return null;
+    }
     try {
-        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+        const coord = util.wktCastGeom(pointWkt).getFirstCoordinate();
+        if (!coord || coord.length < 2) return null;
+        return { longitude: coord[0], latitude: coord[1] };
     } catch {
-        return {};
+        return null;
     }
 }
 
-function isPointWkt(value) {
-    return typeof value === "string" && /^POINT\s*\(/i.test(value.trim());
+function resolveZoneCoverUrl(path) {
+    if (!path) return defaultCover;
+    const text = String(path).trim();
+    if (!text) return defaultCover;
+    if (/^https?:\/\//i.test(text)) return text;
+    return base_img_url2 + text.replace(/^\//, "");
 }
 
-function wkbHexToPointWkt(hex) {
-    const text = String(hex || "").trim();
-    if (!/^[0-9a-fA-F]+$/.test(text) || text.length < 42) return null;
-    const bytes = new Uint8Array(text.length / 2);
-    for (let i = 0; i < bytes.length; i++) {
-        bytes[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
-    }
-    const view = new DataView(bytes.buffer);
-    const little = view.getUint8(0) === 1;
-    let type = view.getUint32(1, little);
-    const hasSrid = (type & 0x20000000) !== 0;
-    type &= 0xff;
-    if (type !== 1) return null;
-    let offset = 5;
-    if (hasSrid) offset += 4;
-    if (offset + 16 > bytes.length) return null;
-    const lng = view.getFloat64(offset, little);
-    const lat = view.getFloat64(offset + 8, little);
-    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
-    return `POINT(${lng} ${lat})`;
+function mapZoneItem(zone) {
+    const coords = parsePointCoordinates(zone?.point);
+    return {
+        id: zone.zone_id,
+        zoneId: zone.zone_id,
+        zone_id: zone.zone_id,
+        name: zone.zone_name || "",
+        count: zone.image_count ?? 0,
+        cover: resolveZoneCoverUrl(zone.cover_url),
+        longitude: coords?.longitude,
+        latitude: coords?.latitude,
+        point: zone.point,
+    };
 }
 
-function toPointWkt(value) {
-    if (!value || typeof value !== "string") return null;
-    if (isPointWkt(value)) return value.trim();
-    return wkbHexToPointWkt(value);
-}
+const setMarkerEl = (id, el) => {
+    if (el) markerEls[id] = el;
+    else delete markerEls[id];
+};
 
-function getFarmMapLocation() {
-    const farmData = getFarmData();
-    const candidates = [farmData.wkt, farmData.geom_wkt, farmData.farm_location];
-    for (const item of candidates) {
-        const pointWkt = toPointWkt(item);
-        if (pointWkt) return pointWkt;
+const getFarmCenter = () => {
+    try {
+        const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+        const wkt = farm.wkt || farm.geom_wkt || farm.farm_location;
+        if (typeof wkt === "string" && /^POINT\s*\(/i.test(wkt.trim())) {
+            return util.wktCastGeom(wkt).getFirstCoordinate();
+        }
+    } catch {
+        // ignore
     }
-    return DEFAULT_MAP_LOCATION;
-}
+    const firstZone = zoneList.value.find((z) => z.longitude != null && z.latitude != null);
+    if (firstZone) {
+        return [firstZone.longitude, firstZone.latitude];
+    }
+    return DEFAULT_CENTER;
+};
 
-function getDefaultMapLocation() {
-    return (
-        toPointWkt(localStorage.getItem("MINI_USER_LOCATION_POINT")) ||
-        toPointWkt(store.state.home.miniUserLocationPoint) ||
-        getFarmMapLocation()
-    );
-}
+// ---------- 业务逻辑:地图 / 区域 ----------
+const clearMapOverlays = () => {
+    if (!fileMap.kmap?.map) {
+        mapOverlays.length = 0;
+        return;
+    }
+    mapOverlays.forEach((overlay) => fileMap.kmap.map.removeOverlay(overlay));
+    mapOverlays.length = 0;
+};
 
-function getDefaultMapCoordinate() {
-    return util.wktCastGeom(getDefaultMapLocation()).getFirstCoordinate();
-}
+const bindZoneOverlays = () => {
+    if (!fileMap.kmap?.map) return;
+    clearMapOverlays();
+    zoneList.value.forEach((zone) => {
+        const el = markerEls[zone.id];
+        if (!el || zone.longitude == null || zone.latitude == null) return;
+        const overlay = new Overlay({
+            element: el,
+            position: [zone.longitude, zone.latitude],
+            positioning: "bottom-center",
+            stopEvent: false,
+        });
+        fileMap.kmap.map.addOverlay(overlay);
+        mapOverlays.push(overlay);
+    });
+};
 
-const loadAlbumLayers = () => {
-    const [lng, lat] = getDefaultMapCoordinate();
-    const labels = MOCK_ZONES.map((item) => ({
-        name: t(item.nameKey),
-        longitude: lng + item.dlng,
-        latitude: lat + item.dlat,
-    }));
-    const photos = MOCK_PHOTOS.map((item) => ({
-        count: 2,
-        cover: defaultCover,
-        longitude: lng + item.dlng,
-        latitude: lat + item.dlat,
-    }));
-    const zoneRecords = MOCK_ZONES.map((item) => ({
-        zone_name: "",
-        polygon: squarePolygonWkt(lng + item.dlng, lat + item.dlat, item.delta),
-    }));
-    fileMap.setRecordPolygons(zoneRecords, "album");
-    fileMap.setAlbumMarkers({ labels, photos });
+const fitZonesOnMap = () => {
+    const records = zoneList.value
+        .filter((z) => z.longitude != null && z.latitude != null)
+        .map((z) => ({
+            zone_name: "",
+            polygon: `POINT(${z.longitude} ${z.latitude})`,
+        }));
+    if (!records.length) return;
+    fileMap.setRecordPolygons(records, "album");
 };
 
+async function fetchZoneList() {
+    const farmId = getSelectedFarmId();
+    if (!farmId) {
+        zoneList.value = [];
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.getZones({ farm_id: farmId });
+        const zones = res?.data?.zones;
+        zoneList.value = Array.isArray(zones) ? zones.map(mapZoneItem) : [];
+    } catch (e) {
+        console.warn("[albumMap] getZones failed", e);
+        zoneList.value = [];
+    }
+}
+
 const initAlbumMap = async () => {
     await nextTick();
     if (!mapContainer.value) return;
-    fileMap.initMap(getDefaultMapLocation(), mapContainer.value);
-    loadAlbumLayers();
+    await fetchZoneList();
+    const center = getFarmCenter();
+    fileMap.initMap(`POINT(${center[0]} ${center[1]})`, mapContainer.value);
+    await nextTick();
+    bindZoneOverlays();
     fileMap.kmap?.map?.updateSize?.();
+    fitZonesOnMap();
 };
 
+// ---------- 事件处理 ----------
 const handleLocationChange = (payload) => {
     if (!payload?.coordinateArray || !fileMap.kmap) return;
     fileMap.kmap.getView().animate({
@@ -170,16 +213,18 @@ const handleLocationChange = (payload) => {
 
 const handleLocate = () => {
     if (!fileMap.kmap) return;
+    const center = getFarmCenter();
     fileMap.kmap.getView().animate({
-        center: getDefaultMapCoordinate(),
+        center,
         zoom: 16,
         duration: 0,
     });
 };
 
-onActivated(() => {
-    initAlbumMap();
-});
+// ---------- 生命周期 ----------
+onMounted(initAlbumMap);
+onActivated(initAlbumMap);
+onBeforeUnmount(clearMapOverlays);
 </script>
 
 <style lang="scss" scoped>
@@ -204,6 +249,59 @@ onActivated(() => {
     }
 }
 
+.zone-marker {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    pointer-events: none;
+
+    &__label {
+        padding: 3px 14px;
+        border-radius: 25px;
+        background: #fff;
+        color: #0a0a0a;
+        font-size: 12px;
+    }
+
+    &__thumb {
+        position: relative;
+        width: 42px;
+        height: 42px;
+        margin-top: 8px;
+
+        img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            border: 2px solid #fff;
+            border-radius: 6px;
+            box-sizing: border-box;
+        }
+    }
+
+    &__badge {
+        position: absolute;
+        top: -6px;
+        right: -6px;
+        min-width: 18px;
+        height: 18px;
+        line-height: 16px;
+        border-radius: 50%;
+        background: #fff;
+        color: #0a0a0a;
+        font-size: 12px;
+        text-align: center;
+    }
+
+    &__arrow {
+        width: 0;
+        height: 0;
+        border-left: 5px solid transparent;
+        border-right: 5px solid transparent;
+        border-top: 6px solid #fff;
+    }
+}
+
 .search-bar {
     position: absolute;
     top: 12px;

+ 1 - 0
src/views/old_mini/entry_information/selectLocation.vue

@@ -21,6 +21,7 @@
             v-if="fromAgriAlbum"
             v-model:show="showZoneInfoPopup"
             :county-code="pendingZoneLocation?.adcode"
+            :point-location="pendingZoneLocation?.point || ''"
             @confirm="handleZoneInfoConfirm"
         />
     </div>