@yangyongtao/gaea 1.1.9 → 1.1.11

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,83 @@
1
+ <template>
2
+ <swiperSecAnimation
3
+ v-if="visible"
4
+ class="swiperSec"
5
+ :total="animationData.length"
6
+ :initialIndex="animationData.length - 1"
7
+ @index-change="handleIndexChange"
8
+ />
9
+ </template>
10
+
11
+ <script setup>
12
+ import { ref, onMounted, onBeforeUnmount } from 'vue';
13
+ import swiperSecAnimation from './swiperSecAnimation.vue';
14
+
15
+ const visible = ref(false);
16
+ const animationData = ref([]);
17
+ let currentIndex = 0;
18
+ let tourElement = null;
19
+
20
+ function handleIconClick(event) {
21
+ const detail = event.detail || event;
22
+ if (detail.category !== 'secData') return;
23
+ console.log('[SecTourView] secData event:', 'secAnimation:', !!detail.data?.secAnimation, 'tourModel:', !!detail.tourModel);
24
+ if (!detail.data?.secAnimation?.length || !detail.tourModel) {
25
+ console.warn('[SecTourView] 跳过:secAnimation或tourModel缺失');
26
+ return;
27
+ }
28
+ // 仅缓存数据,等模型锁定后再显示滚动条
29
+ animationData.value = detail.data.secAnimation;
30
+ tourElement = detail.tourModel;
31
+ currentIndex = animationData.value.length - 1;
32
+ visible.value = false;
33
+ }
34
+
35
+ function handleSecLocked() {
36
+ // 模型锁定后才显示断面滚动条
37
+ if (!tourElement || !animationData.value.length) return;
38
+ visible.value = true;
39
+ console.log('[SecTourView] locked, visible:', visible.value);
40
+ }
41
+
42
+ function handleIndexChange(newIndex) {
43
+ if (!tourElement || !animationData.value.length) return;
44
+ const oldIdx = currentIndex;
45
+ const arr = [animationData.value[oldIdx], animationData.value[newIndex]];
46
+ window.APP?.gtlfAnimationPlayer(arr, {
47
+ animationName: 'test' + currentIndex,
48
+ animationType: 'time',
49
+ animationPlaySpeed: 3,
50
+ animationLoop: false,
51
+ }, tourElement);
52
+ currentIndex = newIndex;
53
+ }
54
+
55
+ function handleReset() {
56
+ visible.value = false;
57
+ tourElement = null;
58
+ animationData.value = [];
59
+ }
60
+
61
+ onMounted(() => {
62
+ window.addEventListener('gaea-icon-click', handleIconClick);
63
+ window.addEventListener('gaea-sec-locked', handleSecLocked);
64
+ window.addEventListener('gaea-reset-view', handleReset);
65
+ });
66
+
67
+ onBeforeUnmount(() => {
68
+ window.removeEventListener('gaea-icon-click', handleIconClick);
69
+ window.removeEventListener('gaea-sec-locked', handleSecLocked);
70
+ window.removeEventListener('gaea-reset-view', handleReset);
71
+ });
72
+ </script>
73
+
74
+ <style>
75
+ .swiperSec {
76
+ position: absolute;
77
+ width: 40%;
78
+ bottom: 10%;
79
+ left: 50%;
80
+ transform: translateX(-50%);
81
+ z-index: 100;
82
+ }
83
+ </style>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div class="water-gate-tour-btn" @mouseenter="showMenu = true" @mouseleave="showMenu = false">
3
+ <button class="tour-btn">室内游览</button>
4
+ <div class="tour-menu" v-show="showMenu">
5
+ <div
6
+ v-for="gate in gates"
7
+ :key="gate"
8
+ class="tour-menu-item"
9
+ @click="startTour(gate)"
10
+ >{{ gate }}</div>
11
+ </div>
12
+ </div>
13
+
14
+ <!-- 路径节点组件(挂在外层,避免受按钮 hover 影响) -->
15
+ <pathChart
16
+ v-if="tourActive"
17
+ class="tourPath"
18
+ :locations="positionList"
19
+ :currentIndex="currentIndex"
20
+ @node-click="handleNodeClick"
21
+ direction="horizontal"
22
+ />
23
+ </template>
24
+
25
+ <script setup>
26
+ import { ref, onMounted, onBeforeUnmount } from 'vue';
27
+ import pathChart from './pathChart.vue';
28
+
29
+ const gates = ['沿浦水闸', '联盟水闸', '下在水闸', '岭尾新闸', '岭尾水闸'];
30
+ const showMenu = ref(false);
31
+ const tourActive = ref(false);
32
+ const positionList = ref([]);
33
+ const currentIndex = ref(0);
34
+
35
+ function startTour(gate) {
36
+ const app = window.APP;
37
+ if (!app) return;
38
+ const cfg = app._tourConfig?.[gate];
39
+ if (!cfg) return;
40
+ app.initModelTour(gate, cfg.direction);
41
+ positionList.value = cfg.positionList || [];
42
+ currentIndex.value = 0;
43
+ tourActive.value = positionList.value.length > 0;
44
+ showMenu.value = false;
45
+ }
46
+
47
+ function handleNodeClick(index) {
48
+ window.APP?.tourNodeClick(positionList.value, index);
49
+ currentIndex.value = index;
50
+ }
51
+
52
+ function handleReset() {
53
+ tourActive.value = false;
54
+ positionList.value = [];
55
+ currentIndex.value = 0;
56
+ }
57
+
58
+ onMounted(() => {
59
+ window.addEventListener('gaea-reset-view', handleReset);
60
+ });
61
+
62
+ onBeforeUnmount(() => {
63
+ window.removeEventListener('gaea-reset-view', handleReset);
64
+ });
65
+ </script>
66
+
67
+ <style scoped>
68
+ .water-gate-tour-btn {
69
+ position: relative;
70
+ display: inline-block;
71
+ }
72
+
73
+ .tour-btn {
74
+ padding: 8px 16px;
75
+ border: none;
76
+ border-radius: 6px;
77
+ background: #3b82f6;
78
+ color: #fff;
79
+ cursor: pointer;
80
+ font-size: 14px;
81
+ }
82
+
83
+ .tour-btn:hover {
84
+ background: #2563eb;
85
+ }
86
+
87
+ .tour-menu {
88
+ position: absolute;
89
+ top: 100%;
90
+ left: 0;
91
+ padding-top: 4px;
92
+ background: transparent;
93
+ min-width: 120px;
94
+ z-index: 1000;
95
+ }
96
+
97
+ .tour-menu-item {
98
+ padding: 8px 16px;
99
+ color: #fff;
100
+ font-size: 14px;
101
+ cursor: pointer;
102
+ white-space: nowrap;
103
+ background: rgba(1, 79, 152, 0.95);
104
+ transition: background 0.15s;
105
+ }
106
+
107
+ .tour-menu-item:first-child { border-radius: 6px 6px 0 0; }
108
+ .tour-menu-item:last-child { border-radius: 0 0 6px 6px; }
109
+
110
+ .tour-menu-item:hover {
111
+ background: #007ffd;
112
+ }
113
+
114
+ .tourPath {
115
+ position: fixed;
116
+ bottom: 30px;
117
+ left: 50%;
118
+ transform: translateX(-50%);
119
+ z-index: 1000;
120
+ }
121
+ </style>
@@ -239,7 +239,7 @@ const loadTyphoonRoute = async () => {
239
239
  typhoonPicking.value = false;
240
240
  typhoonLoading.value = true;
241
241
  try {
242
- await APP.addPointIcons(typhoonPoints.value);
242
+ await APP.addTyphoonRoute(typhoonPoints.value);
243
243
  } catch (error) {
244
244
  console.error('加载台风路线错误:', error);
245
245
  } finally {
@@ -266,7 +266,7 @@ onBeforeUnmount(() => {
266
266
  position: absolute;
267
267
  top: 100px;
268
268
  left: 600px;
269
- z-index: 2000;
269
+ z-index: 10000;
270
270
  width: 300px;
271
271
  background: rgba(15, 23, 42, 0.95);
272
272
  border-radius: 12px;
@@ -1,2 +1,7 @@
1
1
  export { default as WeatherControl } from './WeatherControl.vue';
2
2
  export { default as FlyAlongRoute } from './FlyAlongRoute.vue';
3
+ export { default as LayerTree } from './LayerTree.vue';
4
+ export { default as CommonDialog } from './CommonDialog.vue';
5
+ export { default as HydrologicStationDialog } from './HydrologicStationDialog.vue';
6
+ export { default as SecTourView } from './SecTourView.vue';
7
+ export { default as WaterGateTour } from './WaterGateTour.vue';
@@ -0,0 +1,282 @@
1
+ <template>
2
+ <!-- 路径容器:支持横向/纵向布局 -->
3
+ <div
4
+ class="location-path-container"
5
+ :class="{ 'vertical': direction === 'vertical' }"
6
+ :style="{ width: width, height: direction === 'vertical' ? height : 'auto' }"
7
+ >
8
+ <!-- 横向布局 -->
9
+ <div class="path-wrapper-horizontal" v-if="direction === 'horizontal'">
10
+ <template v-for="(location, index) in locations" :key="index">
11
+ <!-- 单个地点项(名称+节点) -->
12
+ <div class="location-item">
13
+ <div
14
+ class="location-name"
15
+ :class="{ 'active-name': index === innerCurrentIndex }"
16
+ >
17
+ {{ location.name }}
18
+ </div>
19
+ <!-- 节点容器:固定尺寸,确保圆心位置可计算 -->
20
+ <div class="node-wrapper">
21
+ <div
22
+ class="location-node"
23
+ :class="{ 'active': index === innerCurrentIndex }"
24
+ :style="{
25
+ width: `${nodeSize}px`,
26
+ height: `${nodeSize}px`,
27
+ borderColor: lineColor,
28
+ backgroundColor: index === innerCurrentIndex ? activeColor : nodeColor
29
+ }"
30
+ @click="handleNodeClick(index)"
31
+ >
32
+ <span class="node-content">
33
+ {{ location.icon || (index + 1) }}
34
+ </span>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ <!-- 横向连线:精准对齐节点圆心 + 整体下移 -->
39
+ <div
40
+ class="path-line line-horizontal"
41
+ v-if="index < locations.length - 1"
42
+ :style="{
43
+ background: lineColor,
44
+ width: `${lineLength}px`,
45
+ height: `${lineWidth}px`,
46
+ }"
47
+ ></div>
48
+ </template>
49
+ </div>
50
+
51
+ <!-- 纵向布局 -->
52
+ <div class="path-wrapper-vertical" v-else>
53
+ <template v-for="(location, index) in locations" :key="index">
54
+ <!-- 单个地点项(名称+节点) -->
55
+ <div class="location-item">
56
+ <div
57
+ class="location-name"
58
+ :class="{ 'active-name': index === innerCurrentIndex }"
59
+ >
60
+ {{ location.name }}
61
+ </div>
62
+ <!-- 节点容器:固定尺寸 -->
63
+ <div class="node-wrapper">
64
+ <div
65
+ class="location-node"
66
+ :class="{ 'active': index === innerCurrentIndex }"
67
+ :style="{
68
+ width: `${nodeSize}px`,
69
+ height: `${nodeSize}px`,
70
+ borderColor: lineColor,
71
+ backgroundColor: index === innerCurrentIndex ? activeColor : nodeColor
72
+ }"
73
+ @click="handleNodeClick(index)"
74
+ >
75
+ <span class="node-content">
76
+ {{ location.icon || (index + 1) }}
77
+ </span>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ <!-- 纵向连线:精准对齐节点圆心 + 可选下移 -->
82
+ <div
83
+ class="path-line line-vertical"
84
+ v-if="index < locations.length - 1"
85
+ :style="{
86
+ background: lineColor,
87
+ height: `${lineLength}px`,
88
+ width: `${lineWidth}px`,
89
+ marginLeft: `calc(((${nodeSize} / 2) - (${lineWidth} / 2)))`,
90
+ transform: `translateY(${lineOffset}px)`
91
+ }"
92
+ ></div>
93
+ </template>
94
+ </div>
95
+ </div>
96
+ </template>
97
+
98
+ <script setup>
99
+ import { defineProps, defineEmits, ref, watch } from 'vue';
100
+
101
+ const emit = defineEmits(['update:currentIndex', 'node-click']);
102
+
103
+ // 组件属性
104
+ const props = defineProps({
105
+ locations: {
106
+ type: Array,
107
+ default: () => [
108
+ { name: '起点' },
109
+ { name: 'A地点' },
110
+ { name: 'B地点' },
111
+ { name: 'C地点' },
112
+ { name: '终点' }
113
+ ]
114
+ },
115
+ currentIndex: {
116
+ type: Number,
117
+ default: 0
118
+ },
119
+ direction: {
120
+ type: String,
121
+ default: 'horizontal',
122
+ validator: (val) => ['horizontal', 'vertical'].includes(val)
123
+ },
124
+ width: { type: String, default: '100%' },
125
+ height: { type: String, default: '400px' },
126
+ nodeSize: { type: Number, default: 32 },
127
+ nodeColor: { type: String, default: '#E5E7EB' },
128
+ activeColor: { type: String, default: '#409EFF' },
129
+ lineColor: { type: String, default: '#409EFF' },
130
+ lineWidth: { type: Number, default: 2 },
131
+ lineLength: { type: Number, default: 40 },
132
+ activeTextColor: { type: String, default: '#409EFF' },
133
+ lineOffset: { type: Number, default: 0 }
134
+ });
135
+
136
+ // 内部响应式选中索引(核心:模板直接用这个变量)
137
+ const innerCurrentIndex = ref(props.currentIndex);
138
+
139
+ // 监听父组件props变化,同步到内部
140
+ watch(() => props.currentIndex, (newVal) => {
141
+ innerCurrentIndex.value = newVal;
142
+ }, { immediate: true });
143
+
144
+ // 监听内部选中状态变化,通知父组件
145
+ watch(innerCurrentIndex, (newVal) => {
146
+ emit('update:currentIndex', newVal);
147
+ });
148
+
149
+ // 节点点击事件(添加日志,方便调试)
150
+ const handleNodeClick = (index) => {
151
+ console.log('点击了节点索引:', index); // 调试用,可删除
152
+ innerCurrentIndex.value = index;
153
+ emit('node-click', index, props.locations[index]);
154
+ };
155
+ </script>
156
+
157
+ <style scoped>
158
+ /* 容器基础样式 */
159
+ .location-path-container {
160
+ padding: 20px;
161
+ box-sizing: border-box;
162
+ font-family: sans-serif;
163
+ position: absolute;
164
+ bottom: 120px;
165
+ }
166
+
167
+ /* ---------------- 横向布局核心样式 ---------------- */
168
+ .path-wrapper-horizontal {
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ flex-wrap: wrap;
173
+ gap: 0;
174
+ }
175
+
176
+ .path-wrapper-horizontal .location-item {
177
+ display: flex;
178
+ flex-direction: column;
179
+ align-items: center;
180
+ width: 3rem;
181
+ }
182
+
183
+ .path-wrapper-horizontal .node-wrapper {
184
+ width: v-bind(nodeSize + 'px');
185
+ height: v-bind(nodeSize + 'px');
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ }
190
+
191
+ .line-horizontal {
192
+ margin: 0;
193
+ align-self: center;
194
+ transform: translate(0, 0.625rem);
195
+ }
196
+
197
+ /* ---------------- 纵向布局核心样式 ---------------- */
198
+ .path-wrapper-vertical {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: 0;
202
+ }
203
+
204
+ .path-wrapper-vertical .location-item {
205
+ display: flex;
206
+ flex-direction: column;
207
+ align-items: flex-start;
208
+ width: 3rem;
209
+ }
210
+
211
+ .path-wrapper-vertical .node-wrapper {
212
+ width: v-bind(nodeSize + 'px');
213
+ height: v-bind(nodeSize + 'px');
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ }
218
+
219
+ .line-vertical {
220
+ margin: 0 0 0 calc((v-bind(nodeSize) / 2) - (v-bind(lineWidth) / 2));
221
+ }
222
+
223
+ /* ---------------- 通用样式 ---------------- */
224
+ .location-name {
225
+ font-size: 14px;
226
+ color: #666;
227
+ margin-bottom: 8px;
228
+ transition: color 0.2s ease;
229
+ white-space: nowrap;
230
+ text-align: center;
231
+ }
232
+ .path-wrapper-vertical .location-name {
233
+ text-align: left;
234
+ }
235
+
236
+ /* 强化选中名称样式优先级 */
237
+ .location-name.active-name {
238
+ color: v-bind(activeTextColor) !important;
239
+ font-weight: 600 !important;
240
+ }
241
+
242
+ /* 地点节点(强化样式优先级) */
243
+ .location-node {
244
+ border-radius: 50%;
245
+ border: 2px solid !important; /* 强制生效 */
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ color: #fff !important; /* 强制生效 */
250
+ font-size: 14px;
251
+ font-weight: 600;
252
+ z-index: 2;
253
+ transition: all 0.2s ease;
254
+ margin: 0;
255
+ padding: 0;
256
+ cursor: pointer;
257
+ }
258
+
259
+ /* 强化选中节点样式,确保覆盖默认样式 */
260
+ .location-node.active {
261
+ background-color: v-bind(activeColor) !important;
262
+ border-color: v-bind(activeColor) !important;
263
+ box-shadow: 0 0 0 4px rgba(64, 158, 255, 0.2) !important;
264
+ }
265
+
266
+ .location-node:hover {
267
+ transform: scale(1.1);
268
+ }
269
+
270
+ /* 未选中节点样式(兜底) */
271
+ .location-node:not(.active) {
272
+ background-color: v-bind(nodeColor) !important;
273
+ border-color: v-bind(lineColor) !important;
274
+ }
275
+
276
+ .path-line {
277
+ transition: opacity 0.2s ease;
278
+ }
279
+ .path-line:hover {
280
+ opacity: 0.8;
281
+ }
282
+ </style>