Bläddra i källkod

fix: 农事执行页面

lxf 10 timmar sedan
förälder
incheckning
7af248672e

+ 2 - 2
src/App.vue

@@ -61,8 +61,8 @@
                     />
                 </template>
             </tabbar-item>
-            <tabbar-item replace to="/youwei_trace">
-                <span>{{ t("tabbar.trace") }}</span>
+            <tabbar-item replace to="/work_execute">
+                <span>{{ t("tabbar.execute") }}</span>
                 <template #icon="props">
                     <img
                         :src="

BIN
src/assets/img/home/task.png


BIN
src/assets/img/tab_bar/farm-active.png


BIN
src/assets/img/tab_bar/farm.png


+ 231 - 0
src/components/pageComponents/AgriculturalInteractionCard.vue

@@ -0,0 +1,231 @@
+<template>
+    <!-- 只封装时间轴区域 -->
+    <div class="timeline">
+        <div class="timeline-item" v-for="timelineItem in item.timelineList" :key="timelineItem.id">
+            <div class="timeline-left">
+                <div class="dot"></div>
+                <div class="line"></div>
+            </div>
+            <div class="timeline-right">
+                <div class="date">
+                    <span class="work-name">{{ timelineItem.farmWorkName }}</span>
+                    <!-- <span v-show="timelineItem.expectedRisk">({{ timelineItem.expectedRisk }})</span> -->
+                    <span class="ignore-btn" v-if="hasPlanPermission" @click="handleIgnore(item,timelineItem)"> 忽略 </span>
+                </div>
+                <div class="text">
+                    预计报价<span class="price">{{ timelineItem.estimatedCost }}元</span>
+                    <span class="action-detail" @click="toDetail(timelineItem, item)"
+                        >查看详情</span
+                    >
+                    <!-- <span class="action-detail" @click="showPriceSheetPopup(timelineItem.farmWorkId, item)">查看报价单</span> -->
+                </div>
+            </div>
+            <div class="timeline-action" @click="handleTimelineAction(timelineItem, item.farmId)">转入农事任务</div>
+        </div>
+    </div>
+    <popup v-model:show="showTaskPopup" z-index="10000" round class="task-tips-popup">
+        <img class="create-farm-icon" src="@/assets/img/home/create-farm-icon.png" alt="" />
+        <div class="create-farm-text">
+            <div>
+                您确认忽略 <span class="main-text">{{ currentTask?.farmName }}</span> 的
+                <span class="main-text">{{ currentTask?.farmWorkName }}</span> 农事吗
+            </div>
+        </div>
+        <div class="create-farm-btn" @click="handlePopupBtn">确认忽略</div>
+    </popup>
+</template>
+
+<script setup>
+import { Popup } from "vant";
+import { computed, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { useRouter } from "vue-router";
+
+const router = useRouter();
+
+const props = defineProps({
+    item: {
+        type: Object,
+        required: true,
+        default: () => ({}),
+    },
+});
+
+// 检查是否有"转入农事"权限
+const hasPlanPermission = computed(() => {
+    try {
+        const userInfoStr = localStorage.getItem("localUserInfo");
+        if (!userInfoStr) return false;
+        const userInfo = JSON.parse(userInfoStr);
+        const permissions = userInfo.agriculturalPermissions || [];
+        return permissions.includes("转入农事");
+    } catch (error) {
+        console.error("解析用户信息失败:", error);
+        return false;
+    }
+});
+
+
+const handleTimelineAction = (timelineItem, farmId) => {
+    if (!hasPlanPermission.value) {
+        ElMessage.warning("您暂无权限操作");
+        return;
+    }
+    router.push({
+        path: "/work_detail",
+        query: {
+            miniJson: JSON.stringify({
+                id: timelineItem.farmWorkId,
+                arrangeId: timelineItem.arrangeId,
+                farmId,
+            }),
+        },
+    });
+};
+
+const emits = defineEmits(["updateList"]);
+
+const handleUploadSuccess = async () => {
+    emits("updateList");
+};
+
+const toDetail = (timelineItem, item) => {
+    router.push({
+        path: "/work_detail",
+        query: {
+            miniJson: JSON.stringify({
+                id: timelineItem.farmWorkId,
+                arrangeId: timelineItem.arrangeId,
+                farmId: item.farmId,
+            }),
+        },
+    });
+};
+
+// 忽略农事库
+const currentTask = ref(null);
+const showTaskPopup = ref(false);
+const handleIgnore = (item, timelineItem) => {
+    currentTask.value = { ...item, ...timelineItem };
+    showTaskPopup.value = true;
+};
+
+const handlePopupBtn = () => {
+    VE_API.home.ignoreFarmWorkLib({ farmWorkLibId: currentTask.value.farmWorkId }).then(({ code }) => {
+        if (code === 0) {
+            showTaskPopup.value = false;
+            ElMessage.success("忽略成功");
+            handleUploadSuccess();
+        }
+    });
+};
+
+</script>
+
+<style scoped lang="scss">
+.timeline {
+    margin-left: -5px;
+    margin-top: 8px;
+    .timeline-item {
+        display: flex;
+        align-items: flex-start;
+        font-size: 14px;
+        color: #ffffff;
+        line-height: 22px;
+        & + .timeline-item {
+            margin-top: 10px;
+        }
+        .timeline-left {
+            width: 22px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            .dot {
+                width: 6px;
+                height: 6px;
+                border-radius: 50%;
+                background: #a2d5fd;
+                margin-top: 6px;
+            }
+            .line {
+                border-left: 1px dashed #a2d5fd;
+                margin-top: 4px;
+                height: 28px;
+            }
+        }
+        .timeline-right {
+            padding-left: 5px;
+            flex: 1;
+            .date {
+                color: #1d2129;
+                font-weight: 500;
+                font-size: 14px;
+                line-height: 22px;
+            }
+            .text {
+                font-size: 12px;
+                color: #d7d7d7;
+                .price {
+                    padding-left: 4px;
+                }
+                .action-detail {
+                    margin-left: 6px;
+                    color: #2199f8;
+                    border-bottom: 1px solid;
+                }
+            }
+            .work-name {
+                padding-left: 4px;
+            }
+            .ignore-btn {
+                margin-left: 6px;
+                color: rgba(29, 33, 41, 0.4);
+                font-size: 13px;
+            }
+        }
+        .timeline-action {
+            align-self: center;
+            height: 28px;
+            line-height: 28px;
+            flex: none;
+            background: rgba(33, 153, 248, 0.1);
+            color: #2199f8;
+            font-size: 12px;
+            padding: 0px 11px;
+            border-radius: 20px;
+        }
+    }
+}
+.task-tips-popup {
+    width: 90%;
+    padding: 28px 28px 20px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    .create-farm-icon {
+        width: 52px;
+        height: 52px;
+        margin-bottom: 12px;
+    }
+    .create-farm-text {
+        font-size: 19px;
+        font-weight: 500;
+        margin-bottom: 32px;
+        text-align: center;
+    }
+    .main-text {
+        color: #2199f8;
+    }
+    .create-farm-btn {
+        width: 100%;
+        box-sizing: border-box;
+        padding: 8px;
+        border-radius: 25px;
+        font-size: 16px;
+        background: #2199f8;
+        color: #fff;
+        text-align: center;
+    }
+}
+</style>

+ 2 - 0
src/i18n/messages.js

@@ -7,6 +7,7 @@ export default {
             agriFile: "农情档案",
             agriRecord: "农事规划",
             trace: "有味溯源",
+            execute: "农事执行",
         },
         common: {
             current: "当前",
@@ -208,6 +209,7 @@ export default {
             agriFile: "Crop Archives",
             agriRecord: "Farm Planning",
             trace: "Traceability",
+            execute: "Execution",
         },
         common: {
             current: "Current",

+ 7 - 0
src/router/globalRoutes.js

@@ -135,4 +135,11 @@ export default [
         meta: { keepAlive: true },
         component: () => import("@/views/old_mini/recordDetails/mapManage.vue"),
     },
+    // 农事执行
+    {
+        path: "/work_execute",
+        name: "WorkExecute",
+        meta: { showTabbar: true, keepAlive: true },
+        component: () => import("@/views/old_mini/work_execute/index.vue"),
+    },
 ];

+ 244 - 0
src/views/old_mini/farm_manage/map/index.js

@@ -0,0 +1,244 @@
+import * as KMap from "@/utils/ol-map/KMap";
+import * as util from "@/common/ol_common.js";
+import config from "@/api/config.js";
+import { Point } from 'ol/geom';
+import Feature from "ol/Feature";
+import Style from "ol/style/Style";
+import Photo from "ol-ext/style/Photo";
+import { Fill, Text, Icon, Stroke } from "ol/style.js";
+import { newPoint } from "@/utils/map";
+
+/**
+ * @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/home/garden-point.png"),
+            scale: 0.5,
+            anchor: [0.5, 1],
+          }),
+        });
+      },
+    });
+
+    this.gardenPointLayer = new KMap.VectorLayer("gardenPointLayer", 99, {
+      minZoom: 6,
+      maxZoom: 22,
+      style: (feature) => {
+        let style1 = new Style({
+          image: new Photo({
+            src: "https://birdseye-img.sysuimars.com/ai_result/2023/11/20/tree_4414/img_27572.jpg" + '?imageView2/1/w/300/interlace/1',
+            radius: 19,
+            shadow: 0,
+            crop: true,
+            onload: function () {
+              that.gardenPointLayer.layer.changed();
+            },
+            displacement: [-1, -1],
+            stroke: new Stroke({
+              width: 2,
+              color: "#fdfcfc00",
+            }),
+          }),
+        });
+        let style2 = new Style({
+          image: new Photo({
+            src: require("@/assets/img/map/garden-border.png"),
+            radius: 24,
+            shadow: 0,
+            crop: false,
+            onload: function () {
+              that.gardenPointLayer.layer.changed();
+            },
+            displacement: [0, -6],
+            stroke: new Stroke({
+              width: 0,
+              color: "#fdfcfc00",
+            }),
+          }),
+        });
+        let style3 = new Style({
+          text: new Text({
+            text: feature.get('farmName') || feature.get('mapInfo'),
+            offsetX: 0,
+            offsetY: -30,
+            font: "bold 12px sans-serif",
+            fill: new Fill({ color: "#fff" }), // 字体颜色
+          }),
+        });
+
+        const canvas = document.createElement('canvas');
+        const ctx = canvas.getContext('2d');
+
+        // 矩形的参数
+        const x = 150; // 矩形中心点的x坐标
+        const y = 100; // 矩形中心点的y坐标
+        const width = 98; // 矩形的宽度
+        const height = 20; // 矩形的高度
+        const cornerRadius = 8; // 圆角半径
+
+        // 创建渐变
+        const gradient = ctx.createLinearGradient(x - width / 2, y, x + width / 2, y);
+        gradient.addColorStop(0, '#2199F8');   // 渐变起始颜色
+        gradient.addColorStop(1, '#2199F8');  // 渐变结束颜色
+
+        // 绘制圆角矩形
+        ctx.beginPath();
+        ctx.moveTo(x - width / 2 + cornerRadius, y - height / 2); // 左上角
+        ctx.lineTo(x + width / 2 - cornerRadius, y - height / 2); // 上边
+        ctx.arc(x + width / 2 - cornerRadius, y - height / 2 + cornerRadius, cornerRadius, -Math.PI / 2, 0); // 右上角
+        ctx.lineTo(x + width / 2, y + height / 2 - cornerRadius); // 右边
+        ctx.arc(x + width / 2 - cornerRadius, y + height / 2 - cornerRadius, cornerRadius, 0, Math.PI / 2); // 右下角
+        ctx.lineTo(x - width / 2 + cornerRadius, y + height / 2); // 下边
+        ctx.arc(x - width / 2 + cornerRadius, y + height / 2 - cornerRadius, cornerRadius, Math.PI / 2, Math.PI); // 左下角
+        ctx.lineTo(x - width / 2, y - height / 2 + cornerRadius); // 左边
+        ctx.arc(x - width / 2 + cornerRadius, y - height / 2 + cornerRadius, cornerRadius, Math.PI, -Math.PI / 2); // 左上角
+        ctx.closePath();
+
+        // 填充颜色
+        ctx.fillStyle = gradient;
+        ctx.fill();
+        const newStyle = new Style({
+          image: new Icon({
+            src: canvas.toDataURL(),
+            displacement: [0, 56],
+          }),
+        });
+
+
+        return [style1, style2, newStyle, style3];
+      },
+    });
+
+    this.gardenHallLayer = new KMap.VectorLayer("gardenHallLayer", 99, {
+      minZoom: 6,
+      maxZoom: 22,
+      style: (feature) => {
+        let style2 = new Style({
+          image: new Icon({
+            src: require("@/assets/img/map/hall.png"),
+            scale: 0.4,
+            anchor: [0.5, 1],
+            displacement: [0, -36],
+          }),
+        });
+        let style3 = new Style({
+          text: new Text({
+            text: feature.get('totalArea') ? feature.get('totalArea').toFixed(2) + '亩' : '--',
+            offsetX: 0,
+            offsetY: -30,
+            font: "bold 12px sans-serif",
+            fill: new Fill({ color: "#fff" }), // 字体颜色
+          }),
+        });
+        let style4 = new Style({
+          text: new Text({
+            text: feature.get('districtName'),
+            offsetX: 0,
+            offsetY: -6,
+            font: "bold 14px sans-serif",
+            fill: new Fill({ color: "#2199F8" }), // 字体颜色
+          }),
+        });
+
+        const canvas = document.createElement('canvas');
+        const ctx = canvas.getContext('2d');
+
+        // 矩形的参数
+        const x = 150; // 矩形中心点的x坐标
+        const y = 100; // 矩形中心点的y坐标
+        const width = 58; // 矩形的宽度
+        const height = 20; // 矩形的高度
+        const cornerRadius = 4; // 圆角半径
+
+        // 创建渐变
+        const gradient = ctx.createLinearGradient(x - width / 2, y, x + width / 2, y);
+        gradient.addColorStop(0, '#2199F8');   // 渐变起始颜色
+        gradient.addColorStop(1, '#2199F8');  // 渐变结束颜色
+
+        // 绘制圆角矩形
+        ctx.beginPath();
+        ctx.moveTo(x - width / 2 + cornerRadius, y - height / 2); // 左上角
+        ctx.lineTo(x + width / 2 - cornerRadius, y - height / 2); // 上边
+        ctx.arc(x + width / 2 - cornerRadius, y - height / 2 + cornerRadius, cornerRadius, -Math.PI / 2, 0); // 右上角
+        ctx.lineTo(x + width / 2, y + height / 2 - cornerRadius); // 右边
+        ctx.arc(x + width / 2 - cornerRadius, y + height / 2 - cornerRadius, cornerRadius, 0, Math.PI / 2); // 右下角
+        ctx.lineTo(x - width / 2 + cornerRadius, y + height / 2); // 下边
+        ctx.arc(x - width / 2 + cornerRadius, y + height / 2 - cornerRadius, cornerRadius, Math.PI / 2, Math.PI); // 左下角
+        ctx.lineTo(x - width / 2, y - height / 2 + cornerRadius); // 左边
+        ctx.arc(x - width / 2 + cornerRadius, y - height / 2 + cornerRadius, cornerRadius, Math.PI, -Math.PI / 2); // 左上角
+        ctx.closePath();
+
+        // 填充颜色
+        ctx.fillStyle = gradient;
+        ctx.fill();
+        const newStyle = new Style({
+          image: new Icon({
+            src: canvas.toDataURL(),
+            displacement: [0, 56],
+          }),
+        });
+
+
+        return [style2, newStyle, style3, style4];
+      },
+    });
+  }
+
+  initMap(location, target, isCapital) {
+    let level = 16;
+    let coordinate = util.wktCastGeom(location).getFirstCoordinate();
+    this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 6, 22, isCapital ? "img" : "vec");
+    let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png";
+    this.kmap.addXYZLayer(xyz2, { minZoom: 6, maxZoom: 22 }, 2);
+    this.kmap.addLayer(this.clickPointLayer.layer);
+    this.kmap.addLayer(this.gardenPointLayer.layer);
+    this.kmap.addLayer(this.gardenHallLayer.layer);
+    if (isCapital) {
+      // this.initData()
+      const point = ["113.61652616170711,23.58399613872042", "113.61767554789421, 23.590079887444034", "113.62757101477101, 23.590796948574365", "113.62240816252164, 23.59499176519138"]
+    } else {
+      let point = new Feature(new Point(coordinate))
+      this.clickPointLayer.addFeature(point)
+    }
+  }
+
+  initData(taskList, label,pointType='point') {
+    this.gardenPointLayer.source.clear();
+    if (taskList.length > 0) {  // 如果任务列表不为空,则添加任务点
+      for (let item of taskList) {
+        item.mapInfo = label ? item[label] : (item.executeDeadlineDate || item.executeDate)?.replace(/^\d{4}-(\d{2})-(\d{2})$/, '$1.$2') + '  ' + item.farmWorkName
+        this.gardenPointLayer.source.addFeature(newPoint(item, pointType, "myGarden"))
+      }
+      this.kmap.getView().fit(this.gardenPointLayer.source.getExtent(), { padding: [42, 20, 30, 20] });
+      const finalZoom = this.kmap.getView().getZoom();
+      if (finalZoom > 18) {
+        this.kmap.getView().setZoom(18);
+      }
+    }
+  }
+
+  initDataHall(taskList) {
+    this.gardenHallLayer.source.clear();
+    if (taskList.length > 0) {  // 如果任务列表不为空,则添加任务点
+      for (let item of taskList) {
+        this.gardenHallLayer.source.addFeature(newPoint(item, "centerPoint", "hallGarden"))
+      }
+      this.kmap.getView().fit(this.gardenHallLayer.source.getExtent(), { padding: [60, 40, 30, 40] });
+      const finalZoom = this.kmap.getView().getZoom();
+      if (finalZoom > 18) {
+        this.kmap.getView().setZoom(18);
+      }
+    }
+  }
+}
+
+export default IndexMap;

+ 86 - 0
src/views/old_mini/work_execute/components/ExecutorHeader.vue

@@ -0,0 +1,86 @@
+<template>
+    <div class="executor-header">
+        <el-avatar :size="size" :src="avatar" class="executor-avatar">
+            {{ name?.charAt(0) }}
+        </el-avatar>
+        <div class="executor-profile">
+            <div class="executor-name-row">
+                <span class="executor-name">{{ name }}</span>
+                <span v-if="subjectName" class="executor-tag">{{ subjectName }}</span>
+            </div>
+            <div v-if="phone" class="executor-phone">电话:{{ phone }}</div>
+        </div>
+        <slot name="info"></slot>
+    </div>
+</template>
+
+<script setup>
+defineProps({
+    name: {
+        type: String,
+        default: "",
+    },
+    phone: {
+        type: String,
+        default: "",
+    },
+    avatar: {
+        type: String,
+        default: "",
+    },
+    subjectName: {
+        type: String,
+        default: "执行者",
+    },
+    size: {
+        type: Number,
+        default: 36,
+    },
+});
+</script>
+
+<style lang="scss" scoped>
+.executor-header {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.executor-avatar {
+    flex-shrink: 0;
+    background: rgba(33, 153, 248, 0.15);
+    color: #2199f8;
+    font-size: 16px;
+}
+
+.executor-profile {
+    flex: 1;
+    min-width: 0;
+}
+
+.executor-name-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 4px;
+}
+
+.executor-name {
+    font-size: 14px;
+    color: #1d2129;
+}
+
+.executor-tag {
+    flex-shrink: 0;
+    padding: 1px 6px;
+    font-size: 10px;
+    color: #2199f8;
+    background: rgba(33, 153, 248, 0.1);
+    border-radius: 2px;
+}
+
+.executor-phone {
+    font-size: 10px;
+    color: #86909c;
+}
+</style>

+ 837 - 0
src/views/old_mini/work_execute/index.vue

@@ -0,0 +1,837 @@
+<template>
+    <div class="task-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
+        <div class="task-top">
+            <div class="map-container" ref="mapContainer"></div>
+        </div>
+        <div class="task-list">
+            <div class="list-filter">
+                <div class="filter-item" :class="{ active: activeIndex === 0 }" @click="handleActiveFilter(0)">
+                    待确认({{ taskCounts[0] || 0 }})
+                </div>
+                <div class="filter-item" :class="{ active: activeIndex === 2 }" @click="handleActiveFilter(2)">
+                    已确认({{ taskCounts[2] || 0 }})
+                </div>
+                <div class="filter-item" :class="{ active: activeIndex === 3 }" @click="handleActiveFilter(3)">
+                    待提醒({{ taskCounts[3] || 0 }})
+                </div>
+            </div>
+            <div class="select-group">
+                <el-select
+                    class="select-item"
+                    v-model="selectParma.farmWorkTypeId"
+                    placeholder="用户类型"
+                    @change="getSimpleList"
+                >
+                    <el-option v-for="item in farmWorkTypeList" :key="item.id" :label="item.name" :value="item.id" />
+                </el-select>
+                <el-select
+                    class="select-item"
+                    v-model="selectParma.districtCode"
+                    placeholder="切换作物"
+                    @change="getSimpleList"
+                >
+                    <el-option v-for="item in districtList" :key="item.code" :label="item.name" :value="item.code" />
+                </el-select>
+            </div>
+            <!-- 任务列表 -->
+            <div class="work-task-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.3)">
+                <div class="task-item" v-for="(item, index) in taskList" :key="index" :class="isTimeoutItem(index) ? 'timeout-item' : ''" @click="handleItem(item, index)">
+                    <div class="item-title">
+                        <img class="task-icon" src="@/assets/img/home/task.png" alt="">
+                        <span class="title-text">{{ item.operation?.name }}</span>
+                        <span class="task-status" :class="`task-status--${item.operation?.type}`">{{ item.operation?.typeName }}</span>
+                    </div>
+                    <span class="task-tag timeout" v-if="isTimeoutItem(index)">
+                        <el-icon><WarningFilled /></el-icon>
+                        {{ getTimeoutText() }}
+                    </span>
+                    <span class="task-tag" v-else>
+                        {{ activeStatus }}
+                    </span>
+                    <div class="item-content">
+                        <div class="item-info">
+                            <div class="info-item">
+                                <div class="info-name">负责人:</div><span class="val-text">
+                                    某某某、某某某</span>
+                            </div>
+                            <div class="info-item">
+                                <div class="info-name">农情研判:</div><span class="val-text">
+                                    {{ item.operation?.work_reason }}</span>
+                            </div>
+                            <div class="info-item" v-if="mode !== 'review'">
+                                <div class="info-name">药物处方:</div><span class="val-text">
+                                    {{ item.operation?.drug }}</span>
+                            </div>
+                        </div>
+                        <div class="excutor-info">
+                            <div class="excutor-info-top">
+                                <executor-header :name="getExecutorName(index)" :phone="getExecutorPhone(index)"
+                                    :avatar="getExecutorAvatar(index)" />
+                            </div>
+                            <div class="executor-stats">
+                                <div class="stat-cell">
+                                    <div class="stat-value">
+                                        {{ item.time ? formatGMTToYMD(item.time) : "--" }}
+                                    </div>
+                                    <div class="stat-label">执行时间</div>
+                                </div>
+                                <div class="cell-line"></div>
+                                <div class="stat-cell">
+                                    <div class="stat-value">{{ item.geohash_sample || "--" }}</div>
+                                    <div class="stat-label">执行区域</div>
+                                </div>
+                                <div class="cell-line"></div>
+                                <div class="stat-cell">
+                                    <div class="stat-value">{{ item.operation?.machine_code || "--" }}</div>
+                                    <div class="stat-label">执行农机</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="unqualified-reason" v-if="activeStatus === '未达标'">
+                        未达标原因:{{ item.operation?.reason || "未在合适时间执行,药液未充分喷撒吸收" }}
+                    </div>
+
+                    <div
+                        class="compare-imgs"
+                        v-if="activeStatus === '未达标' && (item.beforeImage || item.afterImage)"
+                        @click.stop
+                    >
+                        <div class="img-tag">前</div>
+                        <div class="img-tag right-tag">后</div>
+                        <div class="img-item" v-if="item.beforeImage">
+                            <img :src="getWorkImageUrl(item.beforeImage)" alt="" />
+                        </div>
+                        <div class="img-item" v-if="item.afterImage">
+                            <img :src="getWorkImageUrl(item.afterImage)" alt="" />
+                        </div>
+                    </div>
+
+                    <div class="btn-group" v-if="getItemStatusButtons(index).length">
+                        <div v-for="btn in getItemStatusButtons(index)" :key="btn.label" class="edit-btn" :class="btn.type"
+                            @click.stop="handleStatusBtn(btn, item, index)">
+                            {{ btn.label }}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="empty-tip" v-if="!loading && taskList.length === 0">暂无数据</div>
+            </div>
+        </div>
+    </div>
+    <!-- <upload-execute ref="uploadExecuteRef" :onlyShare="onlyShare" @uploadSuccess="handleUploadSuccess" /> -->
+    <!-- 服务报价单 -->
+    <!-- <price-sheet-popup :key="activeIndex" ref="priceSheetPopupRef"></price-sheet-popup> -->
+    <!-- 新增:激活上传弹窗 -->
+    <!-- <active-upload-popup ref="activeUploadPopupRef" @handleUploadSuccess="handleUploadSuccess"></active-upload-popup> -->
+</template>
+
+<script setup>
+import { computed, nextTick, onActivated, onMounted, ref, watch } from "vue";
+import { useRoute } from "vue-router";
+import { useStore } from "vuex";
+import IndexMap from "../farm_manage/map/index";
+import { useRouter } from "vue-router";
+import { WarningFilled } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import ExecutorHeader from "./components/ExecutorHeader.vue";
+import config from "@/api/config.js";
+
+const store = useStore();
+const router = useRouter();
+const route = useRoute();
+const indexMap = new IndexMap();
+const mapContainer = ref(null);
+const tabBarHeight = computed(() => store.state.home.tabBarHeight);
+const selectParma = ref({
+    farmWorkTypeId: null,
+    districtCode: null,
+});
+
+// 任务列表数据(用于显示,可能被筛选)
+const taskList = ref([
+    {
+        time: "Fri, 22 May 2026 00:00:00 GMT",
+        geohash_sample: "ws0gs49ns213",
+        operation: {
+            name: "冬季清园",
+            type: "1",
+            typeName: "标准类",
+            work_reason: "近期气温回升,需及时清理园内枯枝落叶,减少病虫越冬基数",
+            drug: "石硫合剂 500倍液",
+            machine_code: "DJI-T40-001",
+            work_id: 5001,
+        },
+    },
+    {
+        time: "Mon, 26 May 2026 00:00:00 GMT",
+        geohash_sample: "ws0gefxm68u5",
+        operation: {
+            name: "施肥促花",
+            type: "2",
+            typeName: "感知类",
+            work_reason: "花芽分化期养分需求增加,需补充磷钾肥",
+            drug: "磷酸二氢钾 0.3%",
+            machine_code: "DJI-T50-003",
+            work_id: 5002,
+        },
+    },
+    {
+        time: "Wed, 28 May 2026 00:00:00 GMT",
+        geohash_sample: "ws0gefzjqqqw",
+        operation: {
+            name: "病虫害防治",
+            type: "1",
+            typeName: "复核类",
+            work_reason: "监测到蚜虫活动增多,建议及时喷药防治",
+            drug: "吡虫啉 1500倍液",
+            machine_code: "DJI-T40-002",
+            work_id: 5003,
+        },
+    },
+]);
+const mode = "execute";
+const activeStatus = ref("待接受");
+
+const STATUS_BTN_MAP = {
+    待接受: [
+        { label: "重新派发", type: "normal", action: "redispatch" },
+        { label: "提醒接受", type: "primary", action: "remindAccept" },
+    ],
+    已接受: [{ label: "提醒执行", type: "primary", action: "remindExecute" }],
+    执行中: [{ label: "提醒完成", type: "primary", action: "remindComplete" }],
+    已超时: [
+        { label: "重新派发", type: "normal", action: "redispatch" },
+        { label: "提醒接受", type: "primary", action: "remindAccept" },
+    ],
+    未达标: [{ label: "重新下发", type: "primary", action: "redispatch" }],
+};
+
+const nameArr = ["张扬", "王明", "詹金华"];
+const phoneArr = ["13939189356", "13800138000", "13712345678"];
+const avatarArr = [
+    "https://birdseye-img.sysuimars.com/shuichan/image.png",
+    "",
+    "",
+];
+
+const getExecutorName = (index) => nameArr[index % nameArr.length];
+const getExecutorPhone = (index) => phoneArr[index % phoneArr.length];
+const getExecutorAvatar = (index) => avatarArr[index % avatarArr.length] || "";
+
+const isTimeoutItem = (index) => index === 0;
+
+const getTimeoutRemindBtn = () => {
+    const map = {
+        待接受: { label: "提醒接受", type: "primary", action: "remindAccept" },
+        已接受: { label: "提醒执行", type: "primary", action: "remindExecute" },
+        执行中: { label: "提醒完成", type: "primary", action: "remindComplete" },
+    };
+    return map[activeStatus.value];
+};
+
+const getItemStatusButtons = (index) => {
+    const baseButtons = STATUS_BTN_MAP[activeStatus.value] || [];
+    if (!isTimeoutItem(index)) {
+        return baseButtons;
+    }
+    if (activeStatus.value === "待接受") {
+        return baseButtons;
+    }
+    const remindBtn = getTimeoutRemindBtn();
+    if (remindBtn) {
+        return [
+            { label: "重新派发", type: "normal", action: "redispatch" },
+            remindBtn,
+        ];
+    }
+    return [{ label: "重新派发", type: "normal", action: "redispatch" }, ...baseButtons];
+};
+
+const getTimeoutText = () => {
+    switch (activeStatus.value) {
+        case "待接受":
+            return "接受超时";
+        case "已接受":
+            return "执行超时";
+        case "执行中":
+            return "完成超时";
+        default:
+            return "已超时";
+    }
+};
+
+const formatGMTToYMD = (gmtString) => {
+    const date = new Date(gmtString);
+    return date.toISOString().split("T")[0];
+};
+
+const getWorkImageUrl = (image) => {
+    if (!image?.cloud_filename) return "";
+    return config.base_img_url3 + image.cloud_filename;
+};
+
+const handleItem = (item, index) => {
+    router.push({
+        path: "/work_detail",
+        query: {
+            miniJson: JSON.stringify({
+                id: item.operation?.work_id,
+                name: getExecutorName(index),
+                phone: getExecutorPhone(index),
+                avatar: getExecutorAvatar(index),
+                status: activeStatus.value,
+                time: item.time,
+                geohash_sample: item.geohash_sample,
+                machine_code: item.operation?.machine_code,
+            }),
+        },
+    });
+};
+
+const handleStatusBtn = (btn, item, index) => {
+    if (btn.action === "redispatch") {
+        ElMessage.info(`重新派发:${item.operation?.name}`);
+    } else {
+        ElMessage.success("提醒成功!");
+    }
+};
+// 各状态任务数量
+const taskCounts = ref([0, 0, 0]);
+// 当前选中的筛选索引
+const activeIndex = ref(2);
+const noData = ref(false);
+const loading = ref(false);
+// 分页相关
+const page = ref(0);
+const limit = ref(10);
+const loadingMore = ref(false);
+const finished = ref(false);
+
+// 查询未来农事预警
+const getFutureFarmWorkWarning = async (item) => {
+    const res = await VE_API.home.listFutureFarmWorkWarning({ farmId: item.farmId });
+    item.timelineList = res.data || [];
+};
+
+const cityCode = ref("");
+//根据城市的坐标返回区县列表
+const districtList = ref([]);
+function getDistrictListByCity() {
+    VE_API.z_farm_work_record.getDistrictListByCity({ point: mapPoint.value }).then(({ data }) => {
+        districtList.value = data || [];
+        // cityCode.value = data[0].code.slice(0, -2);
+        cityCode.value = "";
+        districtList.value.unshift({ code: cityCode.value, name: "全部" });
+        selectParma.value.districtCode = cityCode.value;
+        resetAndLoad();
+    });
+}
+
+//农事类型列表
+const farmWorkTypeList = ref([]);
+function getFarmWorkTypeList() {
+    VE_API.z_farm_work_record.getFarmWorkTypeList().then(({ data }) => {
+        farmWorkTypeList.value = data;
+        farmWorkTypeList.value.unshift({ id: 0, name: "全部" });
+    });
+}
+
+const mapPoint = ref(null);
+
+onMounted(() => {
+    mapPoint.value = store.state.home.miniUserLocationPoint;
+    // getDistrictListByCity();
+    resetAndLoad();
+    getFarmWorkTypeList();
+    nextTick(() => {
+        indexMap.initMap(mapPoint.value, mapContainer.value, true);
+    });
+});
+
+onActivated(() => {
+    if (route.query.noReload) {
+        return;
+    }
+    // 确保地图已初始化,使用 nextTick 等待 DOM 更新
+    nextTick(() => {
+        // 检查地图实例是否已初始化
+        if (!indexMap.kmap) {
+            // 如果地图未初始化,重新初始化
+            if (mapContainer.value) {
+                mapPoint.value = store.state.home.miniUserLocationPoint;
+                indexMap.initMap(mapPoint.value, mapContainer.value, true);
+                // 等待地图初始化完成后再加载数据
+                setTimeout(() => {
+                    resetAndLoad();
+                }, 300);
+                return;
+            }
+        } else {
+            // 如果地图已初始化,需要等待 tab 切换完成,容器完全可见后再更新尺寸
+            // Tab 切换时容器可能被隐藏,需要更长的延迟确保容器可见
+            if (mapContainer.value && indexMap.kmap.map) {
+                // 检查容器是否可见
+                const checkAndUpdateSize = () => {
+                    const container = mapContainer.value;
+                    if (container) {
+                        const rect = container.getBoundingClientRect();
+                        // 如果容器可见(有宽度和高度),更新地图尺寸
+                        if (rect.width > 0 && rect.height > 0) {
+                            indexMap.kmap.map.updateSize();
+                        } else {
+                            // 如果容器不可见,继续等待
+                            setTimeout(checkAndUpdateSize, 100);
+                        }
+                    }
+                };
+                // 延迟检查,确保 tab 切换完成
+                setTimeout(checkAndUpdateSize, 200);
+            }
+        }
+        resetAndLoad();
+    });
+});
+
+// 监听 activeIndex 变化,重新加载数据
+watch(activeIndex, () => {
+    resetAndLoad();
+});
+
+// 加载列表数据(支持分页)
+async function getSimpleList(isLoadMore = false) {
+    if (!isLoadMore) {
+        // 重置分页
+        page.value = 0;
+        finished.value = false;
+        // taskList.value = [];
+        loading.value = true;
+    } else {
+        loadingMore.value = true;
+    }
+
+    const params = {
+        ...selectParma.value,
+        page: page.value,
+        limit: limit.value,
+        flowStatus: 5,
+    };
+
+    try {
+        const { data } = await VE_API.home.listUnansweredFarms(params);
+
+        if (data && data.length > 0) {
+            // 为每个item初始化timelineList
+            const newItems = data.map((item) => {
+                let sourceData = item?.latestPhenologyProgressBroadcast?.sourceData;
+                if (sourceData) {
+                    try {
+                        sourceData = JSON.parse(sourceData);
+                    } catch (e) {
+                        console.error("解析sourceData失败:", e);
+                        sourceData = null;
+                    }
+                }
+                return {
+                    ...item,
+                    sourceData,
+                    timelineList: [],
+                };
+            });
+
+            // 串行请求,为每个农场获取时间轴数据
+            for (let i = 0; i < newItems.length; i++) {
+                await getFutureFarmWorkWarning(newItems[i]);
+            }
+
+            // 追加数据
+            // const newTaskList = [...taskList.value, ...newItems];
+            // taskList.value = newTaskList.filter(item => item.timelineList.length > 0);
+            
+            // 更新分页
+            page.value += 1;
+            
+            // 判断是否还有更多数据
+            if (data.length < limit.value) {
+                finished.value = true;
+            }
+
+            // 更新地图数据
+            indexMap.initData(taskList.value,'','farmPoint');
+        } else {
+            finished.value = true;
+            if (taskList.value.length === 0) {
+                noData.value = true;
+            }
+        }
+
+        // 数据处理完成后再设置loading为false
+        if (!isLoadMore) {
+            loading.value = false;
+        } else {
+            loadingMore.value = false;
+        }
+    } catch (error) {
+        console.error("获取任务列表失败:", error);
+        if (!isLoadMore) {
+            loading.value = false;
+        } else {
+            loadingMore.value = false;
+        }
+        finished.value = true;
+        if (taskList.value.length === 0) {
+            noData.value = true;
+        }
+    }
+}
+
+// 滚动加载更多
+const onLoad = () => {
+    if (!finished.value && !loadingMore.value) {
+        getSimpleList(true);
+    }
+};
+
+// 重置并重新加载
+const resetAndLoad = () => {
+    getSimpleList(false);
+};
+
+function handleActiveFilter(i) {
+    activeIndex.value = i;
+    selectParma.value.districtCode = cityCode.value;
+    selectParma.value.farmWorkTypeId = null;
+    const statusMap = { 0: "待接受", 2: "已接受", 3: "执行中" };
+    activeStatus.value = statusMap[i] || "待接受";
+}
+
+function handleRemindCustomer(item) {
+    // 接受
+}
+</script>
+
+<style lang="scss" scoped>
+.task-page {
+    width: 100%;
+    height: calc(100vh - 50px - 50px);
+    overflow: auto;
+    box-sizing: border-box;
+    background: #f5f7fb;
+    .map-container {
+        width: 100%;
+        height: 162px;
+        clip-path: inset(0px round 8px);
+    }
+
+    .select-group {
+        display: flex;
+        padding: 0px 12px 0 12px;
+        .select-item {
+            width: 100%;
+            ::v-deep {
+                .el-select__wrapper {
+                    text-align: center;
+                    gap: 2px;
+                    box-shadow: none;
+                    justify-content: center;
+                    background: none;
+                }
+                .el-select__selection {
+                    flex: none;
+                    width: fit-content;
+                }
+                .el-select__placeholder {
+                    position: static;
+                    transform: none;
+                    width: fit-content;
+                    color: rgba(0, 0, 0, 0.2);
+                }
+                .el-select__caret {
+                    color: rgba(0, 0, 0, 0.2);
+                }
+            }
+        }
+    }
+
+    .task-top {
+        padding: 10px 12px 0 12px;
+    }
+
+    .task-content-loading {
+        height: 80px;
+        border-radius: 8px;
+        position: absolute;
+        top: 60px;
+        left: 0;
+        width: 100%;
+    }
+
+    .task-content {
+        min-height: 80px;
+    }
+
+    .empty-data {
+        text-align: center;
+        font-size: 14px;
+        color: #6f7274;
+        padding: 20px 0;
+    }
+    .task-list {
+        position: relative;
+        background: #fff;
+        padding: 12px 12px 8px 12px;
+    }
+    .list-filter {
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        .filter-item {
+            padding: 0 12px;
+            height: 28px;
+            color: rgba(0, 0, 0, 0.5);
+            font-size: 14px;
+            line-height: 28px;
+            border-radius: 20px;
+            &.active {
+                color: #2199f8;
+                background: rgba(33, 153, 248, 0.2);
+            }
+        }
+    }
+    .work-task-list {
+        margin-top: 10px;
+
+        .task-item {
+            margin-top: 10px;
+            background: #fff;
+            border-radius: 8px;
+            padding: 12px 16px;
+            border: 1px solid rgba(0, 0, 0, 0.08);
+            position: relative;
+
+            &.timeout-item {
+                border: 1px solid #f74e4e;
+            }
+
+            .item-title {
+                display: flex;
+                align-items: center;
+                padding-bottom: 10px;
+                border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+                color: #1d2129;
+                font-size: 16px;
+
+                .task-icon {
+                    width: 16px;
+                }
+
+                .title-text {
+                    padding-left: 8px;
+                }
+
+                .task-status {
+                    margin-left: 8px;
+                    border-radius: 2px;
+                    height: 20px;
+                    line-height: 18px;
+                    padding: 1px 6px;
+                    box-sizing: border-box;
+                    font-size: 12px;
+                    color: #ffb32f;
+                    background: rgba(255, 179, 47, 0.1);
+
+                    &.task-status--1 {
+                        background: rgba(33, 153, 248, 0.1);
+                        color: #2199f8;
+                    }
+                    &.task-status--2 {
+                        background: rgba(255, 179, 47, 0.1);
+                        color: #FFB32F;
+                    }
+                    &.task-status--3 {
+                        background: rgba(58, 173, 148, 0.1);
+                        color: #3AAD94;
+                    }
+                }
+            }
+
+            .task-tag {
+                position: absolute;
+                right: 0;
+                top: 0;
+                background: rgba(33, 153, 248, 0.1);
+                color: #2199f8;
+                font-size: 12px;
+                padding: 0 6px;
+                height: 25px;
+                line-height: 25px;
+                border-radius: 0 8px 0 8px;
+
+                &.timeout {
+                    background: #ff4747;
+                    color: #fff;
+                    display: flex;
+                    align-items: center;
+                    gap: 2px;
+                }
+            }
+
+            .item-content {
+                padding: 12px 0;
+
+                .excutor-info {
+                    margin-top: 12px;
+                    background: rgba(189, 189, 189, 0.1);
+                    border-radius: 6px;
+                    padding-bottom: 12px;
+
+                    .excutor-info-top {
+                        padding: 12px 12px 0 12px;
+                    }
+                }
+
+                .executor-stats {
+                    display: flex;
+                    align-items: center;
+                    padding-top: 10px;
+                    justify-content: space-around;
+                }
+
+                .cell-line {
+                    height: 20px;
+                    width: 1px;
+                    background: #e5e6eb;
+                }
+
+                .stat-value {
+                    font-size: 14px;
+                    font-weight: 500;
+                    color: #1d2129;
+                    margin-bottom: 4px;
+                    word-break: break-all;
+                }
+
+                .stat-label {
+                    font-size: 10px;
+                    line-height: 20px;
+                    color: #86909c;
+                }
+            }
+
+            .item-info {
+                color: #86909c;
+                font-size: 12px;
+                line-height: 18px;
+
+                .info-item {
+                    display: flex;
+
+                    .info-name {
+                        flex: none;
+                    }
+                }
+
+                .val-text {
+                    color: #1d2129;
+                }
+
+                .info-item + .info-item {
+                    padding-top: 12px;
+                }
+            }
+
+            .btn-group {
+                display: flex;
+                align-items: center;
+                justify-content: end;
+                width: 100%;
+                gap: 10px;
+            }
+
+            .edit-btn {
+                flex: 1;
+                width: 100%;
+                height: 30px;
+                border-radius: 4px;
+                text-align: center;
+                line-height: 30px;
+                font-size: 14px;
+                border: 0.5px solid transparent;
+
+                &.normal {
+                    flex: none;
+                    width: 131px;
+                    background: #fff;
+                    border: 0.5px solid rgba(0, 0, 0, 0.2);
+                    color: #1d2129;
+                }
+
+                &.primary {
+                    background: #2199f8;
+                    color: #fff;
+                }
+            }
+
+            .compare-imgs {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+                position: relative;
+                margin: 10px 0;
+
+                .img-tag {
+                    position: absolute;
+                    z-index: 10;
+                    top: 0;
+                    left: 0;
+                    font-size: 12px;
+                    color: #fff;
+                    background: rgba(0, 0, 0, 0.7);
+                    border-radius: 5px 0 5px 0;
+                    padding: 0 8px;
+                    height: 15px;
+                    line-height: 15px;
+
+                    &.right-tag {
+                        right: 0;
+                        left: auto;
+                        border-radius: 0 5px 0 5px;
+                    }
+                }
+
+                .img-item {
+                    flex: 1;
+                    height: 120px;
+                    overflow: hidden;
+                    border: 0.5px solid rgba(0, 0, 0, 0.1);
+                    border-radius: 5px;
+
+                    img {
+                        width: 100%;
+                        height: 100%;
+                        border-radius: 5px;
+                        object-fit: cover;
+                    }
+                }
+            }
+        }
+
+        .unqualified-reason {
+            color: #f74e4e;
+            font-size: 12px;
+            padding: 4px 10px;
+            background: rgba(247, 78, 78, 0.08);
+            border-radius: 4px;
+            line-height: 18px;
+        }
+
+        .empty-tip {
+            text-align: center;
+            color: #86909c;
+            font-size: 14px;
+            padding: 20px 0;
+        }
+    }
+}
+</style>