Files
map-demo/HelloWorld.vue
2026-03-06 06:03:12 +00:00

625 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="map-container">
<div ref="mapContainer" class="map"></div>
<!-- 摄像头监控弹窗 -->
<div v-if="showPopup && currentPopupType === 'camera'" class="camera-popup" :style="{ top: popupPosition.y + 'px', left: popupPosition.x + 'px' }">
<div class="popup-header">
<h3>{{ currentCamera.name }}</h3>
<div class="camera-status">
<span class="status-dot" :class="{ online: currentCamera.status === 'online' }"></span>
<span class="status-text">{{ currentCamera.status === 'online' ? '在线' : '离线' }}</span>
</div>
<button @click="closePopup" class="close-btn">×</button>
</div>
<div class="popup-content">
<div class="video-container">
<video
v-if="currentCamera.status === 'online'"
ref="cameraVideo"
:src="currentCamera.streamUrl"
autoplay
muted
controls
class="camera-video"
@error="handleVideoError"
>
您的浏览器不支持视频播放
</video>
<div v-else class="offline-placeholder">
<div class="offline-icon">📹</div>
<p>摄像头离线</p>
</div>
</div>
<!--
<div class="camera-controls">
<button @click="toggleCameraFullscreen" class="control-btn">
{{ isCameraFullscreen ? '退出全屏' : '全屏' }}
</button>
<button @click="takeScreenshot" class="control-btn">截图</button>
<button @click="toggleCameraAudio" class="control-btn">
{{ isAudioMuted ? '开启声音' : '静音' }}
</button>
</div> -->
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const mapContainer = ref(null)
let map = null
let floorsComp = null
// 从2.html中提取的配置参数
const mapConfig = {
verifyUrl: 'https://www.ooomap.com/ooomap-verify/check/50689bb01e0ac2a9d93bb18f1e1260f8',
appID: '87ae6a00e5ca4e33dd7e858a66b73475'
}
const initMap = () => {
if (!mapContainer.value) return
// 检查ooomap是否已加载
if (typeof window.om === 'undefined') {
console.error('ooomap SDK未加载请检查引入是否正确')
return
}
try {
// 初始化ooomap实例 - 使用2.html中的配置
map = new window.om.Map({
container: mapContainer.value,
verifyUrl: mapConfig.verifyUrl,
appID: mapConfig.appID
})
// 当建筑数据加载完成时, 将建筑的室外模型数据合入
map.on('buildingDataLoaded-pre', bd => {
// 注意:这里需要根据实际项目结构调整模型路径
// load local glb models
window.om.ajax({
url: './models/hospital/modelsData.json',
success: function (res) {
bd.models = res.models
}
})
})
// 加入楼层组件
if (typeof window.Comp_floors !== 'undefined') {
floorsComp = new window.Comp_floors({
target: mapContainer.value,
props: {
hasOutdoor: true,
style: 'right: 10px;bottom: 80px;'
}
})
floorsComp.bind(map)
}
// 窗口大小变化时调整地图视图
window.addEventListener('resize', handleResize)
// 地图加载完成回调
map.on('load', () => {
console.log('ooomap加载完成')
})
// 错误处理
map.on('error', (error) => {
console.error('ooomap加载错误:', error)
})
} catch (error) {
console.error('初始化ooomap失败:', error)
}
}
const handleResize = () => {
if (map && map.view) {
map.view.resize()
}
}
onMounted(() => {
// 动态加载ooomap SDK和相关组件
const scripts = [
'https://www.ooomap.com/sdk/dev/ooomap.min.js',
'https://www.ooomap.com/sdk/comps/comp_floors/comp_floors.js'
]
let loadedCount = 0
scripts.forEach(src => {
const script = document.createElement('script')
script.src = src
script.onload = () => {
loadedCount++
if (loadedCount === scripts.length) {
console.log('所有ooomap SDK加载完成')
initMap()
// 地图加载完成后添加标注点
addAnnotationsToMap()
// 添加地图点击事件监听
mapContainer.value.addEventListener('click', handleMapClick)
}
}
script.onerror = () => {
console.error(`ooomap SDK加载失败: ${src}`)
}
document.head.appendChild(script)
})
})
// 标注点数据
const annotations = [
// {
// id: 'reception',
// title: '接待大厅',
// description: '医院的主要接待区域,提供咨询、挂号等服务',
// area: '200平方米',
// capacity: '可容纳50人',
// function: '接待、咨询、挂号',
// position: { x: 100, y: 100 },
// image: '/images/reception.jpg'
// },
// {
// id: 'emergency',
// title: '急诊科',
// description: '24小时开放的急诊医疗服务区域',
// area: '150平方米',
// capacity: '可同时处理10名患者',
// function: '急诊医疗、急救处理',
// position: { x: 300, y: 200 },
// image: '/images/emergency.jpg'
// },
// {
// id: 'pharmacy',
// title: '药房',
// description: '药品发放和咨询服务区域',
// area: '80平方米',
// capacity: '日处理500张处方',
// function: '药品发放、用药咨询',
// position: { x: 500, y: 150 },
// image: '/images/pharmacy.jpg'
// },
// {
// id: 'surgery',
// title: '手术室',
// description: '高标准无菌手术区域',
// area: '120平方米',
// capacity: '可同时进行3台手术',
// function: '外科手术、微创手术',
// position: { x: 200, y: 400 },
// image: '/images/surgery.jpg'
// },
// {
// id: 'ward',
// title: '病房区',
// description: '患者住院治疗和休养区域',
// area: '300平方米',
// capacity: '可容纳30张病床',
// function: '住院治疗、护理服务',
// position: { x: 400, y: 350 },
// image: '/images/ward.jpg'
// }
]
// 摄像头数据
const cameras = [
{
id: 'camera4',
name: '手术室监控',
status: 'online',
streamUrl: 'https://example.com/stream/camera4', // 替换为实际视频流地址
location: '手术室外走廊',
model: 'Dahua IPC-HDBW5842R-ZE',
resolution: '8MP (3840×2160)',
viewAngle: '360°',
position: { x: 760, y: 320 },
type: 'camera'
},
{
id: 'camera5',
name: '病房区监控',
status: 'online',
streamUrl: 'https://example.com/stream/camera5', // 替换为实际视频流地址
location: '病房区走廊',
model: 'Hikvision DS-2CD2386G2-IU',
resolution: '8MP (3840×2160)',
viewAngle: '110°',
position: { x: 820, y: 330 },
type: 'camera'
}
]
// 弹窗状态
const showPopup = ref(false)
const popupPosition = ref({ x: 0, y: 0 })
const currentAnnotation = ref({})
const currentCamera = ref({})
const currentPopupType = ref('annotation') // 'annotation' 或 'camera'
// 显示区域信息弹窗
const showAnnotationPopup = (annotation, event) => {
currentAnnotation.value = annotation
currentPopupType.value = 'annotation'
// 设置弹窗位置(在点击位置附近)
const rect = mapContainer.value.getBoundingClientRect()
popupPosition.value = {
x: event.clientX - rect.left - 150, // 居中显示
y: event.clientY - rect.top + 20
}
// 确保弹窗不会超出地图容器
if (popupPosition.value.x < 10) popupPosition.value.x = 10
if (popupPosition.value.y < 10) popupPosition.value.y = 10
if (popupPosition.value.x > rect.width - 320) popupPosition.value.x = rect.width - 320
if (popupPosition.value.y > rect.height - 300) popupPosition.value.y = rect.height - 300
showPopup.value = true
}
// 显示摄像头弹窗
const showCameraPopup = (camera, event) => {
currentCamera.value = camera
currentPopupType.value = 'camera'
// 设置弹窗位置(在点击位置附近)
const rect = mapContainer.value.getBoundingClientRect()
popupPosition.value = {
x: event.clientX - rect.left - 150, // 居中显示
y: event.clientY - rect.top + 20
}
// 确保弹窗不会超出地图容器
if (popupPosition.value.x < 10) popupPosition.value.x = 10
if (popupPosition.value.y < 10) popupPosition.value.y = 10
if (popupPosition.value.x > rect.width - 320) popupPosition.value.x = rect.width - 320
if (popupPosition.value.y > rect.height - 300) popupPosition.value.y = rect.height - 300
showPopup.value = true
}
// 摄像头控制状态
// const isCameraFullscreen = ref(false)
// const isAudioMuted = ref(true)
const cameraVideo = ref(null)
// 添加标注点到地图
const addAnnotationsToMap = () => {
if (!map) return
// 添加区域标注点
annotations.forEach(annotation => {
// 创建一个虚拟的标注点元素
const markerElement = document.createElement('div')
markerElement.className = 'annotation-marker'
markerElement.innerHTML = `
<div class="marker-dot" style="width: 20px; height: 20px; background: #ff6b6b; border: 3px solid white; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.3);"></div>
<div class="marker-label" style="position: absolute; top: -25px; left: 50%; transform: translateX(-50%); background: #ff6b6b; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; white-space: nowrap;">${annotation.title}</div>
`
markerElement.style.position = 'absolute'
markerElement.style.left = annotation.position.x + 'px'
markerElement.style.top = annotation.position.y + 'px'
markerElement.style.zIndex = '1000'
markerElement.style.cursor = 'pointer'
// 添加点击事件
markerElement.addEventListener('click', (event) => {
event.stopPropagation()
showAnnotationPopup(annotation, event)
})
// 添加到地图容器
mapContainer.value.appendChild(markerElement)
})
// 添加摄像头标注点
cameras.forEach(camera => {
// 创建一个虚拟的摄像头标注点元素
const cameraElement = document.createElement('div')
cameraElement.className = 'camera-marker'
cameraElement.innerHTML = `
<div class="camera-icon" style="width: 24px; height: 24px; background: ${camera.status === 'online' ? '#4CAF50' : '#f44336'}; border: 3px solid white; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 12px; color: white;">📹</div>
<div class="camera-label" style="position: absolute; top: -30px; left: 50%; transform: translateX(-50%); background: ${camera.status === 'online' ? '#4CAF50' : '#f44336'}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; white-space: nowrap;">${camera.name}</div>
`
cameraElement.style.position = 'absolute'
cameraElement.style.left = camera.position.x + 'px'
cameraElement.style.top = camera.position.y + 'px'
cameraElement.style.zIndex = '1000'
cameraElement.style.cursor = 'pointer'
// 添加点击事件
cameraElement.addEventListener('click', (event) => {
event.stopPropagation()
showCameraPopup(camera, event)
})
// 添加到地图容器
mapContainer.value.appendChild(cameraElement)
})
}
// 摄像头相关方法
const closePopup = () => {
showPopup.value = false
currentPopupType.value = 'annotation'
currentAnnotation.value = {}
currentCamera.value = {}
}
// 点击地图其他地方关闭弹窗
const handleMapClick = () => {
if (showPopup.value) {
closePopup()
}
}
const handleVideoError = () => {
console.error('摄像头视频加载失败')
}
onUnmounted(() => {
// 清理事件监听器
window.removeEventListener('resize', handleResize)
// 清理地图点击事件监听
if (mapContainer.value) {
mapContainer.value.removeEventListener('click', handleMapClick)
}
// 清理组件实例
if (floorsComp) {
floorsComp.unbind && floorsComp.unbind()
floorsComp = null
}
// 清理地图实例
if (map) {
map.destroy && map.destroy()
map = null
}
})
</script>
<style scoped>
.map-container {
position: relative;
width: 100%;
height: 800px;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
}
.map {
width: 100%;
height: 100%;
}
.controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
gap: 10px;
}
.controls button {
padding: 8px 16px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.controls button:hover {
background: #f5f5f5;
border-color: #999;
}
.controls button:active {
background: #e5e5e5;
}
/* 标注点样式 */
.annotation-marker {
transition: transform 0.3s ease;
}
.annotation-marker:hover {
transform: scale(1.1);
}
.annotation-marker .marker-dot {
transition: all 0.3s ease;
}
.annotation-marker:hover .marker-dot {
background: #ff5252 !important;
box-shadow: 0 4px 12px rgba(255, 82, 82, 0.4) !important;
}
.annotation-marker .marker-label {
opacity: 0;
transition: opacity 0.3s ease;
}
.annotation-marker:hover .marker-label {
opacity: 1;
}
/* 摄像头标注点样式 */
.camera-marker {
transition: transform 0.3s ease;
}
.camera-marker:hover {
transform: scale(1.1);
}
.camera-marker .camera-icon {
transition: all 0.3s ease;
}
.camera-marker:hover .camera-icon {
transform: scale(1.2);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4) !important;
}
.camera-marker .camera-label {
opacity: 0;
transition: opacity 0.3s ease;
}
.camera-marker:hover .camera-label {
opacity: 1;
}
/* 摄像头弹窗样式 */
.camera-popup {
position: absolute;
width: 400px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
z-index: 2000;
animation: popupFadeIn 0.3s ease;
}
.camera-popup .popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
color: white;
border-radius: 12px 12px 0 0;
}
.camera-status {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #f44336;
}
.status-dot.online {
background: #4CAF50;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.status-text {
font-size: 12px;
font-weight: 500;
}
.camera-popup .popup-content {
padding: 20px;
}
.video-container {
width: 100%;
height: 200px;
background: #000;
border-radius: 8px;
overflow: hidden;
margin-bottom: 15px;
}
.camera-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.offline-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
}
.offline-icon {
font-size: 48px;
margin-bottom: 10px;
}
.camera-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 15px;
}
.info-item {
display: flex;
justify-content: space-between;
font-size: 14px;
}
.info-item .label {
color: #999;
font-weight: 500;
}
.info-item .value {
color: #333;
font-weight: 600;
}
.camera-controls {
display: flex;
gap: 100px;
justify-content: center;
}
.control-btn {
padding: 8px 16px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.3s ease;
}
.control-btn:hover {
background: #1976D2;
}
.control-btn:active {
background: #1565C0;
}
</style>