|
@@ -0,0 +1,1731 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="timeline-container" ref="timelineContainerRef">
|
|
|
|
|
+ <div class="timeline-list" ref="timelineListRef">
|
|
|
|
|
+ <empty v-if="isEmpty" image="https://birdseye-img.sysuimars.com/birdseye-look-mini/custom-empty-image.png"
|
|
|
|
|
+ image-size="80" description="暂无数据" class="empty-state" />
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <!-- <div class="timeline-middle-line"></div>
|
|
|
|
|
+ <div v-for="(t, tIdx) in phenologyStartDates" :key="`term-${uniqueTimestamp}-${tIdx}`"
|
|
|
|
|
+ class="timeline-term" :style="getTermStyle(t, tIdx)">
|
|
|
|
|
+ <span class="term-name">{{ formatDate(t.startDate) }}</span>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+ <div v-for="(p, idx) in phenologyList" :key="`phenology-${uniqueTimestamp}-${idx}`"
|
|
|
|
|
+ class="phenology-bar">
|
|
|
|
|
+ <div class="phenology-title" :class="{
|
|
|
|
|
+ 'phenology-title--wide': showPhenologyName && !phenologyNeedsTwoLabelColumns(p),
|
|
|
|
|
+ 'phenology-red': !shouldShowBlue(p),
|
|
|
|
|
+ 'phenology-blue': shouldShowBlue(p),
|
|
|
|
|
+ }" v-if="shouldShowPhenologyBarTitle(idx)">
|
|
|
|
|
+ {{ p.phenologyName }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(r, rIdx) in Array.isArray(p.reproductiveList) ? p.reproductiveList : []"
|
|
|
|
|
+ :key="`reproductive-${uniqueTimestamp}-${idx}-${rIdx}`" class="reproductive-item">
|
|
|
|
|
+ <div class="arranges" :class="{ 'arranges-min': !showPhenologyName }">
|
|
|
|
|
+ <div v-for="(fw, aIdx) in Array.isArray(r.farmWorkArrangeList) ? r.farmWorkArrangeList : []"
|
|
|
|
|
+ :key="`arrange-${uniqueTimestamp}-${idx}-${rIdx}-${aIdx}`" class="arrange-card" :class="[
|
|
|
|
|
+ getArrangeStatusClass(fw),
|
|
|
|
|
+ {
|
|
|
|
|
+ 'last-card':
|
|
|
|
|
+ aIdx === r.farmWorkArrangeList.length - 1 &&
|
|
|
|
|
+ rIdx !== r.farmWorkArrangeList.length - 1,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 按单张农事展示日期置灰:晚于今天为 future-card;无日期时回退物候期规则
|
|
|
|
|
+ { 'future-card': shouldGrayFarmWorkCard(fw, p) },
|
|
|
|
|
+ ]" @click="handleRowClick(fw)">
|
|
|
|
|
+ <div class="card-content">
|
|
|
|
|
+ <div class="card-left"
|
|
|
|
|
+ @click.stop="handleStatusDetail(fw)"
|
|
|
|
|
+ :style="{ width: fw.sourceDataJson && fw.sourceDataJson.resFilename ? 'calc(100% - 45px)' : '100%' }"
|
|
|
|
|
+ v-if="pageType === 'agri_plan'">
|
|
|
|
|
+ <div class="left-info">
|
|
|
|
|
+ <div class="left-date">{{ formatDate(fw.createTime) }}</div>
|
|
|
|
|
+ <div class="status-tag">{{ archiveTypeObj[fw.archiveType] }}</div>
|
|
|
|
|
+ <div class="text">
|
|
|
|
|
+ <span class="van-ellipsis">{{ fw.title }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="title-text van-ellipsis">{{ fw.description }}</div>
|
|
|
|
|
+ <!-- <div class="title-text van-ellipsis"
|
|
|
|
|
+ v-if="!shouldGrayFarmWorkCard(fw, p) && fw.sourceType != 4">{{ fw.content }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="title-text van-ellipsis">点击查看区域</div> -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-left agri-record-card" v-else>
|
|
|
|
|
+ <div class="left-info">
|
|
|
|
|
+ <div class="left-date">{{ formatDate(fw.recommendDate) }}</div>
|
|
|
|
|
+ <div class="text van-ellipsis" @click.stop="handleStatusDetail(fw)">
|
|
|
|
|
+ <span class="text-name">{{ fw.farmWorkName }}</span>
|
|
|
|
|
+ <el-icon class="text-icon">
|
|
|
|
|
+ <ArrowRight />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="title-wrap van-ellipsis"
|
|
|
|
|
+ v-if="fw.flowStatus != null && fw.flowStatus != 0 && fw.flowStatus != -2">
|
|
|
|
|
+ <div class="title-text">{{ handleTagType(fw.flowStatus,
|
|
|
|
|
+ fw.executeEvidenceAuditStatus) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-right"
|
|
|
|
|
+ v-if="fw.sourceDataJson && fw.sourceDataJson.resFilename && fw.sourceDataJson.resFilename.length > 0"
|
|
|
|
|
+ @click.stop="handleImageClick(fw)">
|
|
|
|
|
+ <img v-if="fw.sourceType === 7"
|
|
|
|
|
+ :src="base_img_url2 + fw.sourceDataJson?.executeImageUrls?.[0]" alt="" />
|
|
|
|
|
+ <img v-else :src="base_img_url2 + fw.sourceDataJson?.resFilename?.[0]?.filename"
|
|
|
|
|
+ alt="" />
|
|
|
|
|
+ <div class="num" v-if="fw?.sourceDataJson?.imageIds">
|
|
|
|
|
+ {{ fw?.sourceDataJson?.imageIds?.length ||
|
|
|
|
|
+ fw?.sourceDataJson?.executeImageUrls?.length || 0 }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="status-right" v-if="fw.archiveType === 2">待校准</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <template v-if="showPhenologyName">
|
|
|
|
|
+ <template v-if="r.name === p.phenologyName">
|
|
|
|
|
+ <template v-if="!shouldShowPhenologyBarTitle(idx)">
|
|
|
|
|
+ <div class="phenology-name" :class="{
|
|
|
|
|
+ single: showPhenologyName && !phenologyNeedsTwoLabelColumns(p),
|
|
|
|
|
+ 'phenology-red': !shouldShowBlueLeft(p, r),
|
|
|
|
|
+ 'text-blue': shouldShowBlueLeft(p, r),
|
|
|
|
|
+ }"
|
|
|
|
|
+ :style="p.phenologyName === getNextPhenologyName(idx, rIdx) ? 'padding: 6px 0;' : ''">
|
|
|
|
|
+ {{ p.phenologyName }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <template v-if="p.phenologyName === getNextPhenologyName(idx, rIdx)">
|
|
|
|
|
+ <div class="phenology-name"
|
|
|
|
|
+ :class="{ 'text-red': !shouldShowBlueLeft(p, r), 'text-blue': shouldShowBlueLeft(p, r) }">
|
|
|
|
|
+ {{ r.name }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div class="phenology-name"
|
|
|
|
|
+ :class="{ 'text-red': !shouldShowBlueLeft(p, r), 'text-blue': shouldShowBlueLeft(p, r) }">
|
|
|
|
|
+ {{ r.name }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="phenology-name mr" :class="{
|
|
|
|
|
+ 'phenology-red': !shouldShowBlueLeft(p, r),
|
|
|
|
|
+ 'phenology-blue': shouldShowBlueLeft(p, r),
|
|
|
|
|
+ }">
|
|
|
|
|
+ {{ p.phenologyName }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, nextTick, watch, onMounted, onUnmounted, onActivated, onDeactivated, computed } from "vue";
|
|
|
|
|
+import { useRouter, useRoute, onBeforeRouteLeave } from "vue-router";
|
|
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
|
|
+import { Empty, showImagePreview } from "vant";
|
|
|
|
|
+import { base_img_url2 } from "@/api/config";
|
|
|
|
|
+
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+const route = useRoute();
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps({
|
|
|
|
|
+ // 农场 ID,用于请求农事规划数据
|
|
|
|
|
+ farmId: {
|
|
|
|
|
+ type: [String, Number],
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 年份
|
|
|
|
|
+ year: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: new Date().getFullYear(),
|
|
|
|
|
+ },
|
|
|
|
|
+ // 是否是标准农事
|
|
|
|
|
+ isStandard: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 方案ID
|
|
|
|
|
+ schemeId: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 类型:agri_record / agri_plan
|
|
|
|
|
+ pageType: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: "agri_plan",
|
|
|
|
|
+ },
|
|
|
|
|
+ // 区域ID
|
|
|
|
|
+ regionId: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 容器ID
|
|
|
|
|
+ containerId: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 类型ID
|
|
|
|
|
+ typeId: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 问题分区ID
|
|
|
|
|
+ problemZoneId: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const emits = defineEmits(["row-click", "card-click"]);
|
|
|
|
|
+
|
|
|
|
|
+const solarTerms = ref([]);
|
|
|
|
|
+const phenologyList = ref([]);
|
|
|
|
|
+// 从物候期列表中提取起始时间,用于时间轴显示
|
|
|
|
|
+const phenologyStartDates = computed(() => {
|
|
|
|
|
+ if (!phenologyList.value || phenologyList.value.length === 0) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+ // 从每个物候期中提取起始时间,并去重排序
|
|
|
|
|
+ const startDatesMap = new Map();
|
|
|
|
|
+ phenologyList.value.forEach((phenology) => {
|
|
|
|
|
+ if (phenology.startDate) {
|
|
|
|
|
+ const dateKey = phenology.startDate;
|
|
|
|
|
+ // 如果该日期还没有添加过,或者需要更新信息
|
|
|
|
|
+ if (!startDatesMap.has(dateKey)) {
|
|
|
|
|
+ startDatesMap.set(dateKey, {
|
|
|
|
|
+ startDate: phenology.startDate,
|
|
|
|
|
+ id: phenology.id || `phenology-${dateKey}`,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ // 转换为数组并按时间排序
|
|
|
|
|
+ const result = Array.from(startDatesMap.values()).sort((a, b) => {
|
|
|
|
|
+ const timeA = safeParseDate(a.startDate);
|
|
|
|
|
+ const timeB = safeParseDate(b.startDate);
|
|
|
|
|
+ if (isNaN(timeA) || isNaN(timeB)) return 0;
|
|
|
|
|
+ return timeA - timeB;
|
|
|
|
|
+ });
|
|
|
|
|
+ return result;
|
|
|
|
|
+});
|
|
|
|
|
+const timelineContainerRef = ref(null);
|
|
|
|
|
+const timelineListRef = ref(null);
|
|
|
|
|
+const getTimelineScrollKey = () =>
|
|
|
|
|
+ `timelineScrollTop:${props.pageType}:${props.farmId ?? "none"}:${props.regionId ?? "none"}:${props.containerId ?? "none"}:${route.path}`;
|
|
|
|
|
+
|
|
|
|
|
+const saveTimelineScrollTop = () => {
|
|
|
|
|
+ if (!timelineContainerRef.value) return;
|
|
|
|
|
+ const scrollTop = timelineContainerRef.value.scrollTop || 0;
|
|
|
|
|
+ sessionStorage.setItem(getTimelineScrollKey(), scrollTop.toString());
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const restoreTimelineScrollTop = () => {
|
|
|
|
|
+ if (!timelineContainerRef.value) return false;
|
|
|
|
|
+ const savedScrollTop = sessionStorage.getItem(getTimelineScrollKey());
|
|
|
|
|
+ if (savedScrollTop == null) return false;
|
|
|
|
|
+ const scrollTop = Number(savedScrollTop);
|
|
|
|
|
+ if (Number.isNaN(scrollTop)) return false;
|
|
|
|
|
+ const maxScrollTop = Math.max(
|
|
|
|
|
+ 0,
|
|
|
|
|
+ (timelineContainerRef.value.scrollHeight || 0) - (timelineContainerRef.value.clientHeight || 0)
|
|
|
|
|
+ );
|
|
|
|
|
+ timelineContainerRef.value.scrollTop = Math.min(scrollTop, maxScrollTop);
|
|
|
|
|
+ return true;
|
|
|
|
|
+};
|
|
|
|
|
+const restoreTimelineScrollTopWithRetry = (retryCount = 4) => {
|
|
|
|
|
+ const restored = restoreTimelineScrollTop();
|
|
|
|
|
+ if (restored || retryCount <= 0) return restored;
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ restoreTimelineScrollTopWithRetry(retryCount - 1);
|
|
|
|
|
+ }, 60);
|
|
|
|
|
+ return false;
|
|
|
|
|
+};
|
|
|
|
|
+// 标记是否为首次加载
|
|
|
|
|
+const isInitialLoad = ref(true);
|
|
|
|
|
+// 存储timeline-list的实际渲染高度
|
|
|
|
|
+const timelineListHeight = ref(0);
|
|
|
|
|
+// 生成唯一的时间戳,用于确保key的唯一性
|
|
|
|
|
+const uniqueTimestamp = ref(Date.now());
|
|
|
|
|
+// ResizeObserver 实例,用于监听高度变化
|
|
|
|
|
+let resizeObserver = null;
|
|
|
|
|
+// 标记是否为空数据
|
|
|
|
|
+const isEmpty = ref(false);
|
|
|
|
|
+// 标记是否正在请求数据,防止重复请求
|
|
|
|
|
+const isRequesting = ref(false);
|
|
|
|
|
+// 记录上一次请求作用域,避免相同参数重复请求
|
|
|
|
|
+const lastRequestedFarmId = ref(null);
|
|
|
|
|
+
|
|
|
|
|
+const farmWorkPlanScopeKey = () =>
|
|
|
|
|
+ JSON.stringify([
|
|
|
|
|
+ props.pageType ?? null,
|
|
|
|
|
+ props.farmId ?? null,
|
|
|
|
|
+ props.regionId ?? null,
|
|
|
|
|
+ props.problemZoneId ?? null,
|
|
|
|
|
+ props.year ?? null,
|
|
|
|
|
+ props.containerId ?? null,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+const resetTimelineData = () => {
|
|
|
|
|
+ solarTerms.value = [];
|
|
|
|
|
+ phenologyList.value = [];
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const setEmptyTimelineData = () => {
|
|
|
|
|
+ resetTimelineData();
|
|
|
|
|
+ isEmpty.value = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取当前季节
|
|
|
|
|
+const getCurrentSeason = () => {
|
|
|
|
|
+ const month = new Date().getMonth() + 1; // 1-12
|
|
|
|
|
+ if (month >= 1 && 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)
|
|
|
|
|
+/** 解析 sourceData JSON,避免大整数(如雪花 ID)被 JSON.parse 精度丢失(>2^53 会变成 500 结尾等) */
|
|
|
|
|
+const parseSourceDataSafe = (str) => {
|
|
|
|
|
+ if (!str) return null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fixed = str.replace(/"imageIds"\s*:\s*\[([^\]]*)\]/g, (_, arr) => {
|
|
|
|
|
+ const quoted = arr.split(",").map((s) => {
|
|
|
|
|
+ const t = s.trim().replace(/^["']|["']$/g, "");
|
|
|
|
|
+ return /^\d+$/.test(t) ? `"${t}"` : s.trim();
|
|
|
|
|
+ }).join(",");
|
|
|
|
|
+ return `"imageIds":[${quoted}]`;
|
|
|
|
|
+ });
|
|
|
|
|
+ return JSON.parse(fixed);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+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 batchValidateData = ref({});
|
|
|
|
|
+const allTrue = ref(false);
|
|
|
|
|
+const invalidIds = ref([]);
|
|
|
|
|
+const invalidArr = ref([]);
|
|
|
|
|
+// 验证农事卡片药肥报价信息是否完整
|
|
|
|
|
+const batchValidatePesticideFertilizerQuotes = (ids, items) => {
|
|
|
|
|
+ if (props.isStandard) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ VE_API.monitor
|
|
|
|
|
+ .batchValidatePesticideFertilizerQuotes({ ids, schemeId: props.schemeId })
|
|
|
|
|
+ .then(({ data, code }) => {
|
|
|
|
|
+ if (code === 0) {
|
|
|
|
|
+ batchValidateData.value = data || {};
|
|
|
|
|
+ allTrue.value = Object.values(data).every((value) => value === true);
|
|
|
|
|
+ invalidIds.value = Object.keys(data).filter((key) => data[key] !== true);
|
|
|
|
|
+
|
|
|
|
|
+ // 清空之前的arrangeIds
|
|
|
|
|
+ invalidArr.value = [];
|
|
|
|
|
+ // 遍历items,判断farmWorkId是否在invalidIds中,如果对应上了就把item.id push进去
|
|
|
|
|
+ items.forEach((item) => {
|
|
|
|
|
+ // 判断item.farmWorkId是否在invalidIds数组中(需要转换为字符串进行比较)
|
|
|
|
|
+ const farmWorkIdStr = String(item.farmWorkId);
|
|
|
|
|
+ if (invalidIds.value.includes(farmWorkIdStr)) {
|
|
|
|
|
+ invalidArr.value.push({
|
|
|
|
|
+ arrangeId: item.id,
|
|
|
|
|
+ farmWorkId: item.farmWorkId,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => { });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取图片 URL 列表
|
|
|
|
|
+const fetchImageUrls = async (params) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await VE_API.ali.getTreeImageList(params);
|
|
|
|
|
+ if (res.code === 0 && Array.isArray(res.data)) {
|
|
|
|
|
+ return res.data.map((item) => {
|
|
|
|
|
+ if (item.filename) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ cloudFilename: item.filename, // 兼容组件
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }).filter(item => item !== null);
|
|
|
|
|
+ }
|
|
|
|
|
+ return [];
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("获取图片列表失败:", error);
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 与 albumCarouselItem.getPhotoSrc 一致:拼 CDN 前缀;无水印预览用 Vant ImagePreview
|
|
|
|
|
+const resolveFarmWorkImagePath = (photo) => {
|
|
|
|
|
+ if (photo == null) return "";
|
|
|
|
|
+ if (typeof photo === "string") return photo;
|
|
|
|
|
+ return photo.cloudFilename || photo.filename || "";
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const buildFarmWorkPreviewUrls = (fw) => {
|
|
|
|
|
+ const sd = fw?.sourceDataJson;
|
|
|
|
|
+ if (!sd) return [];
|
|
|
|
|
+ const toFullUrl = (path) => {
|
|
|
|
|
+ if (!path) return "";
|
|
|
|
|
+ if (/^https?:\/\//i.test(path)) return path;
|
|
|
|
|
+ return base_img_url2 + path;
|
|
|
|
|
+ };
|
|
|
|
|
+ if (fw.sourceType === 7) {
|
|
|
|
|
+ const arr = sd.executeImageUrls || [];
|
|
|
|
|
+ return arr.map((p) => toFullUrl(resolveFarmWorkImagePath(p))).filter(Boolean);
|
|
|
|
|
+ }
|
|
|
|
|
+ const arr = sd.resFilename || [];
|
|
|
|
|
+ return arr.map((p) => toFullUrl(resolveFarmWorkImagePath(p))).filter(Boolean);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleImageClick = (fw) => {
|
|
|
|
|
+ const images = buildFarmWorkPreviewUrls(fw);
|
|
|
|
|
+ if (!images.length) return;
|
|
|
|
|
+ showImagePreview({
|
|
|
|
|
+ images,
|
|
|
|
|
+ startPosition: 0,
|
|
|
|
|
+ closeable: true,
|
|
|
|
|
+ showIndex: true,
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 顶部物候标题:相邻两段 phenologyName 相同时只显示一次(保留第一段)
|
|
|
|
|
+const shouldShowPhenologyBarTitle = (idx) => {
|
|
|
|
|
+ const list = phenologyList.value;
|
|
|
|
|
+ if (!list?.length || idx < 0 || idx >= list.length) return false;
|
|
|
|
|
+ if (idx === 0) return true;
|
|
|
|
|
+ return list[idx]?.phenologyName !== list[idx - 1]?.phenologyName;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 是否存在与物候期不同的生育期行(需要左右两列竖条);否则物候条占满单列宽度即可
|
|
|
|
|
+const phenologyNeedsTwoLabelColumns = (p) => {
|
|
|
|
|
+ const list = Array.isArray(p?.reproductiveList) ? p.reproductiveList : [];
|
|
|
|
|
+ if (!list.length) return false;
|
|
|
|
|
+ return list.some((r) => r.name !== p.phenologyName);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取下一个reproductive-item的phenologyName
|
|
|
|
|
+const getNextPhenologyName = (currentPhenologyIdx, currentReproductiveIdx) => {
|
|
|
|
|
+ const currentPhenology = phenologyList.value[currentPhenologyIdx];
|
|
|
|
|
+ if (!currentPhenology || !Array.isArray(currentPhenology.reproductiveList)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果当前reproductive-item不是最后一个,获取同一个物候期的下一个
|
|
|
|
|
+ if (currentReproductiveIdx < currentPhenology.reproductiveList.length - 1) {
|
|
|
|
|
+ const nextReproductive = currentPhenology.reproductiveList[currentReproductiveIdx + 1];
|
|
|
|
|
+ return nextReproductive?.phenologyName || null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果当前reproductive-item是最后一个,获取下一个物候期的第一个reproductive-item
|
|
|
|
|
+ if (currentPhenologyIdx < phenologyList.value.length - 1) {
|
|
|
|
|
+ const nextPhenology = phenologyList.value[currentPhenologyIdx + 1];
|
|
|
|
|
+ if (
|
|
|
|
|
+ nextPhenology &&
|
|
|
|
|
+ Array.isArray(nextPhenology.reproductiveList) &&
|
|
|
|
|
+ nextPhenology.reproductiveList.length > 0
|
|
|
|
|
+ ) {
|
|
|
|
|
+ const firstReproductive = nextPhenology.reproductiveList[0];
|
|
|
|
|
+ return firstReproductive?.phenologyName || null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 计算物候期需要的实际高度(基于农事数量)
|
|
|
|
|
+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();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有物候期数据,直接使用计算出的总高度
|
|
|
|
|
+ // totalHeight 已经包含了从 10 开始的起始位置和所有物候期的高度
|
|
|
|
|
+ if (totalHeight > 10) {
|
|
|
|
|
+ // 确保总高度至少能容纳所有物候期起始时间(每个至少50px)
|
|
|
|
|
+ const baseHeight = (phenologyStartDates.value?.length || 0) * 50;
|
|
|
|
|
+ // 返回物候期总高度和基础高度的较大值,确保物候期起始时间能正常显示
|
|
|
|
|
+ return Math.max(totalHeight, baseHeight);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有物候期数据,返回基础高度
|
|
|
|
|
+ const baseHeight = (phenologyStartDates.value?.length || 0) * 50;
|
|
|
|
|
+ return baseHeight || 100; // 至少返回100px,避免为0
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getTermStyle = (t, index) => {
|
|
|
|
|
+ // 优先使用实际测量的timeline-list高度,如果没有测量到则使用计算值作为后备
|
|
|
|
|
+ const totalHeight = timelineListHeight.value > 0 ? timelineListHeight.value : calculateTotalHeightByFarmWorks();
|
|
|
|
|
+ // 获取物候期起始时间总数(使用新数组)
|
|
|
|
|
+ const termCount = phenologyStartDates.value?.length || 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 等分高度:总高度 / 物候期起始时间数量
|
|
|
|
|
+ const termHeight = totalHeight / termCount;
|
|
|
|
|
+
|
|
|
|
|
+ // 计算top位置:索引 * 每个物候期起始时间的高度
|
|
|
|
|
+ const top = index * termHeight;
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ position: "absolute",
|
|
|
|
|
+ top: `${top}px`,
|
|
|
|
|
+ left: 0,
|
|
|
|
|
+ width: "35px",
|
|
|
|
|
+ height: `${termHeight}px`, // 高度等分,使用实际测量的高度
|
|
|
|
|
+ display: "flex",
|
|
|
|
|
+ alignItems: "center",
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 点击季节 → 滚动到对应节气(立春/立夏/立秋/立冬)
|
|
|
|
|
+const handleSeasonClick = (seasonValue) => {
|
|
|
|
|
+ const mapping = {
|
|
|
|
|
+ spring: "立春",
|
|
|
|
|
+ summer: "立夏",
|
|
|
|
|
+ autumn: "立秋",
|
|
|
|
|
+ winter: "立冬",
|
|
|
|
|
+ };
|
|
|
|
|
+ const targetName = mapping[seasonValue];
|
|
|
|
|
+ if (!targetName) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 查找对应的节气
|
|
|
|
|
+ const targetIndex = solarTerms.value.findIndex((t) => (t?.displayName || "") === targetName);
|
|
|
|
|
+ if (targetIndex === -1) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 计算目标节气的top位置
|
|
|
|
|
+ const totalHeight = timelineListHeight.value > 0 ? timelineListHeight.value : calculateTotalHeightByFarmWorks();
|
|
|
|
|
+ const termCount = solarTerms.value?.length || 1;
|
|
|
|
|
+ const termHeight = totalHeight / termCount;
|
|
|
|
|
+ const targetTop = targetIndex * termHeight;
|
|
|
|
|
+
|
|
|
|
|
+ // 滚动到目标位置
|
|
|
|
|
+ const wrap = timelineContainerRef.value;
|
|
|
|
|
+ if (!wrap) return;
|
|
|
|
|
+ const viewH = wrap.clientHeight || 0;
|
|
|
|
|
+ const maxScroll = Math.max(0, wrap.scrollHeight - viewH);
|
|
|
|
|
+ // 将目标位置稍微靠上(使用 0.1 视口高度做偏移)
|
|
|
|
|
+ let scrollTop = Math.max(0, targetTop - viewH * 0.1);
|
|
|
|
|
+ if (scrollTop > maxScroll) scrollTop = maxScroll;
|
|
|
|
|
+ wrap.scrollTo({ top: scrollTop, behavior: "smooth" });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const archiveTypeObj = {
|
|
|
|
|
+ 1: "管理信息",
|
|
|
|
|
+ 2: "物候进程",
|
|
|
|
|
+ 3: "物候进程",
|
|
|
|
|
+ 4: "异常发现",
|
|
|
|
|
+ 5: "气象风险",
|
|
|
|
|
+ 6: "农事进度",
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ MANAGEMENT_INFO(1, "管理信息"),
|
|
|
|
|
+ EXPECTED_PHENOLOGY_PROGRESS(2, "预计物候进程"),
|
|
|
|
|
+ PHENOLOGY_PROGRESS(3, "物候进程"),
|
|
|
|
|
+ ABNORMAL_DISCOVERY(4, "异常发现"),
|
|
|
|
|
+ WEATHER_RISK(5, "气象风险"),
|
|
|
|
|
+ FARM_WORK_PROGRESS(6, "农事进度");
|
|
|
|
|
+ * 农事状态样式映射
|
|
|
|
|
+ * @param fw 农事
|
|
|
|
|
+ * @returns 农事状态样式
|
|
|
|
|
+ */
|
|
|
|
|
+// 农事状态样式映射
|
|
|
|
|
+const getArrangeStatusClass = (fw) => {
|
|
|
|
|
+ const t = props.pageType === 'agri_record' ? fw?.flowStatus : fw?.archiveType;
|
|
|
|
|
+ if (props.pageType === 'agri_record') {
|
|
|
|
|
+ if (t == null || t == 0 || t == -2) return "status-default";
|
|
|
|
|
+ const status = getAuditStatusPriority(fw.executeEvidenceAuditStatus);
|
|
|
|
|
+ // 农事记录:内容仍用 status-warning 的红字/红标签,外边框与箭头改为灰色
|
|
|
|
|
+ if (t == -1) return "status-warning status-warning-agri-record";
|
|
|
|
|
+ if (t == 3 || status === 2 || status === 0) return "status-warning-bg";
|
|
|
|
|
+ if (t == 5) return "status-normal";
|
|
|
|
|
+ return "status-act";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (t == 1) return "status-green-info";
|
|
|
|
|
+ if (t == 4 && fw.sourceDataJson?.abnormalType === '病虫') return "status-warning";
|
|
|
|
|
+ if (t == 4 && fw.sourceDataJson?.abnormalType !== '病虫') return "status-complete";
|
|
|
|
|
+ if (t == 5) return "status-orange";
|
|
|
|
|
+ if (t == 6) return "status-green-farm";
|
|
|
|
|
+ return "status-normal";
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleRowClick = (item) => {
|
|
|
|
|
+ // 跳转前记录当前滚动位置
|
|
|
|
|
+ saveTimelineScrollTop();
|
|
|
|
|
+ emits("row-click", item);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取农事规划数据
|
|
|
|
|
+const getFarmWorkPlan = () => {
|
|
|
|
|
+ resetTimelineData();
|
|
|
|
|
+ if (!props.farmId) return;
|
|
|
|
|
+ const scopeKey = farmWorkPlanScopeKey();
|
|
|
|
|
+ if (isRequesting.value || lastRequestedFarmId.value === scopeKey) return;
|
|
|
|
|
+ isRequesting.value = true;
|
|
|
|
|
+ lastRequestedFarmId.value = scopeKey;
|
|
|
|
|
+ // 更新时间戳,确保key变化,触发DOM重新渲染
|
|
|
|
|
+ uniqueTimestamp.value = Date.now();
|
|
|
|
|
+ // 重置测量高度,等待重新测量
|
|
|
|
|
+ timelineListHeight.value = 0;
|
|
|
|
|
+ // 重置空数据状态
|
|
|
|
|
+ isEmpty.value = false;
|
|
|
|
|
+ let savedScrollTop = 0;
|
|
|
|
|
+ if (!isInitialLoad.value && timelineContainerRef.value) {
|
|
|
|
|
+ savedScrollTop = timelineContainerRef.value.scrollTop || 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const apiFunc = props.pageType === "agri_record" ? VE_API.monitor.getFarmWorkPlan : VE_API.monitor.getArchivesList;
|
|
|
|
|
+
|
|
|
|
|
+ const params = {
|
|
|
|
|
+ farmId: props.farmId,
|
|
|
|
|
+ regionId: props.regionId,
|
|
|
|
|
+ problemZoneId: props.problemZoneId,
|
|
|
|
|
+ year: props.year,
|
|
|
|
|
+ };
|
|
|
|
|
+ if (props.pageType === "agri_record") {
|
|
|
|
|
+ params.containerId = props.containerId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ apiFunc(params)
|
|
|
|
|
+ .then(async ({ 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;
|
|
|
|
|
+ // 物候期数据
|
|
|
|
|
+ const processedPhenologyList = Array.isArray(data?.phenologyList)
|
|
|
|
|
+ ? await Promise.all(
|
|
|
|
|
+ data.phenologyList.map(async (it) => {
|
|
|
|
|
+ const reproductiveList = Array.isArray(it.reproductiveList)
|
|
|
|
|
+ ? await Promise.all(
|
|
|
|
|
+ it.reproductiveList.map(async (r) => {
|
|
|
|
|
+ const farmWorkArrangeList = Array.isArray(r.archiveList || r.interactionFarmWorkList)
|
|
|
|
|
+ ? await Promise.all(
|
|
|
|
|
+ (r.archiveList || r.interactionFarmWorkList).map(async (fw) => {
|
|
|
|
|
+ const sourceDataJson = parseSourceDataSafe(fw.data);
|
|
|
|
|
+ // 如果有 imageIds,获取图片 URL
|
|
|
|
|
+ if (
|
|
|
|
|
+ sourceDataJson &&
|
|
|
|
|
+ sourceDataJson.imageIds &&
|
|
|
|
|
+ Array.isArray(sourceDataJson.imageIds) &&
|
|
|
|
|
+ sourceDataJson.imageIds.length > 0
|
|
|
|
|
+ ) {
|
|
|
|
|
+ const resFilenameList = await fetchImageUrls(
|
|
|
|
|
+ {
|
|
|
|
|
+ imageIds: sourceDataJson.imageIds,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ limit: 100,
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+ sourceDataJson.resFilename = resFilenameList;
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...fw,
|
|
|
|
|
+ phenologyName: r.phenologyName,
|
|
|
|
|
+ sourceDataJson,
|
|
|
|
|
+ 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, // 终点 %
|
|
|
|
|
+ phenologyName: it.phenologyName,
|
|
|
|
|
+ startDate: it.startDate,
|
|
|
|
|
+ startTimeMs: safeParseDate(
|
|
|
|
|
+ it.startDate || it.beginDate || it.startTime || it.start || it.start_at
|
|
|
|
|
+ ),
|
|
|
|
|
+ reproductiveList,
|
|
|
|
|
+ };
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ : [];
|
|
|
|
|
+ phenologyList.value = processedPhenologyList;
|
|
|
|
|
+ // 使用多次 nextTick 和 requestAnimationFrame 确保DOM完全渲染
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ // 测量timeline-list的实际渲染高度
|
|
|
|
|
+ if (timelineListRef.value) {
|
|
|
|
|
+ const height =
|
|
|
|
|
+ timelineListRef.value.offsetHeight || timelineListRef.value.clientHeight;
|
|
|
|
|
+ if (height > 0) {
|
|
|
|
|
+ timelineListHeight.value = height;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const hasRestoredScrollTop = restoreTimelineScrollTopWithRetry();
|
|
|
|
|
+ if (isInitialLoad.value) {
|
|
|
|
|
+ if (hasRestoredScrollTop) {
|
|
|
|
|
+ isInitialLoad.value = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果测量失败,延迟一下再尝试滚动
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (timelineListRef.value) {
|
|
|
|
|
+ const height =
|
|
|
|
|
+ timelineListRef.value.offsetHeight ||
|
|
|
|
|
+ timelineListRef.value.clientHeight;
|
|
|
|
|
+ if (height > 0) {
|
|
|
|
|
+ timelineListHeight.value = height;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!restoreTimelineScrollTopWithRetry()) {
|
|
|
|
|
+ const currentSeason = getCurrentSeason();
|
|
|
|
|
+ handleSeasonClick(currentSeason);
|
|
|
|
|
+ }
|
|
|
|
|
+ isInitialLoad.value = false;
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (!hasRestoredScrollTop && timelineContainerRef.value && savedScrollTop > 0) {
|
|
|
|
|
+ timelineContainerRef.value.scrollTop = savedScrollTop;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const farmWorks = phenologyList.value.flatMap((phenology) =>
|
|
|
|
|
+ (Array.isArray(phenology?.reproductiveList) ? phenology.reproductiveList : []).flatMap((reproductive) =>
|
|
|
|
|
+ Array.isArray(reproductive?.farmWorkArrangeList) ? reproductive.farmWorkArrangeList : []
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ const quoteValidationFarmWorks = farmWorks.filter(
|
|
|
|
|
+ (farmWork) => farmWork?.farmWorkId && farmWork?.isFollow !== 0
|
|
|
|
|
+ );
|
|
|
|
|
+ const farmWorkIds = quoteValidationFarmWorks.map((farmWork) => farmWork.farmWorkId);
|
|
|
|
|
+
|
|
|
|
|
+ // 调用验证方法,传入所有ids
|
|
|
|
|
+ if (farmWorkIds.length > 0) {
|
|
|
|
|
+ batchValidatePesticideFertilizerQuotes(farmWorkIds, quoteValidationFarmWorks);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否为空数据:没有节气或没有物候期数据
|
|
|
|
|
+ if (solarTerms.value.length === 0 || phenologyList.value.length === 0) {
|
|
|
|
|
+ isEmpty.value = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ isEmpty.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 接口返回错误码,显示暂无数据
|
|
|
|
|
+ setEmptyTimelineData();
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((error) => {
|
|
|
|
|
+ console.error("获取农事规划数据失败:", error);
|
|
|
|
|
+ ElMessage.error("获取农事规划数据失败");
|
|
|
|
|
+ // 接口报错,显示暂无数据
|
|
|
|
|
+ setEmptyTimelineData();
|
|
|
|
|
+ })
|
|
|
|
|
+ .finally(() => {
|
|
|
|
|
+ // 请求完成,重置请求标志
|
|
|
|
|
+ isRequesting.value = false;
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const updateFarmWorkPlan = () => {
|
|
|
|
|
+ resetTimelineData();
|
|
|
|
|
+ isEmpty.value = false;
|
|
|
|
|
+ getFarmWorkPlan();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const showPhenologyName = computed(() => {
|
|
|
|
|
+ return !props.problemZoneId;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => [props.farmId, props.regionId, props.year, props.problemZoneId, props.pageType, props.containerId],
|
|
|
|
|
+ (val, oldVal) => {
|
|
|
|
|
+ if (!props.farmId) return;
|
|
|
|
|
+ const changed =
|
|
|
|
|
+ !oldVal ||
|
|
|
|
|
+ val.some((item, index) => item !== oldVal[index]);
|
|
|
|
|
+ if (changed) {
|
|
|
|
|
+ lastRequestedFarmId.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ isInitialLoad.value = true;
|
|
|
|
|
+ updateFarmWorkPlan();
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const handleStatusDetail = (fw) => {
|
|
|
|
|
+ // 跳转前记录当前滚动位置
|
|
|
|
|
+ saveTimelineScrollTop();
|
|
|
|
|
+ emits('card-click');
|
|
|
|
|
+ console.log(fw);
|
|
|
|
|
+ if (props.pageType === 'agri_plan') {
|
|
|
|
|
+ if (fw?.archiveType === 2) {
|
|
|
|
|
+ // router.push(`/interaction_list?farmId=${farmIdData.value}®ionId=${popupData.value.regionId}&interactionTypeId=${popupData.value.interactionTypeId}`);
|
|
|
|
|
+ }else if(fw?.archiveType === 6){
|
|
|
|
|
+ router.push({
|
|
|
|
|
+ path: "/work_detail",
|
|
|
|
|
+ query: {
|
|
|
|
|
+ miniJson: JSON.stringify({
|
|
|
|
|
+ paramsPage: JSON.stringify({
|
|
|
|
|
+ farmId: 98570,
|
|
|
|
|
+ farmWorkLibId: '832268348690534411',
|
|
|
|
|
+ recordId: "832268363366404096",
|
|
|
|
|
+ reproductiveId: 149,
|
|
|
|
|
+ typeId: props.typeId
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }else{
|
|
|
|
|
+ router.push({
|
|
|
|
|
+ path: "/agricultural_detail",
|
|
|
|
|
+ query: {
|
|
|
|
|
+ id: fw?.id,
|
|
|
|
|
+ title: archiveTypeObj[fw?.archiveType],
|
|
|
|
|
+ content: fw?.description,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ router.push({
|
|
|
|
|
+ path: "/work_detail",
|
|
|
|
|
+ query: {
|
|
|
|
|
+ miniJson: JSON.stringify({
|
|
|
|
|
+ paramsPage: JSON.stringify({
|
|
|
|
|
+ farmId: props.farmId,
|
|
|
|
|
+ farmWorkLibId: fw?.farmWorkLibId,
|
|
|
|
|
+ recordId: fw?.farmWorkRecordId,
|
|
|
|
|
+ reproductiveId: fw?.reproductiveId,
|
|
|
|
|
+ typeId: props.typeId
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 格式化日期为 MM-DD 格式
|
|
|
|
|
+const formatDate = (dateStr) => {
|
|
|
|
|
+ if (!dateStr) return "--";
|
|
|
|
|
+ const date = new Date(dateStr);
|
|
|
|
|
+ if (Number.isNaN(date.getTime())) return dateStr;
|
|
|
|
|
+ const m = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
|
|
|
+ const d = `${date.getDate()}`.padStart(2, "0");
|
|
|
|
|
+ return `${m}-${d}`;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 格式化日期为 YYYY-MM-DD 格式(用于接口调用)
|
|
|
|
|
+const formatDateForAPI = (dateStr) => {
|
|
|
|
|
+ if (!dateStr) return null;
|
|
|
|
|
+ const date = new Date(dateStr);
|
|
|
|
|
+ if (Number.isNaN(date.getTime())) return null;
|
|
|
|
|
+ const y = date.getFullYear();
|
|
|
|
|
+ const m = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
|
|
|
+ const d = `${date.getDate()}`.padStart(2, "0");
|
|
|
|
|
+ return `${y}-${m}-${d}`;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取下一个即将到来的节气(当前节气)的 progress
|
|
|
|
|
+const getNextTermProgress = () => {
|
|
|
|
|
+ if (!solarTerms.value || solarTerms.value.length === 0) return Infinity;
|
|
|
|
|
+
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ now.setHours(0, 0, 0, 0);
|
|
|
|
|
+
|
|
|
|
|
+ let nextTermProgress = Infinity;
|
|
|
|
|
+
|
|
|
|
|
+ // 找到当前日期之后的下一个节气(当前节气)
|
|
|
|
|
+ solarTerms.value.forEach((term) => {
|
|
|
|
|
+ const termDate = safeParseDate(term.createDate);
|
|
|
|
|
+ if (!isNaN(termDate)) {
|
|
|
|
|
+ const termDateObj = new Date(termDate);
|
|
|
|
|
+ termDateObj.setHours(0, 0, 0, 0);
|
|
|
|
|
+ // 找到大于等于当前日期的第一个节气
|
|
|
|
|
+ if (termDateObj >= now) {
|
|
|
|
|
+ const termProgress = Number(term.progress) || 0;
|
|
|
|
|
+ if (termProgress < nextTermProgress) {
|
|
|
|
|
+ nextTermProgress = termProgress;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有找到未来的节气,说明所有节气都已过,返回 Infinity(所有物候期都显示蓝色)
|
|
|
|
|
+ return nextTermProgress === Infinity ? Infinity : nextTermProgress;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 根据物候期的 progress 判断它所属节气的 progress
|
|
|
|
|
+const getPhenologyTermProgress = (phenologyProgress) => {
|
|
|
|
|
+ if (!solarTerms.value || solarTerms.value.length === 0) return -1;
|
|
|
|
|
+
|
|
|
|
|
+ const progress = Number(phenologyProgress) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 找到物候期所属的节气(progress 最接近且小于等于的节气)
|
|
|
|
|
+ let matchedTermProgress = -1;
|
|
|
|
|
+ solarTerms.value.forEach((term) => {
|
|
|
|
|
+ const termProgress = Number(term.progress) || 0;
|
|
|
|
|
+ if (progress >= termProgress && termProgress > matchedTermProgress) {
|
|
|
|
|
+ matchedTermProgress = termProgress;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 如果物候期的 progress 小于所有节气,返回第一个节气的 progress
|
|
|
|
|
+ if (matchedTermProgress === -1 && solarTerms.value.length > 0) {
|
|
|
|
|
+ const firstTermProgress = Number(solarTerms.value[0].progress) || 0;
|
|
|
|
|
+ return firstTermProgress;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return matchedTermProgress;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleTagType = (tagType, executeEvidenceAuditStatus) => {
|
|
|
|
|
+ if (tagType == 0) return "待触发";
|
|
|
|
|
+ if (tagType == -1) return "已过期";
|
|
|
|
|
+ if (tagType == -2) return "已过期";
|
|
|
|
|
+ if (tagType == 3) {
|
|
|
|
|
+ const status = getAuditStatusPriority(executeEvidenceAuditStatus);
|
|
|
|
|
+ if (status === 2) {
|
|
|
|
|
+ return "审核失败";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "待认证"
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tagType == 5) {
|
|
|
|
|
+ const status = getAuditStatusPriority(executeEvidenceAuditStatus);
|
|
|
|
|
+ if (status === 2) {
|
|
|
|
|
+ return "审核失败";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (status === 0) {
|
|
|
|
|
+ return "审核中";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "已认证";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "待触发"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 审核状态优先级:2 > 0 > 1
|
|
|
|
|
+const getAuditStatusPriority = (auditStatusList) => {
|
|
|
|
|
+ if (!Array.isArray(auditStatusList) || !auditStatusList.length) return 1;
|
|
|
|
|
+ const normalized = auditStatusList.map((x) => Number(x)).filter((x) => [0, 1, 2].includes(x));
|
|
|
|
|
+ if (!normalized.length) return 1;
|
|
|
|
|
+ if (normalized.includes(0)) return 0;
|
|
|
|
|
+ if (normalized.includes(2)) return 2;
|
|
|
|
|
+ return 1;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 右侧农事卡片展示用日期(与模板 left-date 一致:规划 createTime、记录 recommendDate)
|
|
|
|
|
+const getFarmWorkTimelineDateMs = (fw) => {
|
|
|
|
|
+ const primary = props.pageType === "agri_plan" ? fw?.createTime : fw?.recommendDate ?? fw?.createTime;
|
|
|
|
|
+ let ms = safeParseDate(primary);
|
|
|
|
|
+ if (Number.isNaN(ms) || ms <= 0) {
|
|
|
|
|
+ ms = safeParseDate(props.pageType === "agri_plan" ? fw?.recommendDate : fw?.createTime);
|
|
|
|
|
+ }
|
|
|
|
|
+ return ms;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const isFarmWorkDateOnOrBeforeToday = (ms) => {
|
|
|
|
|
+ if (Number.isNaN(ms) || ms <= 0) return false;
|
|
|
|
|
+ const today = new Date();
|
|
|
|
|
+ today.setHours(0, 0, 0, 0);
|
|
|
|
|
+ const d = new Date(ms);
|
|
|
|
|
+ d.setHours(0, 0, 0, 0);
|
|
|
|
|
+ return d.getTime() <= today.getTime();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 单张卡片:有有效展示日期且晚于今天 → 未来农事,右侧应置灰(与 left-date 同一套字段)
|
|
|
|
|
+const isFarmWorkTimelineStrictlyFuture = (fw) => {
|
|
|
|
|
+ const ms = getFarmWorkTimelineDateMs(fw);
|
|
|
|
|
+ if (Number.isNaN(ms) || ms <= 0) return false;
|
|
|
|
|
+ return !isFarmWorkDateOnOrBeforeToday(ms);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 右侧 arrange-card:未来日期置灰;解析不到日期时回退物候期节气/待认证规则(与旧版整段逻辑一致)
|
|
|
|
|
+// 无农事展示日期时的置灰回退:仅用节气/物候期 + 待认证(不含「任一行农事≤今天」),避免右侧卡片被外层标题连带误判
|
|
|
|
|
+const isPhenologyActiveWithoutFarmWorkDates = (phenology) =>
|
|
|
|
|
+ agriRecordHasPendingAuthInPhenology(phenology) || shouldShowBlueBase(phenology);
|
|
|
|
|
+
|
|
|
|
|
+const shouldGrayFarmWorkCard = (fw, phenology) => {
|
|
|
|
|
+ if (isFarmWorkTimelineStrictlyFuture(fw)) return true;
|
|
|
|
|
+ const ms = getFarmWorkTimelineDateMs(fw);
|
|
|
|
|
+ if (!Number.isNaN(ms) && ms > 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return !isPhenologyActiveWithoutFarmWorkDates(phenology);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 某一生育期行下是否存在「农事时间 ≤ 今天」的卡片(与右侧 left-date 字段一致)
|
|
|
|
|
+const reproductiveHasFarmWorkOnOrBeforeToday = (reproductive) => {
|
|
|
|
|
+ const fws = Array.isArray(reproductive?.farmWorkArrangeList) ? reproductive.farmWorkArrangeList : [];
|
|
|
|
|
+ for (const fw of fws) {
|
|
|
|
|
+ if (isFarmWorkDateOnOrBeforeToday(getFarmWorkTimelineDateMs(fw))) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 物候期内是否至少有一行生育期因「农事≤今天」而高亮(用于最外层物候期标题与内层对齐)
|
|
|
|
|
+const phenologyHasFarmWorkOnOrBeforeToday = (phenology) => {
|
|
|
|
|
+ const reproductives = Array.isArray(phenology?.reproductiveList) ? phenology.reproductiveList : [];
|
|
|
|
|
+ return reproductives.some((r) => reproductiveHasFarmWorkOnOrBeforeToday(r));
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 该生育期下:存在农事且「凡有有效展示日期的卡片」全部严格晚于今天 → 左侧本行不因节气规则变蓝(与右侧 future-card 一致)
|
|
|
|
|
+const reproductiveAllDatedFarmWorksStrictlyFuture = (reproductive) => {
|
|
|
|
|
+ const fws = Array.isArray(reproductive?.farmWorkArrangeList) ? reproductive.farmWorkArrangeList : [];
|
|
|
|
|
+ if (fws.length === 0) return false;
|
|
|
|
|
+ const dated = fws.filter((fw) => {
|
|
|
|
|
+ const ms = getFarmWorkTimelineDateMs(fw);
|
|
|
|
|
+ return !Number.isNaN(ms) && ms > 0;
|
|
|
|
|
+ });
|
|
|
|
|
+ if (dated.length === 0) return false;
|
|
|
|
|
+ return dated.every((fw) => isFarmWorkTimelineStrictlyFuture(fw));
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 物候期内:至少有一条农事带有效日期,且全部带有效日期的农事均为「严格未来」(无一 ≤ 今天)→ 最外层标题不因节气单独变蓝
|
|
|
|
|
+const phenologyAllDatedFarmWorksStrictlyFuture = (phenology) => {
|
|
|
|
|
+ const reproductives = Array.isArray(phenology?.reproductiveList) ? phenology.reproductiveList : [];
|
|
|
|
|
+ let anyDated = false;
|
|
|
|
|
+ for (const r of reproductives) {
|
|
|
|
|
+ const fws = Array.isArray(r?.farmWorkArrangeList) ? r.farmWorkArrangeList : [];
|
|
|
|
|
+ for (const fw of fws) {
|
|
|
|
|
+ const ms = getFarmWorkTimelineDateMs(fw);
|
|
|
|
|
+ if (Number.isNaN(ms) || ms <= 0) continue;
|
|
|
|
|
+ anyDated = true;
|
|
|
|
|
+ if (isFarmWorkDateOnOrBeforeToday(ms)) return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return anyDated;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 仅农事记录页(agri_record):该物候期内存在待认证农事时,左侧最外层物候期与各生育期/物候期文案均高亮为蓝;农事规划(agri_plan)不生效
|
|
|
|
|
+const agriRecordHasPendingAuthInPhenology = (phenology) => {
|
|
|
|
|
+ if (props.pageType !== "agri_record") return false;
|
|
|
|
|
+ return (Array.isArray(phenology?.reproductiveList) ? phenology.reproductiveList : []).some((reproductive) =>
|
|
|
|
|
+ (Array.isArray(reproductive?.farmWorkArrangeList) ? reproductive.farmWorkArrangeList : []).some((fw) => {
|
|
|
|
|
+ const s = fw?.flowStatus;
|
|
|
|
|
+ return s === 3 || s === "3";
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 单行生育期下是否有待认证农事(仅农事记录页)
|
|
|
|
|
+const reproductiveHasPendingAuthFarmWork = (reproductive) => {
|
|
|
|
|
+ if (props.pageType !== "agri_record") return false;
|
|
|
|
|
+ return (Array.isArray(reproductive?.farmWorkArrangeList) ? reproductive.farmWorkArrangeList : []).some((fw) => {
|
|
|
|
|
+ const s = fw?.flowStatus;
|
|
|
|
|
+ return s === 3 || s === "3";
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 物候期底色/节气规则(不含「农事卡片日期」「待认证」扩展)
|
|
|
|
|
+const shouldShowBlueBase = (phenology) => {
|
|
|
|
|
+ // 优先使用物候期真实日期判断:未来日期不应显示蓝色
|
|
|
|
|
+ const phenologyTimeMs =
|
|
|
|
|
+ Number(phenology?.startTimeMs) ||
|
|
|
|
|
+ safeParseDate(phenology?.startDate || phenology?.beginDate || phenology?.startTime || phenology?.start || phenology?.start_at);
|
|
|
|
|
+ if (!Number.isNaN(phenologyTimeMs) && phenologyTimeMs > 0) {
|
|
|
|
|
+ const today = new Date();
|
|
|
|
|
+ today.setHours(0, 0, 0, 0);
|
|
|
|
|
+ return phenologyTimeMs <= today.getTime();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 日期缺失时,回退到 progress 规则(兼容老数据)
|
|
|
|
|
+ // 获取下一个即将到来的节气(当前节气)的 progress
|
|
|
|
|
+ const nextTermProgress = getNextTermProgress();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果所有节气都已过(nextTermProgress === Infinity),所有物候期都显示蓝色
|
|
|
|
|
+ if (nextTermProgress === Infinity) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 根据物候期的 progress 判断它属于哪个节气
|
|
|
|
|
+ const phenologyProgress = Math.min(Number(phenology?.progress) || 0, Number(phenology?.progress2) || 0);
|
|
|
|
|
+ const phenologyTermProgress = getPhenologyTermProgress(phenologyProgress);
|
|
|
|
|
+
|
|
|
|
|
+ // 找到下一个节气的完整信息,用于判断物候期是否属于当前节气
|
|
|
|
|
+ let nextTerm = null;
|
|
|
|
|
+ solarTerms.value.forEach((term) => {
|
|
|
|
|
+ const termProgress = Number(term.progress) || 0;
|
|
|
|
|
+ if (termProgress === nextTermProgress) {
|
|
|
|
|
+ nextTerm = term;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 如果物候期所属的节气的 progress < 下一个节气的 progress,显示蓝色
|
|
|
|
|
+ // 如果物候期所属的节气的 progress === 下一个节气的 progress,也显示蓝色(当前节气)
|
|
|
|
|
+ // 也就是说,只有属于当前节气或之前节气的物候期才显示蓝色
|
|
|
|
|
+ if (phenologyTermProgress === -1) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果物候期正好属于下一个节气,需要判断它的 progress 是否在下一个节气的范围内
|
|
|
|
|
+ if (phenologyTermProgress === nextTermProgress && nextTerm) {
|
|
|
|
|
+ // 如果物候期的 progress 小于等于下一个节气的 progress,说明它属于当前节气,显示蓝色
|
|
|
|
|
+ return phenologyProgress <= nextTermProgress;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果物候期所属的节气的 progress < 下一个节气的 progress,显示蓝色
|
|
|
|
|
+ return phenologyTermProgress < nextTermProgress;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 最外层物候期标题:与内层生育期对齐——任一生育行因农事≤今天变蓝;农事记录下有待认证则整段标题也蓝;否则按节气规则(右侧卡片仍按单卡日期置灰)
|
|
|
|
|
+const shouldShowBlue = (phenology) => {
|
|
|
|
|
+ if (phenologyHasFarmWorkOnOrBeforeToday(phenology)) return true;
|
|
|
|
|
+ if (agriRecordHasPendingAuthInPhenology(phenology)) return true;
|
|
|
|
|
+ if (phenologyAllDatedFarmWorksStrictlyFuture(phenology)) return false;
|
|
|
|
|
+ return shouldShowBlueBase(phenology);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 左侧生育期/物候期文案:
|
|
|
|
|
+// 1) 本行有农事<=今天:蓝;
|
|
|
|
|
+// 2) 本行全部有效日期农事均为未来且本行无待认证:灰(与右侧 future-card 一致);
|
|
|
|
|
+// 3) 农事记录下物候期内有待认证:蓝;
|
|
|
|
|
+// 4) 其余走节气/日期底色规则。
|
|
|
|
|
+const shouldShowBlueLeft = (phenology, reproductive) => {
|
|
|
|
|
+ if (reproductiveHasFarmWorkOnOrBeforeToday(reproductive)) return true;
|
|
|
|
|
+ if (reproductiveAllDatedFarmWorksStrictlyFuture(reproductive) && !reproductiveHasPendingAuthFarmWork(reproductive)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (agriRecordHasPendingAuthInPhenology(phenology)) return true;
|
|
|
|
|
+ return shouldShowBlueBase(phenology);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ updateFarmWorkPlan,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 使用 ResizeObserver 监听高度变化,确保在DOM完全渲染后获取准确高度
|
|
|
|
|
+const setupResizeObserver = () => {
|
|
|
|
|
+ if (!timelineListRef.value || typeof ResizeObserver === "undefined") {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果已经存在观察者,先断开
|
|
|
|
|
+ if (resizeObserver) {
|
|
|
|
|
+ resizeObserver.disconnect();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建新的观察者
|
|
|
|
|
+ resizeObserver = new ResizeObserver((entries) => {
|
|
|
|
|
+ for (const entry of entries) {
|
|
|
|
|
+ const height = entry.contentRect.height;
|
|
|
|
|
+ if (height > 0 && height !== timelineListHeight.value) {
|
|
|
|
|
+ timelineListHeight.value = height;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 开始观察
|
|
|
|
|
+ resizeObserver.observe(timelineListRef.value);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 组件挂载后设置 ResizeObserver
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ setupResizeObserver();
|
|
|
|
|
+ restoreTimelineScrollTopWithRetry();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 组件卸载前清理 ResizeObserver
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ saveTimelineScrollTop();
|
|
|
|
|
+ if (resizeObserver) {
|
|
|
|
|
+ resizeObserver.disconnect();
|
|
|
|
|
+ resizeObserver = null;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+onActivated(() => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ restoreTimelineScrollTopWithRetry();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+onDeactivated(() => {
|
|
|
|
|
+ saveTimelineScrollTop();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+onBeforeRouteLeave(() => {
|
|
|
|
|
+ saveTimelineScrollTop();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 在数据更新后重新设置 ResizeObserver
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => phenologyList.value.length,
|
|
|
|
|
+ () => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ setupResizeObserver();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+@mixin arrange-card-status($color, $title-color: null, $title-bg: null, $text-color: null) {
|
|
|
|
|
+ border-color: $color;
|
|
|
|
|
+
|
|
|
|
|
+ .card-content {
|
|
|
|
|
+ @if $text-color !=null {
|
|
|
|
|
+ color: $text-color;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-left {
|
|
|
|
|
+ .left-info {
|
|
|
|
|
+ .left-date {
|
|
|
|
|
+ color: $color;
|
|
|
|
|
+ border-color: $color;
|
|
|
|
|
+ }
|
|
|
|
|
+ .status-tag {
|
|
|
|
|
+ background: $color;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .title-text {
|
|
|
|
|
+ @if $title-color !=null {
|
|
|
|
|
+ color: $title-color;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @if $title-bg !=null {
|
|
|
|
|
+ background: $title-bg;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ border-right-color: $color;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.timeline-container {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+
|
|
|
|
|
+ .timeline-list {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .timeline-middle-line {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 13px;
|
|
|
|
|
+ /* 位于节气文字列中间(列宽约30px) */
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ width: 2px;
|
|
|
|
|
+ background: #e8e8e8;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .phenology-bar {
|
|
|
|
|
+ align-items: stretch;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+
|
|
|
|
|
+ .phenology-title {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ writing-mode: vertical-rl;
|
|
|
|
|
+ text-orientation: upright;
|
|
|
|
|
+ letter-spacing: 3px;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+
|
|
|
|
|
+ &.phenology-blue {
|
|
|
|
|
+ background: #2199f8;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.phenology-red {
|
|
|
|
|
+ background: #f1f1f1;
|
|
|
|
|
+ color: #808080;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.phenology-title--wide {
|
|
|
|
|
+ width: 39px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .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;
|
|
|
|
|
+
|
|
|
|
|
+ .phenology-name {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ line-height: 16px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ padding: 4px 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ writing-mode: vertical-rl;
|
|
|
|
|
+ text-orientation: upright;
|
|
|
|
|
+
|
|
|
|
|
+ &.mr {
|
|
|
|
|
+ margin-right: 3px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.single {
|
|
|
|
|
+ width: 39px;
|
|
|
|
|
+ line-height: 39px;
|
|
|
|
|
+
|
|
|
|
|
+ &.text-blue,
|
|
|
|
|
+ &.text-red {
|
|
|
|
|
+ line-height: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.phenology-blue {
|
|
|
|
|
+ background: #2199f8;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.phenology-red {
|
|
|
|
|
+ background: #f1f1f1;
|
|
|
|
|
+ color: #808080;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.text-blue {
|
|
|
|
|
+ background: rgba(33, 153, 248, 0.15);
|
|
|
|
|
+ color: #2199f8;
|
|
|
|
|
+ border: 1px solid #2199f8;
|
|
|
|
|
+ line-height: 16px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.text-red {
|
|
|
|
|
+ background: rgba(128, 128, 128, 0.15);
|
|
|
|
|
+ color: #808080;
|
|
|
|
|
+ border: 1px solid rgba(128, 128, 128, 0.35);
|
|
|
|
|
+ line-height: 16px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arranges {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ max-width: calc(100vw - 88px);
|
|
|
|
|
+ min-width: calc(100vw - 83px);
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+ letter-spacing: 0px;
|
|
|
|
|
+
|
|
|
|
|
+ &.arranges-min {
|
|
|
|
|
+ max-width: calc(100vw - 58px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // min-height: 90px;
|
|
|
|
|
+ .arrange-card {
|
|
|
|
|
+ width: 95%;
|
|
|
|
|
+ border: 0.5px solid #2199f8;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ padding: 8px 15px 8px 10px;
|
|
|
|
|
+ writing-mode: horizontal-tb;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ // &.last-card {
|
|
|
|
|
+ // margin-bottom: 0;
|
|
|
|
|
+ // }
|
|
|
|
|
+ .card-content {
|
|
|
|
|
+ color: #242424;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+
|
|
|
|
|
+ .card-left {
|
|
|
|
|
+ width: calc(100% - 45px);
|
|
|
|
|
+
|
|
|
|
|
+ .left-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ .left-date {
|
|
|
|
|
+ color: #2199f8;
|
|
|
|
|
+ border: 1px solid #2199f8;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ line-height: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status-tag {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ background: #47B881;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ width: 63px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .text {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ width: calc(100% - 115px);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .title-text {
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ width: fit-content;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ color: #2199F8;
|
|
|
|
|
+ padding: 0 6px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ background: rgba(33, 153, 248, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.agri-record-card {
|
|
|
|
|
+ .text{
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+ .title-wrap {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ .expert-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #B7B7B7;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .blue-tag {
|
|
|
|
|
+ color: #2199F8;
|
|
|
|
|
+ background: rgba(33, 153, 248, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-right {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 45px;
|
|
|
|
|
+ height: 45px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .num {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ top: -4px;
|
|
|
|
|
+ right: -6px;
|
|
|
|
|
+ background: #BFBFBF;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status-right {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ background: #2199F8;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ padding: 0 5px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: "";
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: -5px;
|
|
|
|
|
+ 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.3;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // .arrange-card.status-normal {
|
|
|
|
|
+ // border-color: #2199f8;
|
|
|
|
|
+
|
|
|
|
|
+ // &::before {
|
|
|
|
|
+ // border-right-color: #2199f8;
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-normal {
|
|
|
|
|
+ @include arrange-card-status(#2199f8, #2199f8, rgba(33, 153, 248, 0.1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-warning {
|
|
|
|
|
+ @include arrange-card-status(#FF4E4E, #FF4E4E, rgba(255, 78, 78, 0.1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-warning.status-warning-agri-record {
|
|
|
|
|
+ border-color: #bbbbbb;
|
|
|
|
|
+
|
|
|
|
|
+ .card-left {
|
|
|
|
|
+ .left-info {
|
|
|
|
|
+ .left-date {
|
|
|
|
|
+ color: #bbbbbb;
|
|
|
|
|
+ border-color: #bbbbbb;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .text-name,
|
|
|
|
|
+ .text-icon {
|
|
|
|
|
+ color: #bbbbbb;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ border-right-color: #bbbbbb;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-complete {
|
|
|
|
|
+ @include arrange-card-status(#FF943D, #FF943D, rgba(255, 149, 61, 0.1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-green-info {
|
|
|
|
|
+ @include arrange-card-status(#47B881, #fff, rgba(71, 184, 129, 0.1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-orange {
|
|
|
|
|
+ @include arrange-card-status(#FFB129, #FFB129, rgba(253, 181, 55, 0.1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-green-farm {
|
|
|
|
|
+ @include arrange-card-status(#5BB349, #5BB349, rgba(91, 179, 73, 0.1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-act {
|
|
|
|
|
+ @include arrange-card-status(#FF953D, #fff, #FF953D);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-default {
|
|
|
|
|
+ @include arrange-card-status(#BBBBBB, #fff, #BBBBBB);
|
|
|
|
|
+
|
|
|
|
|
+ .card-left {
|
|
|
|
|
+ .left-info {
|
|
|
|
|
+
|
|
|
|
|
+ .text-name,
|
|
|
|
|
+ .text-icon {
|
|
|
|
|
+ color: #BBBBBB;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 未来节气对应的农事卡片:跟随左侧物候期的“未开始”灰色样式
|
|
|
|
|
+ .arrange-card.future-card {
|
|
|
|
|
+ @include arrange-card-status(#e4e4e4, null, null, rgba(36, 36, 36, 0.5));
|
|
|
|
|
+
|
|
|
|
|
+ .card-left {
|
|
|
|
|
+ .left-info {
|
|
|
|
|
+ .left-date {
|
|
|
|
|
+ color: #CACACA;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .arrange-card.status-warning-bg {
|
|
|
|
|
+ @include arrange-card-status(#FF943D, #fff, #FF943D, #000);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .reproductive-item+.reproductive-item {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ padding-top: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .phenology-bar+.phenology-bar {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ padding-top: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .timeline-term {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 34px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ z-index: 2;
|
|
|
|
|
+ /* 置于中线之上 */
|
|
|
|
|
+ color: rgba(174, 174, 174, 0.6);
|
|
|
|
|
+
|
|
|
|
|
+ .term-name {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 20px;
|
|
|
|
|
+ line-height: 26px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .empty-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ min-height: 200px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|