bxztApp/packages/screen/src/views/cockpit/composables/useEmergencyForceInteraction.js
Zzc 548226263a feat(cockpit): 添加紧急力量地图交互和工具提示
- 添加用于获取紧急力量列表和详细信息的 API 函数
- 将地图上的紧急力量标记与切换功能集成
- 实现用于在点击时显示力量详细信息的工具提示组件
- 创建可组合函数以处理地图交互,包括点击事件和位置更新
- 更新驾驶舱布局以支持覆盖图层和工具提示定位
2025-11-13 18:01:02 +08:00

469 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 应急力量地图交互 Composable
* 处理应急力量标记点的点击事件,使用 HTML Overlay 显示详情信息
*
* @module composables/useEmergencyForceInteraction
*/
import { ref, watch, onBeforeUnmount } from 'vue'
import * as Cesium from 'cesium'
import { fetchEmergencyForceDetail } from '../api/emergencyForce'
/**
* 应急力量地图交互 Hook
*
* @param {Object} mapStore - 地图 Store 实例
* @param {Object} options - 配置选项
* @param {boolean} [options.flyToPoint=false] - 点击时是否飞行到点位
* @param {number} [options.flyDuration=1.5] - 飞行动画时长(秒)
* @param {number} [options.flyDistance=500] - 飞行后的相机距离(米)
* @returns {Object} 交互状态和方法
*/
export function useEmergencyForceInteraction(mapStore, options = {}) {
// ==================== 配置选项 ====================
const config = {
flyToPoint: options.flyToPoint ?? false,
flyDuration: options.flyDuration ?? 1.5,
flyDistance: options.flyDistance ?? 500
}
// ==================== 状态管理 ====================
/**
* 是否启用交互
*/
const enabled = ref(false)
/**
* 当前选中的实体
*/
const selectedEntity = ref(null)
/**
* Tooltip 是否可见
*/
const tooltipVisible = ref(false)
/**
* Tooltip 屏幕位置
* @type {Ref<{x: number, y: number}|null>}
*/
const tooltipPosition = ref(null)
/**
* Tooltip 数据
* @type {Ref<{qxmc?: string, yjllpz?: string, wzQtwz?: string}>}
*/
const tooltipData = ref({})
/**
* 加载状态
*/
const loading = ref(false)
/**
* 错误信息
*/
const error = ref('')
/**
* 请求取消控制器
*/
const abortController = ref(null)
/**
* Cesium 事件处理器
*/
let clickHandler = null
/**
* postRender 事件监听器
*/
let postRenderListener = null
// ==================== 工具函数 ====================
/**
* 获取 Cesium Viewer 实例
* @returns {Cesium.Viewer|null}
*/
const getViewer = () => {
if (!mapStore.isReady()) {
console.warn('地图尚未就绪')
return null
}
return mapStore.getViewer()
}
/**
* 取消正在进行的请求
*/
const cancelRequest = () => {
if (abortController.value) {
try {
abortController.value.abort()
} catch (error) {
console.warn('取消请求失败', error)
} finally {
abortController.value = null
}
}
}
/**
* 更新 Tooltip 位置
* 将 3D 世界坐标转换为屏幕坐标
* 特别处理 clampToGround 实体,确保使用地形高度
*/
const updateTooltipPosition = () => {
if (!selectedEntity.value || !tooltipVisible.value) {
return
}
const viewer = getViewer()
if (!viewer) return
try {
const position = selectedEntity.value.position?.getValue(Cesium.JulianDate.now())
if (!position) return
// 处理 clampToGround 实体
// 对于使用 CLAMP_TO_GROUND 的 billboard需要获取实际的地形高度
let clampedPosition = position
// 转换为地理坐标
const cartographic = Cesium.Cartographic.fromCartesian(position)
// 尝试获取地形高度
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic)
// 如果成功获取地形高度,使用地形高度重建位置
if (Cesium.defined(height)) {
cartographic.height = height
clampedPosition = Cesium.Cartographic.toCartesian(cartographic)
} else {
// 备用方案:尝试使用 clampToHeight
const clampedCartesian = viewer.scene.clampToHeight(position)
if (clampedCartesian) {
clampedPosition = clampedCartesian
}
}
}
// 使用修正后的位置进行屏幕坐标转换
const screenPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition)
if (screenPosition) {
tooltipPosition.value = {
x: screenPosition.x,
y: screenPosition.y
}
}
} catch (error) {
console.error('更新 Tooltip 位置失败:', error)
}
}
/**
* 注册 postRender 监听器
* 实时更新 Tooltip 位置(相机移动时)
*/
const registerPostRenderListener = () => {
const viewer = getViewer()
if (!viewer || postRenderListener) return
postRenderListener = () => {
updateTooltipPosition()
}
viewer.scene.postRender.addEventListener(postRenderListener)
}
/**
* 注销 postRender 监听器
*/
const unregisterPostRenderListener = () => {
const viewer = getViewer()
if (!viewer || !postRenderListener) return
try {
viewer.scene.postRender.removeEventListener(postRenderListener)
postRenderListener = null
} catch (error) {
console.error('注销 postRender 监听器失败:', error)
}
}
/**
* 相机飞行到指定实体
* @param {Cesium.Entity} entity - 目标实体
* @returns {Promise<void>}
*/
const flyToEntity = async (entity) => {
const viewer = getViewer()
if (!viewer || !config.flyToPoint) return
try {
const position = entity.position?.getValue(Cesium.JulianDate.now())
if (!position) return
await viewer.camera.flyTo({
destination: position,
duration: config.flyDuration,
offset: new Cesium.HeadingPitchRange(
0,
Cesium.Math.toRadians(-45),
config.flyDistance
)
})
} catch (error) {
console.error('相机飞行失败:', error)
}
}
/**
* 显示 Tooltip
* @param {Cesium.Entity} entity - 目标实体
* @param {Object} data - 详情数据
*/
const showTooltip = (entity, data) => {
selectedEntity.value = entity
tooltipData.value = data
tooltipVisible.value = true
updateTooltipPosition()
registerPostRenderListener()
}
/**
* 隐藏 Tooltip
*/
const hideTooltip = () => {
tooltipVisible.value = false
tooltipPosition.value = null
tooltipData.value = {}
selectedEntity.value = null
error.value = ''
unregisterPostRenderListener()
}
// ==================== 核心逻辑 ====================
/**
* 处理应急力量实体点击
* @param {Cesium.Entity} entity - 被点击的实体
*/
const handleEmergencyForceClick = async (entity) => {
// 取消之前的请求
cancelRequest()
// 提取 rid
const rid = entity.properties?.rid?.getValue()
if (!rid) {
console.warn('应急力量实体缺少 rid 属性')
error.value = '缺少标识信息'
showTooltip(entity, {})
return
}
// 显示加载状态
loading.value = true
error.value = ''
showTooltip(entity, {})
// 可选:飞行到点位
await flyToEntity(entity)
// 创建取消控制器
const controller = new AbortController()
abortController.value = controller
try {
// 请求详情数据
const response = await fetchEmergencyForceDetail(rid, {
signal: controller.signal
})
// 检查是否仍然选中该实体
if (selectedEntity.value !== entity) {
return
}
// 解析响应数据
let detailData = response
if (response && typeof response === 'object') {
if (response.data) {
detailData = response.data
}
}
// 更新 Tooltip 数据
tooltipData.value = {
qxmc: detailData.qxmc || '',
yjllpz: detailData.yjllpz || '',
wzQtwz: detailData.wzQtwz || ''
}
} catch (err) {
// 处理请求取消
if (err.name === 'AbortError' || err.name === 'CanceledError') {
return
}
// 处理其他错误
console.error('加载应急力量详情失败:', err)
let errorMessage = '加载失败'
if (err.response) {
errorMessage += `: ${err.response.status}`
if (err.response.data?.message) {
errorMessage += ` - ${err.response.data.message}`
}
} else if (err.message) {
errorMessage += `: ${err.message}`
}
error.value = errorMessage
} finally {
loading.value = false
abortController.value = null
}
}
/**
* 处理地图点击事件
* @param {Object} click - 点击事件对象
*/
const handleMapClick = (click) => {
if (!enabled.value) return
const viewer = getViewer()
if (!viewer) return
try {
// 拾取点击的对象
const pickedObject = viewer.scene.pick(click.position)
// 检查是否点击了应急力量实体
if (
pickedObject &&
pickedObject.id &&
pickedObject.id.id &&
typeof pickedObject.id.id === 'string' &&
pickedObject.id.id.startsWith('emergencyForce-')
) {
// 点击了应急力量标记
handleEmergencyForceClick(pickedObject.id)
} else {
// 点击了其他地方,隐藏 Tooltip
if (tooltipVisible.value) {
hideTooltip()
}
}
} catch (error) {
console.error('处理地图点击失败:', error)
}
}
/**
* 注册点击事件监听器
*/
const registerClickHandler = () => {
const viewer = getViewer()
if (!viewer) {
console.warn('无法注册点击事件:地图未就绪')
return
}
// 避免重复注册
if (clickHandler) {
console.warn('点击事件已注册')
return
}
try {
clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
clickHandler.setInputAction(
handleMapClick,
Cesium.ScreenSpaceEventType.LEFT_CLICK
)
console.log('应急力量点击事件已注册')
} catch (error) {
console.error('注册点击事件失败', error)
}
}
/**
* 注销点击事件监听器
*/
const unregisterClickHandler = () => {
if (clickHandler) {
try {
clickHandler.destroy()
clickHandler = null
console.log('应急力量点击事件已注销')
} catch (error) {
console.error('注销点击事件失败', error)
}
}
}
/**
* 清理所有资源
*/
const cleanup = () => {
cancelRequest()
hideTooltip()
unregisterClickHandler()
unregisterPostRenderListener()
loading.value = false
}
// ==================== 生命周期 ====================
/**
* 监听启用状态变化
*/
watch(enabled, (newValue) => {
if (newValue) {
// 启用交互
if (mapStore.isReady()) {
registerClickHandler()
} else {
// 等待地图就绪
mapStore.onReady(() => {
if (enabled.value) {
registerClickHandler()
}
})
}
} else {
// 禁用交互
cleanup()
}
})
/**
* 组件卸载前清理
*/
onBeforeUnmount(() => {
cleanup()
})
// ==================== 返回接口 ====================
return {
// 状态
enabled,
loading,
tooltipVisible,
tooltipPosition,
tooltipData,
error,
// 方法
cleanup,
hideTooltip
}
}