Browse Source

feat:对接农事列表接口和报告接口

wangsisi 3 hours ago
parent
commit
d51a26406c

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

@@ -32,7 +32,8 @@ module.exports = {
         type: "get",
     },
     // 分区农事任务列表:GET /api/questionnaire/zone_fw_task_list
-    // 参数 zone_id + farm_id + trigger_date;返回 fw_tasks(标准化)/ weather_work(气象)
+    // 参数 zone_id + farm_id + trigger_date + crop_type
+    // 返回 fw_tasks(标准)/ weather_work(气象)/ abnormal_fw_tasks(异常)
     getZoneFwTaskList: {
         url: config.base_new_url + "questionnaire/zone_fw_task_list",
         type: "get",

+ 302 - 50
src/views/old_mini/agri_file/pages/diagnosisReport.vue

@@ -1,12 +1,12 @@
 <template>
-    <div class="diagnosis-report-page">
+    <div class="diagnosis-report-page" v-loading="loading">
         <div class="diagnosis-header">
             <div class="diagnosis-header__title-row">
                 <span class="diagnosis-header__title">种植报告</span>
                 <img class="diagnosis-header__icon" src="@/assets/img/report/star.png" alt="" />
             </div>
-            <div class="diagnosis-header__desc">梅县区雁洋镇南福村的飞鸟智慧农业</div>
-            <div class="diagnosis-header__base">梅州柚子基地</div>
+            <div class="diagnosis-header__desc">{{ headerDesc }}</div>
+            <div class="diagnosis-header__base">{{ headerBase }}</div>
         </div>
         <div class="diagnosis-content">
             <div class="variety-bar">
@@ -29,6 +29,7 @@
                     <span>基本信息</span>
                 </div>
                 <div class="base-data__body">
+                    <div v-if="!loading && !baseInfoList.length" class="base-data__empty">暂无基本信息</div>
                     <div v-for="item in baseInfoList" :key="item.key" class="base-info-item">
                         <div class="base-info-item__row">
                             <span class="base-info-item__label">{{ item.label }}</span>
@@ -62,6 +63,12 @@
                 </div>
                 <div class="report-panel">
                     <div class="report-panel__title">{{ activeReportTabMeta.label }}</div>
+                    <div
+                        v-if="activeReportHtml"
+                        class="report-panel__body"
+                        v-html="activeReportHtml"
+                    ></div>
+                    <div v-else class="report-panel__empty">暂无报告内容</div>
                 </div>
             </div>
         </div>
@@ -77,8 +84,9 @@
 </template>
 
 <script setup>
-import { computed, ref } from "vue";
+import { computed, onActivated, onMounted, ref } from "vue";
 import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
 import { Plus, Edit } from "@element-plus/icons-vue";
 import adjustPhenologyPopup from "../components/adjustPhenologyPopup.vue";
 import restingDatePopup from "../components/restingDatePopup.vue";
@@ -89,61 +97,242 @@ import tab3Icon from "@/assets/img/report/tab-3.png";
 import tab4Icon from "@/assets/img/report/tab-4.png";
 import tab5Icon from "@/assets/img/report/tab-5.png";
 
+// ---------------------------------------------------------------------------
+// Props / Emits — 无
+// ---------------------------------------------------------------------------
 const router = useRouter();
-const varietyList = ref([
-    { id: 1, name: "金柚" }
-]);
-const activeVarietyId = ref(1);
-const adjustPhenologyPopupRef = ref(null);
-const restingDatePopupRef = ref(null);
-const editPlantPointPopupRef = ref(null);
 
-/** 基本信息展示数据:后续可替换为接口 */
-const baseInfoList = ref([
-    {
-        key: "crop",
-        label: "种植作物:",
-        value: "柚子 — 金柚",
-        actionInline: { key: "changeVariety", text: "更改品种" }
-    },
-    {
-        key: "trait",
-        label: "品种特质:",
-        value: "金柚果大皮薄,果肉晶莹剔透,酸甜适中,富含维生素C和膳食纤维,具有清热解渴、健脾开胃的功效。其果皮可用于提取精油,果肉适合鲜食及加工成果汁、果酱等产品。"
-    },
-    {
-        key: "phenology",
-        label: "物候信息:",
-        value: "当前为果实膨大期,预计采收时间为2024年10月中旬。",
-        actionInline: { key: "adjustPhenology", text: "调整物候" }
-    },
-    {
-        key: "soil",
-        label: "土壤条件:",
-        value: "土壤pH值6.2,有机质含量2.8%,氮磷钾含量适中,土壤通气性和保水性良好,适合柚子生长。建议定期检测土壤养分,适时补充有机肥。",
-        actionBlock: { key: "soilSense", text: "土壤感知" }
-    },
-    {
-        key: "terrain",
-        label: "地形条件:",
-        value: "坡度8°,海拔185米,南向坡面,排水良好。",
-        actionInline: { key: "editLocation", text: "修改定位" }
-    }
-]);
+// ---------------------------------------------------------------------------
+// 常量
+// ---------------------------------------------------------------------------
+/** module1_basic 字段标签 → 基本信息展示项 */
+const BASE_FIELD_META = [
+    { key: "crop", labels: ["作物品种", "种植作物"], label: "种植作物:", actionInline: { key: "changeVariety", text: "更改品种" } },
+    { key: "trait", labels: ["品种特质"], label: "品种特质:" },
+    { key: "phenology", labels: ["物候信息", "物候期"], label: "物候信息:", actionInline: { key: "adjustPhenology", text: "调整物候" } },
+    { key: "soil", labels: ["土壤条件"], label: "土壤条件:", actionBlock: { key: "soilSense", text: "土壤感知" } },
+    { key: "terrain", labels: ["地形条件"], label: "地形条件:", actionInline: { key: "editLocation", text: "修改定位" } },
+];
+
+/** Tab key → report/check 返回字段 */
+const REPORT_TAB_FIELD = {
+    weather: "module2_weather",
+    pest: "module3_pest",
+    quality: "module4_quality",
+    input: "module5_input",
+    manage: "module6_management",
+};
 
-/** 报告模块 Tab */
 const reportTabs = [
     { key: "weather", label: "气象风险", icon: tab1Icon },
     { key: "pest", label: "病虫风险", icon: tab2Icon },
     { key: "quality", label: "品质潜力", icon: tab3Icon },
     { key: "input", label: "必要投入", icon: tab4Icon },
-    { key: "manage", label: "农事管理", icon: tab5Icon }
+    { key: "manage", label: "农事管理", icon: tab5Icon },
 ];
+
+// ---------------------------------------------------------------------------
+// 页面状态
+// ---------------------------------------------------------------------------
+const loading = ref(false);
+const varietyList = ref([]);
+const activeVarietyId = ref(null);
+const headerDesc = ref("");
+const headerBase = ref("");
+const baseInfoList = ref([]);
+/** 各 Tab 对应的 markdown 原文 */
+const moduleMarkdownMap = ref({});
 const activeReportTab = ref("weather");
+const adjustPhenologyPopupRef = ref(null);
+const restingDatePopupRef = ref(null);
+const editPlantPointPopupRef = ref(null);
+
+// ---------------------------------------------------------------------------
+// 计算属性
+// ---------------------------------------------------------------------------
 const activeReportTabMeta = computed(
     () => reportTabs.find((tab) => tab.key === activeReportTab.value) || reportTabs[0]
 );
 
+const activeReportHtml = computed(() => {
+    const field = REPORT_TAB_FIELD[activeReportTab.value];
+    const md = moduleMarkdownMap.value[field] || "";
+    return md ? markdownToHtml(md) : "";
+});
+
+// ---------------------------------------------------------------------------
+// 工具函数
+// ---------------------------------------------------------------------------
+function getSelectedFarm() {
+    try {
+        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+    } catch {
+        return {};
+    }
+}
+
+/** 解析 module1_basic 中 `**标签**:内容` 段落 */
+function parseBasicSections(markdown = "") {
+    const text = String(markdown).replace(/\r\n/g, "\n");
+    const sections = {};
+    const re = /\*\*([^*]+)\*\*[::]\s*/g;
+    const matches = [...text.matchAll(re)];
+    matches.forEach((match, index) => {
+        const label = String(match[1] || "").trim();
+        const start = match.index + match[0].length;
+        const end = index + 1 < matches.length ? matches[index + 1].index : text.length;
+        sections[label] = text.slice(start, end).trim();
+    });
+    return sections;
+}
+
+function findSectionValue(sections, labels) {
+    for (const label of labels) {
+        if (sections[label]) return sections[label];
+    }
+    return "";
+}
+
+/** 轻量 markdown → HTML(标题 / 粗体 / 列表) */
+function markdownToHtml(markdown = "") {
+    const escapeHtml = (str) =>
+        String(str)
+            .replace(/&/g, "&amp;")
+            .replace(/</g, "&lt;")
+            .replace(/>/g, "&gt;");
+
+    const lines = String(markdown).replace(/\r\n/g, "\n").split("\n");
+    const html = [];
+    let inList = false;
+
+    const flushList = () => {
+        if (!inList) return;
+        html.push("</ul>");
+        inList = false;
+    };
+
+    const inline = (str) =>
+        escapeHtml(str).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
+
+    lines.forEach((raw) => {
+        const line = raw.trimEnd();
+        const trimmed = line.trim();
+        if (!trimmed) {
+            flushList();
+            return;
+        }
+        if (/^###\s+/.test(trimmed)) {
+            flushList();
+            html.push(`<h4>${inline(trimmed.replace(/^###\s+/, ""))}</h4>`);
+            return;
+        }
+        if (/^##\s+/.test(trimmed)) {
+            flushList();
+            html.push(`<h3>${inline(trimmed.replace(/^##\s+/, ""))}</h3>`);
+            return;
+        }
+        if (/^#\s+/.test(trimmed)) {
+            flushList();
+            html.push(`<h2>${inline(trimmed.replace(/^#\s+/, ""))}</h2>`);
+            return;
+        }
+        if (/^[-*]\s+/.test(trimmed)) {
+            if (!inList) {
+                html.push("<ul>");
+                inList = true;
+            }
+            html.push(`<li>${inline(trimmed.replace(/^[-*]\s+/, ""))}</li>`);
+            return;
+        }
+        flushList();
+        html.push(`<p>${inline(trimmed)}</p>`);
+    });
+    flushList();
+    return html.join("");
+}
+
+function buildBaseInfoList(sections) {
+    return BASE_FIELD_META.map((meta) => ({
+        key: meta.key,
+        label: meta.label,
+        value: findSectionValue(sections, meta.labels) || "—",
+        actionInline: meta.actionInline,
+        actionBlock: meta.actionBlock,
+    }));
+}
+
+function applyReportData(report, farm) {
+    const region = report.region || farm.rawAddress || farm.farm_address || farm.city || "";
+    const farmName = farm.farm_name || farm.name || "";
+    headerDesc.value = region ? `${region}的飞鸟智慧农业` : "飞鸟智慧农业";
+    headerBase.value = farmName || "我的基地";
+
+    const cropName = report.variety || report.crop || farm.variety_name || farm.categoryLabel || "作物";
+    varietyList.value = [{ id: report.id || 1, name: cropName }];
+    activeVarietyId.value = varietyList.value[0].id;
+
+    const sections = parseBasicSections(report.module1_basic || "");
+    baseInfoList.value = buildBaseInfoList(sections);
+
+    moduleMarkdownMap.value = {
+        module2_weather: report.module2_weather || "",
+        module3_pest: report.module3_pest || "",
+        module4_quality: report.module4_quality || "",
+        module5_input: report.module5_input || "",
+        module6_management: report.module6_management || "",
+    };
+}
+
+function resetReportView(farm = {}) {
+    headerDesc.value = farm.rawAddress || farm.farm_address || farm.city
+        ? `${farm.rawAddress || farm.farm_address || farm.city}的飞鸟智慧农业`
+        : "飞鸟智慧农业";
+    headerBase.value = farm.farm_name || farm.name || "我的基地";
+    varietyList.value = [];
+    activeVarietyId.value = null;
+    baseInfoList.value = [];
+    moduleMarkdownMap.value = {};
+}
+
+// ---------------------------------------------------------------------------
+// 业务逻辑:加载报告
+// ---------------------------------------------------------------------------
+async function fetchDiagnosisReport() {
+    const farm = getSelectedFarm();
+    const farmId = farm.farm_id ?? farm.id;
+    const zoneId = farm.zone_id ?? farm.zoneId;
+    if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
+        resetReportView(farm);
+        ElMessage.warning("请先选择农场与分区");
+        return;
+    }
+
+    loading.value = true;
+    try {
+        const raw = await VE_API.questionnaire.checkReportGenerated({
+            farm_id: farmId,
+            zone_id: zoneId,
+        });
+        const res = Array.isArray(raw) ? raw[0] : raw;
+        const report = res?.data?.report;
+        if (res?.code === 200 && report) {
+            applyReportData(report, farm);
+            return;
+        }
+        resetReportView(farm);
+        ElMessage.warning(res?.msg || res?.message || "暂无诊断报告");
+    } catch (error) {
+        console.error("fetchDiagnosisReport failed", error);
+        resetReportView(farm);
+        ElMessage.error("报告加载失败");
+    } finally {
+        loading.value = false;
+    }
+}
+
+// ---------------------------------------------------------------------------
+// 事件处理 / 生命周期
+// ---------------------------------------------------------------------------
 const handleBaseAction = (key) => {
     if (key === "adjustPhenology") {
         adjustPhenologyPopupRef.value?.open();
@@ -157,12 +346,10 @@ const handleBaseAction = (key) => {
         router.push("/soil_sense");
         return;
     }
-    // 后续接入对应弹窗 / 跳转
     console.log("base action:", key);
 };
 
 const handlePhenologyConfirm = (payload) => {
-    // 物候弹窗确认后,接着弹出历史开花/播种时间弹窗
     restingDatePopupRef.value?.open(payload);
 };
 
@@ -171,14 +358,16 @@ const handleSwitchGrowing = () => {
 };
 
 const handleRestingDateConfirm = (payload) => {
-    // 后续接入接口保存
     console.log("resting date confirm:", payload);
 };
 
 const handlePlantPointConfirm = (payload) => {
-    // 后续接入接口保存
     console.log("plant point confirm:", payload);
 };
+
+// onMounted(fetchDiagnosisReport);
+// tabbar 切换时组件被 keep-alive 缓存,需在激活时重新拉报告
+onActivated(fetchDiagnosisReport);
 </script>
 
 <style lang="scss" scoped>
@@ -261,7 +450,8 @@ const handlePlantPointConfirm = (payload) => {
         .base-data {
             margin: 10px 0;
             padding: 5px;
-            min-height: 382px;
+            // min-height: 382px;
+            min-height: auto;
             background: url("@/assets/img/report/base-bg.png") no-repeat center top / 100% 100%;
             box-sizing: border-box;
 
@@ -289,6 +479,13 @@ const handlePlantPointConfirm = (payload) => {
                 background: #fff;
                 border-radius: 8px;
 
+                .base-data__empty {
+                    padding: 20px 0;
+                    text-align: center;
+                    color: #999;
+                    font-size: 13px;
+                }
+
                 .base-info-item {
                     margin-bottom: 8px;
                     &__row {
@@ -402,6 +599,61 @@ const handlePlantPointConfirm = (payload) => {
                     z-index: -1;
                 }
             }
+
+            &__empty {
+                margin-top: 14px;
+                padding: 24px 0;
+                text-align: center;
+                color: #999;
+                font-size: 13px;
+            }
+
+            &__body {
+                margin-top: 12px;
+                color: #333;
+                font-size: 14px;
+                line-height: 1.7;
+                word-break: break-word;
+
+                :deep(h2) {
+                    margin: 0 0 10px;
+                    font-size: 16px;
+                    font-weight: 600;
+                    color: #1d2129;
+                }
+
+                :deep(h3) {
+                    margin: 14px 0 8px;
+                    font-size: 15px;
+                    font-weight: 600;
+                    color: #1d2129;
+                }
+
+                :deep(h4) {
+                    margin: 12px 0 6px;
+                    font-size: 14px;
+                    font-weight: 600;
+                    color: #4e5969;
+                }
+
+                :deep(p) {
+                    margin: 0 0 8px;
+                }
+
+                :deep(ul) {
+                    margin: 0 0 10px;
+                    padding-left: 18px;
+                }
+
+                :deep(li) {
+                    margin-bottom: 4px;
+                }
+
+                :deep(strong) {
+                    font-weight: 600;
+                    color: #1d2129;
+                }
+            }
         }
     }
 }

+ 3 - 7
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -607,22 +607,18 @@ function readSelectedFarmIds() {
     }
 }
 
-/** 新增成功后触发 check + generate_6module,不等待、不落库 */
+/** 新增成功后触发 generate_6module,不等待、不落库 */
 function triggerReportApis(res) {
     const data = res?.data && typeof res.data === "object" ? res.data : res;
     const ids = pickFarmZoneIds(data) || pickFarmZoneIds(res) || readSelectedFarmIds() || {};
     const reportParams = pendingReportParams.value || {};
-    const checkParams = {
+    const generateParams = {
         farm_id: ids.farm_id ?? "",
         zone_id: ids.zone_id ?? "",
-    };
-    const generateParams = {
-        ...checkParams,
         region: reportParams.region || "",
         crop: reportParams.crop || "",
     };
     // 仅触发请求,不做结果处理
-    VE_API.questionnaire.checkReportGenerated(checkParams).catch(() => {});
     VE_API.report.generate6Module(generateParams).catch(() => {});
 }
 
@@ -670,7 +666,7 @@ async function submitEntry(includeEquipment = true) {
         const res = Array.isArray(raw) ? raw[0] : raw;
         if (res?.code === 200) {
             pendingReportParams.value = buildReportParams();
-            // 新增成功后立刻触发 check / generate_6module,不等待结果
+            // 新增成功后立刻触发 generate_6module,不等待结果
             triggerReportApis(res);
             // 新增成功后拉列表,选中最后一项
             await selectNewlyCreatedFarm();

+ 109 - 26
src/views/old_mini/work_detail/index.vue

@@ -45,10 +45,10 @@
                     </div>
                 </div>
 
-                <div class="status-r">
+                <!-- <div class="status-r">
                     <img class="status-r__icon" src="@/assets/img/home/user-icon-fill.png" alt="">
                     外包农事
-                </div>
+                </div> -->
             </div>
 
             <div class="work-wrap" :class="{ 'from-share': isFromShare }">
@@ -94,10 +94,10 @@
                                 </div>
                             </div>
                         </div>
-                        <div class="plan-row">
+                        <!-- <div class="plan-row">
                             <div class="plan-row__label">{{ $t('workDetail.triggerCondition') }}</div>
                             <div class="plan-row__value">{{ displayTrigger }}</div>
-                        </div>
+                        </div> -->
                         <div class="plan-row">
                             <div class="plan-row__label">{{ $t('workDetail.prescription') }}</div>
                             <div class="plan-row__value">{{ displayPrescription }}</div>
@@ -114,7 +114,7 @@
                 </div>
 
                 <!-- 农资信息 -->
-                <div class="box-wrap stage-card material-card">
+                <!-- <div class="box-wrap stage-card material-card">
                     <div class="work-info">
                         <div class="material-card__header">
                             <div class="material-card__title">
@@ -133,7 +133,7 @@
                             <div class="plan-row__value">{{ materialNotes }}</div>
                         </div>
                     </div>
-                </div>
+                </div> -->
             </div>
         </div>
 
@@ -166,6 +166,12 @@ import AreaMap from "./components/areaMap.js";
 import AgriMaterialTable from "./components/agriMaterialTable.vue";
 import WKT from "ol/format/WKT.js";
 
+// ---------- 常量 ----------
+/** 与 work_execute 列表约定:点击任务写入的缓存 key */
+const FW_TASK_CACHE_KEY = "WORK_EXECUTE_FW_TASK_DETAIL";
+const MANUAL_SCHEME_KEYS = ["人工方案", "人工"];
+const MACHINE_SCHEME_KEYS = ["机械方案", "机械"];
+
 const route = useRoute();
 const { locale } = useI18n();
 const router = useRouter();
@@ -212,9 +218,10 @@ const toQueryNum = (v) => {
     return Number.isNaN(n) ? undefined : n;
 };
 
-/** 列表进入:带 zone_id + date + 人工/机械 id,走 fw_task_details */
+/** 列表进入:带 fromZoneList,或旧链路 zone_id + date + 人工/机械 id */
 const isFwTaskMode = computed(() => {
     const p = pageParams.value || {};
+    if (isTruthyFlag(p.fromZoneList)) return true;
     return hasQueryValue(p.zone_id) && hasQueryValue(p.date) && (hasQueryValue(p.manual_id) || hasQueryValue(p.mechanical_id));
 });
 
@@ -237,7 +244,8 @@ const syncMiniJsonFromRoute = () => {
 
 const miniJson = ref(null);
 const farmData = ref({});
-const taskDetail = ref(null); // fw_task_details 返回
+/** 列表缓存 / 旧详情接口归一后的任务数据 */
+const taskDetail = ref(null);
 
 const wktFmt = new WKT();
 const MAP_CENTER_FALLBACK = "POINT(113.1093017627431 22.57454083668)";
@@ -314,35 +322,102 @@ const farmWorkTypeObj = {
     8: "标准调节类",
 };
 
-const fetchFwTaskDetails = () => {
-    const query = fwTaskQuery.value;
-    if (query.zone_id == null || !query.date) return;
+function readFwTaskCache() {
+    try {
+        return parseMaybeJson(sessionStorage.getItem(FW_TASK_CACHE_KEY));
+    } catch {
+        return null;
+    }
+}
+
+/** 按当前人工/机械方案取文案;字符串直接返回 */
+function pickSchemeText(value, method = "machine") {
+    if (value == null || value === "") return "";
+    if (typeof value === "string" || typeof value === "number") return String(value);
+    if (typeof value !== "object") return "";
+    const preferred = method === "manual" ? MANUAL_SCHEME_KEYS : MACHINE_SCHEME_KEYS;
+    const fallback = method === "manual" ? MACHINE_SCHEME_KEYS : MANUAL_SCHEME_KEYS;
+    for (const key of preferred) {
+        if (value[key]) return String(value[key]);
+    }
+    for (const key of fallback) {
+        if (value[key]) return String(value[key]);
+    }
+    const first = Object.values(value).find((item) => item != null && item !== "");
+    return first != null ? String(first) : "";
+}
+
+/** 列表缓存 → 详情页统一结构(兼容旧 manual/mechanical 形态) */
+function normalizeCachedTask(cache) {
+    const raw = cache?.raw;
+    if (!raw || typeof raw !== "object") return null;
+    const meta = cache.meta || {};
+    const statusText = raw.status_text || raw.fw_status || meta.statusText || "";
+    return {
+        ...raw,
+        fw_status: statusText,
+        fw_explain:
+            typeof raw.fw_explain === "string"
+                ? raw.fw_explain
+                : pickSchemeText(raw.fw_explain, "manual") ||
+                  pickSchemeText(raw.fw_explain_simple, "manual") ||
+                  pickSchemeText(raw.fw_simple_explain, "manual"),
+        // 人工 / 机械方案字段按列表结构拆开,供切换 Tab 使用
+        manual: {
+            fw_trigger: pickSchemeText(raw.fw_trigger, "manual") || raw.abnormal_type || "",
+            fw_precrip: pickSchemeText(raw.fw_precrip, "manual"),
+            fw_params: pickSchemeText(raw.fw_params, "manual"),
+            fw_atten: pickSchemeText(raw.fw_atten, "manual"),
+            materials: raw.materials || raw.agri_materials || [],
+        },
+        mechanical: {
+            fw_trigger: pickSchemeText(raw.fw_trigger, "machine") || raw.abnormal_type || "",
+            fw_precrip: pickSchemeText(raw.fw_precrip, "machine"),
+            fw_params: pickSchemeText(raw.fw_params, "machine"),
+            fw_atten: pickSchemeText(raw.fw_atten, "machine"),
+            materials: raw.materials || raw.agri_materials || [],
+        },
+        _displayNameManual: pickSchemeText(raw.fw_name, "manual"),
+        _displayNameMachine: pickSchemeText(raw.fw_name, "machine"),
+    };
+}
+
+/** 优先用列表写入的缓存,不再请求 fw_task_details */
+const applyCachedFwTask = () => {
+    const cache = readFwTaskCache();
+    const normalized = normalizeCachedTask(cache);
+    if (!normalized) return false;
     farmData.value = {};
-    const params = { zone_id: query.zone_id, date: query.date };
-    if (query.manual_id != null) params.manual_id = query.manual_id;
-    if (query.mechanical_id != null) params.mechanical_id = query.mechanical_id;
-    VE_API.questionnaire.getFwTaskDetails(params).then((res) => {
-        if (res?.code === 200 && res.data) {
-            taskDetail.value = res.data;
-            initWorkDetailMap();
-        }
-    });
+    taskDetail.value = normalized;
+    initWorkDetailMap();
+    return true;
 };
 
 const getDetail = () => {
     if (isFwTaskMode.value) {
-        fetchFwTaskDetails();
+        if (applyCachedFwTask()) return;
+        // 无缓存(如刷新丢 session)时不再打旧详情接口,仅保留 query 基础信息
+        farmData.value = {};
+        taskDetail.value = {
+            fw_name: pageParams.value?.fw_name || "",
+            fw_status: statusVal.value === "pending" ? "待执行" : statusVal.value || "",
+            manual: { fw_trigger: "--" },
+            mechanical: { fw_trigger: "--" },
+            _displayNameManual: pageParams.value?.fw_name || "",
+            _displayNameMachine: pageParams.value?.fw_name || "",
+        };
+        initWorkDetailMap();
         return;
     }
     taskDetail.value = null;
     const id = workId.value;
     if (id == null || id === "") return;
-    VE_API.monitor.getWorkDetail({ id }).then(res => {
+    VE_API.monitor.getWorkDetail({ id }).then((res) => {
         if (res.code === 200 && res.data.length) {
             farmData.value = res.data[0];
             initWorkDetailMap();
         }
-    })
+    });
 };
 
 const handleComplete = () => {
@@ -375,9 +450,16 @@ const displayDate = computed(() => {
     if (pageParams.value?.date) return formatTitleDate(pageParams.value.date);
     return "08.18";
 });
-const displayName = computed(
-    () => taskDetail.value?.fw_name || pageParams.value?.fw_name || farmData.value?.work_name || ""
-);
+const displayName = computed(() => {
+    const data = taskDetail.value;
+    if (data) {
+        const byMethod =
+            executeMethod.value === "manual" ? data._displayNameManual : data._displayNameMachine;
+        if (byMethod) return byMethod;
+        if (typeof data.fw_name === "string" && data.fw_name) return data.fw_name;
+    }
+    return pageParams.value?.fw_name || farmData.value?.work_name || "";
+});
 const displayExplain = computed(
     () => taskDetail.value?.fw_explain || farmData.value?.interaction_reason || ""
 );
@@ -535,6 +617,7 @@ const handleShare = () => {
                     isAbnormal: pageParams.value?.isAbnormal,
                     isWeather: pageParams.value?.isWeather,
                     isHighRisk: pageParams.value?.isHighRisk,
+                    fromZoneList: 1,
                     fromShare: 1,
                 }
                 : {

+ 105 - 31
src/views/old_mini/work_execute/index.vue

@@ -40,9 +40,9 @@
                     </div>
 
                     <div class="task-card__body">
-                        <div class="info-line">
+                        <!-- <div class="info-line">
                             触发条件:{{ item.fw_trigger || '--' }}
-                        </div>
+                        </div> -->
                         <div class="info-line">
                             {{ t("workExecute.farmSituation") }}{{ item.analysisSummary }}
                         </div>
@@ -52,10 +52,10 @@
                     </div>
 
                     <div class="task-card__footer">
-                        <div class="footer-l">
+                        <!-- <div class="footer-l">
                             <img class="footer-l__icon" src="@/assets/img/home/user-icon-fill.png" alt="">
                             外包农事
-                        </div>
+                        </div> -->
                         <div class="footer-r">
                             <div class="card-btn card-btn--ghost" @click.stop="handleForward(item)">
                                 {{ t("workExecute.forward") }}
@@ -97,11 +97,16 @@ import wx from "weixin-js-sdk";
 
 
 // ---------- 常量 ----------
-/** 任务类型:fw_tasks 标准化农事 / weather_work 气象农事 */
+/** 任务类型:fw_tasks 标准农事 / weather_work 气象农事 / abnormal_fw_tasks 异常农事 */
 const TASK_KIND = {
     NORMAL: "normal",
     WEATHER: "weather",
+    ABNORMAL: "abnormal",
 };
+/** 方案文案优先取人工,接口里标准任务是字符串,气象/异常是按方案拆开的对象 */
+const SCHEME_TEXT_KEYS = ["人工方案", "机械方案", "人工", "机械"];
+/** 列表进详情:整条任务缓存,详情页不再打 fw_task_details */
+const FW_TASK_CACHE_KEY = "WORK_EXECUTE_FW_TASK_DETAIL";
 const COMPLETED_STATUS_RE = /已完成|已执行|已认证|本人完成/;
 
 const { t } = useI18n();
@@ -150,6 +155,19 @@ function getSelectedZoneId() {
     return id != null && id !== "" ? Number(id) : null;
 }
 
+/** crop_type 取作物编码,如 LCH;各入口字段不统一 */
+function getSelectedCropType() {
+    const farm = getSelectedFarm();
+    return (
+        farm.category_code ||
+        farm.crop_code ||
+        farm.crop_type ||
+        farm.farm_category ||
+        farm.farm_variety ||
+        ""
+    );
+}
+
 // ---------- 页面状态 ----------
 const filterDate = ref(getTodayDate()); // 当前选中日期
 const taskList = ref([]); // 已映射的农事卡片(归属接口 date)
@@ -164,12 +182,17 @@ const pendingCount = computed(() => dayTaskList.value.filter((item) => item.stat
 const completedCount = computed(() => dayTaskList.value.filter((item) => item.status === "completed").length);
 const displayTaskList = computed(() => dayTaskList.value.filter((item) => item.status === activeTab.value));
 
-/** zone_id / farm_id 取当前选中农场分区,trigger_date 取日历选中日(默认当天) */
-const resolveQueryParams = () => ({
-    zone_id: getSelectedZoneId(),
-    farm_id: getSelectedFarmId(),
-    trigger_date: filterDate.value || getTodayDate(),
-});
+/** zone_id / farm_id / crop_type 取当前选中农场,trigger_date 取日历选中日 */
+const resolveQueryParams = () => {
+    const params = {
+        zone_id: getSelectedZoneId(),
+        farm_id: getSelectedFarmId(),
+        trigger_date: filterDate.value || getTodayDate(),
+    };
+    const cropType = getSelectedCropType();
+    if (cropType) params.crop_type = cropType;
+    return params;
+};
 
 const isCompletedStatus = (status) => COMPLETED_STATUS_RE.test(String(status || ""));
 
@@ -184,24 +207,44 @@ const buildPointWkt = (lng, lat, index = 0) => {
     return `POINT(${Number(lng) + offset} ${Number(lat) + offset * 0.6})`;
 };
 
-/** 单条任务映射为卡片;kind 区分标准化 / 气象 */
+/** 名称/说明可能是字符串,也可能是 { 人工方案, 机械方案 } / { 人工, 机械 } */
+const pickSchemeText = (value) => {
+    if (value == null || value === "") return "";
+    if (typeof value === "string" || typeof value === "number") return String(value);
+    if (typeof value !== "object") return "";
+    for (const key of SCHEME_TEXT_KEYS) {
+        if (value[key]) return String(value[key]);
+    }
+    const first = Object.values(value).find((item) => item != null && item !== "");
+    return first != null ? String(first) : "";
+};
+
+/** 单条任务映射为卡片;kind 区分标准 / 气象 / 异常 */
 const mapTaskItem = (task, index, { dateStr, lat, lng, zoneId, kind }) => {
     const executeDateFull = task.date || dateStr;
-    const completed = isCompletedStatus(task.fw_status);
     const isWeather = kind === TASK_KIND.WEATHER;
-    const prefix = isWeather ? "weather" : "fw";
+    const isAbnormal = kind === TASK_KIND.ABNORMAL;
+    const statusText = pickSchemeText(task.fw_status || task.status_text);
+    const completed = isCompletedStatus(statusText);
+    const prefix = isAbnormal ? "abnormal" : isWeather ? "weather" : "fw";
+    const title = pickSchemeText(task.fw_name);
+    const trigger = pickSchemeText(task.fw_trigger) || (isAbnormal ? task.abnormal_type || "" : "");
     return {
-        id: `${prefix}_${task.manual_id || task.mechanical_id || index}_${executeDateFull}`,
+        id: `${prefix}_${task.manual_id || task.mechanical_id || pickSchemeText(task.fw_code) || index}_${executeDateFull}_${index}`,
         status: completed ? "completed" : "pending",
-        title: task.fw_name || "",
+        title,
         isHighRisk: false,
-        isNormal: !isWeather,
-        isAbnormal: false,
+        isNormal: kind === TASK_KIND.NORMAL,
+        isAbnormal,
         isWeather,
-        cornerTip: task.fw_status || (completed ? "已完成" : "待执行"),
-        analysisSummary: task.fw_simple_explain || task.fw_explain || "",
+        cornerTip: statusText || (completed ? "已完成" : "待执行"),
+        analysisSummary:
+            pickSchemeText(task.fw_explain_simple) ||
+            pickSchemeText(task.fw_simple_explain) ||
+            pickSchemeText(task.fw_explain),
+        fw_trigger: trigger,
         areaCode: zoneId ? `分区 ${zoneId}` : "",
-        farmName: task.fw_name || "",
+        farmName: title,
         executeDate: formatExecuteDate(executeDateFull),
         executeDateFull,
         zoneId,
@@ -210,25 +253,30 @@ const mapTaskItem = (task, index, { dateStr, lat, lng, zoneId, kind }) => {
         latitude: lat,
         longitude: lng,
         wkt: buildPointWkt(lng, lat, index),
+        // 详情页直接消费原始列表项,避免再请求 fw_task_details
+        rawTask: task,
     };
 };
 
-/** 合并 fw_tasks(标准化)+ weather_work(气象);无单条日期时归到本次请求日 */
+/** 合并三类农事;无单条日期时归到本次请求日 */
 const mapZoneTaskList = (data, requestDate) => {
     const dateStr = requestDate || resolveRangeStart(data);
     const lat = data.latitude ?? 22.970229;
     const lng = data.longitude ?? 113.746646;
     const zoneId = data.zone_id ?? getSelectedZoneId();
     const ctx = { dateStr, lat, lng, zoneId };
-    const normalList = (Array.isArray(data.fw_tasks) ? data.fw_tasks : [])
-        .filter(Boolean)
-        .map((task, index) => mapTaskItem(task, index, { ...ctx, kind: TASK_KIND.NORMAL }));
-    const weatherList = (Array.isArray(data.weather_work) ? data.weather_work : [])
-        .filter(Boolean)
-        .map((task, index) =>
-            mapTaskItem(task, normalList.length + index, { ...ctx, kind: TASK_KIND.WEATHER })
-        );
-    return { list: [...normalList, ...weatherList], lat, lng, dateStr, zoneId };
+    const mapList = (raw, kind, offset) =>
+        (Array.isArray(raw) ? raw : [])
+            .filter(Boolean)
+            .map((task, index) => mapTaskItem(task, offset + index, { ...ctx, kind }));
+    const normalList = mapList(data.fw_tasks, TASK_KIND.NORMAL, 0);
+    const weatherList = mapList(data.weather_work, TASK_KIND.WEATHER, normalList.length);
+    const abnormalList = mapList(
+        data.abnormal_fw_tasks,
+        TASK_KIND.ABNORMAL,
+        normalList.length + weatherList.length
+    );
+    return { list: [...normalList, ...weatherList, ...abnormalList], lat, lng, dateStr, zoneId };
 };
 
 const resolveRangeStart = (data) => {
@@ -339,9 +387,35 @@ const buildFwTaskQuery = (item) => ({
     isAbnormal: item?.isAbnormal ? 1 : 0,
     isWeather: item?.isWeather ? 1 : 0,
     isHighRisk: item?.isHighRisk ? 1 : 0,
+    fromZoneList: 1,
 });
 
+/** 写入详情页缓存:原始接口字段 + 列表侧元信息 */
+const saveFwTaskCache = (item) => {
+    if (!item?.rawTask) return;
+    sessionStorage.setItem(
+        FW_TASK_CACHE_KEY,
+        JSON.stringify({
+            raw: item.rawTask,
+            meta: {
+                zone_id: item.zoneId ?? getSelectedZoneId(),
+                date: item.executeDateFull || getTodayDate(),
+                lat: item.latitude,
+                lng: item.longitude,
+                statusVal: item.status,
+                statusText: item.cornerTip || "",
+                fw_name: item.title || item.farmName || "",
+                isNormal: item.isNormal ? 1 : 0,
+                isAbnormal: item.isAbnormal ? 1 : 0,
+                isWeather: item.isWeather ? 1 : 0,
+                isHighRisk: item.isHighRisk ? 1 : 0,
+            },
+        })
+    );
+};
+
 const handleViewDetail = (item) => {
+    saveFwTaskCache(item);
     router.push({
         path: "/work_detail",
         query: buildFwTaskQuery(item),