huweili-cesium 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,152 +1,175 @@
1
1
  <template>
2
- <!-- WebSocket连接状态指示器:动态挂载到 Cesium 自动生成的 .cesium-viewer -->
3
- <Teleport v-if="cesiumViewerEl" :to="cesiumViewerEl">
4
- <div class="ws-drone-indicator">
5
- <div class="ws-drone-indicator-dot" :class="wsIndicatorClass"></div>
6
- </div>
7
- </Teleport>
8
- </template>
9
-
10
- <script setup>
11
- /**
12
- * WsIndicator.vue - WebSocket连接状态指示器组件
13
- *
14
- * 组件功能:
15
- * - 实时显示WebSocket连接状态
16
- * - 提供可视化的连接状态反馈
17
- *
18
- * 状态说明:
19
- * - 断开连接时红色高亮(.disconnected)
20
- * - 连接正常时微弱绿色(.connected)
21
- * - 收到无人机数据时绿色高亮闪烁(.active)
22
- */
23
- import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
24
- import { useEventBus } from '../../js/utils/useEventBus.js'
25
-
26
- const { on, off } = useEventBus()
27
-
28
- // Cesium Viewer 自动生成的 DOM 容器
29
- const cesiumViewerEl = ref(null)
30
-
31
- // WebSocket连接状态
32
- const wsConnected = ref(false)
33
- // 无人机数据接收高亮状态(用于闪烁效果)
34
- const wsDroneIndicatorActive = ref(false)
35
-
36
- /**
37
- * 计算指示器的CSS类名
38
- * 根据连接状态和数据接收状态返回对应的类名
39
- */
40
- const wsIndicatorClass = computed(() => {
41
- if (!wsConnected.value) {
42
- return 'disconnected' // 断开连接时红色高亮
43
- }
44
- if (wsDroneIndicatorActive.value) {
45
- return 'active' // 收到无人机数据时绿色高亮
46
- }
47
- return 'connected' // 连接正常但无数据时微弱绿色
48
- })
49
-
50
- /**
51
- * 处理无人机数据接收事件
52
- * 收到数据时设置连接状态为true,并触发高亮闪烁
53
- */
54
- const handleWsDroneDataReceived = () => {
55
- wsConnected.value = true // 收到数据说明连接正常
56
- wsDroneIndicatorActive.value = true // 触发绿色高亮
57
- // 300ms后恢复微弱绿色状态
58
- setTimeout(() => {
59
- wsDroneIndicatorActive.value = false
60
- }, 300)
2
+ <!-- WebSocket连接状态指示器:挂载到当前地图容器内的 .cesium-viewer -->
3
+ <Teleport v-if="cesiumViewerEl" :to="cesiumViewerEl">
4
+ <div class="ws-drone-indicator">
5
+ <div class="ws-drone-indicator-dot" :class="wsIndicatorClass"></div>
6
+ </div>
7
+ </Teleport>
8
+ </template>
9
+
10
+ <script setup>
11
+ /**
12
+ * WsIndicator.vue - WebSocket连接状态指示器组件
13
+ *
14
+ * 状态说明:
15
+ * - 断开连接时红色高亮(.disconnected)
16
+ * - 连接正常时微弱绿色(.connected)
17
+ * - 收到无人机数据时绿色高亮闪烁(.active)
18
+ */
19
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
20
+ import { useEventBus } from '../../js/utils/useEventBus.js'
21
+
22
+ const props = defineProps({
23
+ /** 地图根容器 ref(index.vue 中的 cesium-container) */
24
+ container: {
25
+ type: Object,
26
+ default: null,
27
+ },
28
+ })
29
+
30
+ const { on, off } = useEventBus()
31
+
32
+ const cesiumViewerEl = ref(null)
33
+ const wsConnected = ref(false)
34
+ const wsDroneIndicatorActive = ref(false)
35
+
36
+ let viewerObserver = null
37
+ let resolveTimer = null
38
+
39
+ const wsIndicatorClass = computed(() => {
40
+ if (!wsConnected.value) {
41
+ return 'disconnected'
61
42
  }
62
-
63
- /**
64
- * 处理WebSocket连接成功事件
65
- */
66
- const handleWsConnected = () => {
67
- wsConnected.value = true
43
+ if (wsDroneIndicatorActive.value) {
44
+ return 'active'
68
45
  }
69
-
70
- /**
71
- * 处理WebSocket断开连接事件
72
- */
73
- const handleWsDisconnected = () => {
74
- wsConnected.value = false
46
+ return 'connected'
47
+ })
48
+
49
+ const resolveViewerElement = () => {
50
+ const root = props.container?.value
51
+ if (!root) return null
52
+ return root.querySelector('.cesium-viewer')
53
+ }
54
+
55
+ const bindViewerElement = () => {
56
+ const viewer = resolveViewerElement()
57
+ if (!viewer) return false
58
+ cesiumViewerEl.value = viewer
59
+ return true
60
+ }
61
+
62
+ const stopResolveViewerElement = () => {
63
+ viewerObserver?.disconnect()
64
+ viewerObserver = null
65
+ if (resolveTimer) {
66
+ clearTimeout(resolveTimer)
67
+ resolveTimer = null
75
68
  }
76
-
77
- /**
78
- * 组件挂载时订阅事件
79
- */
80
- onMounted(async () => {
81
- await nextTick()
82
- cesiumViewerEl.value = document.querySelector('.cesium-viewer')
83
-
84
- // 订阅无人机数据接收事件
85
- on('wsDroneDataReceived', handleWsDroneDataReceived)
86
- // 订阅WebSocket连接成功事件
87
- on('wsConnected', handleWsConnected)
88
- // 订阅WebSocket断开连接事件
89
- on('wsDisconnected', handleWsDisconnected)
90
-
91
- // 检查全局连接状态(处理组件挂载前已连接的情况)
92
- if (window.wsConnected) {
93
- wsConnected.value = true
69
+ }
70
+
71
+ const startResolveViewerElement = () => {
72
+ stopResolveViewerElement()
73
+ if (bindViewerElement()) return
74
+
75
+ const root = props.container?.value
76
+ if (!root) return
77
+
78
+ viewerObserver = new MutationObserver(() => {
79
+ if (bindViewerElement()) {
80
+ stopResolveViewerElement()
94
81
  }
95
82
  })
96
-
97
- /**
98
- * 组件卸载时取消订阅,避免内存泄漏
99
- */
100
- onUnmounted(() => {
101
- off('wsDroneDataReceived', handleWsDroneDataReceived)
102
- off('wsConnected', handleWsConnected)
103
- off('wsDisconnected', handleWsDisconnected)
104
- })
105
- </script>
106
-
107
- <style scoped lang="scss">
108
- /* WebSocket指示器容器 */
109
- .ws-drone-indicator {
110
- position: absolute;
111
- left: 0px;
112
- bottom: 0px;
113
- width: 13px;
114
- height: 13px;
115
- border-radius: 3px;
116
- background: rgba(40, 40, 40, 0.7);
117
- display: flex;
118
- align-items: center;
119
- justify-content: center;
120
- z-index: 2147483647;
121
- pointer-events: none;
122
- }
123
-
124
- /* 指示器圆点基础样式 */
125
- .ws-drone-indicator-dot {
126
- width: 4px;
127
- height: 4px;
128
- border-radius: 50%;
129
- background: #0f3;
130
- opacity: 0.25;
131
- transition: opacity 0.08s ease, box-shadow 0.08s ease;
132
- }
133
-
134
- /* 连接正常状态 - 微弱绿色 */
135
- .ws-drone-indicator-dot.connected {
136
- opacity: 0.3;
137
- box-shadow: none;
138
- }
139
-
140
- /* 收到数据状态 - 绿色高亮 */
141
- .ws-drone-indicator-dot.active {
142
- opacity: 1;
143
- box-shadow: 0 0 6px #0f3;
144
- }
145
-
146
- /* 断开连接状态 - 红色高亮 */
147
- .ws-drone-indicator-dot.disconnected {
148
- background: #f33;
149
- opacity: 1;
150
- box-shadow: 0 0 6px #f33;
83
+ viewerObserver.observe(root, { childList: true, subtree: true })
84
+
85
+ // 地图初始化较慢时兜底重试,避免 Teleport 目标一直为空
86
+ resolveTimer = window.setTimeout(() => {
87
+ bindViewerElement()
88
+ stopResolveViewerElement()
89
+ }, 10000)
90
+ }
91
+
92
+ const handleWsDroneDataReceived = () => {
93
+ wsConnected.value = true
94
+ wsDroneIndicatorActive.value = true
95
+ setTimeout(() => {
96
+ wsDroneIndicatorActive.value = false
97
+ }, 300)
98
+ }
99
+
100
+ const handleWsConnected = () => {
101
+ wsConnected.value = true
102
+ }
103
+
104
+ const handleWsDisconnected = () => {
105
+ wsConnected.value = false
106
+ }
107
+
108
+ watch(
109
+ () => props.container?.value,
110
+ () => {
111
+ cesiumViewerEl.value = null
112
+ startResolveViewerElement()
113
+ },
114
+ { immediate: true },
115
+ )
116
+
117
+ onMounted(() => {
118
+ on('wsDroneDataReceived', handleWsDroneDataReceived)
119
+ on('wsConnected', handleWsConnected)
120
+ on('wsDisconnected', handleWsDisconnected)
121
+
122
+ if (window.wsConnected) {
123
+ wsConnected.value = true
151
124
  }
152
- </style>
125
+ })
126
+
127
+ onUnmounted(() => {
128
+ stopResolveViewerElement()
129
+ off('wsDroneDataReceived', handleWsDroneDataReceived)
130
+ off('wsConnected', handleWsConnected)
131
+ off('wsDisconnected', handleWsDisconnected)
132
+ })
133
+ </script>
134
+
135
+ <style scoped lang="scss">
136
+ .ws-drone-indicator {
137
+ position: absolute;
138
+ left: 0px;
139
+ bottom: 0px;
140
+ width: 13px;
141
+ height: 13px;
142
+ border-radius: 3px;
143
+ background: rgba(40, 40, 40, 0.7);
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ z-index: 2147483647;
148
+ pointer-events: none;
149
+ }
150
+
151
+ .ws-drone-indicator-dot {
152
+ width: 4px;
153
+ height: 4px;
154
+ border-radius: 50%;
155
+ background: #0f3;
156
+ opacity: 0.25;
157
+ transition: opacity 0.08s ease, box-shadow 0.08s ease;
158
+ }
159
+
160
+ .ws-drone-indicator-dot.connected {
161
+ opacity: 0.3;
162
+ box-shadow: none;
163
+ }
164
+
165
+ .ws-drone-indicator-dot.active {
166
+ opacity: 1;
167
+ box-shadow: 0 0 6px #0f3;
168
+ }
169
+
170
+ .ws-drone-indicator-dot.disconnected {
171
+ background: #f33;
172
+ opacity: 1;
173
+ box-shadow: 0 0 6px #f33;
174
+ }
175
+ </style>
package/index.vue CHANGED
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="cesium-map-wrapper">
3
3
  <div class="cesium-container" ref="cesiumContainer" />
4
- <WsIndicator />
4
+ <WsIndicator :container="cesiumContainer" />
5
5
  </div>
6
6
  </template>
7
7
 
@@ -1002,8 +1002,16 @@ export function drawFenceNew() {
1002
1002
  let centerPointEntity = null
1003
1003
  // activePointEntity:活动点实体(跟随鼠标的边界点)
1004
1004
  let activePointEntity = null
1005
- // ellipseEntity:椭圆实体(用于显示圆形围栏的预览)
1006
- let ellipseEntity = null
1005
+ // circleWallEntity / circleOutlineEntity / radiusLineEntity:绘制过程预览(与最终围栏一致的贴地圆边)
1006
+ let circleWallEntity = null
1007
+ let circleOutlineEntity = null
1008
+ let radiusLineEntity = null
1009
+ // 预览几何缓存,仅在鼠标移动时更新,避免每帧重复计算
1010
+ let previewCirclePositions = []
1011
+ let previewWallMaxHeights = []
1012
+ let previewWallMinHeights = []
1013
+ let previewRadiusLinePositions = []
1014
+ let previewRenderFrameId = null
1007
1015
  // tipEntity:提示文字实体(显示"点击确定圆心"等提示)
1008
1016
  let tipEntity = null
1009
1017
  // radiusLabelEntity:绘制过程中显示当前半径的标签
@@ -1018,7 +1026,9 @@ export function drawFenceNew() {
1018
1026
  // ==================== 临时实体ID配置 ====================
1019
1027
  // 这些是绘制过程中临时显示的实体,绘制完成后会清理
1020
1028
  const tempIds = {
1021
- ellipse: `${options.id}_draw_circle_wall`, // 临时椭圆围栏
1029
+ circleWall: `${options.id}_draw_circle_wall`,
1030
+ circleOutline: `${options.id}_draw_circle_outline`,
1031
+ radiusLine: `${options.id}_draw_radius_line`,
1022
1032
  center: `${options.id}_draw_center_point`, // 临时圆心点
1023
1033
  active: `${options.id}_draw_active_point`, // 临时边界点
1024
1034
  tip: `${options.id}_draw_tip`, // 临时提示文字
@@ -1139,6 +1149,42 @@ export function drawFenceNew() {
1139
1149
  })
1140
1150
  }
1141
1151
 
1152
+ const updatePreviewGeometry = () => {
1153
+ if (!centerPosition || !edgePosition) {
1154
+ previewCirclePositions = []
1155
+ previewWallMaxHeights = []
1156
+ previewWallMinHeights = []
1157
+ previewRadiusLinePositions = []
1158
+ return
1159
+ }
1160
+
1161
+ const radius = getRadius()
1162
+ previewRadiusLinePositions = [centerPosition, edgePosition]
1163
+
1164
+ if (radius <= 0) {
1165
+ previewCirclePositions = []
1166
+ previewWallMaxHeights = []
1167
+ previewWallMinHeights = []
1168
+ return
1169
+ }
1170
+
1171
+ const centerData = cartesianToLngLatHeight(centerPosition)
1172
+ const circleLngLats = generateCircleLngLatPositions(centerData.lng, centerData.lat, radius)
1173
+ const footprint = circleLngLats.map(cartesianFootprintFromLngLat)
1174
+ previewCirclePositions = [...footprint, footprint[0]]
1175
+ previewWallMaxHeights = previewCirclePositions.map(() => height)
1176
+ previewWallMinHeights = previewCirclePositions.map(() => 0)
1177
+ }
1178
+
1179
+ const schedulePreviewUpdate = () => {
1180
+ if (previewRenderFrameId !== null) return
1181
+ previewRenderFrameId = requestAnimationFrame(() => {
1182
+ previewRenderFrameId = null
1183
+ updatePreviewGeometry()
1184
+ map.scene.requestRender()
1185
+ })
1186
+ }
1187
+
1142
1188
  // ==================== 核心业务函数 ====================
1143
1189
 
1144
1190
  /**
@@ -1162,25 +1208,45 @@ export function drawFenceNew() {
1162
1208
  })
1163
1209
  }
1164
1210
 
1165
- // 2. 创建椭圆实体(如果还没有)
1166
- if (!ellipseEntity) {
1167
- ellipseEntity = map.entities.add({
1168
- id: tempIds.ellipse,
1169
- // 使用CallbackProperty实现动态更新:当centerPosition或radius变化时自动更新
1170
- position: new Cesium.CallbackProperty(() => centerPosition, false),
1171
- ellipse: {
1172
- // 椭圆的长半轴(动态半径)
1173
- semiMajorAxis: new Cesium.CallbackProperty(() => getRadius(), false),
1174
- // 椭圆的短半轴(与长半轴相同 = 圆形)
1175
- semiMinorAxis: new Cesium.CallbackProperty(() => getRadius(), false),
1176
- height: 0, // 椭圆底部高度(地面)
1177
- extrudedHeight: height, // 椭圆顶部高度(围栏高度)
1178
- material: color.withAlpha(0), // 填充材质(透明)
1179
- outline: true, // 显示边框
1180
- outlineColor: color, // 边框颜色
1181
- outlineWidth, // 边框宽度
1182
- numberOfVerticalLines: 64 // 垂直线条数,数值越大圆形越圆滑
1183
- }
1211
+ // 2. 创建贴地圆边预览(确定圆心后显示,与最终围栏 polyline / wall 一致)
1212
+ if (centerPosition && !circleWallEntity) {
1213
+ circleWallEntity = map.entities.add({
1214
+ id: tempIds.circleWall,
1215
+ wall: {
1216
+ positions: new Cesium.CallbackProperty(() => previewCirclePositions, false),
1217
+ maximumHeights: new Cesium.CallbackProperty(() => previewWallMaxHeights, false),
1218
+ minimumHeights: new Cesium.CallbackProperty(() => previewWallMinHeights, false),
1219
+ material: color.withAlpha(0.18),
1220
+ outline: true,
1221
+ outlineColor: color,
1222
+ outlineWidth,
1223
+ },
1224
+ })
1225
+ }
1226
+
1227
+ if (centerPosition && !circleOutlineEntity) {
1228
+ circleOutlineEntity = map.entities.add({
1229
+ id: tempIds.circleOutline,
1230
+ polyline: {
1231
+ positions: new Cesium.CallbackProperty(() => previewCirclePositions, false),
1232
+ width: outlineWidth,
1233
+ material: color,
1234
+ clampToGround: true,
1235
+ arcType: Cesium.ArcType.GEODESIC,
1236
+ },
1237
+ })
1238
+ }
1239
+
1240
+ if (centerPosition && !radiusLineEntity) {
1241
+ radiusLineEntity = map.entities.add({
1242
+ id: tempIds.radiusLine,
1243
+ polyline: {
1244
+ positions: new Cesium.CallbackProperty(() => previewRadiusLinePositions, false),
1245
+ width: Math.max(1, outlineWidth - 1),
1246
+ material: color.withAlpha(0.85),
1247
+ clampToGround: true,
1248
+ arcType: Cesium.ArcType.GEODESIC,
1249
+ },
1184
1250
  })
1185
1251
  }
1186
1252
 
@@ -1241,17 +1307,28 @@ export function drawFenceNew() {
1241
1307
  */
1242
1308
  const cleanupTempEntities = () => {
1243
1309
  // 遍历所有临时实体,如果存在就从地图上移除
1244
- ;[ellipseEntity, centerPointEntity, activePointEntity, radiusLabelEntity, tipEntity, nameLabelEntity].forEach((entity) => {
1310
+ ;[circleWallEntity, circleOutlineEntity, radiusLineEntity, centerPointEntity, activePointEntity, radiusLabelEntity, tipEntity, nameLabelEntity].forEach((entity) => {
1245
1311
  if (entity) {
1246
1312
  map.entities.remove(entity)
1247
1313
  }
1248
1314
  })
1249
1315
  // 将变量清空,防止内存泄露
1250
- ellipseEntity = null
1316
+ circleWallEntity = null
1317
+ circleOutlineEntity = null
1318
+ radiusLineEntity = null
1251
1319
  centerPointEntity = null
1252
1320
  activePointEntity = null
1253
1321
  radiusLabelEntity = null
1322
+ tipEntity = null
1254
1323
  nameLabelEntity = null
1324
+ previewCirclePositions = []
1325
+ previewWallMaxHeights = []
1326
+ previewWallMinHeights = []
1327
+ previewRadiusLinePositions = []
1328
+ if (previewRenderFrameId !== null) {
1329
+ cancelAnimationFrame(previewRenderFrameId)
1330
+ previewRenderFrameId = null
1331
+ }
1255
1332
  }
1256
1333
 
1257
1334
  /**
@@ -1291,9 +1368,17 @@ export function drawFenceNew() {
1291
1368
  const footprint = circleLngLats.map(cartesianFootprintFromLngLat)
1292
1369
  const wallPositions = [...footprint, footprint[0]]
1293
1370
 
1294
- if (ellipseEntity) {
1295
- map.entities.remove(ellipseEntity)
1296
- ellipseEntity = null
1371
+ if (circleWallEntity) {
1372
+ map.entities.remove(circleWallEntity)
1373
+ circleWallEntity = null
1374
+ }
1375
+ if (circleOutlineEntity) {
1376
+ map.entities.remove(circleOutlineEntity)
1377
+ circleOutlineEntity = null
1378
+ }
1379
+ if (radiusLineEntity) {
1380
+ map.entities.remove(radiusLineEntity)
1381
+ radiusLineEntity = null
1297
1382
  }
1298
1383
 
1299
1384
  const fenceEntity = map.entities.add({
@@ -1451,6 +1536,7 @@ export function drawFenceNew() {
1451
1536
  centerPosition = Cesium.Cartesian3.clone(cartesian)
1452
1537
  edgePosition = Cesium.Cartesian3.clone(cartesian) // 边界点初始化为圆心
1453
1538
  ensurePreviewEntities() // 确保预览实体都创建好
1539
+ schedulePreviewUpdate()
1454
1540
  emitChange() // 触发变化回调
1455
1541
  return
1456
1542
  }
@@ -1473,6 +1559,7 @@ export function drawFenceNew() {
1473
1559
  // 如果已经确定了圆心 → 实时更新边界点,让预览跟随鼠标
1474
1560
  if (!centerPosition) return
1475
1561
  edgePosition = Cesium.Cartesian3.clone(cartesian)
1562
+ schedulePreviewUpdate()
1476
1563
  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
1477
1564
 
1478
1565
  // ==================== 事件3:鼠标右键点击 ====================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huweili-cesium",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "基于 Cesium 的地图工具库(无人机态势、轨迹、围栏、工具栏等)",
5
5
  "type": "module",
6
6
  "main": "./index.js",