CesiumMap_REMOTE_843.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <template>
  2. <div id="cesiumContainer" class="cesium-ignore-autofit" style="width: 100%; height: 100%; position: relative; z-index: 1; transform: none !important; zoom: 1 !important;"></div>
  3. <!-- POI详情弹窗 -->
  4. <div v-if="showPopup" class="poi-popup" :style="popupStyle">
  5. <div class="popup-header">
  6. <span class="popup-title">{{ poiData.name }} <a href="#" class="popup-detail" @click.prevent="goToTwinStation">详情</a></span>
  7. <button class="popup-close" @click="closePopup">×</button>
  8. </div>
  9. <div class="popup-content">
  10. <div class="popup-section">
  11. <div class="popup-label">实时数据</div>
  12. <div class="popup-value">水位: {{ poiData.waterLevel }}m</div>
  13. <div class="popup-value">流量: {{ poiData.flow }}m³/s</div>
  14. <div class="popup-value">降雨量: {{ poiData.rainfall }}mm/h</div>
  15. </div>
  16. <div class="popup-section">
  17. <div class="popup-label">历史同期值</div>
  18. <div class="popup-value">水位: {{ poiData.historyWaterLevel }}m</div>
  19. <div class="popup-value">流量: {{ poiData.historyFlow }}m³/s</div>
  20. </div>
  21. </div>
  22. </div>
  23. </template>
  24. <script setup>
  25. import { ref, onMounted, onUnmounted, computed } from 'vue'
  26. import * as Cesium from 'cesium'
  27. import 'cesium/Build/CesiumUnminified/Widgets/widgets.css'
  28. import { useRouter } from 'vue-router'
  29. const emit = defineEmits(['toggleMap'])
  30. const viewer = ref(null)
  31. const showPopup = ref(false)
  32. const popupPosition = ref({ x: 0, y: 0 })
  33. const router = useRouter()
  34. const DESIGN_WIDTH = 1920
  35. const DESIGN_HEIGHT = 1080
  36. const getScaleRatio = () => {
  37. return Math.min(window.innerWidth / DESIGN_WIDTH, window.innerHeight / DESIGN_HEIGHT)
  38. }
  39. const poiData = ref({
  40. name: '黑林水文站',
  41. waterLevel: '3.25',
  42. flow: '12.5',
  43. rainfall: '0.5',
  44. historyWaterLevel: '3.10',
  45. historyFlow: '11.8',
  46. device: 'HL-Sensor-001'
  47. })
  48. const popupStyle = computed(() => {
  49. return {
  50. left: popupPosition.value.x + 'px',
  51. top: popupPosition.value.y + 'px'
  52. }
  53. })
  54. let blinkInterval = null
  55. const addPoiMarker = () => {
  56. // 添加黑林水文站POI
  57. const heilinPosition = Cesium.Cartesian3.fromDegrees(118.8770798, 35.0320627)
  58. // 创建一个不可见的点实体,用于扩大点击区域
  59. const clickEntity = viewer.value.entities.add({
  60. id: 'heilin-station-click',
  61. position: heilinPosition,
  62. point: {
  63. pixelSize: 30,
  64. color: Cesium.Color.TRANSPARENT,
  65. outlineColor: Cesium.Color.TRANSPARENT,
  66. outlineWidth: 0
  67. }
  68. })
  69. const entity = viewer.value.entities.add({
  70. id: 'heilin-station',
  71. position: heilinPosition,
  72. billboard: {
  73. image: '/src/assets/images/水文站.png',
  74. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  75. horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
  76. pixelOffset: new Cesium.Cartesian2(0, -15),
  77. scale: 0.8,
  78. width: 30,
  79. height: 37.5,
  80. disableDepthTestDistance: Number.POSITIVE_INFINITY
  81. },
  82. label: {
  83. text: '水位 3.25m\n流量 12.5m³/s\n降雨量 0.5mm/h',
  84. font: 'bold 14px 黑体',
  85. fillColor: Cesium.Color.WHITE,
  86. outlineColor: Cesium.Color.fromCssColorString('#003860'),
  87. outlineWidth: 2,
  88. style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  89. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  90. horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
  91. pixelOffset: new Cesium.Cartesian2(0, -30),
  92. showBackground: true,
  93. backgroundColor: new Cesium.Color(0, 0.3, 0.5, 0.7),
  94. backgroundPadding: new Cesium.Cartesian2(8, 4),
  95. disableDepthTestDistance: Number.POSITIVE_INFINITY
  96. }
  97. })
  98. // 添加标题标签
  99. viewer.value.entities.add({
  100. id: 'heilin-station-title',
  101. position: heilinPosition,
  102. label: {
  103. text: '黑林水文站',
  104. font: 'bold 14px sans-serif',
  105. fillColor: Cesium.Color.WHITE,
  106. outlineColor: Cesium.Color.fromCssColorString('#003860'),
  107. outlineWidth: 2,
  108. style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  109. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  110. horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
  111. pixelOffset: new Cesium.Cartesian2(0, -85),
  112. showBackground: true,
  113. backgroundColor: new Cesium.Color(0, 0.2, 0.4, 0.9),
  114. backgroundPadding: new Cesium.Cartesian2(22, 4),
  115. disableDepthTestDistance: Number.POSITIVE_INFINITY
  116. }
  117. })
  118. // 添加塔山小水库POI
  119. const reservoirPosition = Cesium.Cartesian3.fromDegrees(118.9540555, 34.9355329)
  120. const reservoirEntity = viewer.value.entities.add({
  121. id: 'tashan-reservoir',
  122. position: reservoirPosition,
  123. billboard: {
  124. image: '/src/assets/images/水库蓝.png',
  125. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  126. horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
  127. pixelOffset: new Cesium.Cartesian2(0, -15),
  128. scale: 0.8,
  129. width: 30,
  130. height: 30,
  131. disableDepthTestDistance: Number.POSITIVE_INFINITY
  132. },
  133. label: {
  134. text: '库水位 18.5m\n库容 2350.8万m³\n汛限 20.0m',
  135. font: 'bold 14px 黑体',
  136. fillColor: Cesium.Color.WHITE,
  137. outlineColor: Cesium.Color.fromCssColorString('#003860'),
  138. outlineWidth: 2,
  139. style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  140. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  141. horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
  142. pixelOffset: new Cesium.Cartesian2(0, -30),
  143. showBackground: true,
  144. backgroundColor: new Cesium.Color(0, 0.3, 0.5, 0.7),
  145. backgroundPadding: new Cesium.Cartesian2(8, 4),
  146. disableDepthTestDistance: Number.POSITIVE_INFINITY
  147. }
  148. })
  149. // 添加标题标签
  150. viewer.value.entities.add({
  151. id: 'tashan-reservoir-title',
  152. position: reservoirPosition,
  153. label: {
  154. text: '小塔山水库',
  155. font: 'bold 14px sans-serif',
  156. fillColor: Cesium.Color.WHITE,
  157. outlineColor: Cesium.Color.fromCssColorString('#003860'),
  158. outlineWidth: 2,
  159. style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  160. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  161. horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
  162. pixelOffset: new Cesium.Cartesian2(0, -85),
  163. showBackground: true,
  164. backgroundColor: new Cesium.Color(0, 0.2, 0.4, 0.9),
  165. backgroundPadding: new Cesium.Cartesian2(22, 4),
  166. disableDepthTestDistance: Number.POSITIVE_INFINITY
  167. }
  168. })
  169. const handler = new Cesium.ScreenSpaceEventHandler(viewer.value.scene.canvas)
  170. handler.setInputAction((movement) => {
  171. const scaleRatio = getScaleRatio()
  172. const correctedX = movement.position.x / scaleRatio
  173. const correctedY = movement.position.y / scaleRatio
  174. const pickedObjects = viewer.value.scene.drillPick(new Cesium.Cartesian2(correctedX, correctedY))
  175. for (let i = 0; i < pickedObjects.length; i++) {
  176. const pickedObj = pickedObjects[i]
  177. if (Cesium.defined(pickedObj) && pickedObj.id) {
  178. if (pickedObj.id.id === 'heilin-station' || pickedObj.id.id === 'heilin-station-click') {
  179. showPopup.value = true
  180. popupPosition.value = {
  181. x: movement.position.x - 120,
  182. y: movement.position.y - 150
  183. }
  184. break
  185. }
  186. }
  187. }
  188. }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
  189. // 移除闪烁效果
  190. entity.billboard.scale = 0.8
  191. reservoirEntity.billboard.scale = 0.8
  192. }
  193. const createPoiImage = () => {
  194. const canvas = document.createElement('canvas')
  195. canvas.width = 40
  196. canvas.height = 50
  197. const ctx = canvas.getContext('2d')
  198. ctx.beginPath()
  199. ctx.moveTo(20, 0)
  200. ctx.lineTo(40, 30)
  201. ctx.lineTo(20, 50)
  202. ctx.lineTo(0, 30)
  203. ctx.closePath()
  204. const gradient = ctx.createRadialGradient(20, 25, 5, 20, 25, 25)
  205. gradient.addColorStop(0, '#00ffff')
  206. gradient.addColorStop(1, '#0066ff')
  207. ctx.fillStyle = gradient
  208. ctx.fill()
  209. ctx.strokeStyle = '#ffffff'
  210. ctx.lineWidth = 2
  211. ctx.stroke()
  212. return canvas
  213. }
  214. const closePopup = () => {
  215. showPopup.value = false
  216. }
  217. const goToTwinStation = () => {
  218. // 执行平滑flyto动画到黑林水文站位置
  219. const heilinPosition = Cesium.Cartesian3.fromDegrees(118.8770798, 35.0320627, 10000)
  220. viewer.value.camera.flyTo({
  221. destination: heilinPosition,
  222. duration: 2.0,
  223. easingFunction: Cesium.EasingFunction.SINE_IN_OUT
  224. })
  225. // 延迟跳转到水文站详情页面
  226. setTimeout(() => {
  227. router.push('/heilin-station')
  228. }, 2000)
  229. closePopup()
  230. }
  231. onMounted(async () => {
  232. viewer.value = new Cesium.Viewer('cesiumContainer', {
  233. terrainProvider: await Cesium.createWorldTerrainAsync(),
  234. animation: false,
  235. baseLayerPicker: false,
  236. fullscreenButton: false,
  237. geocoder: false,
  238. homeButton: false,
  239. infoBox: false,
  240. sceneModePicker: false,
  241. selectionIndicator: false,
  242. timeline: false,
  243. navigationHelpButton: false,
  244. navigationInstructionsInitiallyVisible: false,
  245. shouldAnimate: false,
  246. showRenderLoopErrors: false
  247. })
  248. if (viewer.value.cesiumWidget.creditContainer) {
  249. viewer.value.cesiumWidget.creditContainer.style.display = 'none'
  250. }
  251. const toolbar = viewer.value._element.querySelector('.cesium-viewer-toolbar')
  252. if (toolbar) toolbar.style.display = 'none'
  253. const animationContainer = viewer.value._element.querySelector('.cesium-viewer-animationContainer')
  254. if (animationContainer) animationContainer.style.display = 'none'
  255. const timelineContainer = viewer.value._element.querySelector('.cesium-viewer-timelineContainer')
  256. if (timelineContainer) timelineContainer.style.display = 'none'
  257. try {
  258. // 加载流域边界GeoJSON
  259. const response = await fetch('/Heilin.geojson')
  260. const geojson = await response.json()
  261. const dataSource = await Cesium.GeoJsonDataSource.load(geojson, {
  262. stroke: Cesium.Color.fromCssColorString('#00d5ff'),
  263. strokeWidth: 3,
  264. fill: Cesium.Color.fromCssColorString('#00d5ff').withAlpha(0.1)
  265. })
  266. viewer.value.dataSources.add(dataSource)
  267. // 加载水系GeoJSON
  268. try {
  269. const waterResponse = await fetch('/黑林水系.geojson')
  270. const waterGeojson = await waterResponse.json()
  271. const waterDataSource = await Cesium.GeoJsonDataSource.load(waterGeojson, {
  272. stroke: Cesium.Color.fromCssColorString('#62f6fb'),
  273. strokeWidth: 2,
  274. fill: Cesium.Color.fromCssColorString('#62f6fb').withAlpha(0.2)
  275. })
  276. viewer.value.dataSources.add(waterDataSource)
  277. } catch (waterError) {
  278. console.error('加载水系GeoJSON失败:', waterError)
  279. }
  280. // 加载小塔山水库GeoJSON
  281. try {
  282. const reservoirResponse = await fetch('/小塔山水库.geojson')
  283. const reservoirGeojson = await reservoirResponse.json()
  284. const reservoirDataSource = await Cesium.GeoJsonDataSource.load(reservoirGeojson, {
  285. stroke: Cesium.Color.fromCssColorString('#00d4ff'),
  286. strokeWidth: 2,
  287. fill: Cesium.Color.fromCssColorString('#00d4ff').withAlpha(0.3)
  288. })
  289. viewer.value.dataSources.add(reservoirDataSource)
  290. } catch (reservoirError) {
  291. console.error('加载小塔山水库GeoJSON失败:', reservoirError)
  292. }
  293. const entities = dataSource.entities.values
  294. if (entities.length > 0) {
  295. let minLat = 90, maxLat = -90, minLon = 180, maxLon = -180
  296. for (const entity of entities) {
  297. if (entity.polygon && entity.polygon.hierarchy) {
  298. const hierarchy = entity.polygon.hierarchy.getValue()
  299. const positions = hierarchy.positions
  300. for (const pos of positions) {
  301. const cartographic = Cesium.Cartographic.fromCartesian(pos)
  302. const lat = Cesium.Math.toDegrees(cartographic.latitude)
  303. const lon = Cesium.Math.toDegrees(cartographic.longitude)
  304. minLat = Math.min(minLat, lat)
  305. maxLat = Math.max(maxLat, lat)
  306. minLon = Math.min(minLon, lon)
  307. maxLon = Math.max(maxLon, lon)
  308. }
  309. }
  310. }
  311. viewer.value.camera.flyTo({
  312. destination: Cesium.Cartesian3.fromDegrees(118.9019, 34.985, 33000),
  313. duration: 0
  314. })
  315. }
  316. } catch (error) {
  317. console.error('加载GeoJSON失败:', error)
  318. viewer.value.camera.flyTo({
  319. destination: Cesium.Cartesian3.fromDegrees(118.9019, 34.985, 33000),
  320. duration: 0
  321. })
  322. }
  323. addPoiMarker()
  324. window.addEventListener('resize', handleResize)
  325. })
  326. const handleResize = () => {
  327. if (viewer.value) {
  328. viewer.value.resize()
  329. }
  330. }
  331. onUnmounted(() => {
  332. window.removeEventListener('resize', handleResize)
  333. if (blinkInterval) {
  334. clearInterval(blinkInterval)
  335. }
  336. if (viewer.value) {
  337. viewer.value.destroy()
  338. }
  339. })
  340. </script>
  341. <style scoped>
  342. .poi-popup {
  343. position: fixed;
  344. width: 240px;
  345. background: rgba(0, 20, 40, 0.95);
  346. border: 1px solid rgba(0, 213, 255, 0.6);
  347. border-radius: 8px;
  348. box-shadow: 0 0 20px rgba(0, 213, 255, 0.4);
  349. z-index: 10;
  350. pointer-events: auto;
  351. overflow: hidden;
  352. }
  353. .popup-header {
  354. display: flex;
  355. justify-content: space-between;
  356. align-items: center;
  357. padding: 12px 15px;
  358. background: linear-gradient(90deg, rgba(0, 60, 120, 0.9), rgba(0, 100, 150, 0.7));
  359. border-bottom: 1px solid rgba(0, 213, 255, 0.4);
  360. }
  361. .popup-title {
  362. font-size: 16px;
  363. font-weight: bold;
  364. color: #00ffff;
  365. text-shadow: 0 0 5px rgba(0, 255, 255, 0.5);
  366. }
  367. .popup-detail {
  368. font-size: 12px;
  369. color: #62f6fb;
  370. text-decoration: none;
  371. margin-left: 8px;
  372. cursor: pointer;
  373. transition: color 0.3s;
  374. }
  375. .popup-detail:hover {
  376. color: #ffffff;
  377. text-decoration: underline;
  378. }
  379. .popup-close {
  380. background: none;
  381. border: none;
  382. color: #7bbef6;
  383. font-size: 20px;
  384. cursor: pointer;
  385. padding: 0;
  386. line-height: 1;
  387. transition: color 0.3s;
  388. }
  389. .popup-close:hover {
  390. color: #ffffff;
  391. }
  392. .popup-content {
  393. padding: 15px;
  394. }
  395. .popup-section {
  396. margin-bottom: 15px;
  397. }
  398. .popup-section:last-child {
  399. margin-bottom: 0;
  400. }
  401. .popup-label {
  402. font-size: 14px;
  403. font-weight: bold;
  404. color: #62f6fb;
  405. margin-bottom: 8px;
  406. text-shadow: 0 0 3px rgba(0, 212, 255, 0.5);
  407. }
  408. .popup-value {
  409. font-size: 13px;
  410. color: #e0fcff;
  411. margin-bottom: 5px;
  412. line-height: 1.5;
  413. }
  414. </style>