refactor(3d-situational-awareness): 模块化为可组合架构
- 将功能提取到专用的可组合项中(useCesiumLifecycle、useEmergencyDispatch、useMapClickHandler、useMockData、usePathLines、useRangeCircle) - 将常量分离到有组织的文件中(坐标、模拟数据、相机预设) - 添加TypeScript类型定义以提高类型安全性 - 重构主组件以使用可组合模式,提高可维护性和性能 - 更新路由器和组件以支持新架构
This commit is contained in:
parent
ba4b979584
commit
6f2259547e
@ -48,7 +48,8 @@ const routes = [
|
||||
path: '/3DSituationalAwareness',
|
||||
name: '3DSituationalAwareness',
|
||||
meta: {
|
||||
screen: true
|
||||
screen: true,
|
||||
skipInitialCameraView: true // 跳过MapViewport的自动初始视图,由页面自己控制相机
|
||||
},
|
||||
component: () => import('../views/3DSituationalAwarenessRefactor/index.vue')
|
||||
}
|
||||
|
||||
@ -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 (弹窗标题背景)
|
||||
|
||||
@ -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"]);
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -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,8 +202,9 @@ export function useDualMapCompare() {
|
||||
// 设置相机同步(单向:右→左)
|
||||
setupCameraSync(rightViewerInstance, leftViewerInstance)
|
||||
|
||||
// 根据选项决定是否加载左侧模型
|
||||
if (!skipLeftModelLoad) {
|
||||
// 异步加载灾前模型到左侧(不阻塞对比模式启用)
|
||||
// 让模型在后台加载,用户可以立即看到对比效果
|
||||
console.log('[useDualMapCompare] 开始异步加载左侧灾前模型...')
|
||||
load3DTileset(leftViewerInstance, 'before', false)
|
||||
.then(tileset => {
|
||||
@ -186,6 +216,9 @@ export function 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()
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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, // 最大俯仰角(度)
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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: '清除点' },
|
||||
]
|
||||
@ -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%)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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[]
|
||||
Loading…
x
Reference in New Issue
Block a user