Explorar o código

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

lxf hai 2 días
pai
achega
240dda7340

+ 8 - 0
src/api/modules/home.js

@@ -26,4 +26,12 @@ module.exports = {
         url: config.base_dev_url + "pest_disease_dictionary/listByIds",
         type: "post",
     },
+    fetchQuestPopup: {
+        url: config.base_dev_url + "work_code/phenology/quest/popup",
+        type: "get",
+    },
+    saveQuestPopup: {
+        url: config.base_dev_url + "work_code/phenology/quest/save",
+        type: "post",
+    },
 }

+ 116 - 32
src/views/old_mini/home/components/problemReminder.vue

@@ -10,8 +10,10 @@
                 <img class="header-icon" src="@/assets/img/home/file-icon.png" alt="" />
             </div>
             <div class="question-section-wrapper">
-                <span class="question-text">在白点期,您当前农场是否出现白点?</span>
-                <div class="img"></div>
+                <span class="question-text">{{ questPopupData.quest }}</span>
+                <div class="img">
+                    <img :src="questPopupData.image" alt="" />
+                </div>
                 <div class="options-section">
                     <span class="options-label">您可以选择</span>
                     <div class="options-buttons">
@@ -22,8 +24,15 @@
             </div>
             <!-- 底部按钮区域 -->
             <div class="bottom-buttons">
-                <div class="bottom-btn no-btn" @click="noClick">否</div>
-                <div class="bottom-btn yes-btn" @click="yesClick">是</div>
+                <div
+                    v-for="(opt, idx) in bottomAnswerOptions"
+                    :key="idx"
+                    class="bottom-btn"
+                    :class="{ 'yes-btn': isYesOption(opt), 'no-btn': !isYesOption(opt) }"
+                    @click="onBottomOptionClick(opt)"
+                >
+                    {{ getOptionLabel(opt) }}
+                </div>
             </div>
         </div>
     </Popup>
@@ -34,45 +43,121 @@
         </div>
         <div class="no-popup-btn" @click="noShow = false">我知道了</div>
     </Popup>
-        <!-- 农事信息弹窗 -->
+    <!-- 农事信息弹窗 -->
     <detail-dialog ref="detailDialogRef"></detail-dialog>
 </template>
 <script setup>
 import { Popup } from "vant";
-import { ref } from "vue";
+import { ref, onActivated } from "vue";
 import wx from "weixin-js-sdk";
-import detailDialog from "@/components/detailDialog.vue"
+import detailDialog from "@/components/detailDialog.vue";
 import { useRouter } from "vue-router";
 const router = useRouter();
 
 const show = ref(false);
 const noShow = ref(false);
-const noClick = () => {
-    show.value = false;
-    noShow.value = true;
-}
-const yesClick = () => {
-    show.value = false;
-    detailDialogRef.value.showDialog()
-}
 const dropdownGardenItem = ref({
-    organId:766,
-    periodId:1,
-    wktVal:'wktVal',
-    address:'address',
-    district:'district',
-    name:'荔博园',
+    organId: 766,
+    periodId: 1,
+    wktVal: "wktVal",
+    address: "address",
+    district: "district",
+    name: "荔博园",
 });
 const toUpload = () => {
     wx.miniProgram.navigateTo({
         url: `/pages/subPages/carmera/index?gardenData=${JSON.stringify(dropdownGardenItem.value)}`,
     });
-}
+};
 
 function toPage() {
-    router.push("/expert_list")
+    router.push("/expert_list");
+}
+const detailDialogRef = ref(null);
+
+onActivated(() => {
+    fetchQuestPopup();
+});
+
+const questPopupData = ref({});
+const bottomAnswerOptions = ref([]);
+const fetchQuestPopup = () => {
+    VE_API.home
+        .fetchQuestPopup({ farmId: 766 })
+        .then(({ data }) => {
+            if (Array.isArray(data) && data.length > 0) {
+                show.value = true;
+                questPopupData.value = data[0];
+                bottomAnswerOptions.value = transformAnswerOptions(questPopupData.value?.answerOptions);
+            }
+        })
+        .catch(() => {});
+};
+
+function transformAnswerOptions(raw) {
+    let parsed = raw;
+    if (typeof raw === "string") {
+        try {
+            parsed = JSON.parse(raw);
+        } catch (e) {
+            parsed = [raw];
+        }
+    }
+    if (Array.isArray(parsed)) {
+        return parsed.map((item) => {
+            if (item && typeof item === "object") {
+                const label = item.name;
+                const value = item.value;
+                return { label, value };
+            }
+            return { label: String(item), value: item };
+        });
+    }
+    return [];
+}
+
+function getOptionLabel(opt) {
+    return opt?.label ?? String(opt ?? "");
+}
+
+function isYesOption(opt) {
+    const label = (opt?.label ?? "").toString();
+    const value = opt?.value;
+    if (typeof value === "boolean") return value === true;
+    if (typeof value === "number") return value === 1;
+    if (typeof value === "string") {
+        const v = value.trim().toLowerCase();
+        if (v === "1" || v === "true" || v === "yes") return true;
+    }
+    const yesKeywords = ["是", "yes", "确认", "同意", "有", "发生"];
+    return yesKeywords.some((k) => label.toLowerCase().includes(k.toLowerCase()));
+}
+
+function onBottomOptionClick(opt) {
+    const agriDate = getTodayStr();
+    VE_API.home
+        .saveQuestPopup({
+            farmId: 766,
+            phenologyId: questPopupData.value.phenologyId,
+            indicatorId: questPopupData.value.indicatorId,
+            answerValue: opt.value,
+            agriDate: agriDate,
+        })
+        .then((res) => {
+            if (res.code === 0) {
+                show.value = false;
+                noShow.value = true;
+            }
+        });
+}
+
+function getTodayStr() {
+    const d = new Date();
+    const y = d.getFullYear();
+    const m = String(d.getMonth() + 1).padStart(2, '0');
+    const day = String(d.getDate()).padStart(2, '0');
+    return `${y}-${m}-${day}`;
 }
-const detailDialogRef = ref(null)
 </script>
 <style lang="scss" scoped>
 .problem-reminder-popup {
@@ -119,7 +204,7 @@ const detailDialogRef = ref(null)
                 color: #252525;
                 font-weight: 500;
             }
-            .img{
+            .img {
                 margin: 12px 0;
                 width: 100%;
                 height: 140px;
@@ -136,9 +221,9 @@ const detailDialogRef = ref(null)
                 font-weight: 500;
                 .options-label {
                     font-size: 14px;
-                    color: #2199F8;
+                    color: #2199f8;
                 }
-            
+
                 .options-buttons {
                     display: flex;
                     flex-wrap: wrap;
@@ -151,7 +236,6 @@ const detailDialogRef = ref(null)
                         flex: 1;
                     }
                 }
-            
             }
         }
         // 底部按钮区域样式
@@ -159,7 +243,7 @@ const detailDialogRef = ref(null)
             display: flex;
             gap: 12px;
             margin-top: 10px;
