index.vue 25 KB

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