index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. <template>
  2. <div class="map-container">
  3. <div ref="mapRoot" class="map"></div>
  4. <!-- 以下为悬浮信息等 -->
  5. <div
  6. v-if="hoverInfo"
  7. :style="hoverStyle"
  8. class="hover-info"
  9. @mouseenter="hovering = true"
  10. @mouseleave="onHoverLeave"
  11. >
  12. <h3>{{ hoverInfo.fieldName }}</h3>
  13. <div>
  14. <!-- <p>
  15. <span>状态:</span
  16. ><span>{{
  17. hoverInfo.analysisState === 30 ? "已完成分析" : "未完成分析"
  18. }}</span>
  19. </p> -->
  20. <p>
  21. <span>风场编号</span><span>{{ hoverInfo.codeNumber }}</span>
  22. </p>
  23. <p>
  24. <span>风机数量</span><span>{{ newWind.engineTotalCount }} 台</span>
  25. </p>
  26. <p>
  27. <span>总容量</span
  28. ><span
  29. >{{
  30. newWind?.ratedCapacityNumber ? newWind.ratedCapacityNumber : 0
  31. }}
  32. MW</span
  33. >
  34. </p>
  35. <p>
  36. <span>经度</span><span>{{ hoverInfo.longitude }}°</span>
  37. </p>
  38. <p>
  39. <span>纬度</span><span>{{ hoverInfo.latitude }}°</span>
  40. </p>
  41. <!-- <p>
  42. <span>分析模型数</span
  43. ><span
  44. >{{
  45. hoverInfo.analysisTypeCount ? hoverInfo.analysisTypeCount : 0
  46. }}个</span
  47. >
  48. </p>
  49. <p>
  50. <span>分析开始时间</span>
  51. <span>{{
  52. newWind.analysisStartTime
  53. ? $formatDateTWO(newWind.analysisStartTime)
  54. : "暂无数据"
  55. }}</span>
  56. </p>
  57. <p>
  58. <span>分析结束时间</span>
  59. <span>{{
  60. newWind.analysisEndTime
  61. ? $formatDateTWO(newWind.analysisEndTime)
  62. : "暂无数据"
  63. }}</span>
  64. </p>
  65. <p>
  66. <span>数据开始时间</span>
  67. <span>{{
  68. newWind.dataStartTime
  69. ? $formatDateTWO(newWind.dataStartTime)
  70. : "暂无数据"
  71. }}</span>
  72. </p>
  73. <p>
  74. <span>数据结束时间</span>
  75. <span>{{
  76. newWind.dataEndTime
  77. ? $formatDateTWO(newWind.dataEndTime)
  78. : "暂无数据"
  79. }}</span>
  80. </p> -->
  81. </div>
  82. </div>
  83. <div
  84. v-if="hoverfengji"
  85. :style="hoverfengjiStyle"
  86. class="hover-fengji"
  87. @mouseenter="hoveringFengji = true"
  88. @mouseleave="onFengjiLeave"
  89. >
  90. <h3>{{ hoverfengji.engineName }}</h3>
  91. <div>
  92. <p>
  93. <span>风机编号</span><span>{{ hoverfengji.engineCode }}</span>
  94. </p>
  95. <p>
  96. <span>额定容量</span
  97. ><span
  98. >{{
  99. hoverfengji?.ratedCapacity ? hoverfengji?.ratedCapacity : 0
  100. }}kW</span
  101. >
  102. </p>
  103. <p>
  104. <span>海拔高度</span
  105. ><span>{{ hoverfengji.elevationHeight }} /米</span>
  106. </p>
  107. <p>
  108. <span>轮毂高度</span><span>{{ hoverfengji.hubHeight }} /米</span>
  109. </p>
  110. <p>
  111. <span>经度</span><span>{{ hoverfengji.longitude }}</span>
  112. </p>
  113. <p>
  114. <span>维度</span><span>{{ hoverfengji.latitude }}</span>
  115. </p>
  116. <p>
  117. 是否标杆风机
  118. <span>{{ hoverfengji.sightcing == 1 ? "是" : "否" }}</span>
  119. </p>
  120. <p>
  121. <span>额定风速</span><span>{{ hoverfengji.ratedWindSpeed }} m/s</span>
  122. </p>
  123. <p>
  124. <span>切入风速</span
  125. ><span>{{ hoverfengji.ratedCutInWindspeed }} m/s</span>
  126. </p>
  127. <p>
  128. <span>切出风速</span
  129. ><span>{{ hoverfengji.ratedCutOutWindspeed }} m/s</span>
  130. </p>
  131. </div>
  132. </div>
  133. <div
  134. v-if="hoverta"
  135. :style="hovertaStyle"
  136. class="hover-ta"
  137. @mouseenter="hoveringTa = true"
  138. @mouseleave="onTaLeave"
  139. >
  140. <h3>{{ hoverta.anemometerName }}</h3>
  141. <div>
  142. <p>
  143. <span>测风塔编号</span><span>{{ hoverta.anemometerCode }}</span>
  144. </p>
  145. <p>
  146. <span>经度</span><span>{{ hoverta.longitude }}</span>
  147. </p>
  148. <p>
  149. <span>纬度</span><span>{{ hoverta.latitude }}</span>
  150. </p>
  151. <p>
  152. <span>测风塔高度</span
  153. ><span>{{ hoverta.anemometerHeightStrings }}/米</span>
  154. </p>
  155. </div>
  156. </div>
  157. <!-- 异常描述弹窗 -->
  158. <el-dialog title="异常描述" :visible.sync="dialogVisible" width="50%">
  159. <el-table :data="tableData" max-height="500" style="width: 100%">
  160. <el-table-column prop="analysisTypeName" label="类型" width="300">
  161. </el-table-column>
  162. <el-table-column prop="errDesc" label="描述"> </el-table-column>
  163. </el-table>
  164. </el-dialog>
  165. </div>
  166. </template>
  167. <script>
  168. import "ol/ol.css";
  169. import { Map, View, Feature } from "ol";
  170. import TileLayer from "ol/layer/Tile.js";
  171. import { XYZ } from "ol/source";
  172. import { fromLonLat } from "ol/proj";
  173. import { Vector } from "ol/source";
  174. import { Vector as VectorLayer } from "ol/layer";
  175. import { Point } from "ol/geom";
  176. import { Icon, Style, Stroke, Fill } from "ol/style";
  177. import ZoomSlider from "ol/control/ZoomSlider.js";
  178. import { defaults as defaultControls } from "ol/control.js";
  179. import GeoJSON from "ol/format/GeoJSON";
  180. // 假设你已经有正确的山西省边界数据文件(GeoJSON 格式)
  181. // import shanxiBoundary from "./shanxi.json";
  182. import shanxiBoundary from "./jilin.json";
  183. // import icon01 from "../../assets/img/iconFC.png";
  184. // import icon02 from "../../assets/img/iconFC.png";
  185. // import icon03 from "../../assets/img/iconFC.png";
  186. import icon01 from "../../assets/img/iconFJ.png";
  187. import icon02 from "../../assets/img/iconFJ.png";
  188. import icon03 from "../../assets/img/iconFJ.png";
  189. import icon04 from "../../assets/img/iconFJ.png";
  190. import icon05 from "../../assets/img/icon05.png";
  191. import icon06 from "../../assets/img/iconFJ.png";
  192. import defaultIcon from "../../assets/img/iconFJ.png";
  193. // import icon01 from "../../assets/img/icon01.png";
  194. // import icon02 from "../../assets/img/icon02.png";
  195. // import icon03 from "../../assets/img/icon03.png";
  196. // import icon04 from "../../assets/img/icon04.png";
  197. // import icon05 from "../../assets/img/icon05.png";
  198. // import icon06 from "../../assets/img/iconFJ.png";
  199. // import defaultIcon from "../../assets/img/iconFJ.png";
  200. import { queryErrDescByEngine, queryFloatingWindowInfo } from "@/api/ledger.js";
  201. export default {
  202. props: {
  203. windEngineGroupByFieldCodeDetail: {
  204. type: Object,
  205. default: () => ({}),
  206. },
  207. },
  208. name: "T-map",
  209. data() {
  210. return {
  211. map: null,
  212. dialogVisible: false,
  213. tableData: [],
  214. hoverInfo: null,
  215. hovering: false,
  216. hideTimer: null,
  217. hoverStyle: {
  218. position: "absolute",
  219. left: "0px",
  220. top: "0px",
  221. },
  222. hoverfengji: null,
  223. hoverfengjiStyle: {
  224. position: "absolute",
  225. left: "0px",
  226. top: "0px",
  227. },
  228. hoverta: null,
  229. hovertaStyle: {
  230. position: "absolute",
  231. left: "0px",
  232. top: "0px",
  233. },
  234. hoveringFengji: false,
  235. hoveringTa: false,
  236. fengjiTimer: null,
  237. taTimer: null,
  238. newWind: {},
  239. };
  240. },
  241. mounted() {
  242. this.map = new Map({
  243. target: this.$refs.mapRoot,
  244. view: new View({
  245. projection: "EPSG:4326",
  246. center: fromLonLat([116.389, 39.903]),
  247. zoom: 3,
  248. minZoom: 3,
  249. maxZoom: 15,
  250. extent: [69, 18, 136, 54],
  251. }),
  252. layers: [
  253. new TileLayer({
  254. source: new XYZ({
  255. url: process.env.VUE_APP_MAPVIEW,
  256. maxZoom: 12,
  257. }),
  258. }),
  259. // ...(process.env.VUE_APP_MAPVIEW_ADMIN
  260. // ? [
  261. // new TileLayer({
  262. // source: new XYZ({
  263. // url: process.env.VUE_APP_MAPVIEW_ADMIN,
  264. // maxZoom: 12,
  265. // }),
  266. // }),
  267. // ]
  268. // : []),
  269. new VectorLayer({
  270. id: "marker",
  271. source: new Vector(),
  272. }),
  273. ],
  274. controls: defaultControls().extend([new ZoomSlider()]),
  275. });
  276. // -------------【添加山西省真实边界线】------------- 定位全国的时候注释这个代码
  277. // if (this.$route.path === "/home/cockpitManage") {
  278. // // 通过导入的 JSON 文件加载边界数据(GeoJSON 格式),并解析成矢量要素
  279. // const shanxiSource = new Vector({
  280. // features: new GeoJSON().readFeatures(shanxiBoundary, {
  281. // featureProjection: "EPSG:4326",
  282. // }),
  283. // });
  284. // // 创建矢量图层,仅显示边界线(填充颜色设为透明)
  285. // const shanxiLayer = new VectorLayer({
  286. // source: shanxiSource,
  287. // style: new Style({
  288. // stroke: new Stroke({
  289. // color: "rgba(59, 130, 246, 0.5)",
  290. // width: 3,
  291. // }),
  292. // fill: new Fill({
  293. // color: "rgba(59, 130, 246, 0.05)", // 透明填充
  294. // }),
  295. // }),
  296. // });
  297. // this.map.addLayer(shanxiLayer);
  298. // const markerLayer = this.map
  299. // .getLayers()
  300. // .getArray()
  301. // .find((layer) => {
  302. // return layer.get("id") === "marker";
  303. // });
  304. // if (markerLayer) {
  305. // markerLayer.setZIndex(10);
  306. // }
  307. // // 同时设置高亮图层 zIndex 较低
  308. // shanxiLayer.setZIndex(1);
  309. // // -------------【结束】-------------
  310. // // const targetExtent = [106.8, 34.3, 118.6, 41.2];//山西
  311. // const targetExtent = [119.65, 40.87, 131.31, 46.30];//吉林
  312. // this.map.getView().fit(targetExtent, { duration: 2000 });
  313. // }
  314. // 这个放在外面
  315. this.initEvent();
  316. this.$nextTick(() => {
  317. if (this.map) {
  318. this.map.updateSize();
  319. }
  320. });
  321. },
  322. activated() {
  323. if (!this.map) return;
  324. this.map.setTarget(this.$refs.mapRoot || null);
  325. this.$nextTick(() => {
  326. this.map && this.map.updateSize();
  327. });
  328. },
  329. deactivated() {
  330. if (this.map) {
  331. this.map.setTarget(null);
  332. }
  333. },
  334. beforeDestroy() {
  335. if (this.map) {
  336. this.map.setTarget(null);
  337. }
  338. },
  339. methods: {
  340. onHoverLeave() {
  341. this.hovering = false;
  342. clearTimeout(this.hideTimer);
  343. this.hideTimer = setTimeout(() => {
  344. if (!this.hovering) {
  345. this.hoverInfo = null;
  346. }
  347. }, 100); // 这里延迟100ms防止意外移出
  348. },
  349. onFengjiLeave() {
  350. this.hoveringFengji = false;
  351. clearTimeout(this.fengjiTimer);
  352. this.fengjiTimer = setTimeout(() => {
  353. if (!this.hoveringFengji) {
  354. this.hoverfengji = null;
  355. }
  356. }, 100); // 你可以根据实际需要调大一点时间
  357. },
  358. onTaLeave() {
  359. this.hoveringTa = false;
  360. clearTimeout(this.taTimer);
  361. this.taTimer = setTimeout(() => {
  362. if (!this.hoveringTa) {
  363. this.hoverta = null;
  364. }
  365. }, 100);
  366. },
  367. addMarker(data = { point: [120.2, 30.35], val: "1" }) {
  368. const layer = this.map
  369. .getLayers()
  370. .getArray()
  371. .find((element) => {
  372. return element.get("id") === "marker";
  373. });
  374. const source = layer.getSource();
  375. const iconSrc = this.getIconForValue(data.val);
  376. const scale = data.val === "4" ? [0.5, 0.5] : [0.3, 0.3];
  377. const feature = new Feature({
  378. geometry: new Point(fromLonLat(data.point, "EPSG:4326")),
  379. name: "marker",
  380. data,
  381. });
  382. feature.setStyle(
  383. new Style({
  384. image: new Icon({
  385. src: iconSrc,
  386. scale: scale,
  387. anchor: [0.5, 1],
  388. opacity: 1,
  389. }),
  390. }),
  391. );
  392. source.addFeature(feature);
  393. },
  394. clearMarkers() {
  395. const layer = this.map
  396. .getLayers()
  397. .getArray()
  398. .find((element) => {
  399. return element.get("id") === "marker";
  400. });
  401. if (layer) {
  402. const source = layer.getSource();
  403. source.clear();
  404. }
  405. },
  406. getIconForValue(val) {
  407. switch (val) {
  408. case "-1":
  409. case -1:
  410. return icon01;
  411. case "10":
  412. case 10:
  413. return icon01;
  414. case 0:
  415. case "0":
  416. return icon02;
  417. case 20:
  418. case "20":
  419. return icon02;
  420. case 1:
  421. case "1":
  422. return icon03;
  423. case 30:
  424. case "30":
  425. return icon03;
  426. case "4":
  427. return icon04;
  428. case "5":
  429. return icon05;
  430. case "6":
  431. return icon06;
  432. default:
  433. return defaultIcon;
  434. }
  435. },
  436. initEvent() {
  437. let lastHoveredFeature = null;
  438. const calculateHoverTop = (mouseY, hoverHeight) => {
  439. const componentHeight = this.$el.clientHeight;
  440. const offset = 10;
  441. if (mouseY + hoverHeight + offset > componentHeight) {
  442. return mouseY - hoverHeight - offset;
  443. }
  444. return mouseY - hoverHeight / 2;
  445. };
  446. const calculateHoverLeft = (mouseX, hoverWidth) => {
  447. const componentWidth = this.$el.clientWidth;
  448. const offset = 10;
  449. if (mouseX + hoverWidth + offset > componentWidth) {
  450. return mouseX - hoverWidth - offset;
  451. }
  452. if (mouseX - hoverWidth - offset < 0) {
  453. return offset;
  454. }
  455. return mouseX + offset;
  456. };
  457. this.map.on("pointermove", (evt) => {
  458. const features = this.map.getFeaturesAtPixel(evt.pixel, {
  459. hitTolerance: 1,
  460. });
  461. if (features && features.length > 0) {
  462. const feature = features.at(0);
  463. const data = feature.get("data");
  464. // 如果没有 data 属性,则不处理,但允许事件冒泡
  465. if (!data) {
  466. return;
  467. }
  468. const val = data.val;
  469. // 如果当前悬停的 feature 与上一次不同,则处理
  470. if (lastHoveredFeature !== feature) {
  471. const hoverHeight = 250;
  472. const hoverWidth = 200;
  473. const topPosition = calculateHoverTop(evt.pixel[1], hoverHeight);
  474. const leftPosition = calculateHoverLeft(evt.pixel[0], hoverWidth);
  475. if (val == "1" || val == "30" || val == "-1" || val == "10") {
  476. this.hoverInfo = data;
  477. this.hoverStyle.left = `${leftPosition}px`;
  478. this.hoverStyle.top = `${topPosition}px`;
  479. this.getwind();
  480. } else if (val == "4") {
  481. this.hoverfengji = data;
  482. this.hoverfengjiStyle.left = `${leftPosition}px`;
  483. this.hoverfengjiStyle.top = `${topPosition}px`;
  484. this.currentFeatureData = data;
  485. } else if (val == "5") {
  486. this.hoverta = data;
  487. this.hovertaStyle.left = `${leftPosition}px`;
  488. this.hovertaStyle.top = `${topPosition}px`;
  489. this.currentFeatureData = data;
  490. } else if (val == "6") {
  491. this.currentFeatureData = data;
  492. } else {
  493. this.hoverInfo = false;
  494. this.hoverfengji = false;
  495. this.hoverta = false;
  496. this.currentFeatureData = false;
  497. }
  498. lastHoveredFeature = feature;
  499. }
  500. } else {
  501. // 没有特征时,清空所有悬浮信息
  502. if (!this.hoveringInfo) this.hoverInfo = null;
  503. if (!this.hoveringFengji) this.hoverfengji = null;
  504. if (!this.hoveringTa) this.hoverta = null;
  505. this.currentFeatureData = null;
  506. lastHoveredFeature = null;
  507. }
  508. });
  509. // click 事件处理
  510. this.map.on("click", (evt) => {
  511. const features = this.map.getFeaturesAtPixel(evt.pixel, {
  512. hitTolerance: 1,
  513. });
  514. if (features && features.length > 0) {
  515. const feature = features.at(0);
  516. const data = feature.get("data");
  517. // 如果没有 data,直接返回,让事件继续冒泡
  518. if (!data) {
  519. return;
  520. }
  521. const val = data.val;
  522. if (val === "6") {
  523. this.handleFeatureClick(data);
  524. } else {
  525. this.$emit("feature-click", data);
  526. }
  527. } else if (
  528. this.currentFeatureData &&
  529. this.currentFeatureData.val === "6"
  530. ) {
  531. this.handleFeatureClick(this.currentFeatureData);
  532. }
  533. });
  534. },
  535. handleFeatureClick(featureData) {
  536. let dateArr = {
  537. batchCode: this.$route.query.batchCode,
  538. engineCode: featureData.engineCode,
  539. };
  540. queryErrDescByEngine(dateArr).then((res) => {
  541. this.dialogVisible = true;
  542. this.tableData = res.data;
  543. });
  544. },
  545. getwind() {
  546. // if (this.hoverInfo && this.hoverInfo.batchCode) {
  547. const param = {
  548. batchcode: this.hoverInfo.batchCode,
  549. fieldCode: this.hoverInfo.codeNumber,
  550. };
  551. queryFloatingWindowInfo(param)
  552. .then((res) => {
  553. this.newWind = res.data;
  554. })
  555. .catch((err) => {
  556. console.error("获取风信息失败", err);
  557. });
  558. // } else {
  559. // console.warn("hoverInfo 或 batchCode 未定义");
  560. // }
  561. },
  562. moveAndZoom(data = { point: [120.2, 30.35], zoom: 15 }) {
  563. this.map.getView().animate({
  564. center: fromLonLat(data.point, "EPSG:4326"),
  565. zoom: data.zoom,
  566. duration: 2000,
  567. });
  568. },
  569. },
  570. };
  571. </script>
  572. <style scoped lang="scss">
  573. .map-container {
  574. position: relative;
  575. width: 100%;
  576. height: 100%;
  577. overflow: hidden;
  578. }
  579. .map {
  580. width: 100%;
  581. height: 100%;
  582. }
  583. .hover-info {
  584. position: absolute;
  585. color: var(--map-hover-text, #303133);
  586. padding: 0;
  587. z-index: 100;
  588. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  589. border-radius: 4px;
  590. overflow: hidden;
  591. h3 {
  592. background-color: var(--map-hover-header-bg, var(--primary-color, #008080));
  593. color: var(--map-hover-header-text, #ffffff);
  594. width: 240px;
  595. padding: 5px 10px;
  596. font-size: 16px;
  597. margin: 0;
  598. }
  599. div {
  600. background-color: var(--map-hover-body-bg, rgba(255, 255, 255, 0.96));
  601. color: var(--map-hover-text, #303133);
  602. width: 240px;
  603. font-size: 12px;
  604. p {
  605. padding: 5px 10px;
  606. display: flex;
  607. justify-content: space-between;
  608. margin: 0;
  609. span:first-child {
  610. color: var(--map-hover-label-text, #606266);
  611. }
  612. }
  613. }
  614. }
  615. .hover-fengji {
  616. position: absolute;
  617. color: var(--map-hover-text, #303133);
  618. padding: 0;
  619. z-index: 100;
  620. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  621. border-radius: 4px;
  622. overflow: hidden;
  623. h3 {
  624. background-color: var(--map-hover-header-bg, var(--primary-color, #008080));
  625. color: var(--map-hover-header-text, #ffffff);
  626. width: 240px;
  627. padding: 5px 10px;
  628. font-size: 16px;
  629. margin: 0;
  630. }
  631. div {
  632. background-color: var(--map-hover-body-bg, rgba(255, 255, 255, 0.96));
  633. color: var(--map-hover-text, #303133);
  634. width: 240px;
  635. font-size: 12px;
  636. p {
  637. padding: 5px 10px;
  638. display: flex;
  639. justify-content: space-between;
  640. margin: 0;
  641. span:first-child {
  642. color: var(--map-hover-label-text, #606266);
  643. }
  644. }
  645. }
  646. }
  647. .hover-ta {
  648. position: absolute;
  649. color: white;
  650. padding: 0;
  651. z-index: 100;
  652. h3 {
  653. background-color: #d90019b2;
  654. width: 240px;
  655. padding: 5px 10px;
  656. font-size: 16px;
  657. }
  658. div {
  659. background-color: #d9001994;
  660. width: 240px;
  661. font-size: 12px;
  662. p {
  663. padding: 5px 10px;
  664. display: flex;
  665. justify-content: space-between;
  666. }
  667. }
  668. }
  669. </style>