-        
+
             .bottom-btn {
                 flex: 1;
                 padding: 8px 0;
@@ -169,7 +253,7 @@ const detailDialogRef = ref(null)
                 text-align: center;
                 background: #fff;
                 border: 1px solid #e5e5e5;
-        
+
                 &.yes-btn {
                     background-image: linear-gradient(180deg, #76c3ff 0%, #2199f8 100%);
                     color: #fff;
@@ -189,13 +273,13 @@ const detailDialogRef = ref(null)
         font-size: 24px;
         font-weight: 500;
         text-align: center;
-        .no-popup-title-sub{
+        .no-popup-title-sub {
             font-size: 16px;
             margin-top: 8px;
         }
     }
     .no-popup-btn {
-        background-color: #2199F8;
+        background-color: #2199f8;
         padding: 8px;
         border-radius: 20px;
         color: #fff;

+ 1007 - 0
src/views/old_mini/monitor/subPages/plan copy.vue

@@ -0,0 +1,1007 @@
+<template>
+    <div class="plan-page">
+        <custom-header name="农事规划"></custom-header>
+        <div class="plan-content">
+            <div class="filter-wrap">
+                <div class="season-tabs">
+                    <div
+                        v-for="s in seasons"
+                        :key="s.value"
+                        class="season-tab"
+                        :class="{ active: s.value === activeSeason }"
+                        @click="activeSeason = s.value"
+                    >
+                        {{ s.label }}
+                    </div>
+                </div>
+                <div class="status-filter">
+                    <div v-for="status in statusList" :key="status.value" class="status-item" :class="status.color">
+                        <div class="status-dot"></div>
+                        <span class="status-text">{{ status.label }}</span>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 三行循环时间线 -->
+            <div class="cycle-timeline-container">
+                <div class="cycle-timeline">
+                    <div
+                        v-for="(row, rowIndex) in timelineRows"
+                        :key="rowIndex"
+                        class="cycle-row"
+                        :class="{ 'odd-index': rowIndex % 2 === 1 }"
+                    >
+                        <div
+                            v-for="(item, itemIndex) in row.items"
+                            :key="itemIndex"
+                            class="cycle-item"
+                            @click="handleRowClick(item)"
+                            :class="[item.type + '-item']"
+                        >
+                            <!-- 节气节点 -->
+                            <template v-if="item.type === 'term'">
+                                <!-- <div class="cycle-term-dot"></div> -->
+                                <div class="cycle-term-label">{{ item.name || item.id }}</div>
+                            </template>
+                        </div>
+
+                        <!-- 生育期名称(根据时间范围显示在对应行) -->
+                        <div class="cycle-phenology-wrap" v-if="getPhenologyBarsForRow(rowIndex).length > 0">
+                            <div
+                                v-for="p in getPhenologyBarsForRow(rowIndex)"
+                                :key="p.id"
+                                class="cycle-label"
+                                :class="p.color"
+                                :style="
+                                    isOddVisualRow(rowIndex)
+                                        ? { right: p.left, width: p.width }
+                                        : { left: p.left, width: p.width }
+                                "
+                            >
+                                {{ p.name }}
+                                <div v-if="p.arranges && p.arranges.length" class="arranges">
+                                    <div v-for="a in p.arranges" :key="a.id" :class="['cycle-task-box', a.status]">
+                                        <div class="cycle-task-text">{{ a.farmWorkName || a.name }}</div>
+                                        <!-- 任务连接器 -->
+                                        <div class="cycle-task-connector"></div>
+                                        <div
+                                            v-if="a.status === 'complete' || a.status === 'warning'"
+                                            class="status-icon"
+                                            :class="a.status"
+                                        >
+                                            <el-icon v-if="a.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
+                            v-if="rowIndex < timelineRows.length - 1"
+                            class="cycle-connector"
+                            :class="[
+                                rowIndex % 2 === 1 ? 'middle-connector' : 'top-connector',
+                                getConnectorColorClass(rowIndex),
+                            ]"
+                        >
+                            <img v-if="isConnectorGray(rowIndex)" src="@/assets/img/monitor/defalut-arrow.png" alt="" />
+                            <img v-else src="@/assets/img/monitor/arrow.png" alt="" />
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="control-section">
+                <div class="toggle-group">
+                    <el-switch v-model="isDefaultEnabled" />
+                    <span class="toggle-label">{{ isDefaultEnabled ? "默认" : "" }}发起农情需求</span>
+                </div>
+                <div class="add-button-group">
+                    <div class="add-button button" @click="addNewTask">新增农事</div>
+                    <div class="button" @click="manageTask">农事管理</div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <!-- 农事信息弹窗 -->
+    <detail-dialog ref="detailDialogRef"></detail-dialog>
+    <!-- 新增:激活上传弹窗 -->
+    <active-upload-popup></active-upload-popup>
+</template>
+
+<script setup>
+import { reactive, ref, onMounted, nextTick, onBeforeUnmount } from "vue";
+import customHeader from "@/components/customHeader.vue";
+import { useRouter, useRoute } from "vue-router";
+import detailDialog from "@/components/detailDialog.vue";
+import activeUploadPopup from "@/components/popup/activeUploadPopup.vue";
+const router = useRouter();
+const route = useRoute();
+
+// 状态列表数据
+const seasons = reactive([
+    { value: "spring", label: "春季" },
+    { value: "summer", label: "夏季" },
+    { value: "autumn", label: "秋季" },
+    { value: "winter", label: "冬季" },
+]);
+const activeSeason = ref("spring");
+
+const statusList = reactive([
+    { value: "pending", label: "待触发", color: "gray" },
+    { value: "executing", label: "待完成", color: "blue" },
+    { value: "completed", label: "已完成", color: "green" },
+    { value: "expired", label: "已过期", color: "orange" },
+]);
+
+// 切换开关状态
+const isDefaultEnabled = ref(true);
+
+// 时间线行数据(由接口节气生成)
+const timelineRows = reactive([]);
+
+// 目标定位日期(当前生育期参考点)
+const targetDate = new Date("2025-04-04T00:00:00");
+// 每行“当前生育期”标记的位置样式(按行索引)
+const phenologyPositions = ref({});
+
+// 生育期条(按行分组)
+const phenologyBarsByRow = ref([]);
+// 每一行可视区域的实际像素宽度(用于将最小像素宽度换算为百分比)
+const rowWidths = ref([]);
+// 节气 id 到对象的索引,便于通过 id 查找节气日期
+let solarTermIdToTerm = {};
+// 接口返回的生育期数据
+const phenologyList = ref([]);
+
+// 安全日期解析(兼容 'YYYY-MM-DD HH:mm:ss' / 'YYYY/MM/DD HH:mm:ss')
+const parseDate = (val) => {
+    if (!val) return null;
+    if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
+    if (typeof val === "number") return new Date(val);
+    if (typeof val === "string") {
+        // 统一到可被 Safari 解析的格式
+        const s = val.replace(/-/g, "/").replace("T", " ");
+        const d = new Date(s);
+        return isNaN(d.getTime()) ? null : d;
+    }
+    return null;
+};
+
+onMounted(() => {
+    getFarmWorkPlan();
+    window.addEventListener("resize", handleResize, { passive: true });
+});
+onBeforeUnmount(() => {
+    window.removeEventListener("resize", handleResize);
+});
+
+const handleResize = () => {
+    // 重新测量并基于最新宽度重算条目
+    nextTick(() => {
+        measureRowWidths();
+        // 需要基于最新数据重算
+        if (phenologyList.value && phenologyList.value.length && cachedValidSolarTerms.value) {
+            groupPhenologyBarsByRow(phenologyList.value, cachedValidSolarTerms.value);
+        }
+    });
+};
+
+// 缓存已过滤/排序后的节气用于重复计算
+const cachedValidSolarTerms = ref(null);
+
+const getFarmWorkPlan = () => {
+    const paramFarmId = Number(route.query.farmId) || undefined;
+    VE_API.monitor
+        .farmWorkPlan({ farmId: paramFarmId ?? 92844 }) // 优先使用路由传入的 farmId
+        .then(({ data, code }) => {
+            if (code === 0) {
+                const solarTermsList = data.solarTermsList;
+                // 仅保留 type === 1 的节气,按需要的顺序(示例:反转)
+                // 取 type===1 的节气,并按日期降序排序(晚到早)
+                const validSolarTerms = Array.isArray(solarTermsList)
+                    ? solarTermsList
+                          .filter((t) => t && t.type === 1 && t.createDate)
+                          .sort((a, b) => {
+                              const da = parseDate(a.createDate)?.getTime() ?? 0;
+                              const db = parseDate(b.createDate)?.getTime() ?? 0;
+                              return db - da;
+                          })
+                    : [];
+                cachedValidSolarTerms.value = validSolarTerms;
+                generateTimelineData(validSolarTerms);
+                computeCurrentPhenologyPositions(validSolarTerms, targetDate);
+                // 保存生育期数据并生成各行生育期条
+                phenologyList.value = Array.isArray(data.phenologyList) ? data.phenologyList : [];
+                // 生成 id->term 的索引
+                solarTermIdToTerm = {};
+                validSolarTerms.forEach((t) => {
+                    if (t && (t.id || t.solarTermsId)) solarTermIdToTerm[t.id ?? t.solarTermsId] = t;
+                });
+                // 先等待 DOM 渲染完成后测量每行宽度,再据此计算最小可显示宽度
+                nextTick(() => {
+                    measureRowWidths();
+                    groupPhenologyBarsByRow(phenologyList.value, validSolarTerms);
+                });
+            }
+        })
+        .catch((error) => {
+            console.error("获取农事规划数据失败:", error);
+        });
+};
+
+// 测量每一行生育期容器的实际宽度
+const measureRowWidths = () => {
+    const rows = document.querySelectorAll(".cycle-timeline .cycle-row");
+    const widths = [];
+    rows.forEach((rowEl, idx) => {
+        const wrap = rowEl.querySelector(".cycle-phenology-wrap");
+        widths[idx] = wrap ? wrap.offsetWidth : 0;
+    });
+    rowWidths.value = widths;
+};
+
+// 生成时间轴数据
+const generateTimelineData = (solarTerms) => {
+    // 清空
+    timelineRows.splice(0, timelineRows.length);
+
+    // 无数据则给一行示例
+    if (!solarTerms || solarTerms.length === 0) {
+        timelineRows.push({
+            items: [
+                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
+                { type: "term", name: "节气" },
+                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
+                { type: "term", name: "节气" },
+                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
+                { type: "term", name: "节气" },
+            ],
+        });
+        return;
+    }
+
+    const itemsPerRow = 6; // 任务/节气交替
+    const termsPerRow = 3; // 每行3个节气
+    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
+
+    for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
+        const rowItems = [];
+        const startTermIndex = rowIndex * termsPerRow;
+
+        for (let i = 0; i < itemsPerRow; i++) {
+            if (i % 2 === 0) {
+                // 任务位
+                const taskData = getTaskDataForIndex(Math.floor(i / 2));
+                rowItems.push({
+                    type: "task",
+                    status: taskData.status,
+                    taskName: taskData.taskName,
+                    taskDesc: taskData.taskDesc,
+                    icon: taskData.icon,
+                });
+            } else {
+                // 节气位
+                const termIndex = startTermIndex + Math.floor(i / 2);
+                if (termIndex < solarTerms.length) {
+                    const term = solarTerms[termIndex] || {};
+                    rowItems.push({
+                        type: "term",
+                        status: "default",
+                        name: term.name || term.solarTermsName || term.termName || "节气",
+                        id: term.id,
+                        createDate: term.createDate,
+                    });
+                } else {
+                    // 不足时补任务
+                    rowItems.push({
+                        type: "task",
+                        status: "default",
+                        taskName: "梢期",
+                        taskDesc: "杀虫",
+                    });
+                }
+            }
+        }
+
+        timelineRows.push({ items: rowItems });
+    }
+};
+
+// 任务占位数据(可按需接后端)
+const getTaskDataForIndex = (index) => {
+    const defaultTasks = [
+        { status: "default", taskName: "梢期", taskDesc: "杀虫" },
+        { status: "active", taskName: "梢期", taskDesc: "营养" },
+        { status: "complete", taskName: "梢期", taskDesc: "修剪", icon: { type: "complete" } },
+        { status: "warning", taskName: "梢期", taskDesc: "施肥", icon: { type: "warning" } },
+        { status: "normal", taskName: "梢期", taskDesc: "灌溉", icon: { type: "normal" } },
+    ];
+    return defaultTasks[index % defaultTasks.length];
+};
+
+// 计算“当前生育期”在各行的定位(只在包含目标日期的那一行显示)
+const computeCurrentPhenologyPositions = (solarTerms, date) => {
+    phenologyPositions.value = {};
+    if (!Array.isArray(solarTerms) || solarTerms.length === 0 || !(date instanceof Date)) return;
+
+    const termsPerRow = 3;
+    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
+
+    // 1) 找到最接近目标日期的节气(按时间升序)
+    const termsAsc = solarTerms
+        .filter((t) => t && t.createDate)
+        .slice()
+        .sort((a, b) => (parseDate(a.createDate)?.getTime() ?? 0) - (parseDate(b.createDate)?.getTime() ?? 0));
+    if (termsAsc.length === 0) return;
+
+    const targetMs = date.getTime();
+    let nearest = termsAsc[0];
+    let bestDiff = Math.abs((parseDate(nearest.createDate)?.getTime() ?? 0) - targetMs);
+    for (let i = 1; i < termsAsc.length; i++) {
+        const ms = parseDate(termsAsc[i].createDate)?.getTime() ?? 0;
+        const diff = Math.abs(ms - targetMs);
+        if (diff < bestDiff) {
+            bestDiff = diff;
+            nearest = termsAsc[i];
+        }
+    }
+
+    // 2) 将该节气映射回当前(降序)数组中的索引与行
+    const nearestIdxDesc = solarTerms.findIndex((t) => t && nearest && t.id === nearest.id);
+    const rowIndex = Math.max(0, Math.floor(nearestIdxDesc / termsPerRow));
+
+    const startIdx = rowIndex * termsPerRow;
+    const endIdx = Math.min(startIdx + termsPerRow - 1, solarTerms.length - 1);
+    if (startIdx > endIdx) return;
+
+    const rowTerms = solarTerms.slice(startIdx, endIdx + 1);
+    // 视觉顺序用于方向(偶数行正向,奇数行反向),但时间范围应取该行真实最早/最晚
+    const rowDates = rowTerms
+        .map((t) => parseDate(t?.createDate))
+        .filter((d) => d && !isNaN(d.getTime()))
+        .map((d) => d.getTime());
+    if (rowDates.length === 0) return;
+    const minMs = Math.min(...rowDates);
+    const maxMs = Math.max(...rowDates);
+    const rowStart = new Date(minMs);
+    const rowEnd = new Date(maxMs);
+
+    // 3) 若目标日期不在该行范围内,则就近夹到边界(避免跨行导致丢失)
+    let anchorMs = targetMs;
+    if (anchorMs < minMs) anchorMs = minMs;
+    if (anchorMs > maxMs) anchorMs = maxMs;
+
+    // 4) 计算在该行范围内的比例
+    const total = Math.max(1, maxMs - minMs);
+    const ratio = Math.max(0, Math.min(1, (anchorMs - minMs) / total));
+    const percent = `${(ratio * 100).toFixed(2)}%`;
+
+    // 5) 偶数行用 left,奇数行用 right,与 Z 字方向一致
+    if (rowIndex % 2 === 1) {
+        phenologyPositions.value[rowIndex] = { right: percent };
+    } else {
+        phenologyPositions.value[rowIndex] = { left: percent };
+    }
+};
+// moved above with other refs
+
+// 将生育期条按行计算定位与宽度
+const groupPhenologyBarsByRow = (phenologyList, solarTerms) => {
+    phenologyBarsByRow.value = [];
+    if (
+        !Array.isArray(phenologyList) ||
+        phenologyList.length === 0 ||
+        !Array.isArray(solarTerms) ||
+        solarTerms.length === 0
+    ) {
+        return;
+    }
+
+    const termsPerRow = 3;
+    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
+
+    // 行范围:使用该行包含的节气最早/最晚时间(按真实时间线性映射)
+    const rowRanges = [];
+    for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
+        const startIdx = rowIndex * termsPerRow;
+        const endIdx = Math.min(startIdx + termsPerRow - 1, solarTerms.length - 1);
+        const rowTerms = solarTerms.slice(startIdx, endIdx + 1);
+        const rowDates = rowTerms
+            .map((t) => parseDate(t?.createDate))
+            .filter((d) => d && !isNaN(d.getTime()))
+            .map((d) => d.getTime());
+        if (rowDates.length === 0) continue;
+        const minMs = Math.min(...rowDates);
+        const maxMs = Math.max(...rowDates);
+        const rowStart = new Date(minMs);
+        const rowEnd = new Date(maxMs);
+        const totalMs = Math.max(1, rowEnd.getTime() - rowStart.getTime());
+        rowRanges.push({ rowIndex, rowStart, rowEnd, totalMs });
+        phenologyBarsByRow.value[rowIndex] = [];
+    }
+
+    // 中点归属法:每条生育期归属到中点所在的行;在行内按 Z 字方向计算 left/right 与 width
+    phenologyList.forEach((p, pIndex) => {
+        const list = Array.isArray(p?.reproductiveList) ? p.reproductiveList : [];
+        const baseColorClass = pIndex % 2 === 0 ? "blue" : "orange";
+        list.forEach((r) => {
+            // 优先使用节气 id 对应的节气日期
+            let sTermDate = null;
+            let eTermDate = null;
+            if (r?.startSolarTermId && solarTermIdToTerm[r.startSolarTermId]?.createDate) {
+                sTermDate = parseDate(solarTermIdToTerm[r.startSolarTermId].createDate);
+            }
+            if (r?.endSolarTermId && solarTermIdToTerm[r.endSolarTermId]?.createDate) {
+                eTermDate = parseDate(solarTermIdToTerm[r.endSolarTermId].createDate);
+            }
+
+            const s = sTermDate || parseDate(r?.startDate);
+            const e = eTermDate || parseDate(r?.endDate);
+            if (!s || !e) return;
+            const start = new Date(Math.min(s.getTime(), e.getTime()));
+            const end = new Date(Math.max(s.getTime(), e.getTime()));
+            if (end < start) return;
+            const mid = new Date(start.getTime() + (end.getTime() - start.getTime()) / 2);
+
+            // 找到中点所在行;若不在任何行,则归最近行
+            let target = rowRanges.find(({ rowStart, rowEnd }) => mid >= rowStart && mid <= rowEnd);
+            if (!target) {
+                target = rowRanges.reduce((best, curr) => {
+                    const dist =
+                        mid < curr.rowStart
+                            ? curr.rowStart.getTime() - mid.getTime()
+                            : mid.getTime() - curr.rowEnd.getTime();
+                    if (!best || dist < best.dist) return { dist, curr };
+                    return best;
+                }, null)?.curr;
+            }
+            if (!target) return;
+
+            // 位置:基于真实的 startDate(不截断),确保相邻条的间距 = (后一个startDate - 前一个endDate) 的时间差映射
+            const startRatio = (start.getTime() - target.rowStart.getTime()) / target.totalMs;
+
+            // 宽度:基于真实的 endDate - startDate 的时间差
+            const actualDuration = end.getTime() - start.getTime();
+            const widthRatio = actualDuration / target.totalMs;
+
+            // 限制到行范围内
+            const leftRatio = Math.max(0, Math.min(1, startRatio));
+            const rightRatio = Math.max(0, Math.min(1, (end.getTime() - target.rowStart.getTime()) / target.totalMs));
+            let clampedWidthRatio = Math.max(0.001, Math.min(widthRatio, rightRatio - leftRatio));
+
+            // 强制最小显示宽度:若换算到像素后小于 CSS 中的 min-width:22px,则使用最小可见宽度
+            const MIN_LABEL_PX = 22; // 与样式 .cycle-label 的最小宽度保持一致
+            const rowWidthPx = rowWidths.value?.[target.rowIndex] || 0;
+            let leftPercent = leftRatio * 100;
+            let widthPercent = clampedWidthRatio * 100;
+            if (rowWidthPx > 0) {
+                const minPercent = (MIN_LABEL_PX / rowWidthPx) * 100;
+                if (widthPercent < minPercent) {
+                    widthPercent = minPercent;
+                }
+                // 若越界则左移以保证完全可见
+                if (leftPercent + widthPercent > 100) {
+                    leftPercent = Math.max(0, 100 - widthPercent);
+                }
+                // 回填为比例供后续使用
+                clampedWidthRatio = widthPercent / 100;
+            } else {
+                // 无法测量时,保底给一个不至于 0 的最小显示比例(以 360px 近似,22px/360≈6.1%)
+                if (widthPercent < 6.2) {
+                    widthPercent = 6.2;
+                    if (leftPercent + widthPercent > 100) leftPercent = Math.max(0, 100 - widthPercent);
+                    clampedWidthRatio = widthPercent / 100;
+                }
+            }
+
+            const isFuture = start.getTime() > Date.now();
+            const colorToUse = isFuture ? "" : baseColorClass;
+            // 组装农事安排:按 reproductiveId 归属到当前生育期
+            const arrangeList = Array.isArray(r.farmWorkArrangeList)
+                ? r.farmWorkArrangeList.filter((fw) => !fw.reproductiveId || fw.reproductiveId === r.id)
+                : [];
+            const arrangeItems = arrangeList.map((fw) => {
+                let status = "default";
+                const t = fw.farmWorkType;
+                if (t == null || t === 0) {
+                    status = "default";
+                } else if (t >= 1 && t <= 4) {
+                    status = "normal";
+                } else if (t === 5) {
+                    status = "complete";
+                } else if (t === 6) {
+                    status = "warning";
+                }
+                return {
+                    id: fw.id,
+                    name: fw.farmWorkName,
+                    status,
+                };
+            });
+
+            // // 附加两条测试数据:已完成、已过期
+            // arrangeItems.push(
+            //     { id: `${r.id}-test-complete`, name: "测试完成", status: "complete" },
+            //     { id: `${r.id}-test-warning`, name: "测试过期", status: "warning" }
+            // );
+
+            phenologyBarsByRow.value[target.rowIndex].push({
+                id: r.id || `${p.id || "p"}-${start.getTime()}-${end.getTime()}`,
+                name: r.name && r.name.trim() ? r.name.trim() : r.phenologyName || "生育期",
+                left: `${leftPercent.toFixed(4)}%`,
+                width: `${(clampedWidthRatio * 100).toFixed(4)}%`,
+                startTime: start.getTime(), // 用于排序,确保相邻条的顺序正确
+                color: colorToUse,
+                arranges: arrangeItems,
+            });
+        });
+    });
+
+    // 每行内部按 startTime 排序,确保相邻条的间距正确反映时间差
+    phenologyBarsByRow.value.forEach((rowBars) => {
+        rowBars.sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
+    });
+};
+
+// 获取指定行的生育期条
+const getPhenologyBarsForRow = (rowIndex) => {
+    return phenologyBarsByRow.value[rowIndex] || [];
+};
+
+// 视觉奇偶:自下而上计算奇偶(与 UI Z 字一致)
+const isOddVisualRow = (rowIndex) => {
+    const total = timelineRows.length;
+    if (total <= 0) return rowIndex % 2 === 1;
+    const visualIndex = total - 1 - rowIndex;
+    return visualIndex % 2 === 1;
+};
+
+// 新增农事
+const addNewTask = () => {
+    router.push({
+        path: "/modify_work",
+        query: { data: JSON.stringify(["生长异常"]), gardenId: 766, isAdd: true },
+    });
+};
+
+const manageTask = () => {
+    router.push({
+        path: "/agri_services_manage",
+        query: {
+            type: "manage",
+        },
+    });
+};
+
+const detailDialogRef = ref(null);
+
+const handleRowClick = (item) => {
+    if (item.status === "complete") {
+        router.push({
+            path: "/review_work",
+            query: {
+                id: item.id,
+            },
+        });
+    } else if (item.type !== "term" && item.status === "default") {
+        detailDialogRef.value.showDialog();
+    } else if (item.status === "warning" || item.status === "normal") {
+        router.push({
+            path: "/completed_work",
+            query: {
+                id: item.id,
+                status: item.status,
+            },
+        });
+        // router.push({
+        //     path: "/services_agri",
+        //     query: {
+        //         id: item.id,
+        //         status: item.status,
+        //     },
+        // });
+    }
+};
+
+// 行连接器颜色:若后续生育期未开始则灰色,否则保持其颜色(蓝/橙)
+const getConnectorColorClass = (rowIndex) => {
+    const nextIndex = rowIndex + 1;
+    const bars = getPhenologyBarsForRow(nextIndex);
+    if (!bars || bars.length === 0) return "";
+
+    const nextIsOddIndex = nextIndex % 2 === 1; // 奇数行为左侧连接器
+
+    const parsePercent = (val) => {
+        if (typeof val !== "string") return 0;
+        const n = parseFloat(val.replace("%", ""));
+        return isNaN(n) ? 0 : n;
+    };
+
+    let target = bars[0];
+    if (nextIsOddIndex) {
+        // 左侧:选最靠左的条
+        target = bars.reduce((best, cur) => (parsePercent(cur.left) < parsePercent(best.left) ? cur : best), bars[0]);
+    } else {
+        // 右侧:选最靠右的条(left + width 最大)
+        const score = (b) => parsePercent(b.left) + parsePercent(b.width);
+        target = bars.reduce((best, cur) => (score(cur) > score(best) ? cur : best), bars[0]);
+    }
+
+    // 未来(未开始)时,color 为空串;过去/当前一律显示蓝色
+    const hasStarted = !!target?.color;
+    return hasStarted ? "" : "connector-gray";
+};
+
+// 行连接器是否为灰色(用于切换箭头图片)
+const isConnectorGray = (rowIndex) => getConnectorColorClass(rowIndex) === "connector-gray";
+</script>
+
+<style scoped lang="scss">
+.plan-page {
+    width: 100%;
+    height: 100vh;
+    background: #fff;
+    .plan-content {
+        .filter-wrap {
+            background: #fff;
+            padding: 13px 12px;
+            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+            border-radius: 0 0 20px 20px;
+            .status-filter {
+                background: #fff;
+                padding: 3px 17px;
+                display: flex;
+                align-items: center;
+                gap: 16px;
+                font-size: 12px;
+
+                .status-item {
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    gap: 6px;
+                    flex: 1;
+                    &.gray {
+                        color: #c4c6c9;
+                        .status-dot {
+                            background-color: #c4c6c9;
+                        }
+                    }
+
+                    &.blue {
+                        color: #2199f8;
+                        .status-dot {
+                            background-color: #2199f8;
+                        }
+                    }
+
+                    &.green {
+                        color: #1ca900;
+                        .status-dot {
+                            background-color: #1ca900;
+                        }
+                    }
+
+                    &.orange {
+                        color: #ff953d;
+                        .status-dot {
+                            background-color: #ff953d;
+                        }
+                    }
+                    .status-dot {
+                        width: 6px;
+                        height: 6px;
+                        border-radius: 50%;
+                    }
+                }
+            }
+
+            .season-tabs {
+                display: flex;
+                gap: 8px;
+                margin-bottom: 12px;
+                .season-tab {
+                    flex: 1;
+                    padding: 7px;
+                    text-align: center;
+                    background: #f3f3f3;
+                    color: #898a8a;
+                    border-radius: 3px;
+                    border: 1px solid transparent;
+                    font-size: 12px;
+                }
+                .season-tab.active {
+                    background: #ffffff;
+                    color: #2199f8;
+                    border-color: #2199f8;
+                }
+            }
+        }
+
+        // 循环时间线样式
+        .cycle-timeline-container {
+            padding: 35px 15px 25px;
+            height: calc(100vh - 135px - 69px - 60px);
+            overflow-y: auto;
+            overflow-x: hidden;
+            .cycle-timeline {
+                position: relative;
+                .cycle-row {
+                    position: relative;
+                    display: flex;
+                    justify-content: space-between;
+                    align-items: center;
+                    margin-bottom: 60px;
+                    padding-right: 30px;
+                    &.odd-index {
+                        padding: 0;
+                        padding-left: 30px;
+                        flex-direction: row-reverse;
+                        .cycle-phenology-wrap {
+                            left: 6px;
+                            width: calc(100% - 13px);
+                        }
+                    }
+
+                    &:last-child {
+                        margin-bottom: 0;
+                        .cycle-phenology-wrap {
+                            left: 20px;
+                            width: calc(100% - 10px);
+                        }
+                    }
+
+                    // 水平时间线
+                    &::before {
+                        content: "";
+                        position: absolute;
+                        top: 0;
+                        left: 6px;
+                        right: 6px;
+                        height: 5px;
+                        border-left: 2px solid #fff;
+                        border-right: 2px solid #fff;
+                        background: #e8e8e8;
+                        transform: translateY(-50%);
+                        z-index: 1;
+                    }
+
+                    .cycle-item {
+                        position: relative;
+                        z-index: 2;
+                        top: 12px;
+
+                        &.term-item {
+                            display: flex;
+                            flex-direction: column;
+                            align-items: center;
+                            top: -11px;
+
+                            .cycle-term-dot {
+                                width: 6px;
+                                height: 6px;
+                                background: #c7c7c7;
+                                border-radius: 50%;
+                                margin-bottom: 4px;
+                            }
+
+                            .cycle-term-label {
+                                font-size: 11px;
+                                color: #c7c7c7;
+                                margin-top: 16px;
+                            }
+
+                            &.active {
+                                .cycle-term-dot {
+                                    background: #858383;
+                                }
+                                .cycle-term-label {
+                                    color: #858383;
+                                }
+                            }
+                        }
+                    }
+
+                    .cycle-phenology-wrap {
+                        position: absolute;
+                        top: -23px;
+                        left: 6px;
+                        width: calc(100% - 10px);
+                        z-index: 3;
+                        height: 100px;
+                        overflow: hidden;
+                        .cycle-label {
+                            position: absolute;
+                            color: #4e4e4e;
+                            font-size: 12px;
+                            min-width: 24px;
+                            height: 20px;
+                            line-height: 20px;
+                            text-align: center;
+                            background: rgba(180, 182, 183, 0.1);
+                            border-bottom: 6px solid #e8e8e8;
+                        }
+                        .cycle-label + .cycle-label {
+                            border-right: 1px solid #fff;
+                        }
+                        .cycle-label.blue {
+                            color: #2199f8;
+                            background: rgba(33, 153, 248, 0.1);
+                            border-bottom-color: #2199f8;
+                        }
+                        .cycle-label.orange {
+                            color: #ff953d;
+                            background: #fff2e7;
+                            border-bottom-color: #ff953d;
+                        }
+                        .arranges {
+                            display: flex;
+                            gap: 8px;
+                            padding-top: 26px;
+                            justify-content: center;
+                            flex-wrap: nowrap;
+                            // 使用与任务框一致的视觉风格
+                            .cycle-task-box {
+                                border: 1px solid rgba(199, 199, 199, 0.5);
+                                border-radius: 2px;
+                                width: 36px;
+                                height: 36px;
+                                min-width: 36px;
+                                line-height: 15px;
+                                font-size: 12px;
+                                box-sizing: border-box;
+                                padding: 2px 0;
+                                text-align: center;
+                                position: relative;
+                                color: #c7c7c7;
+                                .status-icon {
+                                    position: absolute;
+                                    bottom: -10px;
+                                    right: -10px;
+                                }
+                            }
+
+                            .cycle-task-connector {
+                                position: absolute;
+                                top: -4px;
+                                left: 50%;
+                                transform: translateX(-50%);
+                                width: 0;
+                                height: 0;
+                                border-left: 4px solid transparent;
+                                border-right: 4px solid transparent;
+                                border-bottom: 4px solid #dde1e7;
+                            }
+
+                            .cycle-task-box.warning {
+                                border-color: #ff953d;
+                            }
+                            .cycle-task-box.warning .cycle-task-text {
+                                color: #ff953d;
+                            }
+                            .cycle-task-box.warning + .cycle-task-connector,
+                            .cycle-task-box.warning .cycle-task-connector {
+                                border-bottom-color: #ff953d;
+                            }
+
+                            .cycle-task-box.complete {
+                                border-color: #1ca900;
+                            }
+                            .cycle-task-box.complete .cycle-task-text {
+                                color: #1ca900;
+                            }
+                            .cycle-task-box.complete + .cycle-task-connector,
+                            .cycle-task-box.complete .cycle-task-connector {
+                                border-bottom-color: #1ca900;
+                            }
+
+                            .cycle-task-box.normal {
+                                border-color: #2199f8;
+                            }
+                            .cycle-task-box.normal .cycle-task-text {
+                                color: #2199f8;
+                            }
+                            .cycle-task-box.normal + .cycle-task-connector,
+                            .cycle-task-box.normal .cycle-task-connector {
+                                border-bottom-color: #2199f8;
+                            }
+                        }
+                    }
+
+                    .cycle-connector {
+                        position: absolute;
+                        right: 0;
+                        top: 45.5px;
+                        transform: translateY(-50%);
+                        width: 2px;
+                        height: 87px;
+                        border: 5px solid #9dcaf7;
+                        border-left: none;
+                        background: transparent;
+                        img{
+                            width: 13px;
+                            height: 13px;
+                            position: absolute;
+                            top: 50%;
+                            transform: translateY(-50%);
+                            left: -8px;
+                            z-index: 1;
+                        }
+
+                        &.top-connector {
+                            border-top-right-radius: 5px;
+                            border-bottom-right-radius: 5px;
+                            img{
+                                left: -2px;
+                            }
+                        }
+
+                        &.middle-connector {
+                            border-top-left-radius: 5px;
+                            border-bottom-left-radius: 5px;
+                            left: 0;
+                            border-right: none;
+                            border-left: 5px solid #9dcaf7;
+                        }
+
+                        // 动态颜色
+                        &.connector-gray {
+                            border-color: #c4c6c9;
+                        }
+                        &.connector-gray.middle-connector {
+                            border-left-color: #c4c6c9;
+                        }
+                    }
+                }
+            }
+        }
+
+        // 控制区域样式
+        .control-section {
+            position: fixed;
+            width: 100%;
+            left: 0;
+            box-sizing: border-box;
+            bottom: 0px;
+            background: #fff;
+            padding: 16px 12px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            border-top: 1px solid #f0f0f0;
+
+            .toggle-group {
+                display: flex;
+                align-items: center;
+                gap: 8px;
+
+                .toggle-label {
+                    font-size: 13px;
+                    color: #141414;
+                }
+            }
+
+            .add-button-group {
+                display: flex;
+                align-items: center;
+                gap: 8px;
+                .button {
+                    color: #2199f8;
+                    border-radius: 25px;
+                    padding: 9px 15px;
+                    border: 1px solid #2199f8;
+                }
+                .add-button {
+                    background: linear-gradient(120deg, #76c3ff 0%, #2199f8 100%);
+                    color: white;
+                    border: 1px solid transparent;
+                }
+            }
+        }
+    }
+}
+</style>

+ 301 - 732
src/views/old_mini/monitor/subPages/plan.vue

@@ -9,7 +9,7 @@
                         :key="s.value"
                         class="season-tab"
                         :class="{ active: s.value === activeSeason }"
-                        @click="activeSeason = s.value"
+                        @click="handleSeasonClick(s.value)"
                     >
                         {{ s.label }}
                     </div>
@@ -21,79 +21,62 @@
                     </div>
                 </div>
             </div>
-
-            <!-- 三行循环时间线 -->
-            <div class="cycle-timeline-container">
-                <div class="cycle-timeline">
+            <div class="timeline-container" ref="timelineContainerRef">
+                <div class="timeline-list" :style="getListStyle">
+                    <div class="timeline-middle-line"></div>
+                    <!-- 物候期覆盖条(progress 为起点,progress2 为终点,单位 %) -->
                     <div
-                        v-for="(row, rowIndex) in timelineRows"
-                        :key="rowIndex"
-                        class="cycle-row"
-                        :class="{ 'odd-index': rowIndex % 2 === 1 }"
+                        v-for="(p, idx) in phenologyList"
+                        :key="p.id ?? idx"
+                        class="phenology-bar"
+                        :style="getPhenologyBarStyle(p)"
                     >
-                        <div
-                            v-for="(item, itemIndex) in row.items"
-                            :key="itemIndex"
-                            class="cycle-item"
-                            @click="handleRowClick(item)"
-                            :class="[item.type + '-item']"
-                        >
-                            <!-- 节气节点 -->
-                            <template v-if="item.type === 'term'">
-                                <!-- <div class="cycle-term-dot"></div> -->
-                                <div class="cycle-term-label">{{ item.name || item.id }}</div>
-                            </template>
-                        </div>
-
-                        <!-- 生育期名称(根据时间范围显示在对应行) -->
-                        <div class="cycle-phenology-wrap" v-if="getPhenologyBarsForRow(rowIndex).length > 0">
+                        <div class="reproductive-list">
                             <div
-                                v-for="p in getPhenologyBarsForRow(rowIndex)"
-                                :key="p.id"
-                                class="cycle-label"
-                                :class="p.color"
-                                :style="
-                                    isOddVisualRow(rowIndex)
-                                        ? { right: p.left, width: p.width }
-                                        : { left: p.left, width: p.width }
-                                "
+                                v-for="(r, rIdx) in Array.isArray(p.reproductiveList) ? p.reproductiveList : []"
+                                :key="r.id ?? rIdx"
+                                class="reproductive-item"
                             >
-                                {{ p.name }}
-                                <div v-if="p.arranges && p.arranges.length" class="arranges">
-                                    <div v-for="a in p.arranges" :key="a.id" :class="['cycle-task-box', a.status]">
-                                        <div class="cycle-task-text">{{ a.farmWorkName || a.name }}</div>
-                                        <!-- 任务连接器 -->
-                                        <div class="cycle-task-connector"></div>
+                                {{ r.name }}
+                                <div class="arranges">
+                                    <div
+                                        v-for="(fw, aIdx) in Array.isArray(r.farmWorkArrangeList)
+                                            ? r.farmWorkArrangeList
+                                            : []"
+                                        :key="fw.id ?? aIdx"
+                                        class="arrange-box"
+                                        :class="getArrangeStatusClass(fw)"
+                                    >
+                                        <span class="arrange-text">{{ fw.farmWorkName }}</span>
                                         <div
-                                            v-if="a.status === 'complete' || a.status === 'warning'"
+                                            v-if="
+                                                getArrangeStatusClass(fw) === 'status-complete' ||
+                                                getArrangeStatusClass(fw) === 'status-warning'
+                                            "
                                             class="status-icon"
-                                            :class="a.status"
+                                            :class="getArrangeStatusClass(fw)"
                                         >
-                                            <el-icon v-if="a.status === 'complete'" size="16" color="#1CA900"
-                                                ><SuccessFilled
-                                            /></el-icon>
-                                            <el-icon v-else size="18" color="#FF953D"><WarnTriangleFilled /></el-icon>
+                                            <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
-                            v-if="rowIndex < timelineRows.length - 1"
-                            class="cycle-connector"
-                            :class="[
-                                rowIndex % 2 === 1 ? 'middle-connector' : 'top-connector',
-                                getConnectorColorClass(rowIndex),
-                            ]"
-                        >
-                            <img v-if="isConnectorGray(rowIndex)" src="@/assets/img/monitor/defalut-arrow.png" alt="" />
-                            <img v-else src="@/assets/img/monitor/arrow.png" alt="" />
-                        </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>
-
             <div class="control-section">
                 <div class="toggle-group">
                     <el-switch v-model="isDefaultEnabled" />
@@ -113,11 +96,12 @@
 </template>
 
 <script setup>
-import { reactive, ref, onMounted, nextTick, onBeforeUnmount } from "vue";
+import { reactive, ref, onMounted, computed } from "vue";
 import customHeader from "@/components/customHeader.vue";
 import { useRouter, useRoute } from "vue-router";
 import detailDialog from "@/components/detailDialog.vue";
 import activeUploadPopup from "@/components/popup/activeUploadPopup.vue";
+import { SuccessFilled, WarningFilled } from "@element-plus/icons-vue";
 const router = useRouter();
 const route = useRoute();
 
@@ -128,7 +112,7 @@ const seasons = reactive([
     { value: "autumn", label: "秋季" },
     { value: "winter", label: "冬季" },
 ]);
-const activeSeason = ref("spring");
+const activeSeason = ref("");
 
 const statusList = reactive([
     { value: "pending", label: "待触发", color: "gray" },
@@ -137,95 +121,58 @@ const statusList = reactive([
     { value: "expired", label: "已过期", color: "orange" },
 ]);
 
-// 切换开关状态
-const isDefaultEnabled = ref(true);
-
-// 时间线行数据(由接口节气生成)
-const timelineRows = reactive([]);
-
-// 目标定位日期(当前生育期参考点)
-const targetDate = new Date("2025-04-04T00:00:00");
-// 每行“当前生育期”标记的位置样式(按行索引)
-const phenologyPositions = ref({});
-
-// 生育期条(按行分组)
-const phenologyBarsByRow = ref([]);
-// 每一行可视区域的实际像素宽度(用于将最小像素宽度换算为百分比)
-const rowWidths = ref([]);
-// 节气 id 到对象的索引,便于通过 id 查找节气日期
-let solarTermIdToTerm = {};
-// 接口返回的生育期数据
+const solarTerms = ref([]);
 const phenologyList = ref([]);
 
-// 安全日期解析(兼容 'YYYY-MM-DD HH:mm:ss' / 'YYYY/MM/DD HH:mm:ss')
-const parseDate = (val) => {
-    if (!val) return null;
-    if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
-    if (typeof val === "number") return new Date(val);
-    if (typeof val === "string") {
-        // 统一到可被 Safari 解析的格式
-        const s = val.replace(/-/g, "/").replace("T", " ");
-        const d = new Date(s);
-        return isNaN(d.getTime()) ? null : d;
-    }
-    return null;
-};
-
 onMounted(() => {
     getFarmWorkPlan();
-    window.addEventListener("resize", handleResize, { passive: true });
-});
-onBeforeUnmount(() => {
-    window.removeEventListener("resize", handleResize);
 });
 
-const handleResize = () => {
-    // 重新测量并基于最新宽度重算条目
-    nextTick(() => {
-        measureRowWidths();
-        // 需要基于最新数据重算
-        if (phenologyList.value && phenologyList.value.length && cachedValidSolarTerms.value) {
-            groupPhenologyBarsByRow(phenologyList.value, cachedValidSolarTerms.value);
-        }
-    });
-};
-
-// 缓存已过滤/排序后的节气用于重复计算
-const cachedValidSolarTerms = ref(null);
-
 const getFarmWorkPlan = () => {
-    const paramFarmId = Number(route.query.farmId) || undefined;
     VE_API.monitor
-        .farmWorkPlan({ farmId: paramFarmId ?? 92844 }) // 优先使用路由传入的 farmId
+        .farmWorkPlan({ farmId: route.query.farmId })
         .then(({ data, code }) => {
             if (code === 0) {
-                const solarTermsList = data.solarTermsList;
-                // 仅保留 type === 1 的节气,按需要的顺序(示例:反转)
-                // 取 type===1 的节气,并按日期降序排序(晚到早)
-                const validSolarTerms = Array.isArray(solarTermsList)
-                    ? solarTermsList
-                          .filter((t) => t && t.type === 1 && t.createDate)
-                          .sort((a, b) => {
-                              const da = parseDate(a.createDate)?.getTime() ?? 0;
-                              const db = parseDate(b.createDate)?.getTime() ?? 0;
-                              return db - da;
-                          })
+                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) => ({
+                          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 : [],
+                      }))
                     : [];
-                cachedValidSolarTerms.value = validSolarTerms;
-                generateTimelineData(validSolarTerms);
-                computeCurrentPhenologyPositions(validSolarTerms, targetDate);
-                // 保存生育期数据并生成各行生育期条
-                phenologyList.value = Array.isArray(data.phenologyList) ? data.phenologyList : [];
-                // 生成 id->term 的索引
-                solarTermIdToTerm = {};
-                validSolarTerms.forEach((t) => {
-                    if (t && (t.id || t.solarTermsId)) solarTermIdToTerm[t.id ?? t.solarTermsId] = t;
-                });
-                // 先等待 DOM 渲染完成后测量每行宽度,再据此计算最小可显示宽度
-                nextTick(() => {
-                    measureRowWidths();
-                    groupPhenologyBarsByRow(phenologyList.value, validSolarTerms);
-                });
+
+                // // 测试数据:补充完成/预警案例以展示样式
+                // if (phenologyList.value.length > 0) {
+                //     const rlist = phenologyList.value[0]?.reproductiveList;
+                //     if (Array.isArray(rlist) && rlist.length > 0) {
+                //         const first = rlist[0];
+                //         if (!Array.isArray(first.farmWorkArrangeList)) first.farmWorkArrangeList = [];
+                //         first.farmWorkArrangeList.push(
+                //             { id: "test-complete", farmWorkName: "测试完成", farmWorkType: 5 },
+                //             { id: "test-warning", farmWorkName: "测试预警", farmWorkType: 6 }
+                //         );
+                //     }
+                // }
             }
         })
         .catch((error) => {
@@ -233,333 +180,8 @@ const getFarmWorkPlan = () => {
         });
 };
 
-// 测量每一行生育期容器的实际宽度
-const measureRowWidths = () => {
-    const rows = document.querySelectorAll(".cycle-timeline .cycle-row");
-    const widths = [];
-    rows.forEach((rowEl, idx) => {
-        const wrap = rowEl.querySelector(".cycle-phenology-wrap");
-        widths[idx] = wrap ? wrap.offsetWidth : 0;
-    });
-    rowWidths.value = widths;
-};
-
-// 生成时间轴数据
-const generateTimelineData = (solarTerms) => {
-    // 清空
-    timelineRows.splice(0, timelineRows.length);
-
-    // 无数据则给一行示例
-    if (!solarTerms || solarTerms.length === 0) {
-        timelineRows.push({
-            items: [
-                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
-                { type: "term", name: "节气" },
-                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
-                { type: "term", name: "节气" },
-                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
-                { type: "term", name: "节气" },
-            ],
-        });
-        return;
-    }
-
-    const itemsPerRow = 6; // 任务/节气交替
-    const termsPerRow = 3; // 每行3个节气
-    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
-
-    for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
-        const rowItems = [];
-        const startTermIndex = rowIndex * termsPerRow;
-
-        for (let i = 0; i < itemsPerRow; i++) {
-            if (i % 2 === 0) {
-                // 任务位
-                const taskData = getTaskDataForIndex(Math.floor(i / 2));
-                rowItems.push({
-                    type: "task",
-                    status: taskData.status,
-                    taskName: taskData.taskName,
-                    taskDesc: taskData.taskDesc,
-                    icon: taskData.icon,
-                });
-            } else {
-                // 节气位
-                const termIndex = startTermIndex + Math.floor(i / 2);
-                if (termIndex < solarTerms.length) {
-                    const term = solarTerms[termIndex] || {};
-                    rowItems.push({
-                        type: "term",
-                        status: "default",
-                        name: term.name || term.solarTermsName || term.termName || "节气",
-                        id: term.id,
-                        createDate: term.createDate,
-                    });
-                } else {
-                    // 不足时补任务
-                    rowItems.push({
-                        type: "task",
-                        status: "default",
-                        taskName: "梢期",
-                        taskDesc: "杀虫",
-                    });
-                }
-            }
-        }
-
-        timelineRows.push({ items: rowItems });
-    }
-};
-
-// 任务占位数据(可按需接后端)
-const getTaskDataForIndex = (index) => {
-    const defaultTasks = [
-        { status: "default", taskName: "梢期", taskDesc: "杀虫" },
-        { status: "active", taskName: "梢期", taskDesc: "营养" },
-        { status: "complete", taskName: "梢期", taskDesc: "修剪", icon: { type: "complete" } },
-        { status: "warning", taskName: "梢期", taskDesc: "施肥", icon: { type: "warning" } },
-        { status: "normal", taskName: "梢期", taskDesc: "灌溉", icon: { type: "normal" } },
-    ];
-    return defaultTasks[index % defaultTasks.length];
-};
-
-// 计算“当前生育期”在各行的定位(只在包含目标日期的那一行显示)
-const computeCurrentPhenologyPositions = (solarTerms, date) => {
-    phenologyPositions.value = {};
-    if (!Array.isArray(solarTerms) || solarTerms.length === 0 || !(date instanceof Date)) return;
-
-    const termsPerRow = 3;
-    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
-
-    // 1) 找到最接近目标日期的节气(按时间升序)
-    const termsAsc = solarTerms
-        .filter((t) => t && t.createDate)
-        .slice()
-        .sort((a, b) => (parseDate(a.createDate)?.getTime() ?? 0) - (parseDate(b.createDate)?.getTime() ?? 0));
-    if (termsAsc.length === 0) return;
-
-    const targetMs = date.getTime();
-    let nearest = termsAsc[0];
-    let bestDiff = Math.abs((parseDate(nearest.createDate)?.getTime() ?? 0) - targetMs);
-    for (let i = 1; i < termsAsc.length; i++) {
-        const ms = parseDate(termsAsc[i].createDate)?.getTime() ?? 0;
-        const diff = Math.abs(ms - targetMs);
-        if (diff < bestDiff) {
-            bestDiff = diff;
-            nearest = termsAsc[i];
-        }
-    }
-
-    // 2) 将该节气映射回当前(降序)数组中的索引与行
-    const nearestIdxDesc = solarTerms.findIndex((t) => t && nearest && t.id === nearest.id);
-    const rowIndex = Math.max(0, Math.floor(nearestIdxDesc / termsPerRow));
-
-    const startIdx = rowIndex * termsPerRow;
-    const endIdx = Math.min(startIdx + termsPerRow - 1, solarTerms.length - 1);
-    if (startIdx > endIdx) return;
-
-    const rowTerms = solarTerms.slice(startIdx, endIdx + 1);
-    // 视觉顺序用于方向(偶数行正向,奇数行反向),但时间范围应取该行真实最早/最晚
-    const rowDates = rowTerms
-        .map((t) => parseDate(t?.createDate))
-        .filter((d) => d && !isNaN(d.getTime()))
-        .map((d) => d.getTime());
-    if (rowDates.length === 0) return;
-    const minMs = Math.min(...rowDates);
-    const maxMs = Math.max(...rowDates);
-    const rowStart = new Date(minMs);
-    const rowEnd = new Date(maxMs);
-
-    // 3) 若目标日期不在该行范围内,则就近夹到边界(避免跨行导致丢失)
-    let anchorMs = targetMs;
-    if (anchorMs < minMs) anchorMs = minMs;
-    if (anchorMs > maxMs) anchorMs = maxMs;
-
-    // 4) 计算在该行范围内的比例
-    const total = Math.max(1, maxMs - minMs);
-    const ratio = Math.max(0, Math.min(1, (anchorMs - minMs) / total));
-    const percent = `${(ratio * 100).toFixed(2)}%`;
-
-    // 5) 偶数行用 left,奇数行用 right,与 Z 字方向一致
-    if (rowIndex % 2 === 1) {
-        phenologyPositions.value[rowIndex] = { right: percent };
-    } else {
-        phenologyPositions.value[rowIndex] = { left: percent };
-    }
-};
-// moved above with other refs
-
-// 将生育期条按行计算定位与宽度
-const groupPhenologyBarsByRow = (phenologyList, solarTerms) => {
-    phenologyBarsByRow.value = [];
-    if (
-        !Array.isArray(phenologyList) ||
-        phenologyList.length === 0 ||
-        !Array.isArray(solarTerms) ||
-        solarTerms.length === 0
-    ) {
-        return;
-    }
-
-    const termsPerRow = 3;
-    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
-
-    // 行范围:使用该行包含的节气最早/最晚时间(按真实时间线性映射)
-    const rowRanges = [];
-    for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
-        const startIdx = rowIndex * termsPerRow;
-        const endIdx = Math.min(startIdx + termsPerRow - 1, solarTerms.length - 1);
-        const rowTerms = solarTerms.slice(startIdx, endIdx + 1);
-        const rowDates = rowTerms
-            .map((t) => parseDate(t?.createDate))
-            .filter((d) => d && !isNaN(d.getTime()))
-            .map((d) => d.getTime());
-        if (rowDates.length === 0) continue;
-        const minMs = Math.min(...rowDates);
-        const maxMs = Math.max(...rowDates);
-        const rowStart = new Date(minMs);
-        const rowEnd = new Date(maxMs);
-        const totalMs = Math.max(1, rowEnd.getTime() - rowStart.getTime());
-        rowRanges.push({ rowIndex, rowStart, rowEnd, totalMs });
-        phenologyBarsByRow.value[rowIndex] = [];
-    }
-
-    // 中点归属法:每条生育期归属到中点所在的行;在行内按 Z 字方向计算 left/right 与 width
-    phenologyList.forEach((p, pIndex) => {
-        const list = Array.isArray(p?.reproductiveList) ? p.reproductiveList : [];
-        const baseColorClass = pIndex % 2 === 0 ? "blue" : "orange";
-        list.forEach((r) => {
-            // 优先使用节气 id 对应的节气日期
-            let sTermDate = null;
-            let eTermDate = null;
-            if (r?.startSolarTermId && solarTermIdToTerm[r.startSolarTermId]?.createDate) {
-                sTermDate = parseDate(solarTermIdToTerm[r.startSolarTermId].createDate);
-            }
-            if (r?.endSolarTermId && solarTermIdToTerm[r.endSolarTermId]?.createDate) {
-                eTermDate = parseDate(solarTermIdToTerm[r.endSolarTermId].createDate);
-            }
-
-            const s = sTermDate || parseDate(r?.startDate);
-            const e = eTermDate || parseDate(r?.endDate);
-            if (!s || !e) return;
-            const start = new Date(Math.min(s.getTime(), e.getTime()));
-            const end = new Date(Math.max(s.getTime(), e.getTime()));
-            if (end < start) return;
-            const mid = new Date(start.getTime() + (end.getTime() - start.getTime()) / 2);
-
-            // 找到中点所在行;若不在任何行,则归最近行
-            let target = rowRanges.find(({ rowStart, rowEnd }) => mid >= rowStart && mid <= rowEnd);
-            if (!target) {
-                target = rowRanges.reduce((best, curr) => {
-                    const dist =
-                        mid < curr.rowStart
-                            ? curr.rowStart.getTime() - mid.getTime()
-                            : mid.getTime() - curr.rowEnd.getTime();
-                    if (!best || dist < best.dist) return { dist, curr };
-                    return best;
-                }, null)?.curr;
-            }
-            if (!target) return;
-
-            // 位置:基于真实的 startDate(不截断),确保相邻条的间距 = (后一个startDate - 前一个endDate) 的时间差映射
-            const startRatio = (start.getTime() - target.rowStart.getTime()) / target.totalMs;
-
-            // 宽度:基于真实的 endDate - startDate 的时间差
-            const actualDuration = end.getTime() - start.getTime();
-            const widthRatio = actualDuration / target.totalMs;
-
-            // 限制到行范围内
-            const leftRatio = Math.max(0, Math.min(1, startRatio));
-            const rightRatio = Math.max(0, Math.min(1, (end.getTime() - target.rowStart.getTime()) / target.totalMs));
-            let clampedWidthRatio = Math.max(0.001, Math.min(widthRatio, rightRatio - leftRatio));
-
-            // 强制最小显示宽度:若换算到像素后小于 CSS 中的 min-width:22px,则使用最小可见宽度
-            const MIN_LABEL_PX = 22; // 与样式 .cycle-label 的最小宽度保持一致
-            const rowWidthPx = rowWidths.value?.[target.rowIndex] || 0;
-            let leftPercent = leftRatio * 100;
-            let widthPercent = clampedWidthRatio * 100;
-            if (rowWidthPx > 0) {
-                const minPercent = (MIN_LABEL_PX / rowWidthPx) * 100;
-                if (widthPercent < minPercent) {
-                    widthPercent = minPercent;
-                }
-                // 若越界则左移以保证完全可见
-                if (leftPercent + widthPercent > 100) {
-                    leftPercent = Math.max(0, 100 - widthPercent);
-                }
-                // 回填为比例供后续使用
-                clampedWidthRatio = widthPercent / 100;
-            } else {
-                // 无法测量时,保底给一个不至于 0 的最小显示比例(以 360px 近似,22px/360≈6.1%)
-                if (widthPercent < 6.2) {
-                    widthPercent = 6.2;
-                    if (leftPercent + widthPercent > 100) leftPercent = Math.max(0, 100 - widthPercent);
-                    clampedWidthRatio = widthPercent / 100;
-                }
-            }
-
-            const isFuture = start.getTime() > Date.now();
-            const colorToUse = isFuture ? "" : baseColorClass;
-            // 组装农事安排:按 reproductiveId 归属到当前生育期
-            const arrangeList = Array.isArray(r.farmWorkArrangeList)
-                ? r.farmWorkArrangeList.filter((fw) => !fw.reproductiveId || fw.reproductiveId === r.id)
-                : [];
-            const arrangeItems = arrangeList.map((fw) => {
-                let status = "default";
-                const t = fw.farmWorkType;
-                if (t == null || t === 0) {
-                    status = "default";
-                } else if (t >= 1 && t <= 4) {
-                    status = "normal";
-                } else if (t === 5) {
-                    status = "complete";
-                } else if (t === 6) {
-                    status = "warning";
-                }
-                return {
-                    id: fw.id,
-                    name: fw.farmWorkName,
-                    status,
-                };
-            });
-
-            // // 附加两条测试数据:已完成、已过期
-            // arrangeItems.push(
-            //     { id: `${r.id}-test-complete`, name: "测试完成", status: "complete" },
-            //     { id: `${r.id}-test-warning`, name: "测试过期", status: "warning" }
-            // );
-
-            phenologyBarsByRow.value[target.rowIndex].push({
-                id: r.id || `${p.id || "p"}-${start.getTime()}-${end.getTime()}`,
-                name: r.name && r.name.trim() ? r.name.trim() : r.phenologyName || "生育期",
-                left: `${leftPercent.toFixed(4)}%`,
-                width: `${(clampedWidthRatio * 100).toFixed(4)}%`,
-                startTime: start.getTime(), // 用于排序,确保相邻条的顺序正确
-                color: colorToUse,
-                arranges: arrangeItems,
-            });
-        });
-    });
-
-    // 每行内部按 startTime 排序,确保相邻条的间距正确反映时间差
-    phenologyBarsByRow.value.forEach((rowBars) => {
-        rowBars.sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
-    });
-};
-
-// 获取指定行的生育期条
-const getPhenologyBarsForRow = (rowIndex) => {
-    return phenologyBarsByRow.value[rowIndex] || [];
-};
-
-// 视觉奇偶:自下而上计算奇偶(与 UI Z 字一致)
-const isOddVisualRow = (rowIndex) => {
-    const total = timelineRows.length;
-    if (total <= 0) return rowIndex % 2 === 1;
-    const visualIndex = total - 1 - rowIndex;
-    return visualIndex % 2 === 1;
-};
-
+// 切换开关状态
+const isDefaultEnabled = ref(true);
 // 新增农事
 const addNewTask = () => {
     router.push({
@@ -578,66 +200,107 @@ const manageTask = () => {
 };
 
 const detailDialogRef = ref(null);
+const timelineContainerRef = ref(null);
 
-const handleRowClick = (item) => {
-    if (item.status === "complete") {
-        router.push({
-            path: "/review_work",
-            query: {
-                id: item.id,
-            },
-        });
-    } else if (item.type !== "term" && item.status === "default") {
-        detailDialogRef.value.showDialog();
-    } else if (item.status === "warning" || item.status === "normal") {
-        router.push({
-            path: "/completed_work",
-            query: {
-                id: item.id,
-                status: item.status,
-            },
-        });
-        // router.push({
-        //     path: "/services_agri",
-        //     query: {
-        //         id: item.id,
-        //         status: item.status,
-        //     },
-        // });
+// 安全解析时间到时间戳(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;
 };
 
-// 行连接器颜色:若后续生育期未开始则灰色,否则保持其颜色(蓝/橙)
-const getConnectorColorClass = (rowIndex) => {
-    const nextIndex = rowIndex + 1;
-    const bars = getPhenologyBarsForRow(nextIndex);
-    if (!bars || bars.length === 0) return "";
-
-    const nextIsOddIndex = nextIndex % 2 === 1; // 奇数行为左侧连接器
+// 计算节气列表容器高度与项位置
+const getListStyle = computed(() => {
+    const total = (solarTerms.value?.length || 0) * 100;
+    const minH = 50 + total + 50; // 上下各 50
+    return { minHeight: `${minH}px` };
+});
 
-    const parsePercent = (val) => {
-        if (typeof val !== "string") return 0;
-        const n = parseFloat(val.replace("%", ""));
-        return isNaN(n) ? 0 : n;
+const getTermStyle = (t) => {
+    const p = Math.max(0, Math.min(100, Number(t?.progress) || 0));
+    const total = (solarTerms.value?.length || 0) * 100;
+    const top = 50 + (p / 100) * total;
+    return {
+        position: "absolute",
+        top: `${top}px`,
+        left: 0,
+        transform: "translateY(-50%)",
+        width: "30px",
+        height: "20px",
+        display: "flex",
+        alignItems: "center",
     };
+};
 
-    let target = bars[0];
-    if (nextIsOddIndex) {
-        // 左侧:选最靠左的条
-        target = bars.reduce((best, cur) => (parsePercent(cur.left) < parsePercent(best.left) ? cur : best), bars[0]);
-    } else {
-        // 右侧:选最靠右的条(left + width 最大)
-        const score = (b) => parsePercent(b.left) + parsePercent(b.width);
-        target = bars.reduce((best, cur) => (score(cur) > score(best) ? cur : best), bars[0]);
-    }
+// 点击季节 → 滚动到对应节气(立春/立夏/立秋/立冬)
+const handleSeasonClick = (seasonValue) => {
+    activeSeason.value = 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 total = (solarTerms.value?.length || 0) * 100;
+    const targetTop = 50 + (p / 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" });
+};
 
-    // 未来(未开始)时,color 为空串;过去/当前一律显示蓝色
-    const hasStarted = !!target?.color;
-    return hasStarted ? "" : "connector-gray";
+// 物候期覆盖条样式(使用像素计算,避免 100% 高度为 0 的问题)
+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 total = (solarTerms.value?.length || 0) * 100; // 有效绘制区高度(px)
+    const topPx = 50 + (start / 100) * total;
+    const heightPx = Math.max(2, ((end - start) / 100) * total);
+    const now = Date.now();
+    const isFuture = Number.isFinite(item?.startTimeMs) ? item.startTimeMs > now : start > 0; // 无开始时间时按起点>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,
+    };
 };
 
-// 行连接器是否为灰色(用于切换箭头图片)
-const isConnectorGray = (rowIndex) => getConnectorColorClass(rowIndex) === "connector-gray";
+// 农事状态样式映射(0:默认,1-4:正常,5:完成,6:预警)
+const getArrangeStatusClass = (fw) => {
+    const t = fw?.farmWorkType;
+    if (t == null || t === 0) return "status-default";
+    if (t >= 1 && t <= 4) return "status-normal";
+    if (t === 5) return "status-complete";
+    if (t === 6) return "status-warning";
+    return "status-default";
+};
 </script>
 
 <style scoped lang="scss">
@@ -721,243 +384,149 @@ const isConnectorGray = (rowIndex) => getConnectorColorClass(rowIndex) === "conn
                 }
             }
         }
-
-        // 循环时间线样式
-        .cycle-timeline-container {
-            padding: 35px 15px 25px;
-            height: calc(100vh - 135px - 69px - 60px);
-            overflow-y: auto;
-            overflow-x: hidden;
-            .cycle-timeline {
+        .timeline-container {
+            height: calc(100vh - 93px - 40px - 73px);
+            overflow: auto;
+            position: relative;
+            box-sizing: border-box;
+            padding: 0 12px;
+            .timeline-list {
                 position: relative;
-                .cycle-row {
+            }
+            .timeline-middle-line {
+                position: absolute;
+                left: 15px; /* 位于节气文字列中间(列宽约30px) */
+                top: 50px;
+                bottom: 50px;
+                width: 2px;
+                background: #e8e8e8;
+                z-index: 1;
+            }
+            .phenology-bar {
+                display: flex;
+                align-items: stretch;
+                justify-content: center;
+                box-sizing: border-box;
+                &::before {
+                    content: "";
+                    position: absolute;
+                    top: 0;
+                    left: 25px;
+                    width: calc(100vw - 100px);
+                    height: 100%;
+                    background: var(--bar-before-bg, rgba(201, 201, 201, 0.1));
+                    z-index: 1;
+                }
+                .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;
-                    display: flex;
-                    justify-content: space-between;
-                    align-items: center;
-                    margin-bottom: 60px;
-                    padding-right: 30px;
-                    &.odd-index {
-                        padding: 0;
-                        padding-left: 30px;
-                        flex-direction: row-reverse;
-                        .cycle-phenology-wrap {
-                            left: 6px;
-                            width: calc(100% - 13px);
-                        }
-                    }
-
-                    &:last-child {
-                        margin-bottom: 0;
-                        .cycle-phenology-wrap {
-                            left: 20px;
-                            width: calc(100% - 10px);
-                        }
-                    }
-
-                    // 水平时间线
-                    &::before {
-                        content: "";
+                    .arranges {
                         position: absolute;
-                        top: 0;
-                        left: 6px;
-                        right: 6px;
-                        height: 5px;
-                        border-left: 2px solid #fff;
-                        border-right: 2px solid #fff;
-                        background: #e8e8e8;
+                        left: 48px; /* 列与中线右侧一段距离 */
+                        top: 50%;
                         transform: translateY(-50%);
-                        z-index: 1;
-                    }
-
-                    .cycle-item {
-                        position: relative;
-                        z-index: 2;
-                        top: 12px;
-
-                        &.term-item {
-                            display: flex;
-                            flex-direction: column;
-                            align-items: center;
-                            top: -11px;
-
-                            .cycle-term-dot {
-                                width: 6px;
-                                height: 6px;
-                                background: #c7c7c7;
-                                border-radius: 50%;
-                                margin-bottom: 4px;
-                            }
-
-                            .cycle-term-label {
-                                font-size: 11px;
-                                color: #c7c7c7;
-                                margin-top: 16px;
-                            }
-
-                            &.active {
-                                .cycle-term-dot {
-                                    background: #858383;
-                                }
-                                .cycle-term-label {
-                                    color: #858383;
-                                }
-                            }
-                        }
-                    }
-
-                    .cycle-phenology-wrap {
-                        position: absolute;
-                        top: -23px;
-                        left: 6px;
-                        width: calc(100% - 10px);
                         z-index: 3;
-                        height: 100px;
-                        overflow: hidden;
-                        .cycle-label {
-                            position: absolute;
-                            color: #4e4e4e;
-                            font-size: 12px;
-                            min-width: 24px;
-                            height: 20px;
-                            line-height: 20px;
-                            text-align: center;
-                            background: rgba(180, 182, 183, 0.1);
-                            border-bottom: 6px solid #e8e8e8;
-                        }
-                        .cycle-label + .cycle-label {
-                            border-right: 1px solid #fff;
-                        }
-                        .cycle-label.blue {
-                            color: #2199f8;
-                            background: rgba(33, 153, 248, 0.1);
-                            border-bottom-color: #2199f8;
-                        }
-                        .cycle-label.orange {
-                            color: #ff953d;
-                            background: #fff2e7;
-                            border-bottom-color: #ff953d;
-                        }
-                        .arranges {
+                        .arrange-box {
+                            width: 36px;
+                            height: 36px;
+                            border: 1px solid rgba(199, 199, 199, 0.6);
+                            border-radius: 2px;
+                            background: #fff;
+                            color: #a5a7a9;
                             display: flex;
-                            gap: 8px;
-                            padding-top: 26px;
+                            align-items: center;
                             justify-content: center;
-                            flex-wrap: nowrap;
-                            // 使用与任务框一致的视觉风格
-                            .cycle-task-box {
-                                border: 1px solid rgba(199, 199, 199, 0.5);
-                                border-radius: 2px;
-                                width: 36px;
-                                height: 36px;
-                                min-width: 36px;
-                                line-height: 15px;
-                                font-size: 12px;
-                                box-sizing: border-box;
-                                padding: 2px 0;
+                            box-sizing: border-box;
+                            position: relative;
+                            font-size: 12px;
+                            .arrange-text {
+                                writing-mode: horizontal-tb;
+                                line-height: 14px;
                                 text-align: center;
-                                position: relative;
-                                color: #c7c7c7;
-                                .status-icon {
-                                    position: absolute;
-                                    bottom: -10px;
-                                    right: -10px;
-                                }
+                                padding-left: 3px;
                             }
-
-                            .cycle-task-connector {
+                            .status-icon {
                                 position: absolute;
-                                top: -4px;
-                                left: 50%;
-                                transform: translateX(-50%);
+                                right: -10px;
+                                bottom: -10px;
+                                z-index: 3;
+                            }
+                            &::before {
+                                content: "";
+                                position: absolute;
+                                left: -4px;
+                                top: 50%;
+                                transform: translateY(-50%);
                                 width: 0;
                                 height: 0;
-                                border-left: 4px solid transparent;
-                                border-right: 4px solid transparent;
-                                border-bottom: 4px solid #dde1e7;
-                            }
-
-                            .cycle-task-box.warning {
-                                border-color: #ff953d;
-                            }
-                            .cycle-task-box.warning .cycle-task-text {
-                                color: #ff953d;
-                            }
-                            .cycle-task-box.warning + .cycle-task-connector,
-                            .cycle-task-box.warning .cycle-task-connector {
-                                border-bottom-color: #ff953d;
-                            }
-
-                            .cycle-task-box.complete {
-                                border-color: #1ca900;
-                            }
-                            .cycle-task-box.complete .cycle-task-text {
-                                color: #1ca900;
-                            }
-                            .cycle-task-box.complete + .cycle-task-connector,
-                            .cycle-task-box.complete .cycle-task-connector {
-                                border-bottom-color: #1ca900;
-                            }
-
-                            .cycle-task-box.normal {
-                                border-color: #2199f8;
-                            }
-                            .cycle-task-box.normal .cycle-task-text {
-                                color: #2199f8;
-                            }
-                            .cycle-task-box.normal + .cycle-task-connector,
-                            .cycle-task-box.normal .cycle-task-connector {
-                                border-bottom-color: #2199f8;
+                                border-top: 4px solid transparent;
+                                border-bottom: 4px solid transparent;
+                                border-right: 4px solid currentColor; /* 与文字/边框颜色一致 */
                             }
                         }
-                    }
-
-                    .cycle-connector {
-                        position: absolute;
-                        right: 0;
-                        top: 45.5px;
-                        transform: translateY(-50%);
-                        width: 2px;
-                        height: 87px;
-                        border: 5px solid #9dcaf7;
-                        border-left: none;
-                        background: transparent;
-                        img{
-                            width: 13px;
-                            height: 13px;
-                            position: absolute;
-                            top: 50%;
-                            transform: translateY(-50%);
-                            left: -8px;
-                            z-index: 1;
+                        .arrange-box + .arrange-box {
+                            margin-right: 16px;
                         }
-
-                        &.top-connector {
-                            border-top-right-radius: 5px;
-                            border-bottom-right-radius: 5px;
-                            img{
-                                left: -2px;
-                            }
-                        }
-
-                        &.middle-connector {
-                            border-top-left-radius: 5px;
-                            border-bottom-left-radius: 5px;
-                            left: 0;
-                            border-right: none;
-                            border-left: 5px solid #9dcaf7;
+                        .arrange-box.status-warning {
+                            border-color: #ff953d;
+                            color: #ff953d;
                         }
-
-                        // 动态颜色
-                        &.connector-gray {
-                            border-color: #c4c6c9;
+                        .arrange-box.status-complete {
+                            border-color: #1ca900;
+                            color: #1ca900;
                         }
-                        &.connector-gray.middle-connector {
-                            border-left-color: #c4c6c9;
+                        .arrange-box.status-normal {
+                            border-color: #2199f8;
+                            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: center;
+                z-index: 2; /* 置于中线之上 */
+                .term-name {
+                    display: inline-block;
+                    width: 100%;
+                    height: 46px;
+                    line-height: 30px;
+                    background: #fff;
+                    font-size: 13px;
+                    word-break: break-all;
+                    writing-mode: vertical-rl;
+                    text-orientation: upright;
+                    color: rgba(174, 174, 174, 0.6);
+                    letter-spacing: 2px;
+                    text-align: center;
+                }
+            }
         }
 
         // 控制区域样式