FarmWorkPlanTimeline.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. <template>
  2. <div class="timeline-container" ref="timelineContainerRef">
  3. <div class="timeline-list" ref="timelineListRef">
  4. <div class="timeline-middle-line"></div>
  5. <div
  6. v-for="(t, tIdx) in solarTerms"
  7. :key="`term-${uniqueTimestamp}-${tIdx}`"
  8. class="timeline-term"
  9. :style="getTermStyle(t, tIdx)"
  10. >
  11. <span class="term-name">{{ t.displayName }}</span>
  12. </div>
  13. <div v-for="(p, idx) in phenologyList" :key="`phenology-${uniqueTimestamp}-${idx}`" class="phenology-bar">
  14. <div
  15. v-for="(r, rIdx) in Array.isArray(p.reproductiveList) ? p.reproductiveList : []"
  16. :key="`reproductive-${uniqueTimestamp}-${idx}-${rIdx}`"
  17. class="reproductive-item"
  18. >
  19. <div class="arranges">
  20. <div
  21. v-for="(fw, aIdx) in Array.isArray(r.farmWorkArrangeList) ? r.farmWorkArrangeList : []"
  22. :key="`arrange-${uniqueTimestamp}-${idx}-${rIdx}-${aIdx}`"
  23. class="arrange-card"
  24. :style="shouldShowIncompleteStatus(fw.farmWorkId) ? 'padding-bottom: 18px;' : 'padding-bottom: 10px'"
  25. :class="getArrangeStatusClass(fw)"
  26. @click="handleRowClick(fw)"
  27. >
  28. <div class="card-header">
  29. <div class="header-left">
  30. <span class="farm-work-name">{{ fw.farmWorkName || "--" }}</span>
  31. <span class="tag-standard">{{ farmWorkType[fw.type] }}</span>
  32. </div>
  33. <div class="header-right" v-if="!isStandard">
  34. {{ fw.isFollow == 1 ? "已关注" : fw.isFollow == 2 ? "托管农事" : "" }}
  35. </div>
  36. </div>
  37. <div class="card-content">
  38. <span>{{ fw.interactionQuestion || "暂无提示" }}</span>
  39. <span v-if="!disableClick" :class="shouldShowIncompleteStatus(fw.farmWorkId) ? 'link-warning' : 'edit-link'" @click.stop="handleEdit(fw)"
  40. >点击编辑</span
  41. >
  42. </div>
  43. <div class="card-status" v-if="shouldShowIncompleteStatus(fw.farmWorkId)">
  44. <el-icon color="#FF953D" size="13"><WarningFilled /></el-icon>
  45. <span>未完善</span>
  46. </div>
  47. </div>
  48. </div>
  49. <div class="phenology-name">{{ r.name }}</div>
  50. </div>
  51. </div>
  52. </div>
  53. </div>
  54. <!-- 互动设置弹窗 -->
  55. <interact-popup ref="interactPopupRef" @handleSaveSuccess="updateFarmWorkPlan"></interact-popup>
  56. </template>
  57. <script setup>
  58. import { ref, nextTick, watch, onMounted, onUnmounted } from "vue";
  59. import interactPopup from "@/components/popup/interactPopup.vue";
  60. import { ElMessage } from "element-plus";
  61. import { WarningFilled } from "@element-plus/icons-vue";
  62. const props = defineProps({
  63. // 农场 ID,用于请求农事规划数据
  64. farmId: {
  65. type: [String, Number],
  66. default: null,
  67. },
  68. // 页面类型:种植方案 / 农事规划,用来控制高度样式
  69. pageType: {
  70. type: String,
  71. default: "",
  72. },
  73. // 是否禁用所有点击事件(用于只读展示)
  74. disableClick: {
  75. type: Boolean,
  76. default: false,
  77. },
  78. containerId: {
  79. type: [Number, String],
  80. default: null,
  81. },
  82. // 是否是标准农事
  83. isStandard: {
  84. type: Boolean,
  85. default: false,
  86. },
  87. // 方案ID
  88. schemeId: {
  89. type: [Number, String],
  90. default: null,
  91. },
  92. });
  93. const farmWorkType = {
  94. 0: "预警农事",
  95. 1: "标准农事",
  96. 2: "建议农事",
  97. 3: "自建农事",
  98. };
  99. const emits = defineEmits(["row-click"]);
  100. const solarTerms = ref([]);
  101. const phenologyList = ref([]);
  102. const timelineContainerRef = ref(null);
  103. const timelineListRef = ref(null);
  104. // 标记是否为首次加载
  105. const isInitialLoad = ref(true);
  106. // 存储timeline-list的实际渲染高度
  107. const timelineListHeight = ref(0);
  108. // 生成唯一的时间戳,用于确保key的唯一性
  109. const uniqueTimestamp = ref(Date.now());
  110. // ResizeObserver 实例,用于监听高度变化
  111. let resizeObserver = null;
  112. // 获取当前季节
  113. const getCurrentSeason = () => {
  114. const month = new Date().getMonth() + 1; // 1-12
  115. if (month >= 3 && month <= 5) {
  116. return "spring"; // 春季:3-5月
  117. } else if (month >= 6 && month <= 8) {
  118. return "summer"; // 夏季:6-8月
  119. } else if (month >= 9 && month <= 10) {
  120. return "autumn"; // 秋季:9-10月
  121. } else {
  122. return "winter"; // 冬季:11-2月
  123. }
  124. };
  125. // 安全解析时间到时间戳(ms)
  126. const safeParseDate = (val) => {
  127. if (!val) return NaN;
  128. if (val instanceof Date) return val.getTime();
  129. if (typeof val === "number") return val;
  130. if (typeof val === "string") {
  131. // 兼容 "YYYY-MM-DD HH:mm:ss" -> Safari
  132. const s = val.replace(/-/g, "/").replace("T", " ");
  133. const d = new Date(s);
  134. return isNaN(d.getTime()) ? NaN : d.getTime();
  135. }
  136. return NaN;
  137. };
  138. const batchValidateData = ref({});
  139. // 验证农事卡片药肥报价信息是否完整
  140. const batchValidatePesticideFertilizerQuotes = (ids) => {
  141. if (props.isStandard) {
  142. return;
  143. }
  144. VE_API.monitor
  145. .batchValidatePesticideFertilizerQuotes({ ids, schemeId: props.schemeId })
  146. .then(({ data,code }) => {
  147. if (code === 0) {
  148. batchValidateData.value = data || {};
  149. }
  150. })
  151. .catch(() => {});
  152. };
  153. // 判断是否应该显示"未完善"状态
  154. // 如果batchValidateData中对应的farmWorkId验证结果为true(已完善),则不显示
  155. // 如果验证结果为false(未完善)或不存在,则显示
  156. const shouldShowIncompleteStatus = (farmWorkId) => {
  157. if (!farmWorkId || !batchValidateData.value || typeof batchValidateData.value !== 'object') {
  158. // 如果没有验证数据,默认不显示
  159. return false;
  160. }
  161. // 从对象中直接获取验证结果,支持字符串和数字类型的key
  162. const isValid = batchValidateData.value[farmWorkId] !== undefined
  163. ? batchValidateData.value[farmWorkId]
  164. : batchValidateData.value[String(farmWorkId)] !== undefined
  165. ? batchValidateData.value[String(farmWorkId)]
  166. : undefined;
  167. // 如果找到了验证结果
  168. if (isValid !== undefined) {
  169. // 如果验证结果为true(已完善),返回false(不显示)
  170. // 如果验证结果为false(未完善),返回true(显示)
  171. return !isValid;
  172. }
  173. // 如果没有找到验证结果,默认不显示
  174. return false;
  175. };
  176. // 计算物候期需要的实际高度(基于农事数量)
  177. const getPhenologyRequiredHeight = (item) => {
  178. // 统计该物候期内的农事数量
  179. let farmWorkCount = 0;
  180. if (Array.isArray(item.reproductiveList)) {
  181. item.reproductiveList.forEach((reproductive) => {
  182. if (Array.isArray(reproductive.farmWorkArrangeList)) {
  183. farmWorkCount += reproductive.farmWorkArrangeList.length;
  184. }
  185. });
  186. }
  187. // 如果没有农事,给一个最小高度
  188. if (farmWorkCount === 0) {
  189. return 50; // 最小50px
  190. }
  191. // 每个农事卡片的高度(根据实际内容,卡片高度可能因内容而异)
  192. // 卡片包含:padding(8px*2) + header(约25px) + content margin(4px+2px) + content(约25-30px) = 约72-77px
  193. // 考虑到内容可能换行,实际高度可能更高,设置为120px更安全,避免卡片重叠
  194. const farmWorkCardHeight = 120; // 卡片高度估算,确保能容纳内容且不重叠
  195. // 卡片之间的间距(与CSS中的gap保持一致)
  196. const cardGap = 12;
  197. // 计算总高度:卡片数量 * 卡片高度 + (卡片数量 - 1) * 间距
  198. // 如果有多个卡片,需要加上它们之间的间距
  199. const totalHeight = farmWorkCount * farmWorkCardHeight + (farmWorkCount > 1 ? (farmWorkCount - 1) * cardGap : 0);
  200. // 返回精确的总高度,只保留最小高度限制,不添加额外余量
  201. return Math.max(totalHeight, 50); // 最小50px,精确匹配农事卡片高度
  202. };
  203. // 计算所有物候期的累积位置和总高度
  204. const calculatePhenologyPositions = () => {
  205. let currentTop = 10; // 起始位置,留出顶部间距
  206. const positions = new Map();
  207. // 按progress排序物候期,确保按时间顺序排列
  208. const sortedPhenologyList = [...phenologyList.value].sort((a, b) => {
  209. const aProgress = Math.min(Number(a?.progress) || 0, Number(a?.progress2) || 0);
  210. const bProgress = Math.min(Number(b?.progress) || 0, Number(b?.progress2) || 0);
  211. return aProgress - bProgress;
  212. });
  213. sortedPhenologyList.forEach((phenology) => {
  214. const height = getPhenologyRequiredHeight(phenology);
  215. // 使用与数据生成时相同的ID生成逻辑
  216. const itemId =
  217. phenology.id ?? phenology.phenologyId ?? phenology.name ?? `${phenology.progress}-${phenology.progress2}`;
  218. positions.set(itemId, {
  219. top: currentTop,
  220. height: height,
  221. });
  222. currentTop += height; // 紧挨着下一个物候期,不留间距
  223. });
  224. return {
  225. positions,
  226. totalHeight: currentTop, // 总高度 = 最后一个物候期的底部位置,不添加额外间距
  227. };
  228. };
  229. // 计算所有农事的总高度(基于物候期紧挨排列)
  230. const calculateTotalHeightByFarmWorks = () => {
  231. const { totalHeight } = calculatePhenologyPositions();
  232. // 如果有物候期数据,直接使用计算出的总高度
  233. // totalHeight 已经包含了从 10 开始的起始位置和所有物候期的高度
  234. if (totalHeight > 10) {
  235. // 确保总高度至少能容纳所有节气(每个节气至少50px)
  236. const baseHeight = (solarTerms.value?.length || 0) * 50;
  237. // 返回物候期总高度和基础高度的较大值,确保节气能正常显示
  238. return Math.max(totalHeight, baseHeight);
  239. }
  240. // 如果没有物候期数据,返回基础高度
  241. const baseHeight = (solarTerms.value?.length || 0) * 50;
  242. return baseHeight || 100; // 至少返回100px,避免为0
  243. };
  244. const getTermStyle = (t, index) => {
  245. // 优先使用实际测量的timeline-list高度,如果没有测量到则使用计算值作为后备
  246. const totalHeight = timelineListHeight.value > 0 ? timelineListHeight.value : calculateTotalHeightByFarmWorks();
  247. // 获取节气总数
  248. const termCount = solarTerms.value?.length || 1;
  249. // 等分高度:总高度 / 节气数量
  250. const termHeight = totalHeight / termCount;
  251. // 计算top位置:索引 * 每个节气的高度
  252. const top = index * termHeight;
  253. return {
  254. position: "absolute",
  255. top: `${top}px`,
  256. left: 0,
  257. width: "30px",
  258. height: `${termHeight}px`, // 高度等分,使用实际测量的高度
  259. display: "flex",
  260. alignItems: "center",
  261. };
  262. };
  263. // 点击季节 → 滚动到对应节气(立春/立夏/立秋/立冬)
  264. const handleSeasonClick = (seasonValue) => {
  265. const mapping = {
  266. spring: "立春",
  267. summer: "立夏",
  268. autumn: "立秋",
  269. winter: "立冬",
  270. };
  271. const targetName = mapping[seasonValue];
  272. if (!targetName) return;
  273. // 查找对应的节气
  274. const targetIndex = solarTerms.value.findIndex((t) => (t?.displayName || "") === targetName);
  275. if (targetIndex === -1) return;
  276. // 计算目标节气的top位置
  277. const totalHeight = timelineListHeight.value > 0 ? timelineListHeight.value : calculateTotalHeightByFarmWorks();
  278. const termCount = solarTerms.value?.length || 1;
  279. const termHeight = totalHeight / termCount;
  280. const targetTop = targetIndex * termHeight;
  281. // 滚动到目标位置
  282. const wrap = timelineContainerRef.value;
  283. if (!wrap) return;
  284. const viewH = wrap.clientHeight || 0;
  285. const maxScroll = Math.max(0, wrap.scrollHeight - viewH);
  286. // 将目标位置稍微靠上(使用 0.1 视口高度做偏移)
  287. let scrollTop = Math.max(0, targetTop - viewH * 0.1);
  288. if (scrollTop > maxScroll) scrollTop = maxScroll;
  289. wrap.scrollTo({ top: scrollTop, behavior: "smooth" });
  290. };
  291. // 农事状态样式映射(0:取消关注,1:关注,2:托管农事,)
  292. const getArrangeStatusClass = (fw) => {
  293. const t = fw?.isFollow;
  294. const warningStatus = shouldShowIncompleteStatus(fw.farmWorkId);
  295. if (warningStatus) return "status-warning";
  296. if (t == 0) return "normal-style";
  297. // if (t >= 0 && t <= 4) return "status-normal";
  298. // if (t === 5) return "status-complete";
  299. return "status-normal";
  300. };
  301. const handleRowClick = (item) => {
  302. emits("row-click", item);
  303. };
  304. const interactPopupRef = ref(null);
  305. const handleEdit = (item) => {
  306. if (props.disableClick) return;
  307. if (interactPopupRef.value) {
  308. interactPopupRef.value.showPopup(item);
  309. }
  310. };
  311. // 获取农事规划数据
  312. const getFarmWorkPlan = () => {
  313. if (!props.farmId && !props.containerId) return;
  314. // 更新时间戳,确保key变化,触发DOM重新渲染
  315. uniqueTimestamp.value = Date.now();
  316. // 重置测量高度,等待重新测量
  317. timelineListHeight.value = 0;
  318. let savedScrollTop = 0;
  319. if (!isInitialLoad.value && timelineContainerRef.value) {
  320. savedScrollTop = timelineContainerRef.value.scrollTop || 0;
  321. }
  322. VE_API.monitor
  323. .farmWorkPlan({ farmId: props.farmId, containerId: props.containerId, schemeId: props.schemeId })
  324. .then(({ data, code }) => {
  325. if (code === 0) {
  326. const list = Array.isArray(data?.solarTermsList) ? data.solarTermsList : [];
  327. const filtered = list
  328. .filter((t) => t && t.type === 1)
  329. .map((t) => ({
  330. id:
  331. t.id ??
  332. t.solarTermsId ??
  333. t.termId ??
  334. `${t.name || t.solarTermsName || t.termName || "term"}-${t.createDate || ""}`,
  335. displayName: t.name || t.solarTermsName || t.termName || "节气",
  336. createDate: t.createDate || null,
  337. progress: Number(t.progress) || 0,
  338. }));
  339. solarTerms.value = filtered;
  340. // 物候期数据
  341. phenologyList.value = Array.isArray(data?.phenologyList)
  342. ? data.phenologyList.map((it) => {
  343. const reproductiveList = Array.isArray(it.reproductiveList)
  344. ? it.reproductiveList.map((r) => {
  345. const farmWorkArrangeList = Array.isArray(r.farmWorkArrangeList)
  346. ? r.farmWorkArrangeList.map((fw) => ({
  347. ...fw,
  348. containerSpaceTimeId: it.containerSpaceTimeId,
  349. }))
  350. : [];
  351. return {
  352. ...r,
  353. farmWorkArrangeList,
  354. };
  355. })
  356. : [];
  357. return {
  358. id: it.id ?? it.phenologyId ?? it.name ?? `${it.progress}-${it.progress2}`,
  359. progress: Number(it.progress) || 0, // 起点 %
  360. progress2: Number(it.progress2) || 0, // 终点 %
  361. startTimeMs: safeParseDate(
  362. it.startDate || it.beginDate || it.startTime || it.start || it.start_at
  363. ),
  364. reproductiveList,
  365. };
  366. })
  367. : [];
  368. // 使用多次 nextTick 和 requestAnimationFrame 确保DOM完全渲染
  369. nextTick(() => {
  370. requestAnimationFrame(() => {
  371. nextTick(() => {
  372. requestAnimationFrame(() => {
  373. // 测量timeline-list的实际渲染高度
  374. if (timelineListRef.value) {
  375. const height = timelineListRef.value.offsetHeight || timelineListRef.value.clientHeight;
  376. if (height > 0) {
  377. timelineListHeight.value = height;
  378. // 如果是首次加载,滚动到当前季节对应的节气
  379. if (isInitialLoad.value) {
  380. const currentSeason = getCurrentSeason();
  381. handleSeasonClick(currentSeason);
  382. isInitialLoad.value = false;
  383. }
  384. }
  385. }
  386. if (isInitialLoad.value) {
  387. // 如果测量失败,延迟一下再尝试滚动
  388. setTimeout(() => {
  389. if (timelineListRef.value) {
  390. const height = timelineListRef.value.offsetHeight || timelineListRef.value.clientHeight;
  391. if (height > 0) {
  392. timelineListHeight.value = height;
  393. }
  394. }
  395. const currentSeason = getCurrentSeason();
  396. handleSeasonClick(currentSeason);
  397. isInitialLoad.value = false;
  398. }, 200);
  399. } else if (timelineContainerRef.value && savedScrollTop > 0) {
  400. timelineContainerRef.value.scrollTop = savedScrollTop;
  401. }
  402. });
  403. });
  404. });
  405. });
  406. // 收集所有farmWorkId
  407. const farmWorkIds = [];
  408. phenologyList.value.forEach((phenology) => {
  409. if (Array.isArray(phenology.reproductiveList)) {
  410. phenology.reproductiveList.forEach((reproductive) => {
  411. if (Array.isArray(reproductive.farmWorkArrangeList)) {
  412. reproductive.farmWorkArrangeList.forEach((farmWork) => {
  413. if (
  414. farmWork.farmWorkId != null &&
  415. farmWork.farmWorkId !== undefined &&
  416. farmWork.farmWorkId !== ""
  417. ) {
  418. farmWorkIds.push(farmWork.farmWorkId);
  419. }
  420. });
  421. }
  422. });
  423. }
  424. });
  425. // 调用验证方法,传入所有ids
  426. if (farmWorkIds.length > 0) {
  427. batchValidatePesticideFertilizerQuotes(farmWorkIds);
  428. }
  429. }
  430. })
  431. .catch((error) => {
  432. console.error("获取农事规划数据失败:", error);
  433. ElMessage.error("获取农事规划数据失败");
  434. });
  435. };
  436. const updateFarmWorkPlan = () => {
  437. solarTerms.value = [];
  438. phenologyList.value = [];
  439. getFarmWorkPlan();
  440. };
  441. watch(
  442. () => props.farmId || props.containerId,
  443. (val) => {
  444. if (val) {
  445. isInitialLoad.value = true;
  446. updateFarmWorkPlan();
  447. }
  448. },
  449. { immediate: true }
  450. );
  451. watch(
  452. () => props.schemeId,
  453. (val) => {
  454. if (val) {
  455. updateFarmWorkPlan();
  456. }
  457. }
  458. );
  459. // 使用 ResizeObserver 监听高度变化,确保在DOM完全渲染后获取准确高度
  460. const setupResizeObserver = () => {
  461. if (!timelineListRef.value || typeof ResizeObserver === 'undefined') {
  462. return;
  463. }
  464. // 如果已经存在观察者,先断开
  465. if (resizeObserver) {
  466. resizeObserver.disconnect();
  467. }
  468. // 创建新的观察者
  469. resizeObserver = new ResizeObserver((entries) => {
  470. for (const entry of entries) {
  471. const height = entry.contentRect.height;
  472. if (height > 0 && height !== timelineListHeight.value) {
  473. timelineListHeight.value = height;
  474. }
  475. }
  476. });
  477. // 开始观察
  478. resizeObserver.observe(timelineListRef.value);
  479. };
  480. // 组件挂载后设置 ResizeObserver
  481. onMounted(() => {
  482. nextTick(() => {
  483. requestAnimationFrame(() => {
  484. setupResizeObserver();
  485. });
  486. });
  487. });
  488. // 组件卸载前清理 ResizeObserver
  489. onUnmounted(() => {
  490. if (resizeObserver) {
  491. resizeObserver.disconnect();
  492. resizeObserver = null;
  493. }
  494. });
  495. // 在数据更新后重新设置 ResizeObserver
  496. watch(
  497. () => phenologyList.value.length,
  498. () => {
  499. nextTick(() => {
  500. requestAnimationFrame(() => {
  501. setupResizeObserver();
  502. });
  503. });
  504. }
  505. );
  506. </script>
  507. <style scoped lang="scss">
  508. .timeline-container {
  509. height: 100%;
  510. overflow: auto;
  511. position: relative;
  512. box-sizing: border-box;
  513. .timeline-list {
  514. position: relative;
  515. }
  516. .timeline-middle-line {
  517. position: absolute;
  518. left: 15px; /* 位于节气文字列中间(列宽约30px) */
  519. top: 0;
  520. bottom: 0;
  521. width: 2px;
  522. background: #e8e8e8;
  523. z-index: 1;
  524. }
  525. .phenology-bar {
  526. align-items: stretch;
  527. justify-content: center;
  528. box-sizing: border-box;
  529. .reproductive-item {
  530. font-size: 12px;
  531. text-align: center;
  532. word-break: break-all;
  533. writing-mode: vertical-rl;
  534. text-orientation: upright;
  535. letter-spacing: 3px;
  536. width: 100%;
  537. line-height: 23px;
  538. color: inherit;
  539. position: relative;
  540. .phenology-name {
  541. width: 26px;
  542. height: 100%;
  543. background: #2199f8;
  544. color: #fff;
  545. margin-right: 18px;
  546. border-radius: 2px;
  547. }
  548. .arranges {
  549. display: flex;
  550. max-width: calc(100vw - 84px);
  551. gap: 10px;
  552. letter-spacing: 0px;
  553. .arrange-card {
  554. width: 93%;
  555. border: 0.5px solid #2199f8;
  556. border-radius: 8px;
  557. background: #fff;
  558. box-sizing: border-box;
  559. position: relative;
  560. padding: 8px;
  561. writing-mode: horizontal-tb;
  562. .card-header {
  563. display: flex;
  564. justify-content: space-between;
  565. align-items: center;
  566. .header-left {
  567. display: flex;
  568. align-items: center;
  569. gap: 8px;
  570. .farm-work-name {
  571. font-size: 14px;
  572. font-weight: 500;
  573. color: #1d2129;
  574. }
  575. .tag-standard {
  576. padding: 0 8px;
  577. background: rgba(119, 119, 119, 0.1);
  578. border-radius: 25px;
  579. font-weight: 400;
  580. font-size: 12px;
  581. color: #000;
  582. }
  583. }
  584. .header-right {
  585. font-size: 12px;
  586. color: #808080;
  587. padding: 0 8px;
  588. border-radius: 25px;
  589. }
  590. }
  591. .card-content {
  592. color: #909090;
  593. text-align: left;
  594. line-height: 1.55;
  595. margin: 4px 0 2px 0;
  596. .edit-link {
  597. color: #2199f8;
  598. margin-left: 5px;
  599. }
  600. .link-warning{
  601. color: #ff953d;
  602. margin-left: 5px;
  603. }
  604. }
  605. .card-status {
  606. position: absolute;
  607. bottom: 0;
  608. right: 0;
  609. background: rgba(255, 149, 61, 0.1);
  610. border-radius: 3px;
  611. color: #ff953d;
  612. padding: 0 6px;
  613. display: flex;
  614. align-items: center;
  615. gap: 4px;
  616. font-size: 12px;
  617. }
  618. &::before {
  619. content: "";
  620. position: absolute;
  621. left: -5px;
  622. top: 50%;
  623. transform: translateY(-50%);
  624. width: 0;
  625. height: 0;
  626. border-top: 5px solid transparent;
  627. border-bottom: 5px solid transparent;
  628. border-right: 5px solid #2199f8;
  629. }
  630. }
  631. .arrange-card.normal-style {
  632. opacity: 0.3;
  633. }
  634. .arrange-card.status-warning {
  635. border-color: #ff953d;
  636. &::before {
  637. border-right-color: #ff953d;
  638. }
  639. }
  640. .arrange-card.status-complete {
  641. border-color: #1ca900;
  642. &::before {
  643. border-right-color: #1ca900;
  644. }
  645. }
  646. .arrange-card.status-normal {
  647. border-color: #2199f8;
  648. &::before {
  649. border-right-color: #2199f8;
  650. }
  651. }
  652. }
  653. }
  654. }
  655. .reproductive-item + .reproductive-item {
  656. padding-top: 3px;
  657. }
  658. .phenology-bar + .phenology-bar {
  659. padding-top: 3px;
  660. }
  661. .timeline-term {
  662. position: absolute;
  663. width: 30px;
  664. padding-right: 16px;
  665. display: flex;
  666. align-items: flex-start;
  667. z-index: 2; /* 置于中线之上 */
  668. .term-name {
  669. display: inline-block;
  670. width: 100%;
  671. min-height: 35px;
  672. line-height: 30px;
  673. background: #f5f7fb;
  674. font-size: 13px;
  675. word-break: break-all;
  676. writing-mode: vertical-rl;
  677. text-orientation: upright;
  678. color: rgba(174, 174, 174, 0.6);
  679. text-align: center;
  680. }
  681. }
  682. }
  683. </style>