index.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  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">
  15. <Close />
  16. </el-icon>
  17. </div>
  18. <div class="interact-card__qa">
  19. <div class="interact-card__question">{{ interactQuestionText }}</div>
  20. <div @click="handleGoPatrol" class="interact-card__btn">去巡园</div>
  21. </div>
  22. </div>
  23. </div>
  24. <!-- 左上角:位置切换 -->
  25. <!-- <div v-if="!showInteractCard" class="location-bar">
  26. <el-icon class="location-bar__icon"><LocationFilled /></el-icon>
  27. <span class="location-bar__name van-ellipsis">{{ locationName }}</span>
  28. <span class="location-bar__action" @click="handleSwitchLocation">切换位置</span>
  29. </div> -->
  30. <!-- 左侧:胁迫图层菜单 -->
  31. <div class="side-menu side-menu--left">
  32. <div v-for="item in stressMenuItems" :key="item.key" class="side-menu__item"
  33. :class="{ active: activeMenuKey === item.key }" @click="handleSelectMenu(item.key)">
  34. <img class="side-menu__icon" :src="item.icon" alt="" />
  35. <div class="side-menu__text">
  36. <span>{{ item.line1 }}</span>
  37. <span>{{ item.line2 }}</span>
  38. </div>
  39. </div>
  40. </div>
  41. <!-- <div class="map-legend" :style="{ bottom: `${inviteBottom}px` }">
  42. <div
  43. v-for="item in mapLegendItems"
  44. :key="item.key"
  45. class="map-legend__item"
  46. >
  47. <span class="map-legend__pill" :class="item.pillClass"></span>
  48. <span class="map-legend__text">{{ item.label }}</span>
  49. </div>
  50. </div> -->
  51. <crop-alert-panel :crop-name="currentCropName" :weather-risk="weatherRisk" :weather-stress="weatherStress"
  52. :weather-risk-explain="weatherRiskExplain" :weather-stress-explain="weatherStressExplain"
  53. :report-generating="reportGenerating" @switch-category="handleSwitchCategory"
  54. @view-detail="handleViewDetail" @patrol-tip="handlePatrolTip" @height-change="handlePanelHeightChange" />
  55. </div>
  56. <!-- 诊断报告弹窗 -->
  57. <diagnosis-report-popup ref="diagnosisReportPopupRef" />
  58. <!-- 恢复种植弹窗:确认已到达后触发诊断报告弹窗检查 -->
  59. <restore-planting-popup @phenology-reached="onPhenologyReached" />
  60. <!-- 录入信息邀请弹窗:分享链接带 showInviteEntry 时展示 -->
  61. <invite-entry-popup ref="invitePopupRef" />
  62. </template>
  63. <script setup>
  64. import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref } from "vue";
  65. import { useRoute, useRouter } from "vue-router";
  66. import { useStore } from "vuex";
  67. import { Close, LocationFilled } from "@element-plus/icons-vue";
  68. import { convertPointToArray } from "@/utils/index";
  69. import GrowthReportMap from "./growthReportMap.js";
  70. import RestorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
  71. import CropAlertPanel from "./components/CropAlertPanel.vue";
  72. import inviteEntryPopup from "@/views/old_mini/entry_information/components/inviteEntryPopup.vue";
  73. import DiagnosisReportPopup from "@/components/popup/diagnosisReportPopup.vue";
  74. import wx from "weixin-js-sdk";
  75. import { useI18n } from "@/i18n";
  76. const { t } = useI18n();
  77. const router = useRouter();
  78. const route = useRoute();
  79. const diagnosisReportPopupRef = ref(null);
  80. const menuIcon = require("@/assets/img/common/water.png");
  81. const menuIcon2 = require("@/assets/img/common/rain.png");
  82. const stressMenuItems = [
  83. {
  84. key: "stress",
  85. line1: "暴雨",
  86. line2: "涝渍",
  87. icon: menuIcon,
  88. mapUrl: require("@/assets/img/map/Flood_Waterlogging_Risk.png"),
  89. extent: [115.7752805213905, 23.913245697921756, 116.55501818800624, 24.798086252779484],
  90. },
  91. {
  92. key: "hot-drought-1",
  93. line1: "高温",
  94. line2: "高湿",
  95. icon: menuIcon2,
  96. mapUrl: require("@/assets/img/map/High_Humidity_Heat_Risk.png"),
  97. extent: [115.77569718805717, 23.912588960529323, 116.55453653938879, 24.79742951538705],
  98. },
  99. ];
  100. /** 恢复种植确认「已到达」后,再检查是否弹出诊断报告 */
  101. const onPhenologyReached = () => {
  102. diagnosisReportPopupRef.value?.checkShouldShow?.();
  103. };
  104. const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
  105. const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
  106. const LOCATION_SESSION_KEY = "GROWTH_REPORT_LOCATION";
  107. const MAP_POINT_STORAGE_KEY = "GROWTH_REPORT_MAP_POINT";
  108. const ENTRY_FARM_DATA_KEY = "ENTRY_FARM_DATA";
  109. function readEntryFarmData() {
  110. try {
  111. return JSON.parse(localStorage.getItem(ENTRY_FARM_DATA_KEY) || "null");
  112. } catch {
  113. return null;
  114. }
  115. }
  116. const entryFarmData = readEntryFarmData();
  117. const mapLegendItems = computed(() => [
  118. { key: "zone", label: "胁迫", pillClass: "map-legend__pill--zone" },
  119. { key: "growth", label: "长势", pillClass: "map-legend__pill--growth" },
  120. { key: "pest", label: "病虫害", pillClass: "map-legend__pill--pest" },
  121. ]);
  122. const store = useStore();
  123. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  124. const mapContainer = ref(null);
  125. const growthReportMap = new GrowthReportMap();
  126. const locationName = ref("");
  127. const currentMapPoint = ref(
  128. entryFarmData?.point ||
  129. localStorage.getItem(MAP_POINT_STORAGE_KEY) ||
  130. localStorage.getItem("selectedFarmPoint") ||
  131. store.state.home.miniUserLocationPoint ||
  132. DEFAULT_MAP_POINT
  133. );
  134. const currentCropName = ref(entryFarmData?.crop || "水稻");
  135. const weatherRisk = ref(entryFarmData?.weatherRisk || "具体预警");
  136. const weatherStress = ref(entryFarmData?.weatherStress || "某某胁迫");
  137. /** 未来预警正文:risk_report.explain_simple */
  138. const weatherRiskExplain = ref("");
  139. /** 当下胁迫正文:stress_report.explain_simple */
  140. const weatherStressExplain = ref("");
  141. /** stress_report 为空时,气象报告生成中 */
  142. const reportGenerating = ref(false);
  143. const activeMenuKey = ref("");
  144. const panelHeight = ref(280);
  145. const inviteBottom = computed(() => panelHeight.value);
  146. const invitePopupRef = ref(null);
  147. const showInteractCard = ref(false);
  148. const interactQuestionText = ref("");
  149. const CHECK_REPORT_INTERVAL_MS = 6000;
  150. let checkWeatherReportTimer = null;
  151. function resolveUserTel() {
  152. const entry = readEntryFarmData();
  153. if (entry?.phone) return String(entry.phone).trim();
  154. try {
  155. const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
  156. return String(userInfo.tel || userInfo.phone || userInfo.mobile || "").trim();
  157. } catch {
  158. return "";
  159. }
  160. }
  161. function stopCheckWeatherReport() {
  162. if (checkWeatherReportTimer) {
  163. clearTimeout(checkWeatherReportTimer);
  164. checkWeatherReportTimer = null;
  165. }
  166. }
  167. /** stress 为空时轮询 check_stress_report,status=1 后再拉 risk */
  168. async function pollWeatherReportGenerated() {
  169. const tel = resolveUserTel();
  170. if (!tel) {
  171. reportGenerating.value = false;
  172. stopCheckWeatherReport();
  173. return;
  174. }
  175. try {
  176. const raw = await VE_API.questionnaire.checkWeatherReportGenerated({ tel });
  177. const res = Array.isArray(raw) ? raw[0] : raw;
  178. const status = Number(res?.data?.status);
  179. if (status === 0) {
  180. reportGenerating.value = true;
  181. stopCheckWeatherReport();
  182. checkWeatherReportTimer = setTimeout(pollWeatherReportGenerated, CHECK_REPORT_INTERVAL_MS);
  183. return;
  184. }
  185. if (status === 1) {
  186. stopCheckWeatherReport();
  187. reportGenerating.value = false;
  188. await fetchRiskReport();
  189. // 生成完成后补拉胁迫正文(避免仍显示空)
  190. await fetchStressReportAfterReady(tel);
  191. return;
  192. }
  193. reportGenerating.value = false;
  194. stopCheckWeatherReport();
  195. } catch {
  196. reportGenerating.value = false;
  197. stopCheckWeatherReport();
  198. }
  199. }
  200. function startCheckWeatherReport() {
  201. reportGenerating.value = true;
  202. stopCheckWeatherReport();
  203. pollWeatherReportGenerated();
  204. }
  205. /**
  206. * risk_report / stress_report:
  207. * 新:{ current_time, reports: [...] }
  208. * 旧:data 为报告对象或数组
  209. */
  210. function unwrapRiskStressData(raw) {
  211. const res = Array.isArray(raw) ? raw[0] : raw;
  212. if (Number(res?.code) !== 200 || !res.data) {
  213. return { reports: [], currentTime: "" };
  214. }
  215. const data = res.data;
  216. if (Array.isArray(data)) {
  217. return { reports: data, currentTime: "" };
  218. }
  219. if (Array.isArray(data.reports)) {
  220. return {
  221. reports: data.reports,
  222. currentTime: data.current_time || "",
  223. };
  224. }
  225. return {
  226. reports: typeof data === "object" ? [data] : [],
  227. currentTime: data.current_time || "",
  228. };
  229. }
  230. function pickReportExplain(item) {
  231. return item?.explain_simple || item?.interact_explain || "";
  232. }
  233. /** 未来预警卡片:risk_report → explain_simple */
  234. async function fetchRiskReport() {
  235. const tel = resolveUserTel();
  236. if (!tel) {
  237. weatherRiskExplain.value = "";
  238. return;
  239. }
  240. try {
  241. const { reports } = unwrapRiskStressData(await VE_API.questionnaire.riskReport({ tel }));
  242. const item = reports[0];
  243. weatherRiskExplain.value = pickReportExplain(item);
  244. if (item?.risk_name) weatherRisk.value = item.risk_name;
  245. } catch {
  246. weatherRiskExplain.value = "";
  247. }
  248. }
  249. /** 当下胁迫卡片:stress_report → explain_simple;reports 为空则轮询是否生成中 */
  250. async function fetchStressReport() {
  251. const tel = resolveUserTel();
  252. if (!tel) {
  253. weatherStressExplain.value = "";
  254. reportGenerating.value = false;
  255. stopCheckWeatherReport();
  256. return;
  257. }
  258. try {
  259. const { reports } = unwrapRiskStressData(await VE_API.questionnaire.stressReport({ tel }));
  260. if (!reports.length) {
  261. weatherStressExplain.value = "";
  262. startCheckWeatherReport();
  263. return;
  264. }
  265. stopCheckWeatherReport();
  266. reportGenerating.value = false;
  267. const item = reports[0];
  268. weatherStressExplain.value = pickReportExplain(item);
  269. if (item?.stress_name) weatherStress.value = item.stress_name;
  270. } catch {
  271. weatherStressExplain.value = "";
  272. }
  273. }
  274. /** 生成完成后只取正文,不再触发轮询 */
  275. async function fetchStressReportAfterReady(tel) {
  276. try {
  277. const { reports } = unwrapRiskStressData(await VE_API.questionnaire.stressReport({ tel }));
  278. const item = reports[0];
  279. weatherStressExplain.value = pickReportExplain(item);
  280. if (item?.stress_name) weatherStress.value = item.stress_name;
  281. } catch {
  282. weatherStressExplain.value = "";
  283. }
  284. }
  285. /** 异常互动:parse_anormal */
  286. async function fetchAbnormalContent() {
  287. const entry = readEntryFarmData();
  288. const cropCategoryName = entry?.crop;
  289. const phenophaseCode = entry?.cropCode;
  290. const riskName = entry?.weatherStress;
  291. if (!cropCategoryName || !phenophaseCode || !riskName) {
  292. showInteractCard.value = false;
  293. return;
  294. }
  295. try {
  296. const res = await VE_API.questionnaire.abnormalContent({
  297. crop_category_name: cropCategoryName,
  298. phenophase_code: String(phenophaseCode),
  299. risk_name: riskName,
  300. });
  301. if (res?.code !== 200 || !res.data) {
  302. showInteractCard.value = false;
  303. return;
  304. }
  305. const title = res.data.interaction_title || "";
  306. const theme = res.data.interaction_theme || "";
  307. interactQuestionText.value = [title, theme].filter(Boolean).join(":");
  308. showInteractCard.value = !!interactQuestionText.value;
  309. } catch {
  310. showInteractCard.value = false;
  311. }
  312. }
  313. function handleGoPatrol() {
  314. router.push({
  315. path: "/growth_track",
  316. query: {
  317. type: "abnormal",
  318. pageName: t("agriFile.patrolAbnormalTitle"),
  319. },
  320. });
  321. }
  322. function getLocationName(point = currentMapPoint.value) {
  323. const locationPoint = point || localStorage.getItem("selectedFarmPoint") || store.state.home.miniUserLocationPoint;
  324. if (!locationPoint) return;
  325. const farmLocation = convertPointToArray(locationPoint);
  326. if (!farmLocation?.length) return;
  327. const params = {
  328. key: MAP_KEY,
  329. location: `${farmLocation[1]},${farmLocation[0]}`,
  330. };
  331. VE_API.old_mini_map.location(params).then(({ result }) => {
  332. locationName.value = result?.address_component
  333. ? result.address_component.city + result.address_component.district
  334. : result?.address + "";
  335. });
  336. }
  337. function saveCurrentMapPoint(point) {
  338. if (!point) return;
  339. currentMapPoint.value = point;
  340. localStorage.setItem(MAP_POINT_STORAGE_KEY, point);
  341. }
  342. function syncEntryFarmData() {
  343. const data = readEntryFarmData();
  344. if (!data) return;
  345. if (data.crop) currentCropName.value = data.crop;
  346. if (data.weatherRisk) weatherRisk.value = data.weatherRisk;
  347. if (data.weatherStress) weatherStress.value = data.weatherStress;
  348. if (data.point) saveCurrentMapPoint(data.point);
  349. }
  350. const initMap = async () => {
  351. await nextTick();
  352. if (!mapContainer.value) return;
  353. if (growthReportMap.kmap) {
  354. growthReportMap.kmap.map?.updateSize?.();
  355. const coordinate = convertPointToArray(currentMapPoint.value)?.map(Number);
  356. if (coordinate?.length === 2) {
  357. growthReportMap.setMapPosition(coordinate);
  358. }
  359. applyMenuOverlay(activeMenuKey.value);
  360. return;
  361. }
  362. const location = currentMapPoint.value || store.state.home.miniUserLocationPoint || DEFAULT_MAP_POINT;
  363. currentMapPoint.value = location;
  364. growthReportMap.initMap(location, mapContainer.value);
  365. applyMenuOverlay(activeMenuKey.value);
  366. };
  367. /** 按侧边菜单叠加对应风险底图 */
  368. function applyMenuOverlay(key) {
  369. const item = stressMenuItems.find((m) => m.key === key);
  370. if (!item?.mapUrl || !item.extent || !growthReportMap.kmap) {
  371. growthReportMap.hideRemoteImage();
  372. return;
  373. }
  374. growthReportMap.showRemoteImage(item.mapUrl, item.extent);
  375. }
  376. function applySelectedLocation() {
  377. const raw = sessionStorage.getItem(LOCATION_SESSION_KEY);
  378. if (!raw) return false;
  379. sessionStorage.removeItem(LOCATION_SESSION_KEY);
  380. let saved = null;
  381. try {
  382. saved = JSON.parse(raw);
  383. } catch {
  384. return false;
  385. }
  386. if (!saved?.point && !saved?.coordinate) return false;
  387. const point = saved.point || `POINT(${saved.coordinate[0]} ${saved.coordinate[1]})`;
  388. const coordinate = (saved.coordinate || convertPointToArray(point)).map(Number);
  389. saveCurrentMapPoint(point);
  390. if (growthReportMap.kmap && coordinate?.length === 2) {
  391. growthReportMap.setMapPosition(coordinate);
  392. }
  393. getLocationName(point);
  394. return true;
  395. }
  396. const handleSwitchLocation = () => {
  397. // 以已确认的 currentMapPoint 为准,避免地图临时中心覆盖已选位置
  398. const mapCenter =
  399. currentMapPoint.value ||
  400. store.state.home.miniUserLocationPoint ||
  401. DEFAULT_MAP_POINT;
  402. router.push({
  403. path: "/entry_select_location",
  404. query: {
  405. mapCenter,
  406. iconType: "current",
  407. from: "growth_report",
  408. },
  409. });
  410. };
  411. const handleSelectMenu = (key) => {
  412. if (!key) return;
  413. // 再次点击同一项则关闭叠加
  414. if (activeMenuKey.value === key) {
  415. activeMenuKey.value = "";
  416. growthReportMap.hideRemoteImage();
  417. return;
  418. }
  419. activeMenuKey.value = key;
  420. applyMenuOverlay(key);
  421. };
  422. const handleInvite = (type) => {
  423. const isService = type === "service";
  424. let inviteName = "好友";
  425. try {
  426. const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
  427. inviteName = userInfo.nickName || userInfo.userName || userInfo.name || inviteName;
  428. } catch {
  429. // ignore
  430. }
  431. const inviteType = isService ? "service" : "farmer";
  432. const query = {
  433. askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
  434. shareText: isService ? "邀请您完善农服信息" : "邀请您完善地块种植信息",
  435. targetUrl: `growth_report`,
  436. paramsPage: JSON.stringify({ inviteName, showInviteEntry: 1, inviteType }),
  437. imageUrl: 'https://birdseye-img.sysuimars.com/temp/field.png',
  438. };
  439. wx.miniProgram.navigateTo({
  440. url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
  441. });
  442. }
  443. function isTruthyFlag(value) {
  444. return value === 1 || value === "1" || value === true || value === "true";
  445. }
  446. function parseMaybeJson(value) {
  447. if (!value) return null;
  448. if (typeof value === "object") return value;
  449. try {
  450. return JSON.parse(value);
  451. } catch {
  452. return null;
  453. }
  454. }
  455. function getInviteTypeFromRoute() {
  456. const candidates = [
  457. route.query.inviteType,
  458. parseMaybeJson(route.query.paramsPage)?.inviteType,
  459. parseMaybeJson(route.query.miniJson)?.inviteType,
  460. parseMaybeJson(parseMaybeJson(route.query.miniJson)?.paramsPage)?.inviteType,
  461. ];
  462. return candidates.find((item) => item === "service" || item === "farmer") || "farmer";
  463. }
  464. function shouldShowInviteEntryPopup() {
  465. if (isTruthyFlag(route.query.showInviteEntry)) return true;
  466. const miniJson = parseMaybeJson(route.query.miniJson);
  467. if (miniJson) {
  468. if (isTruthyFlag(miniJson.showInviteEntry)) return true;
  469. const nested = parseMaybeJson(miniJson.paramsPage);
  470. if (isTruthyFlag(nested?.showInviteEntry)) return true;
  471. }
  472. const paramsPage = parseMaybeJson(route.query.paramsPage);
  473. if (isTruthyFlag(paramsPage?.showInviteEntry)) return true;
  474. return false;
  475. }
  476. function clearInviteEntryQuery() {
  477. if (!shouldShowInviteEntryPopup()) return;
  478. const newQuery = { ...(route.query || {}) };
  479. delete newQuery.showInviteEntry;
  480. if (newQuery.paramsPage) {
  481. const paramsPage = parseMaybeJson(newQuery.paramsPage);
  482. if (paramsPage && typeof paramsPage === "object") {
  483. delete paramsPage.showInviteEntry;
  484. newQuery.paramsPage = JSON.stringify(paramsPage);
  485. }
  486. }
  487. if (newQuery.miniJson) {
  488. const miniJson = parseMaybeJson(newQuery.miniJson);
  489. if (miniJson && typeof miniJson === "object") {
  490. delete miniJson.showInviteEntry;
  491. if (miniJson.paramsPage) {
  492. const nested = parseMaybeJson(miniJson.paramsPage);
  493. if (nested && typeof nested === "object") {
  494. delete nested.showInviteEntry;
  495. miniJson.paramsPage = JSON.stringify(nested);
  496. }
  497. }
  498. newQuery.miniJson = JSON.stringify(miniJson);
  499. }
  500. }
  501. router.replace({ path: route.path, query: newQuery });
  502. }
  503. function tryOpenInviteEntryPopup() {
  504. if (!shouldShowInviteEntryPopup()) return;
  505. const inviteType = getInviteTypeFromRoute();
  506. nextTick(() => {
  507. invitePopupRef.value?.open(inviteType);
  508. clearInviteEntryQuery();
  509. });
  510. }
  511. const handleSwitchCategory = (cropName) => {
  512. if (!cropName) return;
  513. currentCropName.value = cropName;
  514. };
  515. const handleViewDetail = (item) => {
  516. router.push({
  517. path: "/alert_detail",
  518. query: {
  519. title: item?.title || "具体预警",
  520. detailType: item?.detailType || ''
  521. },
  522. });
  523. };
  524. const handlePatrolTip = () => {
  525. };
  526. const handlePanelHeightChange = (height) => {
  527. panelHeight.value = Number(height) || 0;
  528. };
  529. onMounted(() => {
  530. syncEntryFarmData();
  531. getLocationName();
  532. initMap();
  533. tryOpenInviteEntryPopup();
  534. fetchAbnormalContent();
  535. });
  536. onActivated(() => {
  537. syncEntryFarmData();
  538. const applied = applySelectedLocation();
  539. if (!applied) getLocationName();
  540. initMap();
  541. tryOpenInviteEntryPopup();
  542. fetchAbnormalContent();
  543. fetchRiskReport();
  544. fetchStressReport();
  545. });
  546. onDeactivated(() => {
  547. stopCheckWeatherReport();
  548. });
  549. onBeforeUnmount(() => {
  550. stopCheckWeatherReport();
  551. });
  552. </script>
  553. <style lang="scss" scoped>
  554. .growth-report-page {
  555. width: 100%;
  556. height: 100%;
  557. background: #f5f7fb;
  558. box-sizing: border-box;
  559. .interact-float {
  560. position: fixed;
  561. top: 10px;
  562. left: 10px;
  563. right: 10px;
  564. z-index: 30;
  565. pointer-events: none;
  566. .interact-card {
  567. pointer-events: auto;
  568. overflow: hidden;
  569. border-radius: 12px;
  570. background: #fff;
  571. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  572. }
  573. .interact-card__head {
  574. position: relative;
  575. background: linear-gradient(180deg, #e5f4ff 0%, #ffffff 72%);
  576. }
  577. .interact-card__tab {
  578. display: inline-flex;
  579. align-items: center;
  580. gap: 5px;
  581. height: 32px;
  582. padding: 0 32px 10px 8px;
  583. color: #fff;
  584. font-size: 16px;
  585. font-family: "PangMenZhengDao";
  586. background: url("@/assets/img/agricultural/title-bg.png") no-repeat center / 100% 100%;
  587. span {
  588. margin-top: -3px;
  589. }
  590. }
  591. .interact-card__star {
  592. width: 16px;
  593. height: 14px;
  594. }
  595. .interact-card__close {
  596. position: absolute;
  597. top: 8px;
  598. right: 10px;
  599. font-size: 16px;
  600. color: #9c9c9c;
  601. cursor: pointer;
  602. }
  603. .interact-card__qa {
  604. margin: 0 10px 10px;
  605. border-radius: 8px;
  606. }
  607. .interact-card__question {
  608. font-size: 16px;
  609. color: #000;
  610. font-weight: 500;
  611. }
  612. .interact-card__btn {
  613. margin: 10px auto 0;
  614. height: 32px;
  615. line-height: 32px;
  616. background: #2199F8;
  617. color: #fff;
  618. width: 100px;
  619. text-align: center;
  620. border-radius: 4px;
  621. font-size: 15px;
  622. font-weight: 500;
  623. cursor: pointer;
  624. }
  625. }
  626. .location-bar {
  627. position: fixed;
  628. top: 12px;
  629. left: 12px;
  630. z-index: 16;
  631. display: inline-flex;
  632. align-items: center;
  633. gap: 4px;
  634. max-width: calc(100% - 24px);
  635. height: 32px;
  636. padding: 0 12px;
  637. border-radius: 16px;
  638. background: rgba(0, 0, 0, 0.4);
  639. backdrop-filter: blur(4px);
  640. color: #fff;
  641. font-size: 14px;
  642. box-sizing: border-box;
  643. &__icon {
  644. font-size: 16px;
  645. flex-shrink: 0;
  646. }
  647. &__name {
  648. max-width: calc(100vw - 80px);
  649. }
  650. &__action {
  651. flex-shrink: 0;
  652. color: #2199f8;
  653. }
  654. }
  655. .side-menu {
  656. position: fixed;
  657. top: 58px;
  658. z-index: 15;
  659. display: flex;
  660. flex-direction: column;
  661. align-items: center;
  662. gap: 12px;
  663. padding: 10px 6px;
  664. border-radius: 6px;
  665. background: #fff;
  666. box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
  667. box-sizing: border-box;
  668. &--left {
  669. left: 10px;
  670. }
  671. &--right {
  672. right: 10px;
  673. }
  674. &__item {
  675. display: flex;
  676. flex-direction: column;
  677. align-items: center;
  678. gap: 4px;
  679. width: 26px;
  680. color: #8a8a8a;
  681. cursor: pointer;
  682. &.active {
  683. color: #2199f8;
  684. .side-menu__icon {
  685. opacity: 1;
  686. filter: brightness(0) saturate(100%) invert(48%) sepia(98%) saturate(1805%) hue-rotate(178deg) brightness(98%) contrast(97%);
  687. }
  688. }
  689. }
  690. &__icon {
  691. width: 22px;
  692. height: 22px;
  693. object-fit: contain;
  694. opacity: 0.55;
  695. filter: grayscale(1);
  696. }
  697. &__text {
  698. display: flex;
  699. flex-direction: column;
  700. align-items: center;
  701. font-size: 12px;
  702. line-height: 14px;
  703. text-align: center;
  704. }
  705. }
  706. .invite-group {
  707. position: fixed;
  708. right: 16px;
  709. z-index: 20;
  710. pointer-events: none;
  711. transition: bottom 0.2s ease;
  712. .invite-btn {
  713. pointer-events: auto;
  714. height: 32px;
  715. display: flex;
  716. align-items: center;
  717. justify-content: center;
  718. border-radius: 5px;
  719. background: #fff;
  720. box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.1);
  721. gap: 4px;
  722. padding: 0 8px;
  723. color: #2199F8;
  724. font-weight: 500;
  725. font-size: 12px;
  726. .invite-icon {
  727. width: 16px;
  728. }
  729. }
  730. .invite-btn+.invite-btn {
  731. margin-top: 10px;
  732. }
  733. }
  734. .map-legend {
  735. position: fixed;
  736. left: 10px;
  737. z-index: 20;
  738. pointer-events: none;
  739. transition: bottom 0.2s ease;
  740. display: flex;
  741. align-items: center;
  742. gap: 10px;
  743. padding: 4px 10px;
  744. background: rgba(0, 0, 0, 0.46);
  745. backdrop-filter: blur(4px);
  746. border-radius: 4px;
  747. box-sizing: border-box;
  748. &--en {
  749. flex-direction: column;
  750. align-items: flex-start;
  751. gap: 6px;
  752. padding: 8px 10px;
  753. background: rgba(0, 0, 0, 0.72);
  754. border-radius: 6px;
  755. .map-legend__item {
  756. gap: 6px;
  757. }
  758. .map-legend__pill {
  759. width: 18px;
  760. height: 4px;
  761. flex-shrink: 0;
  762. }
  763. .map-legend__text {
  764. font-size: 11px;
  765. line-height: 1.2;
  766. white-space: nowrap;
  767. }
  768. }
  769. &__item {
  770. display: flex;
  771. align-items: center;
  772. gap: 5px;
  773. }
  774. &__pill {
  775. width: 16px;
  776. height: 5px;
  777. border-radius: 10px;
  778. &--zone {
  779. background: #13a27f;
  780. }
  781. &--growth {
  782. background: #ff9138;
  783. }
  784. &--pest {
  785. background: #e62e2d;
  786. }
  787. }
  788. &__text {
  789. font-size: 12px;
  790. color: #fff;
  791. }
  792. }
  793. .report-content {
  794. position: relative;
  795. height: 100%;
  796. box-sizing: border-box;
  797. .map-container {
  798. width: 100%;
  799. height: 100%;
  800. }
  801. }
  802. }
  803. </style>