|
|
@@ -4,6 +4,7 @@ import * as THREE from 'three'
|
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
|
|
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'
|
|
|
import MarkerPopup from './MarkerPopup.vue'
|
|
|
+import { config } from '../config'
|
|
|
|
|
|
const props = defineProps({
|
|
|
metaUrl: { type: String, default: '/terrain-meta.json' },
|
|
|
@@ -24,11 +25,38 @@ const loading = ref(true)
|
|
|
const error = ref(null)
|
|
|
const veModel = ref(props.verticalExaggeration)
|
|
|
const infoText = ref('')
|
|
|
+const isAutoRotating = ref(false)
|
|
|
|
|
|
// 标记点相关
|
|
|
const activeMarker = ref(null)
|
|
|
const markerPopupPosition = ref({ x: 0, y: 0 })
|
|
|
const activeMarkerWorldPos = ref({ x: 0, y: 0, z: 0 })
|
|
|
+const realtimeMetrics = ref([]) // 实时获取的 metrics
|
|
|
+const loadingMetrics = ref(false) // 加载状态
|
|
|
+
|
|
|
+// 获取传感器实时数据
|
|
|
+async function fetchSensorData(nodeId) {
|
|
|
+ try {
|
|
|
+ const response = await fetch(config.getApiUrl('/service/page/sensor.json?paged=1&pageSize=50'), {
|
|
|
+ method: 'POST',
|
|
|
+ headers: config.getHeaders(),
|
|
|
+ body: JSON.stringify({ node_id: nodeId })
|
|
|
+ })
|
|
|
+
|
|
|
+ const result = await response.json()
|
|
|
+
|
|
|
+ if (result.status === 2 && result.data?.data) {
|
|
|
+ return result.data.data.map(sensor => ({
|
|
|
+ label: sensor.name,
|
|
|
+ value: sensor.sdata,
|
|
|
+ unit: sensor.measure_unit || ''
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取传感器数据失败:', error)
|
|
|
+ }
|
|
|
+ return []
|
|
|
+}
|
|
|
|
|
|
// iframe 消息处理
|
|
|
function setupIframeListener() {
|
|
|
@@ -216,6 +244,58 @@ function resetCamera() {
|
|
|
controls.update()
|
|
|
}
|
|
|
|
|
|
+// 加载完成后的旋转动效
|
|
|
+let autoRotateAnimationId = null
|
|
|
+function startAutoRotate() {
|
|
|
+ if (!camera || !controls || !meta) return
|
|
|
+
|
|
|
+ isAutoRotating.value = true
|
|
|
+ const duration = 6000 // 旋转持续时间 6秒
|
|
|
+ const startTime = performance.now()
|
|
|
+ const startAngle = Math.atan2(camera.position.x, camera.position.z)
|
|
|
+ const rotateAngle = Math.PI * 2 // 旋转完整一圈 360 度
|
|
|
+
|
|
|
+ const targetDistance = camera.position.distanceTo(controls.target)
|
|
|
+ const startDistance = targetDistance * 8 // 从 8 倍距离开始
|
|
|
+ const targetPitch = Math.asin(camera.position.y / targetDistance)
|
|
|
+ const startPitch = Math.PI / 2.5 // 从更高的俯视角度开始(约72度)
|
|
|
+
|
|
|
+ function animateRotate(currentTime) {
|
|
|
+ const elapsed = currentTime - startTime
|
|
|
+ const progress = Math.min(elapsed / duration, 1)
|
|
|
+
|
|
|
+ // 使用 easeOutCubic 缓动函数
|
|
|
+ const easeProgress = 1 - Math.pow(1 - progress, 3)
|
|
|
+
|
|
|
+ // 当前距离:从 startDistance 逐渐变化到 targetDistance
|
|
|
+ const currentDistance = startDistance + (targetDistance - startDistance) * easeProgress
|
|
|
+ // 当前俯仰角:从高俯视角逐渐变化到目标角度
|
|
|
+ const currentPitch = startPitch + (targetPitch - startPitch) * easeProgress
|
|
|
+
|
|
|
+ const currentAngle = startAngle + rotateAngle * easeProgress
|
|
|
+ camera.position.x = currentDistance * Math.cos(currentPitch) * Math.sin(currentAngle)
|
|
|
+ camera.position.z = currentDistance * Math.cos(currentPitch) * Math.cos(currentAngle)
|
|
|
+ camera.position.y = currentDistance * Math.sin(currentPitch)
|
|
|
+
|
|
|
+ if (progress < 1) {
|
|
|
+ autoRotateAnimationId = requestAnimationFrame(animateRotate)
|
|
|
+ } else {
|
|
|
+ isAutoRotating.value = false
|
|
|
+ autoRotateAnimationId = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ autoRotateAnimationId = requestAnimationFrame(animateRotate)
|
|
|
+}
|
|
|
+
|
|
|
+function stopAutoRotate() {
|
|
|
+ if (autoRotateAnimationId) {
|
|
|
+ cancelAnimationFrame(autoRotateAnimationId)
|
|
|
+ autoRotateAnimationId = null
|
|
|
+ }
|
|
|
+ isAutoRotating.value = false
|
|
|
+}
|
|
|
+
|
|
|
const TYPE_COLORS = {
|
|
|
video: 0x22c55e,
|
|
|
device: 0x38bdf8,
|
|
|
@@ -246,22 +326,8 @@ function createMarkerLabelElement(marker) {
|
|
|
white-space: nowrap;
|
|
|
pointer-events: auto;
|
|
|
cursor: pointer;
|
|
|
- transition: transform 0.15s, box-shadow 0.15s;
|
|
|
`
|
|
|
- div.innerHTML = `
|
|
|
- <svg width="14" height="16" viewBox="0 0 24 28" style="fill:${color};display:block">
|
|
|
- <path d="M12 0l10.4 6v12L12 24 1.6 18V6z"/>
|
|
|
- </svg>
|
|
|
- <span>${marker.title || '未命名'}</span>
|
|
|
- `
|
|
|
- div.addEventListener('mouseenter', () => {
|
|
|
- div.style.transform = 'scale(1.05)'
|
|
|
- div.style.boxShadow = `0 6px 20px ${color}55`
|
|
|
- })
|
|
|
- div.addEventListener('mouseleave', () => {
|
|
|
- div.style.transform = 'scale(1)'
|
|
|
- div.style.boxShadow = `0 4px 16px ${color}33`
|
|
|
- })
|
|
|
+ div.innerHTML = `<span>${marker.title || '未命名'}</span>`
|
|
|
div.addEventListener('click', (e) => {
|
|
|
e.stopPropagation()
|
|
|
openMarkerPopup(marker)
|
|
|
@@ -357,20 +423,32 @@ function createMarkers() {
|
|
|
scene.add(labelGroup)
|
|
|
}
|
|
|
|
|
|
-function openMarkerPopup(marker) {
|
|
|
+async function openMarkerPopup(marker) {
|
|
|
activeMarker.value = marker
|
|
|
+ realtimeMetrics.value = [] // 清空之前的实时数据
|
|
|
+ loadingMetrics.value = false
|
|
|
updatePopupPosition()
|
|
|
|
|
|
- sendToParent({
|
|
|
- type: 'markerClick',
|
|
|
- lon: marker.lon,
|
|
|
- lat: marker.lat,
|
|
|
- alt: marker.alt,
|
|
|
- title: marker.title,
|
|
|
- content: marker.content,
|
|
|
- markerType: marker.type
|
|
|
- })
|
|
|
+ // TODO: 通知父窗口功能暂时隐藏
|
|
|
+ // sendToParent({
|
|
|
+ // type: 'markerClick',
|
|
|
+ // lon: marker.lon,
|
|
|
+ // lat: marker.lat,
|
|
|
+ // alt: marker.alt,
|
|
|
+ // title: marker.title,
|
|
|
+ // content: marker.content,
|
|
|
+ // markerType: marker.type
|
|
|
+ // })
|
|
|
emit('markerClick', marker)
|
|
|
+
|
|
|
+ // 如果是设备类型,获取实时传感器数据
|
|
|
+ if (marker.type === 'device' && marker.nodeId) {
|
|
|
+ loadingMetrics.value = true
|
|
|
+ const sensorData = await fetchSensorData(marker.nodeId)
|
|
|
+ realtimeMetrics.value = sensorData
|
|
|
+ loadingMetrics.value = false
|
|
|
+ console.log('获取实时传感器数据:', sensorData)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 点击标记点
|
|
|
@@ -453,6 +531,9 @@ async function loadAll() {
|
|
|
|
|
|
buildMesh()
|
|
|
loading.value = false
|
|
|
+
|
|
|
+ // 加载完成后启动旋转动效
|
|
|
+ startAutoRotate()
|
|
|
} catch (err) {
|
|
|
console.error('加载失败:', err)
|
|
|
error.value = '加载失败: ' + (err.message || err)
|
|
|
@@ -510,6 +591,9 @@ onMounted(() => {
|
|
|
// 限制旋转角度,防止看到模型底部
|
|
|
controls.minPolarAngle = Math.PI / 6 // 最小俯仰角(30度)
|
|
|
controls.maxPolarAngle = Math.PI / 2 // 最大俯仰角(90度=垂直向下,不能翻转到底部)
|
|
|
+
|
|
|
+ // 用户交互时停止自动旋转
|
|
|
+ controls.addEventListener('start', stopAutoRotate)
|
|
|
|
|
|
const sun = new THREE.DirectionalLight(0xfff5e0, 2.0)
|
|
|
sun.position.set(400, 600, 300)
|
|
|
@@ -579,6 +663,8 @@ onUnmounted(() => {
|
|
|
window.removeEventListener('message', handleIframeMessage)
|
|
|
window.removeEventListener('resize', onResize)
|
|
|
cancelAnimationFrame(animId)
|
|
|
+ stopAutoRotate()
|
|
|
+ if (controls) controls.removeEventListener('start', stopAutoRotate)
|
|
|
renderer?.domElement?.removeEventListener('dblclick', onDblClick)
|
|
|
renderer?.domElement?.removeEventListener('click', onMarkerClick)
|
|
|
renderer?.dispose()
|
|
|
@@ -591,7 +677,7 @@ onUnmounted(() => {
|
|
|
<div ref="container" class="tv">
|
|
|
<div v-if="loading" class="ov ld">
|
|
|
<div class="sp"></div>
|
|
|
- <div style="margin-top:10px;color:#333">加载地形数据...</div>
|
|
|
+ <div style="margin-top:10px;color:#fff">加载地形数据...</div>
|
|
|
</div>
|
|
|
<div v-if="error" class="ov er">
|
|
|
<div style="font-size:36px">⚠️</div>
|
|
|
@@ -614,7 +700,7 @@ onUnmounted(() => {
|
|
|
@click.stop
|
|
|
>
|
|
|
<button class="popup-close" @click="activeMarker = null">×</button>
|
|
|
- <MarkerPopup :marker="activeMarker" />
|
|
|
+ <MarkerPopup :marker="activeMarker" :realtimeMetrics="realtimeMetrics" :loadingMetrics="loadingMetrics" />
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -629,9 +715,9 @@ onUnmounted(() => {
|
|
|
.b { background:rgba(0,0,0,0.06); border:1px solid rgba(0,0,0,0.12); padding:4px 12px; border-radius:4px; cursor:pointer; font-size:12px; color:#333; }
|
|
|
.b:hover { background:rgba(0,0,0,0.12); }
|
|
|
.ov { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:100; }
|
|
|
-.ld { background:rgba(255,255,255,0.85); }
|
|
|
-.er { background:rgba(255,255,255,0.85); }
|
|
|
-.sp { width:40px; height:40px; border:3px solid #ddd; border-top-color:#409eff; border-radius:50%; animation:sp 0.8s linear infinite; }
|
|
|
+.ld { background:rgba(0,0,0,0.95); }
|
|
|
+.er { background:rgba(0,0,0,0.95); }
|
|
|
+.sp { width:40px; height:40px; border:3px solid rgba(255,255,255,0.2); border-top-color:#409eff; border-radius:50%; animation:sp 0.8s linear infinite; }
|
|
|
@keyframes sp { to { transform:rotate(360deg) } }
|
|
|
|
|
|
/* 标记点弹窗 */
|