Ver código fonte

fix: 首页巡园要点

lxf 2 horas atrás
pai
commit
1753bed197

+ 2 - 2
src/components/pageComponents/PhenologyTrackTimelineItem.vue

@@ -322,8 +322,8 @@ defineExpose({
             padding: 0 5px;
             margin-bottom: 4px;
             border-radius: 2px;
-            background: #2199f8;
-            color: #fff;
+            background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+            color: #1A1408;
             font-size: 12px;
             line-height: 18px;
         }

+ 1 - 1
src/components/popup/ImagePreviewPopup.vue

@@ -16,7 +16,7 @@
                     :show-indicators="false"
                     :autoplay="0"
                     :loop="false"
-                    indicator-color="#2199F8"
+                    indicator-color="#FFD489"
                     @change="handleSwipeChange"
                 >
                     <SwipeItem v-for="(img, index) in normalizedImages" :key="`${img.url}-${index}`">

+ 2 - 2
src/components/popup/phenologyUpdatePopup.vue

@@ -262,8 +262,8 @@ const handleConfirm = () => {
         }
 
         &--primary {
-            background: #2199f8;
-            color: #fff;
+            background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+            color: #1A1408;
         }
     }
 }

+ 2 - 2
src/components/popup/restorePlantingPopup.vue

@@ -633,8 +633,8 @@ linear-gradient(180deg, rgba(255, 212, 137, 0.36) 0%, rgba(255, 212, 137, 0) 12%
         width: 16px;
         height: 16px;
         border-radius: 0 6px 0 6px;
-        background: #2199f8;
-        color: #fff;
+        background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+        color: #1A1408;
         display: flex;
         align-items: center;
         justify-content: center;

+ 2 - 2
src/components/popup/surveyAskPopup.vue

@@ -89,8 +89,8 @@ const handleFill = () => {
     height: 40px;
     line-height: 40px;
     border-radius: 25px;
-    background: #2199f8;
-    color: #fff;
+    background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+    color: #1A1408;
     font-size: 16px;
     box-sizing: border-box;
     padding: 0 30px;

+ 0 - 6
src/router/globalRoutes.js

@@ -138,10 +138,4 @@ export default [
         meta: { showTabbar: true, keepAlive: true },
         component: () => import("@/views/old_mini/agri_file/pages/diagnosisReport.vue"),
     },
-    // 土壤感知
-    {
-        path: "/soil_sense",
-        name: "SoilSense",
-        component: () => import("@/views/old_mini/agri_file/pages/soilSense.vue"),
-    },
 ];

+ 2 - 2
src/styles/common.scss

@@ -326,8 +326,8 @@ div {
             border: 1px solid rgba(153, 153, 153, 0.5);
         }
         &.primary-btn {
-            background: linear-gradient(170deg, #9FD5FF, #2199F8);
-            color: #ffffff;
+            background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+            color: #1A1408;
             border: none;
         }
     }

+ 5 - 5
src/views/old_mini/agri_file/components/adjustPhenologyPopup.vue

@@ -183,9 +183,9 @@ defineExpose({ open, close });
         }
 
         &.selected {
-            color: #2199f8;
-            background: rgba(33, 153, 248, 0.1);
-            border-color: #2199f8;
+            color: #FFD489;
+            background: rgba(255, 212, 137, 0.1);
+            border-color: #FFD489;
         }
     }
 
@@ -195,8 +195,8 @@ defineExpose({ open, close });
         line-height: 44px;
         text-align: center;
         border-radius: 22px;
-        background: #2199f8;
-        color: #fff;
+        background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+        color: #1A1408;
         font-size: 16px;
     }
 }

+ 2 - 2
src/views/old_mini/agri_file/components/editPlantPointPopup.vue

@@ -185,8 +185,8 @@ defineExpose({ open, close });
         line-height: 44px;
         text-align: center;
         border-radius: 22px;
-        background: #2199f8;
-        color: #fff;
+        background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+        color: #1A1408;
         font-size: 16px;
     }
 }

+ 69 - 9
src/views/old_mini/agri_file/components/patrolPanel.vue

@@ -101,8 +101,8 @@
 </template>
 
 <script setup>
