index.vue 26 KB

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