index.vue.bak 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. <template>
  2. <div class="map-index">
  3. <div id="mapChart"></div>
  4. <div class="map-left">
  5. <div class="left-title">模型列表</div>
  6. <div class="left-tree">
  7. <el-tree
  8. :data="data"
  9. :props="defaultProps"
  10. show-checkbox
  11. node-key="id"
  12. :default-expanded-keys="[1, 8]"
  13. :default-checked-keys="[1]"
  14. @node-click="handleNodeClick"
  15. @check-change="handleCheckChange"
  16. />
  17. </div>
  18. </div>
  19. <div class="map-right">
  20. <div class="right-title">模型数据展示</div>
  21. <div class="right-top-title">
  22. <img src="@/assets/map/img/站点.png"/>
  23. <span>预报站点信息</span>
  24. </div>
  25. <div class="station-table">
  26. <el-scrollbar style="height: 100%;">
  27. <table>
  28. <thead>
  29. <tr>
  30. <td class="table-index">序号</td>
  31. <td class="table-head">站码</td>
  32. <td class="table-head">站名</td>
  33. <td class="table-head">实时潮位</td>
  34. <td class="table-head">发生时间</td>
  35. <td class="table-head">警戒潮位</td>
  36. <td class="table-head">距离警戒</td>
  37. <td class="table-head">预报潮位</td>
  38. <td class="table-head">发生时间</td>
  39. <td class="table-head">距离警戒</td>
  40. </tr>
  41. </thead>
  42. <tbody>
  43. <tr
  44. v-for="(item,index) in StnmData.data"
  45. :key="index"
  46. >
  47. <td class="table-index">{{index+1}}</td>
  48. <td class="table-tbody">{{item.stationName}}</td>
  49. <td class="table-tbody">{{ item.stationCode}}</td>
  50. <td class="table-tbody"></td>
  51. <td class="table-tbody"></td>
  52. <td class="table-tbody"></td>
  53. <td class="table-tbody"></td>
  54. <td class="table-tbody"></td>
  55. <td class="table-tbody"></td>
  56. <td class="table-tbody"></td>
  57. </tr>
  58. </tbody>
  59. </table>
  60. </el-scrollbar>
  61. </div>
  62. </div>
  63. <!-- 可拖拽图标组 -->
  64. <div
  65. class="map-fcicon"
  66. :style="{ left: fcPosition.x + 'px', top: fcPosition.y + 'px' }"
  67. v-show="showIcon"
  68. @mousedown="startDrag"
  69. ></div>
  70. <div
  71. class="dialog"
  72. v-if="showDialog"
  73. :style="{ left: dialogPosition.x + 'px', top: dialogPosition.y + 'px' }"
  74. >
  75. <div class="dialog-header" @mousedown="startDialogDrag">
  76. 风场
  77. <button class="close-btn" @click="closeDialog">×</button>
  78. </div>
  79. <div class="dialog-body"></div>
  80. </div>
  81. </div>
  82. </template>
  83. <script setup>
  84. import 'ol/css';
  85. import {ScaleLine, defaults as defaultControls} from 'ol/control';
  86. import Map from 'ol/Map';
  87. import View from 'ol/View';
  88. import TileLayer from "ol/layer/Tile";
  89. import { XYZ, Vector as VectorSource } from 'ol/source.js';
  90. import VectorLayer from "ol/layer/Vector";
  91. import { LineString, Point } from "ol/geom";
  92. import { Icon, Style, Text,Circle } from 'ol/style';
  93. import Fill from "ol/style/Fill";
  94. import Feature from 'ol/Feature';
  95. import Stroke from "ol/style/Stroke";
  96. import Overlay from 'ol/Overlay';
  97. import StnmData from "@/assets/map/json/stnmData.json";
  98. import red_trangle from "@/assets/map/img/Ⅳ.png";
  99. const mapChart = ref(null);
  100. const mapCenter = ref([121.472644, 31.231706]);
  101. const mapZoom = ref(9);
  102. const data = [
  103. {
  104. id:1,
  105. label: '上海市城区洪涝仿真模型',
  106. children: [
  107. {
  108. id:8,
  109. label: '上海市城区洪涝仿真模型-1',
  110. },
  111. ],
  112. },
  113. {
  114. id:2,
  115. label: '上海沿海风暴潮预报模型',
  116. },
  117. {
  118. id:3,
  119. label: '黄浦江水系水文分析预报数值模拟',
  120. },
  121. {
  122. id:4,
  123. label: '苏州河水系水情预报模型',
  124. },
  125. {
  126. id:5,
  127. label: '内涝风险实时预警与预报',
  128. },
  129. {
  130. id:6,
  131. label: '上海市中心城区排水系统模型',
  132. },
  133. {
  134. id:7,
  135. label: '温带风暴潮预报模型',
  136. },
  137. ]
  138. const defaultProps = {
  139. children: 'children',
  140. label: 'label',
  141. }
  142. const stnmVectorLayer = ref(null);
  143. const popupOverlays = ref([]);// 存储弹窗的引用
  144. const showIcon = ref(true);
  145. const showDialog = ref(false);
  146. const isDragging = ref(false);
  147. const fcPosition = ref({ x: 280, y: 40 });
  148. const dialogPosition = ref({ x: 0, y: 0 });
  149. const dragStartPosition = ref({ x: 0, y: 0 });
  150. const isDraggingDialog = ref(false);
  151. const dialogDragStart = ref({ x: 0, y: 0 });
  152. onMounted(() => {
  153. initMap();
  154. console.log('StnmData',StnmData);
  155. });
  156. const initMap = () => {
  157. let vecLayer = new TileLayer({
  158. source: new XYZ({
  159. url: "http://t0.tianditu.gov.cn/vec_w/wmts?" +
  160. "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
  161. "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" +
  162. "&tk=9bb941214f10fbf9a3eab43f45cb2b7e",
  163. }),
  164. });
  165. let cvaLayer = new TileLayer({
  166. source: new XYZ({
  167. url: "http://t0.tianditu.gov.cn/cva_w/wmts?" +
  168. "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
  169. "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" +
  170. "&tk=9bb941214f10fbf9a3eab43f45cb2b7e",
  171. }),
  172. });
  173. mapChart.value= new Map({
  174. target: 'mapChart',
  175. view: new View({
  176. center: mapCenter.value,
  177. zoom: mapZoom.value,
  178. minZoom: 3,
  179. maxZoom: 16,
  180. projection: 'EPSG:4326',
  181. }),
  182. layers: [vecLayer, cvaLayer],
  183. controls: defaultControls({
  184. zoom: false,//不显示放大放小按钮
  185. rotate: false,//不显示指北针控件
  186. attribution: false,//不显示右下角的地图信息控件
  187. scaleLine:false,//不显示比例尺控件
  188. })
  189. });
  190. addStnm();
  191. };
  192. const addStnm = () => {
  193. let features = [];
  194. let stationPopData = [];
  195. const targetStations = ['绿华山', '横沙', '崇西闸', '金山嘴'];
  196. StnmData.data.forEach(item => {
  197. targetStations.includes(item.stationName) && stationPopData.push(item);
  198. let point = new Point([item.lgtd, item.lttd]);
  199. let feature = new Feature({
  200. geometry: point,
  201. name: item.stationName,
  202. properties: item,
  203. });
  204. var style = new Style({
  205. image: new Icon({
  206. anchor: [0.5, 0.5],
  207. scale: 0.08,
  208. src: red_trangle,
  209. }),
  210. });
  211. feature.setStyle(style);
  212. features.push(feature);
  213. })
  214. let vectorSource = new VectorSource({
  215. features: features,
  216. });
  217. stnmVectorLayer.value = new VectorLayer({
  218. source: vectorSource,
  219. });
  220. mapChart.value.addLayer(stnmVectorLayer.value);
  221. showPopupInfo(stationPopData);
  222. };
  223. // 显示地图坐标信息
  224. const showPopupInfo = (data) => {
  225. data.forEach((station, index) => {
  226. // 创建弹窗元素
  227. const popupElement = document.createElement('div');
  228. popupElement.className = 'custom-popup';
  229. popupElement.id = `popup_${station.stationCode}`;
  230. // 设置弹窗内容
  231. popupElement.innerHTML = `
  232. <div class="popup-title">${station.stationName} (${station.stationCode})</div>
  233. <div class="popup-top">
  234. <span>实时潮位:<span></span></span>
  235. <span>发生时间:<span></span></span>
  236. <span>警戒潮位:<span></span></span>
  237. <span>距离警戒:<span></span></span>
  238. </div>
  239. <div class="popup-bottom">
  240. <span>预报潮位:<span></span></span>
  241. <span>发生时间:<span></span></span>
  242. <span>距离警戒:<span></span></span>
  243. </div>
  244. `;
  245. let popupOverlay = new Overlay({
  246. element: popupElement,
  247. positioning: "top-center",
  248. stopEvent: false,
  249. offset: [0, -200],
  250. });
  251. const coordinate = [station.lgtd, station.lttd];
  252. popupOverlay.setPosition(coordinate);
  253. mapChart.value.addOverlay(popupOverlay);
  254. // 保存引用
  255. popupOverlays.value.push(popupOverlay);
  256. });
  257. };
  258. // 清除所有弹窗
  259. const clearPopups = () => {
  260. popupOverlays.value.forEach(overlay => {
  261. mapChart.value.removeOverlay(overlay);
  262. });
  263. popupOverlays.value = [];
  264. };
  265. const startDrag = (event) => {
  266. isDragging.value = true;
  267. dragStartPosition.value = {
  268. x: event.clientX - fcPosition.value.x,
  269. y: event.clientY - fcPosition.value.y
  270. };
  271. window.addEventListener('mousemove', onMouseMove);
  272. window.addEventListener('mouseup', onMouseUp);
  273. };
  274. const onMouseMove = (event) => {
  275. if (!isDragging.value) return;
  276. fcPosition.value = {
  277. x: event.clientX - dragStartPosition.value.x,
  278. y: event.clientY - dragStartPosition.value.y
  279. };
  280. dialogPosition.value = {
  281. x:fcPosition.value.x,
  282. y:fcPosition.value.y
  283. }
  284. };
  285. const onMouseUp = () => {
  286. if (isDragging.value) {
  287. showIcon.value = false;
  288. showDialog.value = true;
  289. isDragging.value = false;
  290. window.removeEventListener('mousemove', onMouseMove);
  291. window.removeEventListener('mouseup', onMouseUp);
  292. }
  293. };
  294. const startDialogDrag = (event) => {
  295. isDraggingDialog.value = true;
  296. dialogDragStart.value = {
  297. x: event.clientX - dialogPosition.value.x,
  298. y: event.clientY - dialogPosition.value.y
  299. };
  300. window.addEventListener('mousemove', onDialogMouseMove);
  301. window.addEventListener('mouseup', onDialogMouseUp);
  302. };
  303. const onDialogMouseMove = (event) => {
  304. if (!isDraggingDialog.value) return;
  305. dialogPosition.value = {
  306. x: event.clientX - dialogDragStart.value.x,
  307. y: event.clientY - dialogDragStart.value.y
  308. };
  309. };
  310. const onDialogMouseUp = () => {
  311. isDraggingDialog.value = false;
  312. window.removeEventListener('mousemove', onDialogMouseMove);
  313. window.removeEventListener('mouseup', onDialogMouseUp);
  314. };
  315. const closeDialog = () => {
  316. showDialog.value = false;
  317. showIcon.value = true;
  318. fcPosition.value = {
  319. x: 280,
  320. y: 40
  321. }
  322. };
  323. onUnmounted(() => {
  324. window.removeEventListener('mousemove', onMouseMove);
  325. window.removeEventListener('mouseup', onMouseUp);
  326. window.removeEventListener('mousemove', onDialogMouseMove);
  327. window.removeEventListener('mouseup', onDialogMouseUp);
  328. });
  329. </script>
  330. <style scoped lang="scss">
  331. /*滚动条里面轨道*/
  332. ::-webkit-scrollbar-track{
  333. background-color: rgba(20, 19, 19,0);
  334. }
  335. /*关键设置 tbody出现滚动条*/
  336. ::-webkit-scrollbar-thumb{
  337. background-color: rgba(58, 100, 179,0.5);
  338. border-radius: 8px 10px;
  339. }
  340. ::v-deep(.el-scrollbar) {
  341. --el-scrollbar-bg-color: rgba(58, 100, 179);
  342. --el-scrollbar-hover-bg-color: rgba(58, 100, 179);
  343. }
  344. .map-index{
  345. height: 100%;
  346. width: 100%;
  347. position: relative;
  348. #mapChart{
  349. height: 100%;
  350. width: 100%;
  351. }
  352. .map-left{
  353. position: absolute;
  354. top: 1%;
  355. left: 0.5%;
  356. height: 98%;
  357. width: 260px;
  358. background:rgba(255,255,255,0.9);
  359. border-radius: 8px;
  360. .left-title{
  361. width: 100%;
  362. height: 36px;
  363. background: url("@/assets/map/img/left-title.png") no-repeat;
  364. background-size: 100% 100%;
  365. font-size: 16px;
  366. font-family: 'PuHuiTi', sans-serif;
  367. font-weight: bolder;
  368. color: #fff;
  369. text-align: center;
  370. line-height: 34px;
  371. }
  372. }
  373. .map-right{
  374. position: absolute;
  375. top: 1%;
  376. right: 0.5%;
  377. height: 98%;
  378. width: 400px;
  379. background:rgba(255,255,255,0.9);
  380. border-radius: 8px;
  381. .right-title{
  382. width: 100%;
  383. height: 36px;
  384. background: url("@/assets/map/img/left-title.png") no-repeat;
  385. background-size: 100% 100%;
  386. font-size: 16px;
  387. font-family: 'PuHuiTi', sans-serif;
  388. font-weight: bolder;
  389. color: #fff;
  390. text-align: center;
  391. line-height: 34px;
  392. }
  393. .right-top-title{
  394. width: 100%;
  395. line-height: 30px;
  396. padding: 10px;
  397. display: flex;
  398. img{
  399. width: 30px;
  400. height:30px;
  401. }
  402. span{
  403. color: #000;
  404. font-size: 16px;
  405. font-family: 'PuHuiTi', sans-serif;
  406. font-weight: bolder;
  407. margin-left: 10px;
  408. }
  409. }
  410. }
  411. }
  412. .station-table{
  413. width: 94%;
  414. margin:0 3%;
  415. .table-index{
  416. width: 40px;
  417. }
  418. .table-head{
  419. width: 80px;
  420. }
  421. table{
  422. width: 100%;
  423. border-spacing: 0px;
  424. border-collapse: collapse; /* 设置表格边框合并为单线 */
  425. border-top: 2px solid #82bcfd;
  426. border-bottom: 2px solid #82bcfd;
  427. thead{
  428. background-image: -webkit-linear-gradient(top, #ebf5ff, #fff);
  429. color:#000;
  430. font-size:14px;
  431. font-family: 'PuHuiTi', sans-serif;
  432. font-weight: bold;
  433. width: 100%;
  434. border-left: 1px solid #d3e8f9;
  435. border-right: 1px solid #d3e8f9;
  436. border-bottom: 1px solid #d3e8f9;
  437. td{
  438. text-align: center;
  439. padding: 10px 0;
  440. }
  441. }
  442. tbody{
  443. color:#000;
  444. font-size:12px;
  445. font-family: 'PuHuiTi', sans-serif;
  446. width: 100%;
  447. border: 1px solid #d3e8f9;
  448. border-top:none;
  449. tr{
  450. line-height: 28px;
  451. background-image: -webkit-linear-gradient(top, #d7eaff, #fff);
  452. }
  453. td{
  454. /* border-bottom: 1px solid #d4f0fc; */
  455. text-align: center;
  456. padding: 5px 0;
  457. }
  458. }
  459. }
  460. table tbody {
  461. display: block;
  462. height: 20vh !important;
  463. overflow-y: scroll;
  464. }
  465. table thead,tbody tr {
  466. display: table;
  467. width: 100%;
  468. table-layout: fixed;
  469. }
  470. }
  471. .map-fcicon {
  472. position: absolute;
  473. width: 40px;
  474. height: 40px;
  475. background-image: url("@/assets/map/img/风场.png");
  476. background-repeat: no-repeat;
  477. background-size: 50%;
  478. background-position: center;
  479. background-color: rgba(72, 174, 228);
  480. color: white;
  481. border-radius: 50%;
  482. display: flex;
  483. justify-content: center;
  484. align-items: center;
  485. cursor: grab;
  486. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  487. transition: transform 0.2s, box-shadow 0.2s;
  488. z-index: 10;
  489. .fc-img{
  490. width: 25px;
  491. height: 25px;
  492. }
  493. }
  494. .map-fcicon:hover {
  495. transform: scale(1.1);
  496. box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
  497. }
  498. .map-fcicon:active {
  499. cursor: grabbing;
  500. }
  501. .dialog {
  502. position: absolute;
  503. width: 300px;
  504. background: white;
  505. border-radius: 10px;
  506. box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
  507. overflow: hidden;
  508. z-index: 100;
  509. }
  510. .dialog-header {
  511. padding: 5px;
  512. background: rgba(52, 152, 219,0.8);
  513. color: white;
  514. cursor: move;
  515. display: flex;
  516. justify-content: space-between;
  517. align-items: center;
  518. }
  519. .dialog-body {
  520. padding: 20px;
  521. color: #2c3e50;
  522. }
  523. .close-btn {
  524. background: none;
  525. border: none;
  526. color: white;
  527. font-size: 20px;
  528. cursor: pointer;
  529. }
  530. .drop-indicator {
  531. position: absolute;
  532. bottom: 20px;
  533. left: 50%;
  534. transform: translateX(-50%);
  535. padding: 10px 20px;
  536. background: #2ecc71;
  537. color: white;
  538. border-radius: 20px;
  539. opacity: 0;
  540. transition: opacity 0.3s;
  541. }
  542. .drop-indicator.visible {
  543. opacity: 1;
  544. }
  545. </style>
  546. <style lang="scss">
  547. .custom-popup {
  548. background: rgba(255, 255, 255, 0.95);
  549. border-radius: 8px;
  550. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
  551. border: 1px solid #3498db;
  552. width: 172px;
  553. .popup-title{
  554. width: 100%;
  555. line-height: 15px;
  556. background-color: rgba(255, 23, 0);
  557. padding: 10px;
  558. text-align: center;
  559. color: #fff;
  560. font-size: 16px;
  561. }
  562. .popup-top{
  563. padding:5px 10px;
  564. width: 100%;
  565. background-color: rgba(58, 100, 179);
  566. display: flex;
  567. flex-direction: column;
  568. color: #fff;
  569. font-size: 14px;
  570. }
  571. .popup-bottom{
  572. padding:5px 10px;
  573. width: 100%;
  574. background-color: rgba(71, 146, 211);
  575. display: flex;
  576. flex-direction: column;
  577. color: #fff;
  578. font-size: 14px;
  579. }
  580. }
  581. </style>