Prechádzať zdrojové kódy

feat:修改农事规划页面样式

wangsisi 1 týždeň pred
rodič
commit
1c4d009bc8

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

@@ -0,0 +1,728 @@
+<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="handleSeasonClick(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="timeline-container" ref="timelineContainerRef">
+                <div class="timeline-list" :style="getListStyle">
+                    <div class="timeline-middle-line"></div>
+                    <!-- 物候期覆盖条(progress 为起点,progress2 为终点,单位 %) -->
+                    <div
+                        v-for="(p, idx) in phenologyList"
+                        :key="p.id ?? idx"
+                        class="phenology-bar"
+                        :style="getPhenologyBarStyle(p)"
+                    >
+                        <div class="reproductive-list">
+                            <div
+                                v-for="(r, rIdx) in Array.isArray(p.reproductiveList) ? p.reproductiveList : []"
+                                :key="r.id ?? rIdx"
+                                class="reproductive-item"
+                                :class="{
+                                    'horizontal-text': getReproductiveItemHeight(p) < 30,
+                                    'vertical-lr-text': getReproductiveItemHeight(p) >= 30
+                                }"
+                                :style="getReproductiveItemHeight(p) < 30 ? { '--item-height': `${getReproductiveItemHeight(p)}px` } : {}"
+                            >
+                                {{ r.name }}
+                                <div class="arranges" :class="{ 'no-wrap': getReproductiveItemHeight(p) <= 35 }">
+                                    <div
+                                        v-for="(fw, aIdx) in Array.isArray(r.farmWorkArrangeList)
+                                            ? r.farmWorkArrangeList
+                                            : []"
+                                        :key="fw.id ?? aIdx"
+                                        class="arrange-box"
+                                        :class="[
+                                            getArrangeStatusClass(fw),
+                                            {
+                                                'small-height': getReproductiveItemHeight(p) <= 35,
+                                                'two-chars': fw.farmWorkName && fw.farmWorkName.trim().length === 2,
+                                                'text-4-6': fw.farmWorkName && getTextLengthClass(fw.farmWorkName) === 'text-4-6',
+                                                'text-7-8': fw.farmWorkName && getTextLengthClass(fw.farmWorkName) === 'text-7-8'
+                                            }
+                                        ]"
+                                        @click="handleRowClick(fw)"
+                                    >
+                                        <span class="arrange-text">{{ formatTextWithLineBreak(fw.farmWorkName) }}</span>
+                                        <div
+                                            v-if="
+                                                getArrangeStatusClass(fw) === 'status-complete' ||
+                                                getArrangeStatusClass(fw) === 'status-warning'
+                                            "
+                                            class="status-icon"
+                                            :class="getArrangeStatusClass(fw)"
+                                        >
+                                            <el-icon
+                                                v-if="getArrangeStatusClass(fw) === 'status-complete'"
+                                                size="16"
+                                                color="#1CA900"
+                                            >
+                                                <SuccessFilled />
+                                            </el-icon>
+                                            <el-icon v-else size="18" color="#FF953D">
+                                                <WarnTriangleFilled />
+                                            </el-icon>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div v-for="t in solarTerms" :key="t.id" class="timeline-term" :style="getTermStyle(t)">
+                        <span class="term-name">{{ t.displayName }}</span>
+                    </div>
+                </div>
+            </div>
+            <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" @triggerFarmWork="triggerFarmWork"></detail-dialog>
+    <!-- 新增:激活上传弹窗 -->
+    <active-upload-popup @handleUploadSuccess="getFarmWorkPlan"></active-upload-popup>
+</template>
+
+<script setup>
+import { reactive, ref, onMounted, computed, nextTick } from "vue";
+import customHeader from "@/components/customHeader.vue";
+import { useRouter, useRoute } from "vue-router";
+import detailDialog from "@/components/detailDialog.vue";
+import eventBus from "@/api/eventBus";
+import activeUploadPopup from "@/components/popup/activeUploadPopup.vue";
+import { ElMessage } from "element-plus";
+import { SuccessFilled, WarningFilled } from "@element-plus/icons-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("");
+
+const statusList = reactive([
+    { value: "pending", label: "待触发", color: "gray" },
+    { value: "executing", label: "待完成", color: "blue" },
+    { value: "completed", label: "已完成", color: "green" },
+    { value: "expired", label: "已过期", color: "orange" },
+]);
+
+const solarTerms = ref([]);
+const phenologyList = ref([]);
+
+// 获取当前季节
+const getCurrentSeason = () => {
+    const month = new Date().getMonth() + 1; // 1-12
+    if (month >= 3 && month <= 5) {
+        return "spring"; // 春季:3-5月
+    } else if (month >= 6 && month <= 8) {
+        return "summer"; // 夏季:6-8月
+    } else if (month >= 9 && month <= 10) {
+        return "autumn"; // 秋季:9-10月
+    } else {
+        return "winter"; // 冬季:12-2月
+    }
+};
+
+onMounted(() => {
+    getFarmWorkPlan();
+});
+
+const getFarmWorkPlan = () => {
+    // 如果不是首次加载,保存当前滚动位置
+    let savedScrollTop = 0;
+    if (!isInitialLoad.value && timelineContainerRef.value) {
+        savedScrollTop = timelineContainerRef.value.scrollTop || 0;
+    }
+    
+    VE_API.monitor
+        .farmWorkPlan({ farmId: route.query.farmId })
+        .then(({ data, code }) => {
+            if (code === 0) {
+                const list = Array.isArray(data?.solarTermsList) ? data.solarTermsList : [];
+                const filtered = list
+                    .filter((t) => t && t.type === 1)
+                    .map((t) => ({
+                        id:
+                            t.id ??
+                            t.solarTermsId ??
+                            t.termId ??
+                            `${t.name || t.solarTermsName || t.termName || "term"}-${t.createDate || ""}`,
+                        displayName: t.name || t.solarTermsName || t.termName || "节气",
+                        createDate: t.createDate || null,
+                        progress: Number(t.progress) || 0,
+                    }));
+                solarTerms.value = filtered;
+                // 物候期数据
+                phenologyList.value = Array.isArray(data?.phenologyList)
+                    ? data.phenologyList.map((it) => ({
+                          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 : [],
+                      }))
+                    : [];
+                
+                // 等待 DOM 更新后处理滚动
+                nextTick(() => {
+                    if (isInitialLoad.value) {
+                        // 首次加载:设置默认季节为当前季节,并自动滚动到对应位置
+                        const currentSeason = getCurrentSeason();
+                        handleSeasonClick(currentSeason);
+                        isInitialLoad.value = false;
+                    } else {
+                        // 非首次加载:恢复之前的滚动位置
+                        if (timelineContainerRef.value && savedScrollTop > 0) {
+                            timelineContainerRef.value.scrollTop = savedScrollTop;
+                        }
+                    }
+                });
+            }
+        })
+        .catch((error) => {
+            console.error("获取农事规划数据失败:", error);
+        });
+};
+
+// 切换开关状态
+const isDefaultEnabled = ref(true);
+// 新增农事
+const addNewTask = () => {
+    ElMessage.warning("该功能正在升级中,敬请期待");
+    // router.push({
+    //     path: "/modify_work",
+    //     query: { data: JSON.stringify(["生长异常"]), gardenId: 766, isAdd: true },
+    // });
+};
+
+const triggerFarmWork = () => {
+    eventBus.emit("activeUpload:show", {
+        gardenIdVal: route.query.farmId,
+        problemTitleVal: '请选择您出现' + curFarmObj.value.farmWorkName + '的时间',
+        arrangeIdVal: curFarmObj.value.id,
+    });
+};
+
+const curFarmObj = ref({});
+const handleRowClick = (item) => {
+    curFarmObj.value = item;
+    // 0:默认,1-4:正常,5:完成,6:预警
+    if (item.flowStatus === 5) {
+        router.push({
+            path: "/review_work",
+            query: {
+                miniJson: JSON.stringify({ id: item.farmWorkRecordId,goBack: true }),
+            },
+        });
+    } else if (item.flowStatus === null) {
+        detailDialogRef.value.showDialog(item.farmWorkId);
+    } else if (item.flowStatus === 6 || (item.flowStatus < 5 && item.flowStatus >= 0)) {
+        router.push({
+            path: "/completed_work",
+            query: {
+                miniJson: JSON.stringify({ id: item.farmWorkRecordId }),
+            },
+        });
+    }
+};
+
+const manageTask = () => {
+    router.push({
+        path: "/agri_services_manage",
+        query: {
+            type: "manage",
+        },
+    });
+};
+
+const detailDialogRef = ref(null);
+const timelineContainerRef = ref(null);
+// 标记是否为首次加载
+const isInitialLoad = ref(true);
+
+// 安全解析时间到时间戳(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 getListStyle = computed(() => {
+    const total = (solarTerms.value?.length || 0) * 100;
+    const minH = 50 + total + 50; // 上下各 50
+    return { minHeight: `${minH}px` };
+});
+
+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",
+    };
+};
+
+// 点击季节 → 滚动到对应节气(立春/立夏/立秋/立冬)
+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" });
+};
+
+// 物候期覆盖条样式(使用像素计算,避免 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,
+    };
+};
+
+// 农事状态样式映射(0:默认,1-4:正常,5:完成,6:预警)
+const getArrangeStatusClass = (fw) => {
+    const t = fw?.flowStatus;
+    if (t == null) return "status-default";
+    if (t >= 0 && t <= 4) return "status-normal";
+    if (t === 5) return "status-complete";
+    if (t === 6) return "status-warning";
+    return "status-default";
+};
+
+// 计算 phenology-bar 的高度(px)
+const getPhenologyBarHeight = (item) => {
+    const p1 = Math.max(0, Math.min(100, Number(item?.progress) || 0));
+    const p2 = Math.max(0, Math.min(100, Number(item?.progress2) || 0));
+    const start = Math.min(p1, p2);
+    const end = Math.max(p1, p2);
+    const total = (solarTerms.value?.length || 0) * 100;
+    const heightPx = Math.max(2, ((end - start) / 100) * total);
+    return heightPx;
+};
+
+// 计算 reproductive-item 的高度(px)
+// 由于 reproductive-list 使用 grid-auto-rows: 1fr,每个 item 会等分 phenology-bar 的高度
+const getReproductiveItemHeight = (phenologyItem) => {
+    const barHeight = getPhenologyBarHeight(phenologyItem);
+    const listLength = Array.isArray(phenologyItem?.reproductiveList) ? phenologyItem.reproductiveList.length : 1;
+    // 如果列表为空,返回 barHeight;否则等分
+    return listLength > 0 ? barHeight / listLength : barHeight;
+};
+
+// 根据文字长度返回对应的 class
+const getTextLengthClass = (text) => {
+    if (!text || typeof text !== "string") return "";
+    const len = text.trim().length;
+    if (len > 4 && len <= 6) return "text-4-6";
+    if (len > 6 && len <= 8) return "text-7-8";
+    return "";
+};
+
+// 处理文本,在括号前换行
+const formatTextWithLineBreak = (text) => {
+    if (!text || typeof text !== "string") return text;
+    // 在左括号前添加换行符
+    return text.replace(/([((])/g, "\n$1");
+};
+
+</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;
+                }
+            }
+        }
+        .timeline-container {
+            height: calc(100vh - 93px - 40px - 73px);
+            overflow: auto;
+            position: relative;
+            box-sizing: border-box;
+            padding: 0 12px;
+            .timeline-list {
+                position: relative;
+            }
+            .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;
+                    &.horizontal-text {
+                        writing-mode: horizontal-tb;
+                        text-orientation: mixed;
+                        letter-spacing: normal;
+                        line-height: calc(var(--item-height, 15px) - 3px);
+                    }
+                    &.vertical-lr-text {
+                        writing-mode: vertical-lr;
+                        text-orientation: upright;
+                        letter-spacing: 3px;
+                        line-height: 26px;
+                    }
+                    .arranges {
+                        position: absolute;
+                        left: 48px; /* 列与中线右侧一段距离 */
+                        top: 50%;
+                        transform: translateY(-50%);
+                        z-index: 3;
+                        display: flex;
+                        flex-wrap: wrap;
+                        flex-direction: row;
+                        align-items: center;
+                        max-width: calc(100vw - 100px);
+                        gap: 16px;
+                        &.no-wrap {
+                            flex-wrap: nowrap;
+                        }
+                        .arrange-box {
+                            width: 36px;
+                            height: 36px;
+                            border: 1px solid rgba(199, 199, 199, 0.6);
+                            border-radius: 2px;
+                            background: #fff;
+                            color: #a5a7a9;
+                            display: flex;
+                            align-items: center;
+                            justify-content: center;
+                            box-sizing: border-box;
+                            position: relative;
+                            font-size: 12px;
+                            flex-shrink: 0;
+                            &.small-height {
+                                height: 20px;
+                                width: 70px;
+                                &.two-chars {
+                                    width: 35px;
+                                }
+                                &.text-4-6 {
+                                    width: 65px;
+                                }
+                                &.text-7-8 {
+                                    width: 66px;
+                                }
+                                .arrange-text {
+                                    writing-mode: vertical-lr;
+                                    white-space: pre-line;
+                                }
+                            }
+                            &.text-4-6 {
+                                width: 65px;
+                            }
+                            &.text-7-8 {
+                                width: 66px;
+                            }
+                            .arrange-text {
+                                writing-mode: horizontal-tb;
+                                line-height: 14px;
+                                text-align: center;
+                                padding-left: 3px;
+                                white-space: pre-line;
+                            }
+                            .status-icon {
+                                position: absolute;
+                                right: -10px;
+                                bottom: -10px;
+                                z-index: 3;
+                            }
+                            &::before {
+                                content: "";
+                                position: absolute;
+                                left: -4px;
+                                top: 50%;
+                                transform: translateY(-50%);
+                                width: 0;
+                                height: 0;
+                                border-top: 4px solid transparent;
+                                border-bottom: 4px solid transparent;
+                                border-right: 4px solid currentColor; /* 与文字/边框颜色一致 */
+                            }
+                        }
+                        .arrange-box.status-warning {
+                            border-color: #ff953d;
+                            color: #ff953d;
+                        }
+                        .arrange-box.status-complete {
+                            border-color: #1ca900;
+                            color: #1ca900;
+                        }
+                        .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;
+                }
+            }
+        }
+
+        // 控制区域样式
+        .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>

