index.vue 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161
  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. <!-- Overlay 宿主:无范围时挂到地图方框下方 -->
  6. <div ref="noRangeTipRef" class="no-range-tip" :style="{ display: noFarmRange ? 'inline-flex' : 'none' }"
  7. @click.stop="goAddZone('farm')">
  8. <span>勾画区域,获地块级长势报告</span>
  9. <el-icon class="no-range-tip__arrow">
  10. <ArrowRight />
  11. </el-icon>
  12. </div>
  13. </div>
  14. <!-- 左上角:位置切换 -->
  15. <div class="location-bar">
  16. <el-icon class="location-bar__icon">
  17. <LocationFilled />
  18. </el-icon>
  19. <span class="location-bar__name van-ellipsis">{{ locationName }}</span>
  20. <span v-if="hasSelectedFarm()" class="location-bar__action" @click="handleSwitchFarm">切换农场</span>
  21. </div>
  22. <div class="growth-top">
  23. <!-- 浮窗:长势互动 -->
  24. <div v-if="!showInteractCard" class="interact-float">
  25. <div class="interact-card">
  26. <el-icon class="interact-card__close" @click="showInteractCard = true">
  27. <Close />
  28. </el-icon>
  29. <div class="interact-card__qa">
  30. <div class="interact-card__question">
  31. <div class="interact-card__question-icon">
  32. <img class="interact-card__question-icon-img" :src="periodIcon" alt="">
  33. </div>
  34. <span>{{ interactQuestionText }}</span>
  35. </div>
  36. <div class="interact-card__options">
  37. <div
  38. v-for="item in interactOptions"
  39. :key="item.id"
  40. class="interact-option"
  41. :class="{
  42. selected: selectedOptionId === item.id,
  43. disabled: interactSubmitting,
  44. }"
  45. @click="handleInteractOptionClick(item)"
  46. >
  47. <span class="interact-option__text">{{ item.name }}</span>
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="growth-top__content">
  54. <!-- 左侧:气象底图图层菜单(有农场 getHomeMapCalc / 无农场 getHomeMapCalcByCounty) -->
  55. <div v-if="stressMenuItems.length" class="side-menu side-menu--left">
  56. <div v-for="item in stressMenuItems" :key="item.key" class="side-menu__item"
  57. :class="{ active: activeMenuKey === item.key }" @click="handleSelectMenu(item.key)">
  58. <img class="side-menu__icon" :src="base_img_url3 + item.icon_path" alt="" />
  59. <div class="side-menu__text">
  60. <span>{{ item.line1 }}</span>
  61. <span>{{ item.line2 }}</span>
  62. </div>
  63. </div>
  64. </div>
  65. <div class="invite-group">
  66. <div class="invite-btn" @click="handleLocateToCenter">
  67. <img class="current-icon" src="@/assets/img/home/current.png" alt="">
  68. </div>
  69. <div class="invite-btn" @click="handleInvite('service')">
  70. <img class="invite-icon" src="@/assets/img/home/user-icon.png" alt="">
  71. 邀请农服
  72. </div>
  73. <div class="invite-btn" @click="handleInvite('farmer')">
  74. <img class="invite-icon" src="@/assets/img/home/user-icon.png" alt="">
  75. 邀请农户
  76. </div>
  77. </div>
  78. </div>
  79. </div>
  80. <div v-if="activeLegendUrl" class="map-legend map-legend--image" :style="{ bottom: `${inviteBottom + 10}px` }">
  81. <img class="map-legend__img" :src="activeLegendUrl" alt="" />
  82. </div>
  83. <crop-alert-panel :crop-name="currentCropName" :farm-id="selectedFarmId" :county-code="countyCode"
  84. @zone-change="handleZoneChange" @height-change="handlePanelHeightChange" />
  85. </div>
  86. <!-- 恢复种植弹窗 -->
  87. <restore-planting-popup />
  88. </template>
  89. <script setup>
  90. import { computed, nextTick, onActivated, onMounted, ref } from "vue";
  91. import { useRouter } from "vue-router";
  92. import { useStore } from "vuex";
  93. import { ElMessage } from "element-plus";
  94. import { ArrowRight, Close, LocationFilled } from "@element-plus/icons-vue";
  95. import { convertPointToArray } from "@/utils/index";
  96. import { fetchRegeo, toLocationParam } from "@/utils/tiandituLocation";
  97. import GrowthReportMap from "./growthReportMap.js";
  98. import CropAlertPanel from "./components/CropAlertPanel.vue";
  99. import wx from "weixin-js-sdk";
  100. import { base_img_url3 } from "@/api/config";
  101. import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
  102. const router = useRouter();
  103. const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
  104. const LOCATION_SESSION_KEY = "GROWTH_REPORT_LOCATION";
  105. const MAP_POINT_STORAGE_KEY = "GROWTH_REPORT_MAP_POINT";
  106. /** 无农场时逆地理得到的区县码,供底图 / 点位预警共用 */
  107. const COUNTY_CODE_STORAGE_KEY = "GROWTH_REPORT_COUNTY_CODE";
  108. const menuIcon = require("@/assets/img/common/farm-active.png");
  109. const periodIcon = require("@/assets/img/agricultural/period.png");
  110. const lightningIcon = require("@/assets/img/agricultural/lightning.png");
  111. const soilIcon = require("@/assets/img/agricultural/soil.png");
  112. const store = useStore();
  113. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  114. const mapContainer = ref(null);
  115. const noRangeTipRef = ref(null);
  116. const growthReportMap = new GrowthReportMap();
  117. const locationName = ref("");
  118. /** status≠1 时展示中心勾画引导 */
  119. const noFarmRange = ref(false);
  120. const selectedFarmId = ref(localStorage.getItem("selectedFarmId") || "");
  121. /** 无农场时区县码(逆地理 adcode) */
  122. const countyCode = ref(localStorage.getItem(COUNTY_CODE_STORAGE_KEY) || "");
  123. /** CropAlertPanel 当前选中分区 */
  124. const panelZoneId = ref(null);
  125. const currentCropName = computed(() => {
  126. // 依赖 selectedFarmId,切换农场后重新读缓存
  127. void selectedFarmId.value;
  128. const farm = getSelectedFarm();
  129. return (
  130. farm.variety_name ||
  131. farm.crop_name ||
  132. farm.farm_variety ||
  133. ""
  134. );
  135. });
  136. /** 侧边气象图层菜单(有农场 getHomeMapCalc / 无农场 getHomeMapCalcByCounty) */
  137. const stressMenuItems = ref([]);
  138. const activeMenuKey = ref("");
  139. /** 当前选中图层图例图 */
  140. const activeLegendUrl = ref("");
  141. const panelHeight = ref(280);
  142. const inviteBottom = computed(() => panelHeight.value);
  143. /** 防止切换农场/分区时旧请求回写 */
  144. let mapCalcToken = 0;
  145. const getSelectedFarm = () => {
  146. try {
  147. return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
  148. } catch {
  149. return {};
  150. }
  151. };
  152. /** 登录写入的用户定位:lng,lat */
  153. const getMiniUserLocationText = () => localStorage.getItem("MINI_USER_LOCATION") || "";
  154. /** 登录写入的用户定位 POINT;无 POINT 时用 lng,lat 拼 */
  155. const getMiniUserLocationPoint = () => {
  156. const point =
  157. localStorage.getItem("MINI_USER_LOCATION_POINT") ||
  158. store.state.home.miniUserLocationPoint ||
  159. "";
  160. if (typeof point === "string" && /^POINT\s*\(/i.test(point.trim())) {
  161. return point.trim();
  162. }
  163. const text = getMiniUserLocationText();
  164. if (text.includes(",")) {
  165. const [lng, lat] = text.split(",").map((s) => s.trim());
  166. if (lng && lat) return `POINT(${lng} ${lat})`;
  167. }
  168. return DEFAULT_MAP_POINT;
  169. };
  170. const hasSelectedFarm = () => {
  171. const farm = getSelectedFarm();
  172. const id =
  173. selectedFarmId.value ||
  174. localStorage.getItem("selectedFarmId") ||
  175. farm.farm_id ||
  176. farm.id;
  177. return id != null && id !== "";
  178. };
  179. /** 地图中心优先用农场 farm_location */
  180. const resolveFarmLocationWkt = (farm = getSelectedFarm()) => {
  181. const candidates = [farm.farm_location, farm.wkt, farm.geom_wkt, localStorage.getItem("selectedFarmPoint")];
  182. for (const item of candidates) {
  183. if (typeof item === "string" && /^POINT\s*\(/i.test(item.trim())) {
  184. return item.trim();
  185. }
  186. }
  187. return getMiniUserLocationPoint();
  188. };
  189. /** 有农场用农场点,无农场用登录用户点位 */
  190. const resolveMapCenterPoint = (farm = getSelectedFarm()) => {
  191. if (hasSelectedFarm()) return resolveFarmLocationWkt(farm);
  192. return getMiniUserLocationPoint();
  193. };
  194. const currentMapPoint = ref(resolveMapCenterPoint());
  195. const getSelectedZoneId = () => {
  196. const farm = getSelectedFarm();
  197. return farm.zone_id ?? farm.zoneId ?? "";
  198. };
  199. const syncSelectedFarmId = () => {
  200. selectedFarmId.value = String(
  201. localStorage.getItem("selectedFarmId") || getSelectedFarm().farm_id || getSelectedFarm().id || ""
  202. );
  203. };
  204. /** status=1 有范围;否则展示勾画引导(无农场不展示) */
  205. async function fetchFarmRangeStatus() {
  206. const farm = getSelectedFarm();
  207. const farmId = selectedFarmId.value || farm.farm_id || farm.id;
  208. const zoneId = panelZoneId.value ?? getSelectedZoneId();
  209. // 无农场不展示勾画引导
  210. if (farmId == null || farmId === "") {
  211. noFarmRange.value = false;
  212. await syncNoRangeGuide();
  213. return;
  214. }
  215. if (zoneId == null || zoneId === "") {
  216. noFarmRange.value = true;
  217. await syncNoRangeGuide();
  218. return;
  219. }
  220. try {
  221. const res = await VE_API.questionnaire.checkZoneRange({
  222. farm_id: farmId,
  223. zone_id: zoneId,
  224. });
  225. noFarmRange.value = Number(res?.data?.status) !== 1;
  226. } catch (e) {
  227. console.warn("[growth_report] checkZoneRange failed", e);
  228. noFarmRange.value = true;
  229. }
  230. await syncNoRangeGuide();
  231. }
  232. /** 无范围时在地图中心点画示意方框 + 下方提示 */
  233. async function syncNoRangeGuide() {
  234. await nextTick();
  235. if (!growthReportMap.kmap) return;
  236. if (!noFarmRange.value) {
  237. growthReportMap.clearNoRangeGuide();
  238. return;
  239. }
  240. const coordinate =
  241. growthReportMap.getCurrentCoordinate() ||
  242. convertPointToArray(currentMapPoint.value)?.map(Number);
  243. if (!coordinate?.length) return;
  244. growthReportMap.showNoRangeGuide(coordinate, noRangeTipRef.value);
  245. }
  246. const goAddZone = (type) => {
  247. router.push({
  248. path: "/add_zone",
  249. query: {
  250. type,
  251. isSuccessPopup: type === "farm" ? "1" : "0",
  252. },
  253. });
  254. };
  255. const showInteractCard = ref(false);
  256. const interactQuestionText = ref("物候问题物候问题");
  257. const interactOptions = ref([{id: 1, name: "未到"}, {id: 2, name: "顺利进入"}, {id: 3, name: "已过"}]);
  258. const selectedOptionId = ref(null);
  259. const interactSubmitting = ref(false);
  260. /** getOuterRecords 返回的 records */
  261. const outerRecords = ref([]);
  262. /** 当前展示的 records 下标 */
  263. const currentRecordIndex = ref(-1);
  264. /** 防止切换分区时旧请求回写 */
  265. let interactFetchToken = 0;
  266. function formatToday() {
  267. const d = new Date();
  268. const y = d.getFullYear();
  269. const m = String(d.getMonth() + 1).padStart(2, "0");
  270. const day = String(d.getDate()).padStart(2, "0");
  271. return `${y}-${m}-${day}`;
  272. }
  273. function getRequestErrorMessage(err) {
  274. const data = err?.response?.data;
  275. if (typeof data === "string" && data) return data;
  276. if (data?.msg) return data.msg;
  277. if (data?.message) return data.message;
  278. if (err?.message) return err.message;
  279. return "提交失败,请稍后再试";
  280. }
  281. /** interact_options 可能是 JSON 字符串或数组 */
  282. function parseInteractOptions(raw) {
  283. let list = raw;
  284. if (typeof raw === "string") {
  285. try {
  286. list = JSON.parse(raw);
  287. } catch {
  288. return [];
  289. }
  290. }
  291. if (!Array.isArray(list)) return [];
  292. return list.map((name, index) => ({
  293. id: index + 1,
  294. name: String(name),
  295. }));
  296. }
  297. function clearInteractCard() {
  298. outerRecords.value = [];
  299. currentRecordIndex.value = -1;
  300. interactQuestionText.value = "";
  301. interactOptions.value = [];
  302. selectedOptionId.value = null;
  303. showInteractCard.value = false;
  304. }
  305. function applyOuterRecordAt(index) {
  306. const records = outerRecords.value;
  307. if (!records.length || index < 0 || index >= records.length) {
  308. clearInteractCard();
  309. return;
  310. }
  311. currentRecordIndex.value = index;
  312. const record = records[index] || {};
  313. const question = record.interact_problem || "";
  314. const options = parseInteractOptions(record.interact_options);
  315. interactQuestionText.value = question;
  316. interactOptions.value = options;
  317. selectedOptionId.value = null;
  318. showInteractCard.value = Boolean(question || options.length);
  319. }
  320. function buildInteractSubmitPayload(optionText) {
  321. const record = outerRecords.value[currentRecordIndex.value] || {};
  322. const zoneId = panelZoneId.value ?? getSelectedZoneId();
  323. return {
  324. zone_id: Number(zoneId),
  325. date: formatToday(),
  326. // interact_code: record.interact_code || "",
  327. period_code: record.period_code || "",
  328. // interact_option: optionText,
  329. };
  330. }
  331. async function submitInteractOption(item) {
  332. if (interactSubmitting.value) return;
  333. selectedOptionId.value = item.id;
  334. interactSubmitting.value = true;
  335. try {
  336. const res = await VE_API.questionnaire.saveInteractResult(
  337. buildInteractSubmitPayload(item.name)
  338. );
  339. if (res?.code === 200) {
  340. ElMessage.success(res.msg || "提交成功");
  341. showInteractCard.value = false;
  342. await fetchInteractTask();
  343. return;
  344. }
  345. ElMessage.error(res?.msg || "提交失败,请稍后再试");
  346. } catch (err) {
  347. ElMessage.error(getRequestErrorMessage(err));
  348. } finally {
  349. interactSubmitting.value = false;
  350. }
  351. }
  352. /** 首项 → 上一条;末项 → 下一条;中间项提交 */
  353. async function handleInteractOptionClick(item) {
  354. const options = interactOptions.value;
  355. if (interactSubmitting.value || !item?.name || !options.length || currentRecordIndex.value < 0) {
  356. return;
  357. }
  358. const isFirst = item.id === options[0]?.id;
  359. const isLast = item.id === options[options.length - 1]?.id;
  360. // 仅一项时按提交处理
  361. if (options.length === 1 || (!isFirst && !isLast)) {
  362. await submitInteractOption(item);
  363. return;
  364. }
  365. selectedOptionId.value = item.id;
  366. if (isFirst) {
  367. if (currentRecordIndex.value > 0) {
  368. applyOuterRecordAt(currentRecordIndex.value - 1);
  369. return;
  370. }
  371. ElMessage.info("已是第一条");
  372. return;
  373. }
  374. if (isLast) {
  375. if (currentRecordIndex.value < outerRecords.value.length - 1) {
  376. applyOuterRecordAt(currentRecordIndex.value + 1);
  377. return;
  378. }
  379. ElMessage.info("已是最后一条");
  380. }
  381. }
  382. async function fetchInteractTask() {
  383. const zoneId = panelZoneId.value ?? getSelectedZoneId();
  384. if (zoneId === "" || zoneId == null) {
  385. clearInteractCard();
  386. return;
  387. }
  388. const token = ++interactFetchToken;
  389. try {
  390. const res = await VE_API.questionnaire.getOuterRecords({
  391. zone_id: zoneId,
  392. });
  393. if (token !== interactFetchToken) return;
  394. if (res?.code !== 200) {
  395. clearInteractCard();
  396. return;
  397. }
  398. const data = res?.data || {};
  399. const records = Array.isArray(data.records) ? data.records : [];
  400. outerRecords.value = records;
  401. if (!records.length) {
  402. clearInteractCard();
  403. return;
  404. }
  405. // 默认展示与外层 interact_code 匹配的那一项
  406. const matchedIndex = records.findIndex(
  407. (item) => item?.interact_code && item.interact_code === data.interact_code
  408. );
  409. applyOuterRecordAt(matchedIndex >= 0 ? matchedIndex : 0);
  410. } catch (e) {
  411. if (token !== interactFetchToken) return;
  412. console.warn("[growth_report] getOuterRecords failed", e);
  413. clearInteractCard();
  414. }
  415. }
  416. /** 农场品类编码:接口 crop_code 对应缓存 category_code */
  417. function resolveCropCode(farm = getSelectedFarm()) {
  418. return farm.category_code || farm.categorycode || farm.crop_code || "";
  419. }
  420. /** 本地 dem 路径转 mapimg 可访问地址:取文件名挂到 /mapimg/ */
  421. function resolveCalcAssetUrl(path) {
  422. if (!path) return "";
  423. const raw = String(path).trim();
  424. if (/^https?:\/\//i.test(raw)) return raw;
  425. const fileName = raw.replace(/\\/g, "/").split("/").filter(Boolean).pop();
  426. if (!fileName) return "";
  427. // /home/dem_ditu/输出/从化区_xxx.png → https://feiniao-farm-h5-v3.feiniaotech.com/mapimg/从化区_xxx.png
  428. return `https://feiniao-farm-h5-v3.feiniaotech.com/mapimg/${encodeURIComponent(fileName)}`;
  429. }
  430. /** risk_name 拆成两行展示,如「高温高湿」→ 高温 / 高湿 */
  431. function splitRiskName(name) {
  432. const text = String(name || "").trim();
  433. if (!text) return { line1: "-", line2: "" };
  434. if (text.length <= 2) return { line1: text, line2: "" };
  435. const mid = Math.ceil(text.length / 2);
  436. return { line1: text.slice(0, mid), line2: text.slice(mid) };
  437. }
  438. function mapCalcRecordToMenuItem(record) {
  439. const { line1, line2 } = splitRiskName(record?.risk_name);
  440. const bounds = Array.isArray(record?.bounds) ? record.bounds.map(Number) : [];
  441. const extent =
  442. bounds.length === 4 && bounds.every((n) => Number.isFinite(n)) ? bounds : null;
  443. return {
  444. key: String(record?.id ?? record?.risk_name ?? ""),
  445. line1,
  446. line2,
  447. icon_path: record?.icon_path || menuIcon,
  448. mapUrl: resolveCalcAssetUrl(record?.png_path),
  449. extent,
  450. legendUrl: resolveCalcAssetUrl(record?.legend_path),
  451. };
  452. }
  453. function clearMapCalcState() {
  454. stressMenuItems.value = [];
  455. activeMenuKey.value = "";
  456. activeLegendUrl.value = "";
  457. growthReportMap.hideRemoteImage();
  458. }
  459. /** 按侧边菜单叠加对应气象底图 + 图例 */
  460. function applyMenuOverlay(key) {
  461. const item = stressMenuItems.value.find((m) => m.key === key);
  462. if (!item?.mapUrl || !item.extent || !growthReportMap.kmap) {
  463. growthReportMap.hideRemoteImage();
  464. activeLegendUrl.value = "";
  465. return;
  466. }
  467. growthReportMap.showRemoteImage(item.mapUrl, item.extent);
  468. activeLegendUrl.value = item.legendUrl || "";
  469. }
  470. function applyMapCalcRecords(records) {
  471. stressMenuItems.value = records.map(mapCalcRecordToMenuItem).filter((item) => item.key);
  472. const first = stressMenuItems.value[0];
  473. if (first) {
  474. activeMenuKey.value = first.key;
  475. applyMenuOverlay(first.key);
  476. } else {
  477. clearMapCalcState();
  478. }
  479. }
  480. /** 无农场:按区县 adcode 拉气象底图 */
  481. async function fetchHomeMapCalcByCounty(countyCode) {
  482. const token = ++mapCalcToken;
  483. if (!countyCode) {
  484. clearMapCalcState();
  485. return;
  486. }
  487. try {
  488. const res = await VE_API.questionnaire.getHomeMapCalcByCounty({
  489. county_code: countyCode,
  490. });
  491. if (token !== mapCalcToken) return;
  492. const records = Array.isArray(res?.data?.records) ? res.data.records.slice(0, 3) : [];
  493. applyMapCalcRecords(records);
  494. } catch (e) {
  495. if (token !== mapCalcToken) return;
  496. console.warn("[growth_report] getHomeMapCalcByCounty failed", e);
  497. clearMapCalcState();
  498. }
  499. }
  500. /** 有农场:按农场/分区拉气象底图 */
  501. async function fetchHomeMapCalc() {
  502. if (!hasSelectedFarm()) return;
  503. const token = ++mapCalcToken;
  504. const farm = getSelectedFarm();
  505. const farmId = selectedFarmId.value || farm.farm_id || farm.id;
  506. const zoneId = panelZoneId.value;
  507. const cropCode = resolveCropCode(farm);
  508. if (
  509. farmId == null ||
  510. farmId === "" ||
  511. zoneId == null ||
  512. zoneId === "" ||
  513. !cropCode
  514. ) {
  515. clearMapCalcState();
  516. return;
  517. }
  518. try {
  519. const res = await VE_API.questionnaire.getHomeMapCalc({
  520. farm_id: farmId,
  521. zone_id: zoneId,
  522. calc_date: formatToday(),
  523. crop_code: cropCode,
  524. });
  525. if (token !== mapCalcToken) return;
  526. const records = Array.isArray(res?.data?.records) ? res.data.records.slice(0, 3) : [];
  527. applyMapCalcRecords(records);
  528. } catch (e) {
  529. if (token !== mapCalcToken) return;
  530. console.warn("[growth_report] getHomeMapCalc failed", e);
  531. clearMapCalcState();
  532. }
  533. }
  534. const handleZoneChange = (zoneId) => {
  535. panelZoneId.value = zoneId != null && zoneId !== "" ? zoneId : null;
  536. fetchHomeMapCalc();
  537. fetchFarmRangeStatus();
  538. // fetchInteractTask();
  539. };
  540. async function getLocationName(point = currentMapPoint.value) {
  541. let locationParam = "";
  542. // 无农场:逆地理用登录点 MINI_USER_LOCATION(lng,lat)
  543. if (!hasSelectedFarm()) {
  544. locationParam = toLocationParam(getMiniUserLocationText());
  545. }
  546. if (!locationParam) {
  547. const locationPoint =
  548. point ||
  549. localStorage.getItem("selectedFarmPoint") ||
  550. getMiniUserLocationPoint();
  551. if (!locationPoint) return;
  552. const farmLocation = convertPointToArray(locationPoint);
  553. if (Array.isArray(farmLocation) && farmLocation.length >= 2) {
  554. locationParam = `${farmLocation[0]},${farmLocation[1]}`;
  555. } else if (typeof farmLocation === "string" && farmLocation.includes(",")) {
  556. locationParam = toLocationParam(farmLocation);
  557. } else {
  558. return;
  559. }
  560. }
  561. try {
  562. const { result } = await fetchRegeo({ location: locationParam });
  563. locationName.value = result?.address_component
  564. ? result.address_component.city + result.address_component.district
  565. : (result?.address || "") + "";
  566. // 无农场时用 adcode 拉区县气象底图,并同步给预警面板
  567. if (!hasSelectedFarm()) {
  568. const adcode = result?.ad_info?.adcode;
  569. if (adcode != null && adcode !== "") {
  570. const code = String(adcode);
  571. countyCode.value = code;
  572. localStorage.setItem(COUNTY_CODE_STORAGE_KEY, code);
  573. await fetchHomeMapCalcByCounty(code);
  574. } else {
  575. countyCode.value = "";
  576. localStorage.removeItem(COUNTY_CODE_STORAGE_KEY);
  577. clearMapCalcState();
  578. }
  579. }
  580. } catch (e) {
  581. console.warn("[growth_report] getLocationName failed", e);
  582. }
  583. }
  584. function saveCurrentMapPoint(point) {
  585. if (!point) return;
  586. currentMapPoint.value = point;
  587. localStorage.setItem(MAP_POINT_STORAGE_KEY, point);
  588. }
  589. const initMap = async () => {
  590. await nextTick();
  591. if (!mapContainer.value) return;
  592. if (growthReportMap.kmap) {
  593. growthReportMap.kmap.map?.updateSize?.();
  594. const coordinate = convertPointToArray(currentMapPoint.value)?.map(Number);
  595. if (coordinate?.length === 2) {
  596. growthReportMap.setMapPosition(coordinate);
  597. }
  598. await syncNoRangeGuide();
  599. applyMenuOverlay(activeMenuKey.value);
  600. return;
  601. }
  602. const location = currentMapPoint.value || store.state.home.miniUserLocationPoint || DEFAULT_MAP_POINT;
  603. currentMapPoint.value = location;
  604. growthReportMap.initMap(location, mapContainer.value);
  605. await syncNoRangeGuide();
  606. applyMenuOverlay(activeMenuKey.value);
  607. };
  608. function applySelectedLocation() {
  609. const raw = sessionStorage.getItem(LOCATION_SESSION_KEY);
  610. if (!raw) return false;
  611. sessionStorage.removeItem(LOCATION_SESSION_KEY);
  612. let saved = null;
  613. try {
  614. saved = JSON.parse(raw);
  615. } catch {
  616. return false;
  617. }
  618. if (!saved?.point && !saved?.coordinate) return false;
  619. const point = saved.point || `POINT(${saved.coordinate[0]} ${saved.coordinate[1]})`;
  620. const coordinate = (saved.coordinate || convertPointToArray(point)).map(Number);
  621. saveCurrentMapPoint(point);
  622. if (growthReportMap.kmap && coordinate?.length === 2) {
  623. growthReportMap.setMapPosition(coordinate);
  624. }
  625. getLocationName(point);
  626. return true;
  627. }
  628. /** 有农场落到 farm_location;无农场落到登录用户点位 */
  629. function syncMapFromSelectedFarm() {
  630. const point = resolveMapCenterPoint();
  631. if (!point) return false;
  632. saveCurrentMapPoint(point);
  633. const coordinate = convertPointToArray(point)?.map(Number);
  634. if (growthReportMap.kmap && coordinate?.length === 2) {
  635. growthReportMap.setMapPosition(coordinate);
  636. }
  637. getLocationName(point);
  638. return true;
  639. }
  640. /** 定位按钮:地图回到农场 / 用户点位中心 */
  641. const handleLocateToCenter = () => {
  642. if (!growthReportMap.kmap) return;
  643. const point = resolveMapCenterPoint() || currentMapPoint.value;
  644. if (!point) return;
  645. saveCurrentMapPoint(point);
  646. const coordinate = convertPointToArray(point)?.map(Number);
  647. if (coordinate?.length === 2) {
  648. growthReportMap.setMapPosition(coordinate);
  649. }
  650. };
  651. const handleSwitchFarm = () => {
  652. router.push("/all_garden");
  653. };
  654. const handleSwitchLocation = () => {
  655. // 以已确认的 currentMapPoint 为准,避免地图临时中心覆盖已选位置
  656. const mapCenter =
  657. currentMapPoint.value ||
  658. store.state.home.miniUserLocationPoint ||
  659. DEFAULT_MAP_POINT;
  660. router.push({
  661. path: "/entry_select_location",
  662. query: {
  663. mapCenter,
  664. iconType: "current",
  665. from: "growth_report",
  666. },
  667. });
  668. };
  669. const handleSelectMenu = (key) => {
  670. if (!key) return;
  671. // 再次点击同一项则关闭叠加
  672. if (activeMenuKey.value === key) {
  673. activeMenuKey.value = "";
  674. activeLegendUrl.value = "";
  675. growthReportMap.hideRemoteImage();
  676. return;
  677. }
  678. activeMenuKey.value = key;
  679. applyMenuOverlay(key);
  680. };
  681. const handleInvite = (type) => {
  682. const isService = type === "service";
  683. let inviteName = "好友";
  684. try {
  685. const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
  686. inviteName = userInfo.nickName || userInfo.userName || userInfo.name || inviteName;
  687. } catch {
  688. // ignore
  689. }
  690. const inviteType = isService ? "service" : "farmer";
  691. const adminId = localStorage.getItem("MINI_USER_ID") || "";
  692. const query = {
  693. askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
  694. shareText: isService ? "邀请您完善农服信息" : "邀请您完善地块种植信息",
  695. targetUrl: `invite_welcome`,
  696. paramsPage: JSON.stringify({ inviteName, inviteType, adminId }),
  697. imageUrl: 'https://birdseye-img.sysuimars.com/farm-h5/field.png',
  698. };
  699. wx.miniProgram.navigateTo({
  700. url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
  701. });
  702. }
  703. const handlePanelHeightChange = (height) => {
  704. panelHeight.value = Number(height) || 0;
  705. };
  706. onMounted(() => {
  707. // syncSelectedFarmId();
  708. // syncMapFromSelectedFarm();
  709. getLocationName();
  710. // initMap();
  711. // fetchFarmRangeStatus();
  712. // fetchInteractTask();
  713. });
  714. onActivated(() => {
  715. syncSelectedFarmId();
  716. const applied = applySelectedLocation();
  717. if (!applied) syncMapFromSelectedFarm();
  718. initMap();
  719. fetchFarmRangeStatus();
  720. // fetchInteractTask();
  721. });
  722. </script>
  723. <style lang="scss" scoped>
  724. .growth-report-page {
  725. width: 100%;
  726. height: 100%;
  727. background: #0A0A0A;
  728. box-sizing: border-box;
  729. .interact-float {
  730. margin-bottom: 6px;
  731. pointer-events: none;
  732. padding: 1px;
  733. background: linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, rgba(153, 153, 153, 0.25) 100%);
  734. border-radius: 14px;
  735. backdrop-filter: blur(4px);
  736. .interact-card {
  737. position: relative;
  738. pointer-events: auto;
  739. overflow: hidden;
  740. border-radius: 14px;
  741. padding: 10px;
  742. background: linear-gradient(180deg, rgba(26, 25, 29, 0.6) 0%, rgba(18, 18, 22, 0.6) 100%);
  743. }
  744. .interact-card__head {
  745. position: relative;
  746. }
  747. .interact-card__tab {
  748. display: inline-flex;
  749. align-items: center;
  750. gap: 5px;
  751. height: 32px;
  752. padding: 0 32px 10px 8px;
  753. color: #fff;
  754. font-size: 16px;
  755. span {
  756. margin-top: -3px;
  757. }
  758. }
  759. .interact-card__star {
  760. width: 16px;
  761. height: 14px;
  762. }
  763. .interact-card__close {
  764. position: absolute;
  765. top: 12px;
  766. right: 10px;
  767. font-size: 16px;
  768. color: rgba(255, 255, 255, 0.5);
  769. cursor: pointer;
  770. }
  771. .interact-card__qa {
  772. }
  773. .interact-card__question {
  774. font-size: 16px;
  775. color: #fff;
  776. display: flex;
  777. align-items: center;
  778. gap: 12px;
  779. .interact-card__question-icon {
  780. display: flex;
  781. align-items: center;
  782. justify-content: center;
  783. width: 31px;
  784. height: 31px;
  785. background: rgba(255, 255, 255, 0.05);
  786. border: 1px solid rgba(255, 255, 255, 0.12);
  787. border-radius: 8px;
  788. .interact-card__question-icon-img {
  789. width: 13px;
  790. }
  791. }
  792. }
  793. .interact-card__options {
  794. display: flex;
  795. gap: 8px;
  796. margin-top: 10px;
  797. }
  798. .interact-option {
  799. position: relative;
  800. flex: 1;
  801. min-width: 0;
  802. height: 41px;
  803. line-height: 41px;
  804. border-radius: 6px;
  805. background: rgba(255, 255, 255, 0.05);
  806. border: 1px solid rgba(255, 255, 255, 0.12);
  807. text-align: center;
  808. box-sizing: border-box;
  809. cursor: pointer;
  810. &__text {
  811. display: block;
  812. padding: 0 6px;
  813. overflow: hidden;
  814. text-overflow: ellipsis;
  815. white-space: nowrap;
  816. font-size: 14px;
  817. color: rgba(255, 255, 255, 0.7);
  818. }
  819. &.selected {
  820. border-color: rgba(255, 255, 255, 0.5);
  821. background: rgba(255, 255, 255, 0.1);
  822. .interact-option__text {
  823. color: #fff;
  824. }
  825. // &::after {
  826. // content: "";
  827. // position: absolute;
  828. // top: -1px;
  829. // right: -1px;
  830. // width: 18px;
  831. // height: 14px;
  832. // background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
  833. // }
  834. }
  835. &.disabled {
  836. opacity: 0.65;
  837. pointer-events: none;
  838. }
  839. }
  840. }
  841. .location-bar {
  842. position: fixed;
  843. top: 12px;
  844. left: 12px;
  845. z-index: 16;
  846. display: inline-flex;
  847. align-items: center;
  848. gap: 4px;
  849. max-width: calc(100% - 24px);
  850. height: 32px;
  851. padding: 0 12px;
  852. border-radius: 16px;
  853. background: rgba(0, 0, 0, 0.4);
  854. backdrop-filter: blur(4px);
  855. color: #fff;
  856. font-size: 14px;
  857. box-sizing: border-box;
  858. &__icon {
  859. font-size: 16px;
  860. flex-shrink: 0;
  861. }
  862. &__name {
  863. max-width: calc(100vw - 80px);
  864. }
  865. &__action {
  866. flex-shrink: 0;
  867. color: #FFD489;
  868. }
  869. }
  870. .growth-top {
  871. width: 100%;
  872. position: fixed;
  873. top: 52px;
  874. z-index: 15;
  875. pointer-events: none;
  876. padding: 0 12px;
  877. box-sizing: border-box;
  878. .growth-top__content {
  879. display: flex;
  880. align-items: flex-start;
  881. justify-content: space-between;
  882. }
  883. }
  884. .side-menu {
  885. display: flex;
  886. flex-direction: column;
  887. align-items: center;
  888. gap: 12px;
  889. padding: 4px;
  890. border-radius: 10px;
  891. pointer-events: auto;
  892. background: #1b1b1d;
  893. box-sizing: border-box;
  894. border: 1px solid rgba(255, 255, 255, 0.12);
  895. &__item {
  896. display: flex;
  897. flex-direction: column;
  898. align-items: center;
  899. gap: 4px;
  900. width: 45px;
  901. color: #C1C1C1;
  902. padding: 5px 10px;
  903. box-sizing: border-box;
  904. cursor: pointer;
  905. &.active {
  906. color: #FFD489;
  907. background: #2C2C2C;
  908. border-radius: 6px;
  909. .side-menu__icon {
  910. opacity: 1;
  911. // filter: brightness(0) saturate(100%) invert(48%) sepia(98%) saturate(1805%) hue-rotate(178deg) brightness(98%) contrast(97%);
  912. }
  913. }
  914. }
  915. &__icon {
  916. width: 21px;
  917. object-fit: contain;
  918. filter: grayscale(1);
  919. }
  920. &__text {
  921. display: flex;
  922. flex-direction: column;
  923. align-items: center;
  924. font-size: 12px;
  925. line-height: 14px;
  926. text-align: center;
  927. }
  928. }
  929. .invite-group {
  930. pointer-events: none;
  931. .invite-btn {
  932. pointer-events: auto;
  933. height: 48px;
  934. width: 48px;
  935. display: flex;
  936. align-items: center;
  937. justify-content: center;
  938. flex-direction: column;
  939. border-radius: 10px;
  940. border: 1px solid rgba(255, 255, 255, 0.2);
  941. background: rgba(20, 20, 22, 0.8);
  942. gap: 6px;
  943. color: rgba(255, 255, 255, 0.6);
  944. font-size: 10px;
  945. .invite-icon {
  946. width: 16px;
  947. }
  948. .current-icon {
  949. width: 20px;
  950. }
  951. }
  952. .invite-btn+.invite-btn {
  953. margin-top: 10px;
  954. }
  955. }
  956. .map-legend {
  957. position: fixed;
  958. right: 10px;
  959. z-index: 20;
  960. pointer-events: none;
  961. // transition: bottom 0.2s ease;
  962. // display: flex;
  963. // align-items: center;
  964. // gap: 10px;
  965. // box-sizing: border-box;
  966. &__img {
  967. display: block;
  968. max-width: 180px;
  969. max-height: 160px;
  970. object-fit: contain;
  971. }
  972. &--en {
  973. flex-direction: column;
  974. align-items: flex-start;
  975. gap: 6px;
  976. padding: 8px 10px;
  977. background: rgba(0, 0, 0, 0.72);
  978. border-radius: 6px;
  979. .map-legend__item {
  980. gap: 6px;
  981. }
  982. .map-legend__pill {
  983. width: 18px;
  984. height: 4px;
  985. flex-shrink: 0;
  986. }
  987. .map-legend__text {
  988. font-size: 11px;
  989. line-height: 1.2;
  990. white-space: nowrap;
  991. }
  992. }
  993. &__item {
  994. display: flex;
  995. align-items: center;
  996. gap: 5px;
  997. }
  998. &__pill {
  999. width: 16px;
  1000. height: 5px;
  1001. border-radius: 10px;
  1002. &--zone {
  1003. background: #13a27f;
  1004. }
  1005. &--growth {
  1006. background: #ff9138;
  1007. }
  1008. &--pest {
  1009. background: #e62e2d;
  1010. }
  1011. }
  1012. &__text {
  1013. font-size: 12px;
  1014. color: #fff;
  1015. }
  1016. }
  1017. .report-content {
  1018. position: relative;
  1019. height: 100%;
  1020. box-sizing: border-box;
  1021. .map-container {
  1022. width: 100%;
  1023. height: 100%;
  1024. }
  1025. }
  1026. .no-range-tip {
  1027. display: none;
  1028. align-items: center;
  1029. gap: 2px;
  1030. max-width: calc(100vw - 48px);
  1031. height: 36px;
  1032. padding: 0 14px;
  1033. border-radius: 18px;
  1034. background: rgba(40, 48, 36, 0.72);
  1035. backdrop-filter: blur(4px);
  1036. color: #fff;
  1037. font-size: 14px;
  1038. white-space: nowrap;
  1039. box-sizing: border-box;
  1040. cursor: pointer;
  1041. &__arrow {
  1042. font-size: 14px;
  1043. flex-shrink: 0;
  1044. }
  1045. }
  1046. }
  1047. </style>