refactor(3d-situational-awareness): 模块化为可组合架构

- 将功能提取到专用的可组合项中(useCesiumLifecycle、useEmergencyDispatch、useMapClickHandler、useMockData、usePathLines、useRangeCircle)
- 将常量分离到有组织的文件中(坐标、模拟数据、相机预设)
- 添加TypeScript类型定义以提高类型安全性
- 重构主组件以使用可组合模式,提高可维护性和性能
- 更新路由器和组件以支持新架构
This commit is contained in:
Zzc 2025-11-25 09:35:28 +08:00
parent ba4b979584
commit 6f2259547e
18 changed files with 2425 additions and 1234 deletions

View File

@ -48,7 +48,8 @@ const routes = [
path: '/3DSituationalAwareness',
name: '3DSituationalAwareness',
meta: {
screen: true
screen: true,
skipInitialCameraView: true // 跳过MapViewport的自动初始视图由页面自己控制相机
},
component: () => import('../views/3DSituationalAwarenessRefactor/index.vue')
}

View File

@ -44,3 +44,16 @@ video-placeholder.png -> SketchPngb3b734375de691a8ba794eee7807988d78f942877ab220
## 调度建议
suggestion-icon.png -> SketchPng08ea47fd72e32082154366a0cbcd9a701074a835d3bae2eb9237b81b2ae775a6.png
suggestion-bg.png -> SketchPng84e383eb0cfecb67b9a0068cf2c81514a13efe72d2ac102b28c4739dfd5bacf6.png
## 视频弹窗VideoModal
video-control-megaphone.png -> SketchPngb86960bc4bc265d41dd03a8ed7c4b084ccd30fc7752088f3d2baad4a44f88489.png (喊话按钮)
video-control-sound.png -> sound.png (声音按钮)
video-control-zoom.png -> SketchPngb9171160837b0b4a4cdb277aca92389cebb62399608ed3a54dcbf23d0d2d3cac.png (放大按钮)
video-modal-close.png -> SketchPngf4cb9473415706d94e50d55177de9ed164db9069c3f3edbb335dcf024c256079.png (关闭按钮)
video-control-left.png -> SketchPng71c4b50fd627cc955d56e657283137361fc216fad2fcb9affeeaaf01b5e3c42e.png (左移)
video-control-right.png -> SketchPng26e936be527abaab4e5b33e1f766c40c1cc5158017be8ea6d500855e5140ff7e.png (右移)
video-control-up.png -> SketchPnge0ff29f6a2cb4a26ebd039f7f58582dbc374f394dc30f74075b7b4e54936ad07.png (上移)
video-control-down.png -> SketchPng0c55ea03b39c80c7bd871acc9717b47a6e59c01173e8411d0d57619ac7aeda3c.png (下移)
video-control-console.png -> 操作台.png (操作台背景)
video-control-shrink.png -> 缩小.png (缩小按钮)
video-modal-title-bg.png -> titlebg.png (弹窗标题背景)

View File

@ -42,13 +42,13 @@
<script setup>
import { ref } from "vue";
import { MAP_TOOLS, DEVICE_WATCH } from "../../constants.js";
import { MAP_TOOLS, DEVICE_WATCH } from "../../constants";
//
const isWatchingDevice = ref(false);
//
const activeTool = ref(null);
//
const activeTool = ref('modelCompare');
//
const emit = defineEmits(["device-watch", "tool-change"]);

View File

@ -0,0 +1,179 @@
import { ref, onUnmounted } from 'vue';
/**
* Cesium 资源生命周期管理
*
* 职责
* 1. 统一管理 Cesium 事件监听器的注册和清理
* 2. 管理 ScreenSpaceEventHandler 的生命周期
* 3. 管理定时器和动画帧的清理
* 4. 防止内存泄漏和 WebGL 资源耗尽
*
* @returns {Object} 资源管理 API
*/
export function useCesiumLifecycle() {
// 存储所有需要清理的资源
const eventHandlers = ref([]);
const postRenderListeners = ref([]);
const timeouts = ref([]);
const intervals = ref([]);
const animationFrames = ref([]);
/**
* 注册 ScreenSpaceEventHandler
* @param {Cesium.ScreenSpaceEventHandler} handler - 事件处理器
*/
const registerEventHandler = (handler) => {
if (handler && !eventHandlers.value.includes(handler)) {
eventHandlers.value.push(handler);
}
};
/**
* 注册 postRender 监听器
* @param {Cesium.Scene} scene - Cesium 场景
* @param {Function} callback - 回调函数
* @returns {Function} 移除监听器的函数
*/
const registerPostRenderListener = (scene, callback) => {
if (!scene || !callback) {
console.warn('[useCesiumLifecycle] 无效的 scene 或 callback');
return () => {};
}
// 添加监听器
scene.postRender.addEventListener(callback);
// 保存引用
const listener = { scene, callback };
postRenderListeners.value.push(listener);
// 返回移除函数
return () => {
scene.postRender.removeEventListener(callback);
const index = postRenderListeners.value.indexOf(listener);
if (index > -1) {
postRenderListeners.value.splice(index, 1);
}
};
};
/**
* 注册定时器
* @param {number} timeoutId - setTimeout 返回的 ID
*/
const registerTimeout = (timeoutId) => {
if (timeoutId) {
timeouts.value.push(timeoutId);
}
};
/**
* 注册间隔定时器
* @param {number} intervalId - setInterval 返回的 ID
*/
const registerInterval = (intervalId) => {
if (intervalId) {
intervals.value.push(intervalId);
}
};
/**
* 注册动画帧
* @param {number} frameId - requestAnimationFrame 返回的 ID
*/
const registerAnimationFrame = (frameId) => {
if (frameId) {
animationFrames.value.push(frameId);
}
};
/**
* 清理所有注册的资源
*/
const cleanup = () => {
console.log('[useCesiumLifecycle] 开始清理资源...');
// 1. 销毁所有事件处理器
eventHandlers.value.forEach((handler) => {
try {
if (handler && !handler.isDestroyed()) {
handler.destroy();
}
} catch (error) {
console.error('[useCesiumLifecycle] 销毁事件处理器失败:', error);
}
});
eventHandlers.value = [];
// 2. 移除所有 postRender 监听器
postRenderListeners.value.forEach(({ scene, callback }) => {
try {
if (scene && callback) {
scene.postRender.removeEventListener(callback);
}
} catch (error) {
console.error('[useCesiumLifecycle] 移除 postRender 监听器失败:', error);
}
});
postRenderListeners.value = [];
// 3. 清除所有定时器
timeouts.value.forEach((timeoutId) => {
try {
clearTimeout(timeoutId);
} catch (error) {
console.error('[useCesiumLifecycle] 清除定时器失败:', error);
}
});
timeouts.value = [];
// 4. 清除所有间隔定时器
intervals.value.forEach((intervalId) => {
try {
clearInterval(intervalId);
} catch (error) {
console.error('[useCesiumLifecycle] 清除间隔定时器失败:', error);
}
});
intervals.value = [];
// 5. 取消所有动画帧
animationFrames.value.forEach((frameId) => {
try {
cancelAnimationFrame(frameId);
} catch (error) {
console.error('[useCesiumLifecycle] 取消动画帧失败:', error);
}
});
animationFrames.value = [];
console.log('[useCesiumLifecycle] 资源清理完成');
};
// 组件卸载时自动清理
onUnmounted(() => {
cleanup();
});
return {
// 注册方法
registerEventHandler,
registerPostRenderListener,
registerTimeout,
registerInterval,
registerAnimationFrame,
// 清理方法
cleanup,
// 状态(用于调试)
getResourceCount: () => ({
eventHandlers: eventHandlers.value.length,
postRenderListeners: postRenderListeners.value.length,
timeouts: timeouts.value.length,
intervals: intervals.value.length,
animationFrames: animationFrames.value.length,
}),
};
}

View File

@ -40,6 +40,15 @@ export function useDualMapCompare() {
return null
}
// 验证容器尺寸
const { clientWidth, clientHeight } = container
console.log(`[useDualMapCompare] 左侧容器尺寸: ${clientWidth}x${clientHeight}`)
if (clientWidth <= 0 || clientHeight <= 0) {
console.error(`[useDualMapCompare] 左侧容器尺寸无效 (width=${clientWidth}, height=${clientHeight})`)
return null
}
// 创建左侧viewer
const viewer = new Cesium.Viewer(container, {
animation: false,
@ -126,13 +135,33 @@ export function useDualMapCompare() {
/**
* 启用对比模式
* @param {Cesium.Viewer} rightViewerInstance - 右侧Viewer实例主地图灾后场景
* @param {Object} options - 配置选项
* @param {boolean} options.skipLeftModelLoad - 是否跳过左侧模型加载
* @param {boolean} options.loadLeftModel - 是否仅加载左侧模型不重新初始化Viewer
*/
const enableCompareMode = async (rightViewerInstance) => {
const enableCompareMode = async (rightViewerInstance, options = {}) => {
const { skipLeftModelLoad = false, loadLeftModel = false } = options
if (!rightViewerInstance) {
console.error('[useDualMapCompare] 右侧主地图Viewer未初始化')
return
}
// 如果只是加载左侧模型Viewer已存在
if (loadLeftModel && leftViewer.value) {
console.log('[useDualMapCompare] 加载左侧灾前模型...')
try {
const tileset = await load3DTileset(leftViewer.value, 'before', false)
if (tileset) {
leftTileset = tileset
console.log('[useDualMapCompare] 左侧灾前模型加载完成')
}
} catch (error) {
console.error('[useDualMapCompare] 左侧模型加载失败:', error)
}
return
}
console.log('[useDualMapCompare] 启用对比模式...')
rightViewer.value = rightViewerInstance
@ -147,8 +176,8 @@ export function useDualMapCompare() {
// 先设置状态触发CSS动画
isCompareMode.value = true
// 等待一小段时间让CSS过渡开始
await new Promise(resolve => setTimeout(resolve, 50))
// 等待CSS过渡完成300ms + 50ms buffer
await new Promise(resolve => setTimeout(resolve, 350))
// 初始化左侧Viewer
const leftViewerInstance = initLeftViewer(leftContainer)
@ -173,19 +202,23 @@ export function useDualMapCompare() {
// 设置相机同步(单向:右→左)
setupCameraSync(rightViewerInstance, leftViewerInstance)
// 异步加载灾前模型到左侧(不阻塞对比模式启用)
// 让模型在后台加载,用户可以立即看到对比效果
console.log('[useDualMapCompare] 开始异步加载左侧灾前模型...')
load3DTileset(leftViewerInstance, 'before', false)
.then(tileset => {
if (tileset) {
leftTileset = tileset
console.log('[useDualMapCompare] 左侧灾前模型加载完成')
}
})
.catch(error => {
console.error('[useDualMapCompare] 左侧模型加载失败:', error)
})
// 根据选项决定是否加载左侧模型
if (!skipLeftModelLoad) {
// 异步加载灾前模型到左侧(不阻塞对比模式启用)
console.log('[useDualMapCompare] 开始异步加载左侧灾前模型...')
load3DTileset(leftViewerInstance, 'before', false)
.then(tileset => {
if (tileset) {
leftTileset = tileset
console.log('[useDualMapCompare] 左侧灾前模型加载完成')
}
})
.catch(error => {
console.error('[useDualMapCompare] 左侧模型加载失败:', error)
})
} else {
console.log('[useDualMapCompare] 跳过左侧模型加载(将延迟加载)')
}
// 右侧保持灾后模型(已加载)
@ -246,10 +279,11 @@ export function useDualMapCompare() {
* 切换对比模式
* @param {boolean} active - true启用false禁用
* @param {Cesium.Viewer} rightViewerInstance - 右侧Viewer实例主地图
* @param {Object} options - 配置选项传递给 enableCompareMode
*/
const toggleCompareMode = async (active, rightViewerInstance) => {
const toggleCompareMode = async (active, rightViewerInstance, options) => {
if (active) {
await enableCompareMode(rightViewerInstance)
await enableCompareMode(rightViewerInstance, options)
} else {
disableCompareMode()
}

View File

@ -0,0 +1,175 @@
import { ref } from 'vue'
import * as Cesium from 'cesium'
import { ANIMATION_CONFIG, ANIMATION_PATHS } from '../constants'
/**
* 应急调度流程管理
*
* 职责
* 1. 管理"一键启动"完整流程
* 2. 协调 loading 动画路径线绘制人员移动动画
* 3. 管理相机飞行到全景位置
* 4. 清理路径起点标记
*
* @param {Object} dependencies - 依赖的 composables
* @param {Object} dependencies.pathLinesComposable - usePathLines 返回的对象
* @param {Object} dependencies.entityAnimationComposable - useEntityAnimation 返回的对象
* @param {Object} dependencies.mapStore - 地图 store
* @param {Function} dependencies.registerTimeoutFn - 注册定时器的函数来自 useCesiumLifecycle
* @returns {Object} 应急调度 API
*/
export function useEmergencyDispatch({
pathLinesComposable,
entityAnimationComposable,
mapStore,
registerTimeoutFn,
}) {
const { drawAllPathLines } = pathLinesComposable
const { startPersonnelMovement, startMultipleMovements, isAnimating } =
entityAnimationComposable
// Loading 状态
const showLoading = ref(false)
/**
* 启动应急调度流程
* @param {Object} payload - 调度参数
*/
const startDispatch = (payload) => {
console.log('[useEmergencyDispatch] 启动力量调度:', payload)
// 防止重复启动
if (isAnimating.value) {
console.warn('[useEmergencyDispatch] 动画正在运行,忽略重复启动')
return
}
const viewer = mapStore.viewer
if (!viewer) {
console.error('[useEmergencyDispatch] viewer 未就绪')
return
}
// 1. 显示 loading 动画
showLoading.value = true
// 2. 绘制红色路径线
drawAllPathLines(viewer, {
color: Cesium.Color.RED,
width: 5,
clampToGround: true,
})
// 3. 延迟执行后续步骤
const timeoutId = setTimeout(() => {
executeDispatchSteps(viewer)
}, ANIMATION_CONFIG.loadingDuration)
// 注册定时器到生命周期管理
registerTimeoutFn(timeoutId)
}
/**
* 执行调度步骤
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
*/
const executeDispatchSteps = (viewer) => {
// 1. 隐藏 loading 动画
showLoading.value = false
// 2. 移除路径起点标记
removePathStartMarkers(viewer)
// 3. 启动人员移动动画
startPersonnelMovement(viewer, {
duration: ANIMATION_CONFIG.duration,
trackEntity: false,
personnelName: '应急救援人员',
department: '应急救援队',
})
// 4. 启动多组移动动画
startMultipleMovements(viewer, {
duration: ANIMATION_CONFIG.duration,
})
// 5. 相机飞向全景位置
flyToOverviewPosition()
}
/**
* 移除路径起点标记
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
*/
const removePathStartMarkers = (viewer) => {
const entitiesToRemove = []
viewer.entities.values.forEach((entity) => {
if (entity.properties && entity.properties.isPathStartMarker) {
entitiesToRemove.push(entity)
}
})
entitiesToRemove.forEach((entity) => {
viewer.entities.remove(entity)
})
console.log(`[useEmergencyDispatch] 已移除 ${entitiesToRemove.length} 个路径起点标记`)
}
/**
* 相机飞向全景位置
* 计算能看到所有路径的最佳视角
*/
const flyToOverviewPosition = () => {
const { camera } = mapStore.services()
console.log('[useEmergencyDispatch] 相机缓慢飞向最佳全景位置...')
// 收集所有路径的起点和终点
const allPathPoints = []
Object.values(ANIMATION_PATHS).forEach((path) => {
const { startPoint, waypoints } = path
// 起点
allPathPoints.push(
new Cesium.Cartesian3(startPoint.x, startPoint.y, startPoint.z)
)
// 终点
const endPoint = waypoints[waypoints.length - 1]
allPathPoints.push(new Cesium.Cartesian3(endPoint.x, endPoint.y, endPoint.z))
})
// 转换为经纬度格式
const trajectoryPoints = allPathPoints.map((point) => {
const cartographic = Cesium.Cartographic.fromCartesian(point)
return {
lon: Cesium.Math.toDegrees(cartographic.longitude),
lat: Cesium.Math.toDegrees(cartographic.latitude),
}
})
// 使用智能聚焦方法飞向能看到所有路径的最佳位置
camera.fitBoundsWithTrajectory(trajectoryPoints, {
duration: ANIMATION_CONFIG.cameraFlyDuration,
padding: ANIMATION_CONFIG.cameraPadding,
})
}
/**
* 停止调度流程
*/
const stopDispatch = () => {
showLoading.value = false
// 停止动画由 useEntityAnimation 管理
console.log('[useEmergencyDispatch] 调度流程已停止')
}
return {
showLoading,
startDispatch,
stopDispatch,
}
}

View File

@ -0,0 +1,316 @@
import * as Cesium from 'cesium'
import { ref } from 'vue'
import { MOCK_VIDEO_URL } from '../constants'
/**
* 地图点击事件处理
*
* 职责
* 1. 管理地图点击事件监听器
* 2. 处理标记点点击逻辑
* 3. 显示 Tooltip 和详情弹窗
*
* @param {Object} options - 配置选项
* @param {Object} options.tooltipComposable - useMapTooltip 返回的对象
* @param {Object} options.icons - 图标资源对象
* @param {Ref} options.rangeCircleEntity - 范围圈实体引用
* @returns {Object} 点击处理 API
*/
export function useMapClickHandler({ tooltipComposable, icons, rangeCircleEntity }) {
const { showTooltip, hideTooltip, enableEntityTracking } = tooltipComposable
// 事件处理器引用
let eventHandler = null
/**
* 设置地图点击事件处理器
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Function} registerEventHandlerFn - 注册事件处理器的函数来自 useCesiumLifecycle
* @param {Function} registerPostRenderFn - 注册 postRender 的函数来自 useCesiumLifecycle
*/
const setupClickHandler = (viewer, registerEventHandlerFn, registerPostRenderFn) => {
if (!viewer) {
console.warn('[useMapClickHandler] viewer 为空')
return
}
// 创建事件处理器
eventHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
// 注册到生命周期管理
registerEventHandlerFn(eventHandler)
// 监听左键点击事件
eventHandler.setInputAction((click) => {
handleMapClick(viewer, click, registerPostRenderFn)
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
console.log('[useMapClickHandler] 地图点击事件监听器已设置')
}
/**
* 处理地图点击事件
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Object} click - 点击事件对象
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const handleMapClick = (viewer, click, registerPostRenderFn) => {
// 获取点击位置的实体
const pickedObject = viewer.scene.pick(click.position)
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
const entity = pickedObject.id
// 过滤掉范围圈实体
if (entity === rangeCircleEntity.value) {
console.log('[useMapClickHandler] 点击了范围圈,尝试穿透')
handleRangeCircleClick(viewer, click, registerPostRenderFn)
return
}
// 检查实体是否有 properties标记点才有
if (entity.properties) {
const type = entity.properties.type?.getValue()
handleMarkerClick(viewer, entity, type, click, registerPostRenderFn)
}
} else {
// 点击空白区域,隐藏 Tooltip
hideTooltip()
}
}
/**
* 处理范围圈点击穿透到下面的实体
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Object} click - 点击事件对象
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const handleRangeCircleClick = (viewer, click, registerPostRenderFn) => {
const drillPickedObjects = viewer.scene.drillPick(click.position)
for (const pickedObj of drillPickedObjects) {
if (Cesium.defined(pickedObj.id) && pickedObj.id !== rangeCircleEntity.value) {
const markerEntity = pickedObj.id
if (markerEntity.properties) {
const type = markerEntity.properties.type?.getValue()
handleMarkerClick(viewer, markerEntity, type, click, registerPostRenderFn)
return
}
}
}
// 没有找到标记点,隐藏 Tooltip
hideTooltip()
}
/**
* 处理标记点点击
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 被点击的实体
* @param {string} type - 标记类型
* @param {Object} click - 点击事件对象
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const handleMarkerClick = (viewer, entity, type, click, registerPostRenderFn) => {
let icon = null
// 根据类型选择图标
if (type === 'soldier') {
icon = icons.soldierIcon
} else if (type === 'device') {
icon = icons.deviceIcon
} else if (type === 'emergencyBase' || type === 'station') {
const stationName = entity.properties.name?.getValue() || ''
icon =
stationName === '忠县公路交通应急物资储备中心'
? icons.emergencyCenterIcon
: icons.emergencyBaseIcon
} else if (type === 'reserveCenter' || type === 'presetPoint') {
icon = type === 'reserveCenter' ? icons.reserveCenterIcon : icons.emergencyBaseIcon
}
if (icon) {
showMarkerTooltip(viewer, entity, icon, registerPostRenderFn)
}
}
/**
* 显示标记点 Tooltip
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 被点击的实体
* @param {string} icon - 图标路径
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const showMarkerTooltip = (viewer, entity, icon, registerPostRenderFn) => {
const properties = entity.properties
const type = properties.type?.getValue()
// 获取实体的 3D 位置
const position = entity.position?.getValue(Cesium.JulianDate.now())
if (!position) {
console.warn('[useMapClickHandler] 无法获取实体位置')
return
}
// 获取地形高度
const cartographic = Cesium.Cartographic.fromCartesian(position)
let clampedPosition = position
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic)
if (Cesium.defined(height)) {
cartographic.height = height
clampedPosition = Cesium.Cartographic.toCartesian(cartographic)
}
}
// 转换为屏幕坐标
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition)
if (!Cesium.defined(canvasPosition)) {
console.warn('[useMapClickHandler] 无法转换坐标到屏幕位置')
return
}
// 计算容器偏移,将 canvas 本地坐标转换为视口坐标
// 在对比模式下,右侧 canvas 相对于视口有 50% 的偏移
const canvas = viewer.scene.canvas
const containerRect = canvas.getBoundingClientRect()
const viewportX = canvasPosition.x + containerRect.left
const viewportY = canvasPosition.y + containerRect.top
// 构建 Tooltip 数据
const tooltipData = buildTooltipData(type, properties)
// 显示 Tooltip使用视口坐标
showTooltip({
x: viewportX,
y: viewportY,
title: tooltipData.title,
icon,
data: tooltipData.data,
entity, // 传递实体用于位置跟踪
})
// 启用实体位置跟踪
enableEntityTracking(viewer, registerPostRenderFn)
}
/**
* 构建 Tooltip 数据
* @param {string} type - 标记类型
* @param {Object} properties - 实体属性
* @returns {Object} Tooltip 数据
*/
const buildTooltipData = (type, properties) => {
let title = ''
const fields = []
const actions = []
let supportVideo = false
let videoTitle = ''
const videoSrc = MOCK_VIDEO_URL
if (type === 'soldier') {
// 应急人员
title = '应急人员'
fields.push(
{ label: '姓名', value: properties.name?.getValue() || '-' },
{ label: '部门', value: properties.department?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' },
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
)
actions.push({
label: '联动',
type: 'link',
data: properties,
})
} else if (type === 'device') {
// 应急装备
title = '应急装备'
fields.push(
{ label: '设备名称', value: properties.name?.getValue() || '-' },
{ label: '设备类型', value: properties.deviceType?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' },
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
)
} else if (type === 'emergencyBase') {
// 应急基地
title = '应急基地'
fields.push(
{ label: '基地名称', value: properties.name?.getValue() || '-' },
{ label: '路线编号', value: properties.routeNumber?.getValue() || '-' },
{ label: '隶属单位', value: properties.department?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
)
actions.push({
label: '连线',
type: 'connect',
data: properties,
})
} else if (type === 'station') {
const stationName = properties.name?.getValue() || ''
const distance = properties.distance?.getValue() || 0
if (stationName === '忠县公路交通应急物资储备中心') {
title = '应急中心'
fields.push(
{ label: '名称', value: '忠县应急中心' },
{ label: '行政等级', value: '国道' },
{ label: '隶属单位', value: '交通公路部门' },
{ label: '位置信息', value: `目前为止距离现场${distance}公里` }
)
supportVideo = true
videoTitle = '应急中心'
} else {
title = '养护站'
fields.push(
{ label: '名称', value: stationName || '-' },
{ label: '距离', value: `${distance}公里` }
)
}
} else if (type === 'reserveCenter') {
// 储备中心
title = '储备中心'
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
)
supportVideo = true
videoTitle = '储备中心'
} else if (type === 'presetPoint') {
// 预置点
title = '预置点'
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
)
supportVideo = true
videoTitle = '预置点'
}
return {
title,
data: {
fields,
actions: actions.length > 0 ? actions : undefined,
supportVideo,
videoTitle,
videoSrc,
},
}
}
/**
* 销毁事件处理器
*/
const destroy = () => {
if (eventHandler && !eventHandler.isDestroyed()) {
eventHandler.destroy()
eventHandler = null
}
}
return {
setupClickHandler,
destroy,
}
}

View File

@ -654,8 +654,7 @@ export function useMapMarkers() {
width: 48,
height: 48,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: resolveBillboardHeightReference(result.samplingSucceeded),
disableDepthTestDistance: Number.POSITIVE_INFINITY
heightReference: resolveBillboardHeightReference(result.samplingSucceeded)
},
properties: {
type,
@ -742,8 +741,7 @@ export function useMapMarkers() {
width: 48,
height: 48,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: resolveBillboardHeightReference(result.samplingSucceeded),
disableDepthTestDistance: Number.POSITIVE_INFINITY
heightReference: resolveBillboardHeightReference(result.samplingSucceeded)
},
properties: {
type: 'station',

View File

@ -1,8 +1,14 @@
import { ref } from 'vue'
import * as Cesium from 'cesium'
/**
* 地图 Tooltip 状态管理
* 地图 Tooltip 状态管理性能优化版
* 用于显示地图标记点的轻量级信息提示框
*
* 性能优化
* 1. 只在 tooltip 可见时注册 postRender 监听器
* 2. 缓存地形高度计算结果
* 3. 使用节流避免过度更新
*/
export function useMapTooltip() {
// Tooltip 状态
@ -16,6 +22,15 @@ export function useMapTooltip() {
data: null // 业务数据,用于内容插槽渲染
})
// 当前跟踪的实体
const currentEntity = ref(null)
// postRender 监听器移除函数
let removePostRenderListener = null
// 地形高度缓存(避免重复采样)
const terrainHeightCache = new Map()
/**
* 显示 Tooltip
* @param {Object} options - Tooltip 配置选项
@ -24,8 +39,9 @@ export function useMapTooltip() {
* @param {string} [options.title=''] - Tooltip 标题文本
* @param {string} [options.icon=''] - 标题左侧图标的图片路径
* @param {Object} [options.data=null] - 业务数据
* @param {Cesium.Entity} [options.entity=null] - 关联的实体用于位置跟踪
*/
const showTooltip = ({ x, y, title = '', icon = '', data = null }) => {
const showTooltip = ({ x, y, title = '', icon = '', data = null, entity = null }) => {
tooltipState.value = {
visible: true,
x,
@ -35,6 +51,9 @@ export function useMapTooltip() {
zIndex: 20,
data
}
// 保存关联的实体
currentEntity.value = entity
}
/**
@ -42,6 +61,16 @@ export function useMapTooltip() {
*/
const hideTooltip = () => {
tooltipState.value.visible = false
currentEntity.value = null
// 移除 postRender 监听器(性能优化)
if (removePostRenderListener) {
removePostRenderListener()
removePostRenderListener = null
}
// 清空地形高度缓存
terrainHeightCache.clear()
}
/**
@ -50,14 +79,103 @@ export function useMapTooltip() {
* @param {number} y - 屏幕 Y 坐标
*/
const updateTooltipPosition = (x, y) => {
tooltipState.value.x = x
tooltipState.value.y = y
if (tooltipState.value.visible) {
tooltipState.value.x = x
tooltipState.value.y = y
}
}
/**
* 启用实体位置跟踪性能优化版
* 只在 tooltip 可见时注册 postRender 监听器
*
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Function} registerPostRenderFn - 注册 postRender 的函数来自 useCesiumLifecycle
*/
const enableEntityTracking = (viewer, registerPostRenderFn) => {
if (!viewer || !registerPostRenderFn) {
console.warn('[useMapTooltip] 无效的 viewer 或 registerPostRenderFn')
return
}
// 如果已经有监听器,先移除
if (removePostRenderListener) {
removePostRenderListener()
}
// 创建更新回调(带缓存优化)
const updateCallback = () => {
if (!currentEntity.value || !tooltipState.value.visible) {
return
}
const entity = currentEntity.value
const position = entity.position?.getValue(Cesium.JulianDate.now())
if (!position) return
// 获取地形高度(带缓存)
const cartographic = Cesium.Cartographic.fromCartesian(position)
const cacheKey = `${cartographic.longitude.toFixed(6)}_${cartographic.latitude.toFixed(6)}`
let clampedPosition = position
if (!terrainHeightCache.has(cacheKey)) {
// 首次采样,缓存结果
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic)
if (Cesium.defined(height)) {
cartographic.height = height
clampedPosition = Cesium.Cartographic.toCartesian(cartographic)
terrainHeightCache.set(cacheKey, clampedPosition)
}
}
} else {
// 使用缓存的高度
clampedPosition = terrainHeightCache.get(cacheKey)
}
// 转换为屏幕坐标
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition)
// 如果标记点在视野外,隐藏 tooltip
if (!Cesium.defined(canvasPosition)) {
hideTooltip()
return
}
// 计算容器偏移,将 canvas 本地坐标转换为视口坐标
const canvas = viewer.scene.canvas
const containerRect = canvas.getBoundingClientRect()
const viewportX = canvasPosition.x + containerRect.left
const viewportY = canvasPosition.y + containerRect.top
// 更新位置(使用视口坐标)
updateTooltipPosition(viewportX, viewportY)
}
// 注册监听器
removePostRenderListener = registerPostRenderFn(viewer.scene, updateCallback)
}
/**
* 禁用实体位置跟踪
*/
const disableEntityTracking = () => {
if (removePostRenderListener) {
removePostRenderListener()
removePostRenderListener = null
}
terrainHeightCache.clear()
}
return {
tooltipState,
currentEntity,
showTooltip,
hideTooltip,
updateTooltipPosition
updateTooltipPosition,
enableEntityTracking,
disableEntityTracking
}
}

View File

@ -0,0 +1,175 @@
import * as Cesium from 'cesium'
import {
MOCK_PERSONNEL_POINTS,
MOCK_DEVICE_POINTS,
ANIMATION_PATHS,
MOCK_VIDEO_URL,
MARKER_ICON_SIZE,
} from '../constants'
/**
* 模拟数据服务
*
* 职责
* 1. 提供类型安全的模拟数据访问
* 2. 将原始数据转换为 Cesium 实体配置
* 3. 便于后续替换为真实 API
*
* @returns {Object} 模拟数据 API
*/
export function useMockData() {
/**
* 获取模拟人员点位数据
* @returns {Array} 人员点位数组
*/
const getPersonnelPoints = () => {
return MOCK_PERSONNEL_POINTS.map((point) => ({
...point,
location: `目前为止距离现场${point.distance}公里`,
}))
}
/**
* 获取模拟设备点位数据
* @returns {Array} 设备点位数组
*/
const getDevicePoints = () => {
return MOCK_DEVICE_POINTS.map((point) => ({
...point,
location: `目前为止距离现场${point.distance}公里`,
}))
}
/**
* 获取所有模拟点位数据人员 + 设备
* @returns {Array} 所有点位数组
*/
const getAllMockPoints = () => {
return [...getPersonnelPoints(), ...getDevicePoints()]
}
/**
* 创建人员实体配置
* @param {Object} point - 人员点位数据
* @param {string} iconUrl - 图标 URL
* @returns {Object} Cesium 实体配置
*/
const createPersonnelEntityConfig = (point, iconUrl) => {
return {
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
billboard: {
image: iconUrl,
width: MARKER_ICON_SIZE.width,
height: MARKER_ICON_SIZE.height,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
properties: {
type: 'soldier',
name: point.name,
department: point.department,
location: point.location,
estimatedArrival: point.estimatedArrival,
},
}
}
/**
* 创建设备实体配置
* @param {Object} point - 设备点位数据
* @param {string} iconUrl - 图标 URL
* @returns {Object} Cesium 实体配置
*/
const createDeviceEntityConfig = (point, iconUrl) => {
return {
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
billboard: {
image: iconUrl,
width: MARKER_ICON_SIZE.width,
height: MARKER_ICON_SIZE.height,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
properties: {
type: 'device',
name: point.name,
deviceType: point.deviceType,
location: point.location,
estimatedArrival: point.estimatedArrival,
},
}
}
/**
* 获取动画路径数据
* @param {string} pathId - 路径 ID ('path1', 'path2', 'path3')
* @returns {Object} 路径数据
*/
const getAnimationPath = (pathId) => {
return ANIMATION_PATHS[pathId]
}
/**
* 获取所有动画路径
* @returns {Object} 所有路径数据
*/
const getAllAnimationPaths = () => {
return ANIMATION_PATHS
}
/**
* 创建路径起点标记配置
* @param {Object} path - 路径数据
* @param {string} iconUrl - 图标 URL
* @returns {Object} Cesium 实体配置
*/
const createPathStartMarkerConfig = (path, iconUrl) => {
const { startPoint, metadata } = path
return {
position: new Cesium.Cartesian3(startPoint.x, startPoint.y, startPoint.z),
billboard: {
image: iconUrl,
width: MARKER_ICON_SIZE.width,
height: MARKER_ICON_SIZE.height,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
properties: {
type: metadata.type,
name: metadata.name,
department: metadata.department || metadata.deviceType,
location: '待命中',
estimatedArrival: '待启动',
isPathStartMarker: true,
pathId: path.id,
},
}
}
/**
* 获取视频 URL
* @returns {string} 视频 URL
*/
const getVideoUrl = () => {
return MOCK_VIDEO_URL
}
return {
// 数据获取
getPersonnelPoints,
getDevicePoints,
getAllMockPoints,
getAnimationPath,
getAllAnimationPaths,
getVideoUrl,
// 实体配置创建
createPersonnelEntityConfig,
createDeviceEntityConfig,
createPathStartMarkerConfig,
}
}

View File

@ -0,0 +1,143 @@
import * as Cesium from 'cesium'
import { ref } from 'vue'
import { ANIMATION_PATHS } from '../constants'
/**
* 路径线管理
*
* 职责
* 1. 绘制应急人员和设备的移动路径线
* 2. 管理路径线实体的生命周期
* 3. 清理路径线资源
*
* @returns {Object} 路径线管理 API
*/
export function usePathLines() {
// 路径线实体数组
const pathLineEntities = ref([])
/**
* 绘制所有路径线
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Object} options - 可选配置
* @param {string} options.color - 路径线颜色默认红色
* @param {number} options.width - 路径线宽度默认 5
* @param {boolean} options.clampToGround - 是否贴地默认 true
*/
const drawAllPathLines = (viewer, options = {}) => {
if (!viewer) {
console.warn('[usePathLines] viewer 为空')
return
}
const { color = Cesium.Color.RED, width = 5, clampToGround = true } = options
// 清除旧的路径线
clearPathLines(viewer)
// 绘制所有路径
Object.values(ANIMATION_PATHS).forEach((path) => {
drawPathLine(viewer, path.waypoints, { color, width, clampToGround })
})
console.log(`[usePathLines] 已绘制 ${pathLineEntities.value.length} 条路径线`)
}
/**
* 绘制单条路径线
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Array} waypoints - 路径点数组
* @param {Object} options - 样式配置
*/
const drawPathLine = (viewer, waypoints, options) => {
const { color, width, clampToGround } = options
// 转换坐标点
const positions = waypoints.map(
(coord) => new Cesium.Cartesian3(coord.x, coord.y, coord.z)
)
// 创建路径线实体
const pathEntity = viewer.entities.add({
polyline: {
positions: positions,
width: width,
material: color,
clampToGround: clampToGround,
},
})
pathLineEntities.value.push(pathEntity)
}
/**
* 绘制指定路径的路径线
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {string} pathId - 路径 ID ('path1', 'path2', 'path3')
* @param {Object} options - 样式配置
*/
const drawPathLineById = (viewer, pathId, options = {}) => {
if (!viewer) {
console.warn('[usePathLines] viewer 为空')
return
}
const path = ANIMATION_PATHS[pathId]
if (!path) {
console.warn(`[usePathLines] 路径 ${pathId} 不存在`)
return
}
const { color = Cesium.Color.RED, width = 5, clampToGround = true } = options
drawPathLine(viewer, path.waypoints, { color, width, clampToGround })
}
/**
* 清除所有路径线
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
*/
const clearPathLines = (viewer) => {
if (!viewer) return
pathLineEntities.value.forEach((entity) => {
if (entity) {
viewer.entities.remove(entity)
}
})
pathLineEntities.value = []
console.log('[usePathLines] 所有路径线已清除')
}
/**
* 显示所有路径线
*/
const showPathLines = () => {
pathLineEntities.value.forEach((entity) => {
if (entity) {
entity.show = true
}
})
}
/**
* 隐藏所有路径线
*/
const hidePathLines = () => {
pathLineEntities.value.forEach((entity) => {
if (entity) {
entity.show = false
}
})
}
return {
pathLineEntities,
drawAllPathLines,
drawPathLineById,
clearPathLines,
showPathLines,
hidePathLines,
}
}

View File

@ -0,0 +1,114 @@
import * as Cesium from 'cesium'
import { ref } from 'vue'
import { DISASTER_CENTER, RANGE_CIRCLE_STYLE } from '../constants'
/**
* 范围圈管理
*
* 职责
* 1. 创建和更新范围圈实体
* 2. 管理范围圈的样式和可见性
* 3. 清理范围圈资源
*
* @returns {Object} 范围圈管理 API
*/
export function useRangeCircle() {
// 范围圈实体引用
const rangeCircleEntity = ref(null)
/**
* 创建或更新范围圈
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {number} radiusKm - 半径公里
* @param {Object} options - 可选配置
* @param {number} options.centerLon - 中心点经度默认使用 DISASTER_CENTER
* @param {number} options.centerLat - 中心点纬度默认使用 DISASTER_CENTER
* @param {Object} options.style - 样式配置默认使用 RANGE_CIRCLE_STYLE
*/
const createOrUpdateRangeCircle = (
viewer,
radiusKm,
options = {}
) => {
if (!viewer) {
console.warn('[useRangeCircle] viewer 为空')
return
}
const {
centerLon = DISASTER_CENTER.lon,
centerLat = DISASTER_CENTER.lat,
style = RANGE_CIRCLE_STYLE,
} = options
const radiusMeters = radiusKm * 1000
// 如果已存在范围圈,先移除
if (rangeCircleEntity.value) {
viewer.entities.remove(rangeCircleEntity.value)
rangeCircleEntity.value = null
}
// 创建新的范围圈
rangeCircleEntity.value = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0),
ellipse: {
semiMinorAxis: radiusMeters,
semiMajorAxis: radiusMeters,
height: 0,
material: Cesium.Color.fromCssColorString(style.fillColor).withAlpha(style.fillAlpha),
outline: true,
outlineColor: Cesium.Color.fromCssColorString(style.outlineColor).withAlpha(
style.outlineAlpha
),
outlineWidth: style.outlineWidth,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
})
// 禁用范围圈的鼠标交互,让点击可以穿透到下面的标记点
if (rangeCircleEntity.value) {
rangeCircleEntity.value.allowPicking = false
}
console.log(`[useRangeCircle] 已创建/更新范围圈: ${radiusKm}km`)
}
/**
* 显示范围圈
*/
const showRangeCircle = () => {
if (rangeCircleEntity.value) {
rangeCircleEntity.value.show = true
}
}
/**
* 隐藏范围圈
*/
const hideRangeCircle = () => {
if (rangeCircleEntity.value) {
rangeCircleEntity.value.show = false
}
}
/**
* 清除范围圈
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
*/
const clearRangeCircle = (viewer) => {
if (rangeCircleEntity.value && viewer) {
viewer.entities.remove(rangeCircleEntity.value)
rangeCircleEntity.value = null
console.log('[useRangeCircle] 范围圈已清除')
}
}
return {
rangeCircleEntity,
createOrUpdateRangeCircle,
showRangeCircle,
hideRangeCircle,
clearRangeCircle,
}
}

View File

@ -0,0 +1,51 @@
/**
* 相机预设配置
* 定义常用的相机视角和飞行参数
*/
/**
* 相机飞行动画配置
*/
export const CAMERA_FLY_OPTIONS = {
duration: 2, // 默认飞行时长(秒)
easingFunction: 'CUBIC_IN_OUT', // 缓动函数
}
/**
* 相机视角预设
*/
export const CAMERA_PRESETS = {
// 鸟瞰视角
birdView: {
height: 2000,
heading: 0,
pitch: -90,
roll: 0,
},
// 倾斜视角
obliqueView: {
height: 800,
heading: 0,
pitch: -45,
roll: 0,
},
// 近景视角
closeView: {
height: 300,
heading: 0,
pitch: -30,
roll: 0,
},
}
/**
* 相机边界配置
*/
export const CAMERA_BOUNDS = {
minHeight: 100, // 最小高度(米)
maxHeight: 10000, // 最大高度(米)
minPitch: -90, // 最小俯仰角(度)
maxPitch: 0, // 最大俯仰角(度)
}

View File

@ -0,0 +1,59 @@
/**
* 坐标常量定义
* 集中管理所有硬编码的坐标数据
*/
/**
* 灾害中心点坐标经纬度
*/
export const DISASTER_CENTER = {
lon: 108.010961,
lat: 30.176459,
}
/**
* 默认相机位置Cartesian3 格式
* 用于初始化相机视角
* @deprecated 建议使用 DEFAULT_CAMERA_POSITION 代替
*/
export const DEFAULT_CAMERA_CARTESIAN = {
x: -1706512.6728840484,
y: 5248689.181706353,
z: 3187701.2527910336,
}
/**
* 默认相机位置经纬度格式
* 相机初始对准的地理位置
*/
export const DEFAULT_CAMERA_POSITION = {
lon: 108.011134,
lat: 30.175697,
}
/**
* 默认相机配置
* 包含高度朝向等参数
*/
export const DEFAULT_CAMERA_VIEW = {
height: 274,
heading: 0,
pitch: -45,
roll: 0,
}
/**
* 范围圈默认半径公里
*/
export const DEFAULT_SEARCH_RADIUS = 10
/**
* 范围圈样式配置
*/
export const RANGE_CIRCLE_STYLE = {
fillColor: '#1CA1FF',
fillAlpha: 0.2,
outlineColor: '#1CA1FF',
outlineAlpha: 0.8,
outlineWidth: 2,
}

View File

@ -1,15 +1,20 @@
/**
* 3D态势感知常量配置
* 常量统一导出
* 提供单一入口访问所有常量
*/
import { getVideoUrl } from '@shared/utils'
export * from './coordinates'
export * from './mockData'
export * from './cameraPresets'
// ========== 从原 constants.js 迁移的常量 ==========
// 视频监控视角类型
export const VIDEO_TYPES = {
PERSONNEL: 'personnel', // 单兵视角
DRONE: 'drone', // 无人机视角
VEHICLE_EXTERNAL: 'vehicle_external', // 指挥车外部视角
VEHICLE_MEETING: 'vehicle_meeting' // 指挥车会议视角
PERSONNEL: 'personnel',
DRONE: 'drone',
VEHICLE_EXTERNAL: 'vehicle_external',
VEHICLE_MEETING: 'vehicle_meeting',
}
// 视频监控列表
@ -18,65 +23,65 @@ export const VIDEO_MONITORS = [
id: 1,
type: VIDEO_TYPES.PERSONNEL,
title: '单兵(张维)设备视角',
// 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', // 日期范围
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,
hasZoom: true,
hasDirectionControl: false // 是否显示方向控制(操作台)
hasDirectionControl: false,
},
{
id: 2,
type: VIDEO_TYPES.DRONE,
title: '无人机(001)视角',
// videoSrc: getVideoUrl('demo/ylzg/无人机视角.mp4'), // 从 OSS 获取视频 URL
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/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: false,
hasMegaphone: true,
hasZoom: true,
hasDirectionControl: true // 无人机有方向控制
hasDirectionControl: true,
},
{
id: 3,
type: VIDEO_TYPES.VEHICLE_EXTERNAL,
title: '指挥车外部视角',
// videoSrc: getVideoUrl('demo/ylzg/单兵视角.mp4'), // 暂时使用单兵视角视频
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/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,
hasZoom: true,
hasDirectionControl: false
hasDirectionControl: false,
},
{
id: 4,
type: VIDEO_TYPES.VEHICLE_MEETING,
title: '指挥车会议视角',
// videoSrc: getVideoUrl('demo/ylzg/无人机视角.mp4'), // 暂时使用无人机视角视频
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/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,
hasZoom: true,
hasDirectionControl: false
}
hasDirectionControl: false,
},
]
// 方向枚举(用于视角控制)
// 方向枚举
export const DIRECTIONS = {
LEFT: 'left',
RIGHT: 'right',
UP: 'up',
DOWN: 'down'
DOWN: 'down',
}
// 现场设备标签页
export const DISPATCH_TABS = [
{ key: 'personnel', label: '现场单兵', count: 23 },
{ key: 'equipment', label: '现场设备', count: 21 },
{ key: 'drone', label: '现场无人机', count: 21 }
{ key: 'drone', label: '现场无人机', count: 21 },
]
// 响应等级
@ -84,21 +89,21 @@ export const RESPONSE_LEVELS = [
{ value: 1, label: '一级', color: '#FF0624' },
{ value: 2, label: '二级', color: '#FF800B' },
{ value: 3, label: '三级', color: '#FFC107' },
{ value: 4, label: '四级', color: '#11BB77' }
{ value: 4, label: '四级', color: '#11BB77' },
]
// 地图观看设备功能
export const DEVICE_WATCH = {
key: 'watchDevice',
label: '正在观看卫星设备',
icon: '正在观看卫星设备'
icon: '正在观看卫星设备',
}
// 地图测量工具(按设计图顺序排列)
// 地图测量工具
export const MAP_TOOLS = [
{ key: 'modelCompare', label: '模型对比', icon: '模型对比' },
{ key: 'measurePosition', label: '测量位置', icon: '测量位置' },
{ key: 'measureDistance', label: '测量距离', icon: '测量距离' },
{ key: 'measureVolume', label: '测量方量', icon: '测量方量' },
{ key: 'clearPoints', label: '清除点', icon: '清除点' }
{ key: 'clearPoints', label: '清除点', icon: '清除点' },
]

View File

@ -0,0 +1,230 @@
/**
* 模拟数据常量
* 用于演示和原型开发后续可替换为真实 API 数据
*/
/**
* 模拟应急人员点位数据
*/
export const MOCK_PERSONNEL_POINTS = [
{
type: 'soldier',
name: '张三',
department: '应急救援队',
lon: 107.97,
lat: 30.25,
distance: 2.5,
estimatedArrival: '10分钟',
},
{
type: 'soldier',
name: '李四',
department: '消防队',
lon: 107.971901,
lat: 30.251428,
distance: 2.3,
estimatedArrival: '8分钟',
},
{
type: 'soldier',
name: '王五',
department: '医疗队',
lon: 107.974901,
lat: 30.241428,
distance: 3.1,
estimatedArrival: '12分钟',
},
{
type: 'soldier',
name: '赵六',
department: '应急救援队',
lon: 108.047344,
lat: 30.164313,
distance: 4.2,
estimatedArrival: '15分钟',
},
{
type: 'soldier',
name: '刘七',
department: '消防队',
lon: 108.046344,
lat: 30.168313,
distance: 3.8,
estimatedArrival: '14分钟',
},
{
type: 'soldier',
name: '陈八',
department: '医疗队',
lon: 108.050344,
lat: 30.170313,
distance: 4.5,
estimatedArrival: '16分钟',
},
]
/**
* 模拟应急装备点位数据
*/
export const MOCK_DEVICE_POINTS = [
{
type: 'device',
name: '救援车辆A',
deviceType: '消防车',
lon: 107.98088,
lat: 30.2487,
distance: 2.8,
estimatedArrival: '11分钟',
},
{
type: 'device',
name: '救援车辆B',
deviceType: '救护车',
lon: 107.97898,
lat: 30.2502,
distance: 2.6,
estimatedArrival: '9分钟',
},
{
type: 'device',
name: '无人机A',
deviceType: 'DJI',
lon: 108.049344,
lat: 30.160313,
distance: 4.8,
estimatedArrival: '17分钟',
},
{
type: 'device',
name: '无人机B',
deviceType: 'DJI',
lon: 108.043344,
lat: 30.169313,
distance: 3.6,
estimatedArrival: '13分钟',
},
]
/**
* 动画路径坐标数据
* 用于"一键启动"功能的人员和设备移动动画
*/
export const ANIMATION_PATHS = {
// 路径1应急救援人员
path1: {
startPoint: {
x: -1706079.1327424292,
y: 5247893.165552528,
z: 3187993.9339800295,
},
waypoints: [
{ x: -1706079.1327424292, y: 5247893.165552528, z: 3187993.9339800295 },
{ x: -1706116.7863268533, y: 5247923.177994122, z: 3187929.297700776 },
{ x: -1706131.4939896727, y: 5247956.7916397555, z: 3187865.1250298577 },
{ x: -1706117.7768181972, y: 5247999.865521995, z: 3187795.4584125844 },
{ x: -1706148.232862157, y: 5248029.100250082, z: 3187735.2203392833 },
{ x: -1706129.4638550146, y: 5248073.941490989, z: 3187662.59740559 },
{ x: -1706131.3071046746, y: 5248086.057462914, z: 3187643.216358425 },
{ x: -1706164.2362053818, y: 5248120.213627388, z: 3187577.1867482658 },
{ x: -1706255.3513903276, y: 5248175.916851786, z: 3187422.819624157 },
{ x: -1706300.2731912779, y: 5248172.011305182, z: 3187397.8767570513 },
{ x: -1706343.1007708232, y: 5248165.925888667, z: 3187382.186124808 },
],
metadata: {
type: 'soldier',
name: '应急救援人员',
department: '应急救援队',
},
},
// 路径2应急设备车
path2: {
startPoint: {
x: -1706935.1617290622,
y: 5248501.604252841,
z: 3186498.2137252213,
},
waypoints: [
{ x: -1706935.1617290622, y: 5248501.604252841, z: 3186498.2137252213 },
{ x: -1706870.4952433936, y: 5248490.611178698, z: 3186558.182971472 },
{ x: -1706837.227189178, y: 5248420.334709732, z: 3186690.581380162 },
{ x: -1706838.280444158, y: 5248431.37975438, z: 3186708.4743944216 },
{ x: -1706773.0696515848, y: 5248373.01216906, z: 3186823.943446862 },
{ x: -1706727.805384834, y: 5248323.242719114, z: 3186915.9148211516 },
{ x: -1706710.854065587, y: 5248281.031240471, z: 3186987.549384787 },
{ x: -1706660.1034110512, y: 5248266.553680115, z: 3187047.6342452955 },
{ x: -1706604.7906532797, y: 5248253.292764083, z: 3187082.311741225 },
{ x: -1706556.3289747096, y: 5248217.41920621, z: 3187155.538613598 },
{ x: -1706516.2888762353, y: 5248217.691641104, z: 3187197.012414378 },
{ x: -1706436.9808225571, y: 5248198.704904958, z: 3187267.311000274 },
{ x: -1706345.7538517928, y: 5248165.481249246, z: 3187379.4674185305 },
],
metadata: {
type: 'device',
name: '应急设备车',
deviceType: '应急装备',
},
},
// 路径3应急救援队员
path3: {
startPoint: {
x: -1707020.4106680197,
y: 5248239.972166492,
z: 3187000.301322692,
},
waypoints: [
{ x: -1707020.4106680197, y: 5248239.972166492, z: 3187000.301322692 },
{ x: -1706846.822522451, y: 5248275.0303560095, z: 3186986.011771928 },
{ x: -1706851.3836389545, y: 5248300.681797178, z: 3186942.7035291386 },
{ x: -1706844.580179418, y: 5248304.977808034, z: 3186932.891914393 },
{ x: -1706818.9276717228, y: 5248278.222342581, z: 3186976.253684671 },
{ x: -1706831.3209432173, y: 5248316.447298632, z: 3186957.2249293313 },
{ x: -1706806.3169318594, y: 5248307.832821272, z: 3186919.6375066047 },
{ x: -1706805.9829374112, y: 5248319.428155191, z: 3186900.13906115 },
{ x: -1706795.069820001, y: 5248321.041829423, z: 3186896.925767256 },
{ x: -1706797.8593846853, y: 5248325.909509062, z: 3186906.0008906457 },
{ x: -1706806.3364665583, y: 5248330.644777029, z: 3186917.773814682 },
{ x: -1706778.3135473165, y: 5248282.550132113, z: 3186963.408730601 },
{ x: -1706750.0602398354, y: 5248252.482706784, z: 3187012.7495610164 },
{ x: -1706738.0772460685, y: 5248263.416236532, z: 3187023.8067475185 },
{ x: -1706691.7246759017, y: 5248270.110156667, z: 3187018.588901565 },
{ x: -1706599.6318286993, y: 5248240.149769867, z: 3187087.254778178 },
{ x: -1706564.1779400432, y: 5248218.9292086465, z: 3187146.335302665 },
{ x: -1706486.5598449395, y: 5248207.86588749, z: 3187216.762631293 },
{ x: -1706445.7375556522, y: 5248203.1875622, z: 3187252.8650745875 },
{ x: -1706409.1757614242, y: 5248182.077526832, z: 3187307.1294440767 },
{ x: -1706408.0269636167, y: 5248192.685664652, z: 3187323.2121423096 },
{ x: -1706390.7352810341, y: 5248211.637499602, z: 3187344.1570185754 },
],
metadata: {
type: 'soldier',
name: '应急救援队员',
department: '应急救援队',
},
},
}
/**
* 视频监控 URL模拟数据
*/
export const MOCK_VIDEO_URL =
'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/单兵视角.mp4'
/**
* 标记点图标尺寸配置
*/
export const MARKER_ICON_SIZE = {
width: 36,
height: 40,
}
/**
* 动画配置
*/
export const ANIMATION_CONFIG = {
duration: 60, // 动画时长(秒)
loadingDuration: 3000, // 加载动画时长(毫秒)
cameraFlyDuration: 5, // 相机飞行时长(秒)
cameraPadding: 0.2, // 相机视野边距20%
}

View File

@ -0,0 +1,247 @@
/**
* 3D
*/
/**
*
*/
export interface Coordinate {
lon: number
lat: number
}
/**
* Cartesian3
*/
export interface Cartesian3Coordinate {
x: number
y: number
z: number
}
/**
*
*/
export interface CameraView {
lon?: number
lat?: number
height: number
heading: number
pitch: number
roll: number
}
/**
*
*/
export interface PersonnelPoint {
type: 'soldier'
name: string
department: string
lon: number
lat: number
distance: number
estimatedArrival: string
location?: string
}
/**
*
*/
export interface DevicePoint {
type: 'device'
name: string
deviceType: string
lon: number
lat: number
distance: number
estimatedArrival: string
location?: string
}
/**
*
*/
export interface AnimationPath {
startPoint: Cartesian3Coordinate
waypoints: Cartesian3Coordinate[]
metadata: {
type: 'soldier' | 'device'
name: string
department?: string
deviceType?: string
}
}
/**
* Tooltip
*/
export interface TooltipField {
label: string
value: string
}
/**
* Tooltip
*/
export interface TooltipAction {
label: string
type: 'link' | 'connect' | 'view'
data: any
}
/**
* Tooltip
*/
export interface TooltipData {
fields: TooltipField[]
actions?: TooltipAction[]
supportVideo?: boolean
videoTitle?: string
videoSrc?: string
}
/**
* Tooltip
*/
export interface TooltipState {
visible: boolean
x: number
y: number
title: string
icon: string
zIndex: number
data: TooltipData | null
}
/**
*
*/
export interface VideoMonitor {
id: string
title: string
videoSrc: string
dateRange: string
hasMegaphone: boolean
hasAudio: boolean
hasDirectionControl: boolean
}
/**
*
*/
export interface PersonnelDetail {
name: string
department: string
distance: number
estimatedArrival: number
avatar: string | null
}
/**
*
*/
export interface EmergencyCenterDetail {
name: string
adminLevel: string
department: string
distance: number
image: string | null
}
/**
*
*/
export interface DisasterInfo {
type: string
volume: number
length: number
width: number
casualties: number
location: string
}
/**
*
*/
export interface ForcePreset {
equipment: number
emergencyBases: number
personnel: number
searchRadius: number
stations: any[]
}
/**
*
*/
export interface ForceDispatch {
responseLevel: string
estimatedClearTime: string
plan: string
}
/**
*
*/
export interface CollaborationInfo {
weatherWarning: any
publicSecurity: any
mediaCenter: any
}
/**
*
*/
export interface IconConfig {
soldierIcon: string
deviceIcon: string
emergencyCenterIcon: string
emergencyBaseIcon: string
reserveCenterIcon: string
}
/**
*
*/
export interface RangeCircleStyle {
fillColor: string
fillAlpha: number
outlineColor: string
outlineAlpha: number
outlineWidth: number
}
/**
*
*/
export interface AnimationConfig {
duration: number
loadingDuration: number
cameraFlyDuration: number
cameraPadding: number
}
/**
* API
*/
export interface ApiResponse<T = any> {
data: T
code?: number
message?: string
}
/**
*
*/
export interface EmergencyResourcesResponse {
stations: any[]
equipment: number
emergencyBases: number
personnel: number
}
/**
*
*/
export type ReserveCentersResponse = any[]