-import { onActivated, onMounted, ref } from "vue";
-import { useRouter } from "vue-router";
+import { nextTick, onActivated, onMounted, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
 import { Icon } from "vant";
 import { ElMessage } from "element-plus";
 import { Camera, Clock, Plus, WarningFilled } from "@element-plus/icons-vue";
@@ -114,6 +114,7 @@ import tipPopup from "@/components/popup/tipPopup.vue";
 import wx from "weixin-js-sdk";
 
 const { t } = useI18n();
+const route = useRoute();
 const router = useRouter();
 
 const PATROL_ICON = require("@/assets/img/common/empty-bg.png");
@@ -167,6 +168,8 @@ const cropArchiveList = ref([]);
 const showUploadPopup = ref(false);
 const showSuccessPopup = ref(false);
 const uploadZoneId = ref("");
+/** 范围校验进行中,避免重复点击触发多次跳转 */
+const uploadChecking = ref(false);
 
 // ---------- 工具函数 ----------
 function formatToday() {
@@ -218,7 +221,7 @@ const mapSectionToPatrol = (section = {}, type, ctx = {}) => {
     const urgencyTone = isSoil ? "warn" : isGrowth ? "success" : "danger";
     const urgencyIcon = isSoil ? waterUrgencyIcon : isGrowth ? calendarUrgencyIcon : "";
     const headerIcon = isSoil ? soilIcon : isGrowth ? periodIcon : lightningIcon;
-    const icon = resolveTaskImage(section.image_url || section.pic_url) || PATROL_ICON;
+    const icon = resolveTaskImage(section.image_url || section.pic_url || section.guide_url) || PATROL_ICON;
     const zoneId = section.zone_id ?? ctx.zoneId ?? selectedZoneId.value;
     return {
         id: section.type_code ? `${section.type_code}:${zoneId}` : `${type}:${zoneId}`,
@@ -480,9 +483,63 @@ const handleShare = (item) => {
     });
 };
 
-const handleUploadClick = (item) => {
-    uploadZoneId.value = resolveCardZoneId(item);
-    showUploadPopup.value = true;
+/** status=1 表示已勾画种植范围;信息不全 / 校验异常时不拦截上传 */
+const hasZoneRange = async (zoneId) => {
+    const farm = getSelectedFarm();
+    const farmId = farm.farm_id ?? farm.id ?? getSelectedFarmId();
+    if (farmId == null || farmId === "" || zoneId == null || zoneId === "") return true;
+    try {
+        const res = await VE_API.questionnaire.checkZoneRange({
+            farm_id: farmId,
+            zone_id: zoneId,
+        });
+        return Number(res?.data?.status) === 1;
+    } catch (e) {
+        console.warn("[patrolPanel] checkZoneRange failed", e);
+        return true;
+    }
+};
+
+/**
+ * 上传前校验地块范围:
+ * 已勾画 → 直接弹上传;未勾画 → 先去 /add_zone 画范围,
+ * 画好后 addZone 带 showUpload=1 回跳 /agri_file,由 tryContinueUpload 恢复上传弹窗
+ */
+const handleUploadClick = async (item) => {
+    if (uploadChecking.value) return;
+    const zoneId = resolveCardZoneId(item);
+    uploadZoneId.value = zoneId;
+    uploadChecking.value = true;
+    try {
+        if (await hasZoneRange(zoneId)) {
+            showUploadPopup.value = true;
+            return;
+        }
+        router.push({
+            path: "/add_zone",
+            query: {
+                type: "farm",
+                targetUrl: "/agri_file",
+                ...(zoneId !== "" && zoneId != null
+                    ? { zoneId, trackId: zoneId }
+                    : {}),
+            },
+        });
+    } finally {
+        uploadChecking.value = false;
+    }
+};
+
+/** 勾画完成返回(showUpload=1):恢复上传弹窗并清掉标记 */
+const tryContinueUpload = () => {
+    if (route.query.showUpload !== "1") return;
+    uploadZoneId.value =
+        route.query.id || uploadZoneId.value || selectedZoneId.value || "";
+    nextTick(() => {
+        showUploadPopup.value = true;
+    });
+    const { showUpload, ...restQuery } = route.query;
+    router.replace({ path: route.path, query: restQuery });
 };
 
 const handleUploadConfirm = async ({ images, date } = {}) => {
@@ -517,7 +574,10 @@ const handleComplete = () => {
 
 // ---------- 生命周期 ----------
 onMounted(loadPatrolData);
-onActivated(loadPatrolData);
+onActivated(() => {
+    loadPatrolData();
+    tryContinueUpload();
+});
 
 defineExpose({
     fetchTypesTask,
@@ -836,8 +896,8 @@ defineExpose({
 
         &.primary-btn {
             flex: 1;
-            background: #2199f8;
-            color: #fff;
+            background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+            color: #1A1408;
         }
     }
 }

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

@@ -447,7 +447,7 @@ onBeforeUnmount(() => {
 
             &__link {
                 padding: 0 2px;
-                color: #2199f8;
+                color: #FFD489;
                 cursor: pointer;
             }
         }

+ 4 - 3
src/views/old_mini/agri_file/pages/addZone.vue

@@ -376,7 +376,8 @@ function resolveSubmitFarmId() {
 
 function resolveSubmitZoneId() {
     const farm = getFarmData();
-    return farm.zone_id || farm.zoneId || "";
+    // 由巡园卡片等入口跳入时,优先按传入的分区勾画,避免落到缓存里的旧分区
+    return route.query.zoneId || farm.zone_id || farm.zoneId || "";
 }
 
 function resolveSubmitRegionName() {
@@ -772,8 +773,8 @@ onDeactivated(() => {
         }
         &.primary-btn {
             flex: 1;
-            background: #2199F8;
-            color: #fff;
+            background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+            color: #1A1408;
         }
     }
 }

+ 2 - 1
src/views/old_mini/agri_file/pages/pestInteract.vue

@@ -342,7 +342,8 @@ onActivated(() => {
     }
 
     .primary-btn {
-        background: #2199f8;
+        background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+        color: #1A1408;
     }
 }
 

+ 0 - 337
src/views/old_mini/agri_file/pages/soilSense.vue

@@ -1,337 +0,0 @@
-<template>
-    <div class="soil-sense-page">
-        <custom-header name="土壤感知" :isGoBack="true" @goback="handleBack" />
-        <div class="soil-sense-body">
-            <div class="soil-hero">
-                <div class="soil-hero__top">
-                    <div class="soil-hero__main">
-                        <div class="soil-hero__title-row">
-                            <span class="soil-hero__title">{{ theme }}</span>
-                            <span class="soil-hero__tag soil-hero__tag--level">
-                                <el-icon><BellFilled /></el-icon>
-                                {{ level }}
-                            </span>
-                            <span class="soil-hero__tag soil-hero__tag--zone">{{ zoneName }}</span>
-                        </div>
-                        <div class="soil-hero__reason">{{ subtitle }}</div>
-                    </div>
-                    <div class="soil-hero__share" @click="handleShare">
-                        <Icon name="share" />
-                        <span>{{ t("agriFile.forward") }}</span>
-                    </div>
-                </div>
-            </div>
-
-            <div
-                v-for="(block, index) in soilBlocks"
-                :key="block.key"
-                class="soil-main"
-                :class="{ 'soil-main--follow': index > 0 }"
-            >
-                <div class="soil-section-title">
-                    <img class="soil-section-title__icon" src="@/assets/img/report/qust-icon.png" alt="" />
-                    <span>{{ block.title }}</span>
-                </div>
-
-                <div class="interact-box">
-                    <div class="interact-box__head">
-                        <span class="interact-box__badge">{{ t("agriFile.interactQuestion") }}</span>
-                        <span class="interact-box__question">{{ block.question }}</span>
-                    </div>
-                    <div class="interact-box__options">
-                        <div
-                            v-for="item in block.options"
-                            :key="item.id"
-                            class="soil-option"
-                            :class="{ selected: block.selectedId === item.id }"
-                            @click="block.selectedId = item.id"
-                        >
-                            {{ item.name }}
-                        </div>
-                    </div>
-                </div>
-
-                <div class="guide-block">
-                    <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
-                    <photo-provider :photo-closable="true">
-                        <photo-consumer :src="block.guideImage">
-                            <img class="guide-block__img" :src="block.guideImage" alt="" />
-                        </photo-consumer>
-                    </photo-provider>
-                </div>
-            </div>
-        </div>
-    </div>
-</template>
-
-<script setup>
-import { ref } from "vue";
-import { useRouter } from "vue-router";
-import { Icon } from "vant";
-import { ElMessage } from "element-plus";
-import { BellFilled } from "@element-plus/icons-vue";
-import customHeader from "@/components/customHeader.vue";
-import { useI18n } from "@/i18n";
-
-const { t } = useI18n();
-const router = useRouter();
-
-const theme = ref(t("agriFile.interactTheme"));
-const subtitle = ref("互动小标题");
-const level = ref(t("agriFile.riskLevel2"));
-const zoneName = ref(t("agriFile.zoneOne"));
-const defaultGuideImage = require("@/assets/img/agricultural/patrol-guide.png");
-
-/** 土壤感知模块:后续可替换为接口 */
-const soilBlocks = ref([
-    {
-        key: "texture",
-        title: "土壤质地判断",
-        question: "请选择您的土壤质地",
-        selectedId: 2,
-        guideImage: defaultGuideImage,
-        options: [
-            { id: 1, name: "砂土" },
-            { id: 2, name: "砂壤土" },
-            { id: 3, name: "壤土" },
-            { id: 4, name: "黏壤土" },
-            { id: 5, name: "黏土" }
-        ]
-    },
-    {
-        key: "ph",
-        title: "土壤酸碱度判断",
-        question: "请选择您的土壤酸碱度",
-        selectedId: 2,
-        guideImage: defaultGuideImage,
-        options: [
-            { id: 1, name: "过酸" },
-            { id: 2, name: "弱酸" },
-            { id: 3, name: "中性" },
-            { id: 4, name: "弱碱性" },
-            { id: 5, name: "过碱" }
-        ]
-    }
-]);
-
-const handleBack = () => {
-    router.back();
-};
-
-const handleShare = () => {
-    ElMessage.info(t("agriFile.forwardTip"));
-};
-</script>
-
-<style lang="scss" scoped>
-.soil-sense-page {
-    width: 100%;
-    height: 100vh;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    background: #f5f5f5;
-    box-sizing: border-box;
-
-    .soil-sense-body {
-        flex: 1;
-        overflow-y: auto;
-        -webkit-overflow-scrolling: touch;
-        padding-bottom: 20px;
-    }
-
-    .soil-hero {
-        padding: 18px 10px 38px;
-        background: #2199f8;
-        color: #fff;
-
-        &__top {
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-        }
-
-        &__title-row {
-            display: flex;
-            align-items: center;
-            flex-wrap: wrap;
-            gap: 8px;
-        }
-
-        &__title {
-            font-size: 28px;
-            font-family: "PangMenZhengDao", "pangmenzhengdao", sans-serif;
-            margin-top: -5px;
-        }
-
-        &__reason {
-            margin-top: 10px;
-            padding: 4px 10px;
-            border-radius: 4px;
-            background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%);
-            font-size: 14px;
-        }
-
-        &__tag {
-            display: inline-flex;
-            align-items: center;
-            gap: 4px;
-            padding: 1px 4px;
-            border-radius: 2px;
-            background: #fff;
-            font-size: 12px;
-        }
-
-        &__tag--level {
-            color: #ff6a6a;
-        }
-
-        &__tag--zone {
-            color: #2199f8;
-        }
-
-        &__share {
-            flex: none;
-            display: flex;
-            align-items: center;
-            gap: 4px;
-            padding: 4px 14px;
-            border-radius: 25px;
-            background: rgba(19, 125, 209, 0.5);
-            color: #fff;
-        }
-    }
-
-    .soil-main {
-        position: relative;
-        z-index: 1;
-        margin: -24px 10px 0;
-        padding: 14px 12px 16px;
-        border-radius: 10px;
-        background: #fff;
-
-        &--follow {
-            margin: 10px 10px 0;
-        }
-    }
-
-    .soil-section-title {
-        display: flex;
-        align-items: center;
-        gap: 6px;
-        margin-bottom: 12px;
-        padding-bottom: 10px;
-        border-bottom: 1px solid rgba(0, 0, 0, 0.08);
-        color: #2199f8;
-        font-size: 16px;
-        font-weight: 500;
-
-        &__icon {
-            width: 20px;
-            height: 20px;
-        }
-    }
-
-    .interact-box {
-        padding: 12px;
-        border-radius: 8px;
-        background: rgba(33, 153, 248, 0.1);
-
-        &__head {
-            display: flex;
-            align-items: center;
-            flex-wrap: wrap;
-            gap: 8px;
-            margin-bottom: 12px;
-        }
-
-        &__badge {
-            display: inline-flex;
-            align-items: center;
-            padding: 2px 8px;
-            border-radius: 4px;
-            background: #2199f8;
-            color: #fff;
-            font-size: 12px;
-            line-height: 18px;
-        }
-
-        &__question {
-            font-size: 14px;
-            font-weight: 500;
-            color: #1d2129;
-        }
-
-        &__options {
-            display: grid;
-            grid-template-columns: repeat(3, 1fr);
-            gap: 8px;
-        }
-    }
-
-    .soil-option {
-        position: relative;
-        height: 36px;
-        line-height: 36px;
-        text-align: center;
-        border-radius: 6px;
-        background: #fff;
-        border: 1px solid transparent;
-        font-size: 14px;
-        color: #666;
-        box-sizing: border-box;
-
-        &.selected {
-            color: #2199f8;
-            border-color: #2199f8;
-
-            &::after {
-                content: "";
-                position: absolute;
-                top: -1px;
-                right: -1px;
-                width: 18px;
-                height: 14px;
-                background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
-            }
-        }
-    }
-
-    .guide-block {
-        margin-top: 10px;
-        border: 1px solid rgba(33, 153, 248, 0.2);
-        background: linear-gradient(360deg, #FFFFFF 90.33%, #ABD8FC 116.61%, #2199F8 148.55%);
-        border-radius: 8px;
-        padding: 10px;
-    }
-
-    .guide-title {
-        position: relative;
-        z-index: 1;
-        display: inline-block;
-        font-size: 16px;
-        font-weight: 500;
-        color: #1d2129;
-
-        &::after {
-            content: "";
-            position: absolute;
-            left: 0;
-            bottom: 2px;
-            z-index: -1;
-            width: 90px;
-            height: 6px;
-            border-radius: 25px;
-            background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
-            opacity: 0.4;
-        }
-    }
-
-    .guide-block__img {
-        display: block;
-        width: 100%;
-        margin-top: 8px;
-        border-radius: 8px;
-    }
-}
-</style>

+ 0 - 226
src/views/old_mini/agri_record_detail/components/completePopup.vue

@@ -1,226 +0,0 @@
-<template>
-    <popup v-model:show="showValue" class="complete-popup" closeable @closed="handleClosed">
-        <div class="popup-content">
-            <div class="time-wrap">
-                <div class="name pb-10">{{ t('agriRecordDetail.completePopup.selectExecuteTime') }}</div>
-                <div class="time-input">
-                    <el-date-picker
-                        v-model="executeTime"
-                        format="YYYY.MM.DD"
-                        value-format="YYYY-MM-DD"
-                        :disabled-date="disabledDate"
-                        size="large"
-                        style="width: 100%"
-                        type="date"
-                        :placeholder="t('agriRecordDetail.selectDate')"
-                        :editable="false"
-                    />
-                </div>
-            </div>
-            <div class="upload-wrap" :class="{ 'upload-cont': fileList.length }">
-                <div class="name">{{ t('agriRecordDetail.completePopup.evidenceTitle') }}</div>
-                <div class="sub-name">{{ t('agriRecordDetail.completePopup.evidenceHint') }}</div>
-                <uploader
-                    class="uploader"
-                    v-model="fileList"
-                    multiple
-                    :max-count="5"
-                    :after-read="afterRead"
-                >
-                    <img class="plus" src="@/assets/img/home/plus.png" alt="" />
-                </uploader>
-            </div>
-            <div class="button-wrap">
-                <div class="button primary" @click="handleConfirm">{{ t('agriRecordDetail.completePopup.confirm') }}</div>
-            </div>
-        </div>
-    </popup>
-</template>
-
-<script setup>
-import { Popup, Uploader } from 'vant';
-import { computed, ref, watch } from 'vue';
-import { ElMessage } from 'element-plus';
-import { useStore } from 'vuex';
-import { useI18n } from '@/i18n';
-import { getFileExt } from '@/utils/util';
-import UploadFile from '@/utils/upliadFile';
-
-const { t } = useI18n();
-const store = useStore();
-const miniUserId = store.state.home.miniUserId;
-
-const props = defineProps({
-    show: {
-        type: Boolean,
-        default: false,
-    },
-});
-
-const emit = defineEmits(['update:show', 'confirm']);
-
-const showValue = computed({
-    get: () => props.show,
-    set: (value) => emit('update:show', value),
-});
-
-const fileList = ref([]);
-const fileArr = ref([]);
-const executeTime = ref('');
-const uploadFileObj = new UploadFile();
-
-const resetForm = () => {
-    fileList.value = [];
-    fileArr.value = [];
-    executeTime.value = '';
-};
-
-watch(
-    () => props.show,
-    (visible) => {
-        if (visible) {
-            resetForm();
-        }
-    }
-);
-
-const afterRead = (file) => {
-    const files = Array.isArray(file) ? file : [file];
-
-    files.forEach((item) => {
-        const fileVal = item.file;
-        if (!fileVal) return;
-
-        item.status = 'uploading';
-        item.message = '上传中...';
-        const ext = getFileExt(fileVal.name);
-        const key = `birdseye-look-mini/${miniUserId}/${Date.now()}.${ext}`;
-        uploadFileObj
-            .put(key, fileVal)
-            .then((resFilename) => {
-                item.status = 'done';
-                item.message = '';
-                fileArr.value.push(resFilename);
-            })
-            .catch(() => {
-                item.status = 'failed';
-                item.message = '上传失败';
-                ElMessage.error(t('agriRecordDetail.completePopup.uploadFailed'));
-            });
-    });
-};
-
-const disabledDate = (time) => time.getTime() > Date.now();
-
-const handleConfirm = () => {
-    if (!executeTime.value) {
-        ElMessage.warning(t('agriRecordDetail.completePopup.selectTimeRequired'));
-        return;
-    }
-    if (!fileArr.value.length) {
-        ElMessage.warning(t('agriRecordDetail.completePopup.uploadRequired'));
-        return;
-    }
-    emit('confirm', {
-        executeDate: executeTime.value,
-        executeEvidence: [...fileArr.value],
-    });
-    showValue.value = false;
-};
-
-const handleClosed = () => {
-    resetForm();
-};
-</script>
-
-<style lang="scss" scoped>
-.complete-popup {
-    width: 90%;
-    border-radius: 8px;
-
-    ::v-deep {
-        .van-popup__close-icon--top-right {
-            top: 8px;
-            right: 10px;
-            color: #333333;
-        }
-    }
-
-    .popup-content {
-        padding: 24px 16px 20px;
-        background: linear-gradient(360deg, #ffffff 74.2%, #d1ebff 100%);
-        border-radius: 8px;
-    }
-}
-
-.pb-10 {
-    padding-bottom: 12px;
-}
-
-.name {
-    color: #000000;
-    font-size: 16px;
-    font-weight: 500;
-}
-
-.sub-name {
-    font-size: 12px;
-    color: rgba(0, 0, 0, 0.4);
-    padding-bottom: 12px;
-}
-
-.upload-wrap {
-    &.upload-cont {
-        ::v-deep {
-            .van-uploader__wrapper {
-                flex-wrap: nowrap;
-            }
-        }
-    }
-
-    .plus {
-        margin-right: 12px;
-        width: 80px;
-        height: 80px;
-    }
-
-    ::v-deep {
-        .van-image__img {
-            border-radius: 8px;
-        }
-
-        .van-uploader,
-        .van-uploader__wrapper,
-        .van-uploader__input-wrapper {
-            width: 100%;
-        }
-    }
-}
-
-.time-wrap {
-    margin-bottom: 12px;
-
-    .time-input {
-        width: 100%;
-    }
-}
-
-.button-wrap {
-    display: flex;
-    justify-content: center;
-    margin: 24px 4px 0;
-
-    .button {
-        border-radius: 20px;
-        padding: 7px 0;
-        text-align: center;
-        font-size: 16px;
-
-        &.primary {
-            flex: 1;
-            background: #2199f8;
-            color: #fff;
-        }
-    }
-}
-</style>

+ 0 - 804
src/views/old_mini/agri_record_detail/index.vue

@@ -1,804 +0,0 @@
-<template>
-    <div class="agri-record-detail">
-        <custom-header :name="t('agriRecordDetail.title')"></custom-header>
-        <div class="content" :class="{ 'content--no-footer': !showBottomButtons }">
-            <div class="status-banner" :class="`status-banner--${statusBannerTheme}`">
-                <div class="status-banner__title">{{ statusBannerTitle }}</div>
-                <div class="status-banner__deadline">{{ t('agriRecordDetail.deadlinePrefix') }} {{ workDetail.work_time }}</div>
-            </div>
-            <div class="box-wrap execution-area-card">
-                <div class="execution-area-card__header">
-                    <span class="title">{{ t('agriRecordDetail.executionArea') }}</span>
-                    <span class="code">ws0gefwg9tdn</span>
-                </div>
-                <div class="map-container" ref="mapContainer"></div>
-                <div class="execution-area-card__footer">
-                    <div class="address">
-                        <div class="address-title">{{ farmInfo.farm_name }}</div>
-                        <div class="address-detail">{{ farmInfo.rawAddress }}</div>
-                    </div>
-                    <div class="nav-btn" @click="handleNavigate">{{ t('agriRecordDetail.navigateHere') }}</div>
-                </div>
-            </div>
-            <div v-if="isCompletedStatus" class="box-wrap execute-photos-card">
-                <div class="execute-photos-card__header">
-                    <span class="execute-photos-card__title">{{ t('workExecute.executePhotos') }}</span>
-                    <span class="execute-photos-card__status">{{ t('agriRecordDetail.auditApproved') }}</span>
-                </div>
-                <div class="execute-photos-card__list">
-                    <div
-                        v-for="(photo, index) in executePhotoList"
-                        :key="index"
-                        class="execute-photos-card__item"
-                    >
-                        <img :src="photo" alt="" />
-                    </div>
-                </div>
-            </div>
-            <div class="box-wrap work-summary-card">
-                <div class="header" :class="{ 'header--en': locale === 'en' }">
-                    <img class="icon" src="@/assets/img/home/task.png" alt="">
-                    <div class="header-main">
-                        <span class="title">{{ workDetail.work_name }}</span>
-                        <div class="tags" v-if="workTagLabel">
-                            <span
-                                class="work-tag"
-                                :class="`work-tag--${getWorkTagTheme(workDetail.farm_work_type)}`"
-                            >{{ workTagLabel }}</span>
-                        </div>
-                    </div>
-                </div>
-                <div class="body">
-                    <div
-                        v-for="(row, index) in detailRows"
-                        :key="index"
-                        class="detail-row"
-                        :class="{
-                            'detail-row--field': isFieldRow(row),
-                            'detail-row--textarea': getEffectiveRowType(row) === 'textarea',
-                        }"
-                    >
-                        <span class="detail-row__label">{{ t(row.labelKey) }}</span>
-                        <span v-if="isTextRow(row)" class="detail-row__value">{{ formatRowValue(row) }}</span>
-                        <el-date-picker
-                            v-else-if="getEffectiveRowType(row) === 'date'"
-                            v-model="row.value"
-                            class="detail-row__control"
-                            type="date"
-                            format="YYYY.MM.DD"
-                            value-format="YYYY-MM-DD"
-                            :placeholder="t('agriRecordDetail.selectDate')"
-                            :editable="false"
-                            style="width: 100%"
-                        />
-                        <el-input
-                            v-else-if="getEffectiveRowType(row) === 'input'"
-                            v-model="row.value"
-                            class="detail-row__control"
-                            :placeholder="t('agriRecordDetail.pleaseInput')"
-                        />
-                        <el-input
-                            v-else-if="getEffectiveRowType(row) === 'textarea'"
-                            v-model="row.value"
-                            class="detail-row__control"
-                            type="textarea"
-                            :rows="3"
-                            :placeholder="t('agriRecordDetail.pleaseInput')"
-                        />
-                    </div>
-                </div>
-            </div>
-            <div class="box-wrap">
-                <div class="info-item">
-                    <div class="info-title"><span class="title-block"></span>{{ t('agriRecordDetail.workPurpose') }}</div>
-                    <div class="info-value">{{ workDetail?.work_purpose }}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-title"><span class="title-block"></span>{{ t('agriRecordDetail.drugPrescription') }}</div>
-                    <div class="info-value">{{ workDetail?.drug_prescription }}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-title"><span class="title-block"></span>{{ t('agriRecordDetail.executionParams') }}</div>
-                    <div class="info-value">{{ workDetail?.execution_method || '--' }}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-title"><span class="title-block"></span>{{ t('agriRecordDetail.precautions') }}</div>
-                    <div class="info-value">{{ workDetail?.precautions }}</div>
-                </div>
-            </div>
-        </div>
-        <div
-            class="custom-bottom-fixed-btns"
-            :class="{ 'custom-bottom-fixed-btns--single': isExecutingStatus && !isEditing }"
-            v-if="showBottomButtons"
-        >
-            <template v-if="isExecutingStatus && !isEditing">
-                <div class="bottom-btn primary-btn green-btn" @click="handleAccept">{{ t('workExecute.markComplete') }}</div>
-            </template>
-            <template v-else>
-                <div class="bottom-btn secondary-btn" @click="handleToggleEdit">
-                    {{ isEditing ? t('agriRecordDetail.cancelEdit') : t('agriRecordDetail.modifyInfo') }}
-                </div>
-                <div class="bottom-btn primary-btn" :class="{ 'orange-btn': !isEditing && !isAcceptedStatus }" @click="handleAccept">{{ primaryActionText }}</div>
-            </template>
-        </div>
-        <tip-popup
-            v-model:show="showAcceptSuccessPopup"
-            type="success"
-            :text="t('agriRecordDetail.acceptSuccessTitle')"
-            :text2="t('agriRecordDetail.acceptSuccessDesc')"
-            hideBtn
-        />
-        <tip-popup
-            v-model:show="showExecuteSuccessPopup"
-            type="executeSuccess"
-            :text="t('agriRecordDetail.executeSuccessTitle')"
-            :text2="t('agriRecordDetail.executeSuccessDesc')"
-            :notice-text="t('agriRecordDetail.executeSuccessNotice')"
-            hideBtn
-        />
-        <complete-popup v-model:show="showCompletePopup" @confirm="handleCompleteConfirm" />
-    </div>
-</template>
-
-<script setup>
-import { ref, computed, onActivated, onDeactivated, onBeforeUnmount, nextTick } from 'vue';
-import { School } from '@element-plus/icons-vue';
-import * as util from '@/common/ol_common.js';
-import customHeader from '@/components/customHeader.vue';
-import { useI18n } from '@/i18n';
-import AreaMap from '../work_detail/components/areaMap.js';
-import tipPopup from '@/components/popup/tipPopup.vue';
-import completePopup from './components/completePopup.vue';
-import { ElMessage } from 'element-plus';
-import { useRoute } from 'vue-router';
-import config from '@/api/config.js';
-import executePhotoPlaceholder1 from '@/assets/img/common/sd-1.jpg';
-import executePhotoPlaceholder2 from '@/assets/img/common/sd-2.jpg';
-import executePhotoPlaceholder3 from '@/assets/img/common/sd-3.jpg';
-import executePhotoPlaceholder4 from '@/assets/img/common/sd-4.jpg';
-
-const EXECUTE_PHOTO_PLACEHOLDERS = [
-    executePhotoPlaceholder1,
-    executePhotoPlaceholder2,
-    executePhotoPlaceholder3,
-    executePhotoPlaceholder4,
-];
-
-const { t, locale } = useI18n();
-
-const route = useRoute();
-const DEFAULT_FARM_POINT = 'POINT(113.6142086995688 23.585836479509055)';
-const mapContainer = ref(null);
-const recordDetailMap = new AreaMap();
-
-const workDetail = ref({});
-
-const farmZones = computed(() => {
-    const zones = workDetail.value?.zones;
-    if (!Array.isArray(zones)) return [];
-    return zones.filter(
-        (z) => z && typeof z.zone_geometry === 'string' && z.zone_geometry.trim()
-    );
-});
-
-function wktCenterPointFromZones(zones) {
-    const list = Array.isArray(zones) ? zones : [];
-    for (const zone of list) {
-        const wkt = zone?.zone_geometry;
-        if (!wkt || typeof wkt !== 'string') continue;
-        try {
-            const c = util.wktCastGeom(wkt.trim()).getFirstCoordinate();
-            return `POINT(${c[0]} ${c[1]})`;
-        } catch {
-            /* try next zone */
-        }
-    }
-    return DEFAULT_FARM_POINT;
-}
-
-function destroyExecutionAreaMap() {
-    recordDetailMap.destroyMap();
-}
-
-function initExecutionAreaMap() {
-    nextTick(() => {
-        if (!mapContainer.value) return;
-        const zones = farmZones.value;
-        recordDetailMap.destroyMap();
-        recordDetailMap.initMap(wktCenterPointFromZones(zones), mapContainer.value);
-        recordDetailMap.initZones(zones);
-    });
-}
-
-const farmInfo = ref({});
-onActivated(async () => {
-    farmInfo.value = JSON.parse(localStorage.getItem('selectedFarmData') || '{}');
-    isEditing.value = false;
-    detailRowsSnapshot = null;
-    await getRegionWorkDetail();
-});
-
-onDeactivated(() => {
-    destroyExecutionAreaMap();
-});
-
-onBeforeUnmount(() => {
-    destroyExecutionAreaMap();
-});
-
-const getRegionWorkDetail = async () => {
-    const params = {
-        id: route.query.id,
-        work_operations_type: route.query.status
-    };
-    const { code, data } = await VE_API.farm.getRegionWorkDetail(params);
-    if (code === 200 && data && data.length > 0) {
-        workDetail.value = data[0];
-        detailRows.value = buildDetailRows(workDetail.value);
-        initExecutionAreaMap();
-    }
-};
-
-const EDITABLE_FIELD_MAP = {
-    work_time: 'date',
-    execution_method: 'textarea',
-};
-
-const isEditing = ref(false);
-const showAcceptSuccessPopup = ref(false);
-const showExecuteSuccessPopup = ref(false);
-const showCompletePopup = ref(false);
-let detailRowsSnapshot = null;
-
-const getEffectiveRowType = (row) => {
-    if (!isEditing.value) return undefined;
-    return EDITABLE_FIELD_MAP[row.field] || undefined;
-};
-
-const isTextRow = (row) => !getEffectiveRowType(row);
-const isFieldRow = (row) => !!getEffectiveRowType(row);
-
-const formatRowValue = (row) => row?.value ?? '';
-
-const normalizeDateValue = (value) => {
-    if (!value) return value;
-    return String(value).replace(/\./g, '-');
-};
-
-const DETAIL_ROW_SCHEMA = [
-    { labelKey: 'agriRecordDetail.fields.executor', field: 'executor' },
-    { labelKey: 'agriRecordDetail.fields.workReasonDetail', field: 'work_reason_detail' },
-    { labelKey: 'agriRecordDetail.fields.cropConditionAnalysis', field: 'crop_condition_analysis' },
-    { labelKey: 'agriRecordDetail.fields.workTime', field: 'work_time', type: 'date' },
-    { labelKey: 'agriRecordDetail.fields.executionMethod', field: 'execution_method', type: 'textarea' },
-];
-
-const buildDetailRows = (data = {}) =>
-    DETAIL_ROW_SCHEMA.map(({ labelKey, field, type }) => ({
-        labelKey,
-        field,
-        type,
-        value: data[field] ?? '',
-    }));
-
-const detailRows = ref([]);
-
-const STATUS_BANNER_MAP = {
-    待接受: 'workExecute.pending',
-    已接受: 'workExecute.accepted',
-    执行中: 'workExecute.executing',
-    已完成: 'workExecute.completed',
-    接受异常: 'workExecute.acceptTimeout',
-    执行异常: 'workExecute.executeAbnormal',
-    完成异常: 'workExecute.completeAbnormal',
-    未达标: 'workExecute.unqualified',
-};
-
-const statusBannerTitle = computed(() => {
-    const statusStr = route.query.statusStr;
-    const key = STATUS_BANNER_MAP[statusStr];
-    return key ? t(key) : statusStr || '';
-});
-
-const statusBannerTheme = computed(() => {
-    const statusStr = route.query.statusStr;
-    if (['接受异常', '执行异常', '完成异常', '未达标'].includes(statusStr)) return 'overtime';
-    if (statusStr === '执行中') return 'executing';
-    if (statusStr === '已完成') return 'completed';
-    if (statusStr === '已接受') return 'accepted';
-    const status = Number(route.query.status);
-    if (status === 0) return 'pending';
-    if (status === 2) return 'accepted';
-    if (status === 3) return 'completed';
-    if (status === 3) return 'completed';
-    return 'pending';
-});
-
-const workTagLabel = computed(() => {
-    const type = workDetail.value?.farm_work_type;
-    if (type == null || type === '') return '';
-    return t(`agriRecordDetail.farmWorkType.${type}`);
-});
-
-const ABNORMAL_STATUS_FLAG_MAP = {
-    接受异常: ["is_aovertime"],
-    执行异常: ["is_eovertime", "is_aovertime"],
-    完成异常: ["is_covertime", "is_aovertime"],
-    未达标: ["is_anormal", "is_unqualified", "is_aovertime"],
-};
-
-const isTruthyFlag = (value) => Number(value) === 1 || value === true;
-
-const isAbnormalDetail = computed(() => {
-    const statusStr = route.query.statusStr;
-    if (["接受异常", "执行异常", "完成异常", "未达标"].includes(statusStr)) {
-        const flags = ABNORMAL_STATUS_FLAG_MAP[statusStr] || ["is_aovertime"];
-        const detail = workDetail.value || {};
-        return flags.some((field) => isTruthyFlag(detail[field]));
-    }
-    return Object.values(ABNORMAL_STATUS_FLAG_MAP).some((flags) =>
-        flags.some((field) => isTruthyFlag(workDetail.value?.[field]))
-    );
-});
-
-const isAcceptedStatus = computed(() =>
-    route.query.statusStr === '已接受' || Number(route.query.status) === 2
-);
-
-const isExecutingStatus = computed(() => route.query.statusStr === '执行中');
-
-const isCompletedStatus = computed(() =>
-    route.query.statusStr === '已完成' || Number(route.query.status) === 3
-);
-
-const showBottomButtons = computed(() => !isAbnormalDetail.value && !isCompletedStatus.value);
-
-const resolveExecutePhotoUrl = (src) => {
-    if (!src) return '';
-    if (typeof src === 'object' && src.cloud_filename) {
-        return config.base_img_url3 + src.cloud_filename;
-    }
-    if (typeof src === 'string') {
-        if (src.startsWith('http')) return src;
-        return config.base_img_url2 + src;
-    }
-    return '';
-};
-
-const executePhotoList = computed(() => {
-    const raw = workDetail.value?.execute_evidence ?? workDetail.value?.executeEvidence;
-    let photos = [];
-
-    if (raw) {
-        if (Array.isArray(raw)) {
-            photos = raw.map(resolveExecutePhotoUrl).filter(Boolean);
-        } else if (typeof raw === 'string') {
-            try {
-                const parsed = JSON.parse(raw);
-                if (Array.isArray(parsed)) {
-                    photos = parsed.map(resolveExecutePhotoUrl).filter(Boolean);
-                }
-            } catch {
-                const url = resolveExecutePhotoUrl(raw);
-                if (url) photos = [url];
-            }
-        }
-    }
-
-    return (photos.length ? photos : EXECUTE_PHOTO_PLACEHOLDERS).slice(0, 4);
-});
-
-const primaryActionText = computed(() => {
-    if (isEditing.value) return t('agriRecordDetail.saveInfo');
-    if (isAcceptedStatus.value) return t('workExecute.startExecute');
-    return t('agriRecordDetail.acceptTask');
-});
-
-const getWorkTagTheme = (type) => {
-    const n = Number(type);
-    if (n === 95) return 'green';
-    if (n >= 91 && n <= 98) return 'orange';
-    return 'blue';
-};
-
-const handleNavigate = () => {};
-
-const handleToggleEdit = () => {
-    if (!isEditing.value) {
-        detailRowsSnapshot = JSON.parse(JSON.stringify(detailRows.value));
-        detailRows.value = detailRows.value.map((row) => {
-            if (row.field === 'work_time' && row.value) {
-                return { ...row, value: normalizeDateValue(row.value) };
-            }
-            return row;
-        });
-        isEditing.value = true;
-        return;
-    }
-    if (detailRowsSnapshot) {
-        detailRows.value = detailRowsSnapshot;
-        detailRowsSnapshot = null;
-    }
-    isEditing.value = false;
-};
-
-const handleAccept = () => {
-    if (isEditing.value) {
-        // TODO: 提交保存农事信息接口
-        isEditing.value = false;
-        detailRowsSnapshot = null;
-        ElMessage.success(t('agriRecordDetail.saveSuccess'));
-        return;
-    }
-    if (isAcceptedStatus.value) {
-        // TODO: 开始执行接口
-        showExecuteSuccessPopup.value = true;
-        return;
-    }
-    if (isExecutingStatus.value) {
-        showCompletePopup.value = true;
-        return;
-    }
-    // TODO: 提交接受农事接口
-    showAcceptSuccessPopup.value = true;
-};
-
-const handleCompleteConfirm = () => {
-    // TODO: 提交完成农事接口,payload: { executeDate, executeEvidence }
-    ElMessage.success(t('agriRecordDetail.saveSuccess'));
-};
-</script>
-
-<style scoped lang="scss">
-.agri-record-detail {
-    width: 100%;
-    height: 100vh;
-    display: flex;
-    flex-direction: column;
-    background: #f2f3f5;
-    overflow: hidden;
-
-    .content {
-        flex: 1;
-        min-height: 0;
-        width: 100%;
-        overflow-y: auto;
-        -webkit-overflow-scrolling: touch;
-        padding-bottom: 90px;
-        box-sizing: border-box;
-
-        &--no-footer {
-            padding-bottom: 12px;
-        }
-        
-        .status-banner {
-            padding: 20px 12px 72px;
-            color: #fff;
-
-            &--0,
-            &--pending {
-                background: #FF9838;
-            }
-
-            &--2,
-            &--accepted,
-            &--executing {
-                background: #2199F8;
-            }
-
-            &--3,
-            &--completed {
-                background: #4ABF32;
-            }
-
-            &--1,
-            &--overtime {
-                background: #FF6A6A;
-            }
-
-            &__title {
-                font-size: 22px;
-            }
-        }
-
-        .box-wrap {
-            background: #ffffff;
-            border-radius: 8px;
-            padding: 10px;
-            margin: 10px 12px 0 12px;
-
-            &.execution-area-card {
-                position: relative;
-                z-index: 1;
-                margin-top: -52px;
-
-                .execution-area-card__header {
-                    display: flex;
-                    align-items: center;
-                    justify-content: space-between;
-                    .title {
-                        font-size: 16px;
-                    }
-                    .code {
-                        color: #565656;
-                    }
-                }
-
-                .map-container {
-                    width: 100%;
-                    height: 166px;
-                    margin: 12px 0;
-                    border-radius: 8px;
-                    overflow: hidden;
-                }
-
-                .execution-area-card__footer {
-                    display: flex;
-                    align-items: center;
-                    justify-content: space-between;
-                    .address {
-                        .address-title {
-                            font-size: 16px;
-                            color: #1D2129;
-                            // overflow: hidden;
-                            // text-overflow: ellipsis;
-                            // white-space: nowrap;
-                        }
-        
-                        .address-detail {
-                            margin-top: 2px;
-                            font-size: 12px;
-                            color: rgba(29, 33, 41, 0.6);
-                            // overflow: hidden;
-                            // text-overflow: ellipsis;
-                            // white-space: nowrap;
-                        }
-                    }
-                }
-
-                .nav-btn {
-                    padding: 6px 10px;
-                    border: 1px solid #2199F8;
-                    border-radius: 25px;
-                    color: #2199F8;
-                }
-            }
-
-            &.execute-photos-card {
-                padding: 12px 10px 10px;
-
-                .execute-photos-card__header {
-                    display: flex;
-                    align-items: center;
-                    justify-content: space-between;
-                    margin-bottom: 12px;
-                }
-
-                .execute-photos-card__title {
-                    font-size: 16px;
-                }
-
-                .execute-photos-card__status {
-                    padding: 2px 8px;
-                    font-size: 12px;
-                    line-height: 18px;
-                    color: #4ABF32;
-                    background: rgba(74, 191, 50, 0.1);
-                    border-radius: 4px;
-                }
-
-                .execute-photos-card__list {
-                    display: flex;
-                    gap: 8px;
-                }
-
-                .execute-photos-card__item {
-                    flex: 1;
-                    aspect-ratio: 1;
-                    border-radius: 8px;
-                    overflow: hidden;
-
-                    img {
-                        display: block;
-                        width: 100%;
-                        height: 100%;
-                        object-fit: cover;
-                    }
-                }
-            }
-
-            &.work-summary-card {
-                padding: 12px 10px 10px;
-
-                .header {
-                    display: flex;
-                    align-items: center;
-                    gap: 8px;
-                    border-bottom: 1px solid rgba(245, 245, 245, 0.99);
-                    padding-bottom: 8px;
-                    margin-bottom: 8px;
-                    .icon {
-                        width: 16px;
-                        height: 16px;
-                        flex-shrink: 0;
-                    }
-
-                    .header-main {
-                        flex: 1;
-                        min-width: 0;
-                        display: flex;
-                        align-items: center;
-                        gap: 8px;
-                    }
-
-                    &--en {
-                        align-items: flex-start;
-
-                        .icon {
-                            margin-top: 2px;
-                        }
-
-                        .header-main {
-                            flex-direction: column;
-                            align-items: flex-start;
-                            gap: 4px;
-                        }
-                    }
-    
-                    .title {
-                        font-size: 16px;
-                        font-weight: 500;
-                    }
-    
-                    .tags {
-                        display: flex;
-                        align-items: center;
-                        gap: 4px;
-                    }
-    
-                    .work-tag {
-                        padding: 0 6px;
-                        border-radius: 2px;
-                        font-size: 12px;
-    
-                        &--orange {
-                            background: rgba(255, 179, 47, 0.1);
-                            color: #FFB32F;
-                        }
-    
-                        &--blue {
-                            background: rgba(33, 153, 248, 0.1);
-                            color: #2199F8;
-                        }
-    
-                        &--green {
-                            background: rgba(58, 173, 148, 0.1);
-                            color: #3AAD94;
-                        }
-                    }
-                }
-
-                .body {
-                    display: flex;
-                    flex-direction: column;
-                    gap: 6px;
-                    .detail-row {
-                        display: flex;
-                        gap: 8px;
-    
-                        &__label {
-                            width: 72px;
-                            min-width: 72px;
-                            flex-shrink: 0;
-                            color: rgba(111, 114, 116, 0.6);
-                        }
-    
-                        &__value {
-                            color: #565656;
-                        }
-
-                        &__control {
-                            flex: 1;
-                            min-width: 0;
-                        }
-
-                        &--field {
-                            align-items: center;
-
-                            :deep(.el-input__wrapper) {
-                                border: 1px solid rgba(33, 153, 248, 0.5);
-                                border-radius: 4px;
-                                box-shadow: none;
-                            }
-
-                            :deep(.el-input__inner) {
-                                color: #2199F8;
-                            }
-
-                            :deep(.el-input__prefix),
-                            :deep(.el-input__suffix) {
-                                color: #2199F8;
-                            }
-                        }
-
-                        &--textarea {
-                            align-items: flex-start;
-
-                            .detail-row__label {
-                                padding-top: 6px;
-                            }
-
-                            :deep(.el-textarea__inner) {
-                                border: 1px solid rgba(33, 153, 248, 0.5);
-                                border-radius: 4px;
-                                box-shadow: none;
-                                color: #2199F8;
-                            }
-                        }
-                    }
-                }
-
-            }
-
-            .info-item {
-                .info-title {
-                    display: flex;
-                    align-items: center;
-                    gap: 4px;
-                    color: #2199F8;
-                    font-weight: 500;
-
-                    .title-block {
-                        width: 6px;
-                        height: 6px;
-                        background: #2199F8;
-                        border-radius: 1px;
-                        transform: rotate(45deg);
-                    }
-                }
-
-                .info-value {
-                    padding-top: 4px;
-                    color: #ACACAC;
-                    line-height: 21px;
-                    white-space: pre-line;
-                }
-
-                .blod-text {
-                    font-weight: 500;
-                }
-            }
-
-            .info-item+.info-item {
-                margin-top: 10px;
-            }
-        }
-    }
-
-    .custom-bottom-fixed-btns {
-        justify-content: space-between;
-
-        &--single {
-            justify-content: center;
-        }
-
-        .secondary-btn{
-            background: #F6F6F6;
-            border: none;
-        }
-        .primary-btn{
-            background: #2199F8;
-            border: none;
-        }
-        .orange-btn{
-            background: #FF953D;
-            color: #fff;
-        }
-        .green-btn{
-            background: #4ABF32;
-            color: #fff;
-        }
-    }
-}
-</style>

+ 0 - 97
src/views/old_mini/agri_record_detail/map/index.js

@@ -1,97 +0,0 @@
-import * as KMap from "@/utils/ol-map/KMap";
-import * as util from "@/common/ol_common.js";
-import config from "@/api/config.js";
-import Style from "ol/style/Style";
-import Icon from "ol/style/Icon";
-import { Point } from 'ol/geom';
-import Feature from "ol/Feature";
-import { reactive } from "vue";
-
-export let mapLocation = reactive({
-  data: null,
-});
-
-/**
- * @description 地图层对象
- */
-class IndexMap {
-  constructor() {
-    let that = this;
-    let vectorStyle = new KMap.VectorStyle();
-    this.vectorStyle = vectorStyle;
-
-    // 位置图标
-    this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
-      style: (f) => {
-        return new Style({
-          image: new Icon({
-            src: require("@/assets/img/map/map_point.png"),
-            scale: 0.5,
-          }),
-        });
-      },
-    });
-  }
-
-  initMap(location, target) {
-    let level = 16;
-    let coordinate = util.wktCastGeom(location).getFirstCoordinate();
-    this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22);
-    let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png";
-    this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2);
-    // this.kmap.addLayer(this.clickPointLayer.layer);
-    this.setMapPoint(coordinate)
-    this.addMapSingerClick()
-  }
-
-  setMapPosition(center) {
-    this.kmap.getView().animate({
-      center,
-      zoom: 16,
-      duration: 0,
-    });
-    this.setMapPoint(center)
-  }
-
-  setMapPoint(coordinate) {
-    this.clickPointLayer.source.clear()
-    let point = new Feature(new Point(coordinate))
-    this.clickPointLayer.addFeature(point)
-  }
-
-  // 地图点击事件
-  addMapSingerClick() {
-    let that = this;
-    that.kmap.on("singleclick", (evt) => {
-      // that.kmap.map.forEachFeatureAtPixel(evt.pixel, function (feature, layer) {
-      //   if ( layer instanceof VectorLayer && layer.get("name") === "reportPolygonLayer" ) {
-      //     areaId.data = feature.get("id")
-      //   }
-      // });
-      that.setMapPoint(evt.coordinate)
-      mapLocation.data = evt.coordinate
-    });
-  }
-
-  clearLayer() {
-    // this.kmap.removeLayer(this.clickPointLayer.layer)
-    this.kmap.polygonLayer.source.clear();
-  }
-
-  setAreaGeometry(geometryArr) {
-    this.clearLayer()
-    let that = this
-    geometryArr.map(item => {
-      that.kmap.setLayerWkt(item)
-    })
-    this.fitView()
-  }
-
-  fitView(){
-    let extent = this.kmap.polygonLayer.source.getExtent()
-    // 地图自适应到区域可视范围
-    this.kmap.getView().fit(extent, { duration: 500, padding: [100, 100, 100, 100] });
-  }
-}
-
-export default IndexMap;

+ 0 - 106
src/views/old_mini/entry_information/components/addMachinePopup.vue

@@ -1,106 +0,0 @@
-<template>
-    <popup
-        v-model:show="showValue"
-        round
-        closeable
-        class="add-machine-popup"
-        :close-on-click-overlay="false"
-        teleport="body"
-    >
-        <div class="add-machine-popup__content">
-            <div class="form-block">
-                <div class="form-block__title">农机名称</div>
-                <el-input v-model="form.name" placeholder="请输入农机名称" />
-            </div>
-
-            <div class="add-machine-popup__confirm" @click="handleConfirm">确认</div>
-        </div>
-    </popup>
-</template>
-
-<script setup>
-import { computed, reactive, watch } from "vue";
-import { Popup } from "vant";
-import { ElMessage } from "element-plus";
-
-const props = defineProps({
-    show: {
-        type: Boolean,
-        default: false,
-    },
-});
-
-const emit = defineEmits(["update:show", "confirm"]);
-
-const form = reactive({
-    name: "",
-});
-
-const showValue = computed({
-    get: () => props.show,
-    set: (value) => emit("update:show", value),
-});
-
-watch(
-    () => props.show,
-    (val) => {
-        if (val) {
-            form.name = "";
-        }
-    }
-);
-
-const handleConfirm = () => {
-    const name = String(form.name || "").trim();
-    if (!name) {
-        ElMessage.warning("请输入农机名称");
-        return;
-    }
-    emit("confirm", { name });
-    emit("update:show", false);
-};
-</script>
-
-<style scoped lang="scss">
-.add-machine-popup {
-    width: 330px;
-
-    :deep(.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;
-        }
-    }
-
-    :deep(.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>

+ 2 - 2
src/views/old_mini/growth_report/components/CropAlertPanel.vue

@@ -766,8 +766,8 @@ defineExpose({ refreshAlerts: fetchAlertCards, refreshZones: fetchZones, reloadF
         height: 20px;
         padding: 0 5px;
         border-radius: 2px;
-        background: #2199f8;
-        color: #fff;
+        background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+        color: #1A1408;
         backdrop-filter: blur(4px);
         font-size: 12px;
         line-height: 20px;

+ 210 - 106
src/views/old_mini/growth_report/index.vue

@@ -22,22 +22,23 @@
         </div>
 
         <div class="growth-top">
-            <!-- 浮窗:长势互动 -->
-            <div v-if="!showInteractCard" class="interact-float">
-                <div class="interact-card">
-                    <el-icon class="interact-card__close" @click="showInteractCard = true">
+            <!-- 浮窗:长势互动(按 物候 → 异常 → 土壤根系 依次展示) -->
+            <div v-if="currentCard" class="interact-float">
+                <div class="interact-card" :class="`interact-card--${currentCard.kind}`">
+                    <el-icon class="interact-card__close" @click="dismissCurrentCard">
                         <Close />
                     </el-icon>
                     <div class="interact-card__qa">
                         <div class="interact-card__question">
                             <div class="interact-card__question-icon">
-                                <img class="interact-card__question-icon-img" :src="periodIcon" alt="">
+                                <img class="interact-card__question-icon-img" :src="currentCard.icon" alt="">
                             </div>
-                            <span>{{ interactQuestionText }}</span>
+                            <span>{{ currentCard.title }}</span>
                         </div>
-                        <div class="interact-card__options">
+                        <!-- 物候 / 土壤根系:选项卡片 -->
+                        <div v-if="currentCard.kind === 'options'" class="interact-card__options">
                             <div
-                                v-for="item in interactOptions"
+                                v-for="item in currentCard.options"
                                 :key="item.id"
                                 class="interact-option"
                                 :class="{
@@ -49,6 +50,14 @@
                                 <span class="interact-option__text">{{ item.name }}</span>
                             </div>
                         </div>
+                        <!-- 异常:结论描述 + 立即巡园 -->
+                        <div v-else class="interact-card__abnormal">
+                            <div class="interact-card__desc">{{ currentCard.desc }}</div>
+                            <div class="interact-card__cta" @click="handlePatrolNow">
+                                立即巡园
+                                <el-icon><ArrowRightBold /></el-icon>
+                            </div>
+                        </div>
                     </div>
                 </div>
             </div>
@@ -281,15 +290,16 @@ const goAddZone = (type) => {
     });
 };
 
-const showInteractCard = ref(false);
-const interactQuestionText = ref("物候问题物候问题");
-const interactOptions = ref([{id: 1, name: "未到"}, {id: 2, name: "顺利进入"}, {id: 3, name: "已过"}]);
+// ---------- 顶部巡园互动浮窗 ----------
+/** 卡片队列:物候(pheno) → 异常(abnormal) → 土壤根系(soil_root),缺失分块不入队 */
+const interactCards = ref([]);
+const currentCardIndex = ref(0);
+const interactDismissed = ref(false);
+const currentCard = computed(() =>
+    interactDismissed.value ? null : interactCards.value[currentCardIndex.value] || null
+);
 const selectedOptionId = ref(null);
 const interactSubmitting = ref(false);
-/** getOuterRecords 返回的 records */
-const outerRecords = ref([]);
-/** 当前展示的 records 下标 */
-const currentRecordIndex = ref(-1);
 /** 防止切换分区时旧请求回写 */
 let interactFetchToken = 0;
 
@@ -310,7 +320,7 @@ function getRequestErrorMessage(err) {
     return "提交失败,请稍后再试";
 }
 
-/** interact_options 可能是 JSON 字符串或数组 */
+/** interact_options / options 可能是 JSON 字符串、字符串数组或 {label,value} 数组 */
 function parseInteractOptions(raw) {
     let list = raw;
     if (typeof raw === "string") {
@@ -321,61 +331,122 @@ function parseInteractOptions(raw) {
         }
     }
     if (!Array.isArray(list)) return [];
-    return list.map((name, index) => ({
-        id: index + 1,
-        name: String(name),
-    }));
+    return list
+        .map((item, index) => ({
+            id: index + 1,
+            name:
+                item && typeof item === "object"
+                    ? String(item.label ?? item.value ?? "")
+                    : String(item),
+        }))
+        .filter((item) => item.name);
+}
+
+function resolveSectionOptions(section = {}) {
+    return parseInteractOptions(section.interact_options ?? section.options);
+}
+
+/** getTypesTask 分块 → 按顺序的浮窗卡片(无对应分块则不显示) */
+function buildInteractCards(data = {}, ctx = {}) {
+    const cards = [];
+    const pushOptionsCard = (key, section, icon) => {
+        if (!section) return;
+        const title =
+            section.interact_problem ||
+            section.question ||
+            section.interact_title ||
+            section.interaction_title ||
+            section.title ||
+            "";
+        const options = resolveSectionOptions(section);
+        if (!title && !options.length) return;
+        cards.push({ key, kind: "options", icon, title, options, raw: section, ...ctx });
+    };
+
+    // 1. 物候:问题 + 选项
+    pushOptionsCard("pheno", data.pheno, periodIcon);
+
+    // 2. 异常:结论标题 + 描述 + 立即巡园
+    if (data.abnormal) {
+        const section = data.abnormal;
+        const title = section.title || section.interaction_title || "";
+        const desc =
+            section.highlight ||
+            section.interaction_theme ||
+            section.interaction_reason ||
+            section.inspection_guide ||
+            "";
+        if (title || desc) {
+            cards.push({
+                key: "abnormal",
+                kind: "abnormal",
+                icon: lightningIcon,
+                title,
+                desc,
+                raw: section,
+                ...ctx,
+            });
+        }
+    }
+
+    // 3. 土壤根系:问题 + 选项
+    pushOptionsCard("soil_root", data.soil_root, soilIcon);
+    return cards;
 }
 
 function clearInteractCard() {
-    outerRecords.value = [];
-    currentRecordIndex.value = -1;
-    interactQuestionText.value = "";
-    interactOptions.value = [];
+    interactCards.value = [];
+    currentCardIndex.value = 0;
+    interactDismissed.value = false;
     selectedOptionId.value = null;
-    showInteractCard.value = false;
 }
 
-function applyOuterRecordAt(index) {
-    const records = outerRecords.value;
-    if (!records.length || index < 0 || index >= records.length) {
-        clearInteractCard();
-        return;
-    }
-    currentRecordIndex.value = index;
-    const record = records[index] || {};
-    const question = record.interact_problem || "";
-    const options = parseInteractOptions(record.interact_options);
-    interactQuestionText.value = question;
-    interactOptions.value = options;
+function applyInteractCards(data = {}, ctx = {}) {
+    interactCards.value = buildInteractCards(data, ctx);
+    currentCardIndex.value = 0;
+    interactDismissed.value = false;
     selectedOptionId.value = null;
-    showInteractCard.value = Boolean(question || options.length);
 }
 
-function buildInteractSubmitPayload(optionText) {
-    const record = outerRecords.value[currentRecordIndex.value] || {};
-    const zoneId = panelZoneId.value ?? getSelectedZoneId();
-    return {
-        zone_id: Number(zoneId),
-        date: formatToday(),
-        // interact_code: record.interact_code || "",
-        period_code: record.period_code || "",
-        // interact_option: optionText,
-    };
+/** 关闭当前卡 → 展示下一张;最后一张则整体收起 */
+function dismissCurrentCard() {
+    selectedOptionId.value = null;
+    if (currentCardIndex.value < interactCards.value.length - 1) {
+        currentCardIndex.value += 1;
+    } else {
+        interactDismissed.value = true;
+    }
 }
 
+/** 选项即答案:物候/土壤根系提交后切到下一张卡 */
 async function submitInteractOption(item) {
-    if (interactSubmitting.value) return;
+    const card = currentCard.value;
+    if (!card || interactSubmitting.value) return;
     selectedOptionId.value = item.id;
     interactSubmitting.value = true;
     try {
-        const res = await VE_API.questionnaire.saveInteractResult(
-            buildInteractSubmitPayload(item.name)
-        );
+        let res;
+        if (card.key === "pheno") {
+            // 物候感知结果沿用原有保存接口
+            res = await VE_API.questionnaire.saveInteractResult({
+                zone_id: Number(card.zoneId),
+                date: formatToday(),
+                period_code: card.raw?.period_code || "",
+            });
+        } else {
+            // 土壤根系:与异常互动同接口,按 fw_code 提交选项
+            res = await VE_API.questionnaire.confirmAbnormalInteract({
+                farm_id: card.farmId,
+                zone_id: card.zoneId,
+                calc_date: card.calcDate,
+                crop_type: card.cropType,
+                fw_code: card.raw?.fw_code || "",
+                options: item.name,
+            });
+        }
         if (res?.code === 200) {
-            ElMessage.success(res.msg || "提交成功");
-            showInteractCard.value = false;
-            await fetchInteractTask();
+            ElMessage.success(res?.msg || "提交成功");
+            dismissCurrentCard();
             return;
         }
         ElMessage.error(res?.msg || "提交失败,请稍后再试");
@@ -386,75 +457,70 @@ async function submitInteractOption(item) {
     }
 }
 
-/** 首项 → 上一条;末项 → 下一条;中间项提交 */
 async function handleInteractOptionClick(item) {
-    const options = interactOptions.value;
-    if (interactSubmitting.value || !item?.name || !options.length || currentRecordIndex.value < 0) {
-        return;
-    }
-    const isFirst = item.id === options[0]?.id;
-    const isLast = item.id === options[options.length - 1]?.id;
-    // 仅一项时按提交处理
-    if (options.length === 1 || (!isFirst && !isLast)) {
-        await submitInteractOption(item);
-        return;
-    }
-    selectedOptionId.value = item.id;
-    if (isFirst) {
-        if (currentRecordIndex.value > 0) {
-            applyOuterRecordAt(currentRecordIndex.value - 1);
-            return;
-        }
-        ElMessage.info("已是第一条");
-        return;
-    }
-    if (isLast) {
-        if (currentRecordIndex.value < outerRecords.value.length - 1) {
-            applyOuterRecordAt(currentRecordIndex.value + 1);
-            return;
-        }
-        ElMessage.info("已是最后一条");
-    }
+    if (interactSubmitting.value || !item?.name || !currentCard.value) return;
+    await submitInteractOption(item);
 }
 
+/** 异常卡「立即巡园」→ 异常互动巡园要点页 */
+const handlePatrolNow = () => {
+    const card = currentCard.value;
+    if (!card) return;
+    router.push({
+        path: "/abnormal_track",
+        query: {
+            id: card.zoneId,
+            ...(card.zoneId !== "" ? { zoneId: card.zoneId } : {}),
+        },
+    });
+    dismissCurrentCard();
+};
+
+/** 首页巡园要点:getTypesTask 一次返回 pheno / abnormal / soil_root */
 async function fetchInteractTask() {
+    const farm = getSelectedFarm();
     const zoneId = panelZoneId.value ?? getSelectedZoneId();
-    if (zoneId === "" || zoneId == null) {
+    const farmId = selectedFarmId.value || farm.farm_id || farm.id;
+    const cropType = resolveCropCode(farm);
+    if (
+        zoneId == null ||
+        zoneId === "" ||
+        farmId == null ||
+        farmId === "" ||
+        !cropType
+    ) {
         clearInteractCard();
         return;
     }
     const token = ++interactFetchToken;
     try {
-        const res = await VE_API.questionnaire.getOuterRecords({
+        const res = await VE_API.pheno.getTypesTask({
+            crop_type: cropType,
+            farm_id: farmId,
             zone_id: zoneId,
+            calc_date: formatToday(),
         });
         if (token !== interactFetchToken) return;
         if (res?.code !== 200) {
             clearInteractCard();
             return;
         }
-        const data = res?.data || {};
-        const records = Array.isArray(data.records) ? data.records : [];
-        outerRecords.value = records;
-        if (!records.length) {
-            clearInteractCard();
-            return;
-        }
-        // 默认展示与外层 interact_code 匹配的那一项
-        const matchedIndex = records.findIndex(
-            (item) => item?.interact_code && item.interact_code === data.interact_code
-        );
-        applyOuterRecordAt(matchedIndex >= 0 ? matchedIndex : 0);
+        applyInteractCards(res?.data || {}, {
+            zoneId,
+            farmId,
+            cropType,
+            calcDate: formatToday(),
+        });
     } catch (e) {
         if (token !== interactFetchToken) return;
-        console.warn("[growth_report] getOuterRecords failed", e);
+        console.warn("[growth_report] getTypesTask failed", e);
         clearInteractCard();
     }
 }
 
-/** 农场品类编码:接口 crop_code 对应缓存 category_code */
+/** 农场品类编码:接口 crop_code / crop_type 对应缓存 category_code */
 function resolveCropCode(farm = getSelectedFarm()) {
-    return farm.category_code || farm.categorycode || farm.crop_code || "";
+    return farm.category_code || farm.categorycode || farm.crop_code || farm.crop_type || "";
 }
 
 /** 本地 dem 路径转 mapimg 可访问地址:取文件名挂到 /mapimg/ */
@@ -584,7 +650,7 @@ const handleZoneChange = (zoneId) => {
     panelZoneId.value = zoneId != null && zoneId !== "" ? zoneId : null;
     fetchHomeMapCalc();
     fetchFarmRangeStatus();
-    // fetchInteractTask();
+    fetchInteractTask();
 };
 
 async function getLocationName(point = currentMapPoint.value) {
@@ -780,7 +846,7 @@ onActivated(() => {
     if (!applied) syncMapFromSelectedFarm();
     initMap();
     fetchFarmRangeStatus();
-    // fetchInteractTask();
+    fetchInteractTask();
 });
 </script>
 
@@ -840,16 +906,19 @@ onActivated(() => {
             cursor: pointer;
         }
 
-        .interact-card__qa {
-        }
-
         .interact-card__question {
             font-size: 16px;
             color: #fff;
             display: flex;
             align-items: center;
             gap: 12px;
+            padding-right: 20px;
+            > span {
+                flex: 1;
+                min-width: 0;
+            }
             .interact-card__question-icon {
+                flex-shrink: 0;
                 display: flex;
                 align-items: center;
                 justify-content: center;
@@ -859,7 +928,7 @@ onActivated(() => {
                 border: 1px solid rgba(255, 255, 255, 0.12);
                 border-radius: 8px;
                 .interact-card__question-icon-img {
-                    width: 13px;
+                    width: 15px;
                 }
             }
         }
@@ -870,6 +939,41 @@ onActivated(() => {
             margin-top: 10px;
         }
 
+        /* 异常卡:结论描述 + 立即巡园 */
+        .interact-card__abnormal {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+            margin-top: 10px;
+        }
+
+        .interact-card__desc {
+            flex: 1;
+            min-width: 0;
+            font-size: 14px;
+            line-height: 21px;
+            color: rgba(255, 255, 255, 0.6);
+            overflow: hidden;
+            word-break: break-all;
+        }
+
+        .interact-card__cta {
+            flex-shrink: 0;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            gap: 2px;
+            height: 32px;
+            padding: 0 10px 0 15px;
+            border-radius: 24px;
+            background: linear-gradient(90deg, #FFEBC9 0%, #FFCA6D 100%);
+            color: #000000;
+            font-size: 12px;
+            white-space: nowrap;
+            box-sizing: border-box;
+            cursor: pointer;
+        }
+
         .interact-option {
             position: relative;
             flex: 1;

+ 2 - 2
src/views/old_mini/recordDetails/components/RegionNamePopup.vue

@@ -149,8 +149,8 @@ const handleConfirm = () => {
         margin-top: 24px;
         padding: 8px;
         border-radius: 25px;
-        background: #2199f8;
-        color: #fff;
+        background: linear-gradient(135deg, #FFEBC9 0%, #FFCA6D 100%);
+        color: #1A1408;
         font-size: 16px;
         text-align: center;
     }

+ 1 - 1
src/views/old_mini/work_detail/components/areaMap.js

@@ -64,7 +64,7 @@ class AreaMap {
         const flySpeed = f.get("fly_speed");
         const labelStyle = f.get("label_style") || (flySpeed ? "card" : "legacy");
         const fillColor = f.get("fill_color") || "#00000080";
-        const strokeColor = f.get("stroke_color") || "#2199F8";
+        const strokeColor = f.get("stroke_color") || "#FFD489";
         const zoneName = f.get("zone_name") || (f.get("mianji") ? `${f.get("mianji")}亩` : "");
 
         const styles = [