+ 198 - 248
src/views/old_mini/monitor/subPages/plan.vue

@@ -2,25 +2,6 @@
     <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="handleSeasonClick(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="timeline-container" ref="timelineContainerRef">
                 <div class="timeline-list" :style="getListStyle">
                     <div class="timeline-middle-line"></div>
@@ -38,30 +19,38 @@
                                 class="reproductive-item"
                                 :class="{
                                     'horizontal-text': getReproductiveItemHeight(p) < 30,
-                                    'vertical-lr-text': getReproductiveItemHeight(p) >= 30
+                                    'vertical-lr-text': getReproductiveItemHeight(p) >= 30,
                                 }"
-                                :style="getReproductiveItemHeight(p) < 30 ? { '--item-height': `${getReproductiveItemHeight(p)}px` } : {}"
+                                :style="
+                                    getReproductiveItemHeight(p) < 30
+                                        ? { '--item-height': `${getReproductiveItemHeight(p)}px` }
+                                        : {}
+                                "
                             >
                                 {{ r.name }}
-                                <div class="arranges" :class="{ 'no-wrap': getReproductiveItemHeight(p) <= 35 }">
+                                <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),
-                                            {
-                                                'small-height': getReproductiveItemHeight(p) <= 35,
-                                                'two-chars': fw.farmWorkName && fw.farmWorkName.trim().length === 2,
-                                                'text-4-6': fw.farmWorkName && getTextLengthClass(fw.farmWorkName) === 'text-4-6',
-                                                'text-7-8': fw.farmWorkName && getTextLengthClass(fw.farmWorkName) === 'text-7-8'
-                                            }
-                                        ]"
+                                        class="arrange-card"
+                                        :class="getArrangeStatusClass(fw)"
                                         @click="handleRowClick(fw)"
                                     >
