Bläddra i källkod

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

wangsisi 1 vecka sedan
förälder
incheckning
9aa979f143

+ 26 - 1
src/api/modules/questionnaire.js

@@ -60,5 +60,30 @@ module.exports = {
     getZones: {
         url: config.base_new_url + "questionnaire/zone_images",
         type: "get",
-    }
+    },
+    // 查询是否填写农事弹窗
+    checkFwInit: {
+        url: config.base_new_url + "questionnaire/work_init/get",
+        type: "get",
+    },
+    // 填写农场农事弹窗更新状态
+    updateFwInitStatus: {
+        url: config.base_new_url + "questionnaire/work_init/set",
+        type: "post",
+    },
+    // 查询首页气象胁迫信息
+    getWeatherReport: {
+        url: config.base_new_url + "questionnaire/risk_report_info",
+        type: "get",
+    },
+    // 查询气象预警报告
+    getWeatherRisk: {
+        url: config.base_new_url + "questionnaire/weather_risk_report",
+        type: "get",
+    },
+    // 查询气象胁迫报告
+    getWeatherStress: {
+        url: config.base_new_url + "questionnaire/weather_stress_report",
+        type: "get",
+    },
 }

+ 1 - 1
src/components/allGarden.vue

@@ -217,7 +217,7 @@ function getDefaultMapPoint() {
 function refreshMapMarkers() {
     if (!indexMap.kmap) return;
     const points = filteredFarmList.value.filter((farm) => farm.wkt);
-    indexMap.initData(points, "name", "wkt");
+    indexMap.initData(points, "farm_name", "wkt", true);
 }
 
 function ensureMap() {

+ 14 - 2
src/components/farmHeader.vue

@@ -19,11 +19,13 @@
 <script setup>
 import { onActivated, onMounted, ref } from "vue";
 import { useRouter } from "vue-router";
+import { useStore } from "vuex";
 import { Switch } from "@element-plus/icons-vue";
 import { useI18n } from "@/i18n";
 
 const { t } = useI18n();
 const router = useRouter();
+const store = useStore();
 
 const defaultAvatar = require("@/assets/img/common/logo.png");
 const farmName = ref("");
@@ -69,10 +71,20 @@ const saveSelectedFarm = (farm) => {
     localStorage.setItem("selectedFarmData", JSON.stringify(farm));
 };
 
+function getMiniUserId() {
+    return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
+}
+
 const loadFirstFarmAsSelected = async () => {
+    const id = getMiniUserId();
+    if (!id) {
+        applyFarmInfo();
+        return;
+    }
     try {
-        const { data } = await VE_API.farm.getFarmList();
-        const first = (data || [])[0];
+        const res = await VE_API.questionnaire.getFarms({ id });
+        const list = Array.isArray(res?.data) ? res.data : [];
+        const first = list[0];
         if (!first) {
             applyFarmInfo();
             return;

+ 1 - 1
src/components/popup/phenologyUpdatePopup.vue

@@ -94,7 +94,7 @@ const props = defineProps({
     /** 农场位置 */
     farmLocation: {
         type: String,
-        default: "位置位置位置",
+        default: "--",
     },
 });
 

+ 1 - 1
src/i18n/messages.js

@@ -142,7 +142,7 @@ export default {
         },
         agriFile: {
             farmName: "农场名称",
-            farmLocation: "位置位置位置位置",
+            farmLocation: "--",
             moreFarms: "更多农场",
             patrolRecord: "巡园记录",
             patrolGrowthTitle: "长势跟踪巡园要点",

+ 495 - 0
src/views/old_mini/agri_file/components/albumPanel.vue

@@ -0,0 +1,495 @@
+<template>
+    <div class="album-panel">
+        <div class="map-card" @click="goAlbumMap">
+            <div ref="mapContainer" class="map-container"></div>
+            <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('region')">
+                <el-icon>
+                    <Plus />
+                </el-icon>
+                <span>{{ t("agriFile.addZone") }}</span>
+            </div>
+            <div
+                v-for="item in zoneList"
+                :key="item.id"
+                :ref="(el) => setMarkerEl(item.id, el)"
+                class="zone-marker"
+            >
+                <div class="zone-marker__label">{{ item.name }}</div>
+                <div class="zone-marker__thumb">
+                    <img :src="item.cover" alt="" />
+                    <span class="zone-marker__badge">{{ item.count }}</span>
+                </div>
+                <div class="zone-marker__arrow"></div>
+            </div>
+        </div>
+        <div class="album-grid" v-if="!noFarmRange">
+            <div class="album-add" @click.stop="goAddZone('region')">
+                <el-icon class="album-add__icon">
+                    <Plus />
+                </el-icon>
+                <span>{{ t("agriFile.addAlbum") }}</span>
+            </div>
+            <div
+                v-for="item in albumList"
+                :key="item.id"
+                class="album-card"
+                :class="{ 'is-empty': !item.count }"
+            >
+                <div class="album-card__cover" @click.stop="goAlbumDetail(item)">
+                    <img v-if="item.count && item.cover" :src="item.cover" alt="" />
+                    <img
+                        v-else
+                        class="album-card__empty-icon"
+                        src="@/assets/img/agricultural/photo.png"
+                        alt=""
+                    />
+                    <span class="album-card__count">
+                        {{ t("agriFile.photoCount", { count: item.count }) }}
+                    </span>
+                    <div class="album-card__add" @click.stop="emit('open-upload')">
+                        <el-icon>
+                            <Plus />
+                        </el-icon>
+                    </div>
+                </div>
+                <div class="album-card__name">{{ item.name }}</div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
+import { useRouter } from "vue-router";
+import Overlay from "ol/Overlay";
+import { useI18n } from "@/i18n";
+import FileMap from "../fileMap";
+import * as util from "@/common/ol_common.js";
+import { base_img_url2 } from "@/api/config";
+
+const props = defineProps({
+    /** 当前是否为相册 Tab,用于切回时刷新地图尺寸 */
+    active: {
+        type: Boolean,
+        default: false,
+    },
+});
+
+const emit = defineEmits(["open-upload"]);
+
+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 DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
+
+// ---------- 页面状态 ----------
+const mapContainer = ref(null);
+const fileMap = new FileMap();
+const markerEls = {};
+const mapOverlays = [];
+const noFarmRange = ref(false);
+const zoneList = ref([]);
+const albumList = ref([]);
+
+// ---------- 工具函数 ----------
+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 parsePointCoordinates(pointWkt) {
+    if (!pointWkt || !/^POINT\s*\(/i.test(String(pointWkt).trim())) {
+        return null;
+    }
+    try {
+        const coord = util.wktCastGeom(pointWkt).getFirstCoordinate();
+        if (!coord || coord.length < 2) return null;
+        return { longitude: coord[0], latitude: coord[1] };
+    } catch {
+        return null;
+    }
+}
+
+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 mapZoneItem(zone) {
+    const coords = parsePointCoordinates(zone?.point);
+    return {
+        id: zone.zone_id,
+        zoneId: 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,
+    };
+}
+
+const setMarkerEl = (id, el) => {
+    if (el) markerEls[id] = el;
+    else delete markerEls[id];
+};
+
+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
+    }
+    const firstZone = zoneList.value.find((z) => z.longitude != null && z.latitude != null);
+    if (firstZone) {
+        return [firstZone.longitude, firstZone.latitude];
+    }
+    return DEFAULT_CENTER;
+};
+
+// ---------- 业务逻辑:地图 / 区域 ----------
+const clearMapOverlays = () => {
+    if (!fileMap.kmap?.map) {
+        mapOverlays.length = 0;
+        return;
+    }
+    mapOverlays.forEach((overlay) => fileMap.kmap.map.removeOverlay(overlay));
+    mapOverlays.length = 0;
+};
+
+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);
+    });
+};
+
+async function fetchZoneList() {
+    const farmId = getSelectedFarmId();
+    if (!farmId) {
+        zoneList.value = [];
+        albumList.value = [];
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.getZones({ farm_id: farmId });
+        const zones = res?.data?.zones;
+        const list = Array.isArray(zones) ? zones.map(mapZoneItem) : [];
+        zoneList.value = list;
+        albumList.value = list;
+    } catch (e) {
+        console.warn("[albumPanel] getZones failed", e);
+        zoneList.value = [];
+        albumList.value = [];
+    }
+}
+
+const initAlbumMap = async () => {
+    await nextTick();
+    if (!mapContainer.value) return;
+    if (noFarmRange.value) {
+        zoneList.value = [];
+        albumList.value = [];
+        return;
+    }
+    await fetchZoneList();
+    const center = getFarmCenter();
+    fileMap.initMap(`POINT(${center[0]} ${center[1]})`, mapContainer.value);
+    await nextTick();
+    bindZoneOverlays();
+    fileMap.kmap?.map?.updateSize?.();
+};
+
+// ---------- 事件处理 ----------
+const goAddZone = (type) => {
+    router.push({
+        path: "/add_zone",
+        query: {
+            type,
+            isSuccessPopup: type === "farm" ? "1" : "0",
+        },
+    });
+};
+
+const goAlbumMap = () => {
+    router.push("/album_map");
+};
+
+const goAlbumDetail = (item) => {
+    router.push({
+        path: "/region_albums",
+        query: {
+            id: item?.id,
+            name: item?.name || "",
+            noImage: item?.count ? "false" : "true",
+        },
+    });
+};
+
+// ---------- 生命周期 ----------
+onMounted(initAlbumMap);
+onActivated(initAlbumMap);
+onBeforeUnmount(clearMapOverlays);
+
+watch(
+    () => props.active,
+    (active) => {
+        if (active) initAlbumMap();
+    }
+);
+</script>
+
+<style lang="scss" scoped>
+.album-panel {
+    .map-card {
+        position: relative;
+        height: 210px;
+    }
+
+    .map-container {
+        width: 100%;
+        height: 100%;
+        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;
+        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;
+        }
+    }
+
+    .add-zone-btn {
+        position: absolute;
+        right: 5px;
+        bottom: 10px;
+        z-index: 5;
+        display: flex;
+        align-items: center;
+        gap: 2px;
+        padding: 5px 10px;
+        border-radius: 4px;
+        background: #fff;
+        color: #2199f8;
+        font-weight: 500;
+    }
+
+    .album-grid {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        gap: 16px;
+        margin-top: 16px;
+        box-sizing: border-box;
+
+        .album-add {
+            align-self: start;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            width: 100%;
+            aspect-ratio: 1;
+            border-radius: 5px;
+            background: #ffffff;
+            color: #969494;
+            font-size: 16px;
+
+            &__icon {
+                margin-bottom: 6px;
+                font-size: 20px;
+            }
+        }
+
+        .album-card {
+            width: 100%;
+
+            &.is-empty {
+                .album-card__cover {
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    overflow: hidden;
+                    border-radius: 8px;
+                    background: linear-gradient(180deg, #e0f1ff 0%, #c2e4ff 100%);
+                }
+            }
+
+            &__cover {
+                position: relative;
+                width: 100%;
+                aspect-ratio: 1;
+
+                img:not(.album-card__empty-icon) {
+                    width: 100%;
+                    height: 100%;
+                    object-fit: cover;
+                    border-radius: 6px;
+                }
+            }
+
+            &__empty-icon {
+                width: 50px;
+                height: 50px;
+            }
+
+            &__count {
+                position: absolute;
+                top: 1px;
+                right: 0;
+                padding: 1px 5px;
+                border-radius: 0 5px 0 5px;
+                background: rgba(0, 0, 0, 0.4);
+                backdrop-filter: blur(4px);
+                color: #fff;
+            }
+
+            &__add {
+                position: absolute;
+                right: 8px;
+                bottom: 8px;
+                z-index: 1;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                width: 44px;
+                height: 44px;
+                border-radius: 50%;
+                background: #2199f8;
+                color: #fff;
+                font-size: 20px;
+                box-shadow: 0 2px 8px rgba(33, 153, 248, 0.35);
+            }
+
+            &__name {
+                margin-top: 10px;
+                color: #1f1f1f;
+            }
+        }
+    }
+}
+</style>

+ 422 - 0
src/views/old_mini/agri_file/components/patrolPanel.vue

@@ -0,0 +1,422 @@
+<template>
+    <div class="patrol-panel">
+        <div
+            v-for="item in patrolList"
+            :key="item.id"
+            class="tracking-item"
+            :class="`tracking-item--${item.theme}`"
+            @click="goGrowthTrack(item)"
+        >
+            <div class="tracking-item__header">
+                <div class="tracking-item__header-row">
+                    <div class="tracking-item__header-left">
+                        <span class="tracking-item__level">
+                            <el-icon><BellFilled /></el-icon>
+                            {{ item.level }}
+                        </span>
+                        <span class="tracking-item__title van-ellipsis">
+                            {{ item.title }}
+                            <span v-if="item.zone" class="tracking-item__zone"> ({{ item.zone }})</span>
+                        </span>
+                    </div>
+                    <div class="tracking-item__share" @click.stop="handleShare(item)">
+                        <Icon size="14" color="#2199F8" name="share" />
+                        <span>{{ t("agriFile.forward") }}</span>
+                    </div>
+                </div>
+            </div>
+            <div class="tracking-item__body">
+                <div class="tracking-item__action">
+                    <div class="tracking-item__action-main">
+                        <div class="tracking-item__icon">
+                            <img :src="item.icon" alt="" />
+                            <span class="tracking-item__reason">{{ item.subject }}</span>
+                        </div>
+                        <div class="tracking-item__issue">{{ item.issue }}</div>
+                    </div>
+                    <div class="tracking-item__btn" @click.stop="handleUploadClick(item)">
+                        <el-icon>
+                            <Plus />
+                        </el-icon>
+                        <span>{{ t("agriFile.uploadPhoto") }}</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="crop-archive">
+            <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div>
+            <PhenologyTrackTimelineItem :list="cropArchiveList" />
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { onActivated, onMounted, ref } from "vue";
+import { useRouter } from "vue-router";
+import { Icon } from "vant";
+import { useI18n } from "@/i18n";
+import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
+import wx from "weixin-js-sdk";
+
+const emit = defineEmits(["open-upload"]);
+
+const { t } = useI18n();
+const router = useRouter();
+
+const PATROL_ICON = require("@/assets/img/report/wh-icon.png");
+
+// ---------- 页面状态 ----------
+const patrolList = ref([
+    {
+        id: 1,
+        type: "growth",
+        theme: "blue",
+        level: "",
+        title: "",
+        subject: "",
+        issue: "",
+        desc: "",
+        icon: PATROL_ICON,
+    },
+    {
+        id: 2,
+        type: "abnormal",
+        theme: "blue",
+        level: "",
+        title: "",
+        subject: "",
+        issue: "",
+        icon: PATROL_ICON,
+    },
+]);
+const cropArchiveList = ref([]);
+/** 与 growthTrack 一致:需勾画时先走 add_zone */
+const needDraw = ref(true);
+
+// ---------- 工具函数 ----------
+/** 外部感知任务 → 巡园卡片(仅取第一条) */
+const mapInteractTaskToPatrol = (task) => {
+    const content = task?.interact_content || {};
+    return {
+        id: task.zone_id || task.interact_code,
+        type: "growth",
+        theme: "blue",
+        level: content.interact_level || "",
+        title: t("agriFile.patrolGrowthTitle") || "",
+        desc: content.interact_reason || "",
+        issue: content.interact_reason || "",
+        subject: content.interact_title || "",
+        icon: PATROL_ICON,
+        zoneId: task.zone_id,
+        interactCode: task.interact_code,
+        raw: task,
+    };
+};
+
+const mapPhenoRecordToArchive = (item, index, zoneName) => ({
+    id: [item.zone_id, item.enter_date, item.period_code, index]
+        .filter((v) => v != null && v !== "")
+        .join("_"),
+    date: item.enter_date || "",
+    zone_name: item.zone_name || zoneName || "",
+    content: item.sentence || (item.period_name ? `到达 ${item.period_name}` : ""),
+    period_code: item.period_code,
+    period_name: item.period_name,
+    shoot_type: item.shoot_type,
+    zone_id: item.zone_id,
+    crop_code: item.crop_code,
+});
+
+// ---------- 业务逻辑:加载 ----------
+const fetchPatrolInteractTask = async () => {
+    try {
+        const res = await VE_API.questionnaire.getExternalInteractTasks({ zone_id: 42 });
+        const tasks = res?.data?.interact_tasks;
+        const first = Array.isArray(tasks) ? tasks[0] : null;
+        if (!first) return;
+        const mapped = mapInteractTaskToPatrol(first);
+        if (patrolList.value.length) {
+            patrolList.value[0] = {
+                ...patrolList.value[0],
+                ...mapped,
+            };
+        } else {
+            patrolList.value = [mapped];
+        }
+    } catch (e) {
+        console.warn("[patrolPanel] getExternalInteractTasks failed", e);
+    }
+};
+
+const fetchCropArchive = async () => {
+    const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+    const params = {
+        farm_id: farm.farm_id,
+        zone_id: 46,
+        crop_code: "LCH",
+    };
+    if (!params) {
+        cropArchiveList.value = [];
+        return;
+    }
+    const { zone_name: zoneName, ...query } = params;
+    try {
+        const res = await VE_API.entry.getPhenoRecord(query);
+        if (res?.code !== 200) {
+            cropArchiveList.value = [];
+            return;
+        }
+        const list = Array.isArray(res.data) ? res.data : [];
+        const fallbackZone = zoneName || t("agriFile.zoneOne");
+        cropArchiveList.value = list
+            .map((item, index) => mapPhenoRecordToArchive(item, index, fallbackZone))
+            .sort((a, b) => String(b.date).localeCompare(String(a.date)));
+    } catch (e) {
+        console.warn("[patrolPanel] getPhenoRecord failed", e);
+        cropArchiveList.value = [];
+    }
+};
+
+const fillMockLabels = () => {
+    const albumName = t("agriFile.zoneOne");
+    patrolList.value = patrolList.value.map((item, index) => {
+        // 第一条由外部感知任务接口填充,不覆盖
+        if (index === 0 && item.interactCode) return item;
+        return {
+            ...item,
+            level: t("agriFile.riskLevel2"),
+            title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
+            zone: albumName,
+            subject: t("agriFile.patrolSubject"),
+            issue: t("agriFile.patrolIssue"),
+        };
+    });
+};
+
+const loadPatrolData = () => {
+    fillMockLabels();
+    fetchPatrolInteractTask();
+    fetchCropArchive();
+};
+
+// ---------- 事件处理 ----------
+const goGrowthTrack = (item) => {
+    router.push({
+        path: "/growth_track",
+        query: {
+            id: item?.id,
+            type: item?.type || "growth",
+            pageName:
+                item.type === "growth"
+                    ? t("agriFile.patrolGrowthTitle")
+                    : t("agriFile.patrolAbnormalTitle"),
+        },
+    });
+};
+
+const handleShare = (item) => {
+    const query = {
+        askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
+        shareText: item?.title || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
+        targetUrl: "growth_track",
+        paramsPage: JSON.stringify({
+            id: item?.id,
+            type: item?.type || "growth",
+            fromShare: 1,
+        }),
+        imageUrl: "https://birdseye-img.sysuimars.com/temp/field.png",
+    };
+    wx.miniProgram.navigateTo({
+        url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
+    });
+};
+
+/** 与 growthTrack 上传一致:需勾画 → add_zone;异常 → pest_interact;否则弹上传 */
+const handleUploadClick = (item) => {
+    const isAbnormal = item?.type === "abnormal";
+    if (needDraw.value) {
+        router.push({
+            path: "/add_zone",
+            query: {
+                type: "farm",
+                targetUrl: isAbnormal ? "/pest_interact" : "/growth_track",
+                ...(isAbnormal ? { isAbnormal: "true" } : {}),
+                ...(item?.id != null ? { trackId: item.id } : {}),
+                ...(item?.type ? { trackType: item.type } : {}),
+            },
+        });
+        return;
+    }
+    if (isAbnormal) {
+        router.push({ path: "/pest_interact" });
+        return;
+    }
+    emit("open-upload");
+};
+
+// ---------- 生命周期 ----------
+onMounted(loadPatrolData);
+onActivated(loadPatrolData);
+</script>
+
+<style lang="scss" scoped>
+.patrol-panel {
+    .tracking-item {
+        width: 100%;
+        margin-bottom: 16px;
+        box-shadow: 0px 4px 4px 0px rgba(33, 153, 248, 0.1);
+
+        &:last-of-type {
+            margin-bottom: 0;
+        }
+
+        &__header {
+            position: relative;
+            padding: 10px 10px 20px;
+            border-radius: 10px 10px 0 0;
+            background: linear-gradient(180deg, #86caff 0%, #2199f8 48.5%);
+        }
+
+        &__header-row {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+        }
+
+        &__header-left {
+            display: flex;
+            align-items: center;
+            gap: 5px;
+        }
+
+        &__level {
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            padding: 1px 4px;
+            border-radius: 2px;
+            background: #fff;
+            color: #ff6a6a;
+            font-size: 12px;
+        }
+
+        &__title {
+            color: #fff;
+            font-size: 16px;
+            font-weight: 500;
+        }
+
+        &__zone {
+            font-size: 12px;
+        }
+
+        &__share {
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            padding: 2px 10px;
+            border-radius: 25px;
+            background: #fff;
+            color: #2199f8;
+            font-size: 12px;
+        }
+
+        &__body {
+            position: relative;
+            padding: 10px;
+            margin-top: -8px;
+            border-radius: 10px;
+            background: #fff;
+
+            &::before {
+                content: "";
+                position: absolute;
+                top: -6px;
+                left: 21%;
+                width: 0;
+                height: 0;
+                border-left: 7px solid transparent;
+                border-right: 7px solid transparent;
+                border-bottom: 7px solid #fff;
+                transform: translateX(-50%);
+            }
+        }
+
+        &__action {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            border: 1px solid rgba(33, 153, 248, 0.2);
+            border-radius: 6px;
+            padding: 8px;
+        }
+
+        &__icon {
+            display: flex;
+            align-items: center;
+            gap: 5px;
+
+            img {
+                width: 18px;
+                height: 16px;
+            }
+        }
+
+        &__reason {
+            color: #2199f8;
+            font-weight: 500;
+        }
+
+        &__issue {
+            margin-top: 2px;
+            color: #2199f8;
+            font-size: 12px;
+        }
+
+        &__action-main {
+            flex: 1;
+            padding-right: 4px;
+        }
+
+        &__btn {
+            flex: none;
+            display: flex;
+            align-items: center;
+            gap: 2px;
+            padding: 7px 10px;
+            border-radius: 5px;
+            background: #2199f8;
+            color: #fff;
+            font-size: 12px;
+        }
+    }
+
+    .crop-archive {
+        padding: 10px;
+        border-radius: 10px;
+        background: #fff;
+
+        &__title {
+            position: relative;
+            display: inline-block;
+            z-index: 1;
+            font-size: 16px;
+            font-weight: 500;
+            margin-bottom: 5px;
+
+            &::after {
+                content: "";
+                position: absolute;
+                left: 0;
+                bottom: 2px;
+                z-index: -1;
+                width: 90px;
+                height: 6px;
+                border-radius: 25px;
+                background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
+                opacity: 0.4;
+            }
+        }
+    }
+}
+</style>

+ 130 - 0
src/views/old_mini/agri_file/components/reportPanel.vue

@@ -0,0 +1,130 @@
+<template>
+    <div class="report-panel">
+        <div v-for="item in reportList" :key="item.id" class="report-card">
+            <div class="report-card__cover">
+                <img :src="item.cover" alt="" />
+                <span class="report-card__date">{{ item.date }}</span>
+                <span class="report-card__status">
+                    <i class="report-card__dot"></i>
+                    {{ t("agriFile.reportUpdate") }}
+                </span>
+            </div>
+            <div class="report-card__body">
+                <div class="report-card__title">{{ item.title }}</div>
+                <div class="report-card__desc">{{ item.desc }}</div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { onActivated, onMounted, ref } from "vue";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+
+// ---------- 常量 ----------
+const reportCover = require("@/assets/img/common/sd-1.jpg");
+
+// ---------- 页面状态 ----------
+const reportList = ref([
+    { id: 1, title: "", desc: "", date: "", cover: reportCover },
+    { id: 2, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-2.jpg") },
+    { id: 3, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-3.jpg") },
+]);
+
+// ---------- 业务逻辑:加载 ----------
+/** 后续替换为诊断报告列表接口 */
+const fillMockLabels = () => {
+    reportList.value = reportList.value.map((item) => ({
+        ...item,
+        title: t("agriFile.reportTitle"),
+        desc: t("agriFile.reportDesc"),
+        date: t("agriFile.reportDate"),
+    }));
+};
+
+const loadReportData = () => {
+    fillMockLabels();
+};
+
+// ---------- 生命周期 ----------
+onMounted(loadReportData);
+onActivated(loadReportData);
+</script>
+
+<style lang="scss" scoped>
+.report-panel {
+    .report-card {
+        margin-bottom: 10px;
+        border-radius: 8px;
+        background: #fff;
+
+        &:last-child {
+            margin-bottom: 0;
+        }
+
+        &__cover {
+            position: relative;
+            width: 100%;
+            height: 158px;
+
+            img {
+                display: block;
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                border-radius: 5px;
+            }
+        }
+
+        &__date {
+            position: absolute;
+            top: 0;
+            padding: 5px 12px;
+            border-radius: 4px;
+            background: rgba(0, 0, 0, 0.45);
+            backdrop-filter: blur(4px);
+            color: #fff;
+            font-size: 16px;
+            border-radius: 5px 0 5px 0;
+        }
+
+        &__status {
+            position: absolute;
+            top: 10px;
+            right: 0;
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            padding: 2px 13px;
+            border-radius: 20px 0 0 20px;
+            background: #ff5454;
+            color: #fff;
+            font-size: 12px;
+        }
+
+        &__dot {
+            width: 6px;
+            height: 6px;
+            border-radius: 50%;
+            background: #fff;
+        }
+
+        &__body {
+            padding: 10px;
+            margin-top: 4px;
+        }
+
+        &__title {
+            font-size: 16px;
+            font-weight: 500;
+        }
+
+        &__desc {
+            margin-top: 4px;
+            color: #9e9b9b;
+        }
+    }
+}
+</style>

+ 56 - 976
src/views/old_mini/agri_file/index.vue

@@ -29,112 +29,13 @@
             </div>
         </div>
         <div class="content-panel">
-            <div v-show="activeNav === 'patrol'">
-                <div v-for="item in patrolList" :key="item.id" class="tracking-item"
-                    :class="`tracking-item--${item.theme}`" @click="goGrowthTrack(item)">
-                    <div class="tracking-item__header">
-                        <div class="tracking-item__header-row">
-                            <div class="tracking-item__header-left">
-                                <span class="tracking-item__level">
-                                    <el-icon><BellFilled /></el-icon>
-                                    {{ item.level }}
-                                </span>
-                                <span class="tracking-item__title van-ellipsis">
-                                    {{ item.title }}<span v-if="item.zone" class="tracking-item__zone"> ({{ item.zone }})</span>
-                                </span>
-                            </div>
-                            <div class="tracking-item__share" @click.stop="handleShare(item)">
-                                <Icon size="14" color="#2199F8" name="share" />
-                                <span>{{ t("agriFile.forward") }}</span>
-                            </div>
-                        </div>
-                    </div>
-                    <div class="tracking-item__body">
-                        <div class="tracking-item__action">
-                            <div class="tracking-item__action-main">
-                                <div class="tracking-item__icon">
-                                    <img :src="item.icon" alt="" />
-                                    <span class="tracking-item__reason">{{ item.subject }}</span>
-                                </div>
-                                <div class="tracking-item__issue">{{ item.issue }}</div>
-                            </div>
-                            <div class="tracking-item__btn" @click.stop="handleUploadClick(item)">
-                                <el-icon>
-                                    <Plus />
-                                </el-icon>
-                                <span>{{ t("agriFile.uploadPhoto") }}</span>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div class="crop-archive">
-                    <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div>
-                    <PhenologyTrackTimelineItem :list="cropArchiveList" />
-                </div>
-            </div>
-            <div v-show="activeNav === 'album'">
-                <div class="map-card" @click="goAlbumMap">
-                    <div ref="mapContainer" class="map-container"></div>
-                    <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('region')">
-                        <el-icon>
-                            <Plus />
-                        </el-icon>
-                        <span>{{ t("agriFile.addZone") }}</span>
-                    </div>
-                    <div v-for="item in zoneList" :key="item.id" :ref="(el) => setMarkerEl(item.id, el)"
-                        class="zone-marker">
-                        <div class="zone-marker__label">{{ item.name }}</div>
-                        <div class="zone-marker__thumb">
-                            <img :src="item.cover" alt="" />
-                            <span class="zone-marker__badge">{{ item.count }}</span>
-                        </div>
-                        <div class="zone-marker__arrow"></div>
-                    </div>
-                </div>
-                <div class="album-grid" v-if="!noFarmRange">
-                    <div class="album-add" @click.stop="goAddZone('region')">
-                        <el-icon class="album-add__icon">
-                            <Plus />
-                        </el-icon>
-                        <span>{{ t("agriFile.addAlbum") }}</span>
-                    </div>
-                    <div v-for="item in albumList" :key="item.id" class="album-card" :class="{ 'is-empty': !item.count }">
-                        <div class="album-card__cover" @click.stop="goAlbumDetail(item)">
-                            <img v-if="item.count && item.cover" :src="item.cover" alt="" />
-                            <img v-else class="album-card__empty-icon" src="@/assets/img/agricultural/photo.png" alt="" />
-                            <span class="album-card__count">{{ t("agriFile.photoCount", { count: item.count }) }}</span>
-                            <div class="album-card__add" @click.stop="openUploadPopup">
-                                <el-icon>
-                                    <Plus />
-                                </el-icon>
-                            </div>
-                        </div>
-                        <div class="album-card__name">{{ item.name }}</div>
-                    </div>
-                </div>
-            </div>
-            <div v-show="activeNav === 'report'">
-                <div v-for="item in reportList" :key="item.id" class="report-card">
-                    <div class="report-card__cover">
-                        <img :src="item.cover" alt="" />
-                        <span class="report-card__date">{{ item.date }}</span>
-                        <span class="report-card__status">
-                            <i class="report-card__dot"></i>
-                            {{ t("agriFile.reportUpdate") }}
-                        </span>
-                    </div>
-                    <div class="report-card__body">
-                        <div class="report-card__title">{{ item.title }}</div>
-                        <div class="report-card__desc">{{ item.desc }}</div>
-                    </div>
-                </div>
-            </div>
+            <patrol-panel v-show="activeNav === 'patrol'" @open-upload="openUploadPopup" />
+            <album-panel
+                v-show="activeNav === 'album'"
+                :active="activeNav === 'album'"
+                @open-upload="openUploadPopup"
+            />
+            <report-panel v-show="activeNav === 'report'" />
         </div>
         <album-upload-popup v-model:show="showUploadPopup" />
         <complete-farm-info-popup v-model:show="showCompleteFarmPopup" @confirm="goCompleteFarmInfo" />
@@ -168,14 +69,10 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
+import { computed, onActivated, onBeforeUnmount, onMounted, ref } from "vue";
 import { useStore } from "vuex";
 import { useRouter } from "vue-router";
-import Overlay from "ol/Overlay";
 import { useI18n } from "@/i18n";
-import { Icon } from 'vant';
-import FileMap from "./fileMap";
-import * as util from "@/common/ol_common.js";
 import farmHeader from "@/components/farmHeader.vue";
 import albumUploadPopup from "./components/albumUploadPopup.vue";
 import completeFarmInfoPopup from "@/components/popup/completeFarmInfoPopup.vue";
@@ -183,28 +80,17 @@ import diagnosisReportPopup from "@/components/popup/diagnosisReportPopup.vue";
 import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
 import proxyAuthPopup from "@/components/popup/proxyAuthPopup.vue";
 import phenologyUpdatePopup from "@/components/popup/phenologyUpdatePopup.vue";
-import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
-import wx from "weixin-js-sdk";
-import { base_img_url2 } from "@/api/config";
+import patrolPanel from "./components/patrolPanel.vue";
+import albumPanel from "./components/albumPanel.vue";
+import reportPanel from "./components/reportPanel.vue";
 
 const { t } = useI18n();
 const store = useStore();
 const router = useRouter();
 const tabBarHeight = computed(() => store.state.home.tabBarHeight);
 
+// ---------- 常量 ----------
 const DIAGNOSIS_GUIDE_STORAGE_KEY = "AGRI_FILE_DIAGNOSIS_REPORT_GUIDE";
-const guideVisible = ref(false);
-
-const defaultCover = require("@/assets/img/home/banner.png");
-const DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
-
-const activeNav = ref("patrol");
-const mapContainer = ref(null);
-const fileMap = new FileMap();
-const markerEls = {};
-const mapOverlays = [];
-
-const noFarmRange = ref(false);
 
 const navCards = [
     {
@@ -224,28 +110,32 @@ const navCards = [
     },
 ];
 
-const zoneList = ref([]);
-
-const albumList = ref([
-    { id: 1, name: "", count: 0, cover: "" },
-    { id: 2, name: "", count: 125, cover: defaultCover },
-    { id: 3, name: "", count: 125, cover: defaultCover },
-]);
-
-const reportCover = require("@/assets/img/common/sd-1.jpg");
-const reportList = ref([
-    { id: 1, title: "", desc: "", date: "", cover: reportCover },
-    { id: 2, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-2.jpg") },
-    { id: 3, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-3.jpg") },
-]);
-
+// ---------- 页面状态 ----------
+const activeNav = ref("patrol");
+const guideVisible = ref(false);
 const showUploadPopup = ref(false);
 const showCompleteFarmPopup = ref(false);
 
+/** 后续由接口控制是否展示代管授权弹窗 */
+const showProxyAuthPopup = ref(false);
+const proxyServiceName = ref("农服名称");
+
+/** 后续由接口控制是否展示申请更换物候期弹窗 */
+const showPhenologyUpdatePopup = ref(false);
+const phenologyUpdateInfo = ref({
+    serviceName: "某某农服",
+    currentStage: "花穗期",
+    targetStage: "小果期",
+    varietyName: "荔枝-桂味",
+    farmLocation: "--",
+});
+
+// ---------- 工具函数 ----------
 function getMiniUserId() {
     return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
 }
 
+// ---------- 业务逻辑 ----------
 /** 无果园信息时展示「完善农场信息」弹窗 */
 async function checkCompleteFarmPopup() {
     const id = getMiniUserId();
@@ -262,368 +152,6 @@ async function checkCompleteFarmPopup() {
         showCompleteFarmPopup.value = false;
     }
 }
-// 后续由接口控制是否展示代管授权弹窗
-const showProxyAuthPopup = ref(false);
-const proxyServiceName = ref("农服名称");
-
-const handleProxyReject = () => {
-    // TODO: 调用拒绝代管接口
-};
-
-const handleProxyAllow = () => {
-    // TODO: 调用允许代管接口
-};
-
-// 后续由接口控制是否展示申请更换物候期弹窗
-const showPhenologyUpdatePopup = ref(false);
-const phenologyUpdateInfo = ref({
-    serviceName: "某某农服",
-    currentStage: "花穗期",
-    targetStage: "小果期",
-    varietyName: "荔枝-桂味",
-    farmLocation: "位置位置位置",
-});
-
-const handlePhenologyUpdateReject = () => {
-    // TODO: 调用暂不更新接口
-};
-
-const handlePhenologyUpdateConfirm = () => {
-    // TODO: 调用确认更新接口
-};
-const PATROL_ICON = require("@/assets/img/report/wh-icon.png");
-const patrolList = ref([
-    {
-        id: 1,
-        type: "growth",
-        theme: "blue",
-        level: "",
-        title: "",
-        subject: "",
-        issue: "",
-        desc: "",
-        icon: PATROL_ICON,
-    },
-    {
-        id: 2,
-        type: "abnormal",
-        theme: "blue",
-        level: "",
-        title: "",
-        subject: "",
-        issue: "",
-        icon: PATROL_ICON,
-    },
-]);
-
-/** 外部感知任务 → 巡园卡片(仅取第一条) */
-const mapInteractTaskToPatrol = (task) => {
-    const content = task?.interact_content || {};
-    return {
-        id: task.zone_id || task.interact_code,
-        type: "growth",
-        theme: "blue",
-        level: content.interact_level || "",
-        title: t("agriFile.patrolGrowthTitle") || "",
-        desc: content.interact_reason || "",
-        issue: content.interact_reason || "",
-        subject: content.interact_title || "",
-        icon: PATROL_ICON,
-        zoneId: task.zone_id,
-        interactCode: task.interact_code,
-        raw: task,
-    };
-};
-
-const fetchPatrolInteractTask = async () => {
-    try {
-        const res = await VE_API.questionnaire.getExternalInteractTasks({ zone_id: 42 });
-        const tasks = res?.data?.interact_tasks;
-        const first = Array.isArray(tasks) ? tasks[0] : null;
-        if (!first) return;
-        const mapped = mapInteractTaskToPatrol(first);
-        if (patrolList.value.length) {
-            patrolList.value[0] = {
-                ...patrolList.value[0],
-                ...mapped,
-            };
-        } else {
-            patrolList.value = [mapped];
-        }
-    } catch (e) {
-        console.warn("[agri_file] getExternalInteractTasks failed", e);
-    }
-};
-
-const cropArchiveList = ref([]);
-
-const mapPhenoRecordToArchive = (item, index, zoneName) => ({
-    id: [item.zone_id, item.enter_date, item.period_code, index].filter((v) => v != null && v !== "").join("_"),
-    date: item.enter_date || "",
-    zone_name: item.zone_name || zoneName || "",
-    content: item.sentence || (item.period_name ? `到达 ${item.period_name}` : ""),
-    period_code: item.period_code,
-    period_name: item.period_name,
-    shoot_type: item.shoot_type,
-    zone_id: item.zone_id,
-    crop_code: item.crop_code,
-});
-
-const fetchCropArchive = async () => {
-    const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
-    const params = {
-        farm_id: farm.farm_id,
-        zone_id: 46,
-        crop_code: 'LCH',
-    }
-    if (!params) {
-        cropArchiveList.value = [];
-        return;
-    }
-    const { zone_name: zoneName, ...query } = params;
-    try {
-        const res = await VE_API.entry.getPhenoRecord(query);
-        if (res?.code !== 200) {
-            cropArchiveList.value = [];
-            return;
-        }
-        const list = Array.isArray(res.data) ? res.data : [];
-        const fallbackZone = zoneName || t("agriFile.zoneOne");
-        cropArchiveList.value = list
-            .map((item, index) => mapPhenoRecordToArchive(item, index, fallbackZone))
-            .sort((a, b) => String(b.date).localeCompare(String(a.date)));
-    } catch (e) {
-        console.warn("[agri_file] getPhenoRecord failed", e);
-        cropArchiveList.value = [];
-    }
-};
-
-const setMarkerEl = (id, el) => {
-    if (el) markerEls[id] = el;
-    else delete markerEls[id];
-};
-
-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 parsePointCoordinates(pointWkt) {
-    if (!pointWkt || !/^POINT\s*\(/i.test(String(pointWkt).trim())) {
-        return null;
-    }
-    try {
-        const coord = util.wktCastGeom(pointWkt).getFirstCoordinate();
-        if (!coord || coord.length < 2) return null;
-        return { longitude: coord[0], latitude: coord[1] };
-    } catch {
-        return null;
-    }
-}
-
-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 mapZoneItem(zone) {
-    const coords = parsePointCoordinates(zone?.point);
-    return {
-        id: zone.zone_id,
-        zoneId: 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,
-    };
-}
-
-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("[agri_file] getZones failed", e);
-        zoneList.value = [];
-    }
-}
-
-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
-    }
-    const firstZone = zoneList.value.find(
-        (z) => z.longitude != null && z.latitude != null
-    );
-    if (firstZone) {
-        return [firstZone.longitude, firstZone.latitude];
-    }
-    return DEFAULT_CENTER;
-};
-
-const clearMapOverlays = () => {
-    if (!fileMap.kmap?.map) {
-        mapOverlays.length = 0;
-        return;
-    }
-    mapOverlays.forEach((overlay) => fileMap.kmap.map.removeOverlay(overlay));
-    mapOverlays.length = 0;
-};
-
-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 initAlbumMap = async () => {
-    await nextTick();
-    if (!mapContainer.value) return;
-    if (noFarmRange.value) {
-        zoneList.value = [];
-        albumList.value = [];
-        return;
-    }
-    await fetchZoneList();
-    const center = getFarmCenter();
-    fileMap.initMap(`POINT(${center[0]} ${center[1]})`, mapContainer.value);
-    await nextTick();
-    bindZoneOverlays();
-    fileMap.kmap?.map?.updateSize?.();
-};
-
-const fillMockLabels = () => {
-    const albumName = t("agriFile.zoneOne");
-    albumList.value = albumList.value.map((item) => ({ ...item, name: albumName }));
-    reportList.value = reportList.value.map((item) => ({
-        ...item,
-        title: t("agriFile.reportTitle"),
-        desc: t("agriFile.reportDesc"),
-        date: t("agriFile.reportDate"),
-    }));
-    patrolList.value = patrolList.value.map((item, index) => {
-        // 第一条由外部感知任务接口填充,不覆盖
-        if (index === 0 && item.interactCode) return item;
-        return {
-            ...item,
-            level: t("agriFile.riskLevel2"),
-            title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
-            zone: albumName,
-            subject: t("agriFile.patrolSubject"),
-            issue: t("agriFile.patrolIssue"),
-        };
-    });
-};
-
-const goAddZone = (type) => {
-    router.push(
-        {
-            path: "/add_zone",
-            query: {
-                type,
-                isSuccessPopup: type === 'farm' ? '1' : '0',
-            },
-        }
-    );
-};
-
-const goAlbumMap = () => {
-    router.push("/album_map");
-};
-
-const goGrowthTrack = (item) => {
-    router.push({
-        path: "/growth_track",
-        query: {
-            id: item?.id,
-            type: item?.type || "growth",
-            pageName: item.type === 'growth' ? t("agriFile.patrolGrowthTitle") : t("agriFile.patrolAbnormalTitle"),
-        },
-    });
-};
-
-const handleShare = (item) => {
-    const query = {
-        askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
-        shareText: item?.title || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
-        targetUrl: "growth_track",
-        paramsPage: JSON.stringify({
-            id: item?.id,
-            type: item?.type || "growth",
-            fromShare: 1,
-        }),
-        imageUrl: "https://birdseye-img.sysuimars.com/temp/field.png",
-    };
-    wx.miniProgram.navigateTo({
-        url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
-    });
-};
-
-const needDraw = ref(true);
-
-const openUploadPopup = () => {
-    showUploadPopup.value = true;
-};
-
-/** 与 growthTrack 上传一致:需勾画 → add_zone;异常 → pest_interact;否则弹上传 */
-const handleUploadClick = (item) => {
-    const isAbnormal = item?.type === "abnormal";
-    if (needDraw.value) {
-        router.push({
-            path: "/add_zone",
-            query: {
-                type: "farm",
-                targetUrl: isAbnormal ? "/pest_interact" : "/growth_track",
-                ...(isAbnormal ? { isAbnormal: "true" } : {}),
-                ...(item?.id != null ? { trackId: item.id } : {}),
-                ...(item?.type ? { trackType: item.type } : {}),
-            },
-        });
-        return;
-    }
-    if (isAbnormal) {
-        router.push({ path: "/pest_interact" });
-        return;
-    }
-    showUploadPopup.value = true;
-};
 
 const dismissGuide = () => {
     if (!guideVisible.value) return;
@@ -636,6 +164,11 @@ const tryShowGuide = () => {
     guideVisible.value = true;
 };
 
+// ---------- 事件处理 ----------
+const openUploadPopup = () => {
+    showUploadPopup.value = true;
+};
+
 const handleGuideUpload = () => {
     dismissGuide();
     openUploadPopup();
@@ -662,43 +195,35 @@ const goCompleteFarmInfo = () => {
     router.push("/entry_information");
 };
 
-const goAlbumDetail = (item) => {
-    router.push({
-        path: "/region_albums",
-        query: {
-            id: item?.id,
-            name: item?.name || "",
-            noImage: item?.count ? "false" : "true",
-        },
-    });
+const handleProxyReject = () => {
+    // TODO: 调用拒绝代管接口
+};
+
+const handleProxyAllow = () => {
+    // TODO: 调用允许代管接口
 };
 
+const handlePhenologyUpdateReject = () => {
+    // TODO: 调用暂不更新接口
+};
+
+const handlePhenologyUpdateConfirm = () => {
+    // TODO: 调用确认更新接口
+};
+
+// ---------- 生命周期 ----------
 onMounted(() => {
-    fillMockLabels();
     checkCompleteFarmPopup();
-    fetchPatrolInteractTask();
-    initAlbumMap();
     tryShowGuide();
     document.addEventListener("click", handleOutsideClick, true);
 });
 onActivated(() => {
-    fillMockLabels();
     checkCompleteFarmPopup();
-    fetchPatrolInteractTask();
-    fetchCropArchive();
-    initAlbumMap();
     tryShowGuide();
 });
 onBeforeUnmount(() => {
-    clearMapOverlays();
     document.removeEventListener("click", handleOutsideClick, true);
 });
-
-watch(activeNav, (key) => {
-    if (key === "album") {
-        initAlbumMap();
-    }
-});
 </script>
 
 <style lang="scss" scoped>
@@ -709,7 +234,7 @@ watch(activeNav, (key) => {
     width: 100%;
     height: 100%;
     overflow: hidden;
-    background: linear-gradient(180deg, #D1EBFF 0%, #FFFFFF 100%);
+    background: linear-gradient(180deg, #d1ebff 0%, #ffffff 100%);
 
     .nav-cards {
         position: relative;
@@ -737,7 +262,7 @@ watch(activeNav, (key) => {
             background: #fff;
             width: 90px;
             height: 64px;
-            box-shadow: 0px 4px 4px 0px #0000000D;
+            box-shadow: 0px 4px 4px 0px #0000000d;
 
             .nav-card__icon {
                 width: 26px;
@@ -746,7 +271,7 @@ watch(activeNav, (key) => {
             }
 
             &.is-active {
-                background: linear-gradient(180deg, #5BB8FF 0%, #2199F8 100%);
+                background: linear-gradient(180deg, #5bb8ff 0%, #2199f8 100%);
                 box-shadow: 0 4px 10px rgba(33, 153, 248, 0.28);
 
                 .nav-card__icon {
@@ -765,7 +290,7 @@ watch(activeNav, (key) => {
                     transform: translateX(-50%);
                     border-left: 6px solid transparent;
                     border-right: 6px solid transparent;
-                    border-top: 6px solid #2199F8;
+                    border-top: 6px solid #2199f8;
                     z-index: 3;
                 }
             }
@@ -816,457 +341,12 @@ watch(activeNav, (key) => {
         overflow-y: auto;
         background: linear-gradient(360deg, rgba(229, 243, 255, 0.55) 0%, rgba(255, 255, 255, 0.55) 100%);
         border-radius: 20px 20px 0 0;
-        border: 1px solid #FFFFFF;
+        border: 1px solid #ffffff;
         padding: 18px 10px;
 
         &::-webkit-scrollbar {
             display: none;
         }
-
-        .map-card {
-            position: relative;
-            height: 210px;
-        }
-
-        .map-container {
-            width: 100%;
-            height: 100%;
-            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;
-            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;
-            }
-        }
-
-        .add-zone-btn {
-            position: absolute;
-            right: 5px;
-            bottom: 10px;
-            z-index: 5;
-            display: flex;
-            align-items: center;
-            gap: 2px;
-            padding: 5px 10px;
-            border-radius: 4px;
-            background: #fff;
-            color: #2199F8;
-            font-weight: 500;
-        }
-
-        .album-grid {
-            display: grid;
-            grid-template-columns: 1fr 1fr;
-            gap: 16px;
-            margin-top: 16px;
-            box-sizing: border-box;
-
-            .album-add {
-                align-self: start;
-                display: flex;
-                flex-direction: column;
-                align-items: center;
-                justify-content: center;
-                width: 100%;
-                aspect-ratio: 1;
-                border-radius: 5px;
-                background: #FFFFFF;
-                color: #969494;
-                font-size: 16px;
-
-                &__icon {
-                    margin-bottom: 6px;
-                    font-size: 20px;
-                }
-            }
-
-            .album-card {
-                width: 100%;
-
-                &.is-empty {
-                    .album-card__cover {
-                        display: flex;
-                        align-items: center;
-                        justify-content: center;
-                        overflow: hidden;
-                        border-radius: 8px;
-                        background: linear-gradient(180deg, #E0F1FF 0%, #C2E4FF 100%);
-                    }
-                }
-
-                &__cover {
-                    position: relative;
-                    width: 100%;
-                    aspect-ratio: 1;
-
-                    img:not(.album-card__empty-icon) {
-                        width: 100%;
-                        height: 100%;
-                        object-fit: cover;
-                        border-radius: 6px;
-                    }
-                }
-
-                &__empty-icon {
-                    width: 50px;
-                    height: 50px;
-                }
-
-                &__count {
-                    position: absolute;
-                    top: 1px;
-                    right: 0;
-                    padding: 1px 5px;
-                    border-radius: 0 5px 0 5px;
-                    background: rgba(0, 0, 0, 0.4);
-                    backdrop-filter: blur(4px);
-                    color: #fff;
-                }
-
-                &__add {
-                    position: absolute;
-                    right: 8px;
-                    bottom: 8px;
-                    z-index: 1;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                    width: 44px;
-                    height: 44px;
-                    border-radius: 50%;
-                    background: #2199F8;
-                    color: #fff;
-                    font-size: 20px;
-                    box-shadow: 0 2px 8px rgba(33, 153, 248, 0.35);
-                }
-
-                &__name {
-                    margin-top: 10px;
-                    color: #1F1F1F;
-                }
-            }
-        }
-
-        .report-card {
-            margin-bottom: 10px;
-            border-radius: 8px;
-            background: #fff;
-
-            &:last-child {
-                margin-bottom: 0;
-            }
-
-            &__cover {
-                position: relative;
-                width: 100%;
-                height: 158px;
-
-                img {
-                    display: block;
-                    width: 100%;
-                    height: 100%;
-                    object-fit: cover;
-                    border-radius: 5px;
-                }
-            }
-
-            &__date {
-                position: absolute;
-                top: 0;
-                padding: 5px 12px;
-                border-radius: 4px;
-                background: rgba(0, 0, 0, 0.45);
-                backdrop-filter: blur(4px);
-                color: #fff;
-                font-size: 16px;
-                border-radius: 5px 0 5px 0;
-            }
-
-            &__status {
-                position: absolute;
-                top: 10px;
-                right: 0;
-                display: flex;
-                align-items: center;
-                gap: 4px;
-                padding: 2px 13px;
-                border-radius: 20px 0 0 20px;
-                background: #FF5454;
-                color: #fff;
-                font-size: 12px;
-            }
-
-            &__dot {
-                width: 6px;
-                height: 6px;
-                border-radius: 50%;
-                background: #fff;
-            }
-
-            &__body {
-                padding: 10px;
-                margin-top: 4px;
-            }
-
-            &__title {
-                font-size: 16px;
-                font-weight: 500;
-            }
-
-            &__desc {
-                margin-top: 4px;
-                color: #9E9B9B;
-            }
-        }
-
-        .tracking-item {
-            width: 100%;
-            margin-bottom: 16px;
-            box-shadow: 0px 4px 4px 0px rgba(33, 153, 248, 0.1);
-
-
-            &:last-of-type {
-                margin-bottom: 0;
-            }
-
-            &__header {
-                position: relative;
-                padding: 10px 10px 20px;
-                border-radius: 10px 10px 0 0;
-                background: linear-gradient(180deg, #86CAFF 0%, #2199F8 48.5%);
-            }
-
-            &__header-row {
-                display: flex;
-                align-items: center;
-                justify-content: space-between;
-            }
-
-            &__header-left {
-                display: flex;
-                align-items: center;
-                gap: 5px;
-            }
-
-            &__level {
-                display: flex;
-                align-items: center;
-                gap: 4px;
-                padding: 1px 4px;
-                border-radius: 2px;
-                background: #fff;
-                color: #FF6A6A;
-                font-size: 12px;
-            }
-
-            &__title {
-                color: #fff;
-                font-size: 16px;
-                font-weight: 500;
-            }
-
-            &__zone {
-                font-size: 12px;
-            }
-
-            &__share {
-                display: flex;
-                align-items: center;
-                gap: 4px;
-                padding: 2px 10px;
-                border-radius: 25px;
-                background: #fff;
-                color: #2199F8;
-                font-size: 12px;
-            }
-
-            &__body {
-                position: relative;
-                padding: 10px;
-                margin-top: -8px;
-                border-radius: 10px;
-                background: #fff;
-
-                &::before {
-                    content: "";
-                    position: absolute;
-                    top: -6px;
-                    left: 21%;
-                    width: 0;
-                    height: 0;
-                    border-left: 7px solid transparent;
-                    border-right: 7px solid transparent;
-                    border-bottom: 7px solid #fff;
-                    transform: translateX(-50%);
-                }
-            }
-
-            &__action {
-                display: flex;
-                align-items: center;
-                justify-content: space-between;
-                border: 1px solid rgba(33, 153, 248, 0.2);
-                border-radius: 6px;
-                padding: 8px;
-            }
-
-            &__icon {
-                display: flex;
-                align-items: center;
-                gap: 5px;
-
-                img {
-                    width: 18px;
-                    height: 16px;
-                }
-            }
-
-            &__reason {
-                color: #2199f8;
-                font-weight: 500;
-            }
-
-            &__issue {
-                margin-top: 2px;
-                color: #2199f8;
-                font-size: 12px;
-            }
-
-            &__action-main {
-                flex: 1;
-                padding-right: 4px;
-            }
-
-            &__btn {
-                flex: none;
-                display: flex;
-                align-items: center;
-                gap: 2px;
-                padding: 7px 10px;
-                border-radius: 5px;
-                background: #2199f8;
-                color: #fff;
-                font-size: 12px;
-            }
-        }
-
-        .crop-archive {
-            padding: 10px;
-            border-radius: 10px;
-            background: #fff;
-
-            &__title {
-                position: relative;
-                display: inline-block;
-                z-index: 1;
-                font-size: 16px;
-                font-weight: 500;
-                margin-bottom: 5px;
-
-                &::after {
-                    content: "";
-                    position: absolute;
-                    left: 0;
-                    bottom: 2px;
-                    z-index: -1;
-                    width: 90px;
-                    height: 6px;
-                    border-radius: 25px;
-                    background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
-                    opacity: 0.4; 
-                }
-            }
-        }
     }
 }
 </style>

+ 4 - 2
src/views/old_mini/work_execute/index.js

@@ -66,6 +66,7 @@ class IndexMap {
     let that = this;
     let vectorStyle = new KMap.VectorStyle();
     this.vectorStyle = vectorStyle;
+    this.onlyName = false;
     this.labelStyleCache = {};
 
     this.borderStyle = new Style({
@@ -83,7 +84,7 @@ class IndexMap {
       minZoom: 6,
       maxZoom: 22,
       style: (feature) => {
-        const labelText = (feature.get("executeDate") + ' ' + feature.get("farmName")) || feature.get("mapInfo") || "";
+        const labelText = this.onlyName ? feature.get("farmName") : ((feature.get("executeDate") + ' ' + feature.get("farmName")) || feature.get("mapInfo") || "");
         if (!this.labelStyleCache[labelText]) {
           this.labelStyleCache[labelText] = new Style({
             image: new Icon({
@@ -109,9 +110,10 @@ class IndexMap {
     this.kmap.addLayer(this.gardenPointLayer.layer);
   }
 
-  initData(taskList, label, pointType = "point") {
+  initData(taskList, label, pointType = "point", onlyNameVal = false) {
     this.labelStyleCache = {};
     this.gardenPointLayer.source.clear();
+    this.onlyName = onlyNameVal;
     if (taskList.length > 0) {
       for (let item of taskList) {
         item.mapInfo = label

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

@@ -69,6 +69,12 @@
         </div>
         <farm-calendar-popup v-model:show="showFarmCalendarPopup" @confirm="goCompleteFarmInfo" />
         <survey-ask-popup v-model:show="showSurveyAskPopup" @confirm="handleSurveyConfirm" />
+
+        <!-- 诊断报告弹窗:组件内接口判断 exists=2 且本地未展示过 -->
+        <diagnosis-report-popup />
+
+        <!-- 恢复种植弹窗 -->
+        <restore-planting-popup />
     </div>
 </template>
 
@@ -81,9 +87,12 @@ 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 diagnosisReportPopup from "@/components/popup/diagnosisReportPopup.vue";
+import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
 import { useI18n } from "@/i18n";
 import wx from "weixin-js-sdk";
 
+
 // ---------- 常量 ----------
 /** zoneId / date 临时固定,后续改为当前分区 / 当天 */
 const QUERY = {
@@ -118,6 +127,10 @@ const completedCount = computed(() => dayTaskList.value.filter((item) => item.st
 const displayTaskList = computed(() => dayTaskList.value.filter((item) => item.status === activeTab.value));
 
 // ---------- 工具函数 ----------
+function getMiniUserId() {
+    return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
+}
+
 const isCompletedStatus = (status) => COMPLETED_STATUS_RE.test(String(status || ""));
 
 const formatExecuteDate = (dateStr) => {
@@ -170,6 +183,23 @@ const resolveRangeStart = (data) => {
 };
 
 // ---------- 业务逻辑 ----------
+/** 无果园信息时展示「完善农场信息 / 专属农事日历」弹窗 */
+async function checkFarmCalendarPopup() {
+    const id = getMiniUserId();
+    if (!id) {
+        showFarmCalendarPopup.value = false;
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.getFarms({ id });
+        const list = Array.isArray(res?.data) ? res.data : [];
+        showFarmCalendarPopup.value = list.length === 0;
+    } catch (e) {
+        console.warn("[work_execute] getFarms failed", e);
+        showFarmCalendarPopup.value = false;
+    }
+}
+
 const refreshMapMarkers = () => {
     if (!indexMap.kmap) return;
     indexMap.initData(getMapMarkerList(), "farmName", "wkt");
@@ -295,6 +325,7 @@ const initMap = () => {
 };
 
 onMounted(() => {
+    checkFarmCalendarPopup();
     nextTick(async () => {
         await fetchFwTasks();
         initMap();
@@ -302,6 +333,7 @@ onMounted(() => {
 });
 
 onActivated(() => {
+    checkFarmCalendarPopup();
     nextTick(() => {
         if (!indexMap.kmap) {
             initMap();