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