index.vue 46 KB

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