diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js new file mode 100644 index 0000000..aa56c92 --- /dev/null +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js @@ -0,0 +1,261 @@ +import { ref } from 'vue' +import * as Cesium from 'cesium' +import soldierIcon from '../assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png' + +/** + * 实体动画管理 Composable + * 负责管理地图实体的路径动画,特别是应急人员沿路径移动 + */ +export function useEntityAnimation() { + // 动画实体引用 + const animatedEntity = ref(null) + + // 动画是否正在运行 + const isAnimating = ref(false) + + /** + * 应急人员路径坐标 (Cartesian3 格式) + * 这些坐标定义了人员从起点到终点的移动路径 + */ + const PERSONNEL_PATH_COORDINATES = [ + { x: -1705480.2641142386, y: 5247435.146513505, z: 3189090.667137187 }, + { x: -1705494.4820981335, y: 5247449.285793587, z: 3189060.452114544 }, + { x: -1705510.0809808762, y: 5247465.767274618, z: 3189023.050648535 }, + { x: -1705511.0338125327, y: 5247476.4118378535, z: 3188998.3643177846 }, + { x: -1705515.8155320068, y: 5247491.504151518, z: 3188970.3225750974 }, + { x: -1705512.9523099929, y: 5247504.710873116, z: 3188943.7936982675 }, + { x: -1705519.7649184526, y: 5247519.060354441, z: 3188915.1883725724 }, + { x: -1705528.241912857, y: 5247539.302819527, z: 3188872.220619207 }, + { x: -1705530.7649293465, y: 5247548.26353356, z: 3188852.4565014304 }, + { x: -1705536.847870567, y: 5247562.107401437, z: 3188816.1164027476 }, + { x: -1705554.2817406887, y: 5247571.234068825, z: 3188789.6105980803 }, + { x: -1705573.026007999, y: 5247580.50225183, z: 3188770.244426234 }, + { x: -1705602.018256302, y: 5247597.236114229, z: 3188743.7470805836 } + ] + + /** + * 启动应急人员沿路径移动动画 + * @param {Cesium.Viewer} viewer - Cesium viewer 实例 + * @param {Object} options - 配置选项 + * @param {number} [options.duration=36] - 动画总时长(秒) + * @param {boolean} [options.trackEntity=false] - 是否相机跟随实体 + * @param {string} [options.personnelName='应急人员'] - 人员名称 + * @param {string} [options.department='应急救援队'] - 所属部门 + * @returns {Cesium.Entity} 返回创建的动画实体 + */ + const startPersonnelMovement = (viewer, options = {}) => { + if (!viewer) { + console.warn('[useEntityAnimation] startPersonnelMovement: viewer 为空') + return null + } + + if (isAnimating.value) { + console.warn('[useEntityAnimation] 动画已在运行中,请先停止当前动画') + return animatedEntity.value + } + + const config = { + duration: options.duration ?? 60, // 默认 60 秒,让移动更清晰可见 + trackEntity: options.trackEntity ?? false, + personnelName: options.personnelName ?? '应急人员', + department: options.department ?? '应急救援队' + } + + console.log('[useEntityAnimation] 开始启动人员移动动画...', config) + + // 设置动画时间范围 + const startTime = Cesium.JulianDate.now() + const stopTime = Cesium.JulianDate.addSeconds( + startTime, + config.duration, + new Cesium.JulianDate() + ) + + // 配置 viewer 时钟 + viewer.clock.startTime = startTime.clone() + viewer.clock.stopTime = stopTime.clone() + viewer.clock.currentTime = startTime.clone() + viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP // 动画结束后停止 + viewer.clock.multiplier = 1 // 实时速度 + viewer.clock.shouldAnimate = true + + // 创建 SampledPositionProperty 定义路径 + const positionProperty = new Cesium.SampledPositionProperty() + + // 计算每个路径点的时间间隔 + const numberOfPoints = PERSONNEL_PATH_COORDINATES.length + const timeInterval = config.duration / (numberOfPoints - 1) + + // 添加路径采样点 + PERSONNEL_PATH_COORDINATES.forEach((coord, index) => { + const time = Cesium.JulianDate.addSeconds( + startTime, + index * timeInterval, + new Cesium.JulianDate() + ) + const position = new Cesium.Cartesian3(coord.x, coord.y, coord.z) + positionProperty.addSample(time, position) + }) + + // 创建脉冲缩放效果 - 让图标闪烁更醒目 + const pulseScale = new Cesium.CallbackProperty((time) => { + const elapsed = Cesium.JulianDate.secondsDifference(time, startTime) + // 使用正弦波产生脉冲效果,频率 3Hz,幅度 ±30% + return 1.0 + Math.sin(elapsed * 3) * 0.3 + }, false) + + // 创建动画实体 + const entity = viewer.entities.add({ + availability: new Cesium.TimeIntervalCollection([ + new Cesium.TimeInterval({ + start: startTime, + stop: stopTime + }) + ]), + position: positionProperty, + orientation: new Cesium.VelocityOrientationProperty(positionProperty), // 自动朝向移动方向 + billboard: { + image: soldierIcon, + width: 48, // 增大尺寸,从 36 增加到 48 + height: 56, // 从 40 增加到 56 + scale: pulseScale, // 使用脉冲缩放效果 + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + // 添加距离缩放,让图标在不同距离下都清晰可见 + scaleByDistance: new Cesium.NearFarScalar( + 1000, 1.5, // 近距离(1000米)放大到 1.5 倍 + 50000, 0.8 // 远距离(50000米)缩小到 0.8 倍 + ) + }, + // 添加发光轨迹线 + path: { + resolution: 1, + material: new Cesium.PolylineGlowMaterialProperty({ + glowPower: 0.4, // 发光强度 + taperPower: 0.5, // 渐变效果 + color: Cesium.Color.CYAN // 青色轨迹 + }), + width: 8, + leadTime: 0, // 前方不显示轨迹 + trailTime: config.duration // 后方显示完整轨迹 + }, + properties: { + type: 'animatedSoldier', + name: config.personnelName, + department: config.department, + isAnimating: true + } + }) + + animatedEntity.value = entity + isAnimating.value = true + + // 可选: 相机跟随实体 + if (config.trackEntity) { + viewer.trackedEntity = entity + // 设置相机视角偏移 + entity.viewFrom = new Cesium.Cartesian3(-100.0, -100.0, 50.0) + } + + console.log('[useEntityAnimation] 人员移动动画已启动') + + // 监听动画结束事件 + const removeListener = viewer.clock.onStop.addEventListener(() => { + console.log('[useEntityAnimation] 动画已结束') + isAnimating.value = false + removeListener() // 移除监听器 + }) + + return entity + } + + /** + * 停止人员移动动画 + * @param {Cesium.Viewer} viewer - Cesium viewer 实例 + * @param {boolean} [removeEntity=true] - 是否移除动画实体 + */ + const stopPersonnelMovement = (viewer, removeEntity = true) => { + if (!viewer) { + console.warn('[useEntityAnimation] stopPersonnelMovement: viewer 为空') + return + } + + console.log('[useEntityAnimation] 停止人员移动动画') + + // 停止时钟动画 + viewer.clock.shouldAnimate = false + + // 取消相机跟随 + if (viewer.trackedEntity === animatedEntity.value) { + viewer.trackedEntity = undefined + } + + // 移除实体 + if (removeEntity && animatedEntity.value) { + viewer.entities.remove(animatedEntity.value) + animatedEntity.value = null + } + + isAnimating.value = false + } + + /** + * 暂停动画 + * @param {Cesium.Viewer} viewer - Cesium viewer 实例 + */ + const pauseAnimation = (viewer) => { + if (!viewer) return + viewer.clock.shouldAnimate = false + console.log('[useEntityAnimation] 动画已暂停') + } + + /** + * 恢复动画 + * @param {Cesium.Viewer} viewer - Cesium viewer 实例 + */ + const resumeAnimation = (viewer) => { + if (!viewer) return + if (isAnimating.value) { + viewer.clock.shouldAnimate = true + console.log('[useEntityAnimation] 动画已恢复') + } + } + + /** + * 获取路径的起点坐标 (用于初始标记点) + * @returns {Cesium.Cartesian3} 起点坐标 + */ + const getStartPosition = () => { + const firstCoord = PERSONNEL_PATH_COORDINATES[0] + return new Cesium.Cartesian3(firstCoord.x, firstCoord.y, firstCoord.z) + } + + /** + * 将 Cartesian3 坐标转换为经纬度 + * @param {Cesium.Cartesian3} cartesian - Cartesian3 坐标 + * @returns {Object} { longitude, latitude, height } + */ + const cartesianToLonLat = (cartesian) => { + const cartographic = Cesium.Cartographic.fromCartesian(cartesian) + return { + longitude: Cesium.Math.toDegrees(cartographic.longitude), + latitude: Cesium.Math.toDegrees(cartographic.latitude), + height: cartographic.height + } + } + + return { + animatedEntity, + isAnimating, + startPersonnelMovement, + stopPersonnelMovement, + pauseAnimation, + resumeAnimation, + getStartPosition, + cartesianToLonLat, + PERSONNEL_PATH_COORDINATES + } +} + +export default useEntityAnimation diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue index 8fc1446..ddfad6e 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue @@ -126,6 +126,18 @@ {{ field.value }} + + + @@ -172,6 +184,7 @@ import { useDualMapCompare } from "./composables/useDualMapCompare"; import { useMapMarkers } from "./composables/useMapMarkers"; import { use3DTiles } from "./composables/use3DTiles"; import { useMapTooltip } from "./composables/useMapTooltip"; +import { useEntityAnimation } from "./composables/useEntityAnimation"; import { useMapStore } from "@/map"; import { request } from "@shared/utils/request"; @@ -233,6 +246,9 @@ const { // 地图 Tooltip 功能 const { tooltipState: mapTooltip, showTooltip, hideTooltip, updateTooltipPosition } = useMapTooltip(); +// 实体动画功能 +const { startPersonnelMovement, stopPersonnelMovement, isAnimating } = useEntityAnimation(); + // 当前显示 tooltip 的实体(用于相机移动时更新位置) const currentTooltipEntity = ref(null); @@ -403,28 +419,45 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => { // 构建 Tooltip 数据 let title = ''; const fields = []; + const actions = []; if (type === 'soldier') { - title = '单兵信息'; + // 应急人员 + title = '应急人员'; fields.push( { label: '姓名', value: properties.name?.getValue() || '-' }, { label: '部门', value: properties.department?.getValue() || '-' }, - { label: '位置', value: properties.location?.getValue() || '-' } + { label: '位置信息', value: properties.location?.getValue() || '-' }, + { label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' } ); + actions.push({ + label: '联动', + type: 'link', + data: entity + }); } else if (type === 'device') { - title = '设备信息'; + // 应急装备 + title = '应急装备'; fields.push( { label: '设备名称', value: properties.name?.getValue() || '-' }, { label: '设备类型', value: properties.deviceType?.getValue() || '-' }, - { label: '位置', value: properties.location?.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.address?.getValue() || '-' }, - { label: '距离', value: properties.distance?.getValue() || '-' } + { 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: entity + }); } else if (type === 'station') { const stationName = properties.name?.getValue() || ''; const distance = properties.distance?.getValue() || 0; @@ -451,17 +484,14 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => { title = '储备中心'; fields.push( { label: '名称', value: properties.name?.getValue() || '-' }, - { label: '区县', value: properties.district?.getValue() || '-' }, - { label: '人员数量', value: properties.personnelCount?.getValue() || '0' }, - { label: '占地面积', value: properties.area?.getValue() ? `${properties.area?.getValue()}㎡` : '-' } + { label: '位置信息', value: properties.location?.getValue() || '-' } ); } else if (type === 'presetPoint') { // 预置点 title = '预置点'; fields.push( { label: '名称', value: properties.name?.getValue() || '-' }, - { label: '区县', value: properties.district?.getValue() || '-' }, - { label: '人员数量', value: properties.personnelCount?.getValue() || '0' } + { label: '位置信息', value: properties.location?.getValue() || '-' } ); } @@ -471,7 +501,7 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => { y: canvasPosition.y, title, icon, - data: { fields } + data: { fields, actions: actions.length > 0 ? actions : undefined } }); // 保存当前实体,用于相机移动时更新位置 @@ -554,17 +584,17 @@ onMounted(() => { // 在默认点附近添加10个模拟点位(应急人员和应急装备) const simulatedPoints = [ // 应急人员 (6个) - { type: 'soldier', name: '张三', department: '应急救援队', lon: 107.97, lat: 30.25, distance: 2.5, icon: soldierIcon }, - { type: 'soldier', name: '李四', department: '消防队', lon: 107.971901, lat: 30.251428, distance: 2.3, icon: soldierIcon }, - { type: 'soldier', name: '王五', department: '医疗队', lon: 107.974901, lat: 30.241428, distance: 3.1, icon: soldierIcon }, - { type: 'soldier', name: '赵六', department: '应急救援队', lon: 108.047344, lat: 30.164313, distance: 4.2, icon: soldierIcon }, - { type: 'soldier', name: '刘七', department: '消防队', lon: 108.046344, lat: 30.168313, distance: 3.8, icon: soldierIcon }, - { type: 'soldier', name: '陈八', department: '医疗队', lon: 108.050344, lat: 30.170313, distance: 4.5, icon: soldierIcon }, + { type: 'soldier', name: '张三', department: '应急救援队', lon: 107.97, lat: 30.25, distance: 2.5, estimatedArrival: '10分钟', icon: soldierIcon }, + { type: 'soldier', name: '李四', department: '消防队', lon: 107.971901, lat: 30.251428, distance: 2.3, estimatedArrival: '8分钟', icon: soldierIcon }, + { type: 'soldier', name: '王五', department: '医疗队', lon: 107.974901, lat: 30.241428, distance: 3.1, estimatedArrival: '12分钟', icon: soldierIcon }, + { type: 'soldier', name: '赵六', department: '应急救援队', lon: 108.047344, lat: 30.164313, distance: 4.2, estimatedArrival: '15分钟', icon: soldierIcon }, + { type: 'soldier', name: '刘七', department: '消防队', lon: 108.046344, lat: 30.168313, distance: 3.8, estimatedArrival: '14分钟', icon: soldierIcon }, + { type: 'soldier', name: '陈八', department: '医疗队', lon: 108.050344, lat: 30.170313, distance: 4.5, estimatedArrival: '16分钟', icon: soldierIcon }, // 应急装备 (4个) - { type: 'device', name: '救援车辆A', deviceType: '消防车', lon: 107.98088, lat: 30.2487, distance: 2.8, icon: deviceIcon }, - { type: 'device', name: '救援车辆B', deviceType: '救护车', lon: 107.97898, lat: 30.2502, distance: 2.6, icon: deviceIcon }, - { type: 'device', name: '无人机A', deviceType: 'DJI', lon: 108.049344, lat: 30.160313, distance: 4.8, icon: deviceIcon }, - { type: 'device', name: '无人机B', deviceType: 'DJI', lon: 108.043344, lat: 30.169313, distance: 3.6, icon: deviceIcon } + { type: 'device', name: '救援车辆A', deviceType: '消防车', lon: 107.98088, lat: 30.2487, distance: 2.8, estimatedArrival: '11分钟', icon: deviceIcon }, + { type: 'device', name: '救援车辆B', deviceType: '救护车', lon: 107.97898, lat: 30.2502, distance: 2.6, estimatedArrival: '9分钟', icon: deviceIcon }, + { type: 'device', name: '无人机A', deviceType: 'DJI', lon: 108.049344, lat: 30.160313, distance: 4.8, estimatedArrival: '17分钟', icon: deviceIcon }, + { type: 'device', name: '无人机B', deviceType: 'DJI', lon: 108.043344, lat: 30.169313, distance: 3.6, estimatedArrival: '13分钟', icon: deviceIcon } ]; simulatedPoints.forEach(point => { @@ -583,13 +613,15 @@ onMounted(() => { type: 'soldier', name: point.name, department: point.department, - location: `目前为止距离现场${point.distance}公里` + location: `目前为止距离现场${point.distance}公里`, + estimatedArrival: point.estimatedArrival } : { type: 'device', name: point.name, deviceType: point.deviceType, - location: `目前为止距离现场${point.distance}公里` + location: `目前为止距离现场${point.distance}公里`, + estimatedArrival: point.estimatedArrival } }); }); @@ -888,6 +920,55 @@ const handlePersonnelLink = (personnel) => { showPersonnelDetail.value = false; }; +/** + * 处理 Tooltip 操作按钮点击事件 + * @param {Object} action - 操作对象,包含 type 和 data + */ +const handleTooltipAction = (action) => { + console.log('[index.vue] Tooltip 操作按钮点击:', action); + + if (action.type === 'link') { + // 应急人员的"联动"操作 + const entity = action.data; + const properties = entity.properties; + + console.log('[index.vue] 应急人员联动:', { + name: properties.name?.getValue(), + department: properties.department?.getValue() + }); + + // 可以在这里实现联动功能,比如: + // 1. 飞行到人员位置 + // 2. 打开详情弹窗 + // 3. 高亮显示相关信息 + ElMessage.success(`已联动应急人员: ${properties.name?.getValue() || '未知'}`); + + // 关闭 tooltip + hideTooltip(); + currentTooltipEntity.value = null; + + } else if (action.type === 'connect') { + // 应急基地的"连线"操作 + const entity = action.data; + const properties = entity.properties; + + console.log('[index.vue] 应急基地连线:', { + name: properties.name?.getValue(), + routeNumber: properties.routeNumber?.getValue() + }); + + // 可以在这里实现连线功能,比如: + // 1. 在地图上绘制线路 + // 2. 显示路径规划 + // 3. 计算距离和时间 + ElMessage.success(`已连线应急基地: ${properties.name?.getValue() || '未知'}`); + + // 关闭 tooltip + hideTooltip(); + currentTooltipEntity.value = null; + } +}; + /** * 关闭地图 Tooltip * 统一的关闭入口,便于后续扩展埋点或联动逻辑 @@ -898,7 +979,7 @@ const handleMapTooltipClose = () => { /** * 处理力量调度启动事件 - * 显示加载动画,3秒后自动隐藏 + * 显示加载动画,3秒后自动隐藏,然后启动人员移动动画 */ const handleStartDispatch = (payload) => { console.log('[index.vue] 启动力量调度:', payload); @@ -906,9 +987,22 @@ const handleStartDispatch = (payload) => { // 显示加载动画 showLoading.value = true; - // 3秒后自动隐藏加载动画 + // 3秒后隐藏加载动画并启动人员移动 setTimeout(() => { showLoading.value = false; + + // 启动应急人员沿路径移动动画 + if (mapStore.viewer) { + console.log('[index.vue] 启动应急人员移动动画...'); + startPersonnelMovement(mapStore.viewer, { + duration: 60, // 60秒完成整个路径,移动更清晰可见 + trackEntity: false, // 不自动跟随相机(可设为 true 启用跟随) + personnelName: '应急救援人员', + department: '应急救援队' + }); + } else { + console.warn('[index.vue] 地图viewer未就绪,无法启动动画'); + } }, 3000); }; @@ -1401,6 +1495,32 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => { } } +// Tooltip 操作按钮样式 +// 用于"连线"、"联动"等交互按钮 +.tooltip-action-btn { + min-width: vw(100); + height: vh(36); + padding: 0 vw(24); + background: url('./assets/images/地图tooltip-button.png') no-repeat center/100% 100%; + border: none; + color: var(--text-white); + font-size: fs(14); + font-family: SourceHanSansCN-Medium, sans-serif; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + + &:hover { + filter: brightness(1.2); + transform: translateY(vh(-2)); + } + + &:active { + filter: brightness(0.9); + transform: translateY(0); + } +} + // 窄容器嵌入的紧凑布局(<1100px 宽度) .situational-awareness.is-compact { --sa-left-width: calc(380 / 1920 * var(--cq-inline-100, 100vw));