|
@@ -1,731 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <div
|
|
|
|
|
- class="timeline-container"
|
|
|
|
|
- ref="timelineContainerRef"
|
|
|
|
|
- :class="{ 'timeline-container-plant': pageType === 'plant' }"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="timeline-list" :style="getListStyle">
|
|
|
|
|
- <div class="timeline-middle-line"></div>
|
|
|
|
|
- <!-- 物候期覆盖条(progress 为起点,progress2 为终点,单位 %) -->
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="(p, idx) in phenologyList"
|
|
|
|
|
- :key="p.id ?? idx"
|
|
|
|
|
- class="phenology-bar"
|
|
|
|
|
- :style="getPhenologyBarStyle(p)"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="reproductive-list">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="(r, rIdx) in Array.isArray(p.reproductiveList) ? p.reproductiveList : []"
|
|
|
|
|
- :key="r.id ?? rIdx"
|
|
|
|
|
- class="reproductive-item"
|
|
|
|
|
- :class="{
|
|
|
|
|
- 'horizontal-text': getReproductiveItemHeight(p) < 30,
|
|
|
|
|
- 'vertical-lr-text': getReproductiveItemHeight(p) >= 30,
|
|
|
|
|
- }"
|
|
|
|
|
- :style="
|
|
|
|
|
- getReproductiveItemHeight(p) < 30
|
|
|
|
|
- ? { '--item-height': `${getReproductiveItemHeight(p)}px` }
|
|
|
|
|
- : {}
|
|
|
|
|
- "
|
|
|
|
|
- >
|
|
|
|
|
- {{ r.name }}
|
|
|
|
|
- <div class="arranges">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="(fw, aIdx) in Array.isArray(r.farmWorkArrangeList) ? r.farmWorkArrangeList : []"
|
|
|
|
|
- :key="fw.id ?? aIdx"
|
|
|
|
|
- class="arrange-card"
|
|
|
|
|
- :class="getArrangeStatusClass(fw)"
|
|
|
|
|
- @click="handleRowClick(fw)"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="card-header">
|
|
|
|
|
- <div class="header-left">
|
|
|
|
|
- <span class="farm-work-name">{{ fw.farmWorkName || "--" }}</span>
|
|
|
|
|
- <span class="tag-standard">标准农事</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="header-right">
|
|
|
|
|
- {{ fw.isFollow == 1 ? "已关注" : fw.isFollow == 2 ? "托管农事" : "" }}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card-content">
|
|
|
|
|
- <span>{{ fw.interactionQuestion || "暂无提示" }}</span>
|
|
|
|
|
- <span v-if="!disableClick" class="edit-link" @click.stop="handleEdit(fw)"
|
|
|
|
|
- >点击编辑</span
|
|
|
|
|
- >
|
|
|
|
|
- </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>
|
|
|
|
|
- <!-- 互动设置弹窗 -->
|
|
|
|
|
- <interact-popup ref="interactPopupRef" @handleSaveSuccess="getFarmWorkPlan"></interact-popup>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup>
|
|
|
|
|
-import { ref, computed, nextTick, watch } from "vue";
|
|
|
|
|
-import interactPopup from "@/components/popup/interactPopup.vue";
|
|
|
|
|
-import { ElMessage } from "element-plus";
|
|
|
|
|
-
|
|
|
|
|
-const props = defineProps({
|
|
|
|
|
- // 农场 ID,用于请求农事规划数据
|
|
|
|
|
- farmId: {
|
|
|
|
|
- type: [String, Number],
|
|
|
|
|
- default: null,
|
|
|
|
|
- },
|
|
|
|
|
- // 页面类型:种植方案 / 农事规划,用来控制高度样式
|
|
|
|
|
- pageType: {
|
|
|
|
|
- type: String,
|
|
|
|
|
- default: "",
|
|
|
|
|
- },
|
|
|
|
|
- // 是否禁用所有点击事件(用于只读展示)
|
|
|
|
|
- disableClick: {
|
|
|
|
|
- type: Boolean,
|
|
|
|
|
- default: false,
|
|
|
|
|
- },
|
|
|
|
|
- containerId: {
|
|
|
|
|
- type: [Number, String],
|
|
|
|
|
- default: null,
|
|
|
|
|
- },
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const emits = defineEmits(["row-click"]);
|
|
|
|
|
-
|
|
|
|
|
-const solarTerms = ref([]);
|
|
|
|
|
-const phenologyList = ref([]);
|
|
|
|
|
-const timelineContainerRef = ref(null);
|
|
|
|
|
-// 标记是否为首次加载
|
|
|
|
|
-const isInitialLoad = ref(true);
|
|
|
|
|
-
|
|
|
|
|
-// 获取当前季节
|
|
|
|
|
-const getCurrentSeason = () => {
|
|
|
|
|
- const month = new Date().getMonth() + 1; // 1-12
|
|
|
|
|
- if (month >= 3 && month <= 5) {
|
|
|
|
|
- return "spring"; // 春季:3-5月
|
|
|
|
|
- } else if (month >= 6 && month <= 8) {
|
|
|
|
|
- return "summer"; // 夏季:6-8月
|
|
|
|
|
- } else if (month >= 9 && month <= 10) {
|
|
|
|
|
- return "autumn"; // 秋季:9-10月
|
|
|
|
|
- } else {
|
|
|
|
|
- return "winter"; // 冬季:11-2月
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 安全解析时间到时间戳(ms)
|
|
|
|
|
-const safeParseDate = (val) => {
|
|
|
|
|
- if (!val) return NaN;
|
|
|
|
|
- if (val instanceof Date) return val.getTime();
|
|
|
|
|
- if (typeof val === "number") return val;
|
|
|
|
|
- if (typeof val === "string") {
|
|
|
|
|
- // 兼容 "YYYY-MM-DD HH:mm:ss" -> Safari
|
|
|
|
|
- const s = val.replace(/-/g, "/").replace("T", " ");
|
|
|
|
|
- const d = new Date(s);
|
|
|
|
|
- return isNaN(d.getTime()) ? NaN : d.getTime();
|
|
|
|
|
- }
|
|
|
|
|
- return NaN;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 计算最小progress值(第一个节气的progress)
|
|
|
|
|
-const minProgress = computed(() => {
|
|
|
|
|
- if (!solarTerms.value || solarTerms.value.length === 0) return 0;
|
|
|
|
|
- const progresses = solarTerms.value.map((t) => Number(t?.progress) || 0).filter((p) => !isNaN(p));
|
|
|
|
|
- return progresses.length > 0 ? Math.min(...progresses) : 0;
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 计算最大progress值
|
|
|
|
|
-const maxProgress = computed(() => {
|
|
|
|
|
- if (!solarTerms.value || solarTerms.value.length === 0) return 100;
|
|
|
|
|
- const progresses = solarTerms.value.map((t) => Number(t?.progress) || 0).filter((p) => !isNaN(p));
|
|
|
|
|
- return progresses.length > 0 ? Math.max(...progresses) : 100;
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 计算所有节气的调整位置,确保相邻节气之间至少有36px间隔
|
|
|
|
|
-const adjustedTermPositions = computed(() => {
|
|
|
|
|
- if (!solarTerms.value || solarTerms.value.length === 0) return new Map();
|
|
|
|
|
-
|
|
|
|
|
- const minP = minProgress.value;
|
|
|
|
|
- const maxP = maxProgress.value;
|
|
|
|
|
- const range = Math.max(1, maxP - minP);
|
|
|
|
|
- const total = calculateTotalHeightByFarmWorks();
|
|
|
|
|
- const termHeight = 46;
|
|
|
|
|
- const minSpacing = 36; // 最小间隔36px
|
|
|
|
|
-
|
|
|
|
|
- // 计算所有节气的初始位置
|
|
|
|
|
- const termsWithPositions = solarTerms.value.map((term) => {
|
|
|
|
|
- const p = Math.max(0, Math.min(100, Number(term?.progress) || 0));
|
|
|
|
|
- const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
|
|
|
|
|
- const originalTop = (normalizedP / 100) * total;
|
|
|
|
|
- return {
|
|
|
|
|
- ...term,
|
|
|
|
|
- id:
|
|
|
|
|
- term.id ??
|
|
|
|
|
- term.solarTermsId ??
|
|
|
|
|
- term.termId ??
|
|
|
|
|
- `${term.name || term.solarTermsName || term.termName || "term"}-${term.createDate || ""}`,
|
|
|
|
|
- progress: p,
|
|
|
|
|
- originalTop,
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 按原始位置排序
|
|
|
|
|
- const sortedTerms = [...termsWithPositions].sort((a, b) => a.originalTop - b.originalTop);
|
|
|
|
|
-
|
|
|
|
|
- // 调整位置,确保相邻节气之间至少有36px间隔
|
|
|
|
|
- const adjustedPositions = new Map();
|
|
|
|
|
- const termPositions = new Array(sortedTerms.length);
|
|
|
|
|
-
|
|
|
|
|
- // 先确定最后一个节气的位置(与物候期底部对齐)
|
|
|
|
|
- const lastIndex = sortedTerms.length - 1;
|
|
|
|
|
- const lastTerm = sortedTerms[lastIndex];
|
|
|
|
|
- if (lastTerm.progress === maxP && range > 0) {
|
|
|
|
|
- termPositions[lastIndex] = total - termHeight;
|
|
|
|
|
- } else {
|
|
|
|
|
- termPositions[lastIndex] = lastTerm.originalTop;
|
|
|
|
|
- }
|
|
|
|
|
- adjustedPositions.set(lastTerm.id, termPositions[lastIndex]);
|
|
|
|
|
-
|
|
|
|
|
- // 从后往前调整,确保相邻节气之间至少有36px间隔
|
|
|
|
|
- for (let i = lastIndex - 1; i >= 0; i--) {
|
|
|
|
|
- const term = sortedTerms[i];
|
|
|
|
|
- let adjustedTop = term.originalTop;
|
|
|
|
|
-
|
|
|
|
|
- // 检查与后一个节气的间隔
|
|
|
|
|
- const nextTop = termPositions[i + 1];
|
|
|
|
|
- if (nextTop - adjustedTop < minSpacing) {
|
|
|
|
|
- adjustedTop = nextTop - minSpacing;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- termPositions[i] = adjustedTop;
|
|
|
|
|
- adjustedPositions.set(term.id, adjustedTop);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return adjustedPositions;
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 计算物候期需要的实际高度(基于农事数量)
|
|
|
|
|
-const getPhenologyRequiredHeight = (item) => {
|
|
|
|
|
- // 统计该物候期内的农事数量
|
|
|
|
|
- let farmWorkCount = 0;
|
|
|
|
|
-
|
|
|
|
|
- if (Array.isArray(item.reproductiveList)) {
|
|
|
|
|
- item.reproductiveList.forEach((reproductive) => {
|
|
|
|
|
- if (Array.isArray(reproductive.farmWorkArrangeList)) {
|
|
|
|
|
- farmWorkCount += reproductive.farmWorkArrangeList.length;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 如果没有农事,给一个最小高度
|
|
|
|
|
- if (farmWorkCount === 0) {
|
|
|
|
|
- return 50; // 最小50px
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 每个农事卡片的高度(根据实际内容,卡片高度可能因内容而异)
|
|
|
|
|
- // 卡片包含:padding(8px*2) + header(约25px) + content margin(4px+2px) + content(约25-30px) = 约72-77px
|
|
|
|
|
- // 考虑到内容可能换行,实际高度可能更高,设置为120px更安全,避免卡片重叠
|
|
|
|
|
- const farmWorkCardHeight = 120; // 卡片高度估算,确保能容纳内容且不重叠
|
|
|
|
|
- // 卡片之间的间距(与CSS中的gap保持一致)
|
|
|
|
|
- const cardGap = 12;
|
|
|
|
|
-
|
|
|
|
|
- // 计算总高度:卡片数量 * 卡片高度 + (卡片数量 - 1) * 间距
|
|
|
|
|
- // 如果有多个卡片,需要加上它们之间的间距
|
|
|
|
|
- const totalHeight = farmWorkCount * farmWorkCardHeight + (farmWorkCount > 1 ? (farmWorkCount - 1) * cardGap : 0);
|
|
|
|
|
-
|
|
|
|
|
- // 返回精确的总高度,只保留最小高度限制,不添加额外余量
|
|
|
|
|
- return Math.max(totalHeight, 50); // 最小50px,精确匹配农事卡片高度
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 计算所有物候期的累积位置和总高度
|
|
|
|
|
-const calculatePhenologyPositions = () => {
|
|
|
|
|
- let currentTop = 10; // 起始位置,留出顶部间距
|
|
|
|
|
- const positions = new Map();
|
|
|
|
|
-
|
|
|
|
|
- // 按progress排序物候期,确保按时间顺序排列
|
|
|
|
|
- const sortedPhenologyList = [...phenologyList.value].sort((a, b) => {
|
|
|
|
|
- const aProgress = Math.min(Number(a?.progress) || 0, Number(a?.progress2) || 0);
|
|
|
|
|
- const bProgress = Math.min(Number(b?.progress) || 0, Number(b?.progress2) || 0);
|
|
|
|
|
- return aProgress - bProgress;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- sortedPhenologyList.forEach((phenology) => {
|
|
|
|
|
- const height = getPhenologyRequiredHeight(phenology);
|
|
|
|
|
- // 使用与数据生成时相同的ID生成逻辑
|
|
|
|
|
- const itemId =
|
|
|
|
|
- phenology.id ?? phenology.phenologyId ?? phenology.name ?? `${phenology.progress}-${phenology.progress2}`;
|
|
|
|
|
- positions.set(itemId, {
|
|
|
|
|
- top: currentTop,
|
|
|
|
|
- height: height,
|
|
|
|
|
- });
|
|
|
|
|
- currentTop += height; // 紧挨着下一个物候期,不留间距
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- positions,
|
|
|
|
|
- totalHeight: currentTop, // 总高度 = 最后一个物候期的底部位置,不添加额外间距
|
|
|
|
|
- };
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 计算所有农事的总高度(基于物候期紧挨排列)
|
|
|
|
|
-const calculateTotalHeightByFarmWorks = () => {
|
|
|
|
|
- const { totalHeight } = calculatePhenologyPositions();
|
|
|
|
|
-
|
|
|
|
|
- // 计算最后一个物候期的底部位置
|
|
|
|
|
- let lastPhenologyBottom = 0;
|
|
|
|
|
- if (phenologyList.value && phenologyList.value.length > 0) {
|
|
|
|
|
- const sortedPhenologyList = [...phenologyList.value].sort((a, b) => {
|
|
|
|
|
- const aProgress = Math.min(Number(a?.progress) || 0, Number(a?.progress2) || 0);
|
|
|
|
|
- const bProgress = Math.min(Number(b?.progress) || 0, Number(b?.progress2) || 0);
|
|
|
|
|
- return aProgress - bProgress;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- let currentTop = 10; // 起始位置
|
|
|
|
|
- sortedPhenologyList.forEach((phenology) => {
|
|
|
|
|
- const height = getPhenologyRequiredHeight(phenology);
|
|
|
|
|
- currentTop += height;
|
|
|
|
|
- });
|
|
|
|
|
- lastPhenologyBottom = currentTop; // 最后一个物候期的底部位置
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 直接使用最后一个物候期的底部位置作为总高度,不添加额外余量
|
|
|
|
|
- // 在getTermStyle中,我们已经调整了最后一个节气的top位置(total - 46)
|
|
|
|
|
- // 这样最后一个节气的底部(total - 46 + 46 = total)就能与物候期底部对齐
|
|
|
|
|
- if (lastPhenologyBottom > 0) {
|
|
|
|
|
- const baseHeight = (solarTerms.value?.length || 0) * 50;
|
|
|
|
|
- // 总高度 = 最后一个物候期的底部位置,精确匹配,不添加额外余量
|
|
|
|
|
- return Math.max(lastPhenologyBottom, totalHeight, baseHeight);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 基础高度:每个节气至少需要一定高度,确保节气标签能显示
|
|
|
|
|
- const baseHeight = (solarTerms.value?.length || 0) * 50;
|
|
|
|
|
-
|
|
|
|
|
- // 返回物候期总高度和基础高度的较大值,不添加最小高度限制
|
|
|
|
|
- return Math.max(totalHeight, baseHeight);
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 列表高度
|
|
|
|
|
-const getListStyle = computed(() => {
|
|
|
|
|
- const minP = minProgress.value;
|
|
|
|
|
- const maxP = maxProgress.value;
|
|
|
|
|
- const range = Math.max(1, maxP - minP); // 避免除0
|
|
|
|
|
- const total = calculateTotalHeightByFarmWorks();
|
|
|
|
|
- const minH = range === 0 ? 0 : total;
|
|
|
|
|
- return { minHeight: `${minH}px` };
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const getTermStyle = (t) => {
|
|
|
|
|
- // 生成与 adjustedTermPositions 中相同的 ID
|
|
|
|
|
- const termId =
|
|
|
|
|
- t.id ??
|
|
|
|
|
- t.solarTermsId ??
|
|
|
|
|
- t.termId ??
|
|
|
|
|
- `${t.name || t.solarTermsName || t.termName || "term"}-${t.createDate || ""}`;
|
|
|
|
|
-
|
|
|
|
|
- // 从调整后的位置映射中获取 top 值
|
|
|
|
|
- const adjustedTop = adjustedTermPositions.value.get(termId);
|
|
|
|
|
-
|
|
|
|
|
- // 如果找不到调整后的位置,使用原始计算方式作为后备
|
|
|
|
|
- let top = adjustedTop;
|
|
|
|
|
- if (adjustedTop === undefined) {
|
|
|
|
|
- const p = Math.max(0, Math.min(100, Number(t?.progress) || 0));
|
|
|
|
|
- const minP = minProgress.value;
|
|
|
|
|
- const maxP = maxProgress.value;
|
|
|
|
|
- const range = Math.max(1, maxP - minP);
|
|
|
|
|
- const total = calculateTotalHeightByFarmWorks();
|
|
|
|
|
- const termHeight = 46;
|
|
|
|
|
- const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
|
|
|
|
|
- top = (normalizedP / 100) * total;
|
|
|
|
|
-
|
|
|
|
|
- // 如果是最后一个节气,调整top位置
|
|
|
|
|
- if (p === maxP && range > 0) {
|
|
|
|
|
- top = total - termHeight;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- position: "absolute",
|
|
|
|
|
- top: `${top}px`,
|
|
|
|
|
- left: 0,
|
|
|
|
|
- width: "30px",
|
|
|
|
|
- // height: "20px",
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- alignItems: "flex-start",
|
|
|
|
|
- };
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 点击季节 → 滚动到对应节气(立春/立夏/立秋/立冬)
|
|
|
|
|
-const handleSeasonClick = (seasonValue) => {
|
|
|
|
|
- const mapping = {
|
|
|
|
|
- spring: "立春",
|
|
|
|
|
- summer: "立夏",
|
|
|
|
|
- autumn: "立秋",
|
|
|
|
|
- winter: "立冬",
|
|
|
|
|
- };
|
|
|
|
|
- const targetName = mapping[seasonValue];
|
|
|
|
|
- if (!targetName) return;
|
|
|
|
|
- const target = (solarTerms.value || []).find((t) => (t?.displayName || "") === targetName);
|
|
|
|
|
- if (!target) return;
|
|
|
|
|
- const p = Math.max(0, Math.min(100, Number(target.progress) || 0));
|
|
|
|
|
- const minP = minProgress.value;
|
|
|
|
|
- const maxP = maxProgress.value;
|
|
|
|
|
- const range = Math.max(1, maxP - minP);
|
|
|
|
|
- const total = calculateTotalHeightByFarmWorks(); // 使用动态计算的总高度
|
|
|
|
|
- const normalizedP = range > 0 ? ((p - minP) / range) * 100 : 0;
|
|
|
|
|
- const targetTop = (normalizedP / 100) * total; // 内容内的像素位置
|
|
|
|
|
- const wrap = timelineContainerRef.value;
|
|
|
|
|
- if (!wrap) return;
|
|
|
|
|
- const viewH = wrap.clientHeight || 0;
|
|
|
|
|
- const maxScroll = Math.max(0, wrap.scrollHeight - viewH);
|
|
|
|
|
- // 将目标位置稍微靠上(使用 0.35 视口高度做偏移)
|
|
|
|
|
- let scrollTop = Math.max(0, targetTop - viewH * 0.1);
|
|
|
|
|
- if (scrollTop > maxScroll) scrollTop = maxScroll;
|
|
|
|
|
- wrap.scrollTo({ top: scrollTop, behavior: "smooth" });
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 物候期覆盖条样式
|
|
|
|
|
-const getPhenologyBarStyle = (item) => {
|
|
|
|
|
- const { positions } = calculatePhenologyPositions();
|
|
|
|
|
- // 使用与数据生成时相同的ID生成逻辑
|
|
|
|
|
- const itemId = item.id ?? item.phenologyId ?? item.name ?? `${item.progress}-${item.progress2}`;
|
|
|
|
|
- const position = positions.get(itemId);
|
|
|
|
|
-
|
|
|
|
|
- // 如果找不到位置信息,使用默认值
|
|
|
|
|
- let topPx = 10;
|
|
|
|
|
- let heightPx = 50;
|
|
|
|
|
-
|
|
|
|
|
- if (position) {
|
|
|
|
|
- topPx = position.top;
|
|
|
|
|
- heightPx = position.height;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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 now = Date.now();
|
|
|
|
|
- const isFuture = Number.isFinite(item?.startTimeMs) ? item.startTimeMs > now : start > 0;
|
|
|
|
|
- const barColor = isFuture ? "rgba(145, 145, 145, 0.1)" : "#2199F8";
|
|
|
|
|
- const beforeBg = isFuture ? "rgba(145, 145, 145, 0.1)" : "rgba(33, 153, 248, 0.1)";
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- position: "absolute",
|
|
|
|
|
- left: "46px",
|
|
|
|
|
- width: "25px",
|
|
|
|
|
- top: `${topPx}px`,
|
|
|
|
|
- height: `${heightPx}px`,
|
|
|
|
|
- background: barColor,
|
|
|
|
|
- color: isFuture ? "#747778" : "#fff",
|
|
|
|
|
- "--bar-before-bg": beforeBg,
|
|
|
|
|
- zIndex: 2,
|
|
|
|
|
- };
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 农事状态样式映射(0:默认,1-4:正常,5:完成,6:预警)
|
|
|
|
|
-const getArrangeStatusClass = (fw) => {
|
|
|
|
|
- const t = fw?.isFollow;
|
|
|
|
|
- if (t == 0) return "normal-style";
|
|
|
|
|
- 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) => {
|
|
|
|
|
- // 直接使用基于农事数量的高度
|
|
|
|
|
- return getPhenologyRequiredHeight(item);
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 计算 reproductive-item 的高度(px)
|
|
|
|
|
-const getReproductiveItemHeight = (phenologyItem) => {
|
|
|
|
|
- const barHeight = getPhenologyBarHeight(phenologyItem);
|
|
|
|
|
- const listLength = Array.isArray(phenologyItem?.reproductiveList) ? phenologyItem.reproductiveList.length : 1;
|
|
|
|
|
- return listLength > 0 ? barHeight / listLength : barHeight;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const handleRowClick = (item) => {
|
|
|
|
|
- emits("row-click", item);
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const interactPopupRef = ref(null);
|
|
|
|
|
-const handleEdit = (item) => {
|
|
|
|
|
- if (props.disableClick) return;
|
|
|
|
|
- if (interactPopupRef.value) {
|
|
|
|
|
- interactPopupRef.value.showPopup(item);
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const containerIdData = ref(null);
|
|
|
|
|
-
|
|
|
|
|
-// 获取农事规划数据
|
|
|
|
|
-const getFarmWorkPlan = () => {
|
|
|
|
|
- if (!props.farmId && !props.containerId) return;
|
|
|
|
|
- let savedScrollTop = 0;
|
|
|
|
|
- if (!isInitialLoad.value && timelineContainerRef.value) {
|
|
|
|
|
- savedScrollTop = timelineContainerRef.value.scrollTop || 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- VE_API.monitor
|
|
|
|
|
- .farmWorkPlan({ farmId: props.farmId, containerId: props.containerId })
|
|
|
|
|
- .then(({ data, code }) => {
|
|
|
|
|
- if (code === 0) {
|
|
|
|
|
- containerIdData.value = data.phenologyList[0].containerSpaceTimeId;
|
|
|
|
|
- const list = Array.isArray(data?.solarTermsList) ? data.solarTermsList : [];
|
|
|
|
|
- const filtered = list
|
|
|
|
|
- .filter((t) => t && t.type === 1)
|
|
|
|
|
- .map((t) => ({
|
|
|
|
|
- id:
|
|
|
|
|
- t.id ??
|
|
|
|
|
- t.solarTermsId ??
|
|
|
|
|
- t.termId ??
|
|
|
|
|
- `${t.name || t.solarTermsName || t.termName || "term"}-${t.createDate || ""}`,
|
|
|
|
|
- displayName: t.name || t.solarTermsName || t.termName || "节气",
|
|
|
|
|
- createDate: t.createDate || null,
|
|
|
|
|
- progress: Number(t.progress) || 0,
|
|
|
|
|
- }));
|
|
|
|
|
- solarTerms.value = filtered;
|
|
|
|
|
- // 物候期数据
|
|
|
|
|
- phenologyList.value = Array.isArray(data?.phenologyList)
|
|
|
|
|
- ? data.phenologyList.map((it) => {
|
|
|
|
|
- const reproductiveList = Array.isArray(it.reproductiveList)
|
|
|
|
|
- ? it.reproductiveList.map((r) => {
|
|
|
|
|
- const farmWorkArrangeList = Array.isArray(r.farmWorkArrangeList)
|
|
|
|
|
- ? r.farmWorkArrangeList.map((fw) => ({
|
|
|
|
|
- ...fw,
|
|
|
|
|
- containerSpaceTimeId: it.containerSpaceTimeId,
|
|
|
|
|
- }))
|
|
|
|
|
- : [];
|
|
|
|
|
- return {
|
|
|
|
|
- ...r,
|
|
|
|
|
- farmWorkArrangeList,
|
|
|
|
|
- };
|
|
|
|
|
- })
|
|
|
|
|
- : [];
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- id: it.id ?? it.phenologyId ?? it.name ?? `${it.progress}-${it.progress2}`,
|
|
|
|
|
- progress: Number(it.progress) || 0, // 起点 %
|
|
|
|
|
- progress2: Number(it.progress2) || 0, // 终点 %
|
|
|
|
|
- startTimeMs: safeParseDate(
|
|
|
|
|
- it.startDate || it.beginDate || it.startTime || it.start || it.start_at
|
|
|
|
|
- ),
|
|
|
|
|
- reproductiveList,
|
|
|
|
|
- };
|
|
|
|
|
- })
|
|
|
|
|
- : [];
|
|
|
|
|
-
|
|
|
|
|
- nextTick(() => {
|
|
|
|
|
- if (isInitialLoad.value) {
|
|
|
|
|
- const currentSeason = getCurrentSeason();
|
|
|
|
|
- handleSeasonClick(currentSeason);
|
|
|
|
|
- isInitialLoad.value = false;
|
|
|
|
|
- } else if (timelineContainerRef.value && savedScrollTop > 0) {
|
|
|
|
|
- timelineContainerRef.value.scrollTop = savedScrollTop;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- .catch((error) => {
|
|
|
|
|
- console.error("获取农事规划数据失败:", error);
|
|
|
|
|
- ElMessage.error("获取农事规划数据失败");
|
|
|
|
|
- });
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-watch(
|
|
|
|
|
- () => props.farmId || props.containerId,
|
|
|
|
|
- (val) => {
|
|
|
|
|
- if (val) {
|
|
|
|
|
- isInitialLoad.value = true;
|
|
|
|
|
- getFarmWorkPlan();
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- { immediate: true }
|
|
|
|
|
-);
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped lang="scss">
|
|
|
|
|
-.timeline-container {
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- overflow: auto;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- .timeline-list {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- }
|
|
|
|
|
- .timeline-middle-line {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- left: 15px; /* 位于节气文字列中间(列宽约30px) */
|
|
|
|
|
- top: 0;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- width: 2px;
|
|
|
|
|
- background: #e8e8e8;
|
|
|
|
|
- z-index: 1;
|
|
|
|
|
- }
|
|
|
|
|
- .phenology-bar {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: stretch;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- .reproductive-list {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-auto-rows: 1fr; /* 子项等高,整体等分父高度 */
|
|
|
|
|
- align-items: stretch;
|
|
|
|
|
- justify-items: center; /* 子项居中 */
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- }
|
|
|
|
|
- .reproductive-item {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- word-break: break-all;
|
|
|
|
|
- writing-mode: vertical-rl;
|
|
|
|
|
- text-orientation: upright;
|
|
|
|
|
- letter-spacing: 3px;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- line-height: 23px;
|
|
|
|
|
- color: inherit;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- &.horizontal-text {
|
|
|
|
|
- writing-mode: horizontal-tb;
|
|
|
|
|
- text-orientation: mixed;
|
|
|
|
|
- letter-spacing: normal;
|
|
|
|
|
- line-height: calc(var(--item-height, 15px) - 3px);
|
|
|
|
|
- }
|
|
|
|
|
- &.vertical-lr-text {
|
|
|
|
|
- writing-mode: vertical-lr;
|
|
|
|
|
- text-orientation: upright;
|
|
|
|
|
- letter-spacing: 3px;
|
|
|
|
|
- line-height: 26px;
|
|
|
|
|
- }
|
|
|
|
|
- .arranges {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- left: 40px; /* 列与中线右侧一段距离 */
|
|
|
|
|
- top: 0;
|
|
|
|
|
- z-index: 3;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- max-width: calc(100vw - 100px);
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
- letter-spacing: 0px;
|
|
|
|
|
- .arrange-card {
|
|
|
|
|
- width: 97%;
|
|
|
|
|
- border: 0.5px solid #2199f8;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- padding: 8px;
|
|
|
|
|
- writing-mode: horizontal-tb;
|
|
|
|
|
- .card-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- .header-left {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- .farm-work-name {
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- color: #1d2129;
|
|
|
|
|
- }
|
|
|
|
|
- .tag-standard {
|
|
|
|
|
- padding: 0 8px;
|
|
|
|
|
- background: rgba(119, 119, 119, 0.1);
|
|
|
|
|
- border-radius: 25px;
|
|
|
|
|
- font-weight: 400;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #000;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .header-right {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #808080;
|
|
|
|
|
- padding: 0 8px;
|
|
|
|
|
- border-radius: 25px;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .card-content {
|
|
|
|
|
- color: #909090;
|
|
|
|
|
- text-align: left;
|
|
|
|
|
- line-height: 1.55;
|
|
|
|
|
- margin: 4px 0 2px 0;
|
|
|
|
|
- .edit-link {
|
|
|
|
|
- color: #2199f8;
|
|
|
|
|
- margin-left: 5px;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .status-icon {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- right: -8px;
|
|
|
|
|
- bottom: -8px;
|
|
|
|
|
- z-index: 3;
|
|
|
|
|
- }
|
|
|
|
|
- &::before {
|
|
|
|
|
- content: "";
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- left: -6px;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- transform: translateY(-50%);
|
|
|
|
|
- width: 0;
|
|
|
|
|
- height: 0;
|
|
|
|
|
- border-top: 5px solid transparent;
|
|
|
|
|
- border-bottom: 5px solid transparent;
|
|
|
|
|
- border-right: 5px solid #2199f8;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .arrange-card.normal-style {
|
|
|
|
|
- opacity: 0.4;
|
|
|
|
|
- }
|
|
|
|
|
- .arrange-card.status-warning {
|
|
|
|
|
- border-color: #ff953d;
|
|
|
|
|
- &::before {
|
|
|
|
|
- border-right-color: #ff953d;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .arrange-card.status-complete {
|
|
|
|
|
- border-color: #1ca900;
|
|
|
|
|
- &::before {
|
|
|
|
|
- border-right-color: #1ca900;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .arrange-card.status-normal {
|
|
|
|
|
- border-color: #2199f8;
|
|
|
|
|
- &::before {
|
|
|
|
|
- border-right-color: #2199f8;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .reproductive-item + .reproductive-item {
|
|
|
|
|
- border-top: 2px solid #fff;
|
|
|
|
|
- }
|
|
|
|
|
- .phenology-bar + .phenology-bar {
|
|
|
|
|
- border-top: 2px solid #fff;
|
|
|
|
|
- }
|
|
|
|
|
- .timeline-term {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- width: 30px;
|
|
|
|
|
- padding-right: 16px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: flex-start;
|
|
|
|
|
- z-index: 2; /* 置于中线之上 */
|
|
|
|
|
- .term-name {
|
|
|
|
|
- display: inline-block;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 46px;
|
|
|
|
|
- line-height: 30px;
|
|
|
|
|
- background: #f5f7fb;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- word-break: break-all;
|
|
|
|
|
- writing-mode: vertical-rl;
|
|
|
|
|
- text-orientation: upright;
|
|
|
|
|
- color: rgba(174, 174, 174, 0.6);
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|