index.vue 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261
  1. <template>
  2. <div class="base-container no-events">
  3. <fnHeader showDate :autoGo="true" hideSwitch></fnHeader>
  4. <div class="content">
  5. <div class="warning-l left">
  6. <div class="warning-top">
  7. <div class="top-l yes-events">
  8. <div>
  9. <el-cascader
  10. style="width: 184px"
  11. :show-all-levels="false"
  12. v-model="areaVal"
  13. :props="props1"
  14. :options="areaListOptions"
  15. @change="toggleArea"
  16. popper-class="area-cascader"
  17. />
  18. </div>
  19. </div>
  20. <div class="top-r yes-events">
  21. <div class="data-box" @click="toggleBox('面积')" :class="{ active: activeBoxName === '面积' }">
  22. <div class="data-value">
  23. <span>{{ areaVal.includes("3186") ? 31.2 : 419.89 }}</span
  24. >万亩
  25. </div>
  26. <div class="data-name">种植面积</div>
  27. </div>
  28. <div
  29. class="data-box"
  30. v-if="areaVal.includes('3186')"
  31. @click="toggleBox('从化荔枝')"
  32. :class="{ active: activeBoxName === '从化荔枝' }"
  33. >
  34. <div class="data-value"><span>11.9</span>万亩</div>
  35. <div class="data-name">疑似失管面积</div>
  36. </div>
  37. <div class="data-box" @click="toggleBox('产量')" :class="{ active: activeBoxName === '产量' }">
  38. <div class="data-value">
  39. <span>{{ areaVal.includes("3186") ? 10.4 : 192.12 }}</span
  40. >万吨
  41. </div>
  42. <div class="data-name">预估产量</div>
  43. </div>
  44. </div>
  45. </div>
  46. <div class="warning-alarm yes-events" v-show="activeBaseTab === '预警分布'">
  47. <alarm-list></alarm-list>
  48. </div>
  49. <div class="time-wrap yes-events">
  50. <time-line></time-line>
  51. </div>
  52. </div>
  53. <div class="action-legend" v-if="activeBaseTab !== '农场分布' && activeBaseTab !== '农服管理'">
  54. <el-tree
  55. ref="treeRef"
  56. class="yes-events"
  57. style="max-width: 250px"
  58. :data="treeActionData"
  59. show-checkbox
  60. node-key="id"
  61. :default-expanded-keys="defaultExpandedKeys"
  62. :default-checked-keys="defaultCheckedKeys"
  63. :props="defaultProps"
  64. @check="getTreeChecks"
  65. >
  66. <template #default="{ node, data }">
  67. <div class="custom-tree-node">
  68. <span>{{ node.label }}</span>
  69. <div v-if="node.level === 1" class="level-legend">
  70. <span class="legend-dot" :style="{ backgroundColor: data.color }"></span>
  71. <span class="legend-text" :style="{ color: data.color }">图例</span>
  72. </div>
  73. </div>
  74. </template>
  75. </el-tree>
  76. </div>
  77. <div v-if="!showDetail" class="warning-r right chart-wrap yes-events">
  78. <chart-list :activeBaseTab="activeBaseTab"></chart-list>
  79. <!-- <farmInfoGroup></farmInfoGroup> -->
  80. </div>
  81. <div v-else class="warning-r right yes-events">
  82. <farmInfoGroup></farmInfoGroup>
  83. </div>
  84. <!-- 地图图例 -->
  85. <map-legend :type="activeBaseTab"></map-legend>
  86. <!-- 地图搜索 -->
  87. <div class="warning-search yes-events">
  88. <el-select
  89. v-model="locationVal"
  90. filterable
  91. remote
  92. reserve-keyword
  93. placeholder="搜索地区"
  94. :remote-method="remoteMethod"
  95. :loading="loading"
  96. @change="handleSearchRes"
  97. class="v-select"
  98. popper-class="focus-farm-select"
  99. style="width: 375px"
  100. >
  101. <template #prefix>
  102. <el-icon class="el-input__icon"><search /></el-icon>
  103. </template>
  104. <el-option
  105. v-for="(item, index) in locationOptions.list"
  106. :key="index"
  107. :label="item.title"
  108. :value="item.point"
  109. >
  110. <span>{{ item.title }}</span>
  111. <span class="sub-title">{{ item.province }}{{ item.city }}{{ item.district }}</span>
  112. </el-option>
  113. </el-select>
  114. </div>
  115. <div class="base-tabs yes-events">
  116. <div
  117. v-for="item in baseTabs"
  118. :key="item"
  119. class="tab-item"
  120. :class="{ active: item === activeBaseTab }"
  121. @click="handleTabClick(item)"
  122. >
  123. {{ item }}
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. <div ref="mapRef" class="bottom-map"></div>
  129. <div id="popup" class="ol-popup-warning">
  130. <div class="warning-info-title">
  131. <div class="icon">
  132. <img src="@/assets/images/common/chart-icon.png" />
  133. </div>
  134. <div id="popup-title"></div>
  135. <div class="close" @click="destroyPopup">
  136. <img src="@/assets/images/warningHome/close-btn.png" />
  137. </div>
  138. </div>
  139. <div id="popup-content" class="info-content"></div>
  140. </div>
  141. <track-dialog></track-dialog>
  142. </template>
  143. <script setup>
  144. import "./map/mockFarmLayer";
  145. import StaticMapLayers from "@/components/static_map_change/Layers.js";
  146. import StaticMapPointLayers from "@/components/static_map_change/pointLayer.js";
  147. import { onMounted, onUnmounted, ref, reactive, nextTick } from "vue";
  148. import fnHeader from "@/components/fnHeader.vue";
  149. import WarningMap from "./warningMap";
  150. import AlarmLayer from "./map/alarmLayer";
  151. import DistributionLayer from "./map/distributionLayer";
  152. import trackDialog from "./components/trackDialog.vue";
  153. import alarmList from "./components/alarmList.vue";
  154. import timeLine from "./components/timeLine.vue";
  155. import eventBus from "@/api/eventBus";
  156. import { areaListOptions } from "./area";
  157. import { useStore } from "vuex";
  158. import farmInfoGroup from "./components/farmInfoGroup.vue";
  159. import mapLegend from "./components/mapLegend.vue";
  160. import chartList from "./components/chart_components/chartList.vue";
  161. let store = useStore();
  162. let warningMap = new WarningMap();
  163. let alarmLayer = null;
  164. let staticMapLayers = null;
  165. let staticMapPointLayers = null;
  166. let distributionLayer = null;
  167. const areaVal = ref(["3"]);
  168. const mapRef = ref(null);
  169. const showDetail = ref(false);
  170. const treeRef = ref(null);
  171. const defaultProps = {
  172. children: "items",
  173. label: "name",
  174. };
  175. import lz from "@/assets/images/map/type/荔枝.png";
  176. import ly from "@/assets/images/map/type/龙眼.png";
  177. import sd from "@/assets/images/map/type/水稻.png";
  178. import xm from "@/assets/images/map/type/小麦.png";
  179. import bc from "@/assets/images/map/type/小麦.png";
  180. import lb from "@/assets/images/map/type/小麦.png";
  181. // 冷链冷库、加工厂图标(与图例保持一致)
  182. import coldChainIcon from "@/assets/images/common/legend-icon-1.png";
  183. import factoryIcon from "@/assets/images/common/legend-icon-2.png";
  184. const originTreeData = [
  185. {
  186. id: 1,
  187. label: "果类",
  188. color: "#51B2FF",
  189. fillColor: "rgba(5, 49, 84, 0.5)",
  190. geom: "MULTIPOLYGON (((113.58668302396221 23.244659822289524, 113.32095411231998 23.24298858095617, 112.99338890476295 23.002328435946534, 113.13878785387456 22.604570799124076, 113.59503930394511 22.86862839611681, 113.58668302396221 23.244659822289524)))",
  191. children: [
  192. {
  193. id: 4,
  194. label: "荔枝",
  195. color: "#2199F8",
  196. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E8%8D%94%E6%9E%9D.png",
  197. imgName: lz,
  198. wktArr: [
  199. "POINT(113.33722309500006 23.204074978290652)",
  200. "POINT(113.53593237057355 23.188789823486065)",
  201. "POINT(113.36970447853234 23.064596505297875)",
  202. ],
  203. },
  204. {
  205. id: 55,
  206. label: "龙眼",
  207. color: "#2199F8",
  208. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E7%99%BD%E7%B3%96.png",
  209. imgName: ly,
  210. wktArr: [
  211. "POINT(113.29900983080294 22.949956545068478)",
  212. "POINT(113.10412186488536 22.800924630297875)",
  213. "POINT(113.32384842738536 22.776086033715462)",
  214. "POINT(113.22640444442656 22.90983245840535)",
  215. ],
  216. },
  217. ],
  218. },
  219. {
  220. id: 2,
  221. label: "粮食",
  222. color: "#FF8E1C",
  223. fillColor: "rgba(83, 46, 8, 0.5)",
  224. geom: "MULTIPOLYGON (((112.42256410334443 23.14863707066071, 112.09584019992684 22.842930537988664, 112.25060416300033 22.62511471413565, 112.80278650958275 22.749307864685775, 112.75119857777094 23.13144100957107, 112.42256410334443 23.14863707066071)))",
  225. children: [
  226. {
  227. id: 5,
  228. label: "水稻",
  229. color: "#FAA53D",
  230. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E6%8C%82%E7%BB%BF.png",
  231. imgName: sd,
  232. wktArr: [
  233. "POINT(112.70843577567298 22.969169477864167)",
  234. "POINT(112.23636397199755 22.85839626464389)",
  235. "POINT(112.35889247053002 23.008153324438585)",
  236. "POINT(112.47894566011911 22.9017143340381)",
  237. ],
  238. },
  239. {
  240. id: 6,
  241. label: "小麦",
  242. color: "#FAA53D",
  243. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E6%97%A0%E6%A0%B8.png",
  244. imgName: xm,
  245. wktArr: [
  246. "POINT(112.36777193304151 22.73892833157863)",
  247. "POINT(112.26684873669083 22.71150357559281)",
  248. "POINT(112.65408646638161 22.84862740364581)",
  249. ],
  250. },
  251. ],
  252. },
  253. {
  254. id: 3,
  255. label: "蔬菜",
  256. color: "#25BC07",
  257. fillColor: "rgba(0, 69, 4, 0.5)",
  258. children: [
  259. {
  260. id: 7,
  261. label: "白菜",
  262. color: "#7ABB00",
  263. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E7%99%BD%E8%8F%9C.png",
  264. imgName: bc,
  265. wktArr: [
  266. "POINT(110.34100329503417 21.516399336978793)",
  267. "POINT(113.89499662443995 22.653799122199416)",
  268. "POINT(113.9329988323152 22.653600638732314)",
  269. "POINT(113.94400024786592 22.614900553599)",
  270. ],
  271. },
  272. {
  273. id: 8,
  274. label: "萝卜",
  275. color: "#7ABB00",
  276. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E8%90%9D%E8%8F%9C.png",
  277. imgName: lb,
  278. wktArr: [
  279. "POINT(110.34100329503417 21.516399336978793)",
  280. "POINT(113.89499662443995 22.653799122199416)",
  281. "POINT(113.9329988323152 22.653600638732314)",
  282. "POINT(113.94400024786592 22.614900553599)",
  283. ],
  284. },
  285. ],
  286. },
  287. ];
  288. const phenologyData = [
  289. {
  290. id: 1,
  291. label: "果类",
  292. color: "#51B2FF",
  293. fillColor: "rgba(5, 49, 84, 0.5)",
  294. geom: "MULTIPOLYGON (((113.58668302396221 23.244659822289524, 113.32095411231998 23.24298858095617, 112.99338890476295 23.002328435946534, 113.13878785387456 22.604570799124076, 113.59503930394511 22.86862839611681, 113.58668302396221 23.244659822289524)))",
  295. children: [
  296. {
  297. id: 4,
  298. label: "荔枝",
  299. color: "#2199F8",
  300. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E8%8D%94%E6%9E%9D.png",
  301. imgName: lz,
  302. children: [
  303. {
  304. id: 13,
  305. label: "秋梢期",
  306. color: "#58B5FF",
  307. imgName: lz,
  308. wktArr: [
  309. "POINT(113.33722309500006 23.204074978290652)",
  310. "POINT(113.53593237057355 23.188789823486065)",
  311. ],
  312. },
  313. {
  314. id: 14,
  315. label: "膨果期",
  316. color: "#1688E3",
  317. imgName: lz,
  318. wktArr: [
  319. "POINT(113.32095411231998 23.24298858095617)",
  320. ],
  321. },
  322. {
  323. id: 15,
  324. label: "成熟期",
  325. color: "#3D8CCB",
  326. imgName: lz,
  327. wktArr: [
  328. "POINT(113.36970447853234 23.064596505297875)",
  329. ],
  330. },
  331. ],
  332. },
  333. {
  334. id: 55,
  335. label: "龙眼",
  336. color: "#2199F8",
  337. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E7%99%BD%E7%B3%96.png",
  338. imgName: ly,
  339. children: [
  340. {
  341. id: 16,
  342. label: "秋梢期",
  343. color: "#5986AA",
  344. imgName: ly,
  345. wktArr: [
  346. "POINT(113.29900983080294 22.949956545068478)",
  347. "POINT(113.10412186488536 22.800924630297875)",
  348. ],
  349. },
  350. {
  351. id: 17,
  352. label: "膨果期",
  353. color: "#79ABD3",
  354. imgName: ly,
  355. wktArr: [
  356. "POINT(113.32384842738536 22.776086033715462)",
  357. "POINT(113.22640444442656 22.90983245840535)",
  358. ],
  359. },
  360. ],
  361. },
  362. ],
  363. },
  364. {
  365. id: 2,
  366. label: "粮食",
  367. color: "#FF8E1C",
  368. fillColor: "rgba(83, 46, 8, 0.5)",
  369. geom: "MULTIPOLYGON (((112.42256410334443 23.14863707066071, 112.09584019992684 22.842930537988664, 112.25060416300033 22.62511471413565, 112.80278650958275 22.749307864685775, 112.75119857777094 23.13144100957107, 112.42256410334443 23.14863707066071)))",
  370. children: [
  371. {
  372. id: 5,
  373. label: "水稻",
  374. color: "#FAA53D",
  375. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E6%8C%82%E7%BB%BF.png",
  376. imgName: sd,
  377. children: [
  378. {
  379. id: 10,
  380. label: "拔节期",
  381. color: "#985400",
  382. imgName: sd,
  383. wktArr: [
  384. "POINT(112.36777193304151 22.73892833157863)",
  385. ],
  386. },
  387. {
  388. id: 11,
  389. label: "孕穗期",
  390. color: "#512D00",
  391. imgName: sd,
  392. wktArr: [
  393. "POINT(112.26684873669083 22.71150357559281)",
  394. ],
  395. },
  396. {
  397. id: 12,
  398. label: "抽穗期",
  399. color: "#DD871D",
  400. imgName: sd,
  401. wktArr: [
  402. "POINT(112.65408646638161 22.84862740364581)",
  403. ],
  404. },
  405. ],
  406. },
  407. {
  408. id: 6,
  409. label: "小麦",
  410. color: "#FAA53D",
  411. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E6%97%A0%E6%A0%B8.png",
  412. imgName: xm,
  413. children: [
  414. {
  415. id: 18,
  416. label: "拔节期",
  417. color: "#FAA53D",
  418. imgName: xm,
  419. wktArr: [
  420. "POINT(112.36777193304151 22.73892833157863)",
  421. ],
  422. },
  423. ],
  424. },
  425. ],
  426. },
  427. {
  428. id: 3,
  429. label: "蔬菜",
  430. color: "#25BC07",
  431. fillColor: "rgba(0, 69, 4, 0.5)",
  432. children: [
  433. {
  434. id: 7,
  435. label: "白菜",
  436. color: "#7ABB00",
  437. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E7%99%BD%E8%8F%9C.png",
  438. imgName: bc,
  439. children: [
  440. {
  441. id: 19,
  442. label: "拔节期",
  443. color: "#7ABB00",
  444. imgName: bc,
  445. wktArr: [
  446. "POINT(110.34100329503417 21.516399336978793)",
  447. "POINT(113.89499662443995 22.653799122199416)",
  448. "POINT(113.9329988323152 22.653600638732314)",
  449. "POINT(113.94400024786592 22.614900553599)",
  450. ],
  451. },
  452. ],
  453. },
  454. {
  455. id: 8,
  456. label: "萝卜",
  457. color: "#7ABB00",
  458. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E8%90%9D%E8%8F%9C.png",
  459. imgName: lb,
  460. children: [
  461. {
  462. id: 20,
  463. label: "拔节期",
  464. color: "#7ABB00",
  465. imgName: lb,
  466. wktArr: [
  467. "POINT(110.34100329503417 21.516399336978793)",
  468. "POINT(113.89499662443995 22.653799122199416)",
  469. "POINT(113.9329988323152 22.653600638732314)",
  470. "POINT(113.94400024786592 22.614900553599)",
  471. ],
  472. },
  473. ],
  474. },
  475. ],
  476. },
  477. ];
  478. const treeActionData = ref(originTreeData);
  479. // 物候期分布下,当前激活的“二级”节点(只允许一个)
  480. const activePhenologySecondId = ref(null);
  481. // 树的默认展开与默认选中(展开/选中第一个“果类”及其子节点)
  482. const defaultExpandedKeys = ref();
  483. const defaultCheckedKeys = ref();
  484. // 顶部基础 tabs
  485. const baseTabs = ["作物分布", "物候期分布", "预警分布", "农场分布", "农服管理"];
  486. const activeBaseTab = ref("作物分布");
  487. const legendImg = ref("");
  488. const warningLayers = ref({});
  489. onMounted(() => {
  490. warningMap.initMap(store.getters.userinfo.location, mapRef.value);
  491. alarmLayer = new AlarmLayer(warningMap.kmap);
  492. staticMapLayers = new StaticMapLayers(warningMap.kmap);
  493. staticMapPointLayers = new StaticMapPointLayers(warningMap.kmap);
  494. distributionLayer = new DistributionLayer(warningMap.kmap);
  495. getSpeciesListData();
  496. // 作物分布默认选中
  497. handleDistributionTreeDefault()
  498. handleDistributionLayer();
  499. eventBus.emit("warningMap:init", warningMap.kmap);
  500. // 图例数据
  501. eventBus.on("alarmList:warningLayers", (data) => {
  502. warningLayers.value = data;
  503. });
  504. // 预警分布图层联动:仅在“预警分布”tab 显示时,才在地图上显示对应图层
  505. eventBus.on("alarmList:changeMapLayer", ({ name, legendUrl }) => {
  506. // 47 行:只在 activeBaseTab === '预警分布' 时显示预警列表
  507. // 这里保持一致:只有在该 tab 下才显示地图图层,否则直接隐藏
  508. if (activeBaseTab.value !== "预警分布") {
  509. staticMapLayers && staticMapLayers.hideAll();
  510. legendImg.value = "";
  511. return;
  512. }
  513. if (legendUrl) {
  514. legendImg.value = legendUrl;
  515. staticMapLayers && staticMapLayers.showSingle(name, true);
  516. } else {
  517. legendImg.value = warningLayers.value[`${name}图例`];
  518. let text = "";
  519. if (name === "日间温度") {
  520. text = "从化地块日温";
  521. } else if (name === "夜间温度") {
  522. text = "从化地块夜温";
  523. } else if (name === "土壤水分") {
  524. text = "从化地块水分";
  525. }
  526. if (text !== "") {
  527. staticMapLayers && staticMapLayers.showSingle(text, true);
  528. } else {
  529. staticMapLayers && staticMapLayers.hideAll();
  530. }
  531. }
  532. });
  533. // ai与地图交互
  534. eventBus.off("chat:showMapLayer", handleMapLayer);
  535. eventBus.on("chat:showMapLayer", handleMapLayer);
  536. });
  537. sessionStorage.removeItem("farmId");
  538. onUnmounted(() => {
  539. eventBus.off("alarmList:changeMapLayer");
  540. });
  541. // 作物分布默认选中并展开第一个节点,在地图上显示对应分布图层
  542. const handleDistributionLayer = () => {
  543. // 默认选中并展开第一个“果类”节点,在地图上显示对应分布图层
  544. const firstCategory = treeActionData.value[0];
  545. if (firstCategory) {
  546. const defaultNodes = [firstCategory, ...(firstCategory.children || [])];
  547. distributionLayer.initData(defaultNodes);
  548. }
  549. };
  550. // 作物分布树形结构默认展开与默认选中
  551. const handleDistributionTreeDefault = () => {
  552. defaultExpandedKeys.value = [treeActionData.value[0]?.id];
  553. defaultCheckedKeys.value = [
  554. treeActionData.value[0]?.id,
  555. ...(treeActionData.value[0]?.children?.map((c) => c.id) || []),
  556. ];
  557. };
  558. // 物候期分布默认选中并展开第一个节点,在地图上显示对应分布图层
  559. const handlePhenologyLayer = () => {
  560. const firstCategory = treeActionData.value[0].children[0];
  561. if (firstCategory) {
  562. const defaultNodes = [firstCategory, ...(firstCategory.children[0]?.children || [])];
  563. distributionLayer.initData(defaultNodes);
  564. }
  565. };
  566. // 物候期分布树形结构默认展开与默认选中
  567. const handlePhenologyTreeDefault = () => {
  568. defaultCheckedKeys.value = [
  569. treeActionData.value[0]?.children[0]?.id,
  570. ...(treeActionData.value[0]?.children[0]?.children?.map((c) => c.id) || []),
  571. ];
  572. defaultExpandedKeys.value = [treeActionData.value[0]?.children[0]?.id];
  573. };
  574. // 预警分布默认选中并展开第一个节点,在地图上显示对应分布图层
  575. const handleAlarmLayer = () => {
  576. const firstCategory = treeActionData.value[0].children[0];
  577. console.log('firstCategory', firstCategory);
  578. if (firstCategory) {
  579. const defaultNodes = [firstCategory];
  580. distributionLayer.initData(defaultNodes);
  581. }
  582. };
  583. // 预警分布树形结构默认展开与默认选中
  584. const handleAlarmTreeDefault = () => {
  585. defaultCheckedKeys.value = [
  586. treeActionData.value[0]?.children[0]?.id
  587. ];
  588. console.log('defaultCheckedKeys', defaultCheckedKeys.value);
  589. defaultExpandedKeys.value = [treeActionData.value[0]?.id];
  590. };
  591. // ai与地图交互
  592. const hideChatMapLayer = ref(true);
  593. const handleMapLayer = ({ mapName, isHome }) => {
  594. if (!isHome) {
  595. hideChatMapLayer.value = false;
  596. }
  597. staticMapPointLayers.hidePoint();
  598. staticMapLayers.hideAll();
  599. // 重置时间轴
  600. // eventBus.emit("map_click_alarm")
  601. if (mapName === "植保机") {
  602. staticMapLayers.show("分散种植", true);
  603. staticMapPointLayers.showPoint();
  604. } else if (mapName) {
  605. // staticMapLayers.show("作物种类")
  606. if (isHome) {
  607. staticMapLayers.show(mapName, true);
  608. } else {
  609. staticMapLayers.showSingle(mapName, true);
  610. }
  611. }
  612. };
  613. const toggleChatMapLayer = () => {
  614. hideChatMapLayer.value = true;
  615. eventBus.emit("chat:hideMapLayer");
  616. staticMapLayers.hideAll();
  617. };
  618. const destroyPopup = () => {
  619. eventBus.emit("map:destroyPopup");
  620. };
  621. const handleTabClick = (item) => {
  622. activeBaseTab.value = item;
  623. // 切换 Tab 时,先清空农场分布图层上的旧数据
  624. if (distributionLayer) {
  625. distributionLayer.clear();
  626. }
  627. // 所有操作前,先清空图层和选中项
  628. legendImg.value = "";
  629. staticMapLayers && staticMapLayers.hideAll();
  630. // 通知预警列表组件清空默认选中项
  631. eventBus.emit("warningHome:clearAlarm");
  632. treeActionData.value = originTreeData;
  633. // 使用 nextTick 确保树组件数据更新后再设置选中状态
  634. nextTick(() => {
  635. if (treeRef.value) {
  636. // 先清空所有选中项
  637. treeRef.value.setCheckedKeys([]);
  638. // 再设置新的选中项
  639. treeRef.value.setCheckedKeys(defaultCheckedKeys.value);
  640. }
  641. });
  642. switch (item) {
  643. case "作物分布":
  644. handleDistributionTreeDefault();
  645. handleDistributionLayer();
  646. break;
  647. case "物候期分布":
  648. treeActionData.value = phenologyData;
  649. handlePhenologyTreeDefault()
  650. handlePhenologyLayer();
  651. break;
  652. case "预警分布":
  653. handleAlarmTreeDefault()
  654. handleAlarmLayer();
  655. // 通知预警列表组件默认选中第一个(因子)项
  656. eventBus.emit("warningHome:activeFirstAlarmFactor");
  657. break;
  658. case "农场分布":
  659. const cropData = [
  660. {
  661. id: 4,
  662. label: "荔枝-物候期",
  663. color: "#2199F8",
  664. farmName: "荔博园",
  665. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E8%8D%94%E6%9E%9D.png",
  666. imgName: lz,
  667. wktArr: [
  668. "POINT(113.33722309500006 23.204074978290652)",
  669. "POINT(113.53593237057355 23.188789823486065)",
  670. "POINT(113.36970447853234 23.064596505297875)",
  671. ],
  672. },
  673. {
  674. id: 55,
  675. label: "荔枝-秋梢期",
  676. color: "#FF8E1C",
  677. farmName: "荔博园",
  678. imgUrl: "https://birdseye-img.sysuimars.com/temp/pz/%E7%99%BD%E7%B3%96.png",
  679. imgName: lz,
  680. wktArr: [
  681. "POINT(113.29900983080294 22.949956545068478)",
  682. "POINT(113.22640444442656 22.90983245840535)",
  683. ],
  684. },
  685. ];
  686. // 设施图层测试数据
  687. const facilityData = [
  688. {
  689. id: 201,
  690. label: "冷链冷库",
  691. imgName: coldChainIcon,
  692. wktArr: ["POINT(113.35 23.10)"],
  693. },
  694. {
  695. id: 202,
  696. label: "加工厂",
  697. imgName: factoryIcon,
  698. wktArr: ["POINT(113.25 23.02)"],
  699. },
  700. ];
  701. distributionLayer.initData(cropData);
  702. distributionLayer.initFacilityData(facilityData);
  703. break;
  704. default:
  705. break;
  706. }
  707. };
  708. const getSpeciesListData = () => {
  709. VE_API.species.speciesList().then((res) => {
  710. treeActionData.value = res.data;
  711. });
  712. };
  713. const props1 = {
  714. checkStrictly: true,
  715. };
  716. const toggleArea = (v) => {
  717. activeBoxName.value = null;
  718. const val = v[v.length - 1];
  719. if (val === "3" || val === "3186") {
  720. eventBus.emit("warningHome:toggleArea", val);
  721. }
  722. };
  723. const activeBoxName = ref(null);
  724. const toggleBox = (name) => {
  725. activeBoxName.value = name;
  726. legendImg.value = warningLayers.value[`${name}图例`];
  727. eventBus.emit("warningHome:toggleMapLayer", name);
  728. };
  729. // 搜索
  730. const locationVal = ref("");
  731. const loading = ref(false);
  732. const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
  733. const handleSearchRes = (v) => {
  734. warningMap.setMapCenter(v);
  735. // onRest();
  736. };
  737. const locationOptions = reactive({
  738. list: [],
  739. });
  740. const remoteMethod = async (keyword) => {
  741. if (keyword) {
  742. locationOptions.list = [];
  743. loading.value = true;
  744. const params = {
  745. key: MAP_KEY,
  746. keyword,
  747. // location: location.value,
  748. location: "22.574540836684672,113.1093017627431",
  749. };
  750. await VE_API.old_mini_map.getCtiyList({ word: keyword }).then(({ data }) => {
  751. if (data && data.length) {
  752. data.forEach((item) => {
  753. item.point = item.location.lat + "," + item.location.lng;
  754. locationOptions.list.push(item);
  755. });
  756. }
  757. });
  758. VE_API.old_mini_map.search(params).then(({ data }) => {
  759. loading.value = false;
  760. data.forEach((item) => {
  761. item.point = item.location.lat + "," + item.location.lng;
  762. locationOptions.list.push(item);
  763. });
  764. });
  765. } else {
  766. locationOptions.list = [];
  767. }
  768. };
  769. // 根据节点 id 在当前树数据中计算其层级(1/2/3)及所属的二级节点 id
  770. const getNodeLevelInfo = (id) => {
  771. const roots = treeActionData.value || [];
  772. for (const root of roots) {
  773. if (root.id === id) {
  774. return { level: 1, secondId: null };
  775. }
  776. if (root.children) {
  777. for (const second of root.children) {
  778. if (second.id === id) {
  779. return { level: 2, secondId: second.id };
  780. }
  781. if (second.children) {
  782. for (const third of second.children) {
  783. if (third.id === id) {
  784. return { level: 3, secondId: second.id };
  785. }
  786. }
  787. }
  788. }
  789. }
  790. }
  791. return { level: 0, secondId: null };
  792. };
  793. const getTreeChecks = (nodeData, data) => {
  794. const { checkedNodes } = data;
  795. let finalCheckedNodes = checkedNodes;
  796. // 物候期分布:限制“二级只能选一个,三级不限个数”
  797. if ((activeBaseTab.value === "物候期分布" || activeBaseTab.value === "预警分布") && treeRef.value) {
  798. const tree = treeRef.value;
  799. const { level, secondId } = getNodeLevelInfo(nodeData.id);
  800. if (level === 2 || level === 3) {
  801. const currentSecondId = secondId;
  802. // 判断当前这个二级分支下,是否还有被选中的节点(包含二级自己或其子级)
  803. const hasAnyCheckedInCurrentSecond = checkedNodes.some((n) => {
  804. const info = getNodeLevelInfo(n.id);
  805. return info.secondId === currentSecondId || (info.level === 2 && n.id === currentSecondId);
  806. });
  807. if (hasAnyCheckedInCurrentSecond) {
  808. // 仍有节点被选中 → 保证只有当前这个二级分支被选中,其它分支全部取消
  809. activePhenologySecondId.value = currentSecondId;
  810. const roots = treeActionData.value || [];
  811. roots.forEach((root) => {
  812. (root.children || []).forEach((second) => {
  813. if (second.id !== currentSecondId) {
  814. // 取消其它二级及其所有子级勾选
  815. tree.setChecked(second.id, false, true);
  816. } else {
  817. // 保持当前二级选中,子级按用户选择
  818. tree.setChecked(second.id, true, false);
  819. }
  820. });
  821. });
  822. } else {
  823. // 当前二级分支已经被全部取消勾选 → 清空激活记录,允许“全部不选”
  824. activePhenologySecondId.value = null;
  825. }
  826. }
  827. // 对树进行了 setChecked 操作后,重新从树组件拿一次最新的选中节点列表
  828. // 这里只需要最后一层(叶子节点 / 有 wktArr 的节点),不用父级节点
  829. const allCheckedNodes = treeRef.value.getCheckedNodes(false, true);
  830. finalCheckedNodes = allCheckedNodes.filter((n) => !n.children || n.children.length === 0 || n.wktArr);
  831. }
  832. // 任意 tab 下,最终都用当前选中的节点驱动地图渲染
  833. distributionLayer.initData(finalCheckedNodes);
  834. };
  835. </script>
  836. <style lang="scss" scoped>
  837. .base-container {
  838. width: 100%;
  839. height: 100vh;
  840. color: #fff;
  841. position: absolute;
  842. box-sizing: border-box;
  843. z-index: 1;
  844. ::v-deep {
  845. .focus-farm {
  846. top: 42px;
  847. }
  848. }
  849. .content {
  850. width: 100%;
  851. height: calc(100% - 74px - 48px);
  852. padding: 16px 20px 0 27px;
  853. display: flex;
  854. justify-content: space-between;
  855. box-sizing: border-box;
  856. position: relative;
  857. .left,
  858. .right {
  859. width: calc(376px + 54px);
  860. height: 100%;
  861. box-sizing: border-box;
  862. // display: flex;
  863. }
  864. .right {
  865. // width: 395px;
  866. width: 376px;
  867. overflow: auto;
  868. position: relative;
  869. .list {
  870. width: 100%;
  871. height: 100%;
  872. }
  873. }
  874. .chart-wrap {
  875. padding: 8px;
  876. background: #101010;
  877. border: 1px solid #444444;
  878. }
  879. .action-legend {
  880. flex: 1;
  881. padding: 0 13px;
  882. display: flex;
  883. justify-content: flex-end;
  884. align-items: baseline;
  885. ::v-deep {
  886. .el-tree {
  887. background: #232323;
  888. border: 1px solid #444444;
  889. border-radius: 5px;
  890. padding: 10px 0;
  891. --el-tree-node-content-height: 34px;
  892. --el-tree-node-hover-bg-color: rgba(255, 212, 137, 0.05);
  893. --el-tree-text-color: #ffd489;
  894. --el-tree-expand-icon-color: #ffd489;
  895. .el-checkbox {
  896. --el-checkbox-bg-color: transparent;
  897. --el-checkbox-input-border: 1px solid #ffd489;
  898. --el-checkbox-checked-input-border-color: #ffd489;
  899. --el-checkbox-checked-bg-color: #ffd489;
  900. --el-checkbox-checked-icon-color: #000;
  901. --el-checkbox-input-border-color-hover: #ffd489;
  902. }
  903. }
  904. .el-tree-node__content {
  905. padding-right: 30px;
  906. }
  907. }
  908. .custom-tree-node {
  909. display: flex;
  910. align-items: center;
  911. justify-content: space-between;
  912. gap: 8px;
  913. }
  914. .level-legend {
  915. display: flex;
  916. align-items: center;
  917. gap: 4px;
  918. padding: 0 5px;
  919. height: 17px;
  920. background: rgba(255, 255, 255, 0.1);
  921. border-radius: 2px;
  922. font-size: 10px;
  923. .legend-dot {
  924. width: 4px;
  925. height: 4px;
  926. border-radius: 50%;
  927. }
  928. }
  929. }
  930. .warning-r {
  931. .map-legend {
  932. position: absolute;
  933. bottom: -33px;
  934. left: -360px;
  935. width: 340px;
  936. img {
  937. width: 340px;
  938. opacity: 0.6;
  939. }
  940. }
  941. .chat-legend {
  942. bottom: -12px;
  943. }
  944. }
  945. .base-tabs {
  946. position: fixed;
  947. top: 32px;
  948. left: 390px;
  949. display: flex;
  950. align-items: center;
  951. .tab-item {
  952. padding: 7px 12px 9px;
  953. margin-right: 28px;
  954. text-align: center;
  955. font-family: "PangMenZhengDao";
  956. font-size: 16px;
  957. color: #fff;
  958. background: rgba(28, 36, 41, 0.8);
  959. border-radius: 4px;
  960. cursor: pointer;
  961. border: 1px solid transparent;
  962. &.active {
  963. color: #ffdf9a;
  964. background: rgba(19, 22, 16, 0.8);
  965. border: 1px solid #ffd489;
  966. }
  967. }
  968. }
  969. .warning-search {
  970. position: fixed;
  971. right: 207px;
  972. top: 28px;
  973. display: flex;
  974. align-items: center;
  975. .focus-farm {
  976. padding-left: 15px;
  977. }
  978. ::v-deep {
  979. .el-select__wrapper {
  980. background: #1d1d1d;
  981. box-shadow: 0 0 0 1px rgba(255, 212, 137, 0.3) inset;
  982. height: 50px;
  983. line-height: 50px;
  984. .el-select__caret,
  985. .el-select__prefix {
  986. color: rgba(255, 212, 137, 0.6);
  987. }
  988. }
  989. .el-select__input {
  990. color: rgba(255, 212, 137, 0.6);
  991. }
  992. .el-select__placeholder {
  993. color: rgba(255, 212, 137, 0.6);
  994. font-size: 20px;
  995. font-family: "PangMenZhengDao";
  996. // text-align: center;
  997. }
  998. }
  999. }
  1000. .warning-top {
  1001. display: flex;
  1002. width: max-content;
  1003. align-items: center;
  1004. .top-l {
  1005. display: flex;
  1006. flex-direction: column;
  1007. align-items: center;
  1008. .type-box {
  1009. margin-top: 10px;
  1010. background: rgba(29, 29, 29, 0.54);
  1011. border: 1px solid rgba(255, 212, 137, 0.3);
  1012. border-radius: 2px;
  1013. text-align: center;
  1014. line-height: 48px;
  1015. height: 48px;
  1016. width: 184px;
  1017. }
  1018. ::v-deep {
  1019. .el-input__wrapper {
  1020. background: rgba(29, 29, 29, 0.54);
  1021. box-shadow: 0 0 0 1px rgba(255, 212, 137, 0.3) inset;
  1022. height: 50px;
  1023. line-height: 50px;
  1024. padding: 0 10px;
  1025. .el-input__inner {
  1026. color: #f7be5a;
  1027. font-size: 20px;
  1028. font-family: "PangMenZhengDao";
  1029. text-align: center;
  1030. }
  1031. }
  1032. .el-select__wrapper {
  1033. background: rgba(29, 29, 29, 0.54);
  1034. box-shadow: 0 0 0 1px rgba(255, 212, 137, 0.3) inset;
  1035. height: 50px;
  1036. line-height: 50px;
  1037. .el-select__caret {
  1038. color: #ffd489;
  1039. }
  1040. }
  1041. .el-select__placeholder {
  1042. color: #f7be5a;
  1043. font-size: 20px;
  1044. font-family: "PangMenZhengDao";
  1045. text-align: center;
  1046. }
  1047. }
  1048. }
  1049. .top-r {
  1050. display: flex;
  1051. .data-box {
  1052. cursor: pointer;
  1053. margin-left: 20px;
  1054. width: 200px;
  1055. height: 104px;
  1056. background: url("@/assets/images/warningHome/box-bg.png") no-repeat center center / 100% 100%;
  1057. display: flex;
  1058. flex-direction: column;
  1059. align-items: center;
  1060. &.active {
  1061. position: relative;
  1062. &::before {
  1063. content: "";
  1064. position: absolute;
  1065. bottom: -26px;
  1066. left: 0;
  1067. right: 0;
  1068. width: 35px;
  1069. height: 17px;
  1070. margin: 0 auto;
  1071. background: url("@/assets/images/warningHome/triangle.png") no-repeat center center / cover;
  1072. }
  1073. }
  1074. .data-value {
  1075. padding-top: 15px;
  1076. font-size: 20px;
  1077. color: rgba(255, 212, 137, 0.4);
  1078. font-family: "PangMenZhengDao";
  1079. span {
  1080. font-size: 38px;
  1081. color: #f7be5a;
  1082. padding-right: 2px;
  1083. }
  1084. }
  1085. .data-name {
  1086. color: #cecece;
  1087. font-size: 16px;
  1088. }
  1089. }
  1090. }
  1091. }
  1092. .warning-alarm {
  1093. width: 88px;
  1094. padding-top: 14px;
  1095. }
  1096. .time-wrap {
  1097. position: fixed;
  1098. bottom: 20px;
  1099. left: 20px;
  1100. width: 1080px;
  1101. height: 71px;
  1102. }
  1103. }
  1104. }
  1105. .bottom-map {
  1106. width: 100%;
  1107. height: 100vh;
  1108. position: absolute;
  1109. z-index: 0;
  1110. }
  1111. </style>
  1112. <style lang="less">
  1113. .ol-scale-line {
  1114. left: auto;
  1115. right: 435px;
  1116. bottom: 13px;
  1117. .ol-scale-line-inner {
  1118. max-width: 80px;
  1119. width: 80px !important;
  1120. color: #fff;
  1121. border-color: #fff;
  1122. }
  1123. }
  1124. .focus-farm-select {
  1125. &.el-popper.is-light {
  1126. background: #232323;
  1127. border-color: rgba(255, 212, 137, 0.3);
  1128. box-shadow: 0px 0px 12px rgba(255, 212, 137, 0.3);
  1129. .el-select-dropdown__item {
  1130. background: none;
  1131. color: rgba(255, 212, 137, 0.6);
  1132. }
  1133. .el-select-dropdown__item.is-selected {
  1134. background: rgba(255, 212, 137, 0.2);
  1135. color: #ffd489;
  1136. }
  1137. }
  1138. &.el-popper.is-light .el-popper__arrow:before {
  1139. background: #232323;
  1140. border-color: rgba(255, 212, 137, 0.3);
  1141. }
  1142. }
  1143. .ol-popup-warning {
  1144. position: relative;
  1145. width: 295px;
  1146. background: rgb(35, 35, 35, 0.86);
  1147. color: #fff;
  1148. font-size: 16px;
  1149. border-radius: 4px;
  1150. .warning-info-title {
  1151. display: flex;
  1152. padding: 6px 10px;
  1153. background: rgba(255, 255, 255, 0.05);
  1154. font-size: 18px;
  1155. border-radius: 4px 4px 0 0;
  1156. .icon {
  1157. padding-right: 6px;
  1158. }
  1159. .close {
  1160. position: absolute;
  1161. right: 12px;
  1162. top: 4px;
  1163. }
  1164. }
  1165. .info-content {
  1166. padding: 16px 20px 40px 20px;
  1167. line-height: 26px;
  1168. text-indent: 2em;
  1169. }
  1170. }
  1171. .area-cascader {
  1172. &.el-popper.is-light {
  1173. background: #232323;
  1174. border-color: rgba(255, 212, 137, 0.3);
  1175. box-shadow: 0px 0px 12px rgba(255, 212, 137, 0.3);
  1176. .el-cascader-menu {
  1177. color: rgba(255, 212, 137, 0.6);
  1178. border-color: rgba(255, 212, 137, 0.3);
  1179. }
  1180. .el-cascader-node.in-active-path,
  1181. .el-cascader-node.is-active,
  1182. .el-cascader-node.is-selectable.in-checked-path {
  1183. color: #f7be5a;
  1184. background: transparent;
  1185. }
  1186. .el-radio__input.is-checked .el-radio__inner {
  1187. background: #f7be5a;
  1188. border-color: #f7be5a;
  1189. }
  1190. .el-cascader-node:not(.is-disabled):hover,
  1191. .el-cascader-node:not(.is-disabled):focus,
  1192. .el-cascader-node:not(.is-disabled):hover {
  1193. background: rgba(255, 212, 137, 0.2);
  1194. }
  1195. }
  1196. .el-radio__inner {
  1197. background-color: rgba(255, 212, 137, 0.3);
  1198. border-color: rgba(255, 212, 137, 0.6);
  1199. }
  1200. &.el-popper.is-light .el-popper__arrow:before {
  1201. background: #232323;
  1202. border-color: rgba(255, 212, 137, 0.3);
  1203. }
  1204. }
  1205. </style>