|
|
@@ -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>
|