Quellcode durchsuchen

fix: 对接巡园记录

lxf vor 1 Tag
Ursprung
Commit
d527696276

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

@@ -0,0 +1,10 @@
+const config = require("../config")
+
+module.exports = {
+    // 首页巡园要点查询:一次返回物候 / 异常 / 土壤任务
+    // 参数:crop_type、farm_id、zone_id、calc_date
+    getTypesTask: {
+        url: config.base_new_url + "pheno/interact_tasks_with_abnormal",
+        type: "get",
+    },
+}

+ 10 - 9
src/components/pageComponents/PhenologyTrackTimelineItem.vue

@@ -34,8 +34,8 @@
                                 class="track-content"
                                 :class="{ 'track-content--alert': item.isAlert }"
                             >
-                                <el-icon v-if="item.isAlert" class="track-content__warn"><WarningFilled /></el-icon>
-                                <span v-html="highlightContent(item.content)"></span>
+                                <el-icon size="16" v-if="item.isAlert" class="track-content__warn"><WarnTriangleFilled /></el-icon>
+                                <span v-html="item.content"></span>
                             </div>
                         </div>
                     </div>
@@ -96,7 +96,7 @@ function isAlertContent(text) {
 }
 
 function normalizeItem(item, index) {
-    const rawDate = item.date || item.enter_date || item.latest_time || "";
+    const rawDate = item.date || item.enter_date || item.trigger_date || item.latest_time || "";
     const content = item.content || item.sentence || item.desc || item.text || "";
     return {
         id: item.id ?? index,
@@ -331,12 +331,12 @@ defineExpose({
         .track-content {
             color: rgba(60, 60, 60, 0.5);
             font-size: 14px;
-            line-height: 21px;
+            // line-height: 21px;
             word-break: break-all;
 
             &--alert {
                 display: flex;
-                align-items: flex-start;
+                align-items: center;
                 gap: 4px;
             }
 
@@ -402,16 +402,16 @@ defineExpose({
 
             .track-content {
                 color: rgba(255, 255, 255, 1);
+                width: fit-content;
 
                 :deep(.track-hl) {
                     color: rgba(255, 212, 137, 1);
                 }
 
                 &--alert {
-                    padding: 8px 10px;
-                    border-radius: 6px;
-                    background: rgba(255, 77, 79, 0.18);
-                    color: rgba(255, 220, 220, 0.9);
+                    padding: 2px 10px;
+                    border-radius: 2px;
+                    background: rgba(126, 35, 33, 0.7);
 
                     :deep(.track-hl) {
                         color: #fff;
@@ -421,6 +421,7 @@ defineExpose({
 
             .track-content__warn {
                 display: inline-flex;
+                align-items: center;
                 color: #ff8f8f;
             }
         }

+ 27 - 23
src/components/popup/leaveConfirmPopup.vue

@@ -1,19 +1,15 @@
 <template>
-    <popup
-        v-model:show="showValue"
-        round
-        teleport="body"
-        class="leave-confirm-popup"
-        :close-on-click-overlay="true"
-    >
-        <img class="leave-confirm-popup__icon" src="@/assets/img/agricultural/notice.png" alt="" />
-        <div class="leave-confirm-popup__title">{{ t("agriFile.confirmLeaveTitle") }}</div>
-        <div class="leave-confirm-popup__desc">
-            <div>{{ t("agriFile.confirmLeaveDesc1") }}</div>
-            <div>{{ t("agriFile.confirmLeaveDesc2") }}</div>
+    <popup v-model:show="showValue" round teleport="body" class="leave-confirm-popup" :close-on-click-overlay="true">
+        <div class="leave-confirm-popup__bg">
+            <img class="leave-confirm-popup__icon" src="@/assets/img/agricultural/notice.png" alt="" />
+            <div class="leave-confirm-popup__title">{{ t("agriFile.confirmLeaveTitle") }}</div>
+            <div class="leave-confirm-popup__desc">
+                <div>{{ t("agriFile.confirmLeaveDesc1") }}</div>
+                <div>{{ t("agriFile.confirmLeaveDesc2") }}</div>
+            </div>
+            <div class="leave-confirm-popup__btn" @click="handleContinue">{{ t("agriFile.continueRecord") }}</div>
+            <div class="leave-confirm-popup__leave" @click="handleLeave">{{ t("agriFile.confirmLeave") }}</div>
         </div>
-        <div class="leave-confirm-popup__btn" @click="handleContinue">{{ t("agriFile.continueRecord") }}</div>
-        <div class="leave-confirm-popup__leave" @click="handleLeave">{{ t("agriFile.confirmLeave") }}</div>
     </popup>
 </template>
 
@@ -52,10 +48,18 @@ const handleLeave = () => {
 <style scoped lang="scss">
 .leave-confirm-popup {
     width: 78%;
-    padding: 72px 20px 22px;
     text-align: center;
+    padding: 1px;
     overflow: visible;
-    background: linear-gradient(360deg, #FFFFFF 0%, #D3EBFF 100%);
+    background: linear-gradient(0deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.12)),
+        linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 12%);
+
+
+    .leave-confirm-popup__bg {
+        padding: 72px 20px 22px;
+        background: linear-gradient(180deg, #1A191D 0%, #121216 100%);
+        border-radius: 16px;
+    }
 
     &__icon {
         position: absolute;
@@ -68,27 +72,27 @@ const handleLeave = () => {
     }
 
     &__title {
-        font-size: 26px;
-        color: #010101;
+        font-size: 24px;
+        color: #fff;
         font-family: "pangmenzhengdao";
     }
 
     &__desc {
         margin-top: 8px;
-        color: #1F1F1F;
+        color: rgba(255, 255, 255, 0.6);
     }
 
     &__btn {
         margin: 20px auto 10px;
         padding: 8px;
-        border-radius: 25px;
-        background: linear-gradient(180deg, #96D1FF 0%, #2199F8 100%);
-        color: #fff;
+        border-radius: 8px;
+        background: linear-gradient(90deg, #FFEBC9 0%, #FFCA6D 100%);
+        color: #1A1408;
         font-size: 16px;
     }
 
     &__leave {
-        color: #AEAEAE;
+        color: rgba(255, 255, 255, 0.6);
     }
 }
 </style>

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

@@ -42,8 +42,9 @@
                 :key="item.id"
                 class="album-card"
                 :class="{ 'is-empty': !item.count }"
+                @click.stop="goAlbumDetail(item)"
             >
-                <div class="album-card__cover" @click.stop="goAlbumDetail(item)">
+                <div class="album-card__cover">
                     <img v-if="item.count && item.cover" :src="item.cover" alt="" />
                     <img
                         v-else

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

@@ -292,7 +292,7 @@ linear-gradient(180deg, rgba(255, 255, 255, 0.42) 0%, rgba(255, 255, 255, 0) 12%
         margin-bottom: 16px;
 
         :deep(.van-uploader__wrapper) {
-            gap: 8px;
+            gap: 6px;
         }
 
         :deep(.van-uploader__preview),

+ 254 - 233
src/views/old_mini/agri_file/components/patrolPanel.vue

@@ -20,12 +20,17 @@
                 <span>新增品种</span>
             </div>
         </div>
-        <div
-            v-for="item in patrolList"
-            :key="item.id"
-            class="tracking-item"
-            @click="goGrowthTrack(item)"
-        >
+        <div v-if="loading" class="patrol-loading">
+            <span class="patrol-loading__spinner"></span>
+            <span class="patrol-loading__text">巡园要点加载中...</span>
+        </div>
+        <template v-else>
+            <div
+                v-for="item in patrolList"
+                :key="item.id"
+                class="tracking-item"
+                @click="goGrowthTrack(item)"
+            >
             <div class="tracking-item__header">
                 <div class="tracking-item__header-left">
                     <img
@@ -37,7 +42,7 @@
                         {{ item.title }}
                     </span>
                 </div>
-                <div v-if="item.level" class="tracking-item__level">
+                <div v-if="item.level" class="tracking-item__level" :class="'level-'+item.level_num">
                     <el-icon><WarningFilled /></el-icon>
                     <span>{{ item.level }}</span>
                 </div>
@@ -79,6 +84,7 @@
                 </div>
             </div>
         </div>
+        </template>
         <div class="crop-archive">
             <!-- <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div> -->
             <PhenologyTrackTimelineItem theme="dark" :list="cropArchiveList" />
@@ -101,6 +107,7 @@ import { Icon } from "vant";
 import { ElMessage } from "element-plus";
 import { Camera, Clock, Plus, WarningFilled } from "@element-plus/icons-vue";
 import { useI18n } from "@/i18n";
+import { base_img_url2 } from "@/api/config";
 import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
 import albumUploadPopup from "./albumUploadPopup.vue";
 import tipPopup from "@/components/popup/tipPopup.vue";
@@ -118,12 +125,31 @@ 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 "";
+    const text = String(url).trim();
+    if (!text) return "";
+    if (/^https?:\/\//i.test(text)) return text;
+    return base_img_url2 + text.replace(/^\//, "");
+};
+
+/** 土壤根系感知占位卡片(接口无土壤任务时兜底展示) */
 const buildSoilPatrolMock = () => ({
     id: "soil-mock",
     type: "soil",
     theme: "blue",
-    level: "一级",
+    level: null,
     title: "土壤根系感知",
     subject: "连阴雨清沟排水",
     issue: "连阴雨泡得芽点发软,不清沟排水,连阴雨泡得芽点发软,不清沟排水,",
@@ -139,24 +165,31 @@ const buildSoilPatrolMock = () => ({
 const zoneList = ref([]);
 const selectedZoneId = ref(null);
 const selectedZoneName = ref("");
-const patrolList = ref([
-    {
-        id: 1,
-        type: "growth",
-        theme: "blue",
-        level: "",
-        title: "",
-        subject: "",
-        issue: "",
-        desc: "",
-        urgencyText: "最佳窗口 10/14 - 10/20",
-        urgencyTone: "success",
-        urgencyIcon: calendarUrgencyIcon,
-        headerIcon: periodIcon,
-        icon: PATROL_ICON,
-    },
-    buildSoilPatrolMock(),
-]);
+
+/** 物候卡片占位(接口无数据 / 请求失败时兜底展示) */
+const buildGrowthPatrolMock = () => ({
+    id: "growth-mock",
+    type: "growth",
+    theme: "blue",
+    level: null,
+    title: t("agriFile.patrolGrowthTitle"),
+    zone: selectedZoneName.value || t("agriFile.zoneOne"),
+    subject: t("agriFile.patrolSubject"),
+    issue: t("agriFile.patrolIssue"),
+    desc: "",
+    urgencyText: "最佳窗口 --",
+    urgencyTone: "success",
+    urgencyIcon: calendarUrgencyIcon,
+    headerIcon: periodIcon,
+    icon: PATROL_ICON,
+});
+
+/** 默认组成:物候 → 土壤;异常卡由接口数据决定,无数据不占位 */
+const buildDefaultPatrolList = () => [buildGrowthPatrolMock(), buildSoilPatrolMock()];
+
+const patrolList = ref(buildDefaultPatrolList());
+/** 巡园接口返回前的加载态 */
+const loading = ref(false);
 const cropArchiveList = ref([]);
 const showUploadPopup = ref(false);
 const showSuccessPopup = ref(false);
@@ -175,66 +208,135 @@ function formatToday() {
 const resolveCropType = (farm = getSelectedFarm()) =>
     farm.category_code || farm.categorycode || farm.crop_code || farm.crop_type || "";
 
-/** 外部感知任务 → 巡园卡片(仅取第一条) */
-const mapInteractTaskToPatrol = (task) => {
-    const content = task?.interact_content || {};
+/** 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";
+    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;
     return {
-        id: task.zone_id || task.interact_code,
-        type: "growth",
+        id: task?.task_key || `${type}:${zoneId}`,
+        type,
         theme: "blue",
-        level: content.interact_level || "",
-        title: t("agriFile.patrolGrowthTitle") || "",
-        desc: content.interact_reason || "",
-        issue: content.interact_reason || "",
-        subject: content.interact_title || "",
-        urgencyText: "最佳窗口 10/14 - 10/20",
-        urgencyTone: "success",
-        urgencyIcon: calendarUrgencyIcon,
-        headerIcon: periodIcon,
-        icon: PATROL_ICON,
-        zoneId: task.zone_id,
-        interactCode: task.interact_code,
+        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 || "",
+        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 || "",
+        zoneId,
+        interactCode: payload.interact_code || content.interact_code || "",
+        fwCode: payload.fw_code || "",
+        taskKey: task?.task_key || "",
+        status: task?.status || payload.interact_status || "",
         raw: task,
     };
 };
 
-/** 异常互动第一条 → 异常巡园卡片 */
-const mapAbnormalInteractToPatrol = (info, stressInfo) => {
-    const level = stressInfo?.level;
+/** 一次接口返回 → 巡园卡片:物候 / 异常各取首条,土壤置末尾 */
+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());
+    return list;
+};
+
+const mapPhenoRecordToArchive = (item, index, zoneName) => {
+    // 物候记录用 enter_date,异常记录只有 trigger_date
+    const date = item.enter_date || item.trigger_date || "";
     return {
-        id: 2,
-        type: "abnormal",
-        theme: "blue",
-        level: level != null && level !== "" ? `${level}级` : "",
-        title: t("agriFile.patrolAbnormalTitle") || "",
-        subject: info?.interaction_title || "",
-        issue: info?.interaction_theme || "",
-        urgencyText: "建议 24 小时内处理",
-        urgencyTone: "danger",
-        urgencyIcon: "",
-        headerIcon: lightningIcon,
-        icon: PATROL_ICON,
-        zone: selectedZoneName.value || "",
-        zoneId: selectedZoneId.value,
-        fwCode: info?.fw_code,
-        raw: info,
+        id: [item.zone_id, item.type, date, item.period_code || item.fw_code, index]
+            .filter((v) => v != null && v !== "")
+            .join("_"),
+        date,
+        zone_name: item.zone_name || zoneName || "",
+        content: item.sentence || (item.period_name ? `到达 ${item.period_name}` : ""),
+        period_code: item.period_code,
+        period_name: item.period_name,
+        shoot_type: item.shoot_type,
+        zone_id: item.zone_id,
+        crop_code: item.crop_code,
+        fw_code: item.fw_code,
+        type: item.type,
     };
 };
 
-const mapPhenoRecordToArchive = (item, index, zoneName) => ({
-    id: [item.zone_id, item.enter_date, item.period_code, index]
-        .filter((v) => v != null && v !== "")
-        .join("_"),
-    date: item.enter_date || "",
-    zone_name: item.zone_name || zoneName || "",
-    content: item.sentence || (item.period_name ? `到达 ${item.period_name}` : ""),
-    period_code: item.period_code,
-    period_name: item.period_name,
-    shoot_type: item.shoot_type,
-    zone_id: item.zone_id,
-    crop_code: item.crop_code,
-});
-
 const getSelectedFarm = () => {
     try {
         return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
@@ -253,47 +355,9 @@ const getSelectedFarmId = () => {
     return id != null && id !== "" ? Number(id) : null;
 };
 
-/** 清空第一条上的感知任务残留,回退为 mock 文案 */
-const clearFirstPatrolInteract = () => {
-    if (!patrolList.value.length) return;
-    const item = patrolList.value[0];
-    patrolList.value[0] = {
-        ...item,
-        id: 1,
-        type: "growth",
-        level: t("agriFile.riskLevel2"),
-        title: t("agriFile.patrolGrowthTitle"),
-        zone: selectedZoneName.value || t("agriFile.zoneOne"),
-        subject: t("agriFile.patrolSubject"),
-        issue: t("agriFile.patrolIssue"),
-        desc: "",
-        urgencyText: "最佳窗口 10/14 - 10/20",
-        urgencyTone: "success",
-        urgencyIcon: calendarUrgencyIcon,
-        headerIcon: periodIcon,
-        zoneId: undefined,
-        interactCode: undefined,
-        raw: undefined,
-    };
-};
-
-/** ab_it_info 为空对象时不展示异常卡片 */
-const isEmptyAbInfo = (abInfo) => {
-    if (!abInfo || typeof abInfo !== "object" || Array.isArray(abInfo)) return true;
-    return Object.keys(abInfo).length === 0;
-};
-
-/** 保证土壤卡片始终在列表末尾(第 3 项;暂写死,后续接接口) */
-const ensureSoilPatrolMock = () => {
-    const mock = buildSoilPatrolMock();
-    const rest = patrolList.value.filter((item) => item.type !== "soil");
-    patrolList.value = [...rest, mock];
-};
-
-/** 移除异常卡片(无数据时不占位),并保持土壤在末尾 */
-const clearAbnormalPatrol = () => {
-    patrolList.value = patrolList.value.filter((item) => item.type !== "abnormal");
-    ensureSoilPatrolMock();
+/** 切换农场 / 分区、请求失败时清空接口数据,回退为占位卡片 */
+const resetPatrolList = () => {
+    patrolList.value = buildDefaultPatrolList();
 };
 
 // ---------- 业务逻辑:加载 ----------
@@ -323,93 +387,35 @@ const fetchZones = async () => {
     }
 };
 
-const fetchPatrolInteractTask = async () => {
-    const zoneId = selectedZoneId.value;
-    if (zoneId == null || zoneId === "") {
-        clearFirstPatrolInteract();
-        return;
-    }
-    try {
-        const res = await VE_API.questionnaire.getExternalInteractTasks({ zone_id: zoneId });
-        const tasks = res?.data?.interact_tasks;
-        const first = Array.isArray(tasks) ? tasks[0] : null;
-        if (!first) {
-            clearFirstPatrolInteract();
-            return;
-        }
-        const mapped = mapInteractTaskToPatrol(first);
-        if (patrolList.value.length) {
-            patrolList.value[0] = {
-                ...patrolList.value[0],
-                ...mapped,
-            };
-        } else {
-            patrolList.value = [mapped];
-        }
-    } catch (e) {
-        console.warn("[patrolPanel] getExternalInteractTasks failed", e);
-    }
-};
-
-/** 异常互动 → 第二张巡园卡片(默认 interact_Info 第一项;ab_it_info 为空则不展示) */
-const fetchAbnormalInteract = async () => {
+/** 首页巡园要点:物候 / 异常 / 土壤由 getTypesTask 一次返回 */
+const fetchTypesTask = async () => {
     const farm = getSelectedFarm();
     const farmId = getSelectedFarmId();
     const zoneId = selectedZoneId.value;
     const cropType = resolveCropType(farm);
     if (farmId == null || zoneId == null || zoneId === "" || !cropType) {
-        clearAbnormalPatrol();
+        resetPatrolList();
         return;
     }
+    loading.value = true;
     try {
-        const res = await VE_API.questionnaire.getAbnormalInteract({
+        const res = await VE_API.pheno.getTypesTask({
             crop_type: cropType,
             farm_id: farmId,
             zone_id: zoneId,
             calc_date: formatToday(),
-            // farm_id: 321,
-            // zone_id: 46,
-            // calc_date: '2025-08-30',
         });
         if (res?.code !== 200) {
-            clearAbnormalPatrol();
+            resetPatrolList();
+            loading.value = false;
             return;
         }
-        const data = res?.data || {};
-        const abInfo = data.ab_it_info;
-        // ab_it_info 为 {} / 空 / 无 interact_Info 时不展示异常卡片
-        if (isEmptyAbInfo(abInfo)) {
-            clearAbnormalPatrol();
-            return;
-        }
-        const list = Array.isArray(abInfo.interact_Info) ? abInfo.interact_Info : [];
-        const first = list[0];
-        if (!first) {
-            clearAbnormalPatrol();
-            return;
-        }
-        const stressInfo = abInfo.stress_info || data.stress_info || {};
-        const mapped = mapAbnormalInteractToPatrol(first, stressInfo);
-        const idx = patrolList.value.findIndex((item) => item.type === "abnormal");
-        if (idx >= 0) {
-            patrolList.value[idx] = {
-                ...patrolList.value[idx],
-                ...mapped,
-            };
-        } else {
-            // 插在土壤之前,保证顺序:长势 → 异常 → 土壤
-            const soilIdx = patrolList.value.findIndex((item) => item.type === "soil");
-            if (soilIdx >= 0) {
-                patrolList.value.splice(soilIdx, 0, mapped);
-            } else {
-                patrolList.value.push(mapped);
-            }
-        }
-        ensureSoilPatrolMock();
+        patrolList.value = buildPatrolList(res?.data || {});
+        loading.value = false;
     } catch (e) {
-        console.warn("[patrolPanel] getAbnormalInteract failed", e);
-        clearAbnormalPatrol();
-        ensureSoilPatrolMock();
+        console.warn("[patrolPanel] getTypesTask failed", e);
+        resetPatrolList();
+        loading.value = false;
     }
 };
 
@@ -442,50 +448,18 @@ const fetchCropArchive = async () => {
     }
 };
 
-const fillMockLabels = () => {
-    const albumName = selectedZoneName.value || t("agriFile.zoneOne");
-    patrolList.value = patrolList.value.map((item, index) => {
-        // 土壤卡片写死占位,不走通用 mock
-        if (item.type === "soil") return { ...buildSoilPatrolMock(), zone: albumName };
-        // 第一条由外部感知任务接口填充,不覆盖
-        if (index === 0 && item.interactCode) return item;
-        // 异常卡片由 getAbnormalInteract 填充,不覆盖
-        if (item.type === "abnormal" && item.fwCode) return item;
-        const isGrowth = item.type === "growth" || index === 0;
-        return {
-            ...item,
-            level: t("agriFile.riskLevel2"),
-            title: t(isGrowth ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
-            zone: albumName,
-            subject: t("agriFile.patrolSubject"),
-            issue: t("agriFile.patrolIssue"),
-            urgencyText: isGrowth ? "最佳窗口 10/14 - 10/20" : "建议 24 小时内处理",
-            urgencyTone: isGrowth ? "success" : "danger",
-            urgencyIcon: isGrowth ? calendarUrgencyIcon : "",
-            headerIcon: isGrowth ? periodIcon : lightningIcon,
-        };
-    });
-    ensureSoilPatrolMock();
-};
-
 /** 按当前选中品种刷新巡园卡片与作物档案 */
 const reloadBySelectedZone = () => {
-    clearFirstPatrolInteract();
-    clearAbnormalPatrol();
-    fillMockLabels();
-    fetchPatrolInteractTask();
-    fetchAbnormalInteract();
+    resetPatrolList();
+    fetchTypesTask();
     fetchCropArchive();
 };
 
 const loadPatrolData = async () => {
-    // 先清掉上一农场感知任务,避免 fillMockLabels 因 interactCode 保留旧数据
-    clearFirstPatrolInteract();
-    clearAbnormalPatrol();
+    // 先清掉上一农场的任务数据,避免切换时残留
+    resetPatrolList();
     await fetchZones();
-    fillMockLabels();
-    fetchPatrolInteractTask();
-    fetchAbnormalInteract();
+    fetchTypesTask();
     fetchCropArchive();
 };
 
@@ -503,28 +477,33 @@ const goAddVariety = () => {
     });
 };
 
+/** 卡片 id 为任务标识,详情 / 上传 / 分享统一使用分区 id */
+const resolveCardZoneId = (item) => item?.zoneId ?? selectedZoneId.value ?? item?.id ?? "";
+
 const goGrowthTrack = (item) => {
     const isAbnormal = item?.type !== "growth";
+    const zoneId = resolveCardZoneId(item);
     router.push({
         path: isAbnormal ? "/abnormal_track" : "/growth_track",
         query: {
-            id: item?.id,
-            ...(item?.zoneId != null && item?.zoneId !== "" ? { zoneId: item.zoneId } : {}),
-            pageName: item.title || "详情"
+            id: zoneId,
+            ...(zoneId !== "" ? { zoneId } : {}),
+            pageName: item.title || "详情",
         },
     });
 };
 
 const handleShare = (item) => {
     const isAbnormal = item?.type === "abnormal";
+    const zoneId = resolveCardZoneId(item);
     const query = {
         askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
         shareText: item?.title || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
         targetUrl: isAbnormal ? "abnormal_track" : "growth_track",
         paramsPage: JSON.stringify({
-            id: item?.id,
+            id: zoneId,
             fromShare: 1,
-            ...(item?.zoneId != null && item?.zoneId !== "" ? { zoneId: item.zoneId } : {}),
+            ...(zoneId !== "" ? { zoneId } : {}),
             pageName: isAbnormal
                 ? t("agriFile.patrolAbnormalTitle")
                 : t("agriFile.patrolGrowthTitle"),
@@ -537,7 +516,7 @@ const handleShare = (item) => {
 };
 
 const handleUploadClick = (item) => {
-    uploadZoneId.value = item?.zoneId ?? item?.id ?? selectedZoneId.value ?? "";
+    uploadZoneId.value = resolveCardZoneId(item);
     showUploadPopup.value = true;
 };
 
@@ -576,8 +555,7 @@ onMounted(loadPatrolData);
 onActivated(loadPatrolData);
 
 defineExpose({
-    fetchPatrolInteractTask,
-    fetchAbnormalInteract,
+    fetchTypesTask,
     fetchCropArchive,
     loadPatrolData,
 });
@@ -652,6 +630,27 @@ defineExpose({
         }
     }
 
+    .patrol-loading {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        gap: 12px;
+        padding: 64px 0;
+        color: rgba(255, 255, 255, 0.6);
+        font-size: 13px;
+        letter-spacing: 1px;
+
+        &__spinner {
+            width: 28px;
+            height: 28px;
+            border-radius: 50%;
+            border: 3px solid rgba(255, 255, 255, 0.15);
+            border-top-color: #ffd489;
+            animation: patrol-spin 0.8s linear infinite;
+        }
+    }
+
     .tracking-item {
         width: 100%;
         margin-bottom: 12px;
@@ -709,6 +708,22 @@ defineExpose({
             font-size: 12px;
             box-sizing: border-box;
 
+            &.level-1 {
+                border-color: rgba(255, 212, 137, 0.41);
+                background-color: rgba(255, 212, 137, 0.16);
+                color: #FFD489;
+            }
+            &.level-2 {
+                border-color: rgba(255, 127, 20, 0.4);
+                background-color: rgba(255, 127, 20, 0.18);
+                color: #FF9B47;
+            }
+            &.level-3 {
+                border-color: rgba(126, 35, 33, 0.2);
+                background-color: rgba(126, 35, 33, 0.2);
+                color: #FF7D7B;
+            }
+
             .el-icon {
                 font-size: 12px;
             }
@@ -861,4 +876,10 @@ defineExpose({
         }
     }
 }
+
+@keyframes patrol-spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
 </style>

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

@@ -438,6 +438,7 @@ const handleSubmit = async (item, index) => {
         });
         if (res?.code === 200) {
             ElMessage.success(t("agriFile.submitSuccess"));
+            await fetchAbnormalInteract();
             return;
         }
         ElMessage.error(res?.msg || "提交失败,请稍后再试");

+ 6 - 7
src/views/old_mini/agri_file/pages/regionAlbums.vue

@@ -19,7 +19,7 @@
             </div>
         </div>
 
-        <div class="region-albums__panel" v-loading="loading">
+        <div class="region-albums__panel" v-loading="loading" element-loading-background="rgba(15, 15, 15, 0.55)">
             <div class="region-albums__panel-content">
                 <div v-if="!loading && isEmpty" class="region-albums__empty">
                     <img
@@ -319,12 +319,11 @@ onDeactivated(() => {
     &__hero-mask {
         position: absolute;
         inset: 0;
-        background: linear-gradient(
-            180deg,
-            rgba(0, 0, 0, 0.25) 0%,
-            rgba(0, 0, 0, 0.05) 45%,
-            rgba(0, 0, 0, 0.35) 100%
-        );
+        top: 30px;
+        height: 120px;
+        left: 0;
+        background: linear-gradient(180deg, rgba(0, 0, 0, 0) 6.97%, rgba(0, 0, 0, 0.35) 36.31%, rgba(0, 0, 0, 0.82) 72.61%);
+
 
         &.is-light {
             background: url('@/assets/img/agricultural/album-bg.png') no-repeat center center /100% 100%;

+ 0 - 781
src/views/old_mini/entry_information/components/manualFarmingService.vue

@@ -1,781 +0,0 @@
-<template>
-    <div class="manual-farming">
-        <div class="manual-farming__content">
-            <div class="page-header">
-                <div class="page-title has-tag-wrap">完善人工农事服务
-                    <span v-if="cropGroupLabel" class="page-tag">{{ cropGroupLabel }}</span>
-                </div>
-                <div class="page-title">让农事调度更高效<span class="page-title-tip">(可多选)</span></div>
-            </div>
-
-            <div class="toolbar">
-                <div class="search-bar">
-                    <el-icon class="search-icon"><Search /></el-icon>
-                    <input
-                        v-model="searchKeyword"
-                        class="search-input"
-                        type="text"
-                        placeholder="请输入农机名称"
-                        @keyup.enter="handleSearch"
-                    />
-                    <div class="search-btn" @click="handleSearch">搜索</div>
-                </div>
-                <div class="add-btn" @click="showAddPopup = true">+ 添加农事</div>
-            </div>
-
-            <div class="task-card" v-if="!isFruitView">
-                <div class="tag-group">
-                    <div
-                        v-for="item in displayFieldTasks"
-                        :key="item.id"
-                        class="tag-item"
-                        :class="{ selected: selectedIds.has(String(item.id)) }"
-                        @click="toggleSelect(item)"
-                    >
-                        <span class="text">{{ item.name }}</span>
-                    </div>
-                </div>
-                <div v-if="!displayFieldTasks.length" class="empty-tip">暂无匹配农事</div>
-            </div>
-
-            <div v-else class="fruit-list">
-                <div v-for="group in displayFruitGroups" :key="group.name" class="task-card fruit-card">
-                    <div class="section-title">
-                        <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
-                        <span>{{ group.name }}</span>
-                    </div>
-                    <div class="tag-group">
-                        <div
-                            v-for="item in group.items"
-                            :key="item.id"
-                            class="tag-item"
-                            :class="{ selected: selectedIds.has(String(item.id)) }"
-                            @click="toggleSelect(item)"
-                        >
-                            <span class="text">{{ item.name }}</span>
-                        </div>
-                    </div>
-                </div>
-                <div v-if="!displayFruitGroups.length" class="empty-tip">暂无匹配农事</div>
-            </div>
-        </div>
-
-        <div class="custom-bottom-fixed-btns">
-            <div class="bottom-btn secondary-btn" @click="handlePrevClick">上一步</div>
-            <div class="bottom-btn primary-btn" :class="{ disabled: submitting }" @click="handlePrimaryClick">
-                {{ primaryBtnText }}
-            </div>
-        </div>
-
-        <popup
-            v-model:show="showAddPopup"
-            round
-            closeable
-            class="add-task-popup"
-            :close-on-click-overlay="false"
-            teleport="body"
-        >
-            <div class="add-task-popup__content">
-                <div class="form-block__title">农事名称</div>
-                <el-input v-model="newTaskName" placeholder="请输入农事名称" />
-                <div class="add-task-popup__confirm" @click="handleAddConfirm">确认</div>
-            </div>
-        </popup>
-
-        <tip-popup
-            v-model:show="showSuccessPopup"
-            type="executeSuccess"
-            text="您的信息已提交成功"
-            buttonText="完成"
-            @confirm="handleComplete"
-        />
-    </div>
-</template>
-
-<script setup>
-import { computed, onMounted, reactive, ref } from "vue";
-import { ElMessage } from "element-plus";
-import { Search } from "@element-plus/icons-vue";
-import { Popup } from "vant";
-import tipPopup from "@/components/popup/tipPopup.vue";
-import { selectNewlyCreatedFarm } from "@/utils/selectedFarm";
-
-const TASK_KEY = "ENTRY_SELECTED_FARM_TASKS";
-const TASK_VIEW_KEY = "ENTRY_MANUAL_VIEW";
-const EQUIPMENT_KEY = "ENTRY_SELECTED_EQUIPMENT";
-const SELECTED_LIST_KEY = "ENTRY_SELECTED_VARIETY_LIST";
-const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
-const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
-const EDIT_UID_KEY = "ENTRY_VARIETY_EDIT_UID";
-const STEP_KEY = "ENTRY_INFORMATION_STEP";
-
-const emit = defineEmits(["prev", "next", "confirm"]);
-const props = defineProps({
-    hasField: { type: Boolean, default: false },
-    hasFruit: { type: Boolean, default: false },
-});
-
-const cropGroupLabel = computed(() => {
-    return isFruitView.value ? "果树类" : "大田类";
-})
-
-const FIELD_TASKS = [
-    { id: 1, name: "人工补苗" },
-    { id: 2, name: "人工授粉" },
-    { id: 3, name: "人工除草" },
-    { id: 4, name: "清沟修渠" },
-    { id: 5, name: "排水晒田" },
-    { id: 6, name: "甘蔗砍收" },
-    { id: 7, name: "棉花采摘" },
-];
-
-const FRUIT_GROUPS = [
-    {
-        name: "树体管理",
-        items: [
-            { id: 201, name: "整形修剪" },
-            { id: 202, name: "嫁接" },
-            { id: 203, name: "环割环剥" },
-            { id: 204, name: "枝梢管理" },
-        ],
-    },
-    {
-        name: "花果管理",
-        items: [
-            { id: 301, name: "疏花疏果" },
-            { id: 302, name: "果实套袋" },
-            { id: 303, name: "人工授粉" },
-            { id: 304, name: "果实增色" },
-        ],
-    },
-    {
-        name: "采收与园地管理",
-        items: [
-            { id: 401, name: "人工采摘" },
-            { id: 402, name: "清园" },
-            { id: 403, name: "清沟修渠" },
-            { id: 404, name: "摇花抖水" },
-            { id: 405, name: "葡萄绑蔓/夏剪" },
-            { id: 406, name: "防寒保暖" },
-        ],
-    },
-    {
-        name: "其他人工服务",
-        items: [
-            { id: 501, name: "补植/定植" },
-            { id: 502, name: "行间中耕松土" },
-        ],
-    },
-];
-
-const searchKeyword = ref("");
-const appliedKeyword = ref("");
-const selectedIds = reactive(new Set());
-const fieldTaskList = ref(FIELD_TASKS.map((item) => ({ ...item })));
-const fruitGroups = ref(FRUIT_GROUPS.map((group) => ({
-    name: group.name,
-    items: group.items.map((item) => ({ ...item })),
-})));
-const showAddPopup = ref(false);
-const newTaskName = ref("");
-const submitting = ref(false);
-const showSuccessPopup = ref(false);
-const currentView = ref("field");
-let customTaskId = 900;
-
-const isFruitView = computed(() => currentView.value === "fruit");
-const needFruitAfterField = computed(() => props.hasField && props.hasFruit);
-const isLastTaskView = computed(() => !props.hasFruit);
-const primaryBtnText = computed(() => {
-    if (submitting.value) return "提交中...";
-    if (isLastTaskView.value) return "提交信息";
-    if (needFruitAfterField.value) {
-        return isFruitView.value ? "下一步 (5/6)" : "下一步 (4/6)";
-    }
-    return "下一步 (3/4)";
-});
-
-const displayFieldTasks = computed(() => {
-    const keyword = (appliedKeyword.value || "").trim();
-    if (!keyword) return fieldTaskList.value;
-    return fieldTaskList.value.filter((item) => item.name.includes(keyword));
-});
-
-const displayFruitGroups = computed(() => {
-    const keyword = (appliedKeyword.value || "").trim();
-    const groups = fruitGroups.value.map((group) => ({
-        ...group,
-        items: keyword ? group.items.filter((item) => item.name.includes(keyword)) : group.items,
-    }));
-    return groups.filter((group) => group.items.length);
-});
-
-const allFruitTasks = computed(() => fruitGroups.value.flatMap((group) => group.items));
-
-function readJson(key) {
-    try {
-        const raw = sessionStorage.getItem(key);
-        return raw ? JSON.parse(raw) : null;
-    } catch {
-        return null;
-    }
-}
-
-function restoreSelection() {
-    const cached = readJson(TASK_KEY);
-    const list = Array.isArray(cached)
-        ? cached
-        : [...(cached?.field || []), ...(cached?.fruit || [])];
-    list.forEach((item) => {
-        if (item?.id == null) return;
-        selectedIds.add(String(item.id));
-        if (item.scope === "fruit" || Number(item.id) >= 200) {
-            const exists = allFruitTasks.value.some((row) => String(row.id) === String(item.id));
-            if (!exists) {
-                fruitGroups.value[fruitGroups.value.length - 1].items.push({
-                    id: item.id,
-                    name: item.name,
-                    custom: true,
-                });
-            }
-            return;
-        }
-        if (!fieldTaskList.value.some((row) => String(row.id) === String(item.id))) {
-            fieldTaskList.value.push({ id: item.id, name: item.name, custom: true });
-        }
-    });
-}
-
-function saveSelectionDraft() {
-    const field = fieldTaskList.value
-        .filter((item) => selectedIds.has(String(item.id)))
-        .map((item) => ({ ...item, scope: "field" }));
-    const fruit = allFruitTasks.value
-        .filter((item) => selectedIds.has(String(item.id)))
-        .map((item) => ({ ...item, scope: "fruit" }));
-    sessionStorage.setItem(TASK_KEY, JSON.stringify({ field, fruit }));
-}
-
-const handleSearch = () => {
-    appliedKeyword.value = searchKeyword.value.trim();
-};
-
-const toggleSelect = (item) => {
-    const id = String(item.id);
-    if (selectedIds.has(id)) selectedIds.delete(id);
-    else selectedIds.add(id);
-    saveSelectionDraft();
-};
-
-const handleAddConfirm = () => {
-    const name = String(newTaskName.value || "").trim();
-    if (!name) {
-        ElMessage.warning("请输入农事名称");
-        return;
-    }
-    const list = isFruitView.value ? allFruitTasks.value : fieldTaskList.value;
-    const exists = list.find((item) => item.name === name);
-    if (exists) {
-        selectedIds.add(String(exists.id));
-        saveSelectionDraft();
-        ElMessage.success("已选中该农事");
-        showAddPopup.value = false;
-        newTaskName.value = "";
-        return;
-    }
-    const newItem = { id: ++customTaskId, name, custom: true };
-    if (isFruitView.value) {
-        fruitGroups.value[fruitGroups.value.length - 1].items.push(newItem);
-    } else {
-        fieldTaskList.value.push(newItem);
-    }
-    selectedIds.add(String(newItem.id));
-    saveSelectionDraft();
-    showAddPopup.value = false;
-    newTaskName.value = "";
-    ElMessage.success("添加成功");
-};
-
-function getVarietyDraftList() {
-    const draft = readJson(SELECTED_LIST_KEY);
-    if (Array.isArray(draft)) return draft;
-    if (Array.isArray(draft?.list)) return draft.list;
-    return [];
-}
-
-function getUserId() {
-    const id = localStorage.getItem("MINI_USER_ID");
-    return id != null && id !== "" ? Number(id) : 0;
-}
-
-/** 邀请录入时为邀请人 id,自主录入时与 user_id 相同 */
-function getAdminId() {
-    const fromSession = sessionStorage.getItem("ENTRY_ADMIN_ID");
-    if (fromSession != null && fromSession !== "") return Number(fromSession);
-    return getUserId();
-}
-
-function buildFarmPayload() {
-    const baForm = readJson("ENTRY_BA_FORM") || {};
-    const varietyList = getVarietyDraftList().filter((item) => item?.id != null && item.id !== "");
-    const equipmentCache = readJson(EQUIPMENT_KEY);
-    const equipmentList = Array.isArray(equipmentCache)
-        ? equipmentCache
-        : [...(equipmentCache?.field || []), ...(equipmentCache?.fruit || [])];
-    const categories = readJson(CATEGORY_SESSION_KEY) || [];
-    const location = readJson(LOCATION_KEY);
-    const cropDetailMap = readJson("ENTRY_CROP_DETAIL") || {};
-    const taskCache = readJson(TASK_KEY);
-    const taskList = Array.isArray(taskCache)
-        ? taskCache
-        : [...(taskCache?.field || []), ...(taskCache?.fruit || [])];
-    const farmTaskId = Number(taskList[0]?.id);
-
-    const primaryCropId = varietyList[0]?.categoryId ?? categories[0]?.id;
-    const primaryDetail = cropDetailMap[String(primaryCropId)] || {};
-
-    const cropBigType =
-        primaryDetail.crop_group ||
-        varietyList.find((item) => item.firstCrop)?.firstCrop ||
-        categories.find((item) => item.firstCrop)?.firstCrop ||
-        (isFruitView.value ? "果树" : "大田");
-
-    const farmLocation =
-        location?.point ||
-        varietyList.find((item) => item.location)?.location ||
-        "";
-
-    const payload = {
-        user_name: baForm.name,
-        tel: baForm.phone,
-        admin_id: getAdminId(),
-        user_id: getUserId(),
-        machine_id: equipmentList
-            .map((item) => Number(item.id))
-            .filter((id) => Number.isFinite(id)),
-        farm_location: farmLocation,
-        farm_address: location?.address || "",
-        crop_big_type: cropBigType,
-        crops: varietyList.map((item) => {
-            const detail = cropDetailMap[String(item.categoryId)] || {};
-            return {
-                crop_type: detail.crop_name || item.categoryName,
-                crop_code: detail.crop_code || "",
-                crop_id: Number(item.categoryId),
-                variety_list: String(item.varietyCode || item.id),
-                phenophase_code: String(item.phenophaseCode || item.phenologyId || ""),
-            };
-        }),
-    };
-
-    if (Number.isFinite(farmTaskId)) {
-        payload.farm_task_id = farmTaskId;
-    }
-
-    return payload;
-}
-
-/** 点击「完成」时用于初始化报告的参数(提交成功时缓存,清空 session 前) */
-const pendingReportParams = ref(null);
-
-/** 从当前 session 组装 report/generate 参数 */
-function buildReportParams() {
-    const baForm = readJson("ENTRY_BA_FORM") || {};
-    const varietyList = getVarietyDraftList().filter((item) => item?.id != null && item.id !== "");
-    const cropDetailMap = readJson("ENTRY_CROP_DETAIL") || {};
-    const meta = readJson("ENTRY_CROP_META") || {};
-    const first = varietyList[0];
-    const detail = first ? cropDetailMap[String(first.categoryId)] || {} : {};
-
-    return {
-        region: meta.city || meta.county || "",
-        crop: detail.crop_name || first?.categoryName || "",
-        variety: first?.name || "",
-        tel: baForm.phone || "",
-    };
-}
-
-/** 跳转长势报告前生成报告 */
-async function generateGrowthReport(params) {
-    if (!params?.region || !params?.crop || !params?.variety || !params?.tel) {
-        console.warn("generateReport skipped: missing params", params);
-        return;
-    }
-
-    try {
-        const raw = await VE_API.report.generateReport(params);
-        const res = Array.isArray(raw) ? raw[0] : raw;
-        if (res?.code !== 200) {
-            console.warn("generateReport failed", res?.msg || res?.message || res);
-        }
-    } catch (error) {
-        console.error("generateReport error", error);
-    }
-}
-
-function clearEntrySession() {
-    sessionStorage.removeItem(SELECTED_LIST_KEY);
-    sessionStorage.removeItem(CATEGORY_SESSION_KEY);
-    sessionStorage.removeItem("ENTRY_BA_FORM");
-    sessionStorage.removeItem(LOCATION_KEY);
-    sessionStorage.removeItem(EDIT_UID_KEY);
-    sessionStorage.removeItem(EQUIPMENT_KEY);
-    sessionStorage.removeItem(TASK_KEY);
-    sessionStorage.removeItem(TASK_VIEW_KEY);
-    sessionStorage.removeItem(STEP_KEY);
-    sessionStorage.removeItem("ENTRY_INVITE_TYPE");
-    sessionStorage.removeItem("ENTRY_ADMIN_ID");
-    sessionStorage.removeItem("ENTRY_CROP_DETAIL");
-    sessionStorage.removeItem("ENTRY_CROP_META");
-}
-
-const handleSubmit = async () => {
-    if (submitting.value) return;
-    const baForm = readJson("ENTRY_BA_FORM");
-    if (!baForm?.name || !baForm?.phone) {
-        ElMessage.warning("请先完善个人信息");
-        return;
-    }
-    submitting.value = true;
-    try {
-        const params = buildFarmPayload();
-        const raw = await VE_API.entry.addFarmInfo(params);
-        // 部分接口返回 [body, httpStatus]
-        const res = Array.isArray(raw) ? raw[0] : raw;
-        if (res?.code === 200) {
-            pendingReportParams.value = buildReportParams();
-            // 新增成功后拉列表,选中最后一项
-            await selectNewlyCreatedFarm();
-            clearEntrySession();
-            showSuccessPopup.value = true;
-        } else {
-            ElMessage.error(res?.msg || res?.message || "提交失败,请稍后再试");
-        }
-    } catch (error) {
-        console.error("entry submit failed", error);
-        ElMessage.error("提交失败");
-    } finally {
-        submitting.value = false;
-    }
-};
-
-const handleComplete = () => {
-    // 只触发初始化,不等待结果;新增已成功即可跳转
-    generateGrowthReport(pendingReportParams.value);
-    pendingReportParams.value = null;
-    showSuccessPopup.value = false;
-    emit("confirm");
-};
-
-const handlePrevClick = () => {
-    if (isFruitView.value && props.hasField) {
-        currentView.value = "field";
-        sessionStorage.setItem(TASK_VIEW_KEY, "field");
-        searchKeyword.value = "";
-        appliedKeyword.value = "";
-        return;
-    }
-    sessionStorage.removeItem(TASK_VIEW_KEY);
-    emit("prev");
-};
-
-const handlePrimaryClick = () => {
-    saveSelectionDraft();
-    if (!isLastTaskView.value) {
-        if (needFruitAfterField.value && !isFruitView.value) {
-            currentView.value = "fruit";
-            sessionStorage.setItem(TASK_VIEW_KEY, "fruit");
-            searchKeyword.value = "";
-            appliedKeyword.value = "";
-            return;
-        }
-        sessionStorage.setItem(TASK_VIEW_KEY, currentView.value);
-        emit("next");
-        return;
-    }
-    handleSubmit();
-};
-
-onMounted(() => {
-    const savedView = sessionStorage.getItem(TASK_VIEW_KEY);
-    if (savedView === "fruit" && props.hasFruit) {
-        currentView.value = "fruit";
-    } else {
-        currentView.value = props.hasField ? "field" : "fruit";
-    }
-    restoreSelection();
-});
-</script>
-
-<style lang="scss" scoped>
-.manual-farming {
-    flex: 1;
-    min-height: 0;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-
-    &__content {
-        flex: 1;
-        min-height: 0;
-        overflow-y: auto;
-        -webkit-overflow-scrolling: touch;
-        padding: 8px 16px 80px;
-    }
-
-    .page-header {
-        padding: 8px 4px 16px;
-
-        .page-title {
-            font-size: 26px;
-            color: #005599;
-            font-family: "PangMenZhengDao";
-            line-height: 30px;
-            
-            &.has-tag-wrap {
-                display: flex;
-                align-items: center;
-                gap: 6px;
-            }
-            .page-tag {
-                height: 24px;
-                line-height: 24px;
-                display: inline-block;
-                background: #2199F8;
-                color: #FFFFFF;
-                font-size: 14px;
-                padding: 0 6px;
-                border-radius: 4px;
-            }
-        }
-
-        .page-subtitle {
-            margin-top: 4px;
-            font-size: 16px;
-            font-weight: 600;
-            color: #005599;
-            line-height: 22px;
-        }
-        .page-title-tip {
-            font-size: 20px;
-        }
-    }
-
-    .toolbar {
-        display: flex;
-        align-items: center;
-        gap: 10px;
-        margin-bottom: 12px;
-    }
-
-    .search-bar {
-        flex: 1;
-        min-width: 0;
-        display: flex;
-        align-items: center;
-        height: 36px;
-        padding: 0 12px;
-        background: rgba(255, 255, 255, 0.5);
-        border: 1px solid rgba(33, 153, 248, 0.5);
-        border-radius: 6px;
-        box-sizing: border-box;
-
-        .search-icon {
-            color: rgba(0, 0, 0, 0.35);
-            font-size: 16px;
-            margin-right: 6px;
-            flex-shrink: 0;
-        }
-
-        .search-input {
-            flex: 1;
-            min-width: 0;
-            border: none;
-            outline: none;
-            background: transparent;
-            font-size: 14px;
-            color: #333;
-
-            &::placeholder {
-                color: rgba(0, 0, 0, 0.3);
-            }
-        }
-
-        .search-btn {
-            flex-shrink: 0;
-            padding-left: 10px;
-            color: #2199f8;
-            font-size: 14px;
-        }
-    }
-
-    .add-btn {
-        flex-shrink: 0;
-        height: 36px;
-        padding: 0 12px;
-        border-radius: 6px;
-        background: #2199f8;
-        color: #fff;
-        font-size: 14px;
-        line-height: 36px;
-        white-space: nowrap;
-    }
-
-    .task-card {
-        background: #fff;
-        border-radius: 12px;
-        padding: 14px 12px 16px;
-    }
-
-    .fruit-card {
-        margin-bottom: 12px;
-    }
-
-    .section-title {
-        display: flex;
-        align-items: center;
-        gap: 8px;
-        margin-bottom: 4px;
-        font-size: 16px;
-        font-weight: 600;
-        color: #1a1a1a;
-
-        .title-icon {
-            width: 15px;
-            height: 15px;
-            object-fit: contain;
-            flex-shrink: 0;
-        }
-    }
-
-    .tag-group {
-        display: grid;
-        grid-template-columns: repeat(3, 1fr);
-        gap: 0 8px;
-    }
-
-    .tag-item {
-        margin-top: 10px;
-        position: relative;
-        border-radius: 6px;
-        box-sizing: border-box;
-        min-height: 36px;
-        padding: 6px 4px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        text-align: center;
-        color: #000;
-        background: #f3f4f6;
-        border: 1px solid transparent;
-
-        .text {
-            font-size: 13px;
-            line-height: 16px;
-            padding: 0 4px;
-        }
-
-        &.selected {
-            border: 1px solid #2199f8;
-            background: #e8f5ff;
-            color: #2199f8;
-
-            &::after {
-                content: "";
-                position: absolute;
-                z-index: 9;
-                top: -1px;
-                right: -1px;
-                width: 18px;
-                height: 14px;
-                background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
-            }
-        }
-    }
-
-    .empty-tip {
-        padding: 40px 0;
-        text-align: center;
-        color: rgba(0, 0, 0, 0.35);
-        font-size: 14px;
-    }
-
-    .custom-bottom-fixed-btns {
-        display: flex;
-        justify-content: space-between;
-        align-items: baseline;
-        gap: 10px;
-        padding: 12px 12px 0;
-        height: 80px;
-        background: #fff;
-        box-sizing: border-box;
-        box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
-
-        .bottom-btn {
-            padding: 0 30px;
-            height: 40px;
-            line-height: 40px;
-            font-size: 14px;
-            border-radius: 25px;
-            text-align: center;
-        }
-
-        .secondary-btn {
-            color: #2199f8;
-            background: #fff;
-            border: 1px solid #2199f8;
-        }
-
-        .primary-btn {
-            background: #2199f8;
-            color: #fff;
-
-            &.disabled {
-                opacity: 0.6;
-            }
-        }
-    }
-}
-</style>
-
-<style lang="scss">
-.add-task-popup {
-    width: 330px;
-
-    .van-popup__close-icon {
-        color: #333;
-        font-size: 18px;
-    }
-
-    &__content {
-        padding: 24px 16px 20px;
-        background: linear-gradient(360deg, #ffffff 74.2%, #d1ebff 100%);
-        border-radius: 16px;
-        box-sizing: border-box;
-    }
-
-    .form-block__title {
-        margin-bottom: 10px;
-        font-size: 16px;
-        font-weight: 500;
-        color: #000;
-    }
-
-    .el-input__wrapper {
-        min-height: 40px;
-        border-radius: 4px;
-        box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) inset;
-        background: #fff;
-    }
-
-    &__confirm {
-        margin-top: 24px;
-        padding: 10px;
-        border-radius: 25px;
-        background: #2199f8;
-        color: #fff;
-        font-size: 16px;
-        text-align: center;
-    }
-}
-</style>

+ 0 - 5
src/views/old_mini/entry_information/components/selectCategory.vue

@@ -409,8 +409,6 @@ function buildVarietyPayload(crop, variety) {
         categoryName: crop.name,
         cropCode: crop.code || "",
         firstCrop: crop.firstCrop || "",
-        phenologyId: "",
-        phenophaseCode: "",
         startTime: "",
         area: "",
         location: getResidentPoint(),
@@ -656,10 +654,8 @@ function buildFarmPayload(crop, variety) {
 
     return {
         user_name: baForm.farmName || baForm.name,
-        tel: baForm.phone || "13000000000",
         admin_id: getAdminId(),
         user_id: getUserId(),
-        machine_id: [],
         farm_location: farmLocation,
         farm_address: location?.address || "",
         crop_big_type: detail.crop_group || crop.firstCrop || "",
@@ -669,7 +665,6 @@ function buildFarmPayload(crop, variety) {
                 crop_code: detail.crop_code || crop.code || "",
                 crop_id: Number(crop.id),
                 variety_list: String(variety.varietyCode || variety.id),
-                phenophase_code: String(variety.phenophaseCode || variety.phenologyId || ""),
             },
         ],
     };

+ 0 - 980
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -1,980 +0,0 @@
-<template>
-    <div class="select-equipment" :class="{ 'is-service-equipment': isService }">
-        <div class="select-equipment__content">
-            <div class="page-header">
-                <div class="page-title has-tag-wrap">
-                    完善设备信息
-                    <span v-if="cropGroupLabel" class="page-tag">{{ cropGroupLabel }}</span>
-                </div>
-                <div class="page-title">让农机调度更精准<span class="page-tag-tip">(可多选)</span></div>
-            </div>
-
-            <div class="toolbar">
-                <div class="search-bar">
-                    <el-icon class="search-icon"><Search /></el-icon>
-                    <input
-                        v-model="searchKeyword"
-                        class="search-input"
-                        type="text"
-                        placeholder="请输入农机名称"
-                        @keyup.enter="handleSearch"
-                    />
-                    <div class="search-btn" @click="handleSearch">搜索</div>
-                </div>
-                <div class="add-btn" @click="handleAddMachine">+ 添加农机</div>
-            </div>
-
-            <div class="equipment-list" v-loading="loading">
-                <div v-for="group in displayGroups" :key="group.name" class="equipment-card">
-                    <div class="section-title">
-                        <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
-                        <span>{{ group.name }}</span>
-                    </div>
-                    <div v-if="group.desc" class="section-desc">{{ group.desc }}</div>
-                    <div class="tag-group">
-                        <div
-                            v-for="item in group.items"
-                            :key="item.id"
-                            class="tag-item"
-                            :class="{ selected: selectedIds.has(String(item.id)) }"
-                            @click="toggleSelect(item)"
-                        >
-                            <span class="text">{{ item.name }}</span>
-                        </div>
-                    </div>
-                </div>
-                <div v-if="!loading && !displayGroups.length" class="empty-tip">暂无匹配农机</div>
-            </div>
-        </div>
-
-        <div class="custom-bottom-fixed-btns">
-            <div class="btns-l">
-                <div class="bottom-btn secondary-btn" @click="emit('prev')">上一步</div>
-                <div
-                    v-if="isLastStep && !isService"
-                    class="bottom-btn secondary-btn"
-                    :class="{ disabled: submitting }"
-                    @click="handleSkip"
-                >
-                    跳过
-                </div>
-            </div>
-            <div class="bottom-btn primary-btn" :class="{ disabled: submitting }" @click="handleSubmit">
-                {{ primaryBtnText }}
-            </div>
-        </div>
-
-        <add-machine-popup
-            v-model:show="showAddPopup"
-            @confirm="handleAddConfirm"
-        />
-
-        <tip-popup
-            v-model:show="showSuccessPopup"
-            type="executeSuccess"
-            :text="isService ? '您的信息已提交成功' : '您的信息已上传'"
-            :text2="isService ? '' : '请等待诊断报告生成'"
-            buttonText="完成"
-            :close-on-click-overlay="false"
-            @confirm="handleComplete"
-        />
-
-    </div>
-</template>
-
-<script setup>
-import { computed, onMounted, reactive, ref } from "vue";
-import { ElMessage } from "element-plus";
-import { Search } from "@element-plus/icons-vue";
-import addMachinePopup from "./addMachinePopup.vue";
-import tipPopup from "@/components/popup/tipPopup.vue";
-import { selectNewlyCreatedFarm } from "@/utils/selectedFarm";
-
-// ---------------------------------------------------------------------------
-// Props / Emits
-// ---------------------------------------------------------------------------
-const emit = defineEmits(["prev", "next", "confirm"]);
-const props = defineProps({
-    /** 农服录入:大田/果树分步选设备;农户录入为 false */
-    isService: { type: Boolean, default: false },
-    /** 当前是否为录入流程最后一步(决定主按钮文案与是否可提交) */
-    isLastStep: { type: Boolean, default: true },
-    /** 农服场景下的设备范围:field | fruit | "" */
-    equipmentScope: { type: String, default: "" },
-    totalSteps: { type: Number, default: 4 },
-});
-
-// ---------------------------------------------------------------------------
-// Session 键名(与录入流程其他步骤共享)
-// ---------------------------------------------------------------------------
-const SESSION = {
-    EQUIPMENT: "ENTRY_SELECTED_EQUIPMENT",
-    TASK: "ENTRY_SELECTED_FARM_TASKS",
-    VARIETY: "ENTRY_SELECTED_VARIETY_LIST",
-    CATEGORY: "ENTRY_SELECTED_CATEGORY",
-    LOCATION: "ENTRY_RESIDENT_LOCATION",
-    EDIT_UID: "ENTRY_VARIETY_EDIT_UID",
-    STEP: "ENTRY_INFORMATION_STEP",
-    BA_FORM: "ENTRY_BA_FORM",
-    MANUAL_VIEW: "ENTRY_MANUAL_VIEW",
-    INVITE_TYPE: "ENTRY_INVITE_TYPE",
-    ADMIN_ID: "ENTRY_ADMIN_ID",
-    /** farmer_crop_detail 返回的品类详情,按 crop_id 缓存 */
-    CROP_DETAIL: "ENTRY_CROP_DETAIL",
-    /** crops 接口返回的城市等元信息 */
-    CROP_META: "ENTRY_CROP_META",
-};
-
-/** 用户手动添加的农机归入此分组,始终置顶 */
-const CUSTOM_GROUP_NAME = "我的农机";
-/** 自定义农机 ID 起始值,避免与接口 machine_id 冲突 */
-const CUSTOM_MACHINE_ID_START = 9000;
-
-// ---------------------------------------------------------------------------
-// 页面状态
-// ---------------------------------------------------------------------------
-const loading = ref(false);
-const submitting = ref(false);
-const showAddPopup = ref(false);
-const showSuccessPopup = ref(false);
-const searchKeyword = ref("");
-const appliedKeyword = ref("");
-/** 已选农机 ID 集合(Set 便于 O(1) 切换) */
-const selectedIds = reactive(new Set());
-/** 按农事阶段分组的农机列表(接口 + 自定义项) */
-const equipmentGroups = ref([]);
-/** 页头品类标签,如「大田类」 */
-const cropGroupLabel = ref("");
-let customMachineId = CUSTOM_MACHINE_ID_START;
-
-// ---------------------------------------------------------------------------
-// 计算属性
-// ---------------------------------------------------------------------------
-/** 按关键词过滤后的展示分组 */
-const displayGroups = computed(() => {
-    const keyword = appliedKeyword.value.trim();
-    if (!keyword) return equipmentGroups.value;
-    return equipmentGroups.value
-        .map((group) => ({
-            ...group,
-            items: group.items.filter((item) => item.name.includes(keyword)),
-        }))
-        .filter((group) => group.items.length);
-});
-
-/** 扁平化所有农机项,供选中态与草稿保存使用 */
-const allEquipmentItems = computed(() =>
-    equipmentGroups.value.flatMap((group) => group.items)
-);
-
-const primaryBtnText = computed(() => {
-    if (submitting.value) return "提交中...";
-    if (props.isLastStep) return "提交信息";
-    const total = props.isService ? props.totalSteps : props.totalSteps || 3;
-    return `下一步 (3/${total})`;
-});
-
-// ---------------------------------------------------------------------------
-// Session 读写
-// ---------------------------------------------------------------------------
-function readJson(key) {
-    try {
-        const raw = sessionStorage.getItem(key);
-        return raw ? JSON.parse(raw) : null;
-    } catch {
-        return null;
-    }
-}
-
-function writeJson(key, value) {
-    sessionStorage.setItem(key, JSON.stringify(value));
-}
-
-// ---------------------------------------------------------------------------
-// 品类与设备范围(农服分大田/果树两步)
-// ---------------------------------------------------------------------------
-function getSelectedCategories() {
-    const list = readJson(SESSION.CATEGORY);
-    return Array.isArray(list) ? list : [];
-}
-
-function isFieldCategory(item) {
-    return item?.firstCrop === "大田" || item?.majorKey === "field";
-}
-
-function isFruitCategory(item) {
-    return item?.firstCrop === "果树" || item?.majorKey === "fruit";
-}
-
-/** 当前步骤应对应的品类:农服按 scope 取大田/果树,农户取第一个 */
-function getCategoryForMachines() {
-    const categories = getSelectedCategories();
-    const scope = getCurrentScope();
-    if (scope === "fruit") return categories.find(isFruitCategory) ?? null;
-    if (scope === "field") return categories.find(isFieldCategory) ?? null;
-    return categories[0] ?? null;
-}
-
-function getCurrentScope() {
-    if (props.equipmentScope === "fruit") return "fruit";
-    if (props.equipmentScope === "field") return "field";
-    return "";
-}
-
-// ---------------------------------------------------------------------------
-// 农机数据结构转换
-// ---------------------------------------------------------------------------
-/** 接口 machines(按 stage 分组)→ 页面分组结构 */
-function mapMachinesToGroups(machines) {
-    if (!Array.isArray(machines)) return [];
-    return machines
-        .map((group) => ({
-            name: group.stage || "",
-            desc: "",
-            items: (group.items || []).map((item) => ({
-                id: item.machine_id,
-                name: item.display_name || item.machine_name || "",
-                machineName: item.machine_name || "",
-                isCropSpecific: !!item.is_crop_specific,
-            })),
-        }))
-        .filter((group) => group.name && group.items.length);
-}
-
-/** 农服缓存为 { field, fruit };农户为数组 */
-function isScopedEquipmentCache(cached) {
-    return cached && !Array.isArray(cached) && (cached.field || cached.fruit);
-}
-
-function flattenEquipment(cached) {
-    if (!cached) return [];
-    if (Array.isArray(cached)) return cached;
-    return [...(cached.field || []), ...(cached.fruit || [])];
-}
-
-function getScopeEquipmentList(cached, scope) {
-    if (!cached) return [];
-    if (Array.isArray(cached)) return scope === "fruit" ? [] : cached;
-    return cached[scope] || [];
-}
-
-function findGroupByItemId(itemId) {
-    return equipmentGroups.value.find((group) =>
-        group.items.some((row) => String(row.id) === String(itemId))
-    );
-}
-
-function updateCropGroupLabel(category, apiCropGroup) {
-    if (apiCropGroup) {
-        cropGroupLabel.value = `${apiCropGroup}类`;
-        return;
-    }
-    cropGroupLabel.value =
-        category?.majorLabel ||
-        (category?.firstCrop ? `${category.firstCrop}类` : "");
-}
-
-/** 缓存 farmer_crop_detail 品类信息,供提交时取 crop_code / crop_group */
-function saveCropDetail(data) {
-    if (data?.crop_id == null || data?.crop_id === "") return;
-    const cache = readJson(SESSION.CROP_DETAIL) || {};
-    cache[String(data.crop_id)] = {
-        crop_id: data.crop_id,
-        crop_code: data.crop_code || "",
-        crop_group: data.crop_group || "",
-        crop_name: data.crop_name || "",
-        crop_type: data.crop_type || "",
-    };
-    writeJson(SESSION.CROP_DETAIL, cache);
-}
-
-function getCropDetailMap() {
-    const cache = readJson(SESSION.CROP_DETAIL);
-    return cache && typeof cache === "object" ? cache : {};
-}
-
-// ---------------------------------------------------------------------------
-// 选中态:恢复 / 持久化
-// ---------------------------------------------------------------------------
-/** 从 session 恢复当前 scope 下的选中项;自定义项若不在列表中则补回分组 */
-function restoreSelection() {
-    const cached = readJson(SESSION.EQUIPMENT);
-    const scope = getCurrentScope();
-    const list = scope ? getScopeEquipmentList(cached, scope) : flattenEquipment(cached);
-
-    list.forEach((item) => {
-        if (item?.id == null) return;
-        selectedIds.add(String(item.id));
-
-        if (!item.custom) return;
-        const exists = allEquipmentItems.value.some(
-            (row) => String(row.id) === String(item.id)
-        );
-        if (exists) return;
-
-        const group =
-            equipmentGroups.value.find((row) => row.name === item.groupName) ||
-            equipmentGroups.value.at(-1);
-        group?.items.push({ id: item.id, name: item.name, custom: true });
-
-        if (Number(item.id) >= customMachineId) {
-            customMachineId = Number(item.id);
-        }
-    });
-}
-
-/**
- * 果树步骤:大田已选且当前果树列表中存在的设备(同 machine_id 或同名)默认勾选
- * 仅做加法,不取消果树侧已有选择
- */
-function applyFieldOverlapSelection() {
-    if (getCurrentScope() !== "fruit") return;
-
-    const fieldList = getScopeEquipmentList(readJson(SESSION.EQUIPMENT), "field");
-    if (!fieldList.length) return;
-
-    const fieldIds = new Set(
-        fieldList.filter((item) => item?.id != null).map((item) => String(item.id))
-    );
-    const fieldNames = new Set(
-        fieldList.map((item) => item?.name).filter(Boolean)
-    );
-
-    let changed = false;
-    allEquipmentItems.value.forEach((item) => {
-        const id = String(item.id);
-        if (selectedIds.has(id)) return;
-        const matched =
-            fieldIds.has(id) ||
-            (item.name && fieldNames.has(item.name)) ||
-            (item.machineName && fieldNames.has(item.machineName));
-        if (!matched) return;
-        selectedIds.add(id);
-        changed = true;
-    });
-
-    if (changed) saveSelectionDraft();
-}
-
-/** 将当前选中项写入 session;农服按 field/fruit 分桶存储 */
-function buildSelectedEquipmentPayload() {
-    const scope = getCurrentScope();
-    return allEquipmentItems.value
-        .filter((item) => selectedIds.has(String(item.id)))
-        .map((item) => ({
-            ...item,
-            scope: scope || "farmer",
-            groupName: findGroupByItemId(item.id)?.name,
-        }));
-}
-
-function saveSelectionDraft() {
-    const selected = buildSelectedEquipmentPayload();
-    const scope = getCurrentScope();
-
-    if (!props.isService) {
-        writeJson(SESSION.EQUIPMENT, selected);
-        return;
-    }
-
-    const cached = readJson(SESSION.EQUIPMENT);
-    const next = isScopedEquipmentCache(cached)
-        ? { field: cached.field || [], fruit: cached.fruit || [] }
-        : { field: Array.isArray(cached) ? cached : [], fruit: [] };
-
-    if (scope === "fruit") next.fruit = selected;
-    else next.field = selected;
-    writeJson(SESSION.EQUIPMENT, next);
-}
-
-// ---------------------------------------------------------------------------
-// 数据加载
-// ---------------------------------------------------------------------------
-async function fetchEquipmentList() {
-    const category = getCategoryForMachines();
-    const cropId = category?.id;
-    updateCropGroupLabel(category);
-
-    if (cropId == null || cropId === "") {
-        equipmentGroups.value = [];
-        ElMessage.warning("请先选择种植品类");
-        return;
-    }
-
-    loading.value = true;
-    try {
-        const res = await VE_API.entry.getMachines({ crop_id: cropId });
-        if (res.code === 200) {
-            const data = res.data || {};
-            saveCropDetail(data);
-            updateCropGroupLabel(category, data.crop_group);
-            equipmentGroups.value = mapMachinesToGroups(data.machines);
-        } else {
-            equipmentGroups.value = [];
-            ElMessage.error(res.msg || "获取农机列表失败");
-        }
-    } catch {
-        equipmentGroups.value = [];
-        ElMessage.error("获取农机列表失败,请稍后再试");
-    } finally {
-        loading.value = false;
-    }
-}
-
-// ---------------------------------------------------------------------------
-// 交互:搜索 / 选择 / 自定义农机
-// ---------------------------------------------------------------------------
-function handleSearch() {
-    appliedKeyword.value = searchKeyword.value.trim();
-}
-
-function handleAddMachine() {
-    showAddPopup.value = true;
-}
-
-function ensureCustomGroupTop() {
-    let customGroup = equipmentGroups.value.find((g) => g.name === CUSTOM_GROUP_NAME);
-    if (!customGroup) {
-        customGroup = { name: CUSTOM_GROUP_NAME, items: [] };
-        equipmentGroups.value.unshift(customGroup);
-        return customGroup;
-    }
-    const index = equipmentGroups.value.indexOf(customGroup);
-    if (index > 0) {
-        equipmentGroups.value.splice(index, 1);
-        equipmentGroups.value.unshift(customGroup);
-    }
-    return customGroup;
-}
-
-function handleAddConfirm({ name }) {
-    const machineName = String(name || "").trim();
-    if (!machineName) return;
-
-    const exists = allEquipmentItems.value.find((item) => item.name === machineName);
-    if (exists) {
-        selectedIds.add(String(exists.id));
-        saveSelectionDraft();
-        ElMessage.success("已选中该农机");
-        return;
-    }
-
-    const customGroup = ensureCustomGroupTop();
-    const newItem = { id: ++customMachineId, name: machineName, custom: true };
-    customGroup.items.push(newItem);
-    selectedIds.add(String(newItem.id));
-    saveSelectionDraft();
-    ElMessage.success("添加成功");
-}
-
-function toggleSelect(item) {
-    const id = String(item.id);
-    if (selectedIds.has(id)) selectedIds.delete(id);
-    else selectedIds.add(id);
-    saveSelectionDraft();
-}
-
-// ---------------------------------------------------------------------------
-// 提交:组装 payload / 清 session
-// ---------------------------------------------------------------------------
-function getVarietyDraftList() {
-    const draft = readJson(SESSION.VARIETY);
-    if (Array.isArray(draft)) return draft;
-    if (Array.isArray(draft?.list)) return draft.list;
-    return [];
-}
-
-function getUserId() {
-    const id = localStorage.getItem("MINI_USER_ID");
-    return id != null && id !== "" ? Number(id) : 0;
-}
-
-/** 邀请录入时为邀请人 id,自主录入时与 user_id 相同 */
-function getAdminId() {
-    const fromSession = sessionStorage.getItem(SESSION.ADMIN_ID);
-    if (fromSession != null && fromSession !== "") return Number(fromSession);
-    return getUserId();
-}
-
-/** 汇总各步骤 session 数据,供 addFarmInfo 提交 */
-function buildFarmPayload(includeEquipment = true) {
-    const baForm = readJson(SESSION.BA_FORM) || {};
-    const varietyList = getVarietyDraftList().filter((item) => item?.id != null && item.id !== "");
-    const equipmentCache = readJson(SESSION.EQUIPMENT);
-    const equipmentList = includeEquipment ? flattenEquipment(equipmentCache) : [];
-    const categories = readJson(SESSION.CATEGORY) || [];
-    const location = readJson(SESSION.LOCATION);
-    const cropDetailMap = getCropDetailMap();
-    const taskCache = readJson(SESSION.TASK);
-    const taskList = Array.isArray(taskCache)
-        ? taskCache
-        : [...(taskCache?.field || []), ...(taskCache?.fruit || [])];
-    const farmTaskId = Number(taskList[0]?.id);
-
-    const primaryCropId =
-        varietyList[0]?.categoryId ??
-        getCategoryForMachines()?.id ??
-        categories[0]?.id;
-    const primaryDetail = cropDetailMap[String(primaryCropId)] || {};
-
-    const cropBigType =
-        primaryDetail.crop_group ||
-        varietyList.find((item) => item.firstCrop)?.firstCrop ||
-        categories.find((item) => item.firstCrop)?.firstCrop ||
-        "";
-
-    const farmLocation =
-        location?.point ||
-        varietyList.find((item) => item.location)?.location ||
-        "";
-
-    const payload = {
-        user_name: baForm.farmName || baForm.name,
-        tel: baForm.phone || "",
-        // admin_id: 94494,
-        admin_id: getAdminId(),
-        user_id: getUserId(),
-        machine_id: equipmentList
-            .map((item) => Number(item.id))
-            .filter((id) => Number.isFinite(id)),
-        farm_location: farmLocation,
-        farm_address: location?.address || "",
-        crop_big_type: cropBigType,
-        crops: varietyList.map((item) => {
-            const detail = cropDetailMap[String(item.categoryId)] || {};
-            return {
-                crop_type: detail.crop_name || item.categoryName,
-                crop_code: detail.crop_code || "",
-                crop_id: Number(item.categoryId),
-                variety_list: String(item.varietyCode || item.id),
-                phenophase_code: String(item.phenophaseCode || item.phenologyId || ""),
-            };
-        }),
-    };
-
-    if (Number.isFinite(farmTaskId)) {
-        payload.farm_task_id = farmTaskId;
-    }
-
-    return payload;
-}
-
-/** 点击「完成」时用于初始化报告的参数(提交成功时缓存,清空 session 前) */
-const pendingReportParams = ref(null);
-
-/** 从当前 session 组装 report/generate 参数 */
-function buildReportParams() {
-    const baForm = readJson(SESSION.BA_FORM) || {};
-    const varietyList = getVarietyDraftList().filter((item) => item?.id != null && item.id !== "");
-    const cropDetailMap = getCropDetailMap();
-    const meta = readJson(SESSION.CROP_META) || {};
-    const first = varietyList[0];
-    const detail = first ? cropDetailMap[String(first.categoryId)] || {} : {};
-
-    return {
-        region: meta.city || meta.county || "",
-        crop: detail.crop_name || first?.categoryName || "",
-        variety: first?.name || "",
-        tel: baForm.phone || "",
-        farmName: baForm.farmName || baForm.name || "",
-    };
-}
-
-/** 从新增接口返回体解析 farm_id / zone_id(兼容 zone_ids 数组等结构) */
-function pickFarmZoneIds(data) {
-    if (!data || typeof data !== "object") return null;
-    const farmId =
-        data.farm_id ??
-        data.farmId ??
-        data.farm?.farm_id ??
-        data.farm?.id ??
-        data.id;
-    // add_farm_info 实际返回 zone_ids: [1369],不是 zone_id
-    const zoneId =
-        data.zone_id ??
-        data.zoneId ??
-        data.zone_ids?.[0] ??
-        data.zoneIds?.[0] ??
-        data.zone?.zone_id ??
-        data.zone?.id ??
-        data.default_zone_id ??
-        data.zones?.[0]?.zone_id ??
-        data.zones?.[0]?.id;
-    if (farmId == null || farmId === "" || zoneId == null || zoneId === "") return null;
-    return { farm_id: farmId, zone_id: zoneId };
-}
-
-/** 新增成功后触发 generate_6module,仅用接口返回的 farm_id/zone_id,不等待、不落库 */
-function triggerReportApis(res) {
-    // 只认 addFarmInfo 返回值,禁止回退到旧的 selectedFarmData
-    const payload = res?.data && typeof res.data === "object" ? res.data : null;
-    const ids = pickFarmZoneIds(payload) || pickFarmZoneIds(res);
-    if (!ids) {
-        console.warn("generate_6module skipped: addFarmInfo 未返回 farm_id/zone_id", res);
-        return;
-    }
-    const reportParams = pendingReportParams.value || {};
-    const generateParams = {
-        farm_id: ids.farm_id,
-        zone_id: ids.zone_id,
-        region: reportParams.region || "",
-        crop: reportParams.crop || "",
-    };
-    VE_API.report.generate6Module(generateParams).catch(() => {});
-}
-
-/** 跳转长势报告前生成报告 */
-async function generateGrowthReport(params) {
-    if (!params?.region || !params?.crop || !params?.variety || !params?.tel) {
-        console.warn("generateReport skipped: missing params", params);
-        return;
-    }
-
-    try {
-        const raw = await VE_API.report.generateReport(params);
-        const res = Array.isArray(raw) ? raw[0] : raw;
-        if (res?.code !== 200) {
-            console.warn("generateReport failed", res?.msg || res?.message || res);
-        }
-    } catch (error) {
-        console.error("generateReport error", error);
-    }
-}
-
-function clearEntrySession() {
-    Object.values(SESSION).forEach((key) => sessionStorage.removeItem(key));
-}
-
-async function submitEntry(includeEquipment = true) {
-    if (submitting.value) return;
-
-    if (!props.isService && !getVarietyDraftList().length) {
-        ElMessage.warning("请先完善种植品种信息");
-        return;
-    }
-
-    const baForm = readJson(SESSION.BA_FORM);
-    if (!baForm?.farmName && !baForm?.name) {
-        ElMessage.warning("请先完善农场信息");
-        return;
-    }
-
-    submitting.value = true;
-    try {
-        const params = buildFarmPayload(includeEquipment);
-        const raw = await VE_API.entry.addFarmInfo(params);
-        // 部分接口返回 [body, httpStatus]
-        const res = Array.isArray(raw) ? raw[0] : raw;
-        if (res?.code === 200) {
-            pendingReportParams.value = buildReportParams();
-            // 新增成功后立刻触发 generate_6module,不等待结果
-            triggerReportApis(res);
-            // 新增成功后拉列表,选中最后一项
-            await selectNewlyCreatedFarm();
-            clearEntrySession();
-            showSuccessPopup.value = true;
-        } else {
-            ElMessage.error(res?.msg || res?.message || "提交失败,请稍后再试");
-        }
-    } catch (error) {
-        console.error("entry submit failed", error);
-        ElMessage.error("提交失败");
-    } finally {
-        submitting.value = false;
-    }
-}
-
-async function handleComplete() {
-    // 只触发初始化,不等待结果;新增已成功即可跳转
-    generateGrowthReport(pendingReportParams.value);
-    pendingReportParams.value = null;
-    showSuccessPopup.value = false;
-    emit("confirm");
-}
-
-/** 农户最后一步可跳过设备选择,仍提交其余信息 */
-function handleSkip() {
-    if (!props.isLastStep) {
-        emit("next");
-        return;
-    }
-    submitEntry(false);
-}
-
-function handleSubmit() {
-    saveSelectionDraft();
-    if (!props.isLastStep) {
-        emit("next");
-        return;
-    }
-    submitEntry(true);
-}
-
-onMounted(async () => {
-    await fetchEquipmentList();
-    restoreSelection();
-    applyFieldOverlapSelection();
-});
-</script>
-
-<style lang="scss" scoped>
-.select-equipment {
-    flex: 1;
-    min-height: 0;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-
-    &__content {
-        flex: 1;
-        min-height: 0;
-        overflow-y: auto;
-        -webkit-overflow-scrolling: touch;
-        padding: 12px 10px 80px;
-    }
-
-    .page-header {
-        padding: 8px 0 16px;
-
-        .page-title {
-            font-size: 26px;
-            color: #005599;
-            font-family: "PangMenZhengDao";
-            line-height: 30px;
-            .page-tag-tip {
-                font-size: 20px;
-            }
-            &.has-tag-wrap {
-                display: flex;
-                align-items: center;
-                gap: 6px;
-            }
-            .page-tag {
-                height: 24px;
-                line-height: 24px;
-                display: inline-block;
-                background: #2199F8;
-                color: #FFFFFF;
-                font-size: 14px;
-                padding: 0 6px;
-                border-radius: 4px;
-            }
-        }
-
-        .page-subtitle {
-            margin-top: 4px;
-            font-size: 14px;
-            color: rgba(46, 46, 46, 0.4);
-            line-height: 20px;
-        }
-        .text-tip {
-            font-size: 20px;
-        }
-    }
-
-    .toolbar {
-        display: flex;
-        align-items: center;
-        gap: 10px;
-        margin-bottom: 12px;
-    }
-
-    .search-bar {
-        flex: 1;
-        min-width: 0;
-        display: flex;
-        align-items: center;
-        height: 36px;
-        padding: 0 12px;
-        background: rgba(255, 255, 255, 0.5);
-        border: 1px solid rgba(33, 153, 248, 0.5);
-        border-radius: 6px;
-        box-sizing: border-box;
-
-        .search-icon {
-            color: rgba(0, 0, 0, 0.35);
-            font-size: 16px;
-            margin-right: 6px;
-            flex-shrink: 0;
-        }
-
-        .search-input {
-            flex: 1;
-            min-width: 0;
-            border: none;
-            outline: none;
-            background: transparent;
-            font-size: 14px;
-            color: #333;
-
-            &::placeholder {
-                color: rgba(0, 0, 0, 0.3);
-            }
-        }
-
-        .search-btn {
-            flex-shrink: 0;
-            padding-left: 10px;
-            color: #2199f8;
-            font-size: 14px;
-            cursor: pointer;
-        }
-    }
-
-    .add-btn {
-        flex-shrink: 0;
-        height: 36px;
-        padding: 0 12px;
-        border-radius: 6px;
-        background: #2199f8;
-        color: #fff;
-        font-size: 14px;
-        line-height: 36px;
-        white-space: nowrap;
-        cursor: pointer;
-    }
-
-    .equipment-card {
-        background: #fff;
-        border-radius: 12px;
-        padding: 14px 12px 16px;
-        margin-bottom: 12px;
-    }
-
-    .section-title {
-        display: flex;
-        align-items: center;
-        gap: 8px;
-        margin-bottom: 4px;
-        font-size: 16px;
-        font-weight: 500;
-        color: #1a1a1a;
-
-        .title-icon {
-            width: 15px;
-            height: 15px;
-            object-fit: contain;
-            flex-shrink: 0;
-        }
-    }
-
-    .section-desc {
-        margin: -2px 0 2px 22px;
-        font-size: 12px;
-        color: rgba(0, 0, 0, 0.4);
-        line-height: 18px;
-    }
-
-    .tag-group {
-        display: grid;
-        grid-template-columns: repeat(3, 1fr);
-        gap: 0 8px;
-        font-size: 14px;
-
-        .tag-item {
-            margin-top: 10px;
-            position: relative;
-            border-radius: 6px;
-            box-sizing: border-box;
-            min-height: 36px;
-            padding: 6px 4px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            text-align: center;
-            cursor: pointer;
-            color: #000;
-            background: #f3f4f6;
-            border: 1px solid transparent;
-
-            .text {
-                display: block;
-                width: 100%;
-                padding: 0 4px;
-                font-size: 13px;
-                line-height: 16px;
-                white-space: normal;
-                word-break: break-all;
-            }
-
-            &.selected {
-                border: 1px solid #2199f8;
-                background: #e8f5ff;
-                color: #2199f8;
-
-                &::after {
-                    content: "";
-                    position: absolute;
-                    z-index: 9;
-                    top: -1px;
-                    right: -1px;
-                    width: 18px;
-                    height: 14px;
-                    background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
-                }
-            }
-        }
-    }
-
-    .empty-tip {
-        padding: 40px 0;
-        text-align: center;
-        color: rgba(0, 0, 0, 0.35);
-        font-size: 14px;
-    }
-
-    .custom-bottom-fixed-btns {
-        display: flex;
-        justify-content: space-between;
-        align-items: baseline;
-        gap: 10px;
-        padding: 12px 12px 0;
-        height: 80px;
-        background: #fff;
-        box-sizing: border-box;
-        box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
-        .btns-l {
-            display: flex;
-            align-items: center;
-            gap: 10px;
-        }
-
-        .bottom-btn {
-            padding: 0 30px;
-            height: 40px;
-            line-height: 40px;
-            box-sizing: border-box;
-            font-size: 14px;
-            border-radius: 25px;
-            text-align: center;
-            white-space: nowrap;
-
-            &.disabled {
-                opacity: 0.6;
-                pointer-events: none;
-            }
-        }
-
-        .secondary-btn {
-            padding: 0 26px;
-            color: #2199f8;
-            background: #fff;
-            border: 1px solid #2199f8;
-        }
-
-        .primary-btn {
-            background: #2199f8;
-            color: #fff;
-        }
-    }
-
-    &.is-service-equipment {
-        .page-subtitle {
-            font-size: 16px;
-            font-weight: 600;
-            color: #005599;
-            line-height: 22px;
-        }
-    }
-}
-</style>

+ 0 - 57
src/views/old_mini/entry_information/index.vue

@@ -25,25 +25,6 @@
                     @next="handleNext"
                     @complete="handleEntryComplete"
                 />
-                <select-equipment
-                    v-else-if="showEquipmentStep"
-                    :key="equipmentScope || 'farmer'"
-                    :is-service="isServiceEntry"
-                    :is-last-step="equipmentIsLastStep"
-                    :equipment-scope="equipmentScope"
-                    :total-steps="totalSteps"
-                    @prev="handlePrev"
-                    @next="handleNext"
-                    @confirm="handleConfirm"
-                />
-                <manual-farming-service
-                    v-else-if="showManualStep"
-                    :has-field="hasFieldSelected"
-                    :has-fruit="hasFruitSelected"
-                    @prev="handlePrev"
-                    @next="handleNext"
-                    @confirm="handleConfirm"
-                />
             </template>
         </div>
     </div>
@@ -56,8 +37,6 @@ import customHeader from "@/components/customHeader.vue";
 import baInformation from "./components/baInformation.vue";
 import serviceInformation from "./components/serviceInformation.vue";
 import selectCategory from "./components/selectCategory.vue";
-import selectEquipment from "./components/selectEquipment.vue";
-import manualFarmingService from "./components/manualFarmingService.vue";
 import entryComplete from "./components/entryComplete.vue";
 
 const STEP_KEY = "ENTRY_INFORMATION_STEP";
@@ -69,13 +48,10 @@ const ENTRY_DRAFT_KEYS = [
     "ENTRY_INFORMATION_STEP",
     "ENTRY_SELECTED_CATEGORY",
     "ENTRY_SELECTED_VARIETY_LIST",
-    "ENTRY_SELECTED_EQUIPMENT",
-    "ENTRY_SELECTED_FARM_TASKS",
     "ENTRY_RESIDENT_LOCATION",
     "ENTRY_BA_FORM",
     "ENTRY_CROP_DETAIL",
     "ENTRY_CROP_META",
-    "ENTRY_MANUAL_VIEW",
     "ENTRY_VARIETY_EDIT_UID",
 ];
 
@@ -130,35 +106,10 @@ const isBothCrops = computed(() => hasFieldSelected.value && hasFruitSelected.va
  * 农户:位置 → 作物+品种(提交)→ 完成页,不再走设备步
  * 农服:保持原分步
  */
-const totalSteps = computed(() => {
-    if (!isServiceEntry.value) return 2;
-    return isBothCrops.value ? 6 : 4;
-});
 const maxStep = computed(() => {
     if (!isServiceEntry.value) return 2;
     return isBothCrops.value ? 5 : 4;
 });
-const showEquipmentStep = computed(() => {
-    if (!isServiceEntry.value) return false;
-    if (hasFieldSelected.value && currentStep.value === 3) return true;
-    if (isBothCrops.value && currentStep.value === 5) return true;
-    if (hasFruitSelected.value && !hasFieldSelected.value && currentStep.value === 4) return true;
-    return false;
-});
-const equipmentScope = computed(() => {
-    if (!isServiceEntry.value) return "";
-    if (!hasFieldSelected.value) return "fruit";
-    return currentStep.value === 5 ? "fruit" : "field";
-});
-const equipmentIsLastStep = computed(() => {
-    if (!isServiceEntry.value) return true;
-    return equipmentScope.value === "fruit";
-});
-const showManualStep = computed(() => {
-    if (!isServiceEntry.value) return false;
-    if (hasFieldSelected.value) return currentStep.value === 4;
-    return hasFruitSelected.value && currentStep.value === 3;
-});
 
 function refreshHasField() {
     try {
@@ -226,9 +177,6 @@ watch(
 
 const handleNext = () => {
     refreshHasField();
-    if (isServiceEntry.value && hasFieldSelected.value && currentStep.value === 3) {
-        sessionStorage.removeItem("ENTRY_MANUAL_VIEW");
-    }
     if (currentStep.value < maxStep.value) {
         currentStep.value += 1;
     }
@@ -259,11 +207,6 @@ function leaveCompleteToReport() {
     resetToFirstStep();
     router.replace("/growth_report");
 }
-
-const handleConfirm = () => {
-    resetToFirstStep();
-    router.replace("/growth_report");
-};
 </script>
 
 <style lang="scss" scoped>

+ 5 - 2
src/views/old_mini/growth_report/index.vue

@@ -53,7 +53,7 @@
                 </div>
             </div>
 
-            <div class="growth-top__content">
+            <div class="growth-top__content" :class="{'space-pos': stressMenuItems.length}">
                 <!-- 左侧:气象底图图层菜单(有农场 getHomeMapCalc / 无农场 getHomeMapCalcByCounty) -->
                 <div v-if="stressMenuItems.length" class="side-menu side-menu--left">
                     <div v-for="item in stressMenuItems" :key="item.key" class="side-menu__item"
@@ -964,7 +964,10 @@ onActivated(() => {
         .growth-top__content {
             display: flex;
             align-items: flex-start;
-            justify-content: space-between;
+            justify-content: end;
+            &.space-pos {
+                justify-content: space-between;
+            }
         }
     }
 

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

@@ -1153,7 +1153,7 @@ const areaMap = new AreaMap();
             gap: 8px;
             font-size: 16px;
             font-weight: 500;
-            color: rgba(255, 255, 255, 0.92);
+            color: #F2E9D8;
             min-width: 0;
 
             .title-bar {