Procházet zdrojové kódy

fix: 对接首页,异常

lxf před 11 hodinami
rodič
revize
98238fbdd7

+ 15 - 0
src/api/modules/questionnaire.js

@@ -117,4 +117,19 @@ module.exports = {
         url: config.base_new_url + "questionnaire/update_zone_polygon",
         type: "post",
     },
+    // 首页气象底图
+    getHomeMapCalc: {
+        url: config.base_new_url + "questionnaire/get_map_calc",
+        type: "get",
+    },
+    // 异常互动查询
+    getAbnormalInteract: {
+        url: config.base_new_url + "questionnaire/abnormal_interact_info",
+        type: "get",
+    },
+    // 异常问题确认
+    confirmAbnormalInteract: {
+        url: config.base_new_url + "questionnaire/abnormal_fw_answer",
+        type: "post",
+    },
 }

binární
src/assets/img/map/default.png


+ 8 - 1
src/router/globalRoutes.js

@@ -172,13 +172,20 @@ export default [
         meta: { keepAlive: true },
         component: () => import("@/components/allGarden.vue"),
     },
-    // 长势跟踪记录
+    // 感知任务巡园要点
     {
         path: "/growth_track",
         name: "GrowthTrack",
         meta: { keepAlive: true },
         component: () => import("@/views/old_mini/agri_file/pages/growthTrack.vue"),
     },
+    // 异常互动巡园要点
+    {
+        path: "/abnormal_track",
+        name: "AbnormalTrack",
+        meta: { keepAlive: true },
+        component: () => import("@/views/old_mini/agri_file/pages/abnormalTrack.vue"),
+    },
     // 定位校验通过桥接(小程序写 localStorage 后回退)
     {
         path: "/location_check_pass",

+ 1 - 1
src/views/old_mini/agri_file/components/albumPanel.vue

@@ -87,7 +87,7 @@ const { t } = useI18n();
 const router = useRouter();
 
 // ---------- 常量 ----------
-const defaultCover = require("@/assets/img/agricultural/photo.png");
+const defaultCover = require("@/assets/img/map/default.png");
 // const defaultCover = "https://birdseye-img-ali-cdn.sysuimars.com/birdseye-look-mini/25862/1751598966682.png";
 const DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
 

+ 125 - 19
src/views/old_mini/agri_file/components/patrolPanel.vue

@@ -111,16 +111,6 @@ const patrolList = ref([
         desc: "",
         icon: PATROL_ICON,
     },
-    {
-        id: 2,
-        type: "abnormal",
-        theme: "blue",
-        level: "",
-        title: "",
-        subject: "",
-        issue: "",
-        icon: PATROL_ICON,
-    },
 ]);
 const cropArchiveList = ref([]);
 const showUploadPopup = ref(false);
@@ -128,6 +118,18 @@ const showSuccessPopup = ref(false);
 const uploadZoneId = ref("");
 
 // ---------- 工具函数 ----------
+function formatToday() {
+    const d = new Date();
+    const y = d.getFullYear();
+    const m = String(d.getMonth() + 1).padStart(2, "0");
+    const day = String(d.getDate()).padStart(2, "0");
+    return `${y}-${m}-${day}`;
+}
+
+/** 农场品类编码,接口 crop_type */
+const resolveCropType = (farm = getSelectedFarm()) =>
+    farm.category_code || farm.categorycode || farm.crop_code || farm.crop_type || "";
+
 /** 外部感知任务 → 巡园卡片(仅取第一条) */
 const mapInteractTaskToPatrol = (task) => {
     const content = task?.interact_content || {};
@@ -147,6 +149,25 @@ const mapInteractTaskToPatrol = (task) => {
     };
 };
 