-                                        <span class="arrange-text">{{ formatTextWithLineBreak(fw.farmWorkName) }}</span>
+                                        <div class="card-header">
+                                            <div class="header-left">
+                                                <span class="farm-work-name">{{ fw.farmWorkName || "农事名称" }}</span>
+                                                <span class="tag-standard">标准农事</span>
+                                            </div>
+                                            <div class="header-right">托管农事</div>
+                                        </div>
+                                        <div class="card-content">
+                                            <span>温馨提示:在某某物候期之后,请密切关注荔枝,关注蒂蛀虫的出现!</span>
+                                            <span class="edit-link" @click.stop="handleEdit(fw)">编辑</span>
+                                        </div>
+                                        <div class="card-divider"></div>
+                                        <div class="card-footer" @click.stop="handleRowClick(fw)">查看详情</div>
                                         <div
                                             v-if="
                                                 getArrangeStatusClass(fw) === 'status-complete' ||
@@ -91,16 +80,9 @@
                     </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 class="custom-bottom-fixed-btns">
+            <div class="bottom-btn primary-btn" @click="addNewTask">新增农事</div>
         </div>
     </div>
     <!-- 农事信息弹窗 -->
@@ -164,7 +146,7 @@ const getFarmWorkPlan = () => {
     if (!isInitialLoad.value && timelineContainerRef.value) {
         savedScrollTop = timelineContainerRef.value.scrollTop || 0;
     }
-    
+
     VE_API.monitor
         .farmWorkPlan({ farmId: route.query.farmId })
         .then(({ data, code }) => {
@@ -196,7 +178,7 @@ const getFarmWorkPlan = () => {
                           reproductiveList: Array.isArray(it.reproductiveList) ? it.reproductiveList : [],
                       }))
                     : [];
-                
+
                 // 等待 DOM 更新后处理滚动
                 nextTick(() => {
                     if (isInitialLoad.value) {
@@ -232,7 +214,7 @@ const addNewTask = () => {
 const triggerFarmWork = () => {
     eventBus.emit("activeUpload:show", {
         gardenIdVal: route.query.farmId,
-        problemTitleVal: '请选择您出现' + curFarmObj.value.farmWorkName + '的时间',
+        problemTitleVal: "请选择您出现" + curFarmObj.value.farmWorkName + "的时间",
         arrangeIdVal: curFarmObj.value.id,
     });
 };
@@ -245,7 +227,7 @@ const handleRowClick = (item) => {
         router.push({
             path: "/review_work",
             query: {
-                miniJson: JSON.stringify({ id: item.farmWorkRecordId,goBack: true }),
+                miniJson: JSON.stringify({ id: item.farmWorkRecordId, goBack: true }),
             },
         });
     } else if (item.flowStatus === null) {
@@ -260,6 +242,12 @@ const handleRowClick = (item) => {
     }
 };
 
+const handleEdit = (item) => {
+    // 处理编辑逻辑
+    ElMessage.info("编辑功能开发中");
+    // 可以在这里添加编辑逻辑,比如打开编辑弹窗等
+};
+
 const manageTask = () => {
     router.push({
         path: "/agri_services_manage",
@@ -288,26 +276,47 @@ const safeParseDate = (val) => {
     return NaN;
 };
 
+// 计算最小progress值(第一个节气的progress)
+const minProgress = computed(() => {
+    if (!solarTerms.value || solarTerms.value.length === 0) return 0;
+    const progresses = solarTerms.value.map((t) => Number(t?.progress) || 0).filter((p) => !isNaN(p));
+    return progresses.length > 0 ? Math.min(...progresses) : 0;
+});
+
+// 计算最大progress值
+const maxProgress = computed(() => {
+    if (!solarTerms.value || solarTerms.value.length === 0) return 100;
+    const progresses = solarTerms.value.map((t) => Number(t?.progress) || 0).filter((p) => !isNaN(p));
+    return progresses.length > 0 ? Math.max(...progresses) : 100;
+});
+
 // 计算节气列表容器高度与项位置
 const getListStyle = computed(() => {
-    const total = (solarTerms.value?.length || 0) * 100;
-    const minH = 50 + total + 50; // 上下各 50
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP); // 避免除0
+    const total = (solarTerms.value?.length || 0) * 320;
+    const minH = total; // 无上下留白
     return { minHeight: `${minH}px` };
 });
 
 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;
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP); // 避免除0
+    const total = (solarTerms.value?.length || 0) * 320;
+    // 将progress映射到0开始的位置,最小progress对应top: 0
+    const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
+    const top = (normalizedP / 100) * total;
     return {
         position: "absolute",
         top: `${top}px`,
         left: 0,
-        transform: "translateY(-50%)",
         width: "30px",
         height: "20px",
         display: "flex",
-        alignItems: "center",
+        alignItems: "flex-start",
     };
 };
 
@@ -325,8 +334,12 @@ const handleSeasonClick = (seasonValue) => {
     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 minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP);
+    const total = (solarTerms.value?.length || 0) * 320;
+    const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
+    const targetTop = (normalizedP / 100) * total; // 内容内的像素位置
     const wrap = timelineContainerRef.value;
     if (!wrap) return;
     const viewH = wrap.clientHeight || 0;
@@ -343,9 +356,38 @@ const getPhenologyBarStyle = (item) => {
     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 minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP);
+    const total = (solarTerms.value?.length || 0) * 320; // 有效绘制区高度(px)
+    // 将progress映射到0开始的位置
+    const normalizedStart = range > 0 ? ((start - minP) / range) * 100 : 0;
+    const normalizedEnd = range > 0 ? ((end - minP) / range) * 100 : 0;
+    let topPx = (normalizedStart / 100) * total;
+    let heightPx = Math.max(2, ((normalizedEnd - normalizedStart) / 100) * total);
+
+    // 计算第一个节气的位置(minProgress对应的位置,应该是0)
+    const firstTermTop = 0; // 第一个节气在top: 0
+    // 节气文字高度为46px,"小"字大约在文字的上半部分,约在15px位置
+    // 让蓝色条的顶部对齐到"小"字的位置(firstTermTop + 15px)
+    const minTop = firstTermTop + 10; // 对齐到"小"字位置(文字高度的约33%)
+    if (topPx < minTop) {
+        // 如果顶部小于最小位置,调整top和height
+        const diff = minTop - topPx;
+        topPx = minTop;
+        heightPx = Math.max(2, heightPx - diff);
+    }
+
+    // 计算最后一个节气的位置(maxProgress对应的位置)
+    const lastTermTop = (100 / 100) * total; // 因为normalizedEnd最大是100
+    // 节气文字高度为46px,"至"字大约在文字的下半部分,约在30px位置
+    // 让蓝色条的底部对齐到"至"字的位置(lastTermTop + 30px)
+    const maxBottom = lastTermTop + 35; // 对齐到"至"字位置(文字高度的约65%)
+    const barBottom = topPx + heightPx;
+    if (barBottom > maxBottom) {
+        heightPx = Math.max(2, maxBottom - topPx);
+    }
+
     const now = Date.now();
     const isFuture = Number.isFinite(item?.startTimeMs) ? item.startTimeMs > now : start > 0; // 无开始时间时按起点>0近似
     // 反转:大于当前时间用灰色,小于等于当前时间用蓝色
@@ -380,8 +422,14 @@ const getPhenologyBarHeight = (item) => {
     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;
-    const heightPx = Math.max(2, ((end - start) / 100) * total);
+    const minP = minProgress.value;
+    const maxP = maxProgress.value;
+    const range = Math.max(1, maxP - minP);
+    const total = (solarTerms.value?.length || 0) * 320;
+    // 将progress映射到0开始的位置
+    const normalizedStart = range > 0 ? ((start - minP) / range) * 100 : 0;
+    const normalizedEnd = range > 0 ? ((end - minP) / range) * 100 : 0;
+    const heightPx = Math.max(2, ((normalizedEnd - normalizedStart) / 100) * total);
     return heightPx;
 };
 
@@ -409,7 +457,6 @@ const formatTextWithLineBreak = (text) => {
     // 在左括号前添加换行符
     return text.replace(/([((])/g, "\n$1");
 };
-
 </script>
 
 <style scoped lang="scss">
@@ -418,83 +465,9 @@ const formatTextWithLineBreak = (text) => {
     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;
-                }
-            }
-        }
+        padding: 12px 0;
         .timeline-container {
-            height: calc(100vh - 93px - 40px - 73px);
+            height: calc(100vh - 40px - 73px);
             overflow: auto;
             position: relative;
             box-sizing: border-box;
@@ -505,8 +478,8 @@ const formatTextWithLineBreak = (text) => {
             .timeline-middle-line {
                 position: absolute;
                 left: 15px; /* 位于节气文字列中间(列宽约30px) */
-                top: 50px;
-                bottom: 50px;
+                top: 0;
+                bottom: 0;
                 width: 2px;
                 background: #e8e8e8;
                 z-index: 1;
@@ -516,16 +489,16 @@ const formatTextWithLineBreak = (text) => {
                 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;
-                }
+                // &::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; /* 子项等高,整体等分父高度 */
@@ -561,92 +534,106 @@ const formatTextWithLineBreak = (text) => {
                     .arranges {
                         position: absolute;
                         left: 48px; /* 列与中线右侧一段距离 */
-                        top: 50%;
-                        transform: translateY(-50%);
+                        top: 0;
                         z-index: 3;
                         display: flex;
-                        flex-wrap: wrap;
-                        flex-direction: row;
-                        align-items: center;
                         max-width: calc(100vw - 100px);
-                        gap: 16px;
-                        &.no-wrap {
-                            flex-wrap: nowrap;
-                        }
-                        .arrange-box {
-                            width: 36px;
-                            height: 36px;
-                            border: 1px solid rgba(199, 199, 199, 0.6);
-                            border-radius: 2px;
+                        gap: 12px;
+                        letter-spacing: 0px;
+                        .arrange-card {
+                            width: 94%;
+                            border: 0.5px solid #2199f8;
+                            border-radius: 8px;
                             background: #fff;
-                            color: #a5a7a9;
-                            display: flex;
-                            align-items: center;
-                            justify-content: center;
                             box-sizing: border-box;
                             position: relative;
-                            font-size: 12px;
-                            flex-shrink: 0;
-                            &.small-height {
-                                height: 20px;
-                                width: 70px;
-                                &.two-chars {
-                                    width: 35px;
+                            padding: 8px;
+                            writing-mode: horizontal-tb;
+                            .card-header {
+                                display: flex;
+                                justify-content: space-between;
+                                align-items: center;
+                                .header-left {
+                                    display: flex;
+                                    align-items: center;
+                                    gap: 8px;
+                                    .farm-work-name {
+                                        font-size: 14px;
+                                        font-weight: 500;
+                                        color: #1d2129;
+                                    }
+                                    .tag-standard {
+                                        padding: 0 8px;
+                                        background: rgba(119, 119, 119, 0.1);
+                                        border-radius: 25px;
+                                        font-weight: 400;
+                                        font-size: 11px;
+                                        color: #000;
+                                    }
                                 }
-                                &.text-4-6 {
-                                    width: 65px;
-                                }
-                                &.text-7-8 {
-                                    width: 66px;
-                                }
-                                .arrange-text {
-                                    writing-mode: vertical-lr;
-                                    white-space: pre-line;
+                                .header-right {
+                                    font-size: 11px;
+                                    color: #2199f8;
+                                    padding: 0 8px;
+                                    border-radius: 25px;
+                                    // background: rgba(33, 153, 248, 0.1);
                                 }
                             }
-                            &.text-4-6 {
-                                width: 65px;
+                            .card-content {
+                                color: #909090;
+                                text-align: left;
+                                line-height: 1.55;
+                                margin: 4px 0 10px 0;
+                                .edit-link {
+                                    color: #2199f8;
+                                }
                             }
-                            &.text-7-8 {
-                                width: 66px;
+                            .card-divider {
+                                height: 0.5px;
+                                background: rgba(0, 0, 0, 0.1);
                             }
-                            .arrange-text {
-                                writing-mode: horizontal-tb;
-                                line-height: 14px;
+                            .card-footer {
                                 text-align: center;
-                                padding-left: 3px;
-                                white-space: pre-line;
+                                font-size: 12px;
+                                color: #adadad;
+                                padding-top: 6px;
                             }
                             .status-icon {
                                 position: absolute;
-                                right: -10px;
-                                bottom: -10px;
+                                right: -8px;
+                                bottom: -8px;
                                 z-index: 3;
                             }
                             &::before {
                                 content: "";
                                 position: absolute;
-                                left: -4px;
+                                left: -6px;
                                 top: 50%;
                                 transform: translateY(-50%);
                                 width: 0;
                                 height: 0;
-                                border-top: 4px solid transparent;
-                                border-bottom: 4px solid transparent;
-                                border-right: 4px solid currentColor; /* 与文字/边框颜色一致 */
+                                border-top: 5px solid transparent;
+                                border-bottom: 5px solid transparent;
+                                border-right: 5px solid #2199f8;
                             }
                         }
-                        .arrange-box.status-warning {
+                        .arrange-card.status-warning {
                             border-color: #ff953d;
-                            color: #ff953d;
+                            &::before {
+                                border-right-color: #ff953d;
+                            }
                         }
-                        .arrange-box.status-complete {
+                        .arrange-card.status-complete {
                             border-color: #1ca900;
-                            color: #1ca900;
+                            &::before {
+                                border-right-color: #1ca900;
+                            }
                         }
-                        .arrange-box.status-normal {
+                        .arrange-card.status-normal {
                             border-color: #2199f8;
-                            color: #2199f8;
+                            &::before {
+                                border-right-color: #2199f8;
+                            }
                         }
                     }
                 }
@@ -662,7 +649,7 @@ const formatTextWithLineBreak = (text) => {
                 width: 30px;
                 padding-right: 16px;
                 display: flex;
-                align-items: center;
+                align-items: flex-start;
                 z-index: 2; /* 置于中线之上 */
                 .term-name {
                     display: inline-block;
@@ -675,53 +662,16 @@ const formatTextWithLineBreak = (text) => {
                     writing-mode: vertical-rl;
                     text-orientation: upright;
                     color: rgba(174, 174, 174, 0.6);
-                    letter-spacing: 2px;
                     text-align: center;
                 }
             }
         }
-
-        // 控制区域样式
-        .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;
-                }
-            }
+    }
+    // 控制区域样式
+    .custom-bottom-fixed-btns {
+        justify-content: center;
+        .primary-btn {
+            padding: 10px 34px;
         }
     }
 }