index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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
  8. ref="weatherInfoRef"
  9. class="weather-info"
  10. @weatherExpanded="weatherExpanded"
  11. @changeGarden="changeGarden"
  12. :isGarden="true"
  13. :gardenId="defaultGardenId"
  14. ></weather-info>
  15. <!-- 作物档案 -->
  16. <div class="archives-time-line">
  17. <div class="archives-time-line-header">
  18. <div class="line-title">作物档案</div>
  19. <el-date-picker style="width: 110px" v-model="date" type="year" placeholder="全部日期" />
  20. </div>
  21. <div class="archives-time-line-content">
  22. <div class="report-box">
  23. <div class="box-content">
  24. <div class="box-title" @click="handleReportClick">
  25. <span>农情互动报告</span>
  26. <el-icon><CaretRight /></el-icon>
  27. </div>
  28. <span class="box-text">当前处于蒂蛀虫高发期,请及时采集</span>
  29. </div>
  30. <img src="@/assets/img/monitor/report-icon.png" alt="" class="report-icon" />
  31. </div>
  32. <div class="time-line">
  33. <archives-farm-time-line :farmId="gardenId"></archives-farm-time-line>
  34. </div>
  35. </div>
  36. </div>
  37. </div>
  38. <tip-popup
  39. v-model:show="showFarmPopup"
  40. type="success"
  41. text="农场领取成功"
  42. :overlay-style="{ 'backdrop-filter': 'blur(4px)' }"
  43. :closeOnClickOverlay="false"
  44. :zIndex="9999"
  45. />
  46. </template>
  47. <script setup>
  48. import customHeader from "@/components/customHeader.vue";
  49. import { ref, computed, onActivated, onDeactivated, onMounted } from "vue";
  50. import { useStore } from "vuex";
  51. import { Badge, List } from "vant";
  52. import weatherInfo from "@/components/weatherInfo.vue";
  53. import { useRouter, useRoute } from "vue-router";
  54. import farmInfoPopup from "../home/components/farmInfoPopup.vue";
  55. import tipPopup from "@/components/popup/tipPopup.vue";
  56. import { ElMessage, ElMessageBox } from "element-plus";
  57. import ArchivesFarmTimeLine from "@/components/pageComponents/ArchivesFarmTimeLine.vue";
  58. const showFarmPopup = ref(false); // 农场领取成功弹窗
  59. const date = ref(new Date());
  60. const defaultGardenId = ref(null);
  61. const isHeaderShow = ref(false);
  62. const isDefaultFarm = ref(false);
  63. const weatherInfoRef = ref(null);
  64. onActivated(() => {
  65. // 用来接收我的农场跳转过来的农场详情逻辑
  66. if (route.query.isHeaderShow) {
  67. isHeaderShow.value = true;
  68. defaultGardenId.value = route.query.farmId;
  69. // 统一转换为布尔值
  70. isDefaultFarm.value = route.query.defaultFarm === "true" || route.query.defaultFarm === true;
  71. }
  72. });
  73. const receiveFarm = (json) => {
  74. VE_API.monitor
  75. .receiveFarm({
  76. agriculturalStoreId: json.agriculturalStoreId,
  77. farmId: json.farmId,
  78. })
  79. .then((res) => {
  80. if (res.code === 0) {
  81. showFarmPopup.value = true;
  82. defaultGardenId.value = json.farmId;
  83. } else {
  84. ElMessage.warning(res.msg);
  85. }
  86. // 清空路由参数
  87. router.replace({ path: route.path });
  88. });
  89. };
  90. const store = useStore();
  91. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  92. const router = useRouter();
  93. const route = useRoute();
  94. const farmInfoRef = ref(null);
  95. function toFarmInfo() {
  96. farmInfoRef.value.handleShow();
  97. }
  98. // 功能卡片数据
  99. const functionCards = ref([
  100. {
  101. title: "农事规划",
  102. route: "/plan",
  103. },
  104. {
  105. title: "农场报告",
  106. status: "最新",
  107. route: "/farm_report",
  108. className: "blue",
  109. },
  110. {
  111. title: "农事方案",
  112. route: "/agricultural_plan",
  113. },
  114. {
  115. title: "复核成效",
  116. status: "最新",
  117. route: "/review-results",
  118. className: "yellow",
  119. },
  120. ]);
  121. const getStayCount = () => {
  122. VE_API.monitor
  123. .getCountByStatusAndFarmId({
  124. farmId: gardenId.value,
  125. startStatus: 1,
  126. endStatus: 3,
  127. })
  128. .then((res) => {
  129. functionCards.value[0].status = null;
  130. if (res.data && res.data != 0) {
  131. functionCards.value[0].status = res.data + " 待完成";
  132. }
  133. });
  134. };
  135. // 实时播报数据
  136. const broadcastList = ref([]);
  137. const loading = ref(false);
  138. const finished = ref(false);
  139. const currentPage = ref(1);
  140. const pageSize = ref(10);
  141. const getBroadcastList = async (page = 1, isLoadMore = false) => {
  142. if (!gardenId.value) {
  143. loading.value = false;
  144. return;
  145. }
  146. // 如果正在加载,直接返回(避免重复请求)
  147. if (loading.value) {
  148. return;
  149. }
  150. loading.value = true;
  151. try {
  152. const res = await VE_API.monitor.broadcastPage({
  153. farmId: gardenId.value,
  154. limit: pageSize.value,
  155. page: page,
  156. });
  157. const newData = res.data || [];
  158. if (isLoadMore) {
  159. broadcastList.value = [...broadcastList.value, ...newData];
  160. } else {
  161. broadcastList.value = newData;
  162. }
  163. // 判断是否还有更多数据
  164. if (newData.length < pageSize.value) {
  165. finished.value = true;
  166. } else {
  167. finished.value = false;
  168. // 如果未完成,页码+1,为下次加载做准备
  169. currentPage.value = page + 1;
  170. }
  171. } catch (error) {
  172. console.error("获取播报列表失败:", error);
  173. finished.value = true;
  174. } finally {
  175. // 确保 loading 状态被正确设置为 false
  176. loading.value = false;
  177. }
  178. };
  179. // 滚动加载更多
  180. const onLoad = async () => {
  181. if (finished.value || loading.value) return;
  182. // 判断是否是首次加载(页码为1)
  183. const isLoadMore = currentPage.value > 1;
  184. const pageToLoad = currentPage.value;
  185. // 加载数据(页码会在 getBroadcastList 成功后自动更新)
  186. await getBroadcastList(pageToLoad, isLoadMore);
  187. };
  188. // 卡片点击事件
  189. const handleCardClick = (card) => {
  190. const params = {
  191. farmId: gardenId.value,
  192. };
  193. router.push({
  194. path: card.route,
  195. query: { ...params, miniJson: JSON.stringify(params) },
  196. });
  197. };
  198. // 播报相关事件
  199. const isSpeaking = ref(false);
  200. const speechSynthesis = window.speechSynthesis;
  201. const handleBroadcast = () => {
  202. if (isSpeaking.value) {
  203. // 如果正在播放,则停止
  204. speechSynthesis.cancel();
  205. isSpeaking.value = false;
  206. return;
  207. }
  208. // 构建播报文本
  209. let broadcastText = "实时播报:";
  210. if (broadcastList.value.length === 0) {
  211. broadcastText += "暂无更多播报";
  212. } else {
  213. broadcastList.value.forEach((item, index) => {
  214. broadcastText += `${index + 1}、${item.title}。${item.content}。`;
  215. });
  216. }
  217. // 创建语音合成对象
  218. const utterance = new SpeechSynthesisUtterance(broadcastText);
  219. // 设置语音参数
  220. utterance.lang = "zh-CN";
  221. utterance.rate = 0.8; // 语速
  222. utterance.pitch = 1; // 音调
  223. utterance.volume = 1; // 音量
  224. // 播放开始事件
  225. utterance.onstart = () => {
  226. isSpeaking.value = true;
  227. };
  228. // 播放结束事件
  229. utterance.onend = () => {
  230. isSpeaking.value = false;
  231. };
  232. // 播放错误事件
  233. utterance.onerror = (event) => {
  234. isSpeaking.value = false;
  235. console.error("播报错误:", event.error);
  236. };
  237. // 开始播报
  238. speechSynthesis.speak(utterance);
  239. };
  240. // 组件卸载时停止语音播放
  241. onDeactivated(() => {
  242. showFarmPopup.value = false;
  243. isDefaultFarm.value = false;
  244. if (isSpeaking.value) {
  245. speechSynthesis.cancel();
  246. isSpeaking.value = false;
  247. }
  248. });
  249. const isExpanded = ref(false);
  250. const weatherExpanded = (isExpandedValue) => {
  251. isExpanded.value = isExpandedValue;
  252. };
  253. // 点击遮罩时收起天气
  254. const handleMaskClick = () => {
  255. if (weatherInfoRef.value && weatherInfoRef.value.toggleExpand) {
  256. weatherInfoRef.value.toggleExpand();
  257. }
  258. };
  259. const gardenId = ref(store.state.home.gardenId);
  260. // 初始化加载数据
  261. onMounted(() => {
  262. if (gardenId.value) {
  263. currentPage.value = 1;
  264. finished.value = false;
  265. broadcastList.value = [];
  266. getStayCount();
  267. // 不在这里手动加载,让 List 组件的 immediate-check 自动触发首次加载
  268. }
  269. });
  270. const changeGarden = ({ id }) => {
  271. gardenId.value = id;
  272. // 更新 store 中的状态
  273. store.commit("home/SET_GARDEN_ID", id);
  274. // 重置分页状态
  275. currentPage.value = 1;
  276. finished.value = false;
  277. broadcastList.value = [];
  278. getStayCount();
  279. getBroadcastList(1, false);
  280. };
  281. function handlePage(url) {
  282. const query = {
  283. farmId: gardenId.value,
  284. };
  285. if (url === "/message_list") {
  286. query.from = "monitor";
  287. }
  288. router.push({
  289. path: url,
  290. query: query,
  291. });
  292. }
  293. function handleReportClick() {
  294. router.push({
  295. path: "/growth_report",
  296. query: { miniJson: JSON.stringify({ id: gardenId.value }) },
  297. });
  298. }
  299. </script>
  300. <style scoped lang="scss">
  301. .monitor-index {
  302. width: 100%;
  303. height: 100%;
  304. padding: 13px 10px;
  305. box-sizing: border-box;
  306. background: linear-gradient(180deg, #f9f9f9 0%, #f0f8ff 31.47%, #f9f9f9 46.81%, #f9f9f9 69.38%, #f9f9f9 100%);
  307. .weather-mask {
  308. position: fixed;
  309. top: 0;
  310. left: 0;
  311. width: 100%;
  312. height: 100%;
  313. background-color: rgba(0, 0, 0, 0.52);
  314. z-index: 2;
  315. }
  316. .weather-info {
  317. width: calc(100% - 20px);
  318. position: absolute;
  319. z-index: 3;
  320. }
  321. .archives-time-line {
  322. position: relative;
  323. margin-top: 96px;
  324. height: calc(100% - 90px);
  325. .archives-time-line-header {
  326. display: flex;
  327. align-items: center;
  328. justify-content: space-between;
  329. .line-title {
  330. position: relative;
  331. padding-left: 14px;
  332. font-size: 16px;
  333. &::before {
  334. content: "";
  335. position: absolute;
  336. left: 5px;
  337. top: 50%;
  338. transform: translateY(-50%);
  339. width: 4px;
  340. height: 15px;
  341. background: #2199f8;
  342. border-radius: 20px;
  343. }
  344. }
  345. }
  346. .archives-time-line-content {
  347. margin-top: 10px;
  348. height: calc(100% - 35px);
  349. background: #fff;
  350. border-radius: 8px;
  351. padding: 10px;
  352. box-sizing: border-box;
  353. .report-box {
  354. background: linear-gradient(120deg, #eef8ff, #bbe3ff);
  355. border-radius: 4px;
  356. padding: 6px 0 0 16px;
  357. display: flex;
  358. align-items: center;
  359. justify-content: space-between;
  360. margin-bottom: 12px;
  361. .box-content {
  362. .box-title {
  363. font-size: 16px;
  364. color: #2199f8;
  365. font-weight: 500;
  366. margin-bottom: 4px;
  367. display: flex;
  368. align-items: center;
  369. }
  370. .box-text {
  371. color: #4e5969;
  372. }
  373. }
  374. .report-icon {
  375. width: 120px;
  376. height: 85px;
  377. }
  378. }
  379. .time-line{
  380. height: calc(100% - 100px);
  381. }
  382. }
  383. }
  384. }
  385. </style>