+/** 异常互动第一条 → 异常巡园卡片 */
+const mapAbnormalInteractToPatrol = (info, stressInfo) => {
+    const level = stressInfo?.level;
+    return {
+        id: 2,
+        type: "abnormal",
+        theme: "blue",
+        level: level != null && level !== "" ? `${level}级` : "",
+        title: t("agriFile.patrolAbnormalTitle") || "",
+        subject: info?.interaction_title || "",
+        issue: info?.interaction_theme || "",
+        icon: PATROL_ICON,
+        zone: selectedZoneName.value || "",
+        zoneId: selectedZoneId.value,
+        fwCode: info?.fw_code,
+        raw: info,
+    };
+};
+
 const mapPhenoRecordToArchive = (item, index, zoneName) => ({
     id: [item.zone_id, item.enter_date, item.period_code, index]
         .filter((v) => v != null && v !== "")
@@ -198,6 +219,17 @@ const clearFirstPatrolInteract = () => {
     };
 };
 
+/** ab_it_info 为空对象时不展示异常卡片 */
+const isEmptyAbInfo = (abInfo) => {
+    if (!abInfo || typeof abInfo !== "object" || Array.isArray(abInfo)) return true;
+    return Object.keys(abInfo).length === 0;
+};
+
+/** 移除异常卡片(无数据时不占位) */
+const clearAbnormalPatrol = () => {
+    patrolList.value = patrolList.value.filter((item) => item.type !== "abnormal");
+};
+
 // ---------- 业务逻辑:加载 ----------
 const applySelectedZone = (zone) => {
     selectedZoneId.value = zone?.zone_id ?? null;
@@ -253,6 +285,60 @@ const fetchPatrolInteractTask = async () => {
     }
 };
 
+/** 异常互动 → 第二张巡园卡片(默认 interact_Info 第一项;ab_it_info 为空则不展示) */
+const fetchAbnormalInteract = async () => {
+    const farm = getSelectedFarm();
+    const farmId = getSelectedFarmId();
+    const zoneId = selectedZoneId.value;
+    const cropType = resolveCropType(farm);
+    if (farmId == null || zoneId == null || zoneId === "" || !cropType) {
+        clearAbnormalPatrol();
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.getAbnormalInteract({
+            crop_type: cropType,
+            // farm_id: farmId,
+            // zone_id: zoneId,
+            // calc_date: formatToday(),
+            farm_id: 321,
+            zone_id: 46,
+            calc_date: '2025-08-30',
+        });
+        if (res?.code !== 200) {
+            clearAbnormalPatrol();
+            return;
+        }
+        const data = res?.data || {};
+        const abInfo = data.ab_it_info;
+        // ab_it_info 为 {} / 空 / 无 interact_Info 时不展示异常卡片
+        if (isEmptyAbInfo(abInfo)) {
+            clearAbnormalPatrol();
+            return;
+        }
+        const list = Array.isArray(abInfo.interact_Info) ? abInfo.interact_Info : [];
+        const first = list[0];
+        if (!first) {
+            clearAbnormalPatrol();
+            return;
+        }
+        const stressInfo = abInfo.stress_info || data.stress_info || {};
+        const mapped = mapAbnormalInteractToPatrol(first, stressInfo);
+        const idx = patrolList.value.findIndex((item) => item.type === "abnormal");
+        if (idx >= 0) {
+            patrolList.value[idx] = {
+                ...patrolList.value[idx],
+                ...mapped,
+            };
+        } else {
+            patrolList.value.push(mapped);
+        }
+    } catch (e) {
+        console.warn("[patrolPanel] getAbnormalInteract failed", e);
+        clearAbnormalPatrol();
+    }
+};
+
 const fetchCropArchive = async () => {
     const farm = getSelectedFarm();
     const zoneId = selectedZoneId.value;
@@ -287,6 +373,8 @@ const fillMockLabels = () => {
     patrolList.value = patrolList.value.map((item, index) => {
         // 第一条由外部感知任务接口填充,不覆盖
         if (index === 0 && item.interactCode) return item;
+        // 异常卡片由 getAbnormalInteract 填充,不覆盖
+        if (item.type === "abnormal" && item.fwCode) return item;
         return {
             ...item,
             level: t("agriFile.riskLevel2"),
@@ -301,17 +389,21 @@ const fillMockLabels = () => {
 /** 按当前选中品种刷新巡园卡片与作物档案 */
 const reloadBySelectedZone = () => {
     clearFirstPatrolInteract();
+    clearAbnormalPatrol();
     fillMockLabels();
     fetchPatrolInteractTask();
+    fetchAbnormalInteract();
     fetchCropArchive();
 };
 
 const loadPatrolData = async () => {
     // 先清掉上一农场感知任务,避免 fillMockLabels 因 interactCode 保留旧数据
     clearFirstPatrolInteract();
+    clearAbnormalPatrol();
     await fetchZones();
     fillMockLabels();
     fetchPatrolInteractTask();
+    fetchAbnormalInteract();
     fetchCropArchive();
 };
 
@@ -330,28 +422,32 @@ const goAddVariety = () => {
 };
 
 const goGrowthTrack = (item) => {
+    const isAbnormal = item?.type === "abnormal";
     router.push({
-        path: "/growth_track",
+        path: isAbnormal ? "/abnormal_track" : "/growth_track",
         query: {
             id: item?.id,
-            type: item?.type || "growth",
-            pageName:
-                item.type === "growth"
-                    ? t("agriFile.patrolGrowthTitle")
-                    : t("agriFile.patrolAbnormalTitle"),
+            ...(item?.zoneId != null && item?.zoneId !== "" ? { zoneId: item.zoneId } : {}),
+            pageName: isAbnormal
+                ? t("agriFile.patrolAbnormalTitle")
+                : t("agriFile.patrolGrowthTitle"),
         },
     });
 };
 
 const handleShare = (item) => {
+    const isAbnormal = item?.type === "abnormal";
     const query = {
         askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
         shareText: item?.title || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
-        targetUrl: "growth_track",
+        targetUrl: isAbnormal ? "abnormal_track" : "growth_track",
         paramsPage: JSON.stringify({
             id: item?.id,
-            type: item?.type || "growth",
             fromShare: 1,
+            ...(item?.zoneId != null && item?.zoneId !== "" ? { zoneId: item.zoneId } : {}),
+            pageName: isAbnormal
+                ? t("agriFile.patrolAbnormalTitle")
+                : t("agriFile.patrolGrowthTitle"),
         }),
         imageUrl: "https://birdseye-img.sysuimars.com/temp/field.png",
     };
@@ -401,6 +497,7 @@ onActivated(loadPatrolData);
 
 defineExpose({
     fetchPatrolInteractTask,
+    fetchAbnormalInteract,
     fetchCropArchive,
     loadPatrolData,
 });
@@ -418,13 +515,22 @@ defineExpose({
         &__tags {
             display: flex;
             align-items: center;
-            flex-wrap: wrap;
+            flex-wrap: nowrap;
             gap: 6px;
             flex: 1;
             min-width: 0;
+            overflow-x: auto;
+            -webkit-overflow-scrolling: touch;
+            scrollbar-width: none;
+
+            &::-webkit-scrollbar {
+                display: none;
+            }
         }
 
         &__tag {
+            flex: none;
+            white-space: nowrap;
             padding: 4px 16px;
             border-radius: 4px;
             background: #fff;

+ 740 - 0
src/views/old_mini/agri_file/pages/abnormalTrack.vue

@@ -0,0 +1,740 @@
+<template>
+    <div class="abnormal-track-page">
+        <custom-header
+            :name="pageTitle"
+            :isGoBack="!isFromShare"
+            :isClose="isFromShare"
+            :showClose="false"
+            @goback="handleBack"
+        />
+        <div class="abnormal-track-body" v-loading="loading">
+            <div class="abnormal-hero">
+                <div class="abnormal-hero__top">
+                    <div class="abnormal-hero__main">
+                        <div class="abnormal-hero__title-row">
+                            <span class="abnormal-hero__title">{{ theme }}</span>
+                            <span class="abnormal-hero__tag abnormal-hero__tag--level">
+                                <el-icon>
+                                    <BellFilled />
+                                </el-icon>
+                                {{ level }}
+                            </span>
+                            <span class="abnormal-hero__tag abnormal-hero__tag--zone">{{ zoneName }}</span>
+                        </div>
+                        <div class="abnormal-head-bottom">
+                          <div class="abnormal-hero__reason">{{ reason }}</div>
+
+                          <div class="abnormal-hero__share" @click="handleShare">
+                                <Icon name="share" />
+                                <span>{{ t("agriFile.forward") }}</span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="abnormal-track-content">
+                <div
+                    class="situation-card__head"
+                    v-for="(item, index) in interactList"
+                    :key="item.fw_code || index"
+                >
+                    <div class="situation-card__name">
+                        <div class="situation-card__title">
+                            <img
+                                class="situation-card__icon"
+                                src="@/assets/img/agricultural/gaojin.png"
+                                alt=""
+                            />
+                            <span>{{ item.abnormal_type }}</span>
+                        </div>
+                        <div class="situation-card__upload" @click="handleUploadClick">
+                            + {{ t("agriFile.uploadPhoto") }}
+                        </div>
+                    </div>
+                    <!-- 互动问题 -->
+                    <div class="interact-qa">
+                        <div class="interact-qa__head">
+                            <span class="interact-qa__tag">{{ t("agriFile.interactQuestion") }}</span>
+                            <span class="interact-qa__question">{{ item.interact_problem }}</span>
+                        </div>
+                        <div class="interact-qa__actions">
+                            <div
+                                v-for="(opt, optIndex) in item.options"
+                                :key="`${item.fw_code || index}-${optIndex}`"
+                                class="interact-qa__option"
+                                :class="{ selected: selectedMap[item.fw_code || index] === optIndex }"
+                                @click="selectedMap[item.fw_code || index] = optIndex"
+                            >
+                                {{ opt }}
+                            </div>
+                            <div
+                                class="interact-qa__submit"
+                                :class="{ disabled: selectedMap[item.fw_code || index] == null }"
+                                @click="handleSubmit(item, index)"
+                            >
+                                提交信息
+                            </div>
+                        </div>
+                    </div>
+                    <div class="situation-card__assess">
+                        <div class="situation-card__assess-title">{{ t("agriFile.agriAssessment") }}</div>
+                        <div class="situation-card__assess-desc">{{ item.interaction_reason }}</div>
+                    </div>
+                    <div class="situation-card__guide">
+                        <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
+                        <div class="situation-card__guide-box">
+                            <img :src="resolveGuideImage(item.pic_url)" alt="" />
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <album-upload-popup v-model:show="showUploadPopup" @confirm="handleUploadConfirm" />
+        <tip-popup
+            v-model:show="showSuccessPopup"
+            type="executeSuccess"
+            text="您已上传成功"
+            text2="请等待诊断报告生成"
+            hideBtn
+        >
+            <template #footer>
+                <div class="bottom-btn-container">
+                    <div class="bottom-btn primary-btn" @click="handleComplete">完成</div>
+                </div>
+            </template>
+        </tip-popup>
+    </div>
+</template>
+
+<script setup>
+import { computed, nextTick, onActivated, reactive, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { Icon } from "vant";
+import { ElMessage } from "element-plus";
+import customHeader from "@/components/customHeader.vue";
+import albumUploadPopup from "../components/albumUploadPopup.vue";
+import tipPopup from "@/components/popup/tipPopup.vue";
+import { useI18n } from "@/i18n";
+import { base_img_url2 } from "@/api/config";
+import wx from "weixin-js-sdk";
+
+const { t } = useI18n();
+const route = useRoute();
+const router = useRouter();
+
+// ---------- 常量 ----------
+const DEFAULT_GUIDE_IMAGE = require("@/assets/img/agricultural/patrol-guide.png");
+
+// ---------- 页面状态 ----------
+const loading = ref(false);
+const submitting = ref(false);
+const showUploadPopup = ref(false);
+const showSuccessPopup = ref(false);
+const needDraw = ref(true); // status≠1 时需先勾画种植范围
+const interactList = ref([]); // ab_it_info.interact_Info 映射后的列表
+const stressInfo = ref({}); // 胁迫等级等公共信息
+const selectedMap = reactive({}); // fw_code/index → 选项下标
+/** 与查询共用的提交上下文,保证 fw 答案与列表同源 */
+const queryContext = ref({
+    farm_id: null,
+    zone_id: null,
+    calc_date: "",
+    crop_type: "",
+});
+
+// ---------- 计算属性 ----------
+const pageTitle = computed(() => route.query.pageName || t("agriFile.patrolAbnormalTitle"));
+const zoneId = computed(() => route.query.zoneId || route.query.id);
+
+const firstInteract = computed(() => interactList.value[0] || null);
+/** interaction_theme */
+const theme = computed(
+    () => firstInteract.value?.interaction_title || t("agriFile.interactTheme")
+);
+/** interaction_title */
+const reason = computed(
+    () => firstInteract.value?.interaction_theme || t("agriFile.interactReason")
+);
+/** stress_info.level */
+const level = computed(() => {
+    const lv = stressInfo.value?.level;
+    if (lv == null || lv === "") return t("agriFile.riskLevel2");
+    return `${lv}级`;
+});
+const zoneName = computed(() => {
+    const farm = getSelectedFarm();
+    return farm.variety_name || farm.zone_name || t("agriFile.zoneOne");
+});
+
+/** 通过分享进入:无返回箭头 */
+const isFromShare = computed(() => {
+    if (isTruthyFlag(route.query.fromShare)) return true;
+    const paramsPage = parseMaybeJson(route.query.paramsPage);
+    if (isTruthyFlag(paramsPage?.fromShare)) return true;
+    const miniJson = parseMaybeJson(route.query.miniJson);
+    if (isTruthyFlag(miniJson?.fromShare)) return true;
+    if (isTruthyFlag(parseMaybeJson(miniJson?.paramsPage)?.fromShare)) return true;
+    return false;
+});
+
+// ---------- 工具函数 ----------
+function isTruthyFlag(value) {
+    return value === 1 || value === "1" || value === true || value === "true";
+}
+
+function parseMaybeJson(value) {
+    if (value == null || value === "") return null;
+    if (typeof value === "object") return value;
+    try {
+        return JSON.parse(value);
+    } catch {
+        return null;
+    }
+}
+
+function getSelectedFarm() {
+    try {
+        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+    } catch {
+        return {};
+    }
+}
+
+function getSelectedFarmId() {
+    const stored = localStorage.getItem("selectedFarmId");
+    if (stored != null && stored !== "") return Number(stored);
+    const farm = getSelectedFarm();
+    const id = farm.farm_id ?? farm.id;
+    return id != null && id !== "" ? Number(id) : null;
+}
+
+/** 农场品类编码,接口 crop_type */
+const resolveCropType = (farm = getSelectedFarm()) =>
+    farm.category_code || farm.categorycode || farm.crop_code || farm.crop_type || "";
+
+function resolveImageUrl(path) {
+    if (!path) return "";
+    const text = String(path).trim();
+    if (!text) return "";
+    if (/^https?:\/\//i.test(text)) return text;
+    return base_img_url2 + text.replace(/^\//, "");
+}
+
+function resolveGuideImage(picUrl) {
+    return resolveImageUrl(picUrl) || DEFAULT_GUIDE_IMAGE;
+}
+
+/** interact_options 可能是 JSON 字符串 */
+function parseInteractOptions(value) {
+    if (Array.isArray(value)) return value.map((name) => String(name));
+    const parsed = parseMaybeJson(value);
+    if (Array.isArray(parsed)) return parsed.map((name) => String(name));
+    return [];
+}
+
+function mapInteractItem(raw) {
+    return {
+        ...raw,
+        options: parseInteractOptions(raw?.interact_options),
+    };
+}
+
+// ---------- 业务逻辑 ----------
+const fetchAbnormalInteract = async () => {
+    const farm = getSelectedFarm();
+    const farmId = getSelectedFarmId();
+    const zId = zoneId.value || farm.zone_id || farm.zoneId;
+    const cropType = resolveCropType(farm);
+    if (farmId == null || zId == null || zId === "" || !cropType) {
+        interactList.value = [];
+        stressInfo.value = {};
+        return;
+    }
+    // 与 patrolPanel 调试参数保持一致;正式环境改回 farmId / zId / 当天日期
+    const params = {
+        crop_type: cropType,
+        farm_id: 321,
+        zone_id: 46,
+        calc_date: "2025-08-30",
+    };
+    queryContext.value = { ...params };
+    loading.value = true;
+    try {
+        const res = await VE_API.questionnaire.getAbnormalInteract(params);
+        if (res?.code !== 200) {
+            interactList.value = [];
+            stressInfo.value = {};
+            return;
+        }
+        const data = res?.data || {};
+        const abInfo = data.ab_it_info || {};
+        const list = Array.isArray(abInfo.interact_Info) ? abInfo.interact_Info : [];
+        interactList.value = list.map(mapInteractItem);
+        stressInfo.value = abInfo.stress_info || data.stress_info || {};
+    } catch (e) {
+        console.warn("[abnormalTrack] getAbnormalInteract failed", e);
+        interactList.value = [];
+        stressInfo.value = {};
+    } finally {
+        loading.value = false;
+    }
+};
+
+/** status=1 有范围无需勾画;status=0 无范围需要勾画 */
+const fetchZoneRangeStatus = async () => {
+    const farm = getSelectedFarm();
+    const farmId = farm.farm_id ?? farm.id;
+    const zId = zoneId.value || farm.zone_id || farm.zoneId;
+    if (farmId == null || farmId === "" || zId == null || zId === "") {
+        needDraw.value = true;
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.checkZoneRange({
+            farm_id: farmId,
+            zone_id: zId,
+        });
+        needDraw.value = Number(res?.data?.status) !== 1;
+    } catch (e) {
+        console.warn("[abnormalTrack] checkZoneRange failed", e);
+        needDraw.value = true;
+    }
+};
+
+// ---------- 事件处理 / 生命周期 ----------
+const handleShare = () => {
+    const query = {
+        askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
+        shareText: theme.value || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
+        targetUrl: "abnormal_track",
+        paramsPage: JSON.stringify({
+            id: route.query.id,
+            zoneId: zoneId.value,
+            fromShare: 1,
+            ...(route.query.pageName ? { pageName: route.query.pageName } : {}),
+        }),
+        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) */
+const handleUploadClick = () => {
+    if (needDraw.value) {
+        router.push({
+            path: "/add_zone",
+            query: {
+                type: "farm",
+                targetUrl: "/abnormal_track",
+                ...(zoneId.value ? { trackId: zoneId.value } : {}),
+            },
+        });
+        return;
+    }
+    showUploadPopup.value = true;
+    // 原异常逻辑:勾画后进病虫害选地块 / 直接进 pest_interact
+    // if (needDraw.value) {
+    //     router.push({
+    //         path: "/add_zone",
+    //         query: {
+    //             type: "farm",
+    //             targetUrl: "/pest_interact",
+    //             isAbnormal: "true",
+    //             ...(zoneId.value ? { trackId: zoneId.value } : {}),
+    //         },
+    //     });
+    //     return;
+    // }
+    // router.push({ path: "/pest_interact" });
+};
+
+const handleUploadConfirm = async ({ images, date } = {}) => {
+    const cloud_url = Array.isArray(images) ? images : [];
+    if (!cloud_url.length) {
+        ElMessage.warning("请先上传照片");
+        return;
+    }
+    const farm = getSelectedFarm();
+    try {
+        const res = await VE_API.questionnaire.uploadImages({
+            cloud_url,
+            farm_id: farm.farm_id ?? farm.id ?? "",
+            zone_id: zoneId.value || "",
+            category_code: farm.category_code || "",
+            zone_name: farm.variety_name || "",
+            create_time: date,
+        });
+        if (res?.code === 200) {
+            showSuccessPopup.value = true;
+            return;
+        }
+        ElMessage.error(res?.msg || "上传失败,请稍后再试");
+    } catch {
+        ElMessage.error("上传失败,请稍后再试");
+    }
+};
+
+const handleComplete = () => {
+    showSuccessPopup.value = false;
+    router.replace("/agri_file");
+};
+
+const handleSubmit = async (item, index) => {
+    const key = item?.fw_code || index;
+    const optIndex = selectedMap[key];
+    if (optIndex == null) {
+        ElMessage.warning(t("agriFile.pleaseSelectOption"));
+        return;
+    }
+    const optionText = item?.options?.[optIndex];
+    if (!optionText || !item?.fw_code) {
+        ElMessage.warning(t("agriFile.pleaseSelectOption"));
+        return;
+    }
+    if (submitting.value) return;
+    submitting.value = true;
+    try {
+        const res = await VE_API.questionnaire.confirmAbnormalInteract({
+            ...queryContext.value,
+            fw_code: item.fw_code,
+            options: String(optionText),
+        });
+        if (res?.code === 200) {
+            ElMessage.success(t("agriFile.submitSuccess"));
+            return;
+        }
+        ElMessage.error(res?.msg || "提交失败,请稍后再试");
+    } catch (e) {
+        console.warn("[abnormalTrack] confirmAbnormalInteract failed", e);
+        ElMessage.error("提交失败,请稍后再试");
+    } finally {
+        submitting.value = false;
+    }
+};
+
+const handleBack = () => {
+    router.back();
+};
+
+onActivated(() => {
+    fetchZoneRangeStatus();
+    fetchAbnormalInteract();
+    if (route.query.showUpload !== "1") return;
+    nextTick(() => {
+        showUploadPopup.value = true;
+        const { showUpload, ...restQuery } = route.query;
+        router.replace({ path: route.path, query: restQuery });
+    });
+});
+</script>
+
+<style lang="scss" scoped>
+.abnormal-track-page {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    background: #f5f5f5;
+
+    .abnormal-track-body {
+        flex: 1;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+
+        .abnormal-hero {
+            padding: 18px 10px 38px;
+            background: #2199f8;
+            color: #fff;
+
+            &__top {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+            }
+
+            &__title-row {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+            }
+
+            &__title {
+                font-size: 22px;
+                font-family: "PangMenZhengDao";
+                margin-top: -5px;
+            }
+
+            .abnormal-head-bottom {
+                margin-top: 10px;
+                display: flex;
+                gap: 6px;
+                align-items: center;
+                justify-content: space-between;
+            }
+
+            &__reason {
+                padding: 4px 10px;
+                border-radius: 4px;
+                background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%);
+            }
+
+            &__tag {
+                flex: none;
+                display: inline-flex;
+                align-items: center;
+                gap: 4px;
+                padding: 1px 4px;
+                border-radius: 2px;
+                background: #fff;
+                font-size: 12px;
+            }
+
+            &__tag--level {
+                color: #ff6a6a;
+            }
+
+            &__tag--zone {
+                color: #2199f8;
+            }
+
+            &__share {
+                flex: none;
+                display: flex;
+                align-items: center;
+                gap: 4px;
+                padding: 4px 14px;
+                border-radius: 25px;
+                background: rgba(19, 125, 209, 0.5);
+                color: #fff;
+            }
+        }
+    }
+
+    .guide-title {
+        position: relative;
+        z-index: 1;
+        display: inline-block;
+        font-size: 16px;
+        font-weight: 500;
+
+        &::after {
+            content: "";
+            position: absolute;
+            left: 0;
+            bottom: 2px;
+            z-index: -1;
+            width: 90px;
+            height: 6px;
+            border-radius: 25px;
+            background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
+            opacity: 0.4;
+        }
+    }
+
+    .abnormal-track-content {
+        
+        position: relative;
+        z-index: 1;
+        margin: -24px 10px 12px;
+    }
+
+    .situation-card {
+        &__head {
+            position: relative;
+            z-index: 1;
+            margin: 0 10px 12px;
+            padding: 12px;
+            border-radius: 10px;
+            background: #fff;
+
+            &:first-child {
+                margin-top: -24px;
+            }
+        }
+
+        &__name {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 10px;
+            padding-bottom: 10px;
+            border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+        }
+
+        &__title {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            color: #2199f8;
+            font-size: 16px;
+            font-weight: 500;
+        }
+
+        &__icon {
+            width: 18px;
+            height: 18px;
+        }
+
+        &__upload {
+            flex: none;
+            padding: 4px 12px;
+            border-radius: 4px;
+            background: #2199f8;
+            color: #fff;
+            font-size: 14px;
+            line-height: 20px;
+            cursor: pointer;
+        }
+
+        &__assess {
+            margin-top: 10px;
+            padding: 10px;
+            border-radius: 8px;
+            background: #f9f9f9;
+        }
+
+        &__assess-title {
+            color: #4f4f4f;
+            font-size: 16px;
+            margin-top: -3px;
+            font-family: "PangMenZhengDao";
+        }
+
+        &__assess-desc {
+            margin-top: 4px;
+            color: #9c9c9c;
+        }
+
+        &__guide {
+            margin-top: 10px;
+            padding: 10px;
+            border: 1px solid rgba(33, 153, 248, 0.2);
+            border-radius: 8px;
+            background: linear-gradient(360deg, #ffffff 90.33%, #abd8fc 116.61%, #2199f8 148.55%);
+        }
+
+        &__guide-box {
+            margin-top: 12px;
+
+            img {
+                display: block;
+                width: 100%;
+            }
+        }
+    }
+
+    .interact-qa {
+        margin-top: 10px;
+        padding: 10px;
+        border-radius: 8px;
+        background: rgba(33, 153, 248, 0.1);
+
+        &__head {
+            display: flex;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 8px;
+        }
+
+        &__tag {
+            flex: none;
+            padding: 2px 6px;
+            border-radius: 2px;
+            background: #2199f8;
+            color: #fff;
+            font-size: 12px;
+            line-height: 18px;
+        }
+
+        &__question {
+            flex: 1;
+            min-width: 0;
+            color: #333;
+            font-size: 14px;
+            font-weight: 500;
+        }
+
+        &__actions {
+            display: flex;
+            gap: 8px;
+            margin-top: 10px;
+        }
+
+        &__option {
+            position: relative;
+            flex: 1;
+            min-width: 0;
+            height: 36px;
+            line-height: 36px;
+            border-radius: 6px;
+            background: #fff;
+            border: 1px solid transparent;
+            color: #666;
+            font-size: 14px;
+            text-align: center;
+            box-sizing: border-box;
+            cursor: pointer;
+
+            &.selected {
+                border-color: #2199f8;
+                color: #2199f8;
+
+                &::after {
+                    content: "";
+                    position: absolute;
+                    top: -1px;
+                    right: -1px;
+                    width: 18px;
+                    height: 14px;
+                    background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
+                }
+            }
+        }
+
+        &__submit {
+            flex: 1;
+            min-width: 0;
+            height: 36px;
+            line-height: 36px;
+            border-radius: 6px;
+            background: #2199f8;
+            color: #fff;
+            font-size: 14px;
+            text-align: center;
+            cursor: pointer;
+
+            &.disabled {
+                opacity: 0.2;
+                cursor: not-allowed;
+            }
+        }
+    }
+}
+
+.bottom-btn-container {
+    display: flex;
+    justify-content: space-between;
+    gap: 12px;
+    width: 100%;
+
+    .bottom-btn {
+        padding: 0 25px;
+        border-radius: 4px;
+        font-size: 16px;
+        height: 40px;
+        line-height: 40px;
+        box-sizing: border-box;
+        font-weight: 500;
+        text-align: center;
+
+        &.primary-btn {
+            flex: 1;
+            background: #2199f8;
+            color: #fff;
+        }
+    }
+}
+</style>

+ 34 - 17
src/views/old_mini/agri_file/pages/addZone.vue

@@ -358,8 +358,6 @@ const handleFarmNameConfirm = async (regionName) => {
         polygon,
     };
     sessionStorage.setItem("FARM_RANGE_GEOMETRY", JSON.stringify(geometryData));
-// console.log("isSuccessPopupParam", isSuccessPopupParam.value);
-console.log("isSuccessPopupParam", isSuccessPopupParam.value, route.query);
     if (isSuccessPopupParam.value) {
         showDrawSuccessPopup.value = true;
         return;
@@ -369,8 +367,7 @@ console.log("isSuccessPopupParam", isSuccessPopupParam.value, route.query);
         setTimeout(() => {
             const query = { showUpload: "1" };
             if (route.query.trackId) query.id = route.query.trackId;
-            if (route.query.trackType) query.type = route.query.trackType;
-            router.replace({ path: "/growth_track", query });
+            router.replace({ path: targetUrl.value || "/growth_track", query });
         }, 800);
         return;
     }
@@ -378,6 +375,20 @@ console.log("isSuccessPopupParam", isSuccessPopupParam.value, route.query);
 };
 
 const showExecuteSuccessPopup = ref(false);
+
+/** 分区创建成功后的离开:继续上传或返回上一页 */
+function leaveAfterZoneCreated() {
+    if (isRegionDrawMode.value && route.query.continueUpload === "1") {
+        sessionStorage.setItem(
+            "AGRI_FILE_CONTINUE_UPLOAD",
+            JSON.stringify({ zoneId: route.query.zoneId || "" })
+        );
+        router.back();
+        return;
+    }
+    leaveAfterConfirm();
+}
+
 const handleZoneInfoConfirm = (formData) => {
     const payload = mapManage.getAreaGeometry();
     const geometryData = {
@@ -386,35 +397,41 @@ const handleZoneInfoConfirm = (formData) => {
         mianji: payload.mianji,
     };
     sessionStorage.setItem("ADD_ZONE_GEOMETRY", JSON.stringify(geometryData));
-    showExecuteSuccessPopup.value = true;
+    // isSuccessPopup=1 才出 tip;=0 时子组件已 ElMessage,直接离开
+    if (isSuccessPopupParam.value) {
+        showExecuteSuccessPopup.value = true;
+        return;
+    }
+    leaveAfterZoneCreated();
 };
 
 const handleDrawSuccessClosed = () => {
     showDrawSuccessPopup.value = false;
     leaveAfterConfirm();
-}
+};
 
 const handleComplete = () => {
     showExecuteSuccessPopup.value = false;
+    leaveAfterZoneCreated();
+};
 
-    // 农事档案上传入口 needDraw 勾画:返回并继续打开上传弹窗
-    if (isRegionDrawMode.value && route.query.continueUpload === "1") {
-        sessionStorage.setItem(
-            "AGRI_FILE_CONTINUE_UPLOAD",
-            JSON.stringify({ zoneId: route.query.zoneId || "" })
-        );
-        router.back();
-        return;
-    }
-
-    leaveAfterConfirm();
+/** keepAlive 下重置弹窗/勾画态,避免上次入口残留 */
+function resetPageTransientState() {
+    showDrawSuccessPopup.value = false;
+    showExecuteSuccessPopup.value = false;
+    showRegionNamePopup.value = false;
+    showZoneInfoPopup.value = false;
+    drawnPolygon.value = "";
+    submittingZoneRange.value = false;
 }
 
 onActivated(() => {
+    resetPageTransientState();
     nextTick(() => initMapView());
 });
 
 onDeactivated(() => {
+    resetPageTransientState();
     mapManage.destroyMap();
 });
 </script>

+ 116 - 275
src/views/old_mini/agri_file/pages/growthTrack.vue

@@ -22,71 +22,48 @@
                             <span class="growth-hero__tag growth-hero__tag--zone">{{ zoneName }}</span>
                         </div>
                         <div class="growth-hero__reason">{{ reason }}</div>
-                    </div> 
+                    </div>
                     <div class="growth-hero__share" @click="handleShare">
                         <Icon name="share" />
                         <span>{{ t("agriFile.forward") }}</span>
                     </div>
                 </div>
             </div>
-            <template v-if="!isAbnormal">
-                <div class="growth-content">
-                    <div class="growth-content__title">{{ t("agriFile.agriAssessment") }}</div>
-                    <div class="growth-content__desc">{{ assessmentDesc }}</div>
-                </div>
-                <div class="interact-card">
-                    <div class="interact-card__head">
-                        <div class="interact-card__tab">
-                            <img class="interact-card__star" src="@/assets/img/agricultural/start.png" alt="" />
-                            <span>{{ t("agriFile.interactQuestion") }}</span>
-                        </div>
-                    </div>
-                    <div class="interact-card__qa">
-                        <div class="interact-card__question">{{ interactProblem }}</div>
-                        <div class="interact-card__options">
-                            <div v-for="item in interactOptions" :key="item.id" class="interact-option"
-                                :class="{ selected: selectedOptionId === item.id }" @click="selectedOptionId = item.id">
-                                <span class="interact-option__text">{{ item.name }}</span>
-                            </div>
-                        </div>
-                        <div class="interact-card__submit" @click="handleSubmit">{{ t("agriFile.submit") }}</div>
+            <div class="growth-content">
+                <div class="growth-content__title">{{ t("agriFile.agriAssessment") }}</div>
+                <div class="growth-content__desc">{{ assessmentDesc }}</div>
+            </div>
+            <div class="interact-card">
+                <div class="interact-card__head">
+                    <div class="interact-card__tab">
+                        <img class="interact-card__star" src="@/assets/img/agricultural/start.png" alt="" />
+                        <span>{{ t("agriFile.interactQuestion") }}</span>
                     </div>
-                    <!-- <div class="interact-card__ref">
-                        <span class="interact-card__ref-label">{{ t("agriFile.referenceImage") }}</span>
-                        <photo-provider :photo-closable="true">
-                            <photo-consumer :src="guideImage">
-                                <img class="interact-card__ref-img" :src="guideImage" alt="" />
-                            </photo-consumer>
-                        </photo-provider>
-                    </div> -->
-                </div>
-                <div class="guide-card">
-                    <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
-                    <photo-provider :photo-closable="true">
-                        <photo-consumer :src="guideImage">
-                            <img class="guide-card__img" :src="guideImage" alt="" />
-                        </photo-consumer>
-                    </photo-provider>
                 </div>
-            </template>
-            <div v-else>
-                <div class="situation-card__head" v-for="item in situationList" :key="item.nameKey">
-                    <div class="situation-card__name">
-                        <img class="situation-card__icon" src="@/assets/img/agricultural/gaojin.png" alt="" />
-                        <span>{{ t(item.nameKey) }}</span>
-                    </div>
-                    <div class="situation-card__assess">
-                        <div class="situation-card__assess-title">{{ t("agriFile.agriAssessment") }}</div>
-                        <div class="situation-card__assess-desc">{{ t("agriFile.agriAssessmentDesc") }}</div>
-                    </div>
-                    <div class="situation-card__guide">
-                        <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
-                        <div class="situation-card__guide-box">
-                            <img :src="guideImage" alt="" />
+                <div class="interact-card__qa">
+                    <div class="interact-card__question">{{ interactProblem }}</div>
+                    <div class="interact-card__options">
+                        <div
+                            v-for="item in interactOptions"
+                            :key="item.id"
+                            class="interact-option"
+                            :class="{ selected: selectedOptionId === item.id }"
+                            @click="selectedOptionId = item.id"
+                        >
+                            <span class="interact-option__text">{{ item.name }}</span>
                         </div>
                     </div>
+                    <div class="interact-card__submit" @click="handleSubmit">{{ t("agriFile.submit") }}</div>
                 </div>
             </div>
+            <div class="guide-card">
+                <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
+                <photo-provider :photo-closable="true">
+                    <photo-consumer :src="guideImage">
+                        <img class="guide-card__img" :src="guideImage" alt="" />
+                    </photo-consumer>
+                </photo-provider>
+            </div>
         </div>
         <div class="guide-card__upload" @click="handleUploadClick">
             <el-icon>
@@ -95,7 +72,13 @@
             <span>{{ t("agriFile.uploadPhoto") }}</span>
         </div>
         <album-upload-popup v-model:show="showUploadPopup" @confirm="handleUploadConfirm" />
-        <tip-popup v-model:show="showSuccessPopup" type="executeSuccess" text="您已上传成功" text2="请等待诊断报告生成" hideBtn>
+        <tip-popup
+            v-model:show="showSuccessPopup"
+            type="executeSuccess"
+            text="您已上传成功"
+            text2="请等待诊断报告生成"
+            hideBtn
+        >
             <template #footer>
                 <div class="bottom-btn-container">
                     <div class="bottom-btn primary-btn" @click="handleComplete">完成</div>
@@ -107,9 +90,9 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onActivated, onMounted, ref } from "vue";
+import { computed, nextTick, onActivated, ref } from "vue";
 import { useRoute, useRouter } from "vue-router";
-import { Icon } from 'vant';
+import { Icon } from "vant";
 import { ElMessage } from "element-plus";
 import customHeader from "@/components/customHeader.vue";
 import albumUploadPopup from "../components/albumUploadPopup.vue";
@@ -123,24 +106,21 @@ const { t } = useI18n();
 const route = useRoute();
 const router = useRouter();
 
-const isAbnormal = computed(() => route.query.type === "abnormal");
-const pageTitle = computed(() => route.query.pageName || "巡园要点");
-
-const id = computed(() => route.query.id);
+// ---------- 常量 ----------
+const DEFAULT_GUIDE_IMAGE = require("@/assets/img/agricultural/patrol-guide.png");
 
-function isTruthyFlag(value) {
-    return value === 1 || value === "1" || value === true || value === "true";
-}
+// ---------- 页面状态 ----------
+const interactTasks = ref([]);
+const currentTaskIndex = ref(0);
+const selectedOptionId = ref(null);
+const showUploadPopup = ref(false);
+const showSuccessPopup = ref(false);
+const showLeavePopup = ref(false);
+const needDraw = ref(true); // status≠1 时需先勾画种植范围
 
-function parseMaybeJson(value) {
-    if (value == null || value === "") return null;
-    if (typeof value === "object") return value;
-    try {
-        return JSON.parse(value);
-    } catch {
-        return null;
-    }
-}
+// ---------- 计算属性 ----------
+const pageTitle = computed(() => route.query.pageName || t("agriFile.patrolGrowthTitle"));
+const id = computed(() => route.query.id);
 
 /** 通过分享进入:无返回箭头 */
 const isFromShare = computed(() => {
@@ -153,20 +133,6 @@ const isFromShare = computed(() => {
     return false;
 });
 
-const DEFAULT_GUIDE_IMAGE = require("@/assets/img/agricultural/patrol-guide.png");
-
-function resolveImageUrl(path) {
-    if (!path) return "";
-    const text = String(path).trim();
-    if (!text) return "";
-    if (/^https?:\/\//i.test(text)) return text;
-    return base_img_url2 + text.replace(/^\//, "");
-}
-
-const interactTasks = ref([]);
-const currentTaskIndex = ref(0);
-const selectedOptionId = ref(null);
-
 const currentTask = computed(() => interactTasks.value[currentTaskIndex.value] || null);
 const currentContent = computed(() => currentTask.value?.interact_content || {});
 
@@ -176,10 +142,7 @@ const theme = computed(
 const reason = computed(
     () => currentContent.value.interact_theme || t("agriFile.interactReason")
 );
-const level = computed(
-    // () => currentTask.value?.weather_risk || t("agriFile.riskLevel2")
-    () => t("agriFile.riskLevel2")
-);
+const level = computed(() => t("agriFile.riskLevel2"));
 const zoneName = computed(() => t("agriFile.zoneOne"));
 const assessmentDesc = computed(
     () => currentContent.value.interact_explain || t("agriFile.agriAssessmentDesc")
@@ -189,9 +152,7 @@ const interactProblem = computed(
 );
 const guideImage = computed(
     () => resolveImageUrl(currentContent.value.guide_url) || DEFAULT_GUIDE_IMAGE
-    // () => resolveImageUrl('感知_玉米_4_32.png') || DEFAULT_GUIDE_IMAGE
 );
-
 const interactOptions = computed(() => {
     const options = currentContent.value.interact_options;
     if (!Array.isArray(options)) return [];
@@ -201,6 +162,40 @@ const interactOptions = computed(() => {
     }));
 });
 
+// ---------- 工具函数 ----------
+function isTruthyFlag(value) {
+    return value === 1 || value === "1" || value === true || value === "true";
+}
+
+function parseMaybeJson(value) {
+    if (value == null || value === "") return null;
+    if (typeof value === "object") return value;
+    try {
+        return JSON.parse(value);
+    } catch {
+        return null;
+    }
+}
+
+function resolveImageUrl(path) {
+    if (!path) return "";
+    const text = String(path).trim();
+    if (!text) return "";
+    if (/^https?:\/\//i.test(text)) return text;
+    return base_img_url2 + text.replace(/^\//, "");
+}
+
+function getSelectedFarm() {
+    try {
+        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+    } catch {
+        return {};
+    }
+}
+
+const hasAnswered = () => selectedOptionId.value != null;
+
+// ---------- 业务逻辑 ----------
 const applyTaskIndex = (index) => {
     currentTaskIndex.value = index;
     selectedOptionId.value = null;
@@ -218,14 +213,28 @@ const fetchInternalInteractTasks = async () => {
     }
 };
 
-const showUploadPopup = ref(false);
-const showSuccessPopup = ref(false);
-const showLeavePopup = ref(false);
-const situationList = [
-    { nameKey: "agriFile.growthAbnormal" },
-    // { nameKey: "agriFile.rootAbnormal" },
-];
+/** status=1 有范围无需勾画;status=0 无范围需要勾画 */
+const fetchZoneRangeStatus = async () => {
+    const farm = getSelectedFarm();
+    const farmId = farm.farm_id ?? farm.id;
+    const zoneId = id.value || farm.zone_id || farm.zoneId;
+    if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
+        needDraw.value = true;
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.checkZoneRange({
+            farm_id: farmId,
+            zone_id: zoneId,
+        });
+        needDraw.value = Number(res?.data?.status) !== 1;
+    } catch (e) {
+        console.warn("[growthTrack] checkZoneRange failed", e);
+        needDraw.value = true;
+    }
+};
 
+// ---------- 事件处理 / 生命周期 ----------
 const handleShare = () => {
     const query = {
         askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
@@ -233,8 +242,8 @@ const handleShare = () => {
         targetUrl: "growth_track",
         paramsPage: JSON.stringify({
             id: id.value,
-            type: route.query.type,
             fromShare: 1,
+            ...(route.query.pageName ? { pageName: route.query.pageName } : {}),
         }),
         imageUrl: "https://birdseye-img.sysuimars.com/temp/field.png",
     };
@@ -257,71 +266,20 @@ const handleSubmit = () => {
     ElMessage.success(t("agriFile.submitSuccess"));
 };
 
-const needDraw = ref(true);
-
-const getSelectedFarm = () => {
-    try {
-        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
-    } catch {
-        return {};
-    }
-};
-
-/** status=1 有范围无需勾画;status=0 无范围需要勾画 */
-const fetchZoneRangeStatus = async () => {
-    const farm = getSelectedFarm();
-    const farmId = farm.farm_id ?? farm.id;
-    const zoneId = id.value || farm.zone_id || farm.zoneId;
-    if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
-        needDraw.value = true;
-        return;
-    }
-    try {
-        const res = await VE_API.questionnaire.checkZoneRange({
-            farm_id: farmId,
-            zone_id: zoneId,
-        });
-        const status = Number(res?.data?.status);
-        needDraw.value = status !== 1;
-    } catch (e) {
-        console.warn("[growthTrack] checkZoneRange failed", e);
-        needDraw.value = true;
-    }
-};
-
-/** 先跳小程序校验定位;通过后由小程序直接打开勾画/病虫害/上传目标页 */
+/** 需勾画则先画范围;否则直接上传 */
 const handleUploadClick = () => {
     if (needDraw.value) {
         router.push({
             path: "/add_zone",
             query: {
                 type: "farm",
-                targetUrl: isAbnormal.value ? "/pest_interact" : "/growth_track",
-                ...(isAbnormal.value ? { isAbnormal: "true" } : {}),
+                targetUrl: "/growth_track",
                 ...(route.query.id ? { trackId: route.query.id } : {}),
-                ...(route.query.type ? { trackType: route.query.type } : {}),
             },
         });
         return;
     }
-    if (isAbnormal.value) {
-        router.push({
-            path: "/pest_interact",
-        });
-        return;
-    }
     showUploadPopup.value = true;
-    // const query = [
-    //     `needDraw=${needDraw.value ? 1 : 0}`,
-    //     `isAbnormal=${isAbnormal.value ? 1 : 0}`,
-    //     route.query.id ? `id=${encodeURIComponent(String(route.query.id))}` : "",
-    //     route.query.type ? `type=${encodeURIComponent(String(route.query.type))}` : "",
-    // ]
-    //     .filter(Boolean)
-    //     .join("&");
-    // wx.miniProgram.navigateTo({
-    //     url: `/pages/subPages/farm_location_check/index?${query}`,
-    // });
 };
 
 const handleUploadConfirm = async ({ images, date } = {}) => {
@@ -355,14 +313,8 @@ const handleComplete = () => {
     router.replace("/agri_file");
 };
 
-const handleContinueUpload = () => {
-    showSuccessPopup.value = false;
-};
-
-const hasAnswered = () => selectedOptionId.value != null;
-
 const handleBack = () => {
-    if (!isAbnormal.value && !hasAnswered()) {
+    if (!hasAnswered()) {
         showLeavePopup.value = true;
         return;
     }
@@ -373,17 +325,9 @@ const confirmLeave = () => {
     router.back();
 };
 
-// onMounted(() => {
-//     if (!isAbnormal.value) {
-//         fetchInternalInteractTasks();
-//     }
-// });
-
 onActivated(() => {
     fetchZoneRangeStatus();
-    if (!isAbnormal.value) {
-        fetchInternalInteractTasks();
-    }
+    fetchInternalInteractTasks();
     if (route.query.showUpload !== "1") return;
     nextTick(() => {
         showUploadPopup.value = true;
@@ -448,21 +392,13 @@ onActivated(() => {
             }
 
             &__tag--level {
-                color: #FF6A6A;
+                color: #ff6a6a;
             }
 
             &__tag--zone {
                 color: #2199f8;
             }
 
-
-            &__tags {
-                margin-top: 10px;
-                display: flex;
-                align-items: center;
-                gap: 8px;
-            }
-
             &__share {
                 flex: none;
                 display: flex;
@@ -485,14 +421,14 @@ onActivated(() => {
         background: #fff;
 
         &__title {
-            color: #4F4F4F;
+            color: #4f4f4f;
             font-size: 16px;
             font-family: "PangMenZhengDao";
         }
 
         &__desc {
             margin-top: 4px;
-            color: #9C9C9C;
+            color: #9c9c9c;
             font-size: 14px;
         }
     }
@@ -535,7 +471,6 @@ onActivated(() => {
             padding: 10px;
             border-radius: 8px;
             background: rgba(33, 153, 248, 0.1);
-
         }
 
         &__question {
@@ -601,28 +536,6 @@ onActivated(() => {
                 }
             }
         }
-
-        &__ref {
-            position: relative;
-            margin: 10px;
-            border-radius: 8px;
-        }
-
-        &__ref-label {
-            position: absolute;
-            z-index: 1;
-            padding: 3px 11px;
-            border-radius: 8px 0 8px 0;
-            background: rgba(0, 0, 0, 0.43);
-            backdrop-filter: blur(5px);
-            color: #fff;
-            font-size: 12px;
-        }
-
-        &__ref-img {
-            display: block;
-            width: 100%;
-        }
     }
 
     .guide-card__upload {
@@ -682,73 +595,6 @@ onActivated(() => {
             margin-top: 8px;
         }
     }
-
-    .situation-card {
-        &__head {
-            position: relative;
-            z-index: 1;
-            margin: 0 10px 12px;
-            padding: 12px;
-            border-radius: 10px;
-            background: #fff;
-
-            &:first-child {
-                margin-top: -24px;
-            }
-        }
-
-        &__name {
-            display: flex;
-            align-items: stretch;
-            gap: 10px;
-            padding-bottom: 10px;
-            border-bottom: 1px solid rgba(0, 0, 0, 0.08);
-            color: #2199f8;
-            font-size: 16px;
-            font-weight: 500;
-        }
-
-        &__icon {
-            width: 18px;
-            height: 18px;
-        }
-
-        &__assess {
-            margin-top: 10px;
-            padding: 10px;
-            border-radius: 8px;
-            background: #F9F9F9;
-        }
-
-        &__assess-title {
-            color: #4F4F4F;
-            font-size: 16px;
-            margin-top: -3px;
-            font-family: "PangMenZhengDao";
-        }
-
-        &__assess-desc {
-            margin-top: 4px;
-            color: #9C9C9C;
-        }
-
-        &__guide {
-            margin-top: 10px;
-            padding: 10px;
-            border: 1px solid rgba(33, 153, 248, 0.2);
-            border-radius: 8px;
-            background: linear-gradient(360deg, #FFFFFF 90.33%, #ABD8FC 116.61%, #2199F8 148.55%);
-        }
-
-        &__guide-box {
-            margin-top: 12px;
-
-            img {
-                display: block;
-                width: 100%;
-            }
-        }
-    }
 }
 
 .bottom-btn-container {
@@ -767,11 +613,6 @@ onActivated(() => {
         font-weight: 500;
         text-align: center;
 
-        &.secondary-btn {
-            color: #b1b1b1;
-            border: 1px solid #eaeaea;
-        }
-
         &.primary-btn {
             flex: 1;
             background: #2199f8;

+ 5 - 3
src/views/old_mini/growth_report/components/CropAlertPanel.vue

@@ -137,7 +137,7 @@ const cropOptions = ref([]);
 const selectedZoneId = ref(null);
 const activeIndex = ref(0);
 
-const emit = defineEmits(["viewDetail", "heightChange"]);
+const emit = defineEmits(["viewDetail", "heightChange", "zoneChange"]);
 
 const weatherIcon = require("@/assets/img/report/weather.png");
 
@@ -219,11 +219,13 @@ const applySelectedZone = (index) => {
         activeIndex.value = 0;
         selectedZoneId.value = null;
         syncCropNameDisplay();
+        emit("zoneChange", null);
         return;
     }
     activeIndex.value = index;
     selectedZoneId.value = zone.zone_id ?? null;
     syncCropNameDisplay();
+    emit("zoneChange", selectedZoneId.value);
 };
 
 const handleSelectCrop = (index) => {
@@ -455,11 +457,11 @@ const handleViewDetail = (item) => {
 };
 
 const handlePatrolTip = (item) => {
+    const isAbnormal = item?.type === "abnormal";
     router.push({
-        path: "/growth_track",
+        path: isAbnormal ? "/abnormal_track" : "/growth_track",
         query: {
             id: item.id,
-            type: item.type || "growth",
         },
     });
 };

+ 26 - 0
src/views/old_mini/growth_report/growthReportMap.js

@@ -1,6 +1,7 @@
 import * as KMap from "@/utils/ol-map/KMap";
 import * as util from "@/common/ol_common.js";
 import config from "@/api/config.js";
+import StaticImgLayer from "@/utils/ol-map/StaticImgLayer";
 import Style from "ol/style/Style";
 import Icon from "ol/style/Icon";
 import Fill from "ol/style/Fill";
@@ -27,6 +28,7 @@ class IndexMap {
     let vectorStyle = new KMap.VectorStyle();
     this.vectorStyle = vectorStyle;
     this.noRangeTipOverlay = null;
+    this.remoteImageLayer = null;
 
     // 位置图标
     this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
@@ -54,6 +56,30 @@ class IndexMap {
     });
   }
 
+  /** 叠加静态风险底图(与 demo growthReportMap.showRemoteImage 一致) */
+  showRemoteImage(url, extent, options = {}) {
+    this.hideRemoteImage();
+    if (!url || !extent?.length || !this.kmap) return;
+    this.remoteImageLayer = new StaticImgLayer(
+      url,
+      {
+        extent,
+        minZoom: 6,
+        maxZoom: 22,
+        opacity: options.opacity ?? 0.5,
+      },
+      50,
+      this.kmap,
+    );
+  }
+
+  hideRemoteImage() {
+    if (this.remoteImageLayer?.layer && this.kmap?.map) {
+      this.kmap.map.removeLayer(this.remoteImageLayer.layer);
+    }
+    this.remoteImageLayer = null;
+  }
+
   initMap(location, target) {
     let level = 16;
     let coordinate = util.wktCastGeom(location).getFirstCoordinate();

+ 148 - 61
src/views/old_mini/growth_report/index.vue

@@ -51,8 +51,8 @@
             <span class="location-bar__action" @click="handleSwitchFarm">切换农场</span>
         </div>
 
-        <!-- 左侧:胁迫图层菜单 -->
-        <div class="side-menu side-menu--left">
+        <!-- 左侧:气象底图图层菜单(getHomeMapCalc) -->
+        <div v-if="stressMenuItems.length" class="side-menu side-menu--left">
             <div
                 v-for="item in stressMenuItems"
                 :key="item.key"
@@ -68,32 +68,12 @@
             </div>
         </div>
 
-        <!-- 右侧:图层菜单 -->
-        <div class="side-menu side-menu--right">
-            <div
-                v-for="item in layerMenuItems"
-                :key="item.key"
-                class="side-menu__item"
-                :class="{ active: activeMenuKey === item.key }"
-                @click="handleSelectMenu(item.key)"
-            >
-                <img class="side-menu__icon" :src="item.icon" alt="" />
-                <div class="side-menu__text">
-                    <span>{{ item.line1 }}</span>
-                    <span>{{ item.line2 }}</span>
-                </div>
-            </div>
-        </div>
-
-        <div class="map-legend" :style="{ bottom: `${inviteBottom}px` }">
-            <div
-                v-for="item in mapLegendItems"
-                :key="item.key"
-                class="map-legend__item"
-            >
-                <span class="map-legend__pill" :class="item.pillClass"></span>
-                <span class="map-legend__text">{{ item.label }}</span>
-            </div>
+        <div
+            v-if="activeLegendUrl"
+            class="map-legend map-legend--image"
+            :style="{ bottom: `${inviteBottom}px` }"
+        >
+            <img class="map-legend__img" :src="activeLegendUrl" alt="" />
         </div>
 
         <div class="invite-group" :style="{ bottom: `${inviteBottom}px` }">
@@ -108,6 +88,7 @@
         <crop-alert-panel
             :crop-name="currentCropName"
             :farm-id="selectedFarmId"
+            @zone-change="handleZoneChange"
             @height-change="handlePanelHeightChange"
         />
     </div>
@@ -138,12 +119,6 @@ const MAP_POINT_STORAGE_KEY = "GROWTH_REPORT_MAP_POINT";
 
 const menuIcon = require("@/assets/img/common/farm-active.png");
 
-const mapLegendItems = computed(() => [
-    { key: "zone", label: "胁迫", pillClass: "map-legend__pill--zone" },
-    { key: "growth", label: "长势", pillClass: "map-legend__pill--growth" },
-    { key: "pest", label: "病虫害", pillClass: "map-legend__pill--pest" },
-]);
-
 const store = useStore();
 const tabBarHeight = computed(() => store.state.home.tabBarHeight);
 
@@ -154,6 +129,8 @@ const locationName = ref("");
 /** status≠1 时展示中心勾画引导 */
 const noFarmRange = ref(false);
 const selectedFarmId = ref(localStorage.getItem("selectedFarmId") || "");
+/** CropAlertPanel 当前选中分区 */
+const panelZoneId = ref(null);
 const currentCropName = computed(() => {
     // 依赖 selectedFarmId,切换农场后重新读缓存
     void selectedFarmId.value;
@@ -165,10 +142,16 @@ const currentCropName = computed(() => {
         ""
     );
 });
-const activeMenuKey = ref("hot-drought-2");
+/** 侧边气象图层菜单(来自 getHomeMapCalc) */
+const stressMenuItems = ref([]);
+const activeMenuKey = ref("");
+/** 当前选中图层图例图 */
+const activeLegendUrl = ref("");
 const panelHeight = ref(280);
 const inviteBottom = computed(() => panelHeight.value);
 const invitePopupRef = ref(null);
+/** 防止切换农场/分区时旧请求回写 */
+let mapCalcToken = 0;
 
 const getSelectedFarm = () => {
     try {
@@ -206,7 +189,7 @@ const syncSelectedFarmId = () => {
 async function fetchFarmRangeStatus() {
     const farm = getSelectedFarm();
     const farmId = selectedFarmId.value || farm.farm_id || farm.id;
-    const zoneId = getSelectedZoneId();
+    const zoneId = panelZoneId.value ?? getSelectedZoneId();
     if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
         noFarmRange.value = true;
         await syncNoRangeGuide();
@@ -278,7 +261,7 @@ function buildInteractSubmitPayload(answerText) {
     const task = currentInteractTask.value || {};
     const content = task.interact_content || {};
     return {
-        zone_id: Number(task.zone_id ?? getSelectedZoneId()),
+        zone_id: Number(task.zone_id ?? panelZoneId.value ?? getSelectedZoneId()),
         date: formatToday(),
         interact_code: task.interact_code || content.interact_code || "",
         interact_answer: answerText,
@@ -328,7 +311,7 @@ async function handleInteractOptionClick(item) {
 }
 
 async function fetchInteractTask() {
-    const zoneId = getSelectedZoneId();
+    const zoneId = panelZoneId.value ?? getSelectedZoneId();
     if (zoneId === "" || zoneId == null) return;
     try {
         const res = await VE_API.questionnaire.getExternalInteractTasks({
@@ -342,17 +325,112 @@ async function fetchInteractTask() {
     }
 }
 
-const stressMenuItems = [
-    { key: "stress", line1: "胁迫", line2: "胁迫", icon: menuIcon },
-    { key: "hot-drought-1", line1: "高温", line2: "干旱", icon: menuIcon },
-    { key: "hot-drought-2", line1: "高温", line2: "干旱", icon: menuIcon },
-    { key: "hot-drought-3", line1: "高温", line2: "干旱", icon: menuIcon },
-];
+/** 农场品类编码:接口 crop_code 对应缓存 category_code */
+function resolveCropCode(farm = getSelectedFarm()) {
+    return farm.category_code || farm.categorycode || farm.crop_code || "";
+}
+
+/** 本地 dem 路径转 mapimg 可访问地址:取文件名挂到 /mapimg/ */
+function resolveCalcAssetUrl(path) {
+    if (!path) return "";
+    const raw = String(path).trim();
+    if (/^https?:\/\//i.test(raw)) return raw;
+    const fileName = raw.replace(/\\/g, "/").split("/").filter(Boolean).pop();
+    if (!fileName) return "";
+    // /home/dem_ditu/输出/从化区_xxx.png → https://feiniao-farm-h5-v3.feiniaotech.com/mapimg/从化区_xxx.png
+    return `https://feiniao-farm-h5-v3.feiniaotech.com/mapimg/${encodeURIComponent(fileName)}`;
+}
+
+/** risk_name 拆成两行展示,如「高温高湿」→ 高温 / 高湿 */
+function splitRiskName(name) {
+    const text = String(name || "").trim();
+    if (!text) return { line1: "-", line2: "" };
+    if (text.length <= 2) return { line1: text, line2: "" };
+    const mid = Math.ceil(text.length / 2);
+    return { line1: text.slice(0, mid), line2: text.slice(mid) };
+}
+
+function mapCalcRecordToMenuItem(record) {
+    const { line1, line2 } = splitRiskName(record?.risk_name);
+    const bounds = Array.isArray(record?.bounds) ? record.bounds.map(Number) : [];
+    const extent =
+        bounds.length === 4 && bounds.every((n) => Number.isFinite(n)) ? bounds : null;
+    return {
+        key: String(record?.id ?? record?.risk_name ?? ""),
+        line1,
+        line2,
+        icon: menuIcon,
+        mapUrl: resolveCalcAssetUrl(record?.png_path),
+        extent,
+        legendUrl: resolveCalcAssetUrl(record?.legend_path),
+    };
+}
+
+function clearMapCalcState() {
+    stressMenuItems.value = [];
+    activeMenuKey.value = "";
+    activeLegendUrl.value = "";
+    growthReportMap.hideRemoteImage();
+}
+
+/** 按侧边菜单叠加对应气象底图 + 图例 */
+function applyMenuOverlay(key) {
+    const item = stressMenuItems.value.find((m) => m.key === key);
+    if (!item?.mapUrl || !item.extent || !growthReportMap.kmap) {
+        growthReportMap.hideRemoteImage();
+        activeLegendUrl.value = "";
+        return;
+    }
+    growthReportMap.showRemoteImage(item.mapUrl, item.extent);
+    activeLegendUrl.value = item.legendUrl || "";
+}
+
+async function fetchHomeMapCalc() {
+    const token = ++mapCalcToken;
+    const farm = getSelectedFarm();
+    const farmId = selectedFarmId.value || farm.farm_id || farm.id;
+    const zoneId = panelZoneId.value;
+    const cropCode = resolveCropCode(farm);
+    if (
+        farmId == null ||
+        farmId === "" ||
+        zoneId == null ||
+        zoneId === "" ||
+        !cropCode
+    ) {
+        clearMapCalcState();
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.getHomeMapCalc({
+            farm_id: farmId,
+            zone_id: zoneId,
+            calc_date: formatToday(),
+            crop_code: cropCode,
+        });
+        if (token !== mapCalcToken) return;
+        const records = Array.isArray(res?.data?.records) ? res.data.records.slice(0,3) : [];
+        stressMenuItems.value = records.map(mapCalcRecordToMenuItem).filter((item) => item.key);
+        const first = stressMenuItems.value[0];
+        if (first) {
+            activeMenuKey.value = first.key;
+            applyMenuOverlay(first.key);
+        } else {
+            clearMapCalcState();
+        }
+    } catch (e) {
+        if (token !== mapCalcToken) return;
+        console.warn("[growth_report] getHomeMapCalc failed", e);
+        clearMapCalcState();
+    }
+}
 
-const layerMenuItems = [
-    { key: "growth", line1: "作物", line2: "长势", icon: menuIcon },
-    { key: "phenology", line1: "物候", line2: "进程", icon: menuIcon },
-];
+const handleZoneChange = (zoneId) => {
+    panelZoneId.value = zoneId != null && zoneId !== "" ? zoneId : null;
+    fetchHomeMapCalc();
+    fetchFarmRangeStatus();
+    fetchInteractTask();
+};
 
 function getLocationName(point = currentMapPoint.value) {
     const locationPoint = point || localStorage.getItem("selectedFarmPoint") || store.state.home.miniUserLocationPoint;
@@ -386,12 +464,14 @@ const initMap = async () => {
             growthReportMap.setMapPosition(coordinate);
         }
         await syncNoRangeGuide();
+        applyMenuOverlay(activeMenuKey.value);
         return;
     }
     const location = currentMapPoint.value || store.state.home.miniUserLocationPoint || DEFAULT_MAP_POINT;
     currentMapPoint.value = location;
     growthReportMap.initMap(location, mapContainer.value);
     await syncNoRangeGuide();
+    applyMenuOverlay(activeMenuKey.value);
 };
 
 function applySelectedLocation() {
@@ -453,7 +533,15 @@ const handleSwitchLocation = () => {
 
 const handleSelectMenu = (key) => {
     if (!key) return;
+    // 再次点击同一项则关闭叠加
+    if (activeMenuKey.value === key) {
+        activeMenuKey.value = "";
+        activeLegendUrl.value = "";
+        growthReportMap.hideRemoteImage();
+        return;
+    }
     activeMenuKey.value = key;
+    applyMenuOverlay(key);
 };
 
 const handleInvite = (type) => {
@@ -768,10 +856,6 @@ onActivated(() => {
             left: 10px;
         }
 
-        &--right {
-            right: 10px;
-        }
-
         &__item {
             display: flex;
             flex-direction: column;
@@ -846,15 +930,18 @@ onActivated(() => {
         left: 10px;
         z-index: 20;
         pointer-events: none;
-        transition: bottom 0.2s ease;
-        display: flex;
-        align-items: center;
-        gap: 10px;
-        padding: 4px 10px;
-        background: rgba(0, 0, 0, 0.46);
-        backdrop-filter: blur(4px);
-        border-radius: 4px;
-        box-sizing: border-box;
+        // transition: bottom 0.2s ease;
+        // display: flex;
+        // align-items: center;
+        // gap: 10px;
+        // box-sizing: border-box;
+
+        &__img {
+            display: block;
+            max-width: 120px;
+            max-height: 160px;
+            object-fit: contain;
+        }
 
         &--en {
             flex-direction: column;

+ 0 - 1
src/views/old_mini/work_detail/index.vue

@@ -508,7 +508,6 @@ const goGrowthTrack = () => {
         path: "/growth_track",
         query: {
             id: farmData.value?.id || workId.value || route.query.id,
-            type: farmData.value?.type || "growth",
         },
     });
 };

+ 5 - 0
vue.config.js

@@ -41,6 +41,11 @@ module.exports = {
                 target: 'https://restapi.amap.com',
                 changeOrigin: true,
                 ws: false,
+            },
+            '/mapimg': {
+                target: 'https://feiniao-farm-h5-v3.feiniaotech.com',
+                changeOrigin: true,
+                ws: false,
             }
         },
         //这里的ip和端口是前端项目的;下面为需要跨域访问后端项目