Compare commits

..

2 Commits

Author SHA1 Message Date
Zzc
a1397c591f feat(map): 为地图标记添加交互式工具提示
- 实现 `useMapTooltip` 可组合函数以管理工具提示状态
- 添加点击处理程序,在标记交互时显示工具提示
- 将标记高度偏移调整为 100 米以保持一致性
- 强制场景渲染,确保 `CLAMP_TO_GROUND` 效果立即生效
2025-11-19 11:13:29 +08:00
Zzc
f60ecb002c feat(video): 将实际视频播放与OSS源集成
向VideoMonitorItem组件添加视频元素以实现实时播放,更新视频监视器常量以使用OSS URL,并实现从OSS配置生成资源URL的工具函数。这将占位符内容替换为功能性的视频流。
2025-11-19 11:12:18 +08:00
7 changed files with 365 additions and 36 deletions

View File

@ -11,7 +11,15 @@
<div class="video-monitor-item__content">
<div class="video-placeholder">
<!-- 这里放置实际的视频流组件 -->
<!-- 视频播放器 -->
<video
:src="monitor.videoSrc"
autoplay
loop
muted
playsinline
/>
<div class="video-time">{{ currentTime }}</div>
<!-- 控制条叠加在视频底部 -->
@ -164,17 +172,15 @@ onUnmounted(() => {
overflow: hidden;
// margin-bottom: vh(12);
//
&::before {
content: '';
//
video {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: vw(48);
height: vh(48);
background: url(../../assets/images/SketchPngb3b734375de691a8ba794eee7807988d78f942877ab220ebea0aac3bbddccd8b.png) center/contain no-repeat;
opacity: 0.3;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.video-time {

View File

@ -7,8 +7,8 @@ import soldierIcon from '../assets/images/SketchPngfbec927027ff9e49207749ebaafd2
import deviceIcon from '../assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png'
import emergencyBaseIcon from '../assets/images/应急基地.png'
// 默认高度偏移(米)
const DEFAULT_HEIGHT_OFFSET = 10
// 默认高度偏移(米)- 与 WuRenJi 保持一致
const DEFAULT_HEIGHT_OFFSET = 100
/**
* 地图标记管理 Composable
@ -136,9 +136,18 @@ export function useMapMarkers() {
// 计算中心点
const center = Cesium.BoundingSphere.fromPoints(positions).center
// 将中心点转换为经纬度然后重新创建位置确保高度为0
// 这样 CLAMP_TO_GROUND 才能正确工作
const centerCartographic = Cesium.Cartographic.fromCartesian(center)
const centerPosition = Cesium.Cartesian3.fromRadians(
centerCartographic.longitude,
centerCartographic.latitude,
0 // 高度设为0让 CLAMP_TO_GROUND 自动贴地
)
// 添加标签
const labelEntity = viewer.entities.add({
position: center,
position: centerPosition,
label: {
text: '模拟塌陷区域',
font: '18px "Microsoft YaHei", sans-serif',
@ -158,7 +167,7 @@ export function useMapMarkers() {
// 添加中心点标记
const pointEntity = viewer.entities.add({
position: center,
position: centerPosition,
point: {
color: Cesium.Color.ORANGE,
pixelSize: 12,
@ -171,6 +180,10 @@ export function useMapMarkers() {
collapseAreaEntities.value = entities
console.log('[useMapMarkers] 塌陷区域绘制完成')
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
viewer.scene.requestRender()
return center
}
@ -291,6 +304,10 @@ export function useMapMarkers() {
markerEntities.value.push(...entities)
console.log(`[useMapMarkers] 添加固定标记 ${entities.length}`)
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
viewer.scene.requestRender()
return entities
}
@ -387,6 +404,10 @@ export function useMapMarkers() {
markerEntities.value.push(...entities)
console.log(`[useMapMarkers] 添加随机标记 ${entities.length}`)
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
viewer.scene.requestRender()
return entities
}
@ -627,6 +648,11 @@ export function useMapMarkers() {
emergencyResourceEntities.value = entities
console.log(`[useMapMarkers] 添加养护站标记 ${entities.length}`)
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
if (entities.length > 0) {
viewer.scene.requestRender()
}
}
return {

View File

@ -0,0 +1,63 @@
import { ref } from 'vue'
/**
* 地图 Tooltip 状态管理
* 用于显示地图标记点的轻量级信息提示框
*/
export function useMapTooltip() {
// Tooltip 状态
const tooltipState = ref({
visible: false,
x: 0,
y: 0,
title: '',
icon: '',
zIndex: 20,
data: null // 业务数据,用于内容插槽渲染
})
/**
* 显示 Tooltip
* @param {Object} options - Tooltip 配置选项
* @param {number} options.x - 屏幕 X 坐标像素
* @param {number} options.y - 屏幕 Y 坐标像素
* @param {string} [options.title=''] - Tooltip 标题文本
* @param {string} [options.icon=''] - 标题左侧图标的图片路径
* @param {Object} [options.data=null] - 业务数据
*/
const showTooltip = ({ x, y, title = '', icon = '', data = null }) => {
tooltipState.value = {
visible: true,
x,
y,
title,
icon,
zIndex: 20,
data
}
}
/**
* 隐藏 Tooltip
*/
const hideTooltip = () => {
tooltipState.value.visible = false
}
/**
* 更新 Tooltip 位置
* @param {number} x - 屏幕 X 坐标
* @param {number} y - 屏幕 Y 坐标
*/
const updateTooltipPosition = (x, y) => {
tooltipState.value.x = x
tooltipState.value.y = y
}
return {
tooltipState,
showTooltip,
hideTooltip,
updateTooltipPosition
}
}

View File

@ -2,6 +2,8 @@
* 3D态势感知常量配置
*/
import { getVideoUrl } from '@shared/utils'
// 视频监控视角类型
export const VIDEO_TYPES = {
PERSONNEL: 'personnel', // 单兵视角
@ -16,7 +18,8 @@ export const VIDEO_MONITORS = [
id: 1,
type: VIDEO_TYPES.PERSONNEL,
title: '单兵(张三三)设备视角',
videoSrc: '/videos/personnel-001.mp4', // 视频源路径
// videoSrc: getVideoUrl('demo/ylzg/单兵视角.mp4'), // 从 OSS 获取视频 URL
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/单兵视角.mp4',
dateRange: '2025/9/1-2025/12/1', // 日期范围
hasAudio: true,
hasMegaphone: true,
@ -27,7 +30,8 @@ export const VIDEO_MONITORS = [
id: 2,
type: VIDEO_TYPES.DRONE,
title: '无人机(001)视角',
videoSrc: '/videos/drone-001.mp4',
// videoSrc: getVideoUrl('demo/ylzg/无人机视角.mp4'), // 从 OSS 获取视频 URL
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/无人机视角.mp4',
dateRange: '2025/9/1-2025/12/1',
hasAudio: false,
hasMegaphone: true,
@ -38,7 +42,8 @@ export const VIDEO_MONITORS = [
id: 3,
type: VIDEO_TYPES.VEHICLE_EXTERNAL,
title: '指挥车外部视角',
videoSrc: '/videos/vehicle-external-001.mp4',
// videoSrc: getVideoUrl('demo/ylzg/单兵视角.mp4'), // 暂时使用单兵视角视频
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/单兵视角.mp4',
dateRange: '2025/9/1-2025/12/1',
hasAudio: true,
hasMegaphone: true,
@ -49,7 +54,8 @@ export const VIDEO_MONITORS = [
id: 4,
type: VIDEO_TYPES.VEHICLE_MEETING,
title: '指挥车会议视角',
videoSrc: '/videos/vehicle-meeting-001.mp4',
// videoSrc: getVideoUrl('demo/ylzg/无人机视角.mp4'), // 暂时使用无人机视角视频
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/无人机视角.mp4',
dateRange: '2025/9/1-2025/12/1',
hasAudio: true,
hasMegaphone: true,

View File

@ -111,11 +111,15 @@ import { useDisasterData } from "./composables/useDisasterData";
import { useDualMapCompare } from "./composables/useDualMapCompare";
import { useMapMarkers } from "./composables/useMapMarkers";
import { use3DTiles } from "./composables/use3DTiles";
import { useMapTooltip } from "./composables/useMapTooltip";
import { useMapStore } from "@/map";
import { request } from "@shared/utils/request";
//
//
import emergencyCenterIcon from "./assets/images/应急中心.png";
import soldierIcon from "./assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png";
import deviceIcon from "./assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png";
import emergencyBaseIcon from "./assets/images/应急基地.png";
// 使
const disasterData = useDisasterData();
@ -150,9 +154,182 @@ const {
clearEmergencyResourceMarkers,
} = useMapMarkers();
// Tooltip
const { tooltipState: mapTooltip, showTooltip, hideTooltip, updateTooltipPosition } = useMapTooltip();
// tooltip
const currentTooltipEntity = ref(null);
// 3D Tiles
const { load3DTileset, waitForTilesetReady } = use3DTiles();
/**
* 设置地图点击事件处理器
* 当用户点击地图标记点时显示 Tooltip
*/
const setupMapClickHandler = (viewer) => {
if (!viewer) return;
// ScreenSpaceEventHandler
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((click) => {
//
const pickedObject = viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
const entity = pickedObject.id;
// properties
if (entity.properties) {
const type = entity.properties.type?.getValue();
// Tooltip
if (type === 'soldier') {
showMarkerTooltip(viewer, entity, click.position, soldierIcon);
} else if (type === 'device') {
showMarkerTooltip(viewer, entity, click.position, deviceIcon);
} else if (type === 'emergencyBase' || type === 'station') {
showMarkerTooltip(viewer, entity, click.position, emergencyBaseIcon);
}
}
} else {
// Tooltip
hideTooltip();
currentTooltipEntity.value = null;
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// tooltip
viewer.scene.postRender.addEventListener(() => {
if (currentTooltipEntity.value && mapTooltip.value.visible) {
updateTooltipPositionForEntity(viewer, currentTooltipEntity.value);
}
});
};
/**
* 显示标记点 Tooltip
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 被点击的实体
* @param {Cesium.Cartesian2} screenPosition - 点击的屏幕坐标备用
* @param {string} icon - 图标路径
*/
const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
const properties = entity.properties;
const type = properties.type?.getValue();
// 3D
const position = entity.position?.getValue(Cesium.JulianDate.now());
if (!position) {
console.warn('[Tooltip] 无法获取实体位置');
return;
}
// 使 CLAMP_TO_GROUND billboard
const cartographic = Cesium.Cartographic.fromCartesian(position);
let clampedPosition = position;
// 使 globe.getHeight
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic);
if (Cesium.defined(height)) {
cartographic.height = height;
clampedPosition = Cesium.Cartographic.toCartesian(cartographic);
}
}
// 3D
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition);
if (!Cesium.defined(canvasPosition)) {
console.warn('[Tooltip] 无法转换坐标到屏幕位置');
return;
}
// Tooltip
let title = '';
const fields = [];
if (type === 'soldier') {
title = '单兵信息';
fields.push(
{ label: '姓名', value: properties.name?.getValue() || '-' },
{ label: '部门', value: properties.department?.getValue() || '-' },
{ label: '位置', value: properties.location?.getValue() || '-' }
);
} else if (type === 'device') {
title = '设备信息';
fields.push(
{ label: '设备名称', value: properties.name?.getValue() || '-' },
{ label: '设备类型', value: properties.deviceType?.getValue() || '-' },
{ label: '位置', value: properties.location?.getValue() || '-' }
);
} else if (type === 'emergencyBase') {
title = '应急基地';
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '地址', value: properties.address?.getValue() || '-' },
{ label: '距离', value: properties.distance?.getValue() || '-' }
);
} else if (type === 'station') {
title = '养护站';
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '距离', value: `${properties.distance?.getValue() || 0}公里` }
);
}
// Tooltip使
showTooltip({
x: canvasPosition.x,
y: canvasPosition.y,
title,
icon,
data: { fields }
});
//
currentTooltipEntity.value = entity;
};
/**
* 更新 Tooltip 位置当相机移动时
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 实体对象
*/
const updateTooltipPositionForEntity = (viewer, entity) => {
const position = entity.position?.getValue(Cesium.JulianDate.now());
if (!position) return;
// 使 CLAMP_TO_GROUND billboard
const cartographic = Cesium.Cartographic.fromCartesian(position);
let clampedPosition = position;
// 使 globe.getHeight
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic);
if (Cesium.defined(height)) {
cartographic.height = height;
clampedPosition = Cesium.Cartographic.toCartesian(cartographic);
}
}
// 3D
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition);
// tooltip
if (!Cesium.defined(canvasPosition)) {
hideTooltip();
currentTooltipEntity.value = null;
return;
}
updateTooltipPosition(canvasPosition.x, canvasPosition.y);
};
//
onMounted(() => {
//
@ -197,6 +374,10 @@ onMounted(() => {
...DEFAULT_CAMERA_VIEW,
duration: 1,
});
// - Tooltip
setupMapClickHandler(viewer);
// 1000ms
// setTimeout(() => {
// camera.flyTo({
@ -204,7 +385,7 @@ onMounted(() => {
// duration: 1,
// });
// }, 5000);
return;
// return;
/**
* 设置相机到指定的笛卡尔坐标
@ -265,7 +446,7 @@ onMounted(() => {
console.log("[index.vue] 开始初始化地图标记...");
const sampledCollapseCenter = await initializeMarkers(viewer, {
useSampledHeights: true, // 使
heightOffset: 10, // 10
heightOffset: 100, // 100 WuRenJi
});
//
@ -409,20 +590,6 @@ const selectedCenter = ref({
image: null,
});
/**
* 地图 Tooltip 状态管理
* 用于显示地图标记点的轻量级信息提示框
*/
const mapTooltip = ref({
visible: false,
x: 0,
y: 0,
title: "",
icon: "",
zIndex: 20, //
data: null, //
});
//
const handleBack = () => {
console.log("返回驾驶舱");

View File

@ -16,3 +16,31 @@ export const APP_CONFIG = {
title: '数据大屏',
version: '1.0.0'
}
// OSS 配置
// 注意: 实际使用时需要从配置中心或环境变量读取,此处为示例配置
export const OSS_CONFIG = {
// http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/%E5%8D%95%E5%85%B5%E8%A7%86%E8%A7%92.mp4
// OSS 服务地址
url: import.meta.env.VITE_OSS_URL || 'http://222.212.85.86:9000',
// OSS bucket
bucket: import.meta.env.VITE_OSS_BUCKET || '300bdf2b-a150-406e-be63-d28bd29b409f'
}
/**
* 获取 OSS 配置
* @returns {{url: string, bucket: string}}
*/
export const getOssConfig = () => {
const { url, bucket } = OSS_CONFIG
// 确保 URL 包含协议
if (url.includes('http://') || url.includes('https://')) {
return { url, bucket }
} else {
return {
url: `http://${url}`,
bucket
}
}
}

View File

@ -1,3 +1,5 @@
import { getOssConfig } from '../config'
/**
* 格式化日期
* @param {Date|string|number} date
@ -73,3 +75,34 @@ export function deepClone(obj) {
}
return clonedObj
}
/**
* 获取 OSS 资源 URL
* @param {string} path - OSS 对象路径, 'demo/ylzg/单兵视角.mp4'
* @returns {string} 完整的 OSS 资源 URL
* @example
* getAssetUrl('demo/ylzg/单兵视角.mp4')
* // => 'http://183.221.225.106:9001/6251daf8-4127-40e0-980d-c86f8a765b20/demo/ylzg/单兵视角.mp4'
*/
export function getAssetUrl(path) {
const { url, bucket } = getOssConfig()
return `${url}/${bucket}/${path}`
}
/**
* 获取视频 URL (getAssetUrl 的别名,用于语义化)
* @param {string} path - 视频文件路径
* @returns {string} 完整的视频 URL
*/
export function getVideoUrl(path) {
return getAssetUrl(path)
}
/**
* 获取图片 URL (getAssetUrl 的别名,用于语义化)
* @param {string} path - 图片文件路径
* @returns {string} 完整的图片 URL
*/
export function getImageUrl(path) {
return getAssetUrl(path)
}