@yangyongtao/gaea 1.0.0

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.
@@ -0,0 +1,585 @@
1
+ <template>
2
+ <div class="fly-route-panel" v-drag>
3
+ <div class="panel-header title">
4
+ <span class="panel-title">沿线飞行</span>
5
+ <button class="close-btn" @click="handleClose">×</button>
6
+ </div>
7
+
8
+ <div class="panel-body">
9
+ <!-- 点位列表 -->
10
+ <div class="section" v-if="waypoints.length > 0">
11
+ <div class="section-header">
12
+ <h4>飞行点位 ({{ waypoints.length }})</h4>
13
+ <button class="btn-clear-all" @click="clearAllWaypoints" title="清空全部点位">
14
+ <i class="fas fa-trash-alt"></i> 清空
15
+ </button>
16
+ </div>
17
+ <div class="waypoint-list">
18
+ <div v-for="(wp, index) in waypoints" :key="index" class="waypoint-item"
19
+ :class="{ 'is-current': currentIndex === index }">
20
+ <div class="wp-index">{{ index + 1 }}</div>
21
+ <div class="wp-info">
22
+ <div class="wp-name">{{ wp.name }}</div>
23
+ <div class="wp-coords">
24
+ 经度 {{ wp.lng.toFixed(6) }} / 纬度 {{ wp.lat.toFixed(6) }} / 高度 {{ wp.alt.toFixed(1) }}m
25
+ </div>
26
+ </div>
27
+ <button class="wp-delete" @click="removeWaypoint(index)" title="删除">×</button>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="empty-tip" v-else>
33
+ 暂无飞行点位,请通过地图点击或手动输入添加
34
+ </div>
35
+
36
+ <!-- 新增点位 -->
37
+ <div class="form-section">
38
+ <h4>添加点位</h4>
39
+ <!-- 地图拾取模式开关 -->
40
+ <div class="pick-row">
41
+ <button class="btn" :class="isPicking ? 'btn-picking-active' : 'btn-picking'" @click="togglePickMode">
42
+ <i class="fas" :class="isPicking ? 'fa-check-circle' : 'fa-map-marker-alt'"></i>
43
+ {{ isPicking ? '拾取中(点击地图添加点位)...' : '地图拾取坐标' }}
44
+ </button>
45
+ </div>
46
+ <div class="form-row">
47
+ <div class="form-group">
48
+ <label>名称</label>
49
+ <input type="text" v-model="inputName" class="form-control" placeholder="点位名称" />
50
+ </div>
51
+ </div>
52
+ <div class="form-row">
53
+ <div class="form-group">
54
+ <label>经度</label>
55
+ <input type="number" v-model.number="inputLng" class="form-control" placeholder="点击地图或手动输入"
56
+ step="0.000001" />
57
+ </div>
58
+ <div class="form-group">
59
+ <label>纬度</label>
60
+ <input type="number" v-model.number="inputLat" class="form-control" placeholder="点击地图或手动输入"
61
+ step="0.000001" />
62
+ </div>
63
+ <div class="form-group">
64
+ <label>高度(m)</label>
65
+ <input type="number" v-model.number="inputAlt" class="form-control" placeholder="高度" />
66
+ </div>
67
+ </div>
68
+ <button class="btn btn-add" @click="addManualWaypoint">添加点位</button>
69
+ </div>
70
+
71
+ <!-- 飞行控制 -->
72
+ <div class="section" v-if="waypoints.length > 0">
73
+ <div class="btn-group">
74
+ <button class="btn btn-fly" @click="startFlight" :disabled="isFlying">
75
+ <i class="fas fa-plane"></i> {{ isFlying ? '飞行中...' : '开始飞行' }}
76
+ </button>
77
+ <button class="btn btn-default" @click="stopFlight" :disabled="!isFlying">
78
+ 停止
79
+ </button>
80
+ <button class="btn btn-end" v-if="isFlyOver" @click="endFlight">
81
+ 结束
82
+ </button>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </template>
88
+
89
+ <script setup>
90
+ import { ref, onBeforeUnmount } from 'vue';
91
+ import { GaeaApp } from '../GaeaMethods.js';
92
+
93
+ const props = defineProps({
94
+ appInstance: { type: Object, default: null }
95
+ });
96
+ const emit = defineEmits(['close', 'fly']);
97
+
98
+ // 使用传入的 appInstance 或创建默认实例
99
+ const APP = props.appInstance || (window.APP || new GaeaApp());
100
+
101
+ // 飞行点位列表
102
+ const waypoints = ref([]);
103
+ const currentIndex = ref(-1);
104
+ const isFlying = ref(false);
105
+ const isFlyOver = ref(false);
106
+
107
+ // 手动输入
108
+ const inputName = ref('');
109
+ const inputLng = ref(null);
110
+ const inputLat = ref(null);
111
+ const inputAlt = ref(500);
112
+
113
+ // 地图拾取模式
114
+ const isPicking = ref(false);
115
+ let canvasClickHandler = null;
116
+
117
+ // 点击地图获取经纬度(参照 GaeaMethods.addMouseUp 的实现)
118
+ const getLatLngFromEvent = (event) => {
119
+ try {
120
+ if (!window.Gaea || !window.Gaea.GaeaMath) return null;
121
+ const result = window.Gaea.GaeaMath.ProjectPosition(
122
+ new window.Gaea.Vector2(event.offsetX, event.offsetY)
123
+ );
124
+ const lonlat = window.Gaea.GaeaMath.CartesianToSphericalDeg(result);
125
+ return {
126
+ lat: lonlat.x, // x = 纬度
127
+ lng: lonlat.y, // y = 经度
128
+ alt: lonlat.z, // z = 高度
129
+ };
130
+ } catch (e) {
131
+ console.warn('地图拾取失败:', e);
132
+ return null;
133
+ }
134
+ };
135
+
136
+ // 切换地图拾取模式
137
+ const togglePickMode = () => {
138
+ if (!isPicking.value) {
139
+ // 开启拾取
140
+ isPicking.value = true;
141
+ const canvas = document.getElementById('canvas');
142
+ if (!canvas) return;
143
+
144
+ canvasClickHandler = (event) => {
145
+ const pos = getLatLngFromEvent(event);
146
+ if (pos) {
147
+ waypoints.value.push({
148
+ name: `点位 ${waypoints.value.length + 1}`,
149
+ lat: +pos.lat.toFixed(6),
150
+ lng: +pos.lng.toFixed(6),
151
+ alt: +pos.alt.toFixed(1) || 500,
152
+ });
153
+ }
154
+ };
155
+ canvas.addEventListener('mousedown', canvasClickHandler, false);
156
+ } else {
157
+ // 关闭拾取
158
+ removeCanvasListener();
159
+ isPicking.value = false;
160
+ }
161
+ };
162
+
163
+ const removeCanvasListener = () => {
164
+ if (canvasClickHandler) {
165
+ const canvas = document.getElementById('canvas');
166
+ if (canvas) {
167
+ canvas.removeEventListener('mousedown', canvasClickHandler, false);
168
+ }
169
+ canvasClickHandler = null;
170
+ }
171
+ };
172
+
173
+ // 手动添加点位
174
+ const addManualWaypoint = () => {
175
+ if (inputLng.value == null || inputLat.value == null) return;
176
+ const name = inputName.value || `点位 ${waypoints.value.length + 1}`;
177
+ waypoints.value.push({
178
+ name,
179
+ lat: inputLat.value,
180
+ lng: inputLng.value,
181
+ alt: inputAlt.value || 500,
182
+ });
183
+ inputName.value = '';
184
+ inputLng.value = null;
185
+ inputLat.value = null;
186
+ inputAlt.value = 500;
187
+ };
188
+
189
+ // 删除点位
190
+ const removeWaypoint = (index) => {
191
+ waypoints.value.splice(index, 1);
192
+ if (currentIndex.value >= waypoints.value.length) {
193
+ currentIndex.value = -1;
194
+ }
195
+ };
196
+
197
+ // 一键清空所有点位
198
+ const clearAllWaypoints = () => {
199
+ waypoints.value = [];
200
+ currentIndex.value = -1;
201
+ };
202
+
203
+ // 飞行
204
+ const startFlight = async () => {
205
+ if (waypoints.value.length < 2 || isFlying.value) return;
206
+
207
+ // 自动关闭地图拾取
208
+ removeCanvasListener();
209
+ isPicking.value = false;
210
+
211
+ isFlying.value = true;
212
+ isFlyOver.value = false;
213
+ currentIndex.value = 0;
214
+
215
+ try {
216
+ const lineData = waypoints.value.map(wp => ({
217
+ x: wp.lat,
218
+ y: wp.lng,
219
+ z: 150
220
+ }));
221
+
222
+ await APP.flyAlongPath(lineData, {
223
+ duration: 20.0,
224
+ xrotation: 30,
225
+ lockEye: false
226
+ });
227
+
228
+ isFlying.value = false;
229
+ isFlyOver.value = true;
230
+ currentIndex.value = -1;
231
+ } catch (e) {
232
+ console.error('飞行失败:', e);
233
+ isFlying.value = false;
234
+ isFlyOver.value = true;
235
+ currentIndex.value = -1;
236
+ }
237
+ };
238
+
239
+ const stopFlight = () => {
240
+ isFlying.value = false;
241
+ isFlyOver.value = true;
242
+ currentIndex.value = -1;
243
+ if (APP && APP.stopFlyAlongPath) {
244
+ APP.stopFlyAlongPath();
245
+ }
246
+ };
247
+
248
+ // 结束飞行:解锁视角 + 恢复定位 + 隐藏弹框
249
+ const endFlight = () => {
250
+ if (APP && APP.stopFlyAlongPath) {
251
+ APP.stopFlyAlongPath();
252
+ }
253
+ // 重置相机视角
254
+ if (APP && APP.cameraReset) {
255
+ APP.cameraReset();
256
+ }
257
+ isFlyOver.value = false;
258
+ isFlying.value = false;
259
+ currentIndex.value = -1;
260
+ emit('close');
261
+ };
262
+
263
+ const handleClose = () => {
264
+ removeCanvasListener();
265
+ stopFlight();
266
+ emit('close');
267
+ };
268
+
269
+ onBeforeUnmount(() => {
270
+ removeCanvasListener();
271
+ stopFlight();
272
+ });
273
+ </script>
274
+
275
+ <style lang="less" scoped>
276
+ .fly-route-panel {
277
+ position: absolute;
278
+ top: 100px;
279
+ left: 600px;
280
+ z-index: 2000;
281
+ width: 380px;
282
+ background: rgba(15, 23, 42, 0.95);
283
+ border-radius: 12px;
284
+ border: 1px solid rgba(59, 130, 246, 0.3);
285
+ overflow: hidden;
286
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
287
+ }
288
+
289
+ .panel-header {
290
+ display: flex;
291
+ justify-content: space-between;
292
+ align-items: center;
293
+ padding: 14px 16px;
294
+ background: rgba(30, 41, 59, 1);
295
+ border-bottom: 1px solid rgba(59, 130, 246, 0.2);
296
+ }
297
+
298
+ .panel-title {
299
+ font-size: 15px;
300
+ font-weight: 600;
301
+ color: #f1f5f9;
302
+ }
303
+
304
+ .close-btn {
305
+ width: 24px;
306
+ height: 24px;
307
+ border: none;
308
+ background: rgba(239, 68, 68, 0.2);
309
+ color: #ef4444;
310
+ border-radius: 4px;
311
+ cursor: pointer;
312
+ font-size: 16px;
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: center;
316
+ }
317
+
318
+ .close-btn:hover {
319
+ background: rgba(239, 68, 68, 0.3);
320
+ }
321
+
322
+ .panel-body {
323
+ padding: 16px;
324
+ }
325
+
326
+ .section {
327
+ margin-bottom: 16px;
328
+ }
329
+
330
+ .section h4 {
331
+ font-size: 13px;
332
+ font-weight: 500;
333
+ color: #cbd5e1;
334
+ margin: 0;
335
+ }
336
+
337
+ .section-header {
338
+ display: flex;
339
+ justify-content: space-between;
340
+ align-items: center;
341
+ margin-bottom: 8px;
342
+ }
343
+
344
+ .btn-clear-all {
345
+ padding: 2px 8px;
346
+ border: none;
347
+ background: rgba(239, 68, 68, 0.15);
348
+ color: #ef4444;
349
+ border-radius: 4px;
350
+ cursor: pointer;
351
+ font-size: 11px;
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 4px;
355
+ }
356
+
357
+ .btn-clear-all:hover {
358
+ background: rgba(239, 68, 68, 0.3);
359
+ }
360
+
361
+ .empty-tip {
362
+ text-align: center;
363
+ padding: 20px;
364
+ color: #64748b;
365
+ font-size: 13px;
366
+ }
367
+
368
+ .waypoint-list {
369
+ max-height: 200px;
370
+ overflow-y: auto;
371
+ }
372
+
373
+ .waypoint-item {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 10px;
377
+ padding: 10px 12px;
378
+ background: rgba(30, 41, 59, 0.6);
379
+ border-radius: 8px;
380
+ margin-bottom: 8px;
381
+ border: 1px solid transparent;
382
+ transition: border-color 0.2s;
383
+
384
+ &.is-current {
385
+ border-color: #3b82f6;
386
+ background: rgba(59, 130, 246, 0.1);
387
+ }
388
+ }
389
+
390
+ .wp-index {
391
+ width: 24px;
392
+ height: 24px;
393
+ border-radius: 50%;
394
+ background: #3b82f6;
395
+ color: #fff;
396
+ font-size: 12px;
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ flex-shrink: 0;
401
+ }
402
+
403
+ .wp-info {
404
+ flex: 1;
405
+ min-width: 0;
406
+ }
407
+
408
+ .wp-name {
409
+ font-size: 14px;
410
+ color: #f1f5f9;
411
+ font-weight: 500;
412
+ }
413
+
414
+ .wp-coords {
415
+ font-size: 11px;
416
+ color: #94a3b8;
417
+ margin-top: 2px;
418
+ white-space: nowrap;
419
+ overflow: hidden;
420
+ text-overflow: ellipsis;
421
+ }
422
+
423
+ .wp-delete {
424
+ width: 24px;
425
+ height: 24px;
426
+ border: none;
427
+ background: rgba(239, 68, 68, 0.15);
428
+ color: #ef4444;
429
+ border-radius: 4px;
430
+ cursor: pointer;
431
+ font-size: 14px;
432
+ flex-shrink: 0;
433
+ }
434
+
435
+ .wp-delete:hover {
436
+ background: rgba(239, 68, 68, 0.3);
437
+ }
438
+
439
+ .form-section {
440
+ margin-bottom: 16px;
441
+ padding: 12px;
442
+ background: rgba(30, 41, 59, 0.3);
443
+ border-radius: 8px;
444
+ }
445
+
446
+ .form-section h4 {
447
+ font-size: 13px;
448
+ font-weight: 500;
449
+ color: #cbd5e1;
450
+ margin-bottom: 8px;
451
+ }
452
+
453
+ .pick-row {
454
+ margin-bottom: 10px;
455
+ }
456
+
457
+ .form-group {
458
+ margin-bottom: 8px;
459
+ }
460
+
461
+ .form-group label {
462
+ display: block;
463
+ font-size: 12px;
464
+ color: #94a3b8;
465
+ margin-bottom: 4px;
466
+ }
467
+
468
+ .form-control {
469
+ width: 100%;
470
+ padding: 8px 10px;
471
+ background: rgba(30, 41, 59, 0.8);
472
+ border: 1px solid rgba(71, 85, 105, 0.5);
473
+ border-radius: 6px;
474
+ color: #f1f5f9;
475
+ font-size: 13px;
476
+ outline: none;
477
+ box-sizing: border-box;
478
+ }
479
+
480
+ .form-control:focus {
481
+ border-color: #3b82f6;
482
+ }
483
+
484
+ .form-row {
485
+ display: flex;
486
+ gap: 8px;
487
+ }
488
+
489
+ .form-row .form-group {
490
+ flex: 1;
491
+ }
492
+
493
+ .btn-group {
494
+ display: flex;
495
+ gap: 8px;
496
+ margin-bottom: 8px;
497
+ }
498
+
499
+ .btn {
500
+ flex: 1;
501
+ padding: 9px 10px;
502
+ border: none;
503
+ border-radius: 6px;
504
+ font-size: 13px;
505
+ font-weight: 500;
506
+ cursor: pointer;
507
+ transition: all 0.2s;
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: center;
511
+ gap: 6px;
512
+ }
513
+
514
+ .btn:disabled {
515
+ opacity: 0.5;
516
+ cursor: not-allowed;
517
+ }
518
+
519
+ .btn-default {
520
+ background: rgba(71, 85, 105, 0.3);
521
+ color: #94a3b8;
522
+ }
523
+
524
+ .btn-default:hover:not(:disabled) {
525
+ background: rgba(71, 85, 105, 0.5);
526
+ }
527
+
528
+ .btn-picking {
529
+ background: rgba(245, 158, 11, 0.2);
530
+ color: #f59e0b;
531
+ border: 1px solid rgba(245, 158, 11, 0.3);
532
+ }
533
+
534
+ .btn-picking:hover {
535
+ background: rgba(245, 158, 11, 0.3);
536
+ }
537
+
538
+ .btn-picking-active {
539
+ background: rgba(34, 197, 94, 0.3);
540
+ color: #22c55e;
541
+ border: 1px solid rgba(34, 197, 94, 0.5);
542
+ animation: pulse 1.5s ease-in-out infinite;
543
+ }
544
+
545
+ @keyframes pulse {
546
+
547
+ 0%,
548
+ 100% {
549
+ opacity: 1;
550
+ }
551
+
552
+ 50% {
553
+ opacity: 0.6;
554
+ }
555
+ }
556
+
557
+ .btn-add {
558
+ width: 100%;
559
+ background: rgba(34, 197, 94, 0.2);
560
+ color: #22c55e;
561
+ border: 1px solid rgba(34, 197, 94, 0.3);
562
+ }
563
+
564
+ .btn-add:hover {
565
+ background: rgba(34, 197, 94, 0.3);
566
+ }
567
+
568
+ .btn-fly {
569
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
570
+ color: #fff;
571
+ }
572
+
573
+ .btn-fly:hover:not(:disabled) {
574
+ background: linear-gradient(135deg, #0891b2, #0e7490);
575
+ }
576
+
577
+ .btn-end {
578
+ background: linear-gradient(135deg, #22c55e, #16a34a);
579
+ color: #fff;
580
+ }
581
+
582
+ .btn-end:hover {
583
+ background: linear-gradient(135deg, #16a34a, #15803d);
584
+ }
585
+ </style>