Ver código fonte

fix: 对接土壤,农事

lxf 6 horas atrás
pai
commit
251896d688

+ 5 - 0
src/api/modules/pheno.js

@@ -7,4 +7,9 @@ module.exports = {
         url: config.base_new_url + "pheno/interact_tasks_with_abnormal",
         type: "get",
     },
+    // 土壤问题互动查询
+    getSoilTasks: {
+        url: config.base_new_url + "pheno/soil_root_perception",
+        type: "get",
+    },
 }

+ 4 - 3
src/api/modules/questionnaire.js

@@ -32,8 +32,7 @@ module.exports = {
         type: "get",
     },
     // 分区农事任务列表:GET /api/questionnaire/zone_fw_task_list
-    // 参数 zone_id + farm_id + trigger_date + crop_type
-    // 返回 fw_tasks(标准)/ weather_work(气象)/ abnormal_fw_tasks(异常)
+    // 参数 zone_id + farm_id + crop_type(单次返回近 7 天窗口,data.days 内每日含 fw_tasks/weather_work/abnormal_fw_tasks)
     getZoneFwTaskList: {
         url: config.base_new_url + "questionnaire/zone_fw_task_list",
         type: "get",
@@ -98,7 +97,9 @@ module.exports = {
         url: config.base_new_url + "questionnaire/zone_image_list",
         type: "get",
     },
-    // 诊断报告列表
+    // 诊断报告列表(最新报告 + 历史报告分页)
+    // GET 参数:farm_id, zone_id, page(从 1 开始), page_size(默认 10)
+    // 返回 data.latest(最新报告)、data.history(按月分组的历史,分页追加)、data.has_more、data.total_pages、data.history_count
     getReportList: {
         url: config.base_new_url + "questionnaire/weather_report_list",
         type: "get",

BIN
src/assets/img/agricultural/soil-detail-bg.png


+ 8 - 0
src/i18n/messages.js

@@ -147,6 +147,10 @@ export default {
             patrolRecord: "巡园记录",
             patrolGrowthTitle: "物候跟踪巡园要点",
             patrolAbnormalTitle: "异常态势巡园要点",
+            patrolSoilTitle: "土壤根系巡园要点",
+            soilRootTheme: "渍根风险提高",
+            soilRootReason: "连阴雨泡根,根系发黑缺氧,赶紧排水护根!",
+            soilRootAssessment: "持续阴雨导致果园积水,根系缺氧发黑,需及时巡园排查。",
             patrolSubject: "--",
             patrolIssue: "暂无数据",
             forward: "转发",
@@ -597,6 +601,10 @@ export default {
             patrolRecord: "Patrol records",
             patrolGrowthTitle: "Growth tracking patrol",
             patrolAbnormalTitle: "Abnormality patrol",
+            patrolSoilTitle: "Soil root patrol",
+            soilRootTheme: "Waterlogging root risk increased",
+            soilRootReason: "Continuous rain floods roots; black, oxygen-deprived roots—drain and protect now!",
+            soilRootAssessment: "Prolonged rain causes orchard waterlogging and root hypoxia; inspect promptly.",
             patrolSubject: "Subject",
             patrolIssue: "Interaction questions interaction questions",
             forward: "Forward",

+ 7 - 0
src/router/globalRoutes.js

@@ -117,6 +117,13 @@ export default [
         meta: { keepAlive: true },
         component: () => import("@/views/old_mini/agri_file/pages/abnormalTrack.vue"),
     },
+    // 土壤根系互动巡园要点
+    {
+        path: "/soil_root_track",
+        name: "SoilRootTrack",
+        meta: { keepAlive: true },
+        component: () => import("@/views/old_mini/agri_file/pages/soilRootTrack.vue"),
+    },
     // 病虫害互动选地块
     {
         path: "/pest_interact",

+ 4 - 0
src/styles/common.scss

@@ -217,6 +217,10 @@ div {
     }
 }
 
+.el-loading-spinner .el-loading-text {
+    color: #FFD489;
+}
+
 @font-face {
     font-family: "SOURCEHANSANSCN_0";
     src: url("@/assets/font/SOURCEHANSANSCN-REGULAR_0.OTF");

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

@@ -62,7 +62,7 @@
                 <div class="album-card__cover-mask">
                     <div class="album-card__name">{{ item.name }}</div>
                     <div class="album-card__name-count">
-                        <span class="name-val">{{ item.name }}</span>
+                        <span class="name-val">{{ item.crop_variety }}</span>
                         ·
                         <span class="count-val">{{ item.count }}张</span>
                     </div>
@@ -169,6 +169,7 @@ function mapZoneItem(zone) {
         zoneId: zone.zone_id,
         zone_id: zone.zone_id,
         name: zone.zone_name || "",
+        crop_variety: zone.crop_variety || "",
         count: zone.image_count ?? 0,
         cover: resolveZoneCoverUrl(zone.cover_url),
         longitude: coords?.longitude,

+ 110 - 145
src/views/old_mini/agri_file/components/patrolPanel.vue

@@ -22,7 +22,7 @@
         </div>
         <div v-if="loading" class="patrol-loading">
             <span class="patrol-loading__spinner"></span>
-            <span class="patrol-loading__text">巡园要点加载中...</span>
+            <span class="patrol-loading__text">加载中...</span>
         </div>
         <template v-else>
             <div
@@ -125,16 +125,6 @@ const calendarUrgencyIcon = require("@/assets/img/agricultural/clock.png");
 /** 土壤:含水率提示 */
 const waterUrgencyIcon = require("@/assets/img/agricultural/water.png");
 
-/** 统一任务的 type_code / task_type → 巡园卡片类型 */
-const TASK_TYPE_MAP = {
-    pheno: "growth",
-    abnormal: "abnormal",
-    soil: "soil",
-    物候: "growth",
-    异常: "abnormal",
-    土壤: "soil",
-};
-
 /** 接口返回的图片可能是 CDN 相对路径 */
 const resolveTaskImage = (url) => {
     if (!url) return "";
@@ -144,23 +134,6 @@ const resolveTaskImage = (url) => {
     return base_img_url2 + text.replace(/^\//, "");
 };
 
-/** 土壤根系感知占位卡片(接口无土壤任务时兜底展示) */
-const buildSoilPatrolMock = () => ({
-    id: "soil-mock",
-    type: "soil",
-    theme: "blue",
-    level: null,
-    title: "土壤根系感知",
-    subject: "连阴雨清沟排水",
-    issue: "连阴雨泡得芽点发软,不清沟排水,连阴雨泡得芽点发软,不清沟排水,",
-    desc: "",
-    urgencyText: "土壤含水率偏高 · 已持续 5 天",
-    urgencyTone: "warn",
-    urgencyIcon: waterUrgencyIcon,
-    headerIcon: soilIcon,
-    icon: PATROL_ICON,
-});
-
 // ---------- 页面状态 ----------
 const zoneList = ref([]);
 const selectedZoneId = ref(null);
@@ -184,8 +157,8 @@ const buildGrowthPatrolMock = () => ({
     icon: PATROL_ICON,
 });
 
-/** 默认组成:物候 → 土壤;异常卡由接口数据决定,无数据不占位 */
-const buildDefaultPatrolList = () => [buildGrowthPatrolMock(), buildSoilPatrolMock()];
+/** 默认组成:仅物候占位;异常 / 土壤根系卡由接口数据决定,无数据不占位 */
+const buildDefaultPatrolList = () => [buildGrowthPatrolMock()];
 
 const patrolList = ref(buildDefaultPatrolList());
 /** 巡园接口返回前的加载态 */
@@ -208,112 +181,83 @@ function formatToday() {
 const resolveCropType = (farm = getSelectedFarm()) =>
     farm.category_code || farm.categorycode || farm.crop_code || farm.crop_type || "";
 
-/** soil_interact_tasks.cards → 统一任务 */
-const mapSoilCardsToTasks = (cards = []) =>
-    cards.map((card, index) => ({
-        ...card,
-        type_code: "soil",
-        task_key: `soil:${card?.id || index}`,
-        payload: card,
-    }));
-
-/** 统一任务列表:优先 data.tasks;兼容按分块下发的旧结构 */
-const normalizeTasks = (data = {}) => {
-    const soilCards = Array.isArray(data.soil_interact_tasks?.cards)
-        ? data.soil_interact_tasks.cards
-        : [];
-    const soilTasks = mapSoilCardsToTasks(soilCards);
-
-    if (Array.isArray(data.tasks) && data.tasks.length) {
-        // 土壤任务可能只在 soil_interact_tasks.cards 中下发
-        const hasSoil = data.tasks.some((task) => task?.type_code === "soil");
-        return hasSoil ? data.tasks : [...data.tasks, ...soilTasks];
-    }
-
-    const tasks = [];
-    const outerTasks = data.outer_interact_tasks?.interact_tasks;
-    if (Array.isArray(outerTasks)) {
-        outerTasks.forEach((task, index) => {
-            tasks.push({
-                type_code: "pheno",
-                task_key: `pheno:${task?.interact_code || index}`,
-                payload: task,
-            });
-        });
-    }
-    const abInfo = data.abnormal_interact_info?.ab_it_info;
-    const abList = Array.isArray(abInfo?.interact_Info) ? abInfo.interact_Info : [];
-    abList.forEach((item, index) => {
-        tasks.push({
-            type_code: "abnormal",
-            task_key: `abnormal:${item?.fw_code || index}`,
-            payload: item,
-        });
-    });
-    tasks.push(...soilTasks);
-    return tasks;
-};
-
-/** 统一巡园任务 → 巡园卡片 */
-const mapTaskToPatrol = (task, data = {}) => {
-    const payload = task?.payload || {};
-    const content = payload.interact_content || {};
-    const type = TASK_TYPE_MAP[task?.type_code] || TASK_TYPE_MAP[task?.task_type] || "growth";
-    const isGrowth = type === "growth";
+/**
+ * 新接口按分块下发的单条巡园任务(pheno / abnormal / soil_root)→ 巡园卡片
+ * 字段直接来自 data.pheno / data.abnormal / data.soil_root;无对应分块则不展示该项
+ */
+const mapSectionToPatrol = (section = {}, type, ctx = {}) => {
+    const isAbnormal = type === "abnormal";
     const isSoil = type === "soil";
-    const abInfo = data.abnormal_interact_info || {};
-    const stressLevel = abInfo.stress_info?.level ?? abInfo.ab_it_info?.stress_info?.level;
-    const level =
-        task?.level ||
-        content.interact_level ||
-        (type === "abnormal" && stressLevel != null && stressLevel !== "" ? `${stressLevel}级` : "");
-    const zoneId = data.zone_id ?? payload.zone_id ?? selectedZoneId.value;
+    const isGrowth = type === "growth";
+    const title = isSoil
+        ? "土壤根系感知"
+        : isGrowth
+          ? t("agriFile.patrolGrowthTitle")
+          : t("agriFile.patrolAbnormalTitle");
+    const subject =
+        section.title ||
+        section.interact_title ||
+        section.interaction_title ||
+        section.pheno_name ||
+        "";
+    const issue =
+        section.highlight ||
+        section.interact_theme ||
+        section.interaction_theme ||
+        section.interaction_reason ||
+        section.interact_reason ||
+        "";
+    const level = section.level || (isSoil ? "一级" : "");
+    const urgencyText =
+        section.action_hint ||
+        (isSoil
+            ? "土壤含水率偏高 · 已持续 5 天"
+            : isGrowth
+              ? "最佳窗口待确认"
+              : "建议 24 小时内处理");
+    const urgencyTone = isSoil ? "warn" : isGrowth ? "success" : "danger";
+    const urgencyIcon = isSoil ? waterUrgencyIcon : isGrowth ? calendarUrgencyIcon : "";
+    const headerIcon = isSoil ? soilIcon : isGrowth ? periodIcon : lightningIcon;
+    const icon = resolveTaskImage(section.image_url || section.pic_url) || PATROL_ICON;
+    const zoneId = section.zone_id ?? ctx.zoneId ?? selectedZoneId.value;
     return {
-        id: task?.task_key || `${type}:${zoneId}`,
+        id: section.type_code ? `${section.type_code}:${zoneId}` : `${type}:${zoneId}`,
         type,
         theme: "blue",
-        level: isSoil ? level || "一级" : level,
-        level_num: task?.level_num,
-        title: isSoil
-            ? task?.title || "土壤根系感知"
-            : isGrowth
-              ? t("agriFile.patrolGrowthTitle")
-              : t("agriFile.patrolAbnormalTitle"),
-        subject: task?.title || content.interact_title || payload.interaction_title || "",
-        issue: isGrowth
-            ? content.interact_reason || task?.highlight || task?.question || ""
-            : task?.highlight || payload.interaction_theme || payload.interaction_reason || "",
+        level,
+        level_num: section.level_num,
+        title,
+        subject,
+        issue,
         desc: "",
-        urgencyText:
-            task?.action_hint ||
-            (isSoil
-                ? "土壤含水率偏高 · 已持续 5 天"
-                : isGrowth
-                  ? "最佳窗口 10/14 - 10/20"
-                  : "建议 24 小时内处理"),
-        urgencyTone: isSoil ? "warn" : isGrowth ? "success" : "danger",
-        urgencyIcon: isSoil ? waterUrgencyIcon : isGrowth ? calendarUrgencyIcon : "",
-        headerIcon: isSoil ? soilIcon : isGrowth ? periodIcon : lightningIcon,
-        icon: resolveTaskImage(task?.image_url) || PATROL_ICON,
-        zone: selectedZoneName.value || "",
+        urgencyText,
+        urgencyTone,
+        urgencyIcon,
+        headerIcon,
+        icon,
+        zone: ctx.zone || selectedZoneName.value || "",
         zoneId,
-        interactCode: payload.interact_code || content.interact_code || "",
-        fwCode: payload.fw_code || "",
-        taskKey: task?.task_key || "",
-        status: task?.status || payload.interact_status || "",
-        raw: task,
+        interactCode: section.interact_code || "",
+        fwCode: section.fw_code || "",
+        taskKey: section.type_code || type,
+        status: section.status || "",
+        raw: section,
     };
 };
 
-/** 一次接口返回 → 巡园卡片:物候 / 异常各取首条,土壤置末尾 */
-const buildPatrolList = (data) => {
-    const cards = normalizeTasks(data).map((task) => mapTaskToPatrol(task, data));
-    const growth = cards.find((item) => item.type === "growth");
-    const abnormal = cards.find((item) => item.type === "abnormal");
-    const soil = cards.find((item) => item.type === "soil");
-    const list = [growth || buildGrowthPatrolMock()];
-    if (abnormal) list.push(abnormal);
-    list.push(soil || buildSoilPatrolMock());
+/**
+ * 一次接口返回 → 巡园卡片:物候(pheno) → 异常(abnormal) → 土壤根系(soil_root)
+ * pheno / abnormal 有数据才展示对应卡片;soil_root 缺失时兜底写死占位
+ */
+const buildPatrolList = (data = {}) => {
+    const zoneId = data.zone_id ?? selectedZoneId.value;
+    const zone = selectedZoneName.value || "";
+    const ctx = { zoneId, zone, data };
+    const list = [];
+    if (data.pheno) list.push(mapSectionToPatrol(data.pheno, "growth", ctx));
+    if (data.abnormal) list.push(mapSectionToPatrol(data.abnormal, "abnormal", ctx));
+    // 土壤根系:接口字段为 soil_root(type_code=soil_root),缺失则不显示
+    if (data.soil_root) list.push(mapSectionToPatrol(data.soil_root, "soil", ctx));
     return list;
 };
 
@@ -421,14 +365,8 @@ const fetchTypesTask = async () => {
 
 const fetchCropArchive = async () => {
     const farm = getSelectedFarm();
-    const zoneId = selectedZoneId.value;
-    if (zoneId == null || zoneId === "") {
-        cropArchiveList.value = [];
-        return;
-    }
     const query = {
         farm_id: farm.farm_id ?? farm.id,
-        zone_id: zoneId,
         crop_code: farm.category_code,
     };
     try {
@@ -437,11 +375,22 @@ const fetchCropArchive = async () => {
             cropArchiveList.value = [];
             return;
         }
-        const list = Array.isArray(res.data) ? res.data : [];
-        const fallbackZone = selectedZoneName.value || t("agriFile.zoneOne");
-        cropArchiveList.value = list
-            .map((item, index) => mapPhenoRecordToArchive(item, index, fallbackZone))
-            .sort((a, b) => String(b.date).localeCompare(String(a.date)));
+        const data = res?.data || {};
+        const zones = Array.isArray(data.zones) ? data.zones : [];
+        // 新结构:data.zones[].records 为各分区的物候 / 异常记录
+        let cursor = 0;
+        const list = [];
+        for (const zone of zones) {
+            const records = Array.isArray(zone.records) ? zone.records : [];
+            const zoneName = zone.crop_variety || zone.zone_name || String(zone.zone_id ?? "");
+            for (const item of records) {
+                list.push(mapPhenoRecordToArchive(item, cursor, zoneName));
+                cursor += 1;
+            }
+        }
+        cropArchiveList.value = list.sort((a, b) =>
+            String(b.date).localeCompare(String(a.date))
+        );
     } catch (e) {
         console.warn("[patrolPanel] getPhenoRecord failed", e);
         cropArchiveList.value = [];
@@ -481,10 +430,16 @@ const goAddVariety = () => {
 const resolveCardZoneId = (item) => item?.zoneId ?? selectedZoneId.value ?? item?.id ?? "";
 
 const goGrowthTrack = (item) => {
-    const isAbnormal = item?.type !== "growth";
+    const type = item?.type;
     const zoneId = resolveCardZoneId(item);
+    const path =
+        type === "soil"
+            ? "/soil_root_track"
+            : type === "abnormal"
+              ? "/abnormal_track"
+              : "/growth_track";
     router.push({
-        path: isAbnormal ? "/abnormal_track" : "/growth_track",
+        path,
         query: {
             id: zoneId,
             ...(zoneId !== "" ? { zoneId } : {}),
@@ -494,19 +449,29 @@ const goGrowthTrack = (item) => {
 };
 
 const handleShare = (item) => {
-    const isAbnormal = item?.type === "abnormal";
+    const type = item?.type;
     const zoneId = resolveCardZoneId(item);
+    let targetUrl;
+    let pageName;
+    if (type === "soil") {
+        targetUrl = "soil_root_track";
+        pageName = t("agriFile.patrolSoilTitle");
+    } else if (type === "abnormal") {
+        targetUrl = "abnormal_track";
+        pageName = t("agriFile.patrolAbnormalTitle");
+    } else {
+        targetUrl = "growth_track";
+        pageName = t("agriFile.patrolGrowthTitle");
+    }
     const query = {
         askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
         shareText: item?.title || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
-        targetUrl: isAbnormal ? "abnormal_track" : "growth_track",
+        targetUrl,
         paramsPage: JSON.stringify({
             id: zoneId,
             fromShare: 1,
             ...(zoneId !== "" ? { zoneId } : {}),
-            pageName: isAbnormal
-                ? t("agriFile.patrolAbnormalTitle")
-                : t("agriFile.patrolGrowthTitle"),
+            pageName,
         }),
         imageUrl: "https://birdseye-img.sysuimars.com/farm-h5/field.png",
     };

+ 125 - 63
src/views/old_mini/agri_file/components/reportPanel.vue

@@ -5,9 +5,10 @@
             <div class="report-section__title">最新报告</div>
 
             <div
-                v-if="latestReport"
+                v-for="report in latestReports"
+                :key="report.id"
                 class="latest-card"
-                @click="handleReportClick(latestReport)"
+                @click="handleReportClick(report)"
             >
                 <div class="latest-card__header">
                     <div class="latest-card__header-left">
@@ -15,23 +16,23 @@
                             <el-icon><Search /></el-icon>
                         </div>
                         <div class="latest-card__type-meta">
-                            <div class="latest-card__type-name">{{ latestReport.title || "—" }}</div>
-                            <div class="latest-card__date">{{ latestReport.dateText }}</div>
+                            <div class="latest-card__type-name">{{ report.title || "—" }}</div>
+                            <div class="latest-card__date">{{ report.dateText }}</div>
                         </div>
                     </div>
-                    <span class="risk-badge">待完善</span>
+                    <span class="risk-badge">{{ report.riskLevel || "—" }}</span>
                 </div>
 
-                <div class="latest-card__subject">东区 · 待完善待完善</div>
+                <div class="latest-card__subject">{{ report.riskName || "—" }}</div>
 
                 <div class="latest-card__conclusion">
                     <div class="latest-card__conclusion-label">核心结论</div>
                     <div class="latest-card__conclusion-text">
-                        {{ latestReport.desc || "—" }}
+                        {{ report.desc || "—" }}
                     </div>
                 </div>
 
-                <div class="latest-card__evidence">
+                <!-- <div class="latest-card__evidence">
                     <div class="latest-card__avatars">
                         <img
                             v-for="(src, idx) in mockPhotoAvatars"
@@ -45,19 +46,21 @@
                     <div class="latest-card__evidence-text">
                         基于 <em>6</em> 张待完善待完善 · <em>4</em> 项待完善
                     </div>
-                </div>
+                </div> -->
 
                 <div class="latest-card__footer">
-                    <div class="latest-card__share" @click.stop="handleShare(latestReport)">
+                    <div class="latest-card__share" @click.stop="handleShare(report)">
                         <Icon size="14" color="rgba(255,255,255,0.75)" name="share" />
                         <span>转发</span>
                     </div>
-                    <div class="latest-card__detail" @click.stop="handleReportClick(latestReport)">
-                        查看完整报告 <el-icon><ArrowRightBold /></el-icon>
+                    <div class="latest-card__detail" @click.stop="handleReportClick(report)">
+                        查看完整报告 <el-icon><ArrowRight /></el-icon>
                     </div>
                 </div>
             </div>
 
+            <div v-if="!latestReports.length" class="report-empty">暂无最新报告</div>
+
             <!-- 报告生成中:暂写死 -->
             <div class="generating-card">
                 <div class="generating-card__spinner"></div>
@@ -75,56 +78,65 @@
             <div class="report-section__head">
                 <div class="report-section__title">历史报告</div>
                 <div class="report-section__meta">
-                    共 <span class="report-section__meta-count">{{ historyReports.length }}</span> 份 · 按时间排序
+                    共 <span class="report-section__meta-count">{{ historyCount }}</span> 份 · 按时间排序
                 </div>
             </div>
 
-            <div v-if="!historyReports.length" class="report-empty">暂无历史报告</div>
-
-            <div
-                v-for="monthGroup in historyMonthGroups"
-                :key="monthGroup.key"
-                class="history-month"
+            <List
+                v-model:loading="historyLoading"
+                :finished="historyFinished"
+                :finished-text="historyMonthGroups.length ? '没有更多了' : ''"
+                @load="loadHistoryPage"
             >
-                <div class="history-month__label">
-                    <span>{{ monthGroup.label }}</span>
-                    <i class="history-month__line"></i>
+                <div v-if="!historyMonthGroups.length && !historyLoading" class="report-empty">
+                    暂无历史报告
                 </div>
 
                 <div
-                    v-for="item in monthGroup.items"
-                    :key="item.id"
-                    class="history-card"
-                    @click="handleReportClick(item)"
+                    v-for="monthGroup in historyMonthGroups"
+                    :key="monthGroup.key"
+                    class="history-month"
                 >
-                    <div class="history-card-wrap">
-                        <img class="history-card__cover" :src="item.cover" alt="" />
-                        <div class="history-card__body">
-                            <div class="history-card__top">
-                                <div class="history-card__type">
-                                    <el-icon><WarningFilled /></el-icon>
-                                    <span>{{ resolveTypeLabel(item.type) }}</span>
+                    <div class="history-month__label">
+                        <span>{{ monthGroup.label }}</span>
+                        <i class="history-month__line"></i>
+                    </div>
+
+                    <div
+                        v-for="item in monthGroup.items"
+                        :key="item.id"
+                        class="history-card"
+                        @click="handleReportClick(item)"
+                    >
+                        <div class="history-card-wrap">
+                            <img class="history-card__cover" :src="item.cover" alt="" />
+                            <div class="history-card__body">
+                                <div class="history-card__top">
+                                    <div class="history-card__type">
+                                        <el-icon><WarningFilled /></el-icon>
+                                        <span>{{ resolveTypeLabel(item.type) }}</span>
+                                    </div>
+                                    <span class="risk-badge">{{ item.riskLevel || "—" }}</span>
+                                </div>
+                                <div class="history-card__title van-ellipsis">
+                                    {{ item.title || "—" }}
+                                </div>
+                                <div class="history-card__desc van-ellipsis">
+                                    {{ item.desc || "—" }}
+                                </div>
+                                <div class="history-card__line"></div>
+                                <div class="history-card__bottom">
+                                    <span class="history-card__time">
+                                        <el-icon><Clock /></el-icon>
+                                        {{ item.dateText }}
+                                    </span>
+                                    <el-icon class="history-card__arrow"><ArrowRight /></el-icon>
                                 </div>
-                                <span class="risk-badge">待完善</span>
-                            </div>
-                            <div class="history-card__title van-ellipsis">
-                                {{ item.title || "—" }}
-                            </div>
-                            <div class="history-card__desc van-ellipsis">
-                                {{ item.desc || "—" }}
-                            </div>
-                            <div class="history-card__line"></div>
-                            <div class="history-card__bottom">
-                                <span class="history-card__time">
-                                    <el-icon><Clock /></el-icon>
-                                    {{ item.dateText }}
-                                </span>
-                                <el-icon class="history-card__arrow"><ArrowRightBold /></el-icon>
                             </div>
                         </div>
                     </div>
                 </div>
-            </div>
+            </List>
         </div>
     </div>
 </template>
@@ -132,7 +144,7 @@
 <script setup>
 import { computed, onActivated, onMounted, ref } from "vue";
 import { useRouter } from "vue-router";
-import { Icon } from "vant";
+import { Icon, List } from "vant";
 import { ArrowRight, Clock, Search, WarningFilled } from "@element-plus/icons-vue";
 import { base_img_url2 } from "@/api/config";
 import wx from "weixin-js-sdk";
@@ -151,15 +163,20 @@ const TYPE_LABEL_MAP = {
     patrol: "巡园诊断报告",
 };
 
-const reportList = ref([]);
+// ---------- 最新报告(today_reports,可多份) ----------
+const latestReports = ref([]);
 
-const latestReport = computed(() => reportList.value[0] || null);
-const historyReports = computed(() => reportList.value.slice(1));
+// ---------- 历史报告(滚动分页) ----------
+const historyList = ref([]);
+const historyLoading = ref(false);
+const historyFinished = ref(false);
+const historyPage = ref(0);
+const historyCount = ref(0);
 
 /** 历史按月分组,key 用 YYYY-MM */
 const historyMonthGroups = computed(() => {
     const map = new Map();
-    historyReports.value.forEach((item) => {
+    historyList.value.forEach((item) => {
         const key = item.monthKey || "unknown";
         if (!map.has(key)) {
             map.set(key, {
@@ -223,7 +240,7 @@ const parseReportTime = (raw) => {
 };
 
 const mapReportItem = (item, index) => {
-    const timeInfo = parseReportTime(item.time);
+    const timeInfo = parseReportTime(item.time || item.date);
     return {
         id: item.id ?? `${item.time || ""}-${index}`,
         title: item.title || "",
@@ -234,6 +251,10 @@ const mapReportItem = (item, index) => {
         monthKey: timeInfo.monthKey,
         monthLabel: timeInfo.monthLabel,
         cover: resolveCoverUrl(item.cover_url),
+        phenoName: item.phenophase_name || "",
+        riskName: item.risk_name || "",
+        riskLevel: item.risk_level || "",
+        riskLevelNum: item.risk_level_num || "",
     };
 };
 
@@ -264,28 +285,65 @@ const handleShare = (item) => {
     });
 };
 
-const fetchReportList = async () => {
+/**
+ * 加载一页历史报告;首屏(page=1)同时写入「最新报告」。
+ * 由 van-list 的 @load 在滚动到底部时持续触发。
+ */
+const loadHistoryPage = async () => {
     const farm = getSelectedFarm();
     const farmId = farm.farm_id ?? farm.id;
     const zoneId = farm.zone_id ?? farm.zoneId;
     if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
-        reportList.value = [];
+        historyFinished.value = true;
+        historyLoading.value = false;
         return;
     }
+    const nextPage = historyPage.value + 1;
+    historyLoading.value = true;
     try {
         const res = await VE_API.questionnaire.getReportList({
             farm_id: farmId,
             zone_id: zoneId,
+            page: nextPage,
+            page_size: 10,
         });
-        const reports = Array.isArray(res?.data?.reports) ? res.data.reports : [];
-        reportList.value = reports.map(mapReportItem);
-    } catch {
-        reportList.value = [];
+        const data = res?.data || {};
+        if (nextPage === 1) {
+            // 最新报告取 today_reports(当日可有多份)
+            latestReports.value = Array.isArray(data.today_reports)
+                ? data.today_reports.map((item, i) => mapReportItem(item, i))
+                : [];
+            historyCount.value = data.history_count ?? 0;
+        }
+        const flat = (Array.isArray(data.history) ? data.history : [])
+            .flatMap((group) => (Array.isArray(group.reports) ? group.reports : []));
+        const start = historyList.value.length;
+        const mapped = flat.map((item, idx) => mapReportItem(item, start + idx));
+        historyList.value = historyList.value.concat(mapped);
+        historyPage.value = nextPage;
+        historyFinished.value =
+            !data.has_more || (data.total_pages && nextPage >= data.total_pages);
+    } catch (e) {
+        // 出错即停止,避免 van-list 死循环重试
+        historyFinished.value = true;
+    } finally {
+        historyLoading.value = false;
     }
 };
 
-onMounted(fetchReportList);
-onActivated(fetchReportList);
+/** 进入 / 再次激活:重置分页并由 van-list 触发首屏加载 */
+const resetAndLoad = () => {
+    historyList.value = [];
+    historyPage.value = 0;
+    historyFinished.value = false;
+    historyLoading.value = true; // 先占位,防止 van-list 重复触发首屏
+    latestReports.value = [];
+    historyCount.value = 0;
+    loadHistoryPage();
+};
+
+onMounted(resetAndLoad);
+onActivated(resetAndLoad);
 </script>
 
 <style lang="scss" scoped>
@@ -356,6 +414,10 @@ onActivated(fetchReportList);
         background: radial-gradient(80% 80% at 90% 0%, rgba(255, 212, 137, 0.12) 0%, rgba(255, 212, 137, 0) 100%);
         box-sizing: border-box;
 
+        & + & {
+            margin-top: 12px;
+        }
+
         &__header {
             display: flex;
             align-items: flex-start;

+ 1122 - 0
src/views/old_mini/agri_file/pages/soilRootTrack.vue

@@ -0,0 +1,1122 @@
+<template>
+    <div class="soil-root-track-page">
+        <custom-header
+            :name="pageTitle"
+            :isGoBack="!isFromShare"
+            :isClose="isFromShare"
+            :showClose="false"
+            @goback="handleBack"
+        />
+        <div class="soil-root-track-body" v-loading="loading" element-loading-background="rgba(15, 15, 15, 0.55)">
+            <div class="soil-root-hero">
+                <div class="soil-root-hero__share" @click="handleShare">
+                    <Icon name="share" color="rgba(255,255,255,0.85)" />
+                    <span>{{ t("agriFile.forward") }}</span>
+                </div>
+                <div class="soil-root-mask"></div>
+                <div class="soil-root-top">
+                    <div class="soil-root-hero__tags">
+                        <span class="soil-root-hero__tag soil-root-hero__tag--level">
+                            <el-icon><WarningFilled /></el-icon>
+                            {{ level }}
+                        </span>
+                        <span class="soil-root-hero__tag soil-root-hero__tag--zone">{{ zoneName }}</span>
+                    </div>
+                    <div class="soil-root-hero__title">{{ theme }}</div>
+                    <div class="soil-root-hero__reason">{{ reason }}</div>
+                </div>
+            </div>
+
+            <div class="soil-root-main">
+                <!-- 农情研判 -->
+                <div class="box-wrap stage-card analysis-card">
+                    <div class="work-info">
+                        <div class="section-title">
+                            <span class="title-bar"></span>
+                            {{ t("agriFile.agriAssessment") }}
+                        </div>
+                        <div class="analysis-card__desc">{{ assessmentDesc }}</div>
+                        <div class="analysis-card__stats">
+                            <div v-for="(item, index) in analysisStats" :key="index" class="analysis-card__stat">
+                                <div class="analysis-card__stat-value" :class="{ highlight: item.highlight }">
+                                    <template v-if="item.unit">
+                                        <span class="num">{{ item.value }}</span>
+                                        <span class="unit">{{ item.unit }}</span>
+                                    </template>
+                                    <template v-else>{{ item.value }}</template>
+                                </div>
+                                <div class="analysis-card__stat-label">{{ item.label }}</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 根系排查互动 -->
+                <!-- <div class="interact-section-head">
+                    <span class="interact-section-head__text"> 根系排查互动 · 共 {{ interactList.length }} 项 </span>
+                    <span class="interact-section-head__line"></span>
+                </div> -->
+
+                <div v-for="(item, index) in interactList" :key="item.fw_code || index" class="soil-root-item">
+                    <div class="soil-root-item__wrap">
+                        <div class="soil-root-item__title">
+                            <img class="soil-root-item__icon" src="@/assets/img/agricultural/gaojin.png" alt="" />
+                            <span>{{ item.abnormal_type || t("agriFile.rootAbnormal") }}</span>
+                        </div>
+
+                        <div class="stage-card interact-card">
+                            <div class="work-info">
+                                <div class="interact-card__head">
+                                    <div class="section-title interact-card__title">
+                                        <span class="title-bar"></span>
+                                        {{ t("agriFile.interactQuestion") }}
+                                    </div>
+                                    <div
+                                        class="interact-card__submit"
+                                        :class="{ disabled: selectedMap[item.fw_code || index] == null }"
+                                        @click="handleSubmit(item, index)"
+                                    >
+                                        确认提交
+                                    </div>
+                                </div>
+                                <div class="interact-card__qa">
+                                    <div class="interact-card__question">{{ item.interact_problem }}</div>
+                                    <div class="interact-card__options">
+                                        <div
+                                            v-for="(opt, optIndex) in item.options"
+                                            :key="`${item.fw_code || index}-${optIndex}`"
+                                            class="interact-option"
+                                            :class="{
+                                                selected: selectedMap[item.fw_code || index] === optIndex,
+                                            }"
+                                            @click="selectedMap[item.fw_code || index] = optIndex"
+                                        >
+                                            <span class="interact-option__text">{{ opt }}</span>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <!-- 巡园向导 -->
+                        <div class="guide-card">
+                            <div class="work-info">
+                                <div class="section-title">
+                                    <img class="guide-icon" src="@/assets/img/farm/guide.png" alt="" />
+                                    {{ t("agriFile.patrolGuide") }}
+                                </div>
+                                <div class="guide-card__img-box">
+                                    <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 class="guide-card__upload-wrap">
+                                <div class="guide-card__upload" @click="handleUploadClick">
+                                    <img class="guide-card__upload-icon" src="@/assets/img/common/camera.png" alt="" />
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 土壤条件判断 -->
+                <!-- <div class="interact-section-head">
+                    <span class="interact-section-head__text"> 土壤条件判断 · 共 {{ soilSections.length }} 项 </span>
+                    <span class="interact-section-head__line"></span>
+                </div> -->
+
+                <!-- <div v-for="section in soilSections" :key="section.key" class="soil-section-card">
+                    <div class="soil-section-card__wrap">
+                        <div class="soil-section-card__title">
+                            <img class="soil-section-card__icon" src="@/assets/img/agricultural/gaojin.png" alt="" />
+                            <span>{{ section.title }}</span>
+                        </div>
+                        <div class="soil-section-card__question">{{ section.question }}</div>
+                        <div class="soil-section-card__options">
+                            <div
+                                v-for="opt in section.options"
+                                :key="`${section.key}-${opt.value}`"
+                                class="interact-option"
+                                :class="{ selected: isSectionSelected(section, opt.value) }"
+                                @click="toggleSectionOption(section, opt.value)"
+                            >
+                                <span class="interact-option__text">{{ opt.label }}</span>
+                            </div>
+                        </div>
+                        <div v-if="section.guide && section.guide.desc" class="soil-section-card__guide">
+                            <div class="section-title">
+                                <img class="guide-icon" src="@/assets/img/farm/guide.png" alt="" />
+                                {{ t("agriFile.patrolGuide") }}
+                            </div>
+                            <div class="soil-section-card__guide-desc">{{ section.guide.desc }}</div>
+                        </div>
+                    </div>
+                </div> -->
+
+                <div v-for="(section, index) in soilSections" :key="index" class="soil-root-item">
+                    <div class="soil-root-item__wrap">
+                        <div class="soil-root-item__title">
+                            <img class="soil-root-item__icon" src="@/assets/img/agricultural/gaojin.png" alt="" />
+                            <span>{{ section.title || t("agriFile.rootAbnormal") }}</span>
+                        </div>
+
+                        <div class="stage-card interact-card">
+                            <div class="work-info">
+                                <div class="interact-card__head">
+                                    <div class="section-title interact-card__title">
+                                        <span class="title-bar"></span>
+                                        {{ t("agriFile.interactQuestion") }}
+                                    </div>
+                                    <div
+                                        class="interact-card__submit"
+                                        @click="handleSubmit(section, index)"
+                                        >
+                                        <!-- :class="{ disabled: selectedMap[item.fw_code || index] == null }" -->
+                                        确认提交
+                                    </div>
+                                </div>
+                                <div class="interact-card__qa">
+                                    <div class="interact-card__question">{{ section.question }}</div>
+                                    <div class="interact-card__options soil-section-card__options">
+                                        <div
+                                            v-for="opt in section.options"
+                                            :key="`${section.key}-${opt.value}`"
+                                            class="interact-option"
+                                            :class="{ selected: isSectionSelected(section, opt.value) }"
+                                            @click="toggleSectionOption(section, opt.value)"
+                                        >
+                                            <span class="interact-option__text">{{ opt.label }}</span>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <!-- 巡园向导 -->
+                        <div class="guide-card">
+                            <div class="work-info">
+                                <div class="section-title">
+                                    <img class="guide-icon" src="@/assets/img/farm/guide.png" alt="" />
+                                    {{ t("agriFile.patrolGuide") }}
+                                </div>
+                            <div class="soil-section-card__guide-desc">{{ section.guide.desc }}</div>
+                                <div class="guide-card__img-box" v-if="section.image_url">
+                                    <photo-provider :photo-closable="true">
+                                        <photo-consumer :src="base_img_url3+section.image_url">
+                                            <img :src="base_img_url3 + section.image_url" alt="" />
+                                        </photo-consumer>
+                                    </photo-provider>
+                                </div>
+                            </div>
+
+                            <div class="guide-card__upload-wrap">
+                                <div class="guide-card__upload" @click="handleUploadClick">
+                                    <img class="guide-card__upload-icon" src="@/assets/img/common/camera.png" alt="" />
+                                </div>
+                            </div>
+                        </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, base_img_url3 } 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 analysisStats = [
+    { value: "126", unit: "mm", label: "累计降雨", highlight: false },
+    { value: "92", unit: "%", label: "土壤含水率", highlight: false },
+    { value: "52", unit: "h", label: "根部积水", highlight: true },
+];
+
+// ---------- 页面状态 ----------
+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([]); // root 根系排查互动项(取 data.root)
+const soilSections = ref([]); // 土壤条件判断(质地/酸碱/墒情/氮磷钾/持水)
+const sectionSelected = reactive({}); // section.key → 选中值(多选为数组)
+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.patrolSoilTitle"));
+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.soilRootTheme"));
+/** interaction_title */
+const reason = computed(() => firstInteract.value?.interaction_theme || t("agriFile.soilRootReason"));
+/** 农情研判说明 */
+const assessmentDesc = computed(() => firstInteract.value?.interaction_reason || t("agriFile.soilRootAssessment"));
+const guideImage = computed(() => resolveGuideImage(firstInteract.value?.pic_url));
+/** 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 formatLocalDate = (date) => {
+    const y = date.getFullYear();
+    const m = String(date.getMonth() + 1).padStart(2, "0");
+    const d = String(date.getDate()).padStart(2, "0");
+    return `${y}-${m}-${d}`;
+};
+
+// ---------- 业务逻辑 ----------
+// 土壤根系感知:GET VE_API.pheno.getSoilTasks({ zone_id }),返回 data.root(根系排查互动)
+// 与 data.sections(土壤条件判断:质地/酸碱/墒情/氮磷钾/持水)
+const fetchSoilRootInteract = async () => {
+    const farm = getSelectedFarm();
+    const zId = zoneId.value || farm.zone_id || farm.zoneId;
+    if (zId == null || zId === "") {
+        interactList.value = [];
+        stressInfo.value = {};
+        soilSections.value = [];
+        return;
+    }
+    // 提交上下文:confirmAbnormalInteract 需完整参数,取本地农场信息补全
+    queryContext.value = {
+        farm_id: farm.farm_id ?? farm.id ?? "",
+        zone_id: zId,
+        calc_date: formatLocalDate(new Date()),
+        crop_type: resolveCropType(farm),
+    };
+    loading.value = true;
+    try {
+        const res = await VE_API.pheno.getSoilTasks({ zone_id: zId });
+        if (res?.code !== 200) {
+            interactList.value = [];
+            stressInfo.value = {};
+            soilSections.value = [];
+            return;
+        }
+        const data = res?.data || {};
+        const root = data.root || {};
+        interactList.value = Object.keys(root).length ? [mapInteractItem(root)] : [];
+        // hero 等级取 root.level_num(如 2),level 计算属性会拼成「2级」
+        stressInfo.value = { level: root.level_num ?? null };
+        soilSections.value = Array.isArray(data.sections) ? data.sections : [];
+    } catch (e) {
+        console.warn("[soilRootTrack] getSoilTasks failed", e);
+        interactList.value = [];
+        stressInfo.value = {};
+        soilSections.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("[soilRootTrack] checkZoneRange failed", e);
+        needDraw.value = true;
+    }
+};
+
+// ---------- 事件处理 / 生命周期 ----------
+const handleShare = () => {
+    const query = {
+        askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
+        shareText: theme.value || "连阴雨泡根,根系发黑缺氧,赶紧排水护根!",
+        targetUrl: "soil_root_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/farm-h5/field.png",
+    };
+    wx.miniProgram.navigateTo({
+        url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
+    });
+};
+
+/** 需勾画则先画范围;否则直接上传(同 abnormalTrack) */
+const handleUploadClick = () => {
+    if (needDraw.value) {
+        router.push({
+            path: "/add_zone",
+            query: {
+                type: "farm",
+                targetUrl: "/soil_root_track",
+                ...(zoneId.value ? { trackId: zoneId.value } : {}),
+            },
+        });
+        return;
+    }
+    showUploadPopup.value = true;
+};
+
+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"));
+            await fetchSoilRootInteract();
+            return;
+        }
+        ElMessage.error(res?.msg || "提交失败,请稍后再试");
+    } catch (e) {
+        console.warn("[soilRootTrack] confirmAbnormalInteract failed", e);
+        ElMessage.error("提交失败,请稍后再试");
+    } finally {
+        submitting.value = false;
+    }
+};
+
+const handleBack = () => {
+    router.back();
+};
+
+// 土壤条件判断:单选 / 多选选项切换
+const isSectionSelected = (section, value) => {
+    const sel = sectionSelected[section.key];
+    if (section.multiple) return Array.isArray(sel) && sel.includes(value);
+    return sel === value;
+};
+
+const toggleSectionOption = (section, value) => {
+    if (section.multiple) {
+        const cur = Array.isArray(sectionSelected[section.key])
+            ? sectionSelected[section.key]
+            : [];
+        sectionSelected[section.key] = cur.includes(value)
+            ? cur.filter((v) => v !== value)
+            : [...cur, value];
+    } else {
+        sectionSelected[section.key] =
+            sectionSelected[section.key] === value ? null : value;
+    }
+};
+
+onActivated(() => {
+    fetchZoneRangeStatus();
+    fetchSoilRootInteract();
+    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>
+.soil-root-track-page {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    background: #0b0b0b;
+    color: #fff;
+
+    .soil-root-track-body {
+        flex: 1;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        padding-bottom: 10px;
+        box-sizing: border-box;
+    }
+
+    .soil-root-hero {
+        position: relative;
+        padding: 106px 70px 16px 12px;
+        box-sizing: border-box;
+        z-index: 2;
+        // min-height: 240px;
+        background: linear-gradient(
+                180deg,
+                rgba(11, 11, 11, 0) 8.51%,
+                rgba(11, 11, 11, 0.416) 42.95%,
+                rgba(11, 11, 11, 0.468) 56.49%,
+                rgba(11, 11, 11, 0.52) 80.47%
+            ),
+            url("@/assets/img/agricultural/soil-detail-bg.png") right bottom / cover no-repeat;
+
+        .soil-root-mask {
+            position: absolute;
+            bottom: 0;
+            left: 0;
+            width: 100%;
+            height: 107px;
+            background: linear-gradient(
+                180deg,
+                rgba(11, 11, 11, 0) 8.47%,
+                rgba(11, 11, 11, 0.8) 26.93%,
+                rgba(11, 11, 11, 0.9) 37.74%,
+                #0b0b0b 56.89%
+            );
+        }
+
+        .soil-root-top {
+            position: relative;
+            z-index: 2;
+            // padding-bottom: 40px;
+        }
+
+        &__share {
+            position: absolute;
+            top: 16px;
+            right: 16px;
+            z-index: 2;
+            display: inline-flex;
+            align-items: center;
+            gap: 4px;
+            height: 28px;
+            padding: 0 12px;
+            border-radius: 14px;
+            background: rgba(0, 0, 0, 0.45);
+            border: 1px solid rgba(255, 255, 255, 0.12);
+            color: rgba(255, 255, 255, 0.85);
+            font-size: 12px;
+        }
+
+        &__tags {
+            display: flex;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 8px;
+            margin-bottom: 12px;
+            padding-right: 88px;
+        }
+
+        &__tag {
+            display: inline-flex;
+            align-items: center;
+            gap: 3px;
+            height: 20px;
+            padding: 0 7px;
+            border-radius: 24px;
+            font-size: 12px;
+            line-height: 20px;
+            box-sizing: border-box;
+            border: 1px solid rgba(138, 106, 46, 1);
+        }
+
+        &__tag--level {
+            background: linear-gradient(90deg, #7e2321 0%, #350302 100%);
+            color: #ff7d7b;
+            border-color: rgba(201, 53, 50, 0.41);
+        }
+
+        &__tag--zone {
+            color: #e8c27a;
+            background: #2a2418;
+        }
+
+        &__title {
+            font-family: "PangMenZhengDao", sans-serif;
+            font-size: 26px;
+            line-height: 34px;
+            background: linear-gradient(180deg, #ffffff 0%, #fecd76 100%);
+            -webkit-background-clip: text;
+            background-clip: text;
+            color: transparent;
+            margin-bottom: 6px;
+        }
+
+        &__reason {
+            font-size: 12px;
+            line-height: 22px;
+            padding: 4px;
+            border-radius: 4px;
+            background: linear-gradient(90deg, rgba(255, 212, 137, 0.1) 0%, rgba(255, 212, 137, 0) 100%);
+            color: rgba(255, 255, 255, 1);
+        }
+    }
+
+    .soil-root-main {
+        position: relative;
+        z-index: 3;
+        // margin-top: -38px;
+        padding: 0 12px 12px;
+        box-sizing: border-box;
+    }
+
+    .box-wrap {
+        padding: 1px;
+        border-radius: 12px;
+        background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 72.15%);
+
+        .work-info {
+            background: linear-gradient(180deg, #1a191d 0%, #121216 100%);
+            border-radius: 12px;
+            padding: 12px 13px;
+            box-sizing: border-box;
+        }
+    }
+
+    .stage-card + .stage-card {
+        margin-top: 12px;
+    }
+
+    .section-title {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        font-size: 16px;
+        font-weight: 500;
+        color: #f2e9d8;
+        margin-bottom: 10px;
+        &.interact-card__title {
+            font-size: 14px;
+        }
+
+        .title-bar {
+            width: 4px;
+            height: 16px;
+            border-radius: 2px;
+            background: #ffd489;
+            flex-shrink: 0;
+        }
+    }
+
+    .analysis-card {
+        .analysis-card__desc {
+            font-size: 14px;
+            line-height: 22px;
+            color: rgba(255, 255, 255, 0.6);
+            margin-bottom: 10px;
+        }
+
+        .analysis-card__stats {
+            display: flex;
+            align-items: stretch;
+            background: rgba(255, 255, 255, 0.05);
+            border-radius: 10px;
+            padding: 8px 0 6px;
+        }
+
+        .analysis-card__stat {
+            flex: 1;
+            min-width: 0;
+            text-align: center;
+            position: relative;
+
+            & + .analysis-card__stat::before {
+                content: "";
+                position: absolute;
+                left: 0;
+                top: 50%;
+                transform: translateY(-50%);
+                width: 1px;
+                height: 32px;
+                background: rgba(255, 255, 255, 0.06);
+            }
+        }
+
+        .analysis-card__stat-value {
+            font-size: 22px;
+            line-height: 27px;
+            color: #fff;
+            margin-bottom: 2px;
+            height: 27px;
+
+            &.highlight {
+                color: #ff9f5a;
+            }
+
+            .num {
+                font-size: 22px;
+                font-weight: 600;
+            }
+
+            .unit {
+                color: rgba(255, 255, 255, 0.5);
+                margin-left: 1px;
+                font-size: 12px;
+            }
+        }
+
+        .analysis-card__stat-label {
+            font-size: 12px;
+            line-height: 18px;
+            color: rgba(255, 255, 255, 0.5);
+        }
+    }
+
+    .interact-section-head {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        margin: 16px 0 12px;
+
+        &__text {
+            flex-shrink: 0;
+            font-size: 12px;
+            line-height: 18px;
+            color: #727272;
+        }
+
+        &__line {
+            flex: 1;
+            height: 1px;
+            background: rgba(255, 255, 255, 0.08);
+        }
+    }
+
+    .soil-root-item {
+        margin-top: 12px;
+        padding: 1px;
+        border-radius: 12px;
+        background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 25%),
+            linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 25%);
+
+        &__wrap {
+            padding: 11px 13px;
+            border-radius: 12px;
+            background: linear-gradient(180deg, #1A191D 0%, #121216 100%);
+        }
+
+        & + .soil-root-item {
+            margin-top: 16px;
+        }
+
+        &__title {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            margin-bottom: 12px;
+            font-size: 16px;
+            font-weight: 500;
+            color: #FFD489;
+        }
+
+        &__icon {
+            width: 18px;
+            height: 23px;
+            object-fit: contain;
+        }
+    }
+
+    .soil-section-card {
+        padding: 1px;
+        margin-top: 12px;
+        border-radius: 12px;
+        background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 25%),
+            linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 25%);
+
+        &__wrap {
+            padding: 11px 13px;
+            border-radius: 12px;
+            background: linear-gradient(180deg, #1A191D 0%, #121216 100%);
+        }
+
+        &__title {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            margin-bottom: 12px;
+            font-size: 16px;
+            font-weight: 500;
+            color: #FFD489;
+        }
+
+        &__icon {
+            width: 18px;
+            height: 23px;
+            object-fit: contain;
+        }
+
+        &__question {
+            font-size: 16px;
+            font-family: "PangMenZhengDao";
+            line-height: 20px;
+            color: #FFD489;
+            margin-bottom: 12px;
+        }
+
+        &__options {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 12px;
+            &.soil-section-card__options {
+                display: grid;
+                grid-template-columns: repeat(3, 1fr);
+            }
+        }
+
+        &__guide {
+            margin-top: 12px;
+            background: rgba(75, 74, 70, 0.2);
+            border-radius: 10px;
+            padding: 10px;
+
+            .section-title {
+                gap: 6px;
+                font-size: 14px;
+                color: rgba(255, 255, 255, 0.6);
+            }
+
+            .guide-icon {
+                width: 14px;
+                height: 14px;
+                object-fit: contain;
+                filter: grayscale(100%);
+                -webkit-filter: grayscale(100%);
+            }
+
+            &-desc {
+                margin: 8px 0;
+                font-size: 13px;
+                line-height: 20px;
+                color: rgba(255, 255, 255, 0.55);
+            }
+        }
+    }
+
+    .interact-card {
+        padding: 10px 12px;
+        border-radius: 12px;
+        border: 1px solid rgba(255, 241, 217, 0.2);
+        background: linear-gradient(180deg, #1A191D 0%, #121216 100%);
+
+        &__head {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 8px;
+            margin-bottom: 12px;
+
+            .section-title {
+                margin-bottom: 0;
+            }
+        }
+
+        &__submit {
+            flex-shrink: 0;
+            height: 27px;
+            padding: 0 15px;
+            border-radius: 24px;
+            background: rgba(255, 212, 137, 0.1);
+            border: 1px solid rgba(255, 212, 137, 0.4);
+            color: #FFD489;
+            font-size: 12px;
+            line-height: 27px;
+            text-align: center;
+            box-sizing: border-box;
+
+            &.disabled {
+                opacity: 0.35;
+            }
+        }
+
+        &__question {
+            font-size: 16px;
+            font-family: "PangMenZhengDao";
+            line-height: 20px;
+            color: #FFD489;
+            margin-bottom: 12px;
+        }
+
+        &__options {
+            display: flex;
+            gap: 12px;
+            flex-wrap: wrap;
+        }
+
+        .interact-option {
+            position: relative;
+            flex: auto;
+            min-width: 0;
+            min-height: 41px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 8px;
+            background: rgba(255, 255, 255, 0.05);
+            border: 1px solid rgba(255, 255, 255, 0.12);
+            box-sizing: border-box;
+            cursor: pointer;
+
+            &__text {
+                display: block;
+                padding: 0 6px;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                font-size: 14px;
+                color: rgba(255, 255, 255, 0.7);
+            }
+
+            &.selected {
+                border-color: rgba(255, 255, 255, 0.5);
+                background: rgba(255, 255, 255, 0.1);
+
+                .interact-option__text {
+                    color: #fff;
+                }
+            }
+        }
+    }
+
+    .guide-card {
+        position: relative;
+        margin-top: 12px;
+        background: rgba(75, 74, 70, 0.2);
+        border-radius: 10px;
+        padding: 10px;
+
+        &__img {
+            display: block;
+            width: 100%;
+            border-radius: 8px;
+        }
+        .guide-card__img-box {
+            img {
+                display: block;
+                width: 100%;
+            }
+        }
+
+        .section-title {
+            gap: 6px;
+            font-size: 14px;
+            color: rgba(255, 255, 255, 0.6);
+        }
+
+        .guide-icon {
+            width: 14px;
+            height: 14px;
+            object-fit: contain;
+            filter: grayscale(100%);
+            -webkit-filter: grayscale(100%);
+        }
+    }
+
+    .guide-card__upload-wrap {
+        position: absolute;
+        right: 25px;
+        bottom: 27px;
+        z-index: 10;
+    }
+
+    .guide-card__upload {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 52px;
+        height: 52px;
+        border-radius: 50%;
+        background: linear-gradient(90deg, #ffebc9 0%, #ffca6d 100%);
+    }
+
+    .guide-card__upload-icon {
+        width: 18px;
+        height: 16px;
+        object-fit: contain;
+    }
+}
+
+.bottom-btn-container {
+    display: flex;
+    justify-content: space-between;
+    gap: 12px;
+    width: 100%;
+
+    .bottom-btn {
+        padding: 0 25px;
+        border-radius: 8px;
+        font-size: 16px;
+        height: 40px;
+        line-height: 40px;
+        box-sizing: border-box;
+        font-weight: 500;
+        text-align: center;
+
+        &.primary-btn {
+            flex: 1;
+            background: linear-gradient(90deg, #ffebc9 0%, #ffca6d 100%);
+            color: #1a1408;
+        }
+    }
+}
+</style>

+ 48 - 3
src/views/old_mini/work_execute/index.js

@@ -63,6 +63,33 @@ function createPillLabel(text) {
   return canvas.toDataURL();
 }
 
+/** 分区数量角标:金底深字小圆点,叠加在点位右上角 */
+function createCountBadge(count) {
+  const canvas = document.createElement("canvas");
+  const ctx = canvas.getContext("2d");
+  const radius = 11;
+  const pad = 2;
+  const size = (radius + pad) * 2;
+  canvas.width = size * LABEL_DPR;
+  canvas.height = size * LABEL_DPR;
+  ctx.scale(LABEL_DPR, LABEL_DPR);
+  ctx.imageSmoothingEnabled = true;
+  ctx.imageSmoothingQuality = "high";
+
+  ctx.beginPath();
+  ctx.arc(radius + pad, radius + pad, radius, 0, Math.PI * 2);
+  ctx.fillStyle = "#FFD489";
+  ctx.fill();
+
+  ctx.fillStyle = "#0A0A0A";
+  ctx.font = "14px sans-serif";
+  ctx.textAlign = "center";
+  ctx.textBaseline = "middle";
+  ctx.fillText(String(count), radius + pad, radius + pad + 0.5);
+
+  return canvas.toDataURL();
+}
+
 /**
  * @description 地图层对象
  */
@@ -73,6 +100,7 @@ class IndexMap {
     this.vectorStyle = vectorStyle;
     this.onlyName = false;
     this.labelStyleCache = {};
+    this.countStyleCache = {};
 
     this.borderStyle = new Style({
       image: new Icon({
@@ -96,12 +124,28 @@ class IndexMap {
               src: createPillLabel(labelText),
               scale: 1 / LABEL_DPR,
               anchor: [0.5, 1],
-              displacement: [0, 32],
+              displacement: [0, 36],
             }),
           });
         }
-
-        return [this.borderStyle, this.labelStyleCache[labelText]];
+        const styles = [this.borderStyle, this.labelStyleCache[labelText]];
+        // 分区数量角标:叠加在点位右上角
+        const count = feature.get("count");
+        if (count != null && count !== "" && Number(count) > 0) {
+          const badgeKey = `count_${count}`;
+          if (!this.countStyleCache[badgeKey]) {
+            this.countStyleCache[badgeKey] = new Style({
+              image: new Icon({
+                src: createCountBadge(count),
+                scale: 1 / LABEL_DPR,
+                anchor: [0.5, 0.5],
+                displacement: [24, 22],
+              }),
+            });
+          }
+          styles.push(this.countStyleCache[badgeKey]);
+        }
+        return styles;
       },
     });
   }
@@ -129,6 +173,7 @@ class IndexMap {
 
   initData(taskList, label, pointType = "point", onlyNameVal = false) {
     this.labelStyleCache = {};
+    this.countStyleCache = {};
     this.gardenPointLayer.source.clear();
     this.onlyName = onlyNameVal;
     if (taskList.length > 0) {

+ 61 - 40
src/views/old_mini/work_execute/index.vue

@@ -1,5 +1,6 @@
 <template>
     <div class="task-page" v-loading="pageLoading" element-loading-background="rgba(15, 15, 15, 0.55)"
+        element-loading-text="加载中..."
         :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
         <farm-header class="task-page__header" />
         <div class="task-top">
@@ -57,7 +58,7 @@
                             <span v-if="item.isNormal" class="badge-tag normal-badge">标准类</span>
                             <span v-if="item.isAbnormal" class="badge-tag abnormal-badge">异常类</span>
                             <span v-if="item.isWeather" class="badge-tag weather-badge">气象类</span>
-                            <span v-if="item.status === 'pending' && item.cornerTip" class="badge-tag status-badge">
+                            <span v-if="item.status === 'pending' && item.cornerTip" class="badge-tag status-badge" :class="'status-'+item.status">
                                 {{ item.cornerTip }}
                             </span>
                             <span v-if="item.status === 'completed' && item.cornerTip"
@@ -73,7 +74,7 @@
                             </div>
                             <div class="info-row">
                                 <span class="info-row__label">执行区域</span>
-                                <span class="info-row__value">{{ item.areaCode || "--" }}</span>
+                                <span class="info-row__value">{{ activeZoneName || "--" }}</span>
                             </div>
                             <div v-if="item.fw_trigger" class="info-row">
                                 <span class="info-row__label">触发条件</span>
@@ -229,26 +230,19 @@ const displayTaskList = computed(() => {
     if (!sortByUrgency.value) return list;
     return [...list].sort((a, b) => Number(!!b.isHighRisk) - Number(!!a.isHighRisk));
 });
-
-/** 日历窗口:今天起连续 7 天 */
-function getCalendarDateRange() {
-    const start = new Date();
-    return Array.from({ length: 7 }, (_, i) => {
-        const d = new Date(start);
-        d.setDate(start.getDate() + i);
-        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}`;
-    });
-}
+/** 当前选中的分区名称(用于卡片「执行区域」展示) */
+const activeZoneName = computed(() => {
+    const id = activeZoneId.value;
+    if (id == null || id === "") return "";
+    const zone = zoneList.value.find((z) => String(z.id) === String(id));
+    return zone?.name || "";
+});
 
 /** zone_id 优先用分区筛选;否则退回农场缓存分区 */
-const resolveQueryParams = (triggerDate = getTodayDate()) => {
+const resolveQueryParams = () => {
     const params = {
         zone_id: activeZoneId.value ?? getSelectedZoneId(),
         farm_id: getSelectedFarmId(),
-        trigger_date: triggerDate,
     };
     const cropType = getSelectedCropType();
     if (cropType) params.crop_type = cropType;
@@ -370,7 +364,7 @@ const mapZoneTaskList = (data, requestDate) => {
         TASK_KIND.ABNORMAL,
         normalList.length + weatherList.length
     );
-    return { list: [...normalList, ...weatherList, ...abnormalList], lat, lng, dateStr, zoneId };
+    return { list: [...abnormalList, ...normalList, ...weatherList,], lat, lng, dateStr, zoneId };
 };
 
 const resolveRangeStart = (data) => {
@@ -422,38 +416,58 @@ async function ensureSelectedFarm() {
 
 const refreshMapMarkers = () => {
     if (!indexMap.kmap) return;
-    indexMap.initData(getMapMarkerList(), "farmName", "wkt");
+    indexMap.initData(getMapMarkerList(), "farmName", "wkt", false);
 };
 
-/** 与日历一致:展示 7 天窗口内全部待执行农事(列表仍按选中日过滤) */
-const getMapMarkerList = () =>
-    taskList.value.filter((item) => item.status === "pending" && item.wkt);
+/** 每个分区一个点位,点位上方显示该分区最新一条农事名称,右上角显示农事数量 */
+const getMapMarkerList = () => {
+    const zoneMap = new Map();
+    for (const item of taskList.value) {
+        if (item.latitude == null || item.longitude == null) continue;
+        const key = item.zoneId ?? "default";
+        if (!zoneMap.has(key)) {
+            zoneMap.set(key, {
+                zoneId: item.zoneId,
+                lat: item.latitude,
+                lng: item.longitude,
+                latest: item,
+                count: 1,
+            });
+            continue;
+        }
+        const cur = zoneMap.get(key);
+        cur.count += 1;
+        if ((item.executeDateFull || "") > (cur.latest.executeDateFull || "")) {
+            cur.latest = item;
+        }
+    }
+    return Array.from(zoneMap.values()).map((zone) => ({
+        id: String(zone.zoneId ?? "default"),
+        zoneId: zone.zoneId,
+        // 仅显示最新一条农事名称
+        farmName: zone.latest?.title || "",
+        executeDate: zone.latest?.executeDate || "",
+        count: zone.count,
+        wkt: buildPointWkt(zone.lng, zone.lat, 0),
+    }));
+};
 
 /**
- * 按日历 7 天逐日请求后合并(接口按 trigger_date 返回当日任务)。
+ * 单次请求接口(近 7 天窗口),遍历 data.days 合并全部农事卡片。
  * 切换日期只本地过滤列表,地图仍用完整 7 天点位。
  */
 const fetchZoneFwTaskList = async () => {
     try {
-        const dates = getCalendarDateRange();
-        const settled = await Promise.all(
-            dates.map((triggerDate) =>
-                VE_API.questionnaire
-                    .getZoneFwTaskList(resolveQueryParams(triggerDate))
-                    .then((res) => ({ triggerDate, data: res?.data || null }))
-                    .catch((e) => {
-                        console.warn("[work_execute] zone_fw_task_list failed", triggerDate, e);
-                        return { triggerDate, data: null };
-                    })
-            )
-        );
+        const res = await VE_API.questionnaire.getZoneFwTaskList(resolveQueryParams());
+        const data = res?.data || null;
+        const days = data && Array.isArray(data.days) ? data.days : [];
         const merged = [];
         const seen = new Set();
         let lat = null;
         let lng = null;
-        for (const { triggerDate, data } of settled) {
-            if (!data) continue;
-            const mapped = mapZoneTaskList(data, triggerDate);
+        for (const day of days) {
+            if (!day) continue;
+            const mapped = mapZoneTaskList(day, day.date || data.start_date);
             if (lat == null && mapped.lat != null) lat = mapped.lat;
             if (lng == null && mapped.lng != null) lng = mapped.lng;
             for (const item of mapped.list) {
@@ -917,8 +931,15 @@ onDeactivated(() => {
 
         .status-badge {
             color: #1a1408;
-            background: #ffd489;
-            border-color: #ffd489;
+            color: rgba(255, 255, 255, 0.6);
+            background-color: rgba(255, 255, 255, 0.05);
+            border: 1px dashed rgba(255, 255, 255, 0.12);
+
+            &.status-pending {
+                background: #ffd489;
+                color: #000000;
+                border-color: #ffd489;
+            }
 
             &.is-done {
                 color: rgba(255, 255, 255, 0.65);