index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. <template>
  2. <div class="growth-report-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
  3. <div class="report-content">
  4. <div class="map-container" ref="mapContainer"></div>
  5. </div>
  6. <!-- 浮窗:长势互动 -->
  7. <div v-if="showInteractCard" class="interact-float">
  8. <div class="interact-card">
  9. <div class="interact-card__head">
  10. <div class="interact-card__tab">
  11. <img class="interact-card__star" src="@/assets/img/agricultural/start.png" alt="" />
  12. <span>长势互动</span>
  13. </div>
  14. <el-icon class="interact-card__close" @click="showInteractCard = false"><Close /></el-icon>
  15. </div>
  16. <div class="interact-card__qa">
  17. <div class="interact-card__question">{{ interactQuestionText }}</div>
  18. <div class="interact-card__options">
  19. <div
  20. v-for="item in interactOptions"
  21. :key="item.id"
  22. class="interact-option"
  23. :class="{ selected: selectedOptionId === item.id }"
  24. @click="selectedOptionId = item.id"
  25. >
  26. <span class="interact-option__text">{{ item.name }}</span>
  27. </div>
  28. </div>
  29. </div>
  30. </div>
  31. </div>
  32. <!-- 左上角:位置切换 -->
  33. <!-- <div v-if="!showInteractCard" class="location-bar">
  34. <el-icon class="location-bar__icon"><LocationFilled /></el-icon>
  35. <span class="location-bar__name van-ellipsis">{{ locationName }}</span>
  36. <span class="location-bar__action" @click="handleSwitchLocation">切换位置</span>
  37. </div> -->
  38. <!-- <div class="map-legend" :style="{ bottom: `${inviteBottom}px` }">
  39. <div
  40. v-for="item in mapLegendItems"
  41. :key="item.key"
  42. class="map-legend__item"
  43. >
  44. <span class="map-legend__pill" :class="item.pillClass"></span>
  45. <span class="map-legend__text">{{ item.label }}</span>
  46. </div>
  47. </div> -->
  48. <crop-alert-panel
  49. :crop-name="currentCropName"
  50. @switch-category="handleSwitchCategory"
  51. @view-detail="handleViewDetail"
  52. @patrol-tip="handlePatrolTip"
  53. @height-change="handlePanelHeightChange"
  54. />
  55. </div>
  56. <!-- 录入信息邀请弹窗:分享链接带 showInviteEntry 时展示 -->
  57. <invite-entry-popup ref="invitePopupRef" />
  58. </template>
  59. <script setup>
  60. import { computed, nextTick, onActivated, onMounted, ref } from "vue";
  61. import { useRoute, useRouter } from "vue-router";
  62. import { useStore } from "vuex";
  63. import { Close, LocationFilled } from "@element-plus/icons-vue";
  64. import { convertPointToArray } from "@/utils/index";
  65. import GrowthReportMap from "./growthReportMap.js";
  66. import CropAlertPanel from "./components/CropAlertPanel.vue";
  67. import inviteEntryPopup from "@/views/old_mini/entry_information/components/inviteEntryPopup.vue";
  68. import wx from "weixin-js-sdk";
  69. const router = useRouter();
  70. const route = useRoute();
  71. const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
  72. const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
  73. const LOCATION_SESSION_KEY = "GROWTH_REPORT_LOCATION";
  74. const MAP_POINT_STORAGE_KEY = "GROWTH_REPORT_MAP_POINT";
  75. const menuIcon = require("@/assets/img/common/farm-active.png");
  76. const mapLegendItems = computed(() => [
  77. { key: "zone", label: "胁迫", pillClass: "map-legend__pill--zone" },
  78. { key: "growth", label: "长势", pillClass: "map-legend__pill--growth" },
  79. { key: "pest", label: "病虫害", pillClass: "map-legend__pill--pest" },
  80. ]);
  81. const store = useStore();
  82. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  83. const mapContainer = ref(null);
  84. const growthReportMap = new GrowthReportMap();
  85. const locationName = ref("");
  86. const currentMapPoint = ref(
  87. localStorage.getItem(MAP_POINT_STORAGE_KEY) ||
  88. localStorage.getItem("selectedFarmPoint") ||
  89. store.state.home.miniUserLocationPoint ||
  90. DEFAULT_MAP_POINT
  91. );
  92. const currentCropName = ref("水稻");
  93. const activeMenuKey = ref("hot-drought-2");
  94. const panelHeight = ref(280);
  95. const inviteBottom = computed(() => panelHeight.value);
  96. const invitePopupRef = ref(null);
  97. const showInteractCard = ref(true);
  98. const interactQuestionText = ref("具体问题具体问题具体问题具体问题具体?");
  99. const interactOptions = ref([
  100. { id: 1, name: "选项一" },
  101. { id: 2, name: "选项二" },
  102. { id: 3, name: "选项三" },
  103. ]);
  104. const selectedOptionId = ref(null);
  105. const stressMenuItems = [
  106. { key: "stress", line1: "胁迫", line2: "胁迫", icon: menuIcon },
  107. { key: "hot-drought-1", line1: "高温", line2: "干旱", icon: menuIcon },
  108. { key: "hot-drought-2", line1: "高温", line2: "干旱", icon: menuIcon },
  109. { key: "hot-drought-3", line1: "高温", line2: "干旱", icon: menuIcon },
  110. ];
  111. const layerMenuItems = [
  112. { key: "growth", line1: "作物", line2: "长势", icon: menuIcon },
  113. { key: "phenology", line1: "物候", line2: "进程", icon: menuIcon },
  114. ];
  115. function getLocationName(point = currentMapPoint.value) {
  116. const locationPoint = point || localStorage.getItem("selectedFarmPoint") || store.state.home.miniUserLocationPoint;
  117. if (!locationPoint) return;
  118. const farmLocation = convertPointToArray(locationPoint);
  119. if (!farmLocation?.length) return;
  120. const params = {
  121. key: MAP_KEY,
  122. location: `${farmLocation[1]},${farmLocation[0]}`,
  123. };
  124. VE_API.old_mini_map.location(params).then(({ result }) => {
  125. locationName.value = result?.address_component
  126. ? result.address_component.city + result.address_component.district
  127. : result?.address + "";
  128. });
  129. }
  130. function saveCurrentMapPoint(point) {
  131. if (!point) return;
  132. currentMapPoint.value = point;
  133. localStorage.setItem(MAP_POINT_STORAGE_KEY, point);
  134. }
  135. const initMap = async () => {
  136. await nextTick();
  137. if (!mapContainer.value) return;
  138. if (growthReportMap.kmap) {
  139. growthReportMap.kmap.map?.updateSize?.();
  140. const coordinate = convertPointToArray(currentMapPoint.value)?.map(Number);
  141. if (coordinate?.length === 2) {
  142. growthReportMap.setMapPosition(coordinate);
  143. }
  144. return;
  145. }
  146. const location = currentMapPoint.value || store.state.home.miniUserLocationPoint || DEFAULT_MAP_POINT;
  147. currentMapPoint.value = location;
  148. growthReportMap.initMap(location, mapContainer.value);
  149. };
  150. function applySelectedLocation() {
  151. const raw = sessionStorage.getItem(LOCATION_SESSION_KEY);
  152. if (!raw) return false;
  153. sessionStorage.removeItem(LOCATION_SESSION_KEY);
  154. let saved = null;
  155. try {
  156. saved = JSON.parse(raw);
  157. } catch {
  158. return false;
  159. }
  160. if (!saved?.point && !saved?.coordinate) return false;
  161. const point = saved.point || `POINT(${saved.coordinate[0]} ${saved.coordinate[1]})`;
  162. const coordinate = (saved.coordinate || convertPointToArray(point)).map(Number);
  163. saveCurrentMapPoint(point);
  164. if (growthReportMap.kmap && coordinate?.length === 2) {
  165. growthReportMap.setMapPosition(coordinate);
  166. }
  167. getLocationName(point);
  168. return true;
  169. }
  170. const handleSwitchLocation = () => {
  171. // 以已确认的 currentMapPoint 为准,避免地图临时中心覆盖已选位置
  172. const mapCenter =
  173. currentMapPoint.value ||
  174. store.state.home.miniUserLocationPoint ||
  175. DEFAULT_MAP_POINT;
  176. router.push({
  177. path: "/entry_select_location",
  178. query: {
  179. mapCenter,
  180. iconType: "current",
  181. from: "growth_report",
  182. },
  183. });
  184. };
  185. const handleSelectMenu = (key) => {
  186. if (!key) return;
  187. activeMenuKey.value = key;
  188. };
  189. const handleInvite = (type) => {
  190. const isService = type === "service";
  191. let inviteName = "好友";
  192. try {
  193. const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
  194. inviteName = userInfo.nickName || userInfo.userName || userInfo.name || inviteName;
  195. } catch {
  196. // ignore
  197. }
  198. const inviteType = isService ? "service" : "farmer";
  199. const query = {
  200. askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
  201. shareText: isService ? "邀请您完善农服信息" : "邀请您完善地块种植信息",
  202. targetUrl: `growth_report`,
  203. paramsPage: JSON.stringify({ inviteName, showInviteEntry: 1, inviteType }),
  204. imageUrl: 'https://birdseye-img.sysuimars.com/temp/field.png',
  205. };
  206. wx.miniProgram.navigateTo({
  207. url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
  208. });
  209. }
  210. function isTruthyFlag(value) {
  211. return value === 1 || value === "1" || value === true || value === "true";
  212. }
  213. function parseMaybeJson(value) {
  214. if (!value) return null;
  215. if (typeof value === "object") return value;
  216. try {
  217. return JSON.parse(value);
  218. } catch {
  219. return null;
  220. }
  221. }
  222. function getInviteTypeFromRoute() {
  223. const candidates = [
  224. route.query.inviteType,
  225. parseMaybeJson(route.query.paramsPage)?.inviteType,
  226. parseMaybeJson(route.query.miniJson)?.inviteType,
  227. parseMaybeJson(parseMaybeJson(route.query.miniJson)?.paramsPage)?.inviteType,
  228. ];
  229. return candidates.find((item) => item === "service" || item === "farmer") || "farmer";
  230. }
  231. function shouldShowInviteEntryPopup() {
  232. if (isTruthyFlag(route.query.showInviteEntry)) return true;
  233. const miniJson = parseMaybeJson(route.query.miniJson);
  234. if (miniJson) {
  235. if (isTruthyFlag(miniJson.showInviteEntry)) return true;
  236. const nested = parseMaybeJson(miniJson.paramsPage);
  237. if (isTruthyFlag(nested?.showInviteEntry)) return true;
  238. }
  239. const paramsPage = parseMaybeJson(route.query.paramsPage);
  240. if (isTruthyFlag(paramsPage?.showInviteEntry)) return true;
  241. return false;
  242. }
  243. function clearInviteEntryQuery() {
  244. if (!shouldShowInviteEntryPopup()) return;
  245. const newQuery = { ...(route.query || {}) };
  246. delete newQuery.showInviteEntry;
  247. if (newQuery.paramsPage) {
  248. const paramsPage = parseMaybeJson(newQuery.paramsPage);
  249. if (paramsPage && typeof paramsPage === "object") {
  250. delete paramsPage.showInviteEntry;
  251. newQuery.paramsPage = JSON.stringify(paramsPage);
  252. }
  253. }
  254. if (newQuery.miniJson) {
  255. const miniJson = parseMaybeJson(newQuery.miniJson);
  256. if (miniJson && typeof miniJson === "object") {
  257. delete miniJson.showInviteEntry;
  258. if (miniJson.paramsPage) {
  259. const nested = parseMaybeJson(miniJson.paramsPage);
  260. if (nested && typeof nested === "object") {
  261. delete nested.showInviteEntry;
  262. miniJson.paramsPage = JSON.stringify(nested);
  263. }
  264. }
  265. newQuery.miniJson = JSON.stringify(miniJson);
  266. }
  267. }
  268. router.replace({ path: route.path, query: newQuery });
  269. }
  270. function tryOpenInviteEntryPopup() {
  271. if (!shouldShowInviteEntryPopup()) return;
  272. const inviteType = getInviteTypeFromRoute();
  273. nextTick(() => {
  274. invitePopupRef.value?.open(inviteType);
  275. clearInviteEntryQuery();
  276. });
  277. }
  278. const handleSwitchCategory = (cropName) => {
  279. if (!cropName) return;
  280. currentCropName.value = cropName;
  281. };
  282. const handleViewDetail = (item) => {
  283. router.push({
  284. path: "/alert_detail",
  285. query: {
  286. title: item?.title || "具体预警",
  287. },
  288. });
  289. };
  290. const handlePatrolTip = () => {
  291. };
  292. const handlePanelHeightChange = (height) => {
  293. panelHeight.value = Number(height) || 0;
  294. };
  295. onMounted(() => {
  296. getLocationName();
  297. initMap();
  298. tryOpenInviteEntryPopup();
  299. });
  300. onActivated(() => {
  301. const applied = applySelectedLocation();
  302. if (!applied) getLocationName();
  303. initMap();
  304. tryOpenInviteEntryPopup();
  305. });
  306. </script>
  307. <style lang="scss" scoped>
  308. .growth-report-page {
  309. width: 100%;
  310. height: 100%;
  311. background: #f5f7fb;
  312. box-sizing: border-box;
  313. .interact-float {
  314. position: fixed;
  315. top: 10px;
  316. left: 10px;
  317. right: 10px;
  318. z-index: 30;
  319. pointer-events: none;
  320. .interact-card {
  321. pointer-events: auto;
  322. overflow: hidden;
  323. border-radius: 12px;
  324. background: #fff;
  325. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  326. }
  327. .interact-card__head {
  328. position: relative;
  329. background: linear-gradient(180deg, #e5f4ff 0%, #ffffff 72%);
  330. }
  331. .interact-card__tab {
  332. display: inline-flex;
  333. align-items: center;
  334. gap: 5px;
  335. height: 32px;
  336. padding: 0 32px 10px 8px;
  337. color: #fff;
  338. font-size: 16px;
  339. font-family: "PangMenZhengDao";
  340. background: url("@/assets/img/agricultural/title-bg.png") no-repeat center / 100% 100%;
  341. span {
  342. margin-top: -3px;
  343. }
  344. }
  345. .interact-card__star {
  346. width: 16px;
  347. height: 14px;
  348. }
  349. .interact-card__close {
  350. position: absolute;
  351. top: 8px;
  352. right: 10px;
  353. font-size: 16px;
  354. color: #9c9c9c;
  355. cursor: pointer;
  356. }
  357. .interact-card__qa {
  358. margin: 0 10px 10px;
  359. padding: 10px;
  360. border-radius: 8px;
  361. background: rgba(33, 153, 248, 0.1);
  362. }
  363. .interact-card__question {
  364. font-size: 14px;
  365. font-weight: 500;
  366. color: #333;
  367. }
  368. .interact-card__options {
  369. display: flex;
  370. gap: 8px;
  371. margin-top: 10px;
  372. }
  373. .interact-option {
  374. position: relative;
  375. flex: 1;
  376. min-width: 0;
  377. height: 36px;
  378. line-height: 36px;
  379. border-radius: 6px;
  380. background: #fff;
  381. border: 1px solid transparent;
  382. text-align: center;
  383. box-sizing: border-box;
  384. cursor: pointer;
  385. &__text {
  386. display: block;
  387. padding: 0 6px;
  388. overflow: hidden;
  389. text-overflow: ellipsis;
  390. white-space: nowrap;
  391. font-size: 14px;
  392. color: #666;
  393. }
  394. &.selected {
  395. border-color: #2199f8;
  396. .interact-option__text {
  397. color: #2199f8;
  398. }
  399. &::after {
  400. content: "";
  401. position: absolute;
  402. top: -1px;
  403. right: -1px;
  404. width: 18px;
  405. height: 14px;
  406. background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
  407. }
  408. }
  409. }
  410. }
  411. .location-bar {
  412. position: fixed;
  413. top: 12px;
  414. left: 12px;
  415. z-index: 16;
  416. display: inline-flex;
  417. align-items: center;
  418. gap: 4px;
  419. max-width: calc(100% - 24px);
  420. height: 32px;
  421. padding: 0 12px;
  422. border-radius: 16px;
  423. background: rgba(0, 0, 0, 0.4);
  424. backdrop-filter: blur(4px);
  425. color: #fff;
  426. font-size: 14px;
  427. box-sizing: border-box;
  428. &__icon {
  429. font-size: 16px;
  430. flex-shrink: 0;
  431. }
  432. &__name {
  433. max-width: calc(100vw - 80px);
  434. }
  435. &__action {
  436. flex-shrink: 0;
  437. color: #2199f8;
  438. }
  439. }
  440. .side-menu {
  441. position: fixed;
  442. top: 58px;
  443. z-index: 15;
  444. display: flex;
  445. flex-direction: column;
  446. align-items: center;
  447. gap: 12px;
  448. padding: 10px 6px;
  449. border-radius: 6px;
  450. background: #fff;
  451. box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
  452. box-sizing: border-box;
  453. &--left {
  454. left: 10px;
  455. }
  456. &--right {
  457. right: 10px;
  458. }
  459. &__item {
  460. display: flex;
  461. flex-direction: column;
  462. align-items: center;
  463. gap: 4px;
  464. width: 26px;
  465. color: #8a8a8a;
  466. cursor: pointer;
  467. &.active {
  468. color: #2199f8;
  469. .side-menu__icon {
  470. opacity: 1;
  471. filter: brightness(0) saturate(100%) invert(48%) sepia(98%) saturate(1805%) hue-rotate(178deg) brightness(98%) contrast(97%);
  472. }
  473. }
  474. }
  475. &__icon {
  476. width: 16px;
  477. height: 16px;
  478. object-fit: contain;
  479. opacity: 0.55;
  480. filter: grayscale(1);
  481. }
  482. &__text {
  483. display: flex;
  484. flex-direction: column;
  485. align-items: center;
  486. font-size: 12px;
  487. line-height: 14px;
  488. text-align: center;
  489. }
  490. }
  491. .invite-group {
  492. position: fixed;
  493. right: 16px;
  494. z-index: 20;
  495. pointer-events: none;
  496. transition: bottom 0.2s ease;
  497. .invite-btn {
  498. pointer-events: auto;
  499. height: 32px;
  500. display: flex;
  501. align-items: center;
  502. justify-content: center;
  503. border-radius: 5px;
  504. background: #fff;
  505. box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.1);
  506. gap: 4px;
  507. padding: 0 8px;
  508. color: #2199F8;
  509. font-weight: 500;
  510. font-size: 12px;
  511. .invite-icon {
  512. width: 16px;
  513. }
  514. }
  515. .invite-btn + .invite-btn {
  516. margin-top: 10px;
  517. }
  518. }
  519. .map-legend {
  520. position: fixed;
  521. left: 10px;
  522. z-index: 20;
  523. pointer-events: none;
  524. transition: bottom 0.2s ease;
  525. display: flex;
  526. align-items: center;
  527. gap: 10px;
  528. padding: 4px 10px;
  529. background: rgba(0, 0, 0, 0.46);
  530. backdrop-filter: blur(4px);
  531. border-radius: 4px;
  532. box-sizing: border-box;
  533. &--en {
  534. flex-direction: column;
  535. align-items: flex-start;
  536. gap: 6px;
  537. padding: 8px 10px;
  538. background: rgba(0, 0, 0, 0.72);
  539. border-radius: 6px;
  540. .map-legend__item {
  541. gap: 6px;
  542. }
  543. .map-legend__pill {
  544. width: 18px;
  545. height: 4px;
  546. flex-shrink: 0;
  547. }
  548. .map-legend__text {
  549. font-size: 11px;
  550. line-height: 1.2;
  551. white-space: nowrap;
  552. }
  553. }
  554. &__item {
  555. display: flex;
  556. align-items: center;
  557. gap: 5px;
  558. }
  559. &__pill {
  560. width: 16px;
  561. height: 5px;
  562. border-radius: 10px;
  563. &--zone {
  564. background: #13a27f;
  565. }
  566. &--growth {
  567. background: #ff9138;
  568. }
  569. &--pest {
  570. background: #e62e2d;
  571. }
  572. }
  573. &__text {
  574. font-size: 12px;
  575. color: #fff;
  576. }
  577. }
  578. .report-content {
  579. position: relative;
  580. height: 100%;
  581. box-sizing: border-box;
  582. .map-container {
  583. width: 100%;
  584. height: 100%;
  585. }
  586. }
  587. }
  588. </style>