2 Commits 6cb9349ec2 ... c954285ec3

Author SHA1 Message Date
  刘秀芳 c954285ec3 Merge branch 'farmer' of http://www.sysuimars.cn:3000/feiniao/feiniao-farm-h5 into farmer 6 days ago
  刘秀芳 2bb4b7f103 fix: 对接农事详情 6 days ago

+ 7 - 3
src/components/pageComponents/ArchivesFarmTimeLine.vue

@@ -806,9 +806,13 @@ watch(
 
 const handleStatusDetail = (fw) => {
     router.push({
-        path: props.pageType === 'agri_plan' ? "/agricultural_detail" : "/status_detail",
-        query: { farmId: props.farmId, regionId: props.regionId,date: fw.createTime.slice(0,10) },
-    });
+        path: "/work_detail",
+        query: { farmWorkLibId: fw.farmWorkLibId, farmId: props.farmId },
+    })
+    // router.push({
+    //     path: props.pageType === 'agri_plan' ? "/agricultural_detail" : "/status_detail",
+    //     query: { farmId: props.farmId, regionId: props.regionId,date: fw.createTime.slice(0,10) },
+    // });
     // if (props.pageType === 'agri_record') {
     //     router.push({
     //         path: "/status_detail",

+ 7 - 0
src/router/globalRoutes.js

@@ -494,4 +494,11 @@ export default [
         component: () => import("@/views/old_mini/youwei_trace/index.vue"),
         meta: { showTabbar: true, keepAlive: true },
     },
+    // 农事详情
+    {
+        path: "/work_detail",
+        name: "WorkDetail",
+        meta: { keepAlive: true },
+        component: () => import("@/views/old_mini/work_detail/index.vue"),
+    },
 ];

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

@@ -5,7 +5,7 @@
         <!-- 组件:天气 -->
         <weather-info ref="weatherInfoRef" class="weather-info" @weatherExpanded="weatherExpanded"
             @changeGarden="changeGarden" :isGarden="true"></weather-info>
-        <div class="report-content-wrap" v-if="hasReport" v-loading="loading">
+        <div class="report-content-wrap" v-if="hasReport" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.1)">
             <swipe class="my-swipe" :loop="false" indicator-color="white" @change="handleSwipeChange">
                 <swipe-item v-for="(item, index) in regionsData" :key="index">
                     <div class="report-content has-report" :style="{ minHeight: `calc(100vh - ${tabBarHeight}px)` }">

+ 43 - 29
src/views/old_mini/interaction/index.vue

@@ -207,16 +207,20 @@ const getFirstSelectedVariety = () => {
 
 const handleConfirm = async () => {
     loading.value = true;
-    // 组装为 initFarmData 接口所需的 farmDataList 结构
+    // INIT / 新增品种:initFarmData;CONFIRM_REPRODUCTIVE:confirmReproductivePhenology(regionPhenologyItems,每条一个区域)
     const status = farmData.value.status;
-    const farmDataList = farmData.value.farms
-        .map((farm) => {
-            const typeIdItems = (farm.regions || [])
-                .filter(
-                    (r) => r.regionId && r.phenologyId && r.phenologyStartDate
-                )
-                .map((r) => {
-                    if (status === 'INIT' || isAddVariety.value) {
+    const isInitOrAddVariety = status === "INIT" || isAddVariety.value;
+
+    let farmDataList;
+    if (isInitOrAddVariety) {
+        farmDataList = farmData.value.farms
+            .map((farm) => {
+                const typeIdItems = (farm.regions || [])
+                    .filter(
+                        (r) =>
+                            r.regionId && r.phenologyId && r.phenologyStartDate
+                    )
+                    .map((r) => {
                         const origin =
                             (farm.typeIdItems || []).find(
                                 (t) => String(t.id) === String(r.regionId)
@@ -227,29 +231,37 @@ const handleConfirm = async () => {
                             phenologyId: r.phenologyId,
                             phenologyStartDate: r.phenologyStartDate,
                         };
-                    } else {
-                        return {
+                    })
+                    .filter((item) => item.id);
+
+                return {
+                    farmId: farm.farmId,
+                    speciesId: farm.speciesId,
+                    typeIdItems,
+                };
+            })
+            .filter((item) => item.typeIdItems.length > 0);
+    } else {
+        // CONFIRM_REPRODUCTIVE:每个区域单独一条,字段 regionPhenologyItems
+        farmDataList = [];
+        for (const farm of farmData.value.farms) {
+            const validRegions = (farm.regions || []).filter(
+                (r) => r.regionId && r.phenologyId && r.phenologyStartDate
+            );
+            for (const r of validRegions) {
+                farmDataList.push({
+                    farmId: farm.farmId,
+                    regionPhenologyItems: [
+                        {
                             regionId: r.regionId,
                             phenologyId: r.phenologyId,
                             phenologyStartDate: r.phenologyStartDate,
-                        }
-                    }
-                })
-                .filter((item) => {
-                    if (status === 'INIT' || isAddVariety.value) {
-                        return item.id;
-                    } else {
-                        return item.regionId;
-                    }
+                        },
+                    ],
                 });
-
-            return {
-                farmId: farm.farmId,
-                speciesId: farm.speciesId,
-                typeIdItems,
-            };
-        })
-        .filter((item) => item.typeIdItems.length > 0);
+            }
+        }
+    }
 
     if (!farmDataList.length) {
         ElMessage.warning("请完善信息");
@@ -261,7 +273,9 @@ const handleConfirm = async () => {
         farmDataList,
     };
 
-    const { code, msg } = await VE_API.farm_v3[status === 'INIT' || isAddVariety.value ? 'initFarmData' : 'confirmReproductivePhenology'](params);
+    const { code, msg } = await VE_API.farm_v3[
+        isInitOrAddVariety ? "initFarmData" : "confirmReproductivePhenology"
+    ](params);
     if (code === 0) {
         loading.value = false;
         // 从点击新增品种进来的才需要勾选区域

+ 141 - 0
src/views/old_mini/work_detail/components/areaMap.js

@@ -0,0 +1,141 @@
+import * as KMap from "@/utils/ol-map/KMap";
+import * as util from "@/common/ol_common.js";
+import config from "@/api/config.js";
+import { Vector as VectorSource } from "ol/source.js";
+import { Point } from 'ol/geom';
+import { newPoint, newAreaFeature } from "@/utils/map";
+import { GeoJSON, WKT } from 'ol/format'
+import { Feature } from "ol";
+import { getArea } from "ol/sphere"
+import * as turf from "@turf/turf"
+import Style from "ol/style/Style";
+import Icon from "ol/style/Icon";
+import { Fill, Text, Stroke } from "ol/style";
+import * as proj from "ol/proj";
+import proj4 from "proj4"
+import { register } from "ol/proj/proj4";
+proj4.defs("EPSG:38572", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs");
+register(proj4);
+
+/**
+ *
+ */
+class AreaMap {
+    constructor() {
+        let that = this;
+        let vectorStyle = new KMap.VectorStyle()
+        this.gardenPolygonLayer = new KMap.VectorLayer("gardenPolygonLayer", 999, {
+            minZoom: 11,
+            maxZoom: 22,
+            source: new VectorSource({}),
+            style: function (f) {
+                let style2 = vectorStyle.getPolygonStyle("#00000080", "#2199F8", 2)
+
+                let style3 = new Style({
+                    text: new Text({
+                        font: "14px sans-serif",
+                        text: f.get("mianji") + "亩",
+                        // offsetX: 28,
+                        offsetY: 10,
+                        fill: new Fill({ color: "#fff" }), // 字体颜色
+                    }),
+                });
+                return [style2]
+            }
+        });
+
+        // 位置图标
+        this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
+            style: (f) => {
+                let pointIcon = new Style({
+                    image: new Icon({
+                        src: require("@/assets/img/home/garden-point.png"),
+                        scale: 0.5,
+                        anchor: [0.5, 1],
+                    }),
+                });
+                let nameText = new Style({
+                    text: new Text({
+                        font: "14px sans-serif",
+                        text: f.get("name"),
+                        offsetY: 10,
+                        fill: new Fill({ color: "#fff" }), // 字体颜色
+                        stroke: new Stroke({
+                            color: "#000",
+                            width: 0.5,
+                        }),
+                    }),
+                });
+                return [pointIcon, nameText]
+            },
+        });
+    }
+
+
+    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.gardenPolygonLayer.layer);
+        this.kmap.addLayer(this.clickPointLayer.layer);
+    }
+
+    initLayer(item) {
+        // this.gardenPolygonLayer.refresh()
+        if (this.gardenPolygonLayer.source) {
+            this.gardenPolygonLayer.source.clear()
+        }
+        if (item.pointWkt) {
+            let f = newPoint(item, "pointWkt")
+            f.set("name", item.name)
+            this.clickPointLayer.source.addFeature(f)
+        }
+        console.log('initLayer', item)
+        if (item.geomWkt) {
+            // let f = new Feature({
+            //     organId: item.organId, // 用于查找点击选中地块的编辑,有多个地块时用id筛选
+            //     geometry: new WKT().readGeometry(item.geomWkt)
+            // })
+            // let geometry = new WKT().readGeometry(item.geomWkt)
+            // geometry.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572"))
+            let f = newAreaFeature(item, "geomWkt")
+            //   let area = getArea(geometry)
+            //   area = (area + area / 2) / 1000;
+            f.set("mianji", item.mianji)
+            this.gardenPolygonLayer.source.addFeature(f)
+        }
+        this.fitView()
+    }
+
+    /**
+   * 调整地图视图以适应地块范围
+   */
+    fitView() {
+        let extent = this.gardenPolygonLayer.source.getExtent()
+        if (extent) {
+            // 地图自适应到区域可视范围
+            this.kmap.getView().fit(extent, { duration: 50, padding: [80, 80, 80, 80] });
+        }
+    }
+
+    fitByGardenId(gardenId, hasMapAnimate) {
+        this.gardenPolygonLayer.source.forEachFeature((f) => {
+            if (f.get("organId") == gardenId) {
+                const extent = f.getGeometry().getExtent()
+                this.kmap.getView().fit(extent, { padding: [160, 60, 340, 60], duration: hasMapAnimate ? 1500 : 0 });
+                const currentZoom = this.kmap.getView().getZoom();
+                if (currentZoom > 16) {
+                    // this.kmap.getView().setZoom(16);
+                    this.kmap.getView().animate({
+                        zoom: 16,
+                        duration: hasMapAnimate ? 1500 : 0 // 动画持续时间,单位为毫秒
+                    });
+                }
+            }
+        })
+    }
+}
+
+export default AreaMap;

+ 292 - 0
src/views/old_mini/work_detail/components/executePopup.vue

@@ -0,0 +1,292 @@
+<template>
+    <popup v-model:show="showValue" class="execute-popup" closeable>
+        <div class="popup-content">
+            <div class="time-wrap">
+                <div class="name pb-10">
+                    请选择实际执行时间
+                </div>
+                <div class="time-input">
+                    <el-date-picker format="YYYY-MM-DD" value-format="YYYY-MM-DD" v-model="executeTime"
+                        :disabled-date="disabledDate" size="large" style="width: 100%" type="date" placeholder="请选择日期"
+                        :editable="false" />
+                </div>
+            </div>
+            <div class="upload-wrap" :class="{ 'upload-cont': fileList.length }">
+                <div class="name">农事凭证</div>
+                <div class="sub-name">肥料使用照片或者执行照片</div>
+                <div class="tips-text">由于系统审核,您照片拍摄位置与农场相差超 3 公里,请在农场现场使用水印相机重新拍摄上传,谢谢配合</div>
+                <uploader class="uploader" v-model="fileList" multiple :max-count="5" :after-read="afterRead"
+                    @click="handleClick('rg')">
+                    <img class="img" v-show="!fileList.length" src="@/assets/img/home/example-4.png" alt="" />
+                    <img class="plus" src="@/assets/img/home/plus.png" alt="" />
+                </uploader>
+                <div class="service-input">
+                    <el-input v-model="serviceInput" placeholder="请输入农资机构名称" />
+                </div>
+            </div>
+            <div class="button-wrap">
+                <div @click="closeTask" class="button primary">确认上传</div>
+            </div>
+        </div>
+    </popup>
+
+    <popup v-model:show="showRemindValue" class="execute-popup" closeable>
+        <div class="popup-content">
+            <div class="name">
+                <span class="required">*</span>请选择下次提醒时间
+            </div>
+            <div class="time-number-input">
+                <el-input-number v-model="remindTime" :min="1" :max="30" :step="1" size="large" style="width: 100%" />
+                <span class="time-unit">天后</span>
+            </div>
+            <div class="button-wrap">
+                <div @click="handleRemind" class="button primary">确认</div>
+            </div>
+        </div>
+    </popup>
+</template>
+
+<script setup>
+import { Popup, Uploader } from "vant";
+import { ref } from "vue";
+import { ElMessage } from "element-plus";
+import { getFileExt } from "@/utils/util";
+import UploadFile from "@/utils/upliadFile";
+import { useStore } from "vuex";
+
+const store = useStore();
+const miniUserId = store.state.home.miniUserId;
+
+const props = defineProps({
+    executionData: {
+        type: Object,
+        default: () => ({}),
+    },
+});
+
+const showValue = ref(false);
+
+const fileList = ref([]);
+const fileArr = ref([]);
+const executeTime = ref("");
+const serviceInput = ref("");
+const uploadFileObj = new UploadFile();
+
+const afterRead = (file) => {
+    // 处理多张照片的情况:file 可能是数组
+    const files = Array.isArray(file) ? file : [file];
+
+    files.forEach((item) => {
+        // 将文件上传至服务器
+        let fileVal = item.file;
+        if (!fileVal) return; // 如果没有 file 属性,跳过
+
+        item.status = "uploading";
+        item.message = "上传中...";
+        let ext = getFileExt(fileVal.name);
+        let key = `birdseye-look-mini/${miniUserId}/${new Date().getTime()}.${ext}`;
+        uploadFileObj.put(key, fileVal).then((resFilename) => {
+            item.status = "done";
+            item.message = "";
+            fileArr.value.push(resFilename);
+        }).catch(() => {
+            item.status = 'failed';
+            item.message = '上传失败';
+            ElMessage.error('图片上传失败,请稍后再试!')
+        });
+    });
+};
+
+function closeTask() {
+    // stepIndex.value = 2
+    if (!fileArr.value.length) return ElMessage.warning('请上传至少两张图片')
+    if (!executeTime.value) return ElMessage.warning('请选择实际执行时间')
+    console.log(fileArr.value, executeTime.value)
+    const params = {
+        // recordId: executionData.value.id,
+        executeDate: executeTime.value,
+        executeEvidence: fileArr.value,
+    }
+    // VE_API.z_farm_work_record.addExecuteImgAndComplete(params).then((res) => {
+    //     if (res.code === 0) {
+    //         ElMessage.success('上传成功')
+    //         showValue.value = false
+    //     }
+    // })
+}
+
+const handleClick = () => {
+    // 预留点击上传区域的埋点或其它逻辑
+};
+
+const disabledDate = (time) => {
+    // 示例:不允许选择未来日期
+    return time.getTime() > Date.now();
+};
+
+const showRemindValue = ref(false);
+const remindTime = ref(1);
+
+function showRemindPopup() {
+    showRemindValue.value = true;
+}
+
+function handleRemind() {
+    if (!remindTime.value) return ElMessage.warning('请选择下次提醒时间')
+    console.log(remindTime.value)
+    showRemindValue.value = false;
+}
+
+function openPopup() {
+    showValue.value = true;
+}
+
+defineExpose({
+    openPopup,
+    showRemindPopup,
+});
+</script>
+
+<style lang="scss" scoped>
+.execute-popup {
+    width: 90%;
+    border-radius: 8px;
+
+    .popup-content {
+        padding: 24px 16px 20px 16px;
+        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;
+    // padding-bottom: 12px;
+
+    .required {
+        color: #ff4d4f;
+        margin-right: 2px;
+    }
+}
+
+.sub-name {
+    font-size: 12px;
+    color: rgba(0, 0, 0, 0.4);
+    padding-bottom: 12px;
+}
+
+.tips-text{
+    color: #FA7406;
+    padding: 5px 10px;
+    border: 1px solid #FA7406;
+    border-radius: 5px;
+    margin-bottom: 10px;
+    background: #fff;
+}
+
+.service-input {
+    padding-top: 12px;
+}
+
+.upload-wrap {
+
+    &.upload-cont {
+        ::v-deep {
+            .van-uploader__wrapper {
+                flex-wrap: nowrap;
+            }
+        }
+    }
+
+    .img {
+        width: 80px;
+        height: 80px;
+        margin-right: 12px;
+    }
+
+    .plus {
+        margin-right: 12px;
+        width: 80px;
+        height: 80px;
+    }
+}
+
+.upload-wrap {
+    ::v-deep {
+        .van-image__img {
+            border-radius: 8px;
+        }
+
+        .avatar-uploader .el-upload {
+            width: 100%;
+            border: 1px dashed #dddddd;
+            border-radius: 6px;
+            cursor: pointer;
+            position: relative;
+            overflow: hidden;
+        }
+
+        .van-uploader,
+        .van-uploader__wrapper,
+        .van-uploader__input-wrapper {
+            width: 100%;
+        }
+
+        .el-icon.avatar-uploader-icon {
+            font-size: 28px;
+            color: #8c939d;
+            width: 100%;
+            height: 128px;
+            text-align: center;
+            background: #f6f6f6;
+        }
+    }
+}
+
+.button-wrap {
+    display: flex;
+    justify-content: center;
+    margin: 24px 4px 0 4px;
+
+    .button {
+        border-radius: 20px;
+        color: #fff;
+        padding: 7px 0;
+        text-align: center;
+        font-size: 16px;
+
+        &.primary {
+            flex: 1;
+            background: #2199f8;
+            color: #fff;
+        }
+    }
+}
+
+.time-wrap {
+    margin-bottom: 12px;
+
+    .time-input {
+        width: 100%;
+    }
+}
+
+.time-number-input {
+    display: flex;
+    align-items: center;
+    width: 100%;
+
+    .time-unit {
+        font-size: 14px;
+        color: rgba(0, 0, 0, 0.4);
+        flex: none;
+        margin-left: 8px;
+    }
+}
+</style>

+ 91 - 0
src/views/old_mini/work_detail/components/mapInfo.vue

@@ -0,0 +1,91 @@
+<template>
+    <div class="map-info">
+        <!-- <div class="popup-title" v-if="showTitle">执行区域</div> -->
+        <div class="map-box">
+            <div class="map" ref="mapContainer"></div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, nextTick, onMounted, watch } from "vue";
+import IndexMap from "@/views/old_mini/home/map/index.js";
+import AreaMap from "./areaMap.js";
+
+const props = defineProps({
+    farmId: {
+        type: [Number, String],
+        default: null,
+    },
+    showTitle: {
+        type: Boolean,
+        default: true,
+    },
+});
+
+const mapContainer = ref(null);
+const indexMap = new IndexMap();
+const areaMap = new AreaMap();
+
+// 加载农场地图信息
+const loadFarmMap = async () => {
+    // if (!props.farmId) return;
+
+    try {
+        const res = await VE_API.farm.getFarmDetail({ farmId: 98230 });
+        if (res.code !== 0 || !res.data) return;
+
+        const farmInfo = res.data;
+        const point = farmInfo.pointWkt;
+
+        nextTick(() => {
+            // 如果地图已经初始化,则更新中心点和地块;否则初始化地图
+            if (areaMap.kmap) {
+                areaMap.initLayer(farmInfo);
+            } else if (point && mapContainer.value) {
+                areaMap.initMap(point, mapContainer.value);
+                areaMap.initLayer(farmInfo);
+            }
+        });
+    } catch (e) {
+        // 忽略地图加载错误,避免影响主流程
+    }
+};
+
+onMounted(() => {
+    loadFarmMap();
+});
+
+// farmId 变化时重新加载地图
+watch(
+    () => props.farmId,
+    () => {
+        loadFarmMap();
+    }
+);
+</script>
+
+<style lang="scss" scoped>
+.map-info {
+    width: 100%;
+}
+    .popup-title {
+        text-align: center;
+        font-size: 24px;
+        font-family: "PangMenZhengDao";
+        padding-bottom: 12px;
+    }
+
+.map-box {
+    width: 100%;
+    height: 350px;
+    position: relative;
+
+    .map {
+        width: 100%;
+        height: 100%;
+        clip-path: inset(0px round 5px);
+        pointer-events: none;
+    }
+}
+</style>

+ 869 - 0
src/views/old_mini/work_detail/index.vue

@@ -0,0 +1,869 @@
+<template>
+    <div class="work-detail">
+        <custom-header name="农事详情" :showClose="false" isGoBack @goback="handleBack" />
+
+        <div class="work-detail-content">
+            <!-- 顶部状态 -->
+            <div class="content-status" :class="'status-' + detail?.flowStatus">
+                <div class="status-l">
+                    <div class="status-title">{{ handleTagType(detail?.flowStatus) }}</div>
+                    <div class="status-sub" v-if="triggerDateText && detail?.flowStatus === 0">
+                        执行时间已经过去 {{ daysDiff }} 天了
+                    </div>
+                    <div class="status-sub" v-if="detail?.flowStatus === 1">
+                        距离执行时间还差 {{ daysDiff }} 天
+                    </div>
+                    <div class="status-sub" v-if="!detail?.flowStatus || detail?.flowStatus === 2">
+                        预计触发时间 {{ detail?.activateTime ? formatDate(detail.activateTime) : "--" }}
+                    </div>
+                </div>
+            </div>
+
+            <div class="work-wrap">
+                <!-- 农事组信息 -->
+                <div class="box-wrap group-info group-box">
+                    <div class="group-name">
+                        该农事为 <span class="light-text">限时溯源农事</span> ,请在 <span class="light-text">3天内</span>
+                        完成溯源认证上传,如果逾期未认证,该农事将不可溯源认证,且不计入飞鸟有味平台
+                    </div>
+                </div>
+
+                <!-- 每一段农事 -->
+                <div v-for="(prescription, index) in stageList" :key="index" class="box-wrap stage-card">
+                    <div class="work-info">
+                        <div class="stage-header">
+                            <div class="stage-title">{{ detail.farmWorkName }}</div>
+                        </div>
+
+                        <div class="stage-info">
+                            <div class="form-item">
+                                <div class="item-name">农事目的</div>
+                                <div class="item-text">
+                                    {{ detail.purpose || prescription.name || "--" }}
+                                </div>
+                            </div>
+                            <div class="form-item">
+                                <div class="item-name">农事时间</div>
+                                <div class="item-text">
+                                    {{ detail.intervelTime ? `间隔 ${detail.intervelTime} 天后 执行` : "--" }}
+                                </div>
+                            </div>
+                            <div class="form-item">
+                                <div class="item-name">执行区域</div>
+                                <div class="item-text light-text area-text">
+                                    桂味种植区域
+                                    <!-- <div class="area-btn" @click="handleViewArea">查看区域</div> -->
+                                    <div class="area-btn area-btn-right" @click="toDraw">建议勾选<el-icon><ArrowRight /></el-icon></div>
+                                </div>
+                            </div>
+                            <div class="form-item">
+                                <div class="item-name">注意事项</div>
+                                <div class="item-text">
+                                    {{ detail.remark || "--" }}
+                                </div>
+                            </div>
+                            <div class="form-item">
+                                <div class="item-name">药肥处方</div>
+                            </div>
+                        </div>
+
+                        <!-- 执行方式 -->
+                        <div class="stage-tabs">
+                            <div v-for="tab in executionTabs" :key="tab.value" class="tab-pill"
+                                :class="{ active: getStageExecutionMethod(index) === tab.value }"
+                                @click="changeExecutionMethod(index, tab.value)">
+                                {{ tab.label }}
+                            </div>
+                        </div>
+
+                        <!-- 药物处方表 -->
+                        <div class="prescription-wrap"
+                            v-if="prescription.pesticideList && prescription.pesticideList.length">
+                            <div class="prescription-table">
+                                <div class="table-header">
+                                    <div class="col col-type">使用功效</div>
+                                    <div class="col col-name">药肥名称</div>
+                                    <div class="col col-ratio">药肥配比</div>
+                                </div>
+
+                                <div v-for="(item, i) in prescription.pesticideList" :key="i" class="table-row">
+                                    <div class="col col-type">
+                                        {{ item.typeName || "--" }}
+                                    </div>
+                                    <div class="col col-name">
+                                        {{ item.name || item.pesticideFertilizerName || "--" }}
+                                    </div>
+                                    <div class="col col-ratio">
+                                        {{ getPesticideParam(item, index)?.ratio || "--" }}倍
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-if="hasRemark(prescription, index)" class="prescription-remark">
+                                <span v-for="(item, idx) in [prescription.pesticideList[0]]" :key="idx">
+                                    <template v-if="getParamRemark(item, index)">
+                                        {{ getParamRemark(item, index) }}
+                                        <br />
+                                    </template>
+                                </span>
+                            </div>
+                        </div>
+                    </div>
+
+
+                    <!-- 农事凭证 -->
+                    <div class="work-info photo-box" v-if="prescription.cropAlbum && prescription.cropAlbum.length">
+                        <div class="photo-title">农事凭证</div>
+                        <div class="photo-sub-title" v-if="info?.appType === 1">来自于 某某某农资机构</div>
+                        <div class="photo-img-wrap">
+                            <photo-provider :photo-closable="true">
+                                <photo-consumer v-for="(src, index) in prescription.cropAlbum" intro="农事凭证" :key="index"
+                                    :src="base_img_url2 + src.filename">
+                                    <div class="photo-img">
+                                        <img :src="base_img_url2 + src.filename" />
+                                    </div>
+                                </photo-consumer>
+                            </photo-provider>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 底部按钮 -->
+                <div class="fixed-btn-wrap center-btn" v-if="info?.appType === 2">
+                    <div class="fixed-btn" @click="handleConvert">
+                        转发农事
+                    </div>
+                </div>
+
+                <div class="fixed-btn-wrap execute-action" v-if="info?.appType === 1 && detail?.flowStatus === 1">
+                    <div class="action-item second" @click="handleConvert">转发农事</div>
+                    <div class="action-item primary" @click="handleExecute">溯源认证</div>
+                </div>
+            </div>
+        </div>
+        <ExecutePopup ref="executePopupRef" />
+
+
+
+        <upload-tips v-model:show="showUploadTipsPopup" />
+    </div>
+    <!-- 执行区域地图弹窗 -->
+    <popup v-model:show="showMapPopup" closeable class="map-popup">
+        <map-info :farmId="detail.farmId" />
+    </popup>
+</template>
+
+<script setup>
+import wx from "weixin-js-sdk";
+import customHeader from "@/components/customHeader.vue";
+import { ref, computed, onMounted, onActivated } from "vue";
+import { useRouter } from "vue-router";
+import { formatDate } from "@/common/commonFun";
+import ExecutePopup from "./components/executePopup.vue";
+import { base_img_url2 } from "@/api/config";
+import UploadTips from "@/components/popup/uploadTips.vue";
+import { Popup } from "vant";
+import MapInfo from "./components/mapInfo.vue";
+import { useRoute } from "vue-router";
+
+const route = useRoute();
+const showUploadTipsPopup = ref(false);
+
+const router = useRouter();
+// const info = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
+const info = { appType: 1 };
+
+/** 接口根级与 detail 合并后的详情(兼容旧 groupList / 新 pesticideList、prescriptionList) */
+const detail = ref({
+    farmId: null,
+    farmWorkLibId: null,
+    farmWorkName: "",
+    flowStatus: null,
+    purpose: "",
+    speciesId: null,
+    speciesName: "",
+    executeDate: null,
+    intervelTime: null,
+    remark: "",
+    pesticideList: [],
+    prescriptionList: [],
+    groupList: [],
+    confirmPicture: [],
+    executeEvidence: [],
+    expertName: "",
+    expertPrescription: "",
+    id: null,
+    postId: null,
+    post: null,
+    expertNameFromFarmBasicInfo: "",
+    rangeWkt: null,
+    activateTime: null,
+});
+
+/** 凭证图片统一为 { filename } */
+const normalizeCropAlbum = (album) => {
+    if (!album || !Array.isArray(album)) return [];
+    return album
+        .map((x) => {
+            if (!x) return null;
+            if (typeof x === "string") return { filename: x };
+            if (x.filename) return x;
+            return null;
+        })
+        .filter(Boolean);
+};
+
+/**
+ * prescriptionList.pesticideFertilizerList → 与旧 groupList.pesticideList 一致(含 params 三行执行方式)
+ */
+const mapFertilizerToPesticideItem = (f) => {
+    if (!f || typeof f !== "object") return null;
+    return {
+        code: f.pesticideFertilizerCode || f.code,
+        name: f.pesticideFertilizerName || f.defaultName || f.name || "",
+        typeName: f.typeName || "",
+        brand: f.brand || "",
+        unit: f.unit,
+        params: [
+            {
+                dosage:
+                    f.muUsage != null && f.muUsage !== ""
+                        ? String(f.muUsage)
+                        : "",
+                executionMethod: 1,
+                ratio:
+                    f.ratio != null && f.ratio !== "" ? String(f.ratio) : "",
+                remark: f.remark || "",
+            },
+            {
+                dosage:
+                    f.muUsage2 != null && f.muUsage2 !== ""
+                        ? String(f.muUsage2)
+                        : "",
+                executionMethod: 2,
+                ratio:
+                    f.ratio2 != null && f.ratio2 !== ""
+                        ? String(f.ratio2)
+                        : "",
+                remark: "",
+            },
+            { dosage: "", executionMethod: 3, ratio: "", remark: "" },
+        ],
+    };
+};
+
+
+const maybeShowUploadTips = () => {
+    if (detail.value?.flowStatus !== 1) return;
+    const shown = localStorage.getItem('upload_tips');
+    if (shown === "1") return;
+
+    localStorage.setItem('upload_tips', "1");
+    showUploadTipsPopup.value = true;
+};
+
+onMounted(() => {
+    maybeShowUploadTips();
+});
+
+onActivated(() => {
+    getDetail();
+});
+
+const getDetail = () => {
+    const { farmWorkLibId, farmId } = route.query;
+    VE_API.z_farm_work_record
+        .getDetailById({ farmWorkLibId, farmId })
+        .then(({ data }) => {
+            const inner =
+                data?.detail && typeof data.detail === "object"
+                    ? { ...data.detail }
+                    : {};
+            detail.value = {
+                ...inner,
+                post: data?.post ?? null,
+                expertNameFromFarmBasicInfo:
+                    data?.expertNameFromFarmBasicInfo ?? "",
+                rangeWkt: data?.rangeWkt ?? null,
+                activateTime: data?.activateTime ?? null,
+            };
+        });
+};
+
+
+// 计算距离执行时间的天数差
+const daysDiff = computed(() => {
+    if (!detail.value?.executeDate) {
+        return 0;
+    }
+
+    const executeDate = new Date(detail.value.executeDate);
+    const today = new Date();
+
+    // 将时间设置为 00:00:00,只比较日期
+    executeDate.setHours(0, 0, 0, 0);
+    today.setHours(0, 0, 0, 0);
+
+    // 计算天数差(毫秒转天数)
+    const diffTime = executeDate.getTime() - today.getTime();
+    const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
+
+    return diffDays;
+});
+
+// 执行方式 Tab 配置
+const executionTabs = [
+    { label: "植保机", value: 1 },
+    { label: "人工手持", value: 2 },
+    { label: "地面机械", value: 3 }
+];
+// 每一段农事的当前执行方式(索引 -> 执行方式),默认 1:植保机
+const stageExecutionMethods = ref({});
+const executePopupRef = ref(null);
+
+
+const triggerDateText = computed(() => {
+    if (!detail.value.executeDate) return "";
+    const d = formatDate(detail.value.executeDate);
+    return d.replace(/-/g, ".");
+});
+
+/**
+ * 展示用「分段」列表:旧数据用 groupList;新接口用 detail.pesticideList 或 prescriptionList 推导
+ */
+const stageList = computed(() => {
+    const d = detail.value;
+    if (!d) return [];
+
+    if (Array.isArray(d.groupList) && d.groupList.length) {
+        return d.groupList;
+    }
+
+    if (Array.isArray(d.pesticideList) && d.pesticideList.length) {
+        return [
+            {
+                name: d.farmWorkName || "",
+                pesticideList: d.pesticideList,
+                cropAlbum: normalizeCropAlbum(
+                    d.confirmPicture?.length
+                        ? d.confirmPicture
+                        : d.executeEvidence
+                ),
+            },
+        ];
+    }
+
+    if (Array.isArray(d.prescriptionList) && d.prescriptionList.length) {
+        return d.prescriptionList.map((p) => ({
+            name: p.name || "",
+            pesticideList: (p.pesticideFertilizerList || [])
+                .map(mapFertilizerToPesticideItem)
+                .filter(Boolean),
+            cropAlbum: normalizeCropAlbum(p.cropAlbum),
+        }));
+    }
+
+    return [];
+});
+
+
+const hasRemark = (prescription, stageIndex) => {
+    if (!prescription?.pesticideList || !Array.isArray(prescription.pesticideList)) return false;
+    const currentMethod = getStageExecutionMethod(stageIndex);
+    return prescription.pesticideList.some((item) => {
+        if (!item.params || !Array.isArray(item.params)) return false;
+        const p = item.params.find((param) => param.executionMethod === currentMethod);
+        return !!(p && p.remark);
+    });
+};
+
+const handleTagType = (tagType) => {
+    if (tagType === 0) return "已过期";
+    if (tagType === 1) return "待认证";
+    if (tagType === 2) return "待触发";
+    if (tagType === 3) return "已完成";
+    return "待触发"
+}
+
+const handleExecute = () => {
+
+    // wx.miniProgram.navigateTo({
+    //     url: `/pages/subPages/location_check/index?lng=113.264435&lat=23.129163`,
+    // });
+    executePopupRef.value.openPopup();
+};
+
+
+const showMapPopup = ref(false);
+
+const handleViewArea = () => {
+    showMapPopup.value = true;
+}
+
+const toDraw = () => {
+    router.push("/draw_region");
+}
+
+const handleBack = () => {
+    router.back();
+};
+
+const handleConvert = () => {
+    const query = {
+        askInfo: { title: "农情互动", content: "是否分享该互动给好友" },
+        shareText: '邀请您农情互动,精准匹配种植方案',
+        targetUrl: `work_detail`,
+        paramsPage: JSON.stringify({ id: detail.value.id }),
+        imageUrl: 'https://birdseye-img.sysuimars.com/birdseye-look-mini/work_img.png',
+    };
+    wx.miniProgram.navigateTo({
+        url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
+    });
+};
+
+// 获取当前段的执行方式
+const getStageExecutionMethod = (stageIndex) => {
+    const val = stageExecutionMethods.value[stageIndex];
+    return val || 1;
+};
+
+// 根据当前执行方式,获取对应的参数(配比、单亩用量、备注)
+const getPesticideParam = (item, stageIndex) => {
+    if (!item?.params || !Array.isArray(item.params)) return null;
+    const currentMethod = getStageExecutionMethod(stageIndex);
+    return (
+        item.params.find((param) => param.executionMethod === currentMethod) || null
+    );
+};
+
+const getParamRemark = (item, stageIndex) => {
+    const param = getPesticideParam(item, stageIndex);
+    return param?.remark || item.remark || "";
+};
+
+const changeExecutionMethod = (stageIndex, value) => {
+    stageExecutionMethods.value = {
+        ...stageExecutionMethods.value,
+        [stageIndex]: value
+    };
+};
+</script>
+
+<style scoped lang="scss">
+.work-detail {
+    height: 100vh;
+    background: #f2f3f5;
+    display: flex;
+    flex-direction: column;
+
+    .work-detail-content {
+        flex: 1;
+        overflow: auto;
+    }
+}
+
+.content-status {
+    position: relative;
+    padding: 16px 12px 0 12px;
+    color: #fff;
+    z-index: 1;
+    height: 100px;
+    box-sizing: border-box;
+
+    &::after {
+        content: "";
+        z-index: -1;
+        position: absolute;
+        left: 0;
+        top: 0;
+        height: 100px;
+        background: #C7C7C7;
+        // background: #FF953D;
+        width: 100%;
+    }
+
+    &.status-0 {
+        &::after {
+            background: #FF4F4F;
+        }
+    }
+
+    &.status-1 {
+        &::after {
+            background: #FF953D;
+        }
+    }
+
+    &.status-2 {
+        &::after {
+            background: #C7C7C7;
+        }
+    }
+
+    &.status-3 {
+        padding-top: 30px;
+
+        &::after {
+            background: #2199F8;
+        }
+    }
+
+    .status-l {
+        .status-title {
+            font-size: 22px;
+        }
+
+        .status-sub {
+            font-size: 14px;
+        }
+    }
+}
+
+.work-wrap {
+    position: relative;
+    top: -16px;
+    padding: 0 12px 12px;
+    z-index: 2;
+    margin-bottom: 60px;
+}
+
+.box-wrap {
+
+    // background: #ffffff;
+    // border-radius: 8px;
+    // padding: 14px 10px 10px 10px;
+    // box-shadow: 0 2px 8px rgba(15, 35, 52, 0.06);
+    .work-info {
+        background: #ffffff;
+        border-radius: 8px;
+        padding: 14px 10px 10px 10px;
+        box-shadow: 0 2px 8px rgba(15, 35, 52, 0.06);
+    }
+
+    .photo-box {
+        margin-top: 10px;
+        padding: 11px 10px;
+        font-size: 14px;
+
+        .photo-title {
+            color: #000;
+            padding-bottom: 9px;
+        }
+
+        .photo-sub-title {
+            padding-bottom: 9px;
+            color: #767676;
+        }
+    }
+}
+
+
+.group-info {
+    margin-bottom: 10px;
+    background: #ffffff;
+    border-radius: 8px;
+    padding: 14px 10px 10px 10px;
+    box-shadow: 0 2px 8px rgba(15, 35, 52, 0.06);
+
+    &.group-box {
+        padding: 10px;
+
+        .group-name {
+            font-size: 14px;
+            color: #767676;
+
+            .group-name-text {
+                color: #000;
+            }
+        }
+    }
+
+    .group-name {
+        font-size: 14px;
+        color: rgba(0, 0, 0, 0.2);
+
+        .group-name-text {
+            color: #767676;
+        }
+    }
+
+    .group-sub {
+        margin-top: 6px;
+        font-size: 12px;
+        color: rgba(0, 0, 0, 0.45);
+        line-height: 1.5;
+    }
+}
+
+.stage-card+.stage-card {
+    margin-top: 10px;
+}
+
+.stage-card {
+
+    .stage-header {
+        padding-bottom: 12px;
+        display: flex;
+        align-items: center;
+        gap: 5px;
+
+        .stage-title {
+            font-size: 16px;
+            color: #000;
+        }
+
+        .title-tag {
+            width: fit-content;
+            font-size: 12px;
+            height: 26px;
+            line-height: 26px;
+            color: #2199F8;
+            background: rgba(33, 153, 248, 0.1);
+            border-radius: 20px;
+            padding: 0 8px;
+        }
+
+        .tag-0 {
+            color: #FF953D;
+            background: rgba(255, 149, 61, 0.1);
+        }
+    }
+
+    .stage-info {
+        padding: 8px 0 2px;
+        border-top: 1px solid #f5f5f5;
+    }
+}
+
+.form-item {
+    display: flex;
+    font-size: 14px;
+    color: #767676;
+    margin-top: 4px;
+
+    .area-btn {
+        border: 1px solid rgba(0, 0, 0, 0.1);
+        padding: 0 10px;
+        color: #767676;
+    }
+
+    .area-btn-right {
+        padding-right: 4px;
+        display: inline-flex;
+        align-items: center;
+        gap: 4px;
+    }
+
+    .item-name {
+        padding-right: 26px;
+        color: rgba(0, 0, 0, 0.2);
+    }
+
+    .item-text {
+        flex: 1;
+        line-height: 21px;
+
+        &.light-text {
+            color: #2199F8;
+        }
+    }
+    .area-text {
+        display: inline-flex;
+        align-items: center;
+        gap: 10px;
+    }
+}
+
+.light-text {
+    color: #2199F8;
+}
+
+.stage-tabs {
+    display: inline-flex;
+    gap: 8px;
+    margin-top: 8px;
+
+    .tab-pill {
+        padding: 0 8px;
+        font-size: 14px;
+        text-align: center;
+        border-radius: 2px;
+        height: 28px;
+        line-height: 28px;
+        color: #767676;
+        background: rgba(171, 171, 171, 0.1);
+
+        &.active {
+            background: rgba(33, 153, 248, 0.1);
+            color: #2199F8;
+        }
+    }
+}
+
+.prescription-wrap {
+    margin-top: 10px;
+    overflow: hidden;
+
+    .prescription-title {
+        padding: 8px 10px;
+        font-size: 13px;
+        font-weight: 500;
+        border-bottom: 1px solid rgba(225, 225, 225, 0.6);
+    }
+}
+
+.prescription-table {
+    font-size: 12px;
+    text-align: center;
+    border-radius: 6px;
+    border: 1px solid rgba(225, 225, 225, 0.6);
+
+    .table-header {
+        display: flex;
+        background: rgba(171, 171, 171, 0.1);
+        padding: 9px 6px;
+        color: #767676;
+    }
+
+    .table-row {
+        display: flex;
+        padding: 12px 6px;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+        color: rgba(0, 0, 0, 0.76);
+
+        &:last-child {
+            border-bottom: none;
+        }
+    }
+
+    .col {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 0 2px;
+    }
+
+    .col-type {
+        width: 56px;
+    }
+
+    .col-name {
+        flex: 1;
+    }
+
+    .col-ratio {
+        width: 64px;
+    }
+
+    .col-dose {
+        width: 64px;
+    }
+}
+
+.prescription-remark {
+    margin-top: 8px;
+    padding: 7px 10px;
+    border-radius: 6px;
+    color: #767676;
+    background: rgba(171, 171, 171, 0.1);
+    line-height: 21px;
+}
+
+
+.photo-img-wrap {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+
+    ::v-deep {
+        .PhotoConsumer {
+            width: 31%;
+            height: 92px;
+        }
+    }
+
+    .photo-img {
+        width: 100%;
+        height: 100%;
+        position: relative;
+        box-sizing: border-box;
+        border: 2px solid transparent;
+        border-radius: 8px;
+        overflow: hidden;
+
+        img {
+            width: 100%;
+            height: 100%;
+            border-radius: 8px;
+            object-fit: cover;
+        }
+    }
+}
+
+.execute-action {
+    display: flex;
+    align-items: center;
+    // justify-content: center;
+    justify-content: space-between;
+    gap: 16px;
+
+    .action-item {
+        padding: 0 26px;
+        height: 40px;
+        line-height: 40px;
+        border-radius: 26px;
+        box-sizing: border-box;
+        font-size: 14px;
+
+        &.second {
+            background: #ffffff;
+            border: 0.5px solid rgba(153, 153, 153, 0.5);
+            color: #999999;
+        }
+
+        &.primary {
+            background: linear-gradient(180deg, #8ACBFF 0%, #2199F8 100%);
+            color: #ffffff;
+        }
+    }
+}
+
+.fixed-btn-wrap {
+    display: flex;
+    // justify-content: center;
+    justify-content: space-between;
+
+    &.center-btn {
+        justify-content: center;
+    }
+
+    position: fixed;
+    bottom: 0px;
+    left: 0;
+    right: 0;
+    background: #fff;
+    padding: 10px 12px 16px 12px;
+    box-sizing: border-box;
+    // box-shadow: 0 -2px 8px rgba(15, 35, 52, 0.06);
+    box-shadow: 2px 2px 4.5px 0px #00000066;
+
+    .fixed-btn {
+        min-width: 110px;
+        height: 40px;
+        line-height: 40px;
+        text-align: center;
+        border-radius: 20px;
+        background: linear-gradient(180deg, #70bffe, #2199f8);
+        color: #ffffff;
+        font-size: 14px;
+    }
+}
+
+.map-popup {
+    width: 92%;
+    // max-width: 420px;
+    border-radius: 8px;
+    padding: 12px;
+    box-sizing: border-box;
+}
+</style>