index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  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"></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="operation-button" :style="{ top: isHeaderShow ? '157px' : '117px' }">
  17. <div class="button-group">
  18. <div class="button-item" @click="toFarmInfo">
  19. <img class="button-icon" src="@/assets/img/tab_bar/home-active.png" alt="" />
  20. <span>基本信息</span>
  21. </div>
  22. <div class="button-item" @click="handlePage('/farm_photo')">
  23. <img class="button-icon" src="@/assets/img/home/photo-icon.png" alt="" />
  24. <span>农场相册</span>
  25. </div>
  26. </div>
  27. <badge dot :offset="[-4, 5]" @click="handlePage('/message_list')">
  28. <div class="add-farm-button">
  29. <img class="icon" src="@/assets/img/monitor/notice.png" alt="" />
  30. <span>农场消息</span>
  31. </div>
  32. </badge>
  33. </div>
  34. <!-- 功能卡片网格 -->
  35. <div class="function-cards">
  36. <div
  37. v-for="(card, index) in functionCards"
  38. :key="index"
  39. class="function-card"
  40. @click="handleCardClick(card)"
  41. >
  42. <div class="card-title">{{ card.title }}</div>
  43. <img :src="require(`@/assets/img/monitor/grid-${index + 1}.png`)" :alt="card.title" />
  44. <div class="card-status" :class="card.className" v-if="card.status">
  45. {{ card.status }}
  46. </div>
  47. </div>
  48. </div>
  49. <!-- 实时播报 -->
  50. <div class="realtime-broadcast">
  51. <div class="broadcast-header">
  52. <div class="header-left">
  53. <span class="broadcast-title">实时播报</span>
  54. <!-- <div class="broadcast-action" :class="{ speaking: isSpeaking }" @click="handleBroadcast">
  55. <img class="speaker-icon" src="@/assets/img/monitor/speaker.png" alt="播报" />
  56. <span class="broadcast-text">{{ isSpeaking ? '停止播报' : '点击播报' }}</span>
  57. </div> -->
  58. </div>
  59. </div>
  60. <list
  61. v-model:loading="loading"
  62. :finished="finished"
  63. finished-text="暂无更多播报"
  64. @load="onLoad"
  65. class="broadcast-list"
  66. :style="{ height: !isHeaderShow ? 'calc(100vh - 460px)' : 'calc(100vh - 510px)' }"
  67. >
  68. <div v-for="(item, index) in broadcastList" :key="index" class="broadcast-item">
  69. <div class="item-content">
  70. <div class="content-top">
  71. <div class="item-icon">
  72. <img src="@/assets/img/monitor/bell.png" alt="通知" />
  73. </div>
  74. <div class="item-title">{{ item.title }}</div>
  75. </div>
  76. <div class="item-status van-multi-ellipsis--l2">{{ item.content }}</div>
  77. </div>
  78. <div class="item-zone" v-if="item.regionId">
  79. <div class="point"></div>
  80. <span>{{ item.regionId }}</span>
  81. </div>
  82. </div>
  83. </list>
  84. </div>
  85. </div>
  86. <!-- 农场信息 -->
  87. <farm-info-popup ref="farmInfoRef" :farmId="gardenId"></farm-info-popup>
  88. <tip-popup
  89. v-model:show="showFarmPopup"
  90. :type="showFarmPopupType"
  91. :text="textPopup"
  92. :overlay-style="{ 'backdrop-filter': 'blur(4px)' }"
  93. :close-on-click-overlay="false"
  94. @confirm="toCreateFarm"
  95. />
  96. <!-- 农场详情操作按钮 -->
  97. <div class="custom-bottom-fixed-btns" v-if="isHeaderShow">
  98. <div class="bottom-btn secondary-btn" @click="handleFarm('delete')">删除农场</div>
  99. <div v-if="!isDefaultFarm" class="bottom-btn primary-btn" @click="handleFarm('setDefault')">
  100. 设为默认农场
  101. </div>
  102. <div v-else class="bottom-btn btn-text">当前已是 默认农场</div>
  103. </div>
  104. </template>
  105. <script setup>
  106. import customHeader from "@/components/customHeader.vue";
  107. import { ref, computed, onActivated, onDeactivated } from "vue";
  108. import { useStore } from "vuex";
  109. import { Badge, List } from "vant";
  110. import weatherInfo from "@/components/weatherInfo.vue";
  111. import { useRouter, useRoute } from "vue-router";
  112. import farmInfoPopup from "../home/components/farmInfoPopup.vue";
  113. import tipPopup from "@/components/popup/tipPopup.vue";
  114. import { ElMessage, ElMessageBox } from "element-plus";
  115. const showFarmPopup = ref(false);
  116. const showFarmPopupType = ref("create");
  117. const textPopup = ref(["您当前还没有农场", "请先创建农场"]);
  118. const toCreateFarm = () => {
  119. if (showFarmPopupType.value == "create") {
  120. router.push("/create_farm?isReload=true&from=monitor");
  121. }
  122. };
  123. const defaultGardenId = ref(null);
  124. const isHeaderShow = ref(false);
  125. const isDefaultFarm = ref(false);
  126. const weatherInfoRef = ref(null);
  127. onActivated(() => {
  128. // 用来接收小程序页面跳转的内容和逻辑
  129. if (route.query.miniJson) {
  130. const json = JSON.parse(route.query.miniJson);
  131. if (json.showSuccess) {
  132. receiveFarm(json);
  133. }
  134. }
  135. if (localStorage.getItem("isGarden") != "true") {
  136. showFarmPopup.value = true;
  137. }
  138. // 用来接收我的农场跳转过来的农场详情逻辑
  139. if (route.query.isHeaderShow) {
  140. isHeaderShow.value = true;
  141. defaultGardenId.value = route.query.farmId;
  142. // 统一转换为布尔值
  143. isDefaultFarm.value = route.query.defaultFarm === 'true' || route.query.defaultFarm === true;
  144. }
  145. });
  146. const receiveFarm = (json) => {
  147. VE_API.monitor
  148. .receiveFarm({
  149. agriculturalStoreId: json.agriculturalStoreId,
  150. farmId: json.farmId,
  151. })
  152. .then((res) => {
  153. if (res.code === 0) {
  154. showFarmPopupType.value = "success";
  155. showFarmPopup.value = true;
  156. textPopup.value = "农场领取成功";
  157. defaultGardenId.value = json.farmId;
  158. } else {
  159. ElMessage.warning(res.msg);
  160. }
  161. // 清空路由参数
  162. router.replace({ path: route.path });
  163. });
  164. };
  165. const handleFarm = (optionType) => {
  166. ElMessageBox.confirm(optionType === "delete" ? "确定删除该农场吗?" : "确定将该农场设为默认农场吗?", "提示", {
  167. confirmButtonText: "确定",
  168. cancelButtonText: "取消",
  169. type: "warning",
  170. })
  171. .then(() => {
  172. const apiCall =
  173. optionType === "delete"
  174. ? VE_API.farm.deleteFarm({ farmId: defaultGardenId.value })
  175. : VE_API.farm.updateFarm({ farmId: defaultGardenId.value, defaultFarm: 1 });
  176. apiCall.then((res) => {
  177. if (res.code === 0) {
  178. ElMessage.success(optionType === "delete" ? "删除成功" : "设为默认农场成功");
  179. if (optionType === "delete") {
  180. router.back();
  181. } else {
  182. isDefaultFarm.value = true;
  183. // 刷新 weatherInfo 组件的农场列表,确保显示最新的默认农场状态
  184. if (weatherInfoRef.value && weatherInfoRef.value.refreshFarmList) {
  185. weatherInfoRef.value.refreshFarmList();
  186. }
  187. }
  188. } else {
  189. ElMessage.error(res.msg);
  190. }
  191. });
  192. })
  193. .catch(() => {});
  194. };
  195. const store = useStore();
  196. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  197. const router = useRouter();
  198. const route = useRoute();
  199. const farmInfoRef = ref(null);
  200. function toFarmInfo() {
  201. farmInfoRef.value.handleShow();
  202. }
  203. // 功能卡片数据
  204. const functionCards = ref([
  205. {
  206. title: "农事规划",
  207. route: "/plan",
  208. },
  209. {
  210. title: "农场报告",
  211. status: "最新",
  212. route: "/farm_report",
  213. className: "blue",
  214. },
  215. {
  216. title: "农事方案",
  217. route: "/agricultural_plan",
  218. },
  219. {
  220. title: "复核成效",
  221. status: "最新",
  222. route: "/review-results",
  223. className: "yellow",
  224. },
  225. ]);
  226. const getStayCount = () => {
  227. VE_API.monitor
  228. .getCountByStatusAndFarmId({
  229. farmId: gardenId.value,
  230. startStatus: 1,
  231. endStatus: 3,
  232. })
  233. .then((res) => {
  234. functionCards.value[0].status = null;
  235. if (res.data && res.data != 0) {
  236. functionCards.value[0].status = res.data + " 待完成";
  237. }
  238. });
  239. };
  240. // 实时播报数据
  241. const broadcastList = ref([]);
  242. const loading = ref(false);
  243. const finished = ref(false);
  244. const currentPage = ref(1);
  245. const pageSize = ref(10);
  246. const getBroadcastList = (page = 1, isLoadMore = false) => {
  247. loading.value = true;
  248. VE_API.monitor
  249. .broadcastPage({
  250. farmId: gardenId.value,
  251. limit: pageSize.value,
  252. page: page,
  253. })
  254. .then((res) => {
  255. const newData = res.data || [];
  256. if (isLoadMore) {
  257. broadcastList.value = [...broadcastList.value, ...newData];
  258. } else {
  259. broadcastList.value = newData;
  260. }
  261. // 判断是否还有更多数据
  262. if (newData.length < pageSize.value) {
  263. finished.value = true;
  264. }
  265. loading.value = false;
  266. })
  267. .finally(() => {
  268. loading.value = false;
  269. });
  270. };
  271. // 滚动加载更多
  272. const onLoad = () => {
  273. if (finished.value) return;
  274. currentPage.value += 1;
  275. getBroadcastList(currentPage.value, true);
  276. };
  277. // 卡片点击事件
  278. const handleCardClick = (card) => {
  279. const params = {
  280. farmId: gardenId.value,
  281. };
  282. router.push({
  283. path: card.route,
  284. query: params,
  285. });
  286. };
  287. // 播报相关事件
  288. const isSpeaking = ref(false);
  289. const speechSynthesis = window.speechSynthesis;
  290. const handleBroadcast = () => {
  291. if (isSpeaking.value) {
  292. // 如果正在播放,则停止
  293. speechSynthesis.cancel();
  294. isSpeaking.value = false;
  295. return;
  296. }
  297. // 构建播报文本
  298. let broadcastText = "实时播报:";
  299. if (broadcastList.value.length === 0) {
  300. broadcastText += "暂无更多播报";
  301. } else {
  302. broadcastList.value.forEach((item, index) => {
  303. broadcastText += `${index + 1}、${item.title}。${item.content}。`;
  304. });
  305. }
  306. // 创建语音合成对象
  307. const utterance = new SpeechSynthesisUtterance(broadcastText);
  308. // 设置语音参数
  309. utterance.lang = "zh-CN";
  310. utterance.rate = 0.8; // 语速
  311. utterance.pitch = 1; // 音调
  312. utterance.volume = 1; // 音量
  313. // 播放开始事件
  314. utterance.onstart = () => {
  315. isSpeaking.value = true;
  316. };
  317. // 播放结束事件
  318. utterance.onend = () => {
  319. isSpeaking.value = false;
  320. };
  321. // 播放错误事件
  322. utterance.onerror = (event) => {
  323. isSpeaking.value = false;
  324. console.error("播报错误:", event.error);
  325. };
  326. // 开始播报
  327. speechSynthesis.speak(utterance);
  328. };
  329. // 组件卸载时停止语音播放
  330. onDeactivated(() => {
  331. showFarmPopup.value = false;
  332. isDefaultFarm.value = false;
  333. if (isSpeaking.value) {
  334. speechSynthesis.cancel();
  335. isSpeaking.value = false;
  336. }
  337. });
  338. const isExpanded = ref(false);
  339. const weatherExpanded = (isExpandedValue) => {
  340. isExpanded.value = isExpandedValue;
  341. };
  342. const gardenId = ref(store.state.home.gardenId);
  343. const changeGarden = ({ id }) => {
  344. localStorage.setItem('isGarden', true);
  345. gardenId.value = id;
  346. // 更新 store 中的状态
  347. store.commit("home/SET_GARDEN_ID", id);
  348. // 重置分页状态
  349. currentPage.value = 1;
  350. finished.value = false;
  351. broadcastList.value = [];
  352. getStayCount();
  353. getBroadcastList();
  354. };
  355. function handlePage(url) {
  356. router.push({
  357. path: url,
  358. query: {
  359. farmId: gardenId.value,
  360. },
  361. });
  362. }
  363. </script>
  364. <style scoped lang="scss">
  365. .monitor-index {
  366. width: 100%;
  367. height: 100%;
  368. padding: 13px 10px;
  369. box-sizing: border-box;
  370. background-image: linear-gradient(250deg, #cbebff 0%, #dceffd 50%, #e7f3fd 100%);
  371. .weather-mask {
  372. position: fixed;
  373. top: 0;
  374. left: 0;
  375. width: 100%;
  376. height: 100%;
  377. background-color: rgba(0, 0, 0, 0.52);
  378. z-index: 2;
  379. }
  380. .weather-info {
  381. width: calc(100% - 20px);
  382. position: absolute;
  383. z-index: 3;
  384. }
  385. .operation-button {
  386. position: absolute;
  387. top: 117px;
  388. left: 12px;
  389. width: calc(100% - 24px);
  390. z-index: 1;
  391. display: flex;
  392. align-items: center;
  393. justify-content: space-between;
  394. font-size: 12px;
  395. font-weight: 500;
  396. .button-group {
  397. display: flex;
  398. align-items: center;
  399. justify-content: space-between;
  400. .button-item {
  401. display: flex;
  402. align-items: center;
  403. justify-content: center;
  404. gap: 4px;
  405. color: rgba(0, 0, 0, 0.8);
  406. background-color: #fff;
  407. border: 1px solid #f2f2f2;
  408. .button-icon {
  409. width: 13px;
  410. height: 13px;
  411. }
  412. }
  413. .button-item:first-child {
  414. margin-right: 10px;
  415. }
  416. }
  417. .add-farm-button {
  418. display: flex;
  419. align-items: center;
  420. justify-content: center;
  421. gap: 4px;
  422. color: rgba(0, 0, 0, 0.8);
  423. background: rgba(33, 153, 248, 0.1);
  424. border: 1px solid #fff;
  425. .icon {
  426. width: 14px;
  427. height: 14px;
  428. }
  429. }
  430. .button-item,
  431. .add-farm-button {
  432. border-radius: 25px;
  433. padding: 8px 12px;
  434. }
  435. }
  436. .function-cards {
  437. display: grid;
  438. grid-template-columns: 1fr 1fr;
  439. gap: 10px;
  440. margin-top: 155px;
  441. .function-card {
  442. background: #fff;
  443. border-radius: 12px;
  444. padding: 20px 0;
  445. box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
  446. position: relative;
  447. display: flex;
  448. align-items: center;
  449. justify-content: center;
  450. gap: 10px;
  451. img {
  452. width: 40px;
  453. height: 40px;
  454. }
  455. .card-title {
  456. font-size: 16px;
  457. font-weight: 500;
  458. color: #1d2129;
  459. }
  460. .card-status {
  461. position: absolute;
  462. top: -5px;
  463. right: 0;
  464. padding: 1px 4px;
  465. border-radius: 6px 8px 8px 2px;
  466. font-size: 11px;
  467. background: #2199f8;
  468. color: #fff;
  469. &.yellow {
  470. background: #fdcf4c;
  471. }
  472. &.blue {
  473. background: #8a87ff;
  474. }
  475. }
  476. }
  477. }
  478. .realtime-broadcast {
  479. margin-top: 10px;
  480. background: #fff;
  481. border-radius: 8px;
  482. box-sizing: border-box;
  483. padding: 12px;
  484. .broadcast-header {
  485. display: flex;
  486. align-items: center;
  487. margin-bottom: 4px;
  488. .header-left {
  489. display: flex;
  490. align-items: center;
  491. gap: 12px;
  492. .broadcast-title {
  493. font-size: 16px;
  494. font-weight: 600;
  495. color: #1d2129;
  496. }
  497. .broadcast-action {
  498. display: flex;
  499. align-items: center;
  500. gap: 4px;
  501. cursor: pointer;
  502. transition: all 0.3s ease;
  503. &:hover {
  504. opacity: 0.8;
  505. }
  506. .speaker-icon {
  507. width: 16px;
  508. height: 14px;
  509. transition: transform 0.3s ease;
  510. }
  511. .broadcast-text {
  512. color: #2199f8;
  513. font-size: 14px;
  514. }
  515. // 播放状态下的样式
  516. &.speaking {
  517. .speaker-icon {
  518. animation: pulse 1s infinite;
  519. }
  520. .broadcast-text {
  521. color: #ff4757;
  522. }
  523. }
  524. }
  525. @keyframes pulse {
  526. 0% {
  527. transform: scale(1);
  528. }
  529. 50% {
  530. transform: scale(1.1);
  531. }
  532. 100% {
  533. transform: scale(1);
  534. }
  535. }
  536. }
  537. }
  538. .broadcast-list {
  539. height: calc(100vh - 460px);
  540. overflow: auto;
  541. .broadcast-item {
  542. display: flex;
  543. align-items: center;
  544. justify-content: space-between;
  545. padding: 12px 0;
  546. border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  547. &:last-child {
  548. border-bottom: none;
  549. }
  550. .item-content {
  551. width: calc(100% - 50px);
  552. .content-top {
  553. display: flex;
  554. align-items: center;
  555. }
  556. .item-icon {
  557. margin-right: 8px;
  558. background: rgba(255, 0, 0, 0.05);
  559. width: 26px;
  560. height: 26px;
  561. border-radius: 50%;
  562. display: flex;
  563. align-items: center;
  564. justify-content: center;
  565. img {
  566. width: 18px;
  567. height: 12px;
  568. }
  569. }
  570. .item-title {
  571. color: #1d2129;
  572. }
  573. .item-status {
  574. margin-top: 3px;
  575. font-size: 13px;
  576. color: rgba(29, 33, 41, 0.5);
  577. .countdown {
  578. color: #ff7254;
  579. }
  580. }
  581. }
  582. .item-zone {
  583. background: rgba(241, 243, 246, 0.5);
  584. color: #7c7e81;
  585. font-size: 12px;
  586. padding: 3px 11px;
  587. border-radius: 25px;
  588. display: flex;
  589. align-items: center;
  590. gap: 8px;
  591. .point {
  592. width: 6px;
  593. height: 6px;
  594. border-radius: 50%;
  595. background: rgba(124, 126, 129, 0.5);
  596. }
  597. }
  598. }
  599. }
  600. }
  601. }
  602. .custom-bottom-fixed-btns {
  603. z-index: 99999;
  604. .primary-btn {
  605. background: rgba(33, 153, 248, 0.1);
  606. color: #2199f8;
  607. border: none;
  608. }
  609. .btn-text {
  610. color: #666666;
  611. }
  612. }
  613. </style>