ソースを参照

Merge branch 'master' of http://www.sysuimars.cn:3000/feiniao/feiniao-farm-h5

lxf 10 時間 前
コミット
91f8477559

+ 20 - 0
src/api/modules/monitor.js

@@ -38,4 +38,24 @@ module.exports = {
         url: config.base_dev_url + "container_phenology_sample_files_speak_title/getFarmSpeakInfo",
         type: "get",
     },
+    //根据containerSpaceTimeId查询物候期列表
+    listPhenology: {
+        url: config.base_dev_url + "container_space_time/listPhenologyByContainerSpaceTimeId",
+        type: "get",
+    },
+    //更新农事编排信息
+    updateFarmWorkArrange: {
+        url: config.base_dev_url + "container_farm_work_arrange/update",
+        type: "post",
+    },
+    //批量保存多个物候期的农场自定义时间
+    batchSaveFarmPhenologyTime: {
+        url: config.base_dev_url + "container_space_time/batchSaveFarmPhenologyTime",
+        type: "post",
+    },
+    //保存农事卡片数据(新增或修改)
+    saveFarmWorkLib: {
+        url: config.base_dev_url + "z_farm_work_lib/save",
+        type: "post",
+    },
 }

BIN
src/assets/img/monitor/popup-header-bg.png


+ 579 - 0
src/components/pageComponents/FarmWorkPlanTimeline.vue

@@ -0,0 +1,579 @@
+<template>
+    <div
+        class="timeline-container"
+        ref="timelineContainerRef"
+        :class="{ 'timeline-container-plant': pageType === 'plant' }"
+    >
+        <div class="timeline-list" :style="getListStyle">
+            <div class="timeline-middle-line"></div>
+            <!-- 物候期覆盖条(progress 为起点,progress2 为终点,单位 %) -->
+            <div
+                v-for="(p, idx) in phenologyList"
+                :key="p.id ?? idx"
+                class="phenology-bar"
+                :style="getPhenologyBarStyle(p)"
+            >
+                <div class="reproductive-list">
+                    <div
+                        v-for="(r, rIdx) in Array.isArray(p.reproductiveList) ? p.reproductiveList : []"
+                        :key="r.id ?? rIdx"
+                        class="reproductive-item"
+                        :class="{
+                            'horizontal-text': getReproductiveItemHeight(p) < 30,
+                            'vertical-lr-text': getReproductiveItemHeight(p) >= 30,
+                        }"
+                        :style="
+                            getReproductiveItemHeight(p) < 30
+                                ? { '--item-height': `${getReproductiveItemHeight(p)}px` }
+                                : {}
+                        "
+                    >
+                        {{ r.name }}
+                        <div class="arranges">
+                            <div
+                                v-for="(fw, aIdx) in Array.isArray(r.farmWorkArrangeList)
+                                    ? r.farmWorkArrangeList
+                                    : []"
+                                :key="fw.id ?? aIdx"
+                                class="arrange-card"
+                                :class="getArrangeStatusClass(fw)"
+                                @click="handleRowClick(fw)"
+                            >
+                                <div class="card-header">
+                                    <div class="header-left">
+                                        <span class="farm-work-name">{{ fw.farmWorkName || "农事名称" }}</span>
+                                        <span class="tag-standard">标准农事</span>
+                                    </div>
+                                    <div class="header-right">托管农事</div>
+                                </div>
+                                <div class="card-content">
+                                    <span>{{ fw.warmReminder || "暂无提示" }}</span>
+                                    <span v-if="!disableClick" class="edit-link" @click.stop="handleEdit(fw)">点击编辑</span>
+                                </div>
+                                <div
+                                    v-if="
+                                        getArrangeStatusClass(fw) === 'status-complete' ||
+                                        getArrangeStatusClass(fw) === 'status-warning'
+                                    "
+                                    class="status-icon"
+                                    :class="getArrangeStatusClass(fw)"
+                                >
+                                    <el-icon
+                                        v-if="getArrangeStatusClass(fw) === 'status-complete'"
+                                        size="16"
+                                        color="#1CA900"
+                                    >
+                                        <SuccessFilled />
+                                    </el-icon>
+                                    <el-icon v-else size="18" color="#FF953D">
+                                        <WarnTriangleFilled />
+                                    </el-icon>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div v-for="t in solarTerms" :key="t.id" class="timeline-term" :style="getTermStyle(t)">
+                <span class="term-name">{{ t.displayName }}</span>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, computed, nextTick, watch } from "vue";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({
+    // 农场 ID,用于请求农事规划数据
+    farmId: {
+        type: [String, Number],
+        default: null,
+    },
+    // 页面类型:种植方案 / 农事规划,用来控制高度样式
+    pageType: {
+        type: String,
+        default: "",
+    },
+    // 是否禁用所有点击事件(用于只读展示)
+    disableClick: {
+        type: Boolean,
+        default: false,
+    },
+    containerId: {
+        type: [Number, String],
+        default: null,
+    },
+});
+
+const emits = defineEmits(["row-click", "edit"]);
+
+const solarTerms = ref([]);
+const phenologyList = ref([]);
+const timelineContainerRef = ref(null);
+// 标记是否为首次加载
+const isInitialLoad = ref(true);
+
+// 获取当前季节
+const getCurrentSeason = () => {
+    const month = new Date().getMonth() + 1; // 1-12
+    if (month >= 3 && month <= 5) {
+        return "spring"; // 春季:3-5月
+    } else if (month >= 6 && month <= 8) {
+        return "summer"; // 夏季:6-8月
+    } else if (month >= 9 && month <= 10) {
+        return "autumn"; // 秋季:9-10月
+    } else {
+        return "winter"; // 冬季:11-2月
+    }
+};
+
+// 安全解析时间到时间戳(ms)
+const safeParseDate = (val) => {
+    if (!val) return NaN;
+    if (val instanceof Date) return val.getTime();
+    if (typeof val === "number") return val;
+    if (typeof val === "string") {
+        // 兼容 "YYYY-MM-DD HH:mm:ss" -> Safari
+        const s = val.replace(/-/g, "/").replace("T", " ");
+        const d = new Date(s);
+        return isNaN(d.getTime()) ? NaN : d.getTime();
+    }
+    return NaN;
+};
+
+// 计算最小progress值(第一个节气的progress)
+const minProgress = computed(() => {
+    if (!solarTerms.value || solarTerms.value.length === 0) return 0;
+    const progresses = solarTerms.value.map((t) => Number(t?.progress) || 0).filter((p) => !isNaN(p));
+    return progresses.length > 0 ? Math.min(...progresses) : 0;
+});
+
+// 计算最大progress值
+const maxProgress = computed(() => {
+    if (!solarTerms.value || solarTerms.value.length === 0) return 100;
+    const progresses = solarTerms.value.map((t) => Number(t?.progress) || 0).filter((p) => !isNaN(p));
+    return progresses.length > 0 ? Math.max(...progresses) : 100;
+});
+
+// 列表高度
+const getListStyle = computed(() => {
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP); // 避免除0
+    const total = (solarTerms.value?.length || 0) * 1200;
+    const minH = range === 0 ? 0 : total;
+    return { minHeight: `${minH}px` };
+});
+
+const getTermStyle = (t) => {
+    const p = Math.max(0, Math.min(100, Number(t?.progress) || 0));
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP); // 避免除0
+    const total = (solarTerms.value?.length || 0) * 1200;
+    // 将progress映射到0开始的位置,最小progress对应top: 0
+    const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
+    const top = (normalizedP / 100) * total;
+    return {
+        position: "absolute",
+        top: `${top}px`,
+        left: 0,
+        width: "30px",
+        height: "20px",
+        display: "flex",
+        alignItems: "flex-start",
+    };
+};
+
+// 点击季节 → 滚动到对应节气(立春/立夏/立秋/立冬)
+const handleSeasonClick = (seasonValue) => {
+    const mapping = {
+        spring: "立春",
+        summer: "立夏",
+        autumn: "立秋",
+        winter: "立冬",
+    };
+    const targetName = mapping[seasonValue];
+    if (!targetName) return;
+    const target = (solarTerms.value || []).find((t) => (t?.displayName || "") === targetName);
+    if (!target) return;
+    const p = Math.max(0, Math.min(100, Number(target.progress) || 0));
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP);
+    const total = (solarTerms.value?.length || 0) * 1200;
+    const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
+    const targetTop = (normalizedP / 100) * total; // 内容内的像素位置
+    const wrap = timelineContainerRef.value;
+    if (!wrap) return;
+    const viewH = wrap.clientHeight || 0;
+    const maxScroll = Math.max(0, wrap.scrollHeight - viewH);
+    // 将目标位置稍微靠上(使用 0.35 视口高度做偏移)
+    let scrollTop = Math.max(0, targetTop - viewH * 0.1);
+    if (scrollTop > maxScroll) scrollTop = maxScroll;
+    wrap.scrollTo({ top: scrollTop, behavior: "smooth" });
+};
+
+// 物候期覆盖条样式
+const getPhenologyBarStyle = (item) => {
+    const p1 = Math.max(0, Math.min(100, Number(item?.progress) || 0));
+    const p2 = Math.max(0, Math.min(100, Number(item?.progress2) || 0));
+    const start = Math.min(p1, p2);
+    const end = Math.max(p1, p2);
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP);
+    const total = (solarTerms.value?.length || 0) * 1200; // 有效绘制区高度(px)
+    // 将progress映射到0开始的位置
+    const normalizedStart = range > 0 ? ((start - minP) / range) * 100 : 0;
+    const normalizedEnd = range > 0 ? ((end - minP) / range) * 100 : 0;
+    let topPx = (normalizedStart / 100) * total;
+    let heightPx = Math.max(2, ((normalizedEnd - normalizedStart) / 100) * total);
+
+    // 顶部对齐
+    const firstTermTop = 0;
+    const minTop = firstTermTop + 10;
+    if (topPx < minTop) {
+        const diff = minTop - topPx;
+        topPx = minTop;
+        heightPx = Math.max(2, heightPx - diff);
+    }
+
+    // 底部对齐
+    const lastTermTop = (100 / 100) * total;
+    const maxBottom = lastTermTop + 35;
+    const barBottom = topPx + heightPx;
+    if (barBottom > maxBottom) {
+        heightPx = Math.max(2, maxBottom - topPx);
+    }
+
+    const now = Date.now();
+    const isFuture = Number.isFinite(item?.startTimeMs) ? item.startTimeMs > now : start > 0;
+    const barColor = isFuture ? "rgba(145, 145, 145, 0.1)" : "#2199F8";
+    const beforeBg = isFuture ? "rgba(145, 145, 145, 0.1)" : "rgba(33, 153, 248, 0.1)";
+    return {
+        position: "absolute",
+        left: "46px",
+        width: "25px",
+        top: `${topPx}px`,
+        height: `${heightPx}px`,
+        background: barColor,
+        color: isFuture ? "#747778" : "#fff",
+        "--bar-before-bg": beforeBg,
+        zIndex: 2,
+    };
+};
+
+// 农事状态样式映射(0:默认,1-4:正常,5:完成,6:预警)
+const getArrangeStatusClass = (fw) => {
+    const t = fw?.flowStatus;
+    if (t == null) return "status-default";
+    if (t >= 0 && t <= 4) return "status-normal";
+    if (t === 5) return "status-complete";
+    if (t === 6) return "status-warning";
+    return "status-default";
+};
+
+// 计算 phenology-bar 的高度(px)
+const getPhenologyBarHeight = (item) => {
+    const p1 = Math.max(0, Math.min(100, Number(item?.progress) || 0));
+    const p2 = Math.max(0, Math.min(100, Number(item?.progress2) || 0));
+    const start = Math.min(p1, p2);
+    const end = Math.max(p1, p2);
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP);
+    const total = (solarTerms.value?.length || 0) * 1200;
+    const normalizedStart = range > 0 ? ((start - minP) / range) * 100 : 0;
+    const normalizedEnd = range > 0 ? ((end - minP) / range) * 100 : 0;
+    const heightPx = Math.max(2, ((normalizedEnd - normalizedStart) / 100) * total);
+    return heightPx;
+};
+
+// 计算 reproductive-item 的高度(px)
+const getReproductiveItemHeight = (phenologyItem) => {
+    const barHeight = getPhenologyBarHeight(phenologyItem);
+    const listLength = Array.isArray(phenologyItem?.reproductiveList) ? phenologyItem.reproductiveList.length : 1;
+    return listLength > 0 ? barHeight / listLength : barHeight;
+};
+
+const handleRowClick = (item) => {
+    if (props.disableClick) return;
+    emits("row-click", item);
+};
+
+const handleEdit = (item) => {
+    if (props.disableClick) return;
+    emits("edit", item);
+};
+
+// 获取农事规划数据
+const getFarmWorkPlan = () => {
+    if (!props.farmId && !props.containerId) return;
+    let savedScrollTop = 0;
+    if (!isInitialLoad.value && timelineContainerRef.value) {
+        savedScrollTop = timelineContainerRef.value.scrollTop || 0;
+    }
+
+    VE_API.monitor
+        .farmWorkPlan({ farmId: props.farmId, containerId: props.containerId })
+        .then(({ data, code }) => {
+            if (code === 0) {
+                const list = Array.isArray(data?.solarTermsList) ? data.solarTermsList : [];
+                const filtered = list
+                    .filter((t) => t && t.type === 1)
+                    .map((t) => ({
+                        id:
+                            t.id ??
+                            t.solarTermsId ??
+                            t.termId ??
+                            `${t.name || t.solarTermsName || t.termName || "term"}-${t.createDate || ""}`,
+                        displayName: t.name || t.solarTermsName || t.termName || "节气",
+                        createDate: t.createDate || null,
+                        progress: Number(t.progress) || 0,
+                    }));
+                solarTerms.value = filtered;
+                // 物候期数据
+                phenologyList.value = Array.isArray(data?.phenologyList)
+                    ? data.phenologyList.map((it) => {
+                          const reproductiveList = Array.isArray(it.reproductiveList)
+                              ? it.reproductiveList.map((r) => {
+                                    const farmWorkArrangeList = Array.isArray(r.farmWorkArrangeList)
+                                        ? r.farmWorkArrangeList.map((fw) => ({
+                                              ...fw,
+                                              containerSpaceTimeId: it.containerSpaceTimeId,
+                                          }))
+                                        : [];
+                                    return {
+                                        ...r,
+                                        farmWorkArrangeList,
+                                    };
+                                })
+                              : [];
+
+                          return {
+                              id: it.id ?? it.phenologyId ?? it.name ?? `${it.progress}-${it.progress2}`,
+                              progress: Number(it.progress) || 0, // 起点 %
+                              progress2: Number(it.progress2) || 0, // 终点 %
+                              startTimeMs: safeParseDate(
+                                  it.startDate || it.beginDate || it.startTime || it.start || it.start_at
+                              ),
+                              reproductiveList,
+                          };
+                      })
+                    : [];
+
+                nextTick(() => {
+                    if (isInitialLoad.value) {
+                        const currentSeason = getCurrentSeason();
+                        handleSeasonClick(currentSeason);
+                        isInitialLoad.value = false;
+                    } else if (timelineContainerRef.value && savedScrollTop > 0) {
+                        timelineContainerRef.value.scrollTop = savedScrollTop;
+                    }
+                });
+            }
+        })
+        .catch((error) => {
+            console.error("获取农事规划数据失败:", error);
+            ElMessage.error("获取农事规划数据失败");
+        });
+};
+
+watch(
+    () => props.farmId || props.containerId,
+    (val) => {
+        if (val) {
+            isInitialLoad.value = true;
+            getFarmWorkPlan();
+        }
+    },
+    { immediate: true }
+);
+</script>
+
+<style scoped lang="scss">
+.timeline-container {
+    height: 100%;
+    overflow: auto;
+    position: relative;
+    box-sizing: border-box;
+    .timeline-list {
+        position: relative;
+    }
+    .timeline-middle-line {
+        position: absolute;
+        left: 15px; /* 位于节气文字列中间(列宽约30px) */
+        top: 0;
+        bottom: 0;
+        width: 2px;
+        background: #e8e8e8;
+        z-index: 1;
+    }
+    .phenology-bar {
+        display: flex;
+        align-items: stretch;
+        justify-content: center;
+        box-sizing: border-box;
+        .reproductive-list {
+            display: grid;
+            grid-auto-rows: 1fr; /* 子项等高,整体等分父高度 */
+            align-items: stretch;
+            justify-items: center; /* 子项居中 */
+            width: 100%;
+            height: 100%;
+            box-sizing: border-box;
+        }
+        .reproductive-item {
+            font-size: 12px;
+            text-align: center;
+            word-break: break-all;
+            writing-mode: vertical-rl;
+            text-orientation: upright;
+            letter-spacing: 3px;
+            width: 100%;
+            line-height: 23px;
+            color: inherit;
+            position: relative;
+            &.horizontal-text {
+                writing-mode: horizontal-tb;
+                text-orientation: mixed;
+                letter-spacing: normal;
+                line-height: calc(var(--item-height, 15px) - 3px);
+            }
+            &.vertical-lr-text {
+                writing-mode: vertical-lr;
+                text-orientation: upright;
+                letter-spacing: 3px;
+                line-height: 26px;
+            }
+            .arranges {
+                position: absolute;
+                left: 40px; /* 列与中线右侧一段距离 */
+                top: 0;
+                z-index: 3;
+                display: flex;
+                max-width: calc(100vw - 100px);
+                gap: 12px;
+                letter-spacing: 0px;
+                .arrange-card {
+                    width: 97%;
+                    border: 0.5px solid #2199f8;
+                    border-radius: 8px;
+                    background: #fff;
+                    box-sizing: border-box;
+                    position: relative;
+                    padding: 8px;
+                    writing-mode: horizontal-tb;
+                    .card-header {
+                        display: flex;
+                        justify-content: space-between;
+                        align-items: center;
+                        .header-left {
+                            display: flex;
+                            align-items: center;
+                            gap: 8px;
+                            .farm-work-name {
+                                font-size: 14px;
+                                font-weight: 500;
+                                color: #1d2129;
+                            }
+                            .tag-standard {
+                                padding: 0 8px;
+                                background: rgba(119, 119, 119, 0.1);
+                                border-radius: 25px;
+                                font-weight: 400;
+                                font-size: 12px;
+                                color: #000;
+                            }
+                        }
+                        .header-right {
+                            font-size: 12px;
+                            color: #808080;
+                            padding: 0 8px;
+                            border-radius: 25px;
+                        }
+                    }
+                    .card-content {
+                        color: #909090;
+                        text-align: left;
+                        line-height: 1.55;
+                        margin: 4px 0 2px 0;
+                        .edit-link {
+                            color: #2199f8;
+                            margin-left: 5px;
+                        }
+                    }
+                    .status-icon {
+                        position: absolute;
+                        right: -8px;
+                        bottom: -8px;
+                        z-index: 3;
+                    }
+                    &::before {
+                        content: "";
+                        position: absolute;
+                        left: -6px;
+                        top: 50%;
+                        transform: translateY(-50%);
+                        width: 0;
+                        height: 0;
+                        border-top: 5px solid transparent;
+                        border-bottom: 5px solid transparent;
+                        border-right: 5px solid #2199f8;
+                    }
+                }
+                .arrange-card.status-warning {
+                    border-color: #ff953d;
+                    &::before {
+                        border-right-color: #ff953d;
+                    }
+                }
+                .arrange-card.status-complete {
+                    border-color: #1ca900;
+                    &::before {
+                        border-right-color: #1ca900;
+                    }
+                }
+                .arrange-card.status-normal {
+                    border-color: #2199f8;
+                    &::before {
+                        border-right-color: #2199f8;
+                    }
+                }
+            }
+        }
+    }
+    .reproductive-item + .reproductive-item {
+        border-top: 2px solid #fff;
+    }
+    .phenology-bar + .phenology-bar {
+        border-top: 2px solid #fff;
+    }
+    .timeline-term {
+        position: absolute;
+        width: 30px;
+        padding-right: 16px;
+        display: flex;
+        align-items: flex-start;
+        z-index: 2; /* 置于中线之上 */
+        .term-name {
+            display: inline-block;
+            width: 100%;
+            height: 46px;
+            line-height: 30px;
+            background: #f5f7fb;
+            font-size: 13px;
+            word-break: break-all;
+            writing-mode: vertical-rl;
+            text-orientation: upright;
+            color: rgba(174, 174, 174, 0.6);
+            text-align: center;
+        }
+    }
+}
+</style>
+
+

+ 90 - 75
src/components/popup/interactPopup.vue

@@ -1,7 +1,7 @@
 <template>
     <popup class="interact-popup" v-model:show="show" closeable :close-on-click-overlay="false" @closed="handleClosed">
         <div class="interact-header">
-            <div class="interact-title">{{ interactTitle }}</div>
+            <div class="interact-title">{{ currentData.farmWorkName || currentData.name }}</div>
         </div>
         <div class="interact-form">
             <div class="form-item">
@@ -10,8 +10,13 @@
                     请选择互动时间
                 </div>
                 <div class="form-input-wrapper">
-                    <el-select v-model="interactTime" size="large" placeholder="请选择物候期" :editable="false">
-                        <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+                    <el-select v-model="formData.phenologyId" size="large" placeholder="请选择物候期" :editable="false">
+                        <el-option
+                            v-for="item in phenologyList"
+                            :key="item.id"
+                            :label="item.name"
+                            :value="item.id"
+                        ></el-option>
                     </el-select>
                 </div>
             </div>
@@ -22,7 +27,7 @@
                 </div>
                 <div class="form-input-wrapper">
                     <el-date-picker
-                        v-model="forceTriggerTime"
+                        v-model="formData.interactionTime"
                         size="large"
                         style="width: 100%"
                         type="date"
@@ -37,7 +42,7 @@
                     请设置互动问题
                 </div>
                 <el-input
-                    v-model="interactQuestion"
+                    v-model="formData.interactionQuestion"
                     type="textarea"
                     :rows="4"
                     placeholder="请设置互动问题"
@@ -66,72 +71,75 @@ const emit = defineEmits(["handleSaveSuccess", "handleDeleteInteract"]);
 const show = ref(false);
 const arrangeId = ref(null);
 const isSaving = ref(false);
-const interactTitle = ref("梢期杀虫");
-const interactTime = ref("");
-const forceTriggerTime = ref("");
-const interactQuestion = ref("");
-
-const options = [
-    {
-        value: "Option1",
-        label: "Option1",
-    },
-    {
-        value: "Option2",
-        label: "Option2",
-    },
-    {
-        value: "Option3",
-        label: "Option3",
-    },
-    {
-        value: "Option4",
-        label: "Option4",
-    },
-    {
-        value: "Option5",
-        label: "Option5",
-    },
-];
+
+const formData = ref({
+    phenologyId: "",
+    interactionTime: "",
+    interactionQuestion: "",
+});
 
 // 计算属性
 const saveButtonText = computed(() => (isSaving.value ? "保存中..." : "保存修改"));
 
 // 工具函数
 const resetInteractData = () => {
-    interactTime.value = "";
-    forceTriggerTime.value = "";
-    interactQuestion.value = "";
+    formData.value = {
+        phenologyId: "",
+        interactionTime: "",
+        interactionQuestion: "",
+    };
 };
 
 // 验证函数
 const validateInteractForm = () => {
-    if (!interactTime.value) {
+    if (!formData.value.phenologyId) {
         ElMessage.warning("请选择互动时间");
         return false;
     }
-    if (!forceTriggerTime.value) {
+    if (!formData.value.interactionTime) {
         ElMessage.warning("请选择强制触发互动时间");
         return false;
     }
-    if (!interactQuestion.value?.trim()) {
+    if (!formData.value.interactionQuestion?.trim()) {
         ElMessage.warning("请设置互动问题");
         return false;
     }
     return true;
 };
 
+const phenologyList = ref(null);
+const getPhenologyList = async (containerSpaceTimeId) => {
+    if (!containerSpaceTimeId) {
+        phenologyList.value = [];
+        return;
+    }
+    const res = await VE_API.monitor.listPhenology({ containerSpaceTimeId });
+    if (res.code === 0) {
+        phenologyList.value = res.data || [];
+    }
+};
+
+const getFarmWorkArrangeDetail = async (id) => {
+    const { data, code } = await VE_API.farm.getFarmWorkArrangeDetail({ id });
+    if(code === 0) {
+        formData.value = {
+            phenologyId: data.phenologyId,
+            interactionTime: data.interactionTime,
+            interactionQuestion: data.interactionQuestion || "",
+        };
+    }
+};
+
+const currentData = ref(null);
 // 显示弹窗方法
-const showPopup = ({ arrangeIdVal, interactTitleVal, interactTimeVal, forceTriggerTimeVal, interactQuestionVal }) => {
+const showPopup = async (data) => {
     // 重置数据
     resetInteractData();
-
+    await getPhenologyList(data.containerSpaceTimeId);
+    await getFarmWorkArrangeDetail(data.id);
     // 设置数据
-    arrangeId.value = arrangeIdVal;
-    interactTitle.value = interactTitleVal || "梢期杀虫";
-    interactTime.value = interactTimeVal || "";
-    forceTriggerTime.value = forceTriggerTimeVal || "";
-    interactQuestion.value = interactQuestionVal || "";
+    currentData.value = data;
+
     isSaving.value = false;
 
     show.value = true;
@@ -154,44 +162,51 @@ const handleDeleteInteract = () => {
         });
 };
 
+function formatDate(date) {
+    // 如果已经是字符串格式 YYYY-MM-DD,直接返回
+    if (typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
+        return date;
+    }
+    // 如果是 Date 对象,进行转换
+    if (date instanceof Date) {
+        let year = date.getFullYear();
+        let month = String(date.getMonth() + 1).padStart(2, "0");
+        let day = String(date.getDate()).padStart(2, "0");
+        return `${year}-${month}-${day}`;
+    }
+    // 其他情况返回原值
+    return date;
+}
+
 const handleSaveInteract = () => {
     if (isSaving.value || !validateInteractForm()) return;
 
     isSaving.value = true;
 
     const paramsObj = {
-        arrangeId: arrangeId.value,
-        interactTime: interactTime.value,
-        forceTriggerTime: forceTriggerTime.value,
-        interactQuestion: interactQuestion.value.trim(),
+        id: currentData.value.id,
+        ...formData.value,
+        interactionTime: formatDate(formData.value.interactionTime),
     };
 
-    // TODO: 调用保存互动设置的API
-    // VE_API.monitor.saveInteractSetting(paramsObj)
-    //     .then((res) => {
-    //         if (res.code === 0) {
-    //             ElMessage.success("保存成功");
-    //             show.value = false;
-    //             emit("handleSaveSuccess", paramsObj);
-    //         } else {
-    //             ElMessage.error(res.message || "保存失败");
-    //         }
-    //     })
-    //     .catch((error) => {
-    //         console.error("保存互动设置失败:", error);
-    //         ElMessage.error("保存失败,请重试");
-    //     })
-    //     .finally(() => {
-    //         isSaving.value = false;
-    //     });
-
-    // 临时模拟保存成功
-    setTimeout(() => {
-        ElMessage.success("保存成功");
-        show.value = false;
-        emit("handleSaveSuccess", paramsObj);
-        isSaving.value = false;
-    }, 500);
+    VE_API.monitor
+        .updateFarmWorkArrange(paramsObj)
+        .then((res) => {
+            if (res.code === 0) {
+                ElMessage.success("保存成功");
+                show.value = false;
+                emit("handleSaveSuccess", paramsObj);
+            } else {
+                ElMessage.error(res.message || "保存失败");
+            }
+        })
+        .catch((error) => {
+            console.error("保存互动设置失败:", error);
+            ElMessage.error("保存失败,请重试");
+        })
+        .finally(() => {
+            isSaving.value = false;
+        });
 };
 
 const handleClosed = () => {

+ 38 - 19
src/views/old_mini/create_farm/index.vue

@@ -123,17 +123,17 @@
                                             <div class="unit">亩</div>
                                         </div>
                                     </el-form-item>
-                                    <el-form-item label="客户类型" prop="clientType" class="select-wrap client-wrap">
+                                    <el-form-item label="客户类型" prop="schemeId" class="select-wrap client-wrap">
                                         <el-select
                                             class="select-item"
-                                            v-model="ruleForm.clientType"
-                                            placeholder="客户类型"
+                                            v-model="ruleForm.schemeId"
+                                            placeholder="请选择"
                                         >
                                             <el-option
                                                 v-for="(item, index) in clientTypeList"
                                                 :key="index"
-                                                :label="item.label"
-                                                :value="item.value"
+                                                :label="item.alias"
+                                                :value="item.id"
                                             />
                                         </el-select>
                                     </el-form-item>
@@ -145,14 +145,14 @@
                                             @input="handleFarmNameInput"
                                         />
                                     </el-form-item>
-                                    <Checkbox
+                                    <!-- <Checkbox
                                         v-if="paramsType !== 'client' && curRole == 0"
                                         class="checkbox"
                                         icon-size="18px"
                                         shape="square"
                                         v-model="ruleForm.defaultFarm"
                                         >是否勾选为默认农场</Checkbox
-                                    >
+                                    > -->
                                 </el-form>
                             </div>
                             <div class="create-btn">
@@ -329,16 +329,33 @@ onMounted(() => {
     }
 });
 
-const clientTypeList = [{
-    label:'全托管客户',
-    value:1
-}]
+// 客户类型列表
+const clientTypeList = ref([])
+const getClientTypeList = (containerId) => {
+    VE_API.home.listMySchemes({containerId}).then(({ data }) => {
+        clientTypeList.value = data || [];
+        if(data && data.length) {
+            ruleForm.schemeId = data[0].id;
+        }
+    });
+};
 
 const polygonArr = ref(null);
 const paramsType = ref(null);
 onActivated(() => {
     paramsType.value = route.query.type;
-
+    // 仅在携带 isReload 标记、且不是编辑/小程序回流场景时,认为是一次全新创建,重置表单和地块,
+    // 避免破坏原有自动生成农场名称等逻辑
+    if (route.query.isReload && paramsType.value !== "edit" && !route.query.miniJson) {
+        // 重置表单字段到初始值
+        ruleFormRef.value && ruleFormRef.value.resetFields();
+        // 重置与地块绘制相关的内部状态
+        polygonArr.value = null;
+        isFromEditMap.value = false;
+        hasDefaultPolygon.value = false;
+        // 清空上一次地块缓存
+        store.commit("home/SET_FARM_POLYGON", null);
+    }
     // 确保地图已初始化,使用 nextTick 等待 DOM 更新
     nextTick(() => {
         // 检查地图实例是否已初始化
@@ -498,7 +515,7 @@ const ruleForm = reactive({
     name: "",
     fzr: "",
     tel: "",
-    clientType: 1,
+    schemeId: null,
     defaultFarm: 0, // 0:否 1:是
 });
 // 自定义验证规则:验证面积必须是大于0的数字
@@ -519,21 +536,21 @@ const validateMianji = (rule, value, callback) => {
 
 const rules = reactive({
     address: [{ required: true, message: "请选择农场位置", trigger: "blur" }],
-    clientType: [{ required: true, message: "请选择客户类型", trigger: "blur" }],
+    schemeId: [{ required: true, message: "请选择客户类型", trigger: "blur" }],
     mu: [
         { required: true, message: "请输入农场面积", trigger: "blur" },
         { validator: validateMianji, trigger: ["blur", "change"] },
     ],
     speciesItem: [{ required: true, message: "请选择品类", trigger: "blur" }],
     typeId: [{ required: true, message: "请选择品种", trigger: "blur" }],
-    name: [{ required: true, message: "请输入您的农场名称", trigger: "blur" }],
-    fzr: [{ required: true, message: "请输入联系人姓名", trigger: "blur" }],
+    name: [{ required: true, message: "请输入您的农场名称", trigger: ["blur", "change"] }],
+    fzr: [{ required: true, message: "请输入联系人姓名", trigger: ["blur", "change"] }],
     tel: [
-        { required: true, message: "请输入联系人电话", trigger: "blur" },
+        { required: true, message: "请输入联系人电话", trigger: ["blur"] },
         {
             pattern: /^1[3-9]\d{9}$/,
             message: "请输入正确的手机号码",
-            trigger: "blur",
+            trigger: ["blur"],
         },
     ],
     defaultFarm: [{ required: true, message: "请选择是否为默认农场", trigger: "blur" }],
@@ -729,7 +746,7 @@ watch(
 const specieList = ref([]);
 
 function getSpecieList() {
-    return VE_API.farm.fetchSpecieList().then(({ data }) => {
+    return VE_API.farm.fetchSpecieList({point: centerPoint.value}).then(({ data }) => {
         specieList.value = data;
         return data;
     });
@@ -737,8 +754,10 @@ function getSpecieList() {
 
 function changeSpecie(v) {
     getFruitsTypeItemList(v.id);
+    ruleForm.schemeId = null;
     // 清空品种选择
     ruleForm.typeId = "";
+    getClientTypeList(v.defaultContainerId);
     // 只有在创建模式下且用户没有手动修改过农场名称时,才自动设置农场名称
     if (route.query.type !== "edit" && !isFarmNameManuallyModified.value) {
         ruleForm.name = farmCity.value + v.name + "农场";

+ 1 - 1
src/views/old_mini/home/subPages/prescriptionPage.vue

@@ -120,7 +120,7 @@ const handlePage = () => {
     }
     
     // 传递所有农场相关的参数,以便在 agricultural_plan 页面创建农场
-    const farmParams = ['wkt', 'speciesId', 'containerId', 'agriculturalCreate', 'geom', 'address', 'mu', 'name', 'fzr', 'tel', 'defaultFarm', 'typeId', 'speciesName'];
+    const farmParams = ['wkt', 'speciesId', 'containerId', 'agriculturalCreate', 'geom', 'address', 'mu', 'name', 'fzr', 'tel', 'defaultFarm', 'typeId', 'speciesName','schemeId'];
     farmParams.forEach(key => {
         if (route.query[key] !== undefined) {
             queryParams[key] = route.query[key];

+ 58 - 102
src/views/old_mini/modify_work/modify.vue

@@ -12,7 +12,7 @@
             >
                 <div class="farm-card">
                     <div class="card-title between">
-                        <div>{{ detailData?.farmWorkName }}<span class="type-tag">标准农事</span></div>
+                        <div>{{ detailData?.name }}<span class="type-tag">标准农事</span></div>
                         <el-popover
                             title=""
                             v-if="isEdit"
@@ -55,7 +55,7 @@
                         </el-form-item>
                         <el-form-item label-width="70px" class="form-item text-item" label="服务区域">
                             <div class="info-text">
-                                {{ detailData?.serviceRegion }}
+                                {{ detailData?.serviceRegion || "--" }}
                             </div>
                         </el-form-item>
                         <el-form-item
@@ -91,7 +91,7 @@
                         <!-- <span class="del-tag">删除互动</span> -->
                     </div>
                     <div class="interact-content">
-                        温馨提示:在某某物候期之后,请密切关注荔枝,关注蒂蛀虫的出现!
+                        {{ detailData?.warmReminder }}
                         <span class="edit-tag" @click="handleEditInteract(detailData)">点击编辑</span>
                     </div>
                 </div>
@@ -115,37 +115,30 @@
                         </el-form-item>
                         <div v-if="dynamicValidateForm.usageMode !== '人工农事'">
                             <el-form-item
-                                v-for="(prescriptionItem, prescriptionI) in dynamicValidateForm.prescriptionList"
-                                :key="prescriptionI"
-                                :prop="'prescriptions.' + prescriptionI + '.value'"
+                                v-for="(domain, index) in dynamicValidateForm.prescriptionList"
+                                :key="index"
+                                :prop="'prescriptionList.' + index + '.value'"
                                 class="prescription-item"
                             >
                                 <div class="recipe-item">
                                     <div class="sub-title">
-                                        <div>{{ prescriptionItem.name }}处方</div>
-                                        <div class="add-tag" @click="addDomain(prescriptionI)">
+                                        <div>{{ domain.name }}处方</div>
+                                        <div class="add-tag" @click="addDomain(index)">
                                             <el-icon color="#2199F8"><Plus /></el-icon>新增药物
                                         </div>
                                     </div>
                                     <div class="recipe-form">
-                                        <el-form-item
+                                        <!-- <el-form-item
                                             v-for="(domain, index) in prescriptionItem.pesticideFertilizerList"
                                             :key="domain.key"
                                             :prop="'pesticideFertilizerList.' + index + '.value'"
-                                        >
+                                        > -->
                                             <div class="form-box">
                                                 <div class="form-index">药肥{{ index + 1 }}</div>
                                                 <div class="box-item" v-if="domain.typeName">
                                                     <div class="form-l">药肥类型</div>
                                                     <div class="form-r r-text">
                                                         {{ domain.typeName }}
-                                                        <!-- <el-select
-                                                        v-model="domain.typeName"
-                                                        placeholder="请选择"
-                                                        style="width: 100%"
-                                                        >
-                                                            <el-option :label="domain.typeName" :value="domain.typeName" />
-                                                    </el-select> -->
                                                     </div>
                                                 </div>
                                                 <div class="box-item">
@@ -156,15 +149,15 @@
                                                             @change="
                                                                 handlePesticideFertilizerChange(prescriptionI, index)
                                                             "
-                                                            v-model="domain.pesticideFertilizerId"
+                                                            v-model="domain.code"
                                                             placeholder="请选择"
                                                             style="width: 150px"
                                                         >
                                                             <el-option
                                                                 v-for="item in pesticideFertilizersOptions"
-                                                                :key="item.id"
+                                                                :key="item.pesticideFertilizerCode"
                                                                 :label="item.defaultName || item.name"
-                                                                :value="item.id"
+                                                                :value="item.pesticideFertilizerCode"
                                                             />
                                                         </el-select>
                                                     </div>
@@ -207,11 +200,6 @@
                                                             <div class="sub-name">(药剂:兑水量)</div>
                                                         </div>
                                                         <div class="form-r input-box text-center">
-                                                            <!-- <el-input
-                                                                v-model="domain.ratio2"
-                                                                style="width: 100%"
-                                                                placeholder="请输入"
-                                                            /> -->
                                                             <el-input
                                                                 v-model="domain.ratio2"
                                                                 type="number"
@@ -249,12 +237,6 @@
                                                             <div class="sub-name">(药剂:兑水量)</div>
                                                         </div>
                                                         <div class="form-r input-box text-center input-unit">
-                                                            <!-- <el-input
-                                                            v-model="domain.ratio"
-                                                            style="width: 100%"
-                                                            placeholder="请输入"
-                                                        /> -->
-
                                                             <el-input
                                                                 v-model="domain.ratio"
                                                                 type="number"
@@ -278,7 +260,7 @@
                                                             placeholder="请输入"
                                                         /> -->
                                                             <el-input
-                                                                v-model="domain.muUsage"
+                                                                v-model="domain.dosage"
                                                                 type="number"
                                                                 step="0.01"
                                                                 style="width: 150px"
@@ -312,7 +294,7 @@
                                                     </el-button>
                                                 </div>
                                             </div>
-                                        </el-form-item>
+                                        <!-- </el-form-item> -->
                                     </div>
                                 </div>
                             </el-form-item>
@@ -438,40 +420,31 @@
                             </div>
                             <div
                                 class="new-table-wrap"
-                                v-for="(prescriptionItem, prescriptionI) in detailData?.prescriptionList"
+                                v-for="(subP, prescriptionI) in detailData?.prescriptionList"
                                 :key="prescriptionI"
                             >
-                                <div
+                                <!-- <div
                                     class="new-prescription"
                                     v-for="(subP, subI) in prescriptionItem.pesticideFertilizerList"
                                     :key="subI"
-                                >
+                                > -->
                                     <div class="new-table">
                                         <div class="line-l">
                                             <div class="line-1 title-1">{{ subP.typeName }}</div>
                                             <div class="line-2">
-                                                {{ subP.defaultName || subP.pesticideFertilizerName }}
+                                                {{ subP.name || subP.pesticideFertilizerName }}
                                             </div>
                                         </div>
                                         <div class="line-r">
                                             <div class="line-3">
                                                 <div class="sub-line title-3">人工</div>
                                                 <div class="sub-line title-4">{{ subP.ratio }}ML</div>
-                                                <div class="sub-line title-5">{{ subP.muUsage }}{{ subP.unit }}</div>
+                                                <div class="sub-line title-5">{{ subP.dosage }}{{ subP.unit }}</div>
                                             </div>
-                                            <!-- <div class="line-4" v-if="detailData?.usageMode === '叶面施'">
-                                                <div class="sub-line title-3 execute-line">无人机</div>
-                                                <div class="sub-line title-4">
-                                                    {{ subP.ratio2 ? subP.ratio2 + subP.unit : "---" }}
-                                                </div>
-                                                <div class="sub-line title-5">
-                                                    {{ subP.muUsage2 ? subP.muUsage2 + subP.unit : "---" }}
-                                                </div>
-                                            </div> -->
                                         </div>
                                     </div>
                                     <div class="note-text" v-if="subP.remark">{{ subP.remark }}</div>
-                                </div>
+                                <!-- </div> -->
                             </div>
                         </div>
                     </div>
@@ -577,10 +550,9 @@ const actionType = ref([]);
 const isEdit = ref(false);
 
 onActivated(() => {
-    const id = route.query.id;
     isEdit.value = route.query.isEdit ? true : false;
-    if (id) {
-        getDetail(id);
+    if (route.query.farmWorkId) {
+        getDetail();
     }
     window.scrollTo(0, 0);
     if (route.query.data) {
@@ -610,26 +582,19 @@ const showPriceSheetPopup = () => {
 };
 
 const detailData = ref({});
-const getDetail = async (id) => {
-    const { data } = await VE_API.z_farm_work_record.getDetail({ id });
-    const res = data[0];
-    detailData.value = res;
-    dynamicValidateForm.executeDate = res.executeDate;
-    dynamicValidateForm.usageMode = res.usageMode;
-
-    res.prescriptionList.forEach((item) => {
-        item.pesticideFertilizerList.forEach((pesticide) => {
-            pesticide.executionMethod = pesticide.executionMethod || 2; // 默认人工
-            pesticide.typeName = item.name;
-        });
-    });
-
-    dynamicValidateForm.prescriptionList = res.prescriptionList;
-    servicePricePerMu.value = detailData.value.farmWorkServiceCost || null;
-
-    getFarmWorkArrangeDetail(res.farmWorkArrangeId);
-
-    getQuotationData();
+const getDetail = async () => {
+    const { data, code } = await VE_API.farm.getFarmWorkLib({ id: route.query.farmWorkId });
+    if(code === 0) {
+        detailData.value = data;
+        dynamicValidateForm.executeDate = data.executeDate;
+        dynamicValidateForm.usageMode = data.usageMode;
+        data.prescriptionList = data.prescription?.pesticideFertilizerList || [];
+    
+        dynamicValidateForm.prescriptionList = data.prescriptionList;
+        servicePricePerMu.value = detailData.value.farmWorkServiceCost || null;
+    
+        // getQuotationData();
+    }
 };
 
 const toEditPrescription = () => {
@@ -697,14 +662,6 @@ const getQuotationData = async () => {
     }
 };
 
-// 获取农场现状
-const farmStatusText = ref("");
-const getFarmWorkArrangeDetail = (id) => {
-    VE_API.farm.getFarmWorkArrangeDetail({ id }).then(({ data }) => {
-        farmStatusText.value = data.farmStatus;
-    });
-};
-
 // 根据执行方式获取单亩用量:1=无人机用muUsage2,2=人工用muUsage
 const getMuUsage = (pesticide) => {
     if (!pesticide) return 0;
@@ -953,13 +910,6 @@ const submitForm = (formEl) => {
     if (!formEl) return;
     formEl.validate((valid) => {
         if (valid) {
-            // router.push({
-            //     path: "/completed_work",
-            //     query: {
-            //         id: 1,
-            //         status: 1,
-            //     },
-            // });
             submit();
         } else {
             console.log("error submit!");
@@ -969,15 +919,27 @@ const submitForm = (formEl) => {
 
 const submit = () => {
     const data = {
-        id: route.query.id,
+        id: route.query.farmWorkId,
         ...dynamicValidateForm,
+        prescription:{
+            ...detailData.value.prescription,
+            pesticideFertilizerList:dynamicValidateForm.prescriptionList
+        }
     };
-    VE_API.z_farm_work_record.issueFarmWorkRecord(data).then(async (res) => {
+    VE_API.monitor.saveFarmWorkLib(data).then(async (res) => {
         if (res.code === 0) {
-            await getDetail(route.query.id);
-            showPriceSheetPopup();
+            await getDetail();
+            ElMessage.success("保存成功");
+            isEdit.value = false;
+            // showPriceSheetPopup();
         }
     });
+    // VE_API.z_farm_work_record.issueFarmWorkRecord(data).then(async (res) => {
+    //     if (res.code === 0) {
+    //         await getDetail();
+    //         showPriceSheetPopup();
+    //     }
+    // });
 };
 
 const handleSaveInteractSuccess = () => {
@@ -991,13 +953,7 @@ const handleDeleteInteract = () => {
 const interactPopupRef = ref(null);
 const handleEditInteract = (item) => {
     if (interactPopupRef.value) {
-        interactPopupRef.value.showPopup({
-            arrangeIdVal: item.id,
-            interactTitleVal: item.farmWorkName || "梢期杀虫", // 使用农事名称作为标题
-            interactTimeVal: item.interactTime || "", // 如果有已保存的互动时间
-            forceTriggerTimeVal: item.forceTriggerTime || "", // 如果有已保存的强制触发时间
-            interactQuestionVal: item.interactQuestion, // 如果有已保存的互动问题
-        });
+        interactPopupRef.value.showPopup({...item, containerSpaceTimeId: route.query.containerSpaceTimeId,id:route.query.id});
     }
 };
 </script>
@@ -1283,7 +1239,7 @@ const handleEditInteract = (item) => {
             .new-prescription + .new-prescription {
                 border-top: 1px solid rgba(225, 225, 225, 0.8);
             }
-            .new-prescription {
+            // .new-prescription {
                 .new-table {
                     display: flex;
                     align-items: center;
@@ -1330,10 +1286,10 @@ const handleEditInteract = (item) => {
                     text-align: left;
                     font-size: 11px;
                 }
-            }
-            .new-prescription + .new-prescription {
-                padding-top: 8px;
-            }
+            // }
+            // .new-prescription + .new-prescription {
+            //     padding-top: 8px;
+            // }
         }
     }
 

+ 250 - 39
src/views/old_mini/monitor/subPages/plan.vue

@@ -8,7 +8,11 @@
                 </el-select>
                 <tab-list type="light" v-model="active" :tabs="tabs" @change="handleTabChange" />
             </div>
-            <div class="timeline-container" ref="timelineContainerRef" :class="{ 'timeline-container-plant': pageType === 'plant' }">
+            <div
+                class="timeline-container"
+                ref="timelineContainerRef"
+                :class="{ 'timeline-container-plant': pageType === 'plant' }"
+            >
                 <div class="timeline-list" :style="getListStyle">
                     <div class="timeline-middle-line"></div>
                     <!-- 物候期覆盖条(progress 为起点,progress2 为终点,单位 %) -->
@@ -52,7 +56,7 @@
                                             <div class="header-right">托管农事</div>
                                         </div>
                                         <div class="card-content">
-                                            <span>温馨提示:在某某物候期之后,请密切关注荔枝,关注蒂蛀虫的出现!</span>
+                                            <span>{{ fw.warmReminder || "暂无提示" }}</span>
                                             <span class="edit-link" @click.stop="handleEdit(fw)">点击编辑</span>
                                         </div>
                                         <div
@@ -85,8 +89,13 @@
                 </div>
             </div>
         </div>
-        <div class="custom-bottom-fixed-btns" :class="{ 'center': pageType !== 'plant' }">
-            <div class="bottom-btn secondary-btn" v-if="pageType === 'plant'">{{ active === 1 ? '复制为新方案' : '编辑方案' }}</div>
+        <div class="custom-bottom-fixed-btns">
+            <div class="bottom-btn-group">
+                <div class="bottom-btn secondary-btn" @click="handlePhenologySetting">物候期设置</div>
+                <div class="bottom-btn secondary-btn" v-if="pageType === 'plant'" @click="openCopyPlanPopup">
+                    {{ active === 1 ? "复制方案" : "方案设置" }}
+                </div>
+            </div>
             <div class="bottom-btn primary-btn" @click="addNewTask">新增农事</div>
         </div>
     </div>
@@ -98,10 +107,57 @@
         @handleSaveSuccess="getFarmWorkPlan"
         @handleDeleteInteract="handleDeleteInteract"
     ></interact-popup>
+    <!-- 复制方案弹窗 -->
+    <Popup v-model:show="showCopyPlan" class="copy-plan-popup" round closeable :close-on-click-overlay="false">
+        <div class="copy-plan-content">
+            <div class="label">{{ active === 1 ? "复制为" : "方案名称" }}</div>
+            <el-input v-model="copyPlanName" size="large" placeholder="请输入方案名称" class="copy-plan-input" />
+        </div>
+        <div class="copy-plan-footer">
+            <div class="btn btn-cancel" @click="handleCancelCopyPlan">{{ active === 1 ? "取消复制" : "删除方案" }}</div>
+            <div class="btn btn-confirm" @click="handleConfirmCopyPlan">
+                {{ active === 1 ? "确定复制" : "确定设置" }}
+            </div>
+        </div>
+    </Popup>
+    <!-- 物候期设置弹窗 -->
+    <Popup
+        v-model:show="showPhenologySetting"
+        class="copy-plan-popup phenology-popup"
+        round
+        closeable
+        :close-on-click-overlay="false"
+    >
+        <div class="phenology-header">物候期时间设置</div>
+        <div class="phenology-list">
+            <div
+                class="phenology-item"
+                v-for="(item, index) in mergedReproductiveList"
+                :key="item.id || index"
+            >
+                <div class="item-label">
+                    <span class="label-text">{{ item.name }}</span>
+                    <span>起始时间</span>
+                </div>
+                <div class="item-value">
+                    <el-date-picker
+                        style="width: 100%"
+                        size="large"
+                        value-format="YYYY-MM-DD"
+                        v-model="item.startDate"
+                        type="date"
+                        placeholder="选择日期"
+                    />
+                </div>
+            </div>
+        </div>
+        <div class="phenology-footer" @click="handleConfirmPhenologySetting">确认设置</div>
+    </Popup>
 </template>
 
 <script setup>
 import { ref, onMounted, computed, nextTick } from "vue";
+import { Popup } from "vant";
 import customHeader from "@/components/customHeader.vue";
 import tabList from "@/components/pageComponents/TabList.vue";
 import { useRouter, useRoute } from "vue-router";
@@ -167,6 +223,63 @@ onMounted(() => {
     getFarmWorkPlan();
 });
 
+const mergedReproductiveList = ref([])
+const getPhenologyList = async (containerSpaceTimeId) => {
+    const res = await VE_API.monitor.listPhenology({ containerSpaceTimeId ,farmId: route.query.farmId });
+    if (res.code === 0) {
+        mergedReproductiveList.value = res.data || [];
+    }
+};
+
+// 复制方案弹窗相关
+const showCopyPlan = ref(false);
+const showPhenologySetting = ref(false);
+const copyPlanName = ref("");
+const openCopyPlanPopup = () => {
+    copyPlanName.value = "";
+    showCopyPlan.value = true;
+};
+
+// 物候期设置弹窗
+const handlePhenologySetting = () => {
+    showPhenologySetting.value = true;
+};
+
+/**
+ * 确认物候期设置
+ */
+const handleConfirmPhenologySetting = async () => {
+    console.log(mergedReproductiveList.value);
+    const params = {
+        farmId: route.query.farmId,
+        items: mergedReproductiveList.value.map(item => ({
+            phenologyId: item.id,
+            startDate: item.startDate,
+        })),
+    };
+    const res = await VE_API.monitor.batchSaveFarmPhenologyTime(params);
+    if (res.code === 0) {
+        ElMessage.success("设置成功");
+        showPhenologySetting.value = false;
+        getFarmWorkPlan();
+    }
+};
+// 取消复制方案
+const handleCancelCopyPlan = () => {
+    showCopyPlan.value = false;
+};
+
+// 确定复制方案
+const handleConfirmCopyPlan = () => {
+    if (!copyPlanName.value.trim()) {
+        ElMessage.warning("请输入方案名称");
+        return;
+    }
+    // TODO: 在此处调用复制方案的接口
+    ElMessage.success("复制成功");
+    showCopyPlan.value = false;
+};
+
 const getFarmWorkPlan = () => {
     // 如果不是首次加载,保存当前滚动位置
     let savedScrollTop = 0;
@@ -179,6 +292,7 @@ const getFarmWorkPlan = () => {
         .then(({ data, code }) => {
             if (code === 0) {
                 const list = Array.isArray(data?.solarTermsList) ? data.solarTermsList : [];
+                getPhenologyList(data.phenologyList[0]?.containerSpaceTimeId)
                 const filtered = list
                     .filter((t) => t && t.type === 1)
                     .map((t) => ({
@@ -194,16 +308,33 @@ const getFarmWorkPlan = () => {
                 solarTerms.value = filtered;
                 // 物候期数据
                 phenologyList.value = Array.isArray(data?.phenologyList)
-                    ? data.phenologyList.map((it) => ({
-                          id: it.id ?? it.phenologyId ?? it.name ?? `${it.progress}-${it.progress2}`,
-                          progress: Number(it.progress) || 0, // 起点 %
-                          progress2: Number(it.progress2) || 0, // 终点 %
-                          // 兼容多种可能的开始时间字段
-                          startTimeMs: safeParseDate(
-                              it.startDate || it.beginDate || it.startTime || it.start || it.start_at
-                          ),
-                          reproductiveList: Array.isArray(it.reproductiveList) ? it.reproductiveList : [],
-                      }))
+                    ? data.phenologyList.map((it) => {
+                          const reproductiveList = Array.isArray(it.reproductiveList)
+                              ? it.reproductiveList.map((r) => {
+                                    const farmWorkArrangeList = Array.isArray(r.farmWorkArrangeList)
+                                        ? r.farmWorkArrangeList.map((fw) => ({
+                                              ...fw,
+                                              containerSpaceTimeId: it.containerSpaceTimeId,
+                                          }))
+                                        : [];
+                                    return {
+                                        ...r,
+                                        farmWorkArrangeList,
+                                    };
+                                })
+                              : [];
+
+                          return {
+                              id: it.id ?? it.phenologyId ?? it.name ?? `${it.progress}-${it.progress2}`,
+                              progress: Number(it.progress) || 0, // 起点 %
+                              progress2: Number(it.progress2) || 0, // 终点 %
+                              // 兼容多种可能的开始时间字段
+                              startTimeMs: safeParseDate(
+                                  it.startDate || it.beginDate || it.startTime || it.start || it.start_at
+                              ),
+                              reproductiveList,
+                          };
+                      })
                     : [];
 
                 // 等待 DOM 更新后处理滚动
@@ -246,21 +377,16 @@ const handleRowClick = (item) => {
     curFarmObj.value = item;
     router.push({
         path: "/modify",
-        query: { id: 277983 },
+        query: { id: item.id, farmWorkId: item.farmWorkId, containerSpaceTimeId: item.containerSpaceTimeId },
     });
 };
 
 const interactPopupRef = ref(null);
 
 const handleEdit = (item) => {
+    console.log(item);
     if (interactPopupRef.value) {
-        interactPopupRef.value.showPopup({
-            arrangeIdVal: item.id,
-            interactTitleVal: item.farmWorkName || "梢期杀虫", // 使用农事名称作为标题
-            interactTimeVal: item.interactTime || "", // 如果有已保存的互动时间
-            forceTriggerTimeVal: item.forceTriggerTime || "", // 如果有已保存的强制触发时间
-            interactQuestionVal: item.interactQuestion, // 如果有已保存的互动问题
-        });
+        interactPopupRef.value.showPopup(item);
     }
 };
 
@@ -268,7 +394,6 @@ const handleDeleteInteract = (params) => {
     getFarmWorkPlan();
 };
 
-const detailDialogRef = ref(null);
 const timelineContainerRef = ref(null);
 // 标记是否为首次加载
 const isInitialLoad = ref(true);
@@ -306,7 +431,7 @@ const getListStyle = computed(() => {
     const minP = minProgress.value;
     const maxP = maxProgress.value;
     const range = Math.max(1, maxP - minP); // 避免除0
-    const total = (solarTerms.value?.length || 0) * 450;
+    const total = (solarTerms.value?.length || 0) * 1200;
     const minH = total; // 无上下留白
     return { minHeight: `${minH}px` };
 });
@@ -316,7 +441,7 @@ const getTermStyle = (t) => {
     const minP = minProgress.value;
     const maxP = maxProgress.value;
     const range = Math.max(1, maxP - minP); // 避免除0
-    const total = (solarTerms.value?.length || 0) * 450;
+    const total = (solarTerms.value?.length || 0) * 1200;
     // 将progress映射到0开始的位置,最小progress对应top: 0
     const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
     const top = (normalizedP / 100) * total;
@@ -347,7 +472,7 @@ const handleSeasonClick = (seasonValue) => {
     const minP = minProgress.value;
     const maxP = maxProgress.value;
     const range = Math.max(1, maxP - minP);
-    const total = (solarTerms.value?.length || 0) * 450;
+    const total = (solarTerms.value?.length || 0) * 1200;
     const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
     const targetTop = (normalizedP / 100) * total; // 内容内的像素位置
     const wrap = timelineContainerRef.value;
@@ -369,7 +494,7 @@ const getPhenologyBarStyle = (item) => {
     const minP = minProgress.value;
     const maxP = maxProgress.value;
     const range = Math.max(1, maxP - minP);
-    const total = (solarTerms.value?.length || 0) * 450; // 有效绘制区高度(px)
+    const total = (solarTerms.value?.length || 0) * 1200; // 有效绘制区高度(px)
     // 将progress映射到0开始的位置
     const normalizedStart = range > 0 ? ((start - minP) / range) * 100 : 0;
     const normalizedEnd = range > 0 ? ((end - minP) / range) * 100 : 0;
@@ -435,7 +560,7 @@ const getPhenologyBarHeight = (item) => {
     const minP = minProgress.value;
     const maxP = maxProgress.value;
     const range = Math.max(1, maxP - minP);
-    const total = (solarTerms.value?.length || 0) * 450;
+    const total = (solarTerms.value?.length || 0) * 1200;
     // 将progress映射到0开始的位置
     const normalizedStart = range > 0 ? ((start - minP) / range) * 100 : 0;
     const normalizedEnd = range > 0 ? ((end - minP) / range) * 100 : 0;
@@ -466,7 +591,7 @@ const getReproductiveItemHeight = (phenologyItem) => {
             gap: 12px;
             margin-bottom: 10px;
             margin-left: 12px;
-            .select-item{
+            .select-item {
                 width: 82px;
                 ::v-deep {
                     .el-select__wrapper {
@@ -477,14 +602,14 @@ const getReproductiveItemHeight = (phenologyItem) => {
                 }
             }
         }
-        
+
         .timeline-container {
             height: calc(100vh - 40px - 73px);
             overflow: auto;
             position: relative;
             box-sizing: border-box;
             padding: 0 12px;
-            &.timeline-container-plant{
+            &.timeline-container-plant {
                 height: calc(100vh - 40px - 73px - 38px);
             }
             .timeline-list {
@@ -538,7 +663,7 @@ const getReproductiveItemHeight = (phenologyItem) => {
                     }
                     .arranges {
                         position: absolute;
-                        left: 48px; /* 列与中线右侧一段距离 */
+                        left: 40px; /* 列与中线右侧一段距离 */
                         top: 0;
                         z-index: 3;
                         display: flex;
@@ -546,7 +671,7 @@ const getReproductiveItemHeight = (phenologyItem) => {
                         gap: 12px;
                         letter-spacing: 0px;
                         .arrange-card {
-                            width: 94%;
+                            width: 97%;
                             border: 0.5px solid #2199f8;
                             border-radius: 8px;
                             background: #fff;
@@ -590,6 +715,7 @@ const getReproductiveItemHeight = (phenologyItem) => {
                                 margin: 4px 0 2px 0;
                                 .edit-link {
                                     color: #2199f8;
+                                    margin-left: 5px;
                                 }
                             }
                             .status-icon {
@@ -663,16 +789,101 @@ const getReproductiveItemHeight = (phenologyItem) => {
     }
     // 控制区域样式
     .custom-bottom-fixed-btns {
-        .bottom-btn {
-            width: 124px;
+        .bottom-btn-group {
+            display: flex;
+            gap: 12px;
+        }
+    }
+}
+.copy-plan-popup {
+    width: 100%;
+    padding: 50px 12px 20px 12px;
+    &::before {
+        content: "";
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 136px;
+        background: url("@/assets/img/monitor/popup-header-bg.png") no-repeat center center / 100% 100%;
+    }
+    .copy-plan-content {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        .label {
+            font-size: 16px;
+            font-weight: 500;
+        }
+        .copy-plan-input {
+            width: calc(100% - 80px);
+        }
+    }
+    .copy-plan-footer {
+        display: flex;
+        gap: 12px;
+        margin-top: 20px;
+        .btn {
+            flex: 1;
+            color: #666666;
+            border: 1px solid #999999;
+            border-radius: 25px;
             padding: 10px 0;
+            font-size: 16px;
+            text-align: center;
+            &.btn-confirm {
+                color: #fff;
+                border: 1px solid #2199f8;
+                background: #2199f8;
+            }
         }
-        &.center{
-            justify-content: center;
-            .bottom-btn {
-                padding: 10px 50px;
+    }
+}
+
+.phenology-popup {
+    padding: 28px 20px 20px;
+    .phenology-header {
+        font-size: 24px;
+        text-align: center;
+        margin-bottom: 20px;
+        font-family: "PangMenZhengDao";
+    }
+    .phenology-list {
+        width: 100%;
+        .phenology-item {
+            width: 100%;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            .item-label {
+                display: flex;
+                align-items: center;
+                gap: 4px;
+                font-size: 15px;
+                color: rgba(0, 0, 0, 0.4);
+                .label-text {
+                    color: #000;
+                    font-size: 16px;
+                    font-weight: 500;
+                }
             }
+            .item-value {
+                width: calc(100% - 156px);
+            }
+        }
+        .phenology-item + .phenology-item {
+            margin-top: 10px;
         }
     }
+    .phenology-footer{
+        width: 100%;
+        text-align: center;
+        font-size: 16px;
+        margin-top: 20px;
+        color: #fff;
+        background: #2199f8;
+        border-radius: 25px;
+        padding: 10px 0;
+    }
 }
 </style>

+ 54 - 115
src/views/old_mini/plan/index.vue

@@ -1,67 +1,27 @@
 <template>
     <div class="farm-card-page">
         <custom-header name="种植方案"></custom-header>
-        <!-- <Tabs v-model:active="activeTab" class="tabs-wrap" v-if="!route.query.containerId">
-            <Tab title="专家方案">
-                <expert-list :isShowHeader="true"></expert-list>
-            </Tab>
-            <Tab title="我的方案">
-                <div class="farm-card-content">
-                    <tab-list
-                        v-if="curRole == 2"
-                        type="light"
-                        v-model="active"
-                        :tabs="tabs"
-                        @change="handleTabChange"
-                        class="tabs-list"
-                    />
-                    <plan-list :schemeId="active" :farmId="route.query.farmId" :containerId="containerId" :isEdit="isEditVal"> </plan-list>
-                </div>
-            </Tab>
-        </Tabs>
-        <div v-else class="system-generated">
+        <div class="system-generated">
             <div class="tip-box">
                 <el-icon size="18"><CircleCheckFilled /></el-icon>
                 <span>系统已生成多套方案,请选择最佳方案</span>
             </div>
             <tab-list
-                v-if="curRole == 2"
                 type="light"
                 v-model="active"
                 :tabs="tabs"
                 @change="handleTabChange"
                 class="tabs-list"
             />
-            <plan-list :schemeId="active" :containerId="containerId" :isEdit="isEditVal"> </plan-list>
-        </div> -->
-        <div class="farm-card-content" v-if="!route.query.containerId">
-            <tab-list
-                v-if="curRole == 2"
-                type="light"
-                v-model="active"
-                :tabs="tabs"
-                @change="handleTabChange"
-                class="tabs-list"
-            />
-            <plan-list :schemeId="active" :farmId="route.query.farmId" :containerId="containerId" :isEdit="true"> </plan-list>
-        </div>
-        <div v-else class="system-generated">
-            <div class="tip-box">
-                <el-icon size="18"><CircleCheckFilled /></el-icon>
-                <span>系统已生成多套方案,请选择最佳方案</span>
-            </div>
-            <tab-list
-                v-if="curRole == 2"
-                type="light"
-                v-model="active"
-                :tabs="tabs"
-                @change="handleTabChange"
-                class="tabs-list"
+            <farm-work-plan-timeline
+                class="timeline-wrap"
+                pageType="plant"
+                :containerId="containerId"
+                :disableClick="true"
             />
-            <plan-list :schemeId="active" :containerId="containerId" :isEdit="false"> </plan-list>
         </div>
     </div>
-    <div v-if="route.query.containerId" class="custom-bottom-fixed-btns">
+    <div class="custom-bottom-fixed-btns">
         <div class="bottom-btn primary-btn" @click="handleConfirmPlan">确认方案</div>
     </div>
 
@@ -77,22 +37,18 @@
 
 <script setup>
 import customHeader from "@/components/customHeader.vue";
-import { onActivated, ref,onDeactivated } from "vue";
+import { onActivated, ref, onDeactivated } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { Tab, Tabs } from "vant";
+import FarmWorkPlanTimeline from "@/components/pageComponents/FarmWorkPlanTimeline.vue";
 import wx from "weixin-js-sdk";
-import expertList from "@/views/old_mini/home/subPages/expertList.vue";
 import tabList from "@/components/pageComponents/TabList.vue";
-import PlanList from "@/components/pageComponents/PlanList.vue";
 import tipPopup from "@/components/popup/tipPopup.vue";
 import { ElMessage } from "element-plus";
 
-const activeTab = ref(1);
 const router = useRouter();
 const route = useRoute();
 
-const curRole = localStorage.getItem("SET_USER_CUR_ROLE");
-
 const tabs = ref([]);
 const active = ref(null);
 const containerId = ref(null);
@@ -102,9 +58,8 @@ const handleTabChange = (id, item) => {
 };
 
 onActivated(() => {
-    if (route.query.containerId || curRole == 2) {
-        getListMySchemes();
-    }
+    containerId.value = route.query.containerId;
+    getListMySchemes();
 });
 
 onDeactivated(() => {
@@ -113,27 +68,12 @@ onDeactivated(() => {
 });
 
 const getListMySchemes = () => {
-    VE_API.home.listMySchemes().then(({ data }) => {
+    VE_API.home.listMySchemes({containerId: route.query.containerId}).then(({ data }) => {
         if (data.length) {
             tabs.value = data || [];
-            if (route.query.containerId) {
-                const index = data.findIndex((item) => item.containerId == 9);
-                active.value = data[index].id;
-                containerId.value = 9;
-            } else {
-                active.value = data[0].id;
-                containerId.value = data[0].containerId;
-            }
-        } else {
-            getSchemes();
-        }
-    });
-};
-
-const getSchemes = () => {
-    VE_API.home.batchInitSchemes({ containerIds: [3], schemeName: "农资荔枝方案" }).then(({ code }) => {
-        if (code == 0) {
-            getListMySchemes();
+            const index = data.findIndex((item) => item.containerId == route.query.containerId);
+            active.value = data[index].id;
+            containerId.value = route.query.containerId;
         }
     });
 };
@@ -143,9 +83,9 @@ const shareData = ref({});
 const handleConfirmPlan = () => {
     // 从路由参数中获取农场数据
     let geomValue = route.query.geom;
-    
+
     // 处理 geom 参数,可能是 JSON 字符串或数组
-    if (typeof geomValue === 'string') {
+    if (typeof geomValue === "string") {
         try {
             // 尝试解析 JSON 字符串
             const parsed = JSON.parse(geomValue);
@@ -154,10 +94,10 @@ const handleConfirmPlan = () => {
             }
         } catch (e) {
             // 如果不是 JSON 字符串,保持原值
-            console.warn('geom 参数解析失败,使用原值:', e);
+            console.warn("geom 参数解析失败,使用原值:", e);
         }
     }
-    
+
     const farmParams = {
         ...route.query,
         containerId: containerId.value,
@@ -165,34 +105,38 @@ const handleConfirmPlan = () => {
         defaultFarm: Boolean(route.query.defaultFarm),
         agriculturalCreate: route.query.agriculturalCreate * 1,
     };
-    
+
     // 验证必填字段
-    if (!farmParams.wkt || !farmParams.speciesId || !farmParams.containerId || !farmParams.address || !farmParams.mu || !farmParams.name || !farmParams.fzr || !farmParams.tel) {
-        ElMessage.error('农场信息不完整,请返回重新填写');
+    if (
+        !farmParams.wkt ||
+        !farmParams.speciesId ||
+        !farmParams.containerId ||
+        !farmParams.address ||
+        !farmParams.mu ||
+        !farmParams.name ||
+        !farmParams.fzr ||
+        !farmParams.tel
+    ) {
+        ElMessage.error("农场信息不完整,请返回重新填写");
         return;
     }
-    
+
     delete farmParams.from;
     // 调用创建农场接口
-    VE_API.farm.saveFarm(farmParams).then((res) => {
-        if (res.code === 0) {
-            shareData.value = res.data;
-            //选择方案
-            VE_API.home.selectSchemes({ schemeId: active.value ,farmId:res.data.id}).then(({code}) => {
-                if (code === 0) {
-                    // showFarmPopup.value = true;
-                    handleClickOverlay();
-                } else {
-                    ElMessage.error(res.msg || '创建失败');
-                }
-            });
-        } else {
-            ElMessage.error(res.msg || '创建失败');
-        }
-    }).catch((err) => {
-        console.error('创建农场失败:', err);
-        ElMessage.error('创建失败,请稍后重试');
-    });
+    VE_API.farm
+        .saveFarm(farmParams)
+        .then((res) => {
+            if (res.code === 0) {
+                shareData.value = res.data;
+                handleClickOverlay();
+            } else {
+                ElMessage.error(res.msg || "创建失败");
+            }
+        })
+        .catch((err) => {
+            console.error("创建农场失败:", err);
+            ElMessage.error("创建失败,请稍后重试");
+        });
 };
 
 const handleShareFarm = () => {
@@ -214,7 +158,7 @@ const handleClickOverlay = () => {
         router.replace(`/${fromPage}`);
     } else {
         // 如果没有 from 参数,默认跳转到首页
-        router.replace('/home');
+        router.replace("/home");
     }
 };
 </script>
@@ -224,7 +168,7 @@ const handleClickOverlay = () => {
     width: 100%;
     height: 100vh;
     background: #f5f7fb;
-    .system-generated{
+    .system-generated {
         padding: 17px 10px 10px;
         height: calc(100vh - 150px);
         .tip-box {
@@ -241,9 +185,12 @@ const handleClickOverlay = () => {
                 margin-left: 5px;
             }
         }
-        .tabs-list{
+        .tabs-list {
             margin-bottom: 10px;
         }
+        .timeline-wrap{
+            height: calc(100vh - 150px - 50px);
+        }
     }
     .tabs-wrap {
         ::v-deep {
@@ -260,18 +207,10 @@ const handleClickOverlay = () => {
             }
         }
     }
-    .farm-card-content {
-        width: 100%;
-        height: calc(100vh - 40px);
-        padding-top: 10px;
-        .tabs-list {
-            margin: 0 0 10px 10px;
-        }
-    }
 }
-.custom-bottom-fixed-btns{
+.custom-bottom-fixed-btns {
     justify-content: center;
-    .bottom-btn{
+    .bottom-btn {
         padding: 10px 40px;
     }
 }