index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <template>
  2. <custom-header v-if="isHeaderShow" name="农场详情"></custom-header>
  3. <div class="monitor-index" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
  4. <!-- 天气遮罩 -->
  5. <div class="weather-mask" v-show="isExpanded" @click="handleMaskClick"></div>
  6. <!-- 天气 -->
  7. <weather-info ref="weatherInfoRef" from="agri_record" class="weather-info" @weatherExpanded="weatherExpanded"
  8. @changeGarden="changeGarden" @changeGardenTab="changeGardenTab" :isGarden="true"
  9. :gardenId="defaultGardenId"></weather-info>
  10. <!-- 农场列表 -->
  11. <div v-show="activeGardenTab === 'list'">
  12. <garden-list ref="gardenListRef" :garden-id="selectedGardenId" @loaded="handleGardenLoaded"
  13. @selectGarden="handleGardenSelected" />
  14. </div>
  15. <!-- 作物档案:外层可滚动,滚动位置单独缓存以便路由/Tab 返回后恢复 -->
  16. <div ref="archivesScrollAreaRef" class="archives-time-line" v-show="activeGardenTab === 'current'">
  17. <div class="trend-monitor-list">
  18. <div class="trend-monitor-card" @click="handleTrendMonitorCardClick(item)"
  19. v-for="(item, index) in trendMonitorMockList" :key="index">
  20. <div class="card-header">
  21. <div class="header-left">
  22. <span class="title">{{ item?.first_work?.work_name }}</span>
  23. <span class="level-tag">二级</span>
  24. </div>
  25. <div class="status-tag" v-if="item?.risk_level">待记录</div>
  26. </div>
  27. <div class="card-row">
  28. <div class="reason-text">{{ item?.first_work?.work_reason_short }}</div>
  29. <div class="question-tag">{{ item?.first_work?.interaction_issue }}</div>
  30. </div>
  31. <!-- <div class="record-list">
  32. <div class="record-item" v-for="(record, recordIndex) in item.records" :key="recordIndex">
  33. {{ record }}
  34. </div>
  35. </div> -->
  36. </div>
  37. </div>
  38. <div class="archives-time-line-content">
  39. <archives-farm-time-line :farmId="gardenId"></archives-farm-time-line>
  40. </div>
  41. </div>
  42. </div>
  43. </template>
  44. <script setup>
  45. import customHeader from "@/components/customHeader.vue";
  46. import { ref, computed, onActivated, onDeactivated, watch, nextTick } from "vue";
  47. import { useStore } from "vuex";
  48. import weatherInfo from "@/components/weatherInfo.vue";
  49. import { useRoute, useRouter, onBeforeRouteLeave } from "vue-router";
  50. import ArchivesFarmTimeLine from "@/components/pageComponents/ArchivesFarmTimeLine.vue";
  51. import gardenList from "@/components/gardenList.vue";
  52. const router = useRouter();
  53. const route = useRoute();
  54. const archivesScrollAreaRef = ref(null);
  55. /** 从趋势卡跳转详情时已主动保存滚动;避免 leave/deactivate 阶段读到异常 scrollTop 覆盖 sessionStorage */
  56. const blockArchivesScrollSaveOnce = ref(false);
  57. const getArchivesOuterScrollKey = () =>
  58. `agriRecordArchivesOuterScroll:${gardenId?.value ?? "none"}:${route.path}`;
  59. const saveArchivesOuterScrollTop = () => {
  60. if (!archivesScrollAreaRef.value || activeGardenTab.value !== "current") return;
  61. const scrollTop = archivesScrollAreaRef.value.scrollTop || 0;
  62. sessionStorage.setItem(getArchivesOuterScrollKey(), String(scrollTop));
  63. };
  64. const restoreArchivesOuterScrollTop = () => {
  65. if (!archivesScrollAreaRef.value || activeGardenTab.value !== "current") return false;
  66. const raw = sessionStorage.getItem(getArchivesOuterScrollKey());
  67. if (raw == null) return false;
  68. const scrollTop = Number(raw);
  69. if (Number.isNaN(scrollTop)) return false;
  70. const el = archivesScrollAreaRef.value;
  71. const maxScrollTop = Math.max(0, (el.scrollHeight || 0) - (el.clientHeight || 0));
  72. el.scrollTop = Math.min(scrollTop, maxScrollTop);
  73. return true;
  74. };
  75. const restoreArchivesOuterScrollTopWithRetry = (retryCount = 6) => {
  76. const ok = restoreArchivesOuterScrollTop();
  77. if (ok || retryCount <= 0) return;
  78. setTimeout(() => restoreArchivesOuterScrollTopWithRetry(retryCount - 1), 50);
  79. };
  80. const defaultGardenId = ref(null);
  81. const selectedGardenId = ref(null);
  82. const gardenListRef = ref(null);
  83. const activeGardenTab = ref('current');
  84. const trendMonitorMockList = ref([]);
  85. const changeGardenTab = (tab) => {
  86. activeGardenTab.value = tab;
  87. };
  88. const getFarmRiskAndTracking = async () => {
  89. trendMonitorMockList.value = [];
  90. // const res = await VE_API.monitor.getFarmRiskAndTracking({
  91. // farm_id: gardenId.value,
  92. // crop_type: JSON.parse(localStorage.getItem("selectedFarmData")).farm_variety,
  93. // });
  94. const res = {
  95. "code": 200,
  96. "data": {
  97. "growth_abnormal_tracking": {
  98. "first_work": {
  99. "interaction_issue": "是否有10%的荔枝出现落果裂果?",
  100. "work_name": "长势异常态势跟踪",
  101. "work_reason_short": "缺锌导致生长缓慢"
  102. },
  103. "risk_level": 2,
  104. "work_id": 4393
  105. },
  106. "pest_risk_assessment": {
  107. "first_work": {
  108. "interaction_issue": "是否有10%的荔枝发现蒂蛀虫?",
  109. "work_name": "病虫害态势监控",
  110. "work_reason_short": "病虫害影响作物生长"
  111. },
  112. "risk_level": 2,
  113. "work_id": 4394
  114. },
  115. "phenology_tracking": {
  116. "first_work": {
  117. "interaction_issue": "是否有60%的荔枝进入转色期",
  118. "work_name": "物候跟踪记录",
  119. "work_reason_short": "预计到达病虫防控关键期"
  120. },
  121. "work_id": 4393
  122. }
  123. }
  124. }
  125. if (res.code === 200) {
  126. trendMonitorMockList.value.push(res.data?.growth_abnormal_tracking);
  127. trendMonitorMockList.value.push(res.data?.pest_risk_assessment);
  128. if (res.data?.phenology_tracking) {
  129. trendMonitorMockList.value.push(res.data?.phenology_tracking);
  130. }
  131. }
  132. }
  133. const handleGardenLoaded = ({ hasFarm }) => {
  134. weatherInfoRef.value?.setGardenLoaded?.(hasFarm);
  135. };
  136. const handleGardenSelected = (garden) => {
  137. selectedGardenId.value = garden?.id ?? null;
  138. weatherInfoRef.value?.setSelectedGarden?.(garden);
  139. };
  140. const isHeaderShow = ref(false);
  141. const weatherInfoRef = ref(null);
  142. onActivated(() => {
  143. blockArchivesScrollSaveOnce.value = false;
  144. // 用来接收我的农场跳转过来的农场详情逻辑
  145. if (route.query.isHeaderShow) {
  146. isHeaderShow.value = true;
  147. defaultGardenId.value = route.query.farmId;
  148. }
  149. const savedFarmId = localStorage.getItem("selectedFarmId");
  150. selectedGardenId.value = savedFarmId ? Number(savedFarmId) : null;
  151. gardenListRef.value?.refreshFarmList?.();
  152. nextTick(() => {
  153. requestAnimationFrame(() => {
  154. restoreArchivesOuterScrollTopWithRetry();
  155. });
  156. });
  157. });
  158. onDeactivated(() => {
  159. if (blockArchivesScrollSaveOnce.value) return;
  160. saveArchivesOuterScrollTop();
  161. });
  162. onBeforeRouteLeave(() => {
  163. if (blockArchivesScrollSaveOnce.value) return;
  164. saveArchivesOuterScrollTop();
  165. });
  166. watch(activeGardenTab, (val, oldVal) => {
  167. if (oldVal === "current" && val !== "current") {
  168. saveArchivesOuterScrollTop();
  169. }
  170. if (val === "current") {
  171. nextTick(() => {
  172. requestAnimationFrame(() => {
  173. restoreArchivesOuterScrollTopWithRetry();
  174. });
  175. });
  176. }
  177. });
  178. const store = useStore();
  179. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  180. const isExpanded = ref(false);
  181. const weatherExpanded = (isExpandedValue) => {
  182. isExpanded.value = isExpandedValue;
  183. };
  184. // 点击遮罩时收起天气
  185. const handleMaskClick = () => {
  186. if (weatherInfoRef.value && weatherInfoRef.value.toggleExpand) {
  187. weatherInfoRef.value.toggleExpand();
  188. }
  189. };
  190. const gardenId = ref(store.state.home.gardenId);
  191. // 初始化加载数据
  192. // onMounted(() => {
  193. // if (gardenId.value) {
  194. // getFarmRiskAndTracking();
  195. // }
  196. // });
  197. const changeGarden = ({ id }) => {
  198. gardenId.value = id;
  199. getFarmRiskAndTracking();
  200. // 更新 store 中的状态
  201. store.commit("home/SET_GARDEN_ID", id);
  202. };
  203. const handleTrendMonitorCardClick = (item) => {
  204. saveArchivesOuterScrollTop();
  205. blockArchivesScrollSaveOnce.value = true;
  206. router.push({
  207. path: "/record_details",
  208. query: { workId: item.work_id, type: item.first_work.work_name },
  209. });
  210. };
  211. </script>
  212. <style scoped lang="scss">
  213. .monitor-index {
  214. width: 100%;
  215. height: 100%;
  216. box-sizing: border-box;
  217. background: linear-gradient(180deg, #2199F8 8%, #F5F5F5 19%, #F5F5F5 73%, #F5F5F5 100%);
  218. .weather-mask {
  219. position: fixed;
  220. top: 0;
  221. left: 0;
  222. width: 100%;
  223. height: 100%;
  224. background-color: rgba(0, 0, 0, 0.52);
  225. z-index: 11;
  226. }
  227. .weather-info {
  228. width: calc(100% - 20px);
  229. position: absolute;
  230. top: 10px;
  231. left: 10px;
  232. z-index: 12;
  233. }
  234. .trend-monitor-list {
  235. display: flex;
  236. flex-direction: column;
  237. gap: 12px;
  238. background: #fff;
  239. border: 1px solid #2199F8;
  240. border-radius: 8px;
  241. padding: 10px;
  242. .trend-monitor-card {
  243. border: 0.5px solid rgba(33, 153, 248, 0.5);
  244. border-radius: 4px;
  245. padding: 6px 8px;
  246. position: relative;
  247. .card-header {
  248. display: flex;
  249. align-items: center;
  250. margin-bottom: 6px;
  251. .header-left {
  252. display: flex;
  253. align-items: center;
  254. gap: 5px;
  255. .title {
  256. font-size: 16px;
  257. }
  258. .level-tag {
  259. padding: 1px 8px;
  260. border-radius: 2px;
  261. background: rgba(255, 78, 78, 0.1);
  262. color: #FF4E4E;
  263. font-size: 12px;
  264. }
  265. }
  266. .status-tag {
  267. background: #FF953D;
  268. color: #ffffff;
  269. font-size: 12px;
  270. border-radius: 0 4px 0 4px;
  271. padding: 2px 5px;
  272. position: absolute;
  273. top: 0;
  274. right: 0;
  275. }
  276. }
  277. .card-row {
  278. background: #F7F7F7;
  279. border-radius: 5px;
  280. padding: 5px;
  281. display: flex;
  282. align-items: center;
  283. justify-content: space-between;
  284. margin-bottom: 6px;
  285. font-size: 12px;
  286. color: #626262;
  287. .question-tag {
  288. background: #2b9af6;
  289. color: #ffffff;
  290. border-radius: 2px;
  291. padding: 2px 6px;
  292. }
  293. }
  294. .record-list {
  295. display: flex;
  296. gap: 6px;
  297. .record-item {
  298. border: 1px solid #2199F8;
  299. border-radius: 2px;
  300. color: #2199F8;
  301. text-align: center;
  302. font-size: 12px;
  303. padding: 0 5px;
  304. }
  305. }
  306. }
  307. }
  308. .archives-time-line {
  309. position: relative;
  310. height: calc(100% - 160px);
  311. padding: 12px;
  312. display: flex;
  313. flex-direction: column;
  314. min-height: 0;
  315. padding-top: 150px;
  316. overflow-y: auto;
  317. -webkit-overflow-scrolling: touch;
  318. .archives-time-line-content {
  319. margin-top: 10px;
  320. flex: none;
  321. min-height: 0;
  322. background: #fff;
  323. border-radius: 8px;
  324. padding: 10px;
  325. box-sizing: border-box;
  326. display: flex;
  327. flex-direction: column;
  328. }
  329. }
  330. }
  331. </style>