Selaa lähdekoodia

fix: 首页无农场

lxf 3 viikkoa sitten
vanhempi
commit
574026a693

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

@@ -133,4 +133,19 @@ module.exports = {
         url: config.base_new_url + "questionnaire/abnormal_fw_answer",
         type: "post",
     },
+    // 首页生成气象地图
+    getHomeMapCalcByCounty: {
+        url: config.base_new_url + "questionnaire/get_map_calc_by_county",
+        type: "get",
+    },
+    // 区县查询胁迫报告(无农场,参数 county_code)
+    getPointStressReport: {
+        url: config.base_new_url + "report/weather_stress_report_latlon",
+        type: "get",
+    },
+    // 区县查询风险报告(无农场,参数 county_code)
+    getPointRiskReport: {
+        url: config.base_new_url + "report/weather_risk_report_latlon",
+        type: "get",
+    },
 }

BIN
src/assets/img/type_icon/水稻.png


BIN
src/assets/img/type_icon/玉米.png


BIN
src/assets/img/type_icon/荔枝.png


+ 0 - 1
src/components/popup/completeFarmInfoPopup.vue

@@ -3,7 +3,6 @@
         v-model:show="showValue"
         round
         teleport="body"
-        :z-index="20000"
         class="complete-farm-popup"
         :close-on-click-overlay="false"
     >

+ 33 - 27
src/components/popup/diagnosisReportPopup.vue

@@ -26,69 +26,75 @@ import { useI18n } from "@/i18n";
 const { t } = useI18n();
 const router = useRouter();
 
-const ENTRY_FARM_DATA_KEY = "ENTRY_FARM_DATA";
-const ENTRY_BA_FORM_KEY = "ENTRY_BA_FORM";
 const SHOWN_CACHE_PREFIX = "DIAGNOSIS_REPORT_POPUP_SHOWN_";
 
 /** 弹窗显隐由组件内接口控制,默认不展示 */
 const visible = ref(false);
 
-const readEntryFarmPhone = () => {
+/** 当前缓存中选中的农场 / 分区 id */
+const getSelectedFarmId = () => {
     try {
-        const farm = JSON.parse(localStorage.getItem(ENTRY_FARM_DATA_KEY) || "null");
-        if (farm?.phone) return String(farm.phone).trim();
+        const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+        return String(
+            localStorage.getItem("selectedFarmId") || farm.farm_id || farm.id || ""
+        );
     } catch {
-        /* ignore */
-    }
-    try {
-        const baForm = JSON.parse(sessionStorage.getItem(ENTRY_BA_FORM_KEY) || "null");
-        if (baForm?.phone) return String(baForm.phone).trim();
-    } catch {
-        /* ignore */
+        return String(localStorage.getItem("selectedFarmId") || "");
     }
+};
+
+const getSelectedZoneId = () => {
     try {
-        const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
-        return String(userInfo.tel || userInfo.phone || userInfo.mobile || "").trim();
+        const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+        const zoneId = farm.zone_id ?? farm.zoneId ?? "";
+        return zoneId === "" || zoneId == null ? "" : String(zoneId);
     } catch {
         return "";
     }
 };
 
-const getShownCacheKey = (tel) => `${SHOWN_CACHE_PREFIX}${tel}`;
-const hasShownPopup = (tel) => !!localStorage.getItem(getShownCacheKey(tel));
+const getShownCacheKey = (farmId, zoneId) =>
+    `${SHOWN_CACHE_PREFIX}${farmId}_${zoneId || ""}`;
+const hasShownPopup = (farmId, zoneId) =>
+    !!localStorage.getItem(getShownCacheKey(farmId, zoneId));
 
-const markPopupShown = (tel) => {
-    if (!tel) return;
-    localStorage.setItem(getShownCacheKey(tel), "1");
+const markPopupShown = (farmId, zoneId) => {
+    if (!farmId) return;
+    localStorage.setItem(getShownCacheKey(farmId, zoneId), "1");
 };
 
 /** exists=2 且本地未展示过时弹窗(不依赖物候确认) */
 const checkShouldShow = async () => {
-    const tel = readEntryFarmPhone();
-    if (!tel || hasShownPopup(tel)) {
+    const farmId = getSelectedFarmId();
+    const zoneId = getSelectedZoneId();
+    if (!farmId || hasShownPopup(farmId, zoneId)) {
         visible.value = false;
         return;
     }
     try {
-        const res = await VE_API.questionnaire.checkReportGenerated({ tel });
-        const shouldShow = res?.code === 200 && Number(res?.data?.exists) === 2;
+        const res = await VE_API.questionnaire.checkReportGenerated({
+            farm_id: farmId,
+            zone_id: zoneId,
+        });
+        const shouldShow = res?.code === 200 && Number(res?.data?.exists) === 1;
         visible.value = shouldShow;
-        if (shouldShow) markPopupShown(tel);
+        if (shouldShow) markPopupShown(farmId, zoneId);
     } catch {
         visible.value = false;
     }
 };
 
 const handleView = () => {
-    const tel = readEntryFarmPhone();
-    markPopupShown(tel);
+    const farmId = getSelectedFarmId();
+    const zoneId = getSelectedZoneId();
+    markPopupShown(farmId, zoneId);
     visible.value = false;
     router.push("/diagnosis_report");
 };
 
 // 用户点遮罩关闭时也记为已展示,避免反复弹出
 watch(visible, (val, oldVal) => {
-    if (oldVal && !val) markPopupShown(readEntryFarmPhone());
+    if (oldVal && !val) markPopupShown(getSelectedFarmId(), getSelectedZoneId());
 });
 
 onMounted(() => {

+ 0 - 1
src/components/popup/farmCalendarPopup.vue

@@ -4,7 +4,6 @@
         round
         teleport="body"
         class="farm-calendar-popup"
-        :z-index="20000"
         :close-on-click-overlay="false"
     >
         <div class="farm-calendar-popup__title">{{ t("agriFile.completeFarmInfo") }}</div>

+ 5 - 1
src/views/old_mini/agri_file/pages/abnormalTrack.vue

@@ -83,7 +83,11 @@
                     <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="" />
+                            <photo-provider :photo-closable="true">
+                                <photo-consumer :src="resolveGuideImage(item.pic_url)">
+                                    <img :src="resolveGuideImage(item.pic_url)" alt="" />
+                                </photo-consumer>
+                            </photo-provider>
                         </div>
                     </div>
                 </div>

+ 8 - 1
src/views/old_mini/dev_login.vue

@@ -54,7 +54,14 @@ const resolveFarmsAfterLogin = async () => {
     try {
         const res = await VE_API.questionnaire.getFarms({ id });
         const list = Array.isArray(res?.data) ? res.data : [];
-        if (!list.length) return;
+        if (!list.length) {
+            // 无农场清掉旧选中缓存,避免长势报告仍按有农场走接口
+            localStorage.removeItem("selectedFarmId");
+            localStorage.removeItem("selectedFarmName");
+            localStorage.removeItem("selectedFarmPoint");
+            localStorage.removeItem("selectedFarmData");
+            return;
+        }
 
         let cached = null;
         try {

+ 43 - 15
src/views/old_mini/growth_report/alert_detail.vue

@@ -88,11 +88,6 @@ import customHeader from "@/components/customHeader.vue";
 
 const route = useRoute();
 
-const FALLBACK_QUERY_PARAMS = {
-    farm_id: "321",
-    zone_id: "46",
-    date: "2026-08-11",
-};
 const DETAIL_TYPE = {
     WARNING: "warning",
     STRESS: "stress",
@@ -102,6 +97,17 @@ const phenologyIcon = require("@/assets/img/common/header-icon-3.png");
 const weatherIcon = require("@/assets/img/common/header-icon-1.png");
 const soilIcon = require("@/assets/img/common/header-icon-2.png");
 const cropCover = require("@/assets/img/home/plant.png");
+/** 当前作物名 → 品类图标(与 CropAlertPanel 一致) */
+const CROP_ICON_MAP = {
+    水稻: require("@/assets/img/type_icon/水稻.png"),
+    玉米: require("@/assets/img/type_icon/玉米.png"),
+    荔枝: require("@/assets/img/type_icon/荔枝.png"),
+};
+
+const resolveCropCover = (cropName) => {
+    const name = String(cropName || "").trim();
+    return (name && CROP_ICON_MAP[name]) || cropCover;
+};
 
 const detailType = computed(() =>
     String(route.query.type || "") === DETAIL_TYPE.STRESS
@@ -141,13 +147,13 @@ const formatLocalDate = (date) => {
     return `${y}-${m}-${d}`;
 };
 
-/** 有选中农场用缓存 farm_id/zone_id + 当天日期,否则用默认参数 */
+/** 有选中农场用缓存 farm_id/zone_id + 当天日期 */
 const getQueryParams = () => {
     const farm = getSelectedFarm();
     const farmId = farm.farm_id ?? farm.id;
     const zoneId = farm.zone_id ?? farm.zoneId;
     if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
-        return { ...FALLBACK_QUERY_PARAMS };
+        return null;
     }
     return {
         farm_id: farmId,
@@ -156,11 +162,24 @@ const getQueryParams = () => {
     };
 };
 
-/** 接口可能返回对象或数组,统一取第一条 */
+/** 无农场时与底图一致,用区县码 */
+const getCountyAlertParams = () => {
+    const code = localStorage.getItem("GROWTH_REPORT_COUNTY_CODE") || "";
+    if (!code) return null;
+    return { county_code: String(code) };
+};
+
+/**
+ * 统一取第一条业务记录:
+ * - 点位接口:data.records[]
+ * - 农场接口:data 为对象或数组
+ */
 const pickFirstRecord = (res) => {
     const data = res?.data ?? res;
+    if (!data) return null;
+    if (Array.isArray(data?.records)) return data.records[0] || null;
     if (Array.isArray(data)) return data[0] || null;
-    if (data && typeof data === "object") return data;
+    if (typeof data === "object") return data;
     return null;
 };
 
@@ -216,11 +235,12 @@ const applyRecord = (record, type) => {
     const isStress = type === DETAIL_TYPE.STRESS;
     detailTitle.value = isStress ? record.stress_name || "" : record.risk_name || "";
 
+    const cropName = record.crop_name || "";
     analysis.value = {
         summary: record.interact_explain || "",
         crop: {
-            name: record.crop_name || "",
-            cover: cropCover,
+            name: cropName,
+            cover: resolveCropCover(cropName),
             metrics: buildMetrics(record, type),
         },
     };
@@ -243,13 +263,21 @@ const applyRecord = (record, type) => {
 
 const fetchDetail = async () => {
     const type = detailType.value;
-    const request =
-        type === DETAIL_TYPE.STRESS
+    const farmParams = getQueryParams();
+    const countyParams = farmParams ? null : getCountyAlertParams();
+    const request = farmParams
+        ? type === DETAIL_TYPE.STRESS
             ? VE_API.questionnaire.getWeatherStress
-            : VE_API.questionnaire.getWeatherRisk;
+            : VE_API.questionnaire.getWeatherRisk
+        : type === DETAIL_TYPE.STRESS
+          ? VE_API.questionnaire.getPointStressReport
+          : VE_API.questionnaire.getPointRiskReport;
+    const params = farmParams || countyParams;
+    if (!params) return;
+
     loading.value = true;
     try {
-        const res = await request(getQueryParams());
+        const res = await request(params);
         applyRecord(pickFirstRecord(res), type);
     } catch {
         // 详情页保持空态

+ 203 - 41
src/views/old_mini/growth_report/components/CropAlertPanel.vue

@@ -13,11 +13,11 @@
                 <div class="panel-shell" ref="panelShellRef">
                     <div class="crop-header">
                         <div class="crop-info">
-                            <el-avatar class="crop-avatar" :size="24" :src="cropAvatar" />
+                            <el-avatar class="crop-avatar" :size="24" :src="resolvedCropAvatar" />
                             <span class="crop-name">{{ currentCropName }}</span>
                             <span class="crop-badge">当前作物</span>
                         </div>
-                        <div class="switch-wrap">
+                        <div v-if="hasFarm" class="switch-wrap">
                             <el-tooltip
                                 v-model:visible="cropMenuVisible"
                                 placement="bottom"
@@ -65,31 +65,40 @@
 
                     <div class="alert-list" v-show="!isBackground">
                         <div
-                            v-for="(item, index) in alertCards"
-                            :key="index"
-                            class="alert-card"
-                        >
-                            <div class="alert-card__header">
-                                <div class="alert-card__title-wrap">
-                                    <img class="alert-card__icon" :src="item.icon" alt="" />
-                                    <span class="alert-card__title">{{ item.title }}</span>
+                            v-if="reportGenerating"
+                            class="alert-generating"
+                            v-loading="true"
+                            element-loading-text="报告生成中,请稍后..."
+                            element-loading-background="rgba(255, 255, 255, 0.9)"
+                        ></div>
+                        <template v-else>
+                            <div
+                                v-for="(item, index) in alertCards"
+                                :key="index"
+                                class="alert-card"
+                            >
+                                <div class="alert-card__header">
+                                    <div class="alert-card__title-wrap">
+                                        <img class="alert-card__icon" :src="item.icon" alt="" />
+                                        <span class="alert-card__title">{{ item.title }}</span>
+                                    </div>
+                                    <span class="alert-card__link" @click="handleViewDetail(item)">
+                                        查看详情
+                                    </span>
+                                </div>
+                                <div class="alert-card__content-wrap">
+                                    <span class="alert-card__content">{{ item.content }}</span>
+                                    <span
+                                        v-if="hasFarm && item.patrolTip && item.title !== '正常气象'"
+                                        class="alert-card__patrol"
+                                        @click="handlePatrolTip(item)"
+                                    >
+                                        <el-icon class="alert-card__patrol-icon"><Link /></el-icon>
+                                        <span>{{ item.patrolTip }}</span>
+                                    </span>
                                 </div>
-                                <span class="alert-card__link" @click="handleViewDetail(item)">
-                                    查看详情
-                                </span>
-                            </div>
-                            <div class="alert-card__content-wrap">
-                                <span class="alert-card__content">{{ item.content }}</span>
-                                <span
-                                    v-if="item.patrolTip && item.title !== '正常气象'"
-                                    class="alert-card__patrol"
-                                    @click="handlePatrolTip(item)"
-                                >
-                                    <el-icon class="alert-card__patrol-icon"><Link /></el-icon>
-                                    <span>{{ item.patrolTip }}</span>
-                                </span>
                             </div>
-                        </div>
+                        </template>
                     </div>
                 </div>
             </div>
@@ -100,7 +109,7 @@
 <script setup>
 import { FloatingPanel } from "vant";
 import { CaretBottom, Link } from "@element-plus/icons-vue";
-import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
+import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { useStore } from "vuex";
 import { useI18n } from "@/i18n";
 import { useRouter } from "vue-router";
@@ -115,12 +124,22 @@ const CARD_ID = {
     STRESS: 2,
 };
 
+const COUNTY_CODE_STORAGE_KEY = "GROWTH_REPORT_COUNTY_CODE";
+/** 有农场预警为空时轮询:间隔 5s,最长 3 分钟 */
+const REPORT_POLL_INTERVAL_MS = 5000;
+const REPORT_POLL_MAX_MS = 3 * 60 * 1000;
+
 const props = defineProps({
     /** 当前选中农场 id,切换农场后触发分区 / 预警重拉 */
     farmId: {
         type: [String, Number],
         default: "",
     },
+    /** 无农场时区县码,与 getHomeMapCalcByCounty 同源 */
+    countyCode: {
+        type: [String, Number],
+        default: "",
+    },
     cropName: {
         type: String,
         default: "",
@@ -140,6 +159,13 @@ const activeIndex = ref(0);
 const emit = defineEmits(["viewDetail", "heightChange", "zoneChange"]);
 
 const weatherIcon = require("@/assets/img/report/weather.png");
+const DEFAULT_CROP_AVATAR = "https://birdseye-img.sysuimars.com/dinggou-mini/defalut-icon.png";
+/** 当前作物名 → 品类图标 */
+const CROP_ICON_MAP = {
+    水稻: require("@/assets/img/type_icon/水稻.png"),
+    玉米: require("@/assets/img/type_icon/玉米.png"),
+    荔枝: require("@/assets/img/type_icon/荔枝.png"),
+};
 
 /** 底部 tabBar 高度(含小程序安全区),优先读 DOM / store */
 const resolveTabBarHeight = () => {
@@ -152,14 +178,29 @@ const resolveTabBarHeight = () => {
 const tabBarHeight = ref(resolveTabBarHeight());
 
 const currentCropName = ref(props.cropName || "");
+/** 无农场时来自点位预警接口的 crop_name */
+const pointCropName = ref("");
+/** 按当前作物名匹配 type_icon,未命中用默认图 */
+const resolvedCropAvatar = computed(() => {
+    const name = String(currentCropName.value || "").trim();
+    if (name && CROP_ICON_MAP[name]) return CROP_ICON_MAP[name];
+    return props.cropAvatar || DEFAULT_CROP_AVATAR;
+});
 const cropMenuVisible = ref(false);
 const guideVisible = ref(false);
 
 /** 防止切换农场时旧请求回写覆盖新数据 */
 let reloadToken = 0;
+/** 有农场报告生成中:空数据轮询 */
+const reportGenerating = ref(false);
+let reportPollTimer = null;
+let reportPollStartedAt = 0;
 
-/** 展示名:优先当前分区 zone_name,其次 props / 农场缓存(新建农场尚无分区时用) */
+/** 展示名:有农场优先分区/农场缓存;无农场用点位接口 crop_name */
 const resolveCropNameDisplay = () => {
+    if (!resolveFarmId()) {
+        return pointCropName.value || "";
+    }
     const zone = zoneList.value[activeIndex.value];
     if (zone?.variety_name) return zone.variety_name;
     const farm = getSelectedFarm();
@@ -191,6 +232,16 @@ watch(
     }
 );
 
+/** 无农场:父页逆地理拿到 county_code 后再拉点位预警 */
+watch(
+    () => props.countyCode,
+    (code, prev) => {
+        if (String(code ?? "") === String(prev ?? "")) return;
+        if (resolveFarmId()) return;
+        nextTick(() => fetchAlertCards());
+    }
+);
+
 const dismissGuide = () => {
     if (!guideVisible.value) return;
     guideVisible.value = false;
@@ -198,6 +249,7 @@ const dismissGuide = () => {
 };
 
 const tryShowGuide = () => {
+    if (!hasFarm.value) return;
     if (localStorage.getItem(GUIDE_STORAGE_KEY)) return;
     guideVisible.value = true;
 };
@@ -340,6 +392,12 @@ const resolveFarmId = () => {
     return id != null && id !== "" ? id : "";
 };
 
+/** 依赖 farmId,无农场时隐藏切换作物 */
+const hasFarm = computed(() => {
+    void props.farmId;
+    return !!resolveFarmId();
+});
+
 /** 保证有zone_id */
 const resolveZoneId = () => {
     const farm = getSelectedFarm();
@@ -388,9 +446,39 @@ const fetchZones = async (token = reloadToken) => {
     }
 };
 
+const stopReportPoll = () => {
+    if (reportPollTimer) {
+        clearTimeout(reportPollTimer);
+        reportPollTimer = null;
+    }
+    reportGenerating.value = false;
+    reportPollStartedAt = 0;
+};
+
+/** 空数据时启动/续期轮询;超时或 token 失效则停止 */
+const scheduleReportPoll = (token, queryParams) => {
+    if (token !== reloadToken) {
+        stopReportPoll();
+        return;
+    }
+    if (!reportPollStartedAt) {
+        reportPollStartedAt = Date.now();
+    }
+    if (Date.now() - reportPollStartedAt >= REPORT_POLL_MAX_MS) {
+        stopReportPoll();
+        return;
+    }
+    reportGenerating.value = true;
+    if (reportPollTimer) clearTimeout(reportPollTimer);
+    reportPollTimer = setTimeout(() => {
+        fetchAlertCards(token, { queryParams, fromPoll: true });
+    }, REPORT_POLL_INTERVAL_MS);
+};
+
 /** 切换农场 / 回页:先清旧 zone,再拉分区,最后拉预警 */
 const reloadFarmAlerts = async () => {
     const token = ++reloadToken;
+    stopReportPoll();
     selectedZoneId.value = null;
     activeIndex.value = 0;
     zoneList.value = [];
@@ -401,11 +489,17 @@ const reloadFarmAlerts = async () => {
     await fetchAlertCards(token);
 };
 
-/** 接口可能返回对象或数组,统一取第一条 */
+/**
+ * 统一取第一条业务记录:
+ * - 点位接口:data.records[]
+ * - 农场接口:data 为对象或数组
+ */
 const pickFirstRecord = (res) => {
     const data = res?.data ?? res;
+    if (!data) return null;
+    if (Array.isArray(data?.records)) return data.records[0] || null;
     if (Array.isArray(data)) return data[0] || null;
-    if (data && typeof data === "object") return data;
+    if (typeof data === "object") return data;
     return null;
 };
 
@@ -429,25 +523,78 @@ const applyWeatherStress = (record) => {
     });
 };
 
-const fetchAlertCards = async (token = reloadToken) => {
-    const queryParams = getQueryParams();
-    if (queryParams.farm_id == null || queryParams.farm_id === "") {
-        if (token !== reloadToken) return;
+/** 无农场:与底图一致,用区县码查风险/胁迫 */
+const getCountyAlertParams = () => {
+    const code =
+        props.countyCode ||
+        localStorage.getItem(COUNTY_CODE_STORAGE_KEY) ||
+        "";
+    if (code == null || code === "") return null;
+    return { county_code: String(code) };
+};
+
+/**
+ * @param token 请求世代
+ * @param options.queryParams 轮询复用参数;options.fromPoll 是否来自空数据轮询
+ */
+const fetchAlertCards = async (token = reloadToken, options = {}) => {
+    const queryParams = options.queryParams || getQueryParams();
+    const hasFarm = queryParams.farm_id != null && queryParams.farm_id !== "";
+    const fromPoll = !!options.fromPoll;
+
+    let riskRes;
+    let stressRes;
+    if (hasFarm) {
+        pointCropName.value = "";
+        [riskRes, stressRes] = await Promise.allSettled([
+            VE_API.questionnaire.getWeatherRisk(queryParams),
+            VE_API.questionnaire.getWeatherStress(queryParams),
+        ]);
+    } else {
+        stopReportPoll();
+        const countyParams = getCountyAlertParams();
+        if (!countyParams) {
+            if (token !== reloadToken) return;
+            applyWeatherRisk(null);
+            applyWeatherStress(null);
+            pointCropName.value = "";
+            syncCropNameDisplay();
+            nextTick(() => syncMidAnchor());
+            return;
+        }
+        [riskRes, stressRes] = await Promise.allSettled([
+            VE_API.questionnaire.getPointRiskReport(countyParams),
+            VE_API.questionnaire.getPointStressReport(countyParams),
+        ]);
+    }
+    // 切换农场期间若有更新请求,丢弃旧回包
+    if (token !== reloadToken) return;
+
+    const riskRecord = riskRes.status === "fulfilled" ? pickFirstRecord(riskRes.value) : null;
+    const stressRecord = stressRes.status === "fulfilled" ? pickFirstRecord(stressRes.value) : null;
+
+    // 有农场且双接口均为空:展示生成中并轮询(5s,最长 3 分钟)
+    if (hasFarm && !riskRecord && !stressRecord) {
         applyWeatherRisk(null);
         applyWeatherStress(null);
+        scheduleReportPoll(token, queryParams);
         nextTick(() => syncMidAnchor());
         return;
     }
-    const [riskRes, stressRes] = await Promise.allSettled([
-        VE_API.questionnaire.getWeatherRisk(queryParams),
-        VE_API.questionnaire.getWeatherStress(queryParams),
-    ]);
-    // 切换农场期间若有更新请求,丢弃旧回包
-    if (token !== reloadToken) return;
+
+    if (hasFarm || fromPoll) {
+        stopReportPoll();
+    }
 
     // 成功但无数据时清空旧卡片,避免切换农场后残留
-    applyWeatherRisk(riskRes.status === "fulfilled" ? pickFirstRecord(riskRes.value) : null);
-    applyWeatherStress(stressRes.status === "fulfilled" ? pickFirstRecord(stressRes.value) : null);
+    applyWeatherRisk(riskRecord);
+    applyWeatherStress(stressRecord);
+
+    // 无农场:作物名取任一接口返回的 crop_name
+    if (!hasFarm) {
+        pointCropName.value = riskRecord?.crop_name || stressRecord?.crop_name || "";
+        syncCropNameDisplay();
+    }
 
     nextTick(() => syncMidAnchor());
 };
@@ -514,6 +661,7 @@ onActivated(() => {
 });
 
 onBeforeUnmount(() => {
+    stopReportPoll();
     document.removeEventListener("click", handleOutsideClick, true);
     resizeObserver?.disconnect?.();
     resizeObserver = null;
@@ -679,6 +827,20 @@ defineExpose({ refreshAlerts: fetchAlertCards, refreshZones: fetchZones, reloadF
         gap: 10px;
     }
 
+    .alert-generating {
+        min-height: 120px;
+        padding: 14px 12px;
+        border-radius: 12px;
+        background: #fff;
+        box-sizing: border-box;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 14px;
+        color: #86909c;
+        text-align: center;
+    }
+
     .alert-card {
         padding: 12px;
         border-radius: 12px;

+ 150 - 31
src/views/old_mini/growth_report/index.vue

@@ -48,10 +48,14 @@
         <div v-if="!showInteractCard" class="location-bar">
             <el-icon class="location-bar__icon"><LocationFilled /></el-icon>
             <span class="location-bar__name van-ellipsis">{{ locationName }}</span>
-            <span class="location-bar__action" @click="handleSwitchFarm">切换农场</span>
+            <span
+                v-if="hasSelectedFarm()"
+                class="location-bar__action"
+                @click="handleSwitchFarm"
+            >切换农场</span>
         </div>
 
-        <!-- 左侧:气象底图图层菜单(getHomeMapCalc) -->
+        <!-- 左侧:气象底图图层菜单(有农场 getHomeMapCalc / 无农场 getHomeMapCalcByCounty) -->
         <div v-if="stressMenuItems.length" class="side-menu side-menu--left">
             <div
                 v-for="item in stressMenuItems"
@@ -88,6 +92,7 @@
         <crop-alert-panel
             :crop-name="currentCropName"
             :farm-id="selectedFarmId"
+            :county-code="countyCode"
             @zone-change="handleZoneChange"
             @height-change="handlePanelHeightChange"
         />
@@ -116,6 +121,8 @@ const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
 const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
 const LOCATION_SESSION_KEY = "GROWTH_REPORT_LOCATION";
 const MAP_POINT_STORAGE_KEY = "GROWTH_REPORT_MAP_POINT";
+/** 无农场时逆地理得到的区县码,供底图 / 点位预警共用 */
+const COUNTY_CODE_STORAGE_KEY = "GROWTH_REPORT_COUNTY_CODE";
 
 const menuIcon = require("@/assets/img/common/farm-active.png");
 
@@ -129,6 +136,8 @@ const locationName = ref("");
 /** status≠1 时展示中心勾画引导 */
 const noFarmRange = ref(false);
 const selectedFarmId = ref(localStorage.getItem("selectedFarmId") || "");
+/** 无农场时区县码(逆地理 adcode) */
+const countyCode = ref(localStorage.getItem(COUNTY_CODE_STORAGE_KEY) || "");
 /** CropAlertPanel 当前选中分区 */
 const panelZoneId = ref(null);
 const currentCropName = computed(() => {
@@ -142,7 +151,7 @@ const currentCropName = computed(() => {
         ""
     );
 });
-/** 侧边气象图层菜单(来自 getHomeMapCalc) */
+/** 侧边气象图层菜单(有农场 getHomeMapCalc / 无农场 getHomeMapCalcByCounty) */
 const stressMenuItems = ref([]);
 const activeMenuKey = ref("");
 /** 当前选中图层图例图 */
@@ -161,6 +170,45 @@ const getSelectedFarm = () => {
     }
 };
 
+/** 登录写入的用户定位:lng,lat */
+const getMiniUserLocationText = () => localStorage.getItem("MINI_USER_LOCATION") || "";
+
+/** 登录写入的用户定位 POINT;无 POINT 时用 lng,lat 拼 */
+const getMiniUserLocationPoint = () => {
+    const point =
+        localStorage.getItem("MINI_USER_LOCATION_POINT") ||
+        store.state.home.miniUserLocationPoint ||
+        "";
+    if (typeof point === "string" && /^POINT\s*\(/i.test(point.trim())) {
+        return point.trim();
+    }
+    const text = getMiniUserLocationText();
+    if (text.includes(",")) {
+        const [lng, lat] = text.split(",").map((s) => s.trim());
+        if (lng && lat) return `POINT(${lng} ${lat})`;
+    }
+    return DEFAULT_MAP_POINT;
+};
+
+/** 腾讯逆地理 location 为 lat,lng;MINI_USER_LOCATION 存的是 lng,lat */
+const toTencentLocationParam = (lngLatText) => {
+    const parts = String(lngLatText || "")
+        .split(",")
+        .map((s) => s.trim());
+    if (parts.length < 2 || !parts[0] || !parts[1]) return "";
+    return `${parts[1]},${parts[0]}`;
+};
+
+const hasSelectedFarm = () => {
+    const farm = getSelectedFarm();
+    const id =
+        selectedFarmId.value ||
+        localStorage.getItem("selectedFarmId") ||
+        farm.farm_id ||
+        farm.id;
+    return id != null && id !== "";
+};
+
 /** 地图中心优先用农场 farm_location */
 const resolveFarmLocationWkt = (farm = getSelectedFarm()) => {
     const candidates = [farm.farm_location, farm.wkt, farm.geom_wkt, localStorage.getItem("selectedFarmPoint")];
@@ -169,10 +217,16 @@ const resolveFarmLocationWkt = (farm = getSelectedFarm()) => {
             return item.trim();
         }
     }
-    return store.state.home.miniUserLocationPoint || DEFAULT_MAP_POINT;
+    return getMiniUserLocationPoint();
+};
+
+/** 有农场用农场点,无农场用登录用户点位 */
+const resolveMapCenterPoint = (farm = getSelectedFarm()) => {
+    if (hasSelectedFarm()) return resolveFarmLocationWkt(farm);
+    return getMiniUserLocationPoint();
 };
 
-const currentMapPoint = ref(resolveFarmLocationWkt());
+const currentMapPoint = ref(resolveMapCenterPoint());
 
 const getSelectedZoneId = () => {
     const farm = getSelectedFarm();
@@ -185,12 +239,18 @@ const syncSelectedFarmId = () => {
     );
 };
 
-/** status=1 有范围;否则展示勾画引导 */
+/** status=1 有范围;否则展示勾画引导(无农场不展示) */
 async function fetchFarmRangeStatus() {
     const farm = getSelectedFarm();
     const farmId = selectedFarmId.value || farm.farm_id || farm.id;
     const zoneId = panelZoneId.value ?? getSelectedZoneId();
-    if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
+    // 无农场不展示勾画引导
+    if (farmId == null || farmId === "") {
+        noFarmRange.value = false;
+        await syncNoRangeGuide();
+        return;
+    }
+    if (zoneId == null || zoneId === "") {
         noFarmRange.value = true;
         await syncNoRangeGuide();
         return;
@@ -385,7 +445,42 @@ function applyMenuOverlay(key) {
     activeLegendUrl.value = item.legendUrl || "";
 }
 
+function applyMapCalcRecords(records) {
+    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();
+    }
+}
+
+/** 无农场:按区县 adcode 拉气象底图 */
+async function fetchHomeMapCalcByCounty(countyCode) {
+    const token = ++mapCalcToken;
+    if (!countyCode) {
+        clearMapCalcState();
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.getHomeMapCalcByCounty({
+            county_code: countyCode,
+        });
+        if (token !== mapCalcToken) return;
+        const records = Array.isArray(res?.data?.records) ? res.data.records.slice(0, 3) : [];
+        applyMapCalcRecords(records);
+    } catch (e) {
+        if (token !== mapCalcToken) return;
+        console.warn("[growth_report] getHomeMapCalcByCounty failed", e);
+        clearMapCalcState();
+    }
+}
+
+/** 有农场:按农场/分区拉气象底图 */
 async function fetchHomeMapCalc() {
+    if (!hasSelectedFarm()) return;
+
     const token = ++mapCalcToken;
     const farm = getSelectedFarm();
     const farmId = selectedFarmId.value || farm.farm_id || farm.id;
@@ -409,15 +504,8 @@ async function fetchHomeMapCalc() {
             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();
-        }
+        const records = Array.isArray(res?.data?.records) ? res.data.records.slice(0, 3) : [];
+        applyMapCalcRecords(records);
     } catch (e) {
         if (token !== mapCalcToken) return;
         console.warn("[growth_report] getHomeMapCalc failed", e);
@@ -432,20 +520,52 @@ const handleZoneChange = (zoneId) => {
     fetchInteractTask();
 };
 
-function getLocationName(point = currentMapPoint.value) {
-    const locationPoint = point || localStorage.getItem("selectedFarmPoint") || store.state.home.miniUserLocationPoint;
-    if (!locationPoint) return;
-    const farmLocation = convertPointToArray(locationPoint);
-    if (!farmLocation?.length) return;
-    const params = {
-        key: MAP_KEY,
-        location: `${farmLocation[1]},${farmLocation[0]}`,
-    };
-    VE_API.old_mini_map.location(params).then(({ result }) => {
+async function getLocationName(point = currentMapPoint.value) {
+    let locationParam = "";
+    // 无农场:逆地理用登录点 MINI_USER_LOCATION(lng,lat → lat,lng)
+    if (!hasSelectedFarm()) {
+        locationParam = toTencentLocationParam(getMiniUserLocationText());
+    }
+    if (!locationParam) {
+        const locationPoint =
+            point ||
+            localStorage.getItem("selectedFarmPoint") ||
+            getMiniUserLocationPoint();
+        if (!locationPoint) return;
+        const farmLocation = convertPointToArray(locationPoint);
+        if (Array.isArray(farmLocation) && farmLocation.length >= 2) {
+            locationParam = `${farmLocation[1]},${farmLocation[0]}`;
+        } else if (typeof farmLocation === "string" && farmLocation.includes(",")) {
+            locationParam = toTencentLocationParam(farmLocation);
+        } else {
+            return;
+        }
+    }
+    try {
+        const { result } = await VE_API.old_mini_map.location({
+            key: MAP_KEY,
+            location: locationParam,
+        });
         locationName.value = result?.address_component
             ? result.address_component.city + result.address_component.district
-            : result?.address + "";
-    });
+            : (result?.address || "") + "";
+        // 无农场时用 adcode 拉区县气象底图,并同步给预警面板
+        if (!hasSelectedFarm()) {
+            const adcode = result?.ad_info?.adcode;
+            if (adcode != null && adcode !== "") {
+                const code = String(adcode);
+                countyCode.value = code;
+                localStorage.setItem(COUNTY_CODE_STORAGE_KEY, code);
+                await fetchHomeMapCalcByCounty(code);
+            } else {
+                countyCode.value = "";
+                localStorage.removeItem(COUNTY_CODE_STORAGE_KEY);
+                clearMapCalcState();
+            }
+        }
+    } catch (e) {
+        console.warn("[growth_report] getLocationName failed", e);
+    }
 }
 
 function saveCurrentMapPoint(point) {
@@ -497,10 +617,9 @@ function applySelectedLocation() {
     return true;
 }
 
-/** 切换农场后:地图中心落到当前农场 farm_location */
+/** 有农场落到 farm_location;无农场落到登录用户点位 */
 function syncMapFromSelectedFarm() {
-    const farm = getSelectedFarm();
-    const point = resolveFarmLocationWkt(farm);
+    const point = resolveMapCenterPoint();
     if (!point) return false;
     saveCurrentMapPoint(point);
     const coordinate = convertPointToArray(point)?.map(Number);