diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/components/LeftPanel/ForceDispatch.vue b/packages/screen/src/views/3DSituationalAwarenessRefactor/components/LeftPanel/ForceDispatch.vue index 7c981da..4b14f8d 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/components/LeftPanel/ForceDispatch.vue +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/components/LeftPanel/ForceDispatch.vue @@ -26,7 +26,7 @@
应急基地与预置点 - {{ dispatchSuggestion.stations }} + {{ dispatchSuggestion.bases }}
diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDisasterData.js b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDisasterData.js index b1bdf01..7c6c8cb 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDisasterData.js +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDisasterData.js @@ -98,16 +98,19 @@ export function useDisasterData() { const stationsCount = forcePreset.value.stations?.length || 0 return { - // 应急物资建议数:基于装备数量 - supplies: forcePreset.value.equipment, + // 应急物资建议数 + supplies: forcePreset.value.materials, - // 应急人员建议数:取总人员的一部分(约5-10%)作为调度建议 - personnel: Math.min(Math.ceil(forcePreset.value.personnel * 0.06), forcePreset.value.personnel), + // 应急人员建议数 + personnel: forcePreset.value.personnel, // 养护站建议数:使用实际可用养护站数量 stations: stationsCount, - // 挖掘机建议数:每2个养护站配置1台挖掘机 + // 应急基地 + bases: forcePreset.value.bases, + + // 挖掘机建议数 excavators: Math.max(1, Math.ceil(stationsCount / 2)), // 阻断信息:固定建议 @@ -140,36 +143,167 @@ export function useDisasterData() { } } + /** + * 将储备中心/预置点接口返回的数据转换为通用 stations 结构 + * @param {Array} reserveData - /yhYjll/list 接口数据 + * @param {Object} disasterCenter - 灾害中心点坐标 { longitude, latitude } + * @returns {Array} 标准化的 stations 数组 + */ + const transformReserveDataToStations = (reserveData = [], disasterCenter = {}) => { + if (!Array.isArray(reserveData)) { + console.warn('[useDisasterData] transformReserveDataToStations: 输入数据不是数组') + return [] + } + + if (reserveData.length === 0) { + console.warn('[useDisasterData] transformReserveDataToStations: 输入数据为空数组') + return [] + } + + console.log(`[useDisasterData] 开始转换 ${reserveData.length} 条储备中心/预置点数据`) + + return reserveData.map((item, index) => { + // 1. 提取并验证类型 + const typeCode = String(item?.gl1Lx ?? '').trim() + if (!typeCode) { + console.warn(`[useDisasterData] 数据项 ${index} 缺少 gl1Lx 字段:`, item) + } + const isReserveCenter = typeCode === '2' + const type = isReserveCenter ? 'reserveCenter' : 'presetPoint' + + // 2. 提取经纬度 + const longitude = typeof item?.gl1Lng === 'number' ? item.gl1Lng : null + const latitude = typeof item?.gl1Lat === 'number' ? item.gl1Lat : null + + // 3. 计算距离(如果接口未提供) + let distance = null + if ( + longitude !== null && + latitude !== null && + typeof disasterCenter?.longitude === 'number' && + typeof disasterCenter?.latitude === 'number' + ) { + distance = Number( + calculateDistance( + { longitude, latitude }, + { longitude: disasterCenter.longitude, latitude: disasterCenter.latitude } + ).toFixed(2) + ) + } + + // 4. 构造标准格式 + const station = { + id: item?.gl1Id || `reserve-${index}`, + name: item?.gl1Yjllmc?.trim() || (isReserveCenter ? '储备中心' : '预置点'), + distance, + type, + longitude, + latitude, + district: item?.gl1Qxmc || '-', + personnelCount: Number(item?.gl1Rysl) || 0, + area: item?.gl1Zdmj || '-' + } + + console.log(`[useDisasterData] 转换成功: ${station.name} (${type}, ${distance}km)`) + return station + }) + } + + /** + * 计算两个经纬度点之间的距离(公里) + * 使用 Haversine 公式 + * @param {Object} pointA - { longitude, latitude } + * @param {Object} pointB - { longitude, latitude } + * @returns {number} 距离(km) + */ + const EARTH_RADIUS_KM = 6371 + const calculateDistance = (pointA, pointB) => { + if ( + typeof pointA?.longitude !== 'number' || + typeof pointA?.latitude !== 'number' || + typeof pointB?.longitude !== 'number' || + typeof pointB?.latitude !== 'number' + ) { + console.warn('[useDisasterData] calculateDistance: 坐标参数无效') + return 0 + } + + const lat1 = toRadians(pointA.latitude) + const lat2 = toRadians(pointB.latitude) + const deltaLat = toRadians(pointB.latitude - pointA.latitude) + const deltaLon = toRadians(pointB.longitude - pointA.longitude) + + const a = + Math.sin(deltaLat / 2) ** 2 + + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) ** 2 + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) + + return EARTH_RADIUS_KM * c + } + + /** + * 角度转弧度 + * @param {number} degrees - 角度值 + * @returns {number} 弧度值 + */ + const toRadians = (degrees) => (degrees * Math.PI) / 180 + /** * 更新力量预置数据 * @param {Object} emergencyResourcesData - 接口返回的应急资源数据 * @param {number} emergencyResourcesData.equipmentCount - 应急装备数量 * @param {number} emergencyResourcesData.personnelCount - 应急人员数量 + * @param {number} emergencyResourcesData.materialCount - 应急物资数量 + * @param {number} emergencyResourcesData.baseCount - 应急基地数量 * @param {Array} emergencyResourcesData.stations - 养护站列表 * @param {string} emergencyResourcesData.stations[].stationId - 养护站ID * @param {string} emergencyResourcesData.stations[].stationName - 养护站名称 * @param {number} emergencyResourcesData.stations[].distance - 距离(km) + * @param {Object} options - 可选配置 + * @param {boolean} options.onlyStatistics - 是否仅更新统计数据(不更新stations) */ - const updateForcePreset = (emergencyResourcesData) => { + const updateForcePreset = (emergencyResourcesData, options = {}) => { if (!emergencyResourcesData) { console.warn('[useDisasterData] 应急资源数据为空,跳过更新') return } - const { equipmentCount, personnelCount, stations } = emergencyResourcesData + const { onlyStatistics = false } = options + const { + equipmentCount, + personnelCount, + materialCount, + baseCount, + stations + } = emergencyResourcesData - // 更新装备和人员数量 + // 更新装备数量 if (typeof equipmentCount === 'number') { forcePreset.value.equipment = equipmentCount } + + // 更新人员数量 if (typeof personnelCount === 'number') { forcePreset.value.personnel = personnelCount } - // 更新养护站列表 - if (Array.isArray(stations)) { - // 根据养护站列表更新应急基地数 - forcePreset.value.bases = stations.length + // 更新物资数量 (新增) + if (typeof materialCount === 'number') { + forcePreset.value.materials = materialCount + } + + // 更新基地数量 (新增) + if (typeof baseCount === 'number') { + forcePreset.value.bases = baseCount + } + + // 仅当不是"仅统计"模式时才更新养护站列表 + if (!onlyStatistics && Array.isArray(stations)) { + // 如果baseCount未提供,则从stations长度推导 + if (typeof baseCount !== 'number') { + forcePreset.value.bases = stations.length + } + forcePreset.value.stations = stations.map((station) => ({ id: station.stationId, name: station.stationName?.trim() || '未命名养护站', @@ -191,6 +325,7 @@ export function useDisasterData() { dispatchSuggestion, totalResources, updateForcePreset, - updateSearchRadius + updateSearchRadius, + transformReserveDataToStations } } diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue index 6a14710..476b96b 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue @@ -327,6 +327,8 @@ const { showMarkers, hideMarkers, markerEntities, + reserveCenterEntities, + emergencyResourceEntities, } = useMapMarkers() // 3D Tiles 加载 @@ -682,19 +684,22 @@ const handleMapToolChange = async ({ tool, active }) => { const handleDistanceChange = async (newDistance) => { console.log(`[index.vue] 距离范围改变为: ${newDistance}km`) - // 更新搜索半径 + // 1. 更新搜索半径 disasterData.updateSearchRadius(newDistance) - // 更新范围圈 + // 2. 更新范围圈 if (mapStore.viewer) { createOrUpdateRangeCircle(mapStore.viewer, newDistance) } - // 重新加载应急资源数据并更新地图标记 - await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat) + // 3. 重新加载应急资源数据并更新地图标记 + // await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat) - // 重新加载储备中心和预置点数据并更新地图标记 + // 4. 重新加载储备中心和预置点数据并更新地图标记 await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat) + + // 5. 重新加载应急统计数据 (新增) + await loadEmergencyBaseAndPreset(DISASTER_CENTER.lon, DISASTER_CENTER.lat) } /** @@ -704,53 +709,154 @@ const showAllMarkersAndRange = () => { const viewer = mapStore.viewer if (!viewer) return - // 1. 显示模拟点位 + console.log('[index.vue] 开始显示快速响应标记...') + + // 1. 清除/隐藏其他所有标记 + // 1.1 隐藏模拟点位(soldier/device)和路径起点标记 viewer.entities.values.forEach((entity) => { if (entity.properties) { const props = entity.properties - if (props.type?.getValue() === 'soldier' || - props.type?.getValue() === 'device' || - props.isPathStartMarker?.getValue()) { - entity.show = true - } - } - }) - - // 2. 显示接口标记 - showMarkers() - - // 3. 显示范围圈 - showRangeCircle() - - console.log('[index.vue] 已显示所有标记点和范围圈') -} - -/** - * 隐藏所有标记点和范围圈 - */ -const hideAllMarkersAndRange = () => { - const viewer = mapStore.viewer - if (!viewer) return - - // 1. 隐藏模拟点位 - viewer.entities.values.forEach((entity) => { - if (entity.properties) { - const props = entity.properties - if (props.type?.getValue() === 'soldier' || - props.type?.getValue() === 'device' || + const type = props.type?.getValue() + if (type === 'soldier' || + type === 'device' || props.isPathStartMarker?.getValue()) { entity.show = false } } }) - // 2. 隐藏接口标记 + // 1.2 隐藏应急资源标记(养护站等) hideMarkers() + console.log('[index.vue] 已清除其他标记') + + // 2. 显示储备中心/预置点标记 + reserveCenterEntities.value.forEach((entity) => { + if (entity) { + entity.show = true + } + }) + console.log(`[index.vue] 显示 ${reserveCenterEntities.value.length} 个储备中心/预置点标记`) + + // 3. 在灾害中心点附近添加人员和装备点位 + const offset = 0.0002 // 约22米偏移,更明显 + + // 检查是否已存在中心点人员和装备标记,避免重复添加 + let existingCenterPersonnel = viewer.entities.values.find( + e => e.properties && e.properties.type && e.properties.type.getValue() === 'centerPersonnel' + ) + let existingCenterEquipment = viewer.entities.values.find( + e => e.properties && e.properties.type && e.properties.type.getValue() === 'centerEquipment' + ) + + // 添加人员点位(如果不存在) + if (!existingCenterPersonnel) { + const personnelEntity = viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(DISASTER_CENTER.lon + offset, DISASTER_CENTER.lat, 0), + billboard: { + image: soldierIcon, + width: 36, + height: 40, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: Number.POSITIVE_INFINITY + }, + properties: new Cesium.PropertyBag({ + type: 'centerPersonnel', + name: '应急人员' + }), + show: true + }) + console.log('[index.vue] 已添加中心点人员标记:', personnelEntity.id) + } else { + existingCenterPersonnel.show = true + console.log('[index.vue] 显示已存在的中心点人员标记') + } + + // 添加装备点位(如果不存在) + if (!existingCenterEquipment) { + const equipmentEntity = viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(DISASTER_CENTER.lon - offset, DISASTER_CENTER.lat, 0), + billboard: { + image: deviceIcon, + width: 36, + height: 40, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: Number.POSITIVE_INFINITY + }, + properties: new Cesium.PropertyBag({ + type: 'centerEquipment', + name: '应急装备' + }), + show: true + }) + console.log('[index.vue] 已添加中心点装备标记:', equipmentEntity.id) + } else { + existingCenterEquipment.show = true + console.log('[index.vue] 显示已存在的中心点装备标记') + } + + // 强制渲染场景 + viewer.scene.requestRender() + + // 4. 显示范围圈 + showRangeCircle() + + console.log('[index.vue] 快速响应标记显示完成') +} + +/** + * 隐藏所有标记点和范围圈 + * 恢复到初始状态(隐藏快速响应相关的标记) + */ +const hideAllMarkersAndRange = () => { + const viewer = mapStore.viewer + if (!viewer) return + + console.log('[index.vue] 开始隐藏快速响应标记...') + + // 1. 隐藏储备中心/预置点标记 + reserveCenterEntities.value.forEach((entity) => { + if (entity) { + entity.show = false + } + }) + console.log(`[index.vue] 隐藏 ${reserveCenterEntities.value.length} 个储备中心/预置点标记`) + + // 2. 隐藏中心点人员和装备标记 + viewer.entities.values.forEach((entity) => { + if (entity.properties?.type) { + const type = entity.properties.type.getValue() + if (type === 'centerPersonnel' || type === 'centerEquipment') { + entity.show = false + } + } + }) + console.log('[index.vue] 隐藏中心点人员和装备标记') + // 3. 隐藏范围圈 hideRangeCircle() - console.log('[index.vue] 已隐藏所有标记点和范围圈') + // 4. 恢复显示模拟点位(如果需要的话,这里暂时不恢复,保持隐藏状态) + // 如果需要恢复显示,取消下面的注释 + // viewer.entities.values.forEach((entity) => { + // if (entity.properties) { + // const props = entity.properties + // const type = props.type?.getValue() + // if (type === 'soldier' || + // type === 'device' || + // props.isPathStartMarker?.getValue()) { + // entity.show = true + // } + // } + // }) + + // 5. 恢复显示应急资源标记(如果需要的话,这里暂时不恢复) + // 如果需要恢复显示,取消下面的注释 + // showMarkers() + + console.log('[index.vue] 快速响应标记隐藏完成') } /** @@ -857,6 +963,19 @@ const loadReserveCentersAndPresets = async (longitude, latitude) => { if (response?.data && Array.isArray(response.data)) { console.log('[index.vue] 储备中心和预置点数据加载成功:', response.data) + // 1. 转换数据为标准 stations 格式并更新到 forcePreset + const transformedStations = disasterData.transformReserveDataToStations( + response.data, + { longitude, latitude } + ) + + if (transformedStations.length > 0) { + // 更新 forcePreset.stations (用于 station-list 显示) + disasterData.forcePreset.value.stations = transformedStations + console.log('[index.vue] 已更新 forcePreset.stations:', transformedStations) + } + + // 2. 添加地图标记 if (mapStore.viewer) { console.log('[index.vue] 添加储备中心和预置点地图标记...') clearReserveCenterMarkers(mapStore.viewer) @@ -880,9 +999,10 @@ const loadReserveCentersAndPresets = async (longitude, latitude) => { } /** - * 统计应急基地与预置点、应急装备、应急物资、应急人员的数量 /snow-ops-platform/yhYjll/statistics + * 加载应急基地与预置点、应急装备、应急物资、应急人员的统计数量 + * /snow-ops-platform/yhYjll/statistics */ -const loadEmergencyBaseAndPreset = async (longitude, latitude, maxDistance) => { +const loadEmergencyBaseAndPreset = async (longitude, latitude) => { try { const response = await request({ url: `/snow-ops-platform/yhYjll/statistics`, @@ -893,16 +1013,24 @@ const loadEmergencyBaseAndPreset = async (longitude, latitude, maxDistance) => { maxDistance: disasterData.forcePreset.value.searchRadius, }, }) + if (response?.data) { - console.log('[index.vue] 应急基地与预置点、应急装备、应急物资、应急人员的数量加载成功:', response.data) + // 保存统计数据到状态 (仅更新统计,不更新stations) + disasterData.updateForcePreset(response.data, { onlyStatistics: true }) + console.log('[index.vue] 应急统计数据加载成功:', response.data) } else { - console.warn('[index.vue] 应急基地与预置点、应急装备、应急物资、应急人员的数量接口返回数据为空') + console.warn('[index.vue] 应急统计数据接口返回数据为空') } + return response } catch (error) { - console.error('[index.vue] 加载应急基地与预置点、应急装备、应急物资、应急人员的数量失败:', error) + console.error('[index.vue] 加载应急统计数据失败:', error) + ElMessage.warning({ + message: '应急统计数据加载失败', + duration: 3000, + }) + return null } - return null } /** @@ -934,6 +1062,13 @@ const loadEmergencyEquipmentAndMaterial = async (longitude, latitude) => { return null } } +loadEmergencyEquipmentAndMaterial(DISASTER_CENTER.lon, DISASTER_CENTER.lat) + .then((response) => { + console.log('[index.vue] 应急装备和应急物资列表加载成功:', response.data) + }) + .catch((error) => { + console.error('[index.vue] 查询应急装备和应急物资列表失败:', error) + }) // ==================== // 场景初始化函数 @@ -1111,7 +1246,7 @@ const initializeScene = async () => { // 11. 加载应急资源数据(在地形就绪后) console.log('[index.vue] 加载应急资源数据...') - await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat) + // await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat) // 默认隐藏接口标记(等待快速匹配激活) hideMarkers() @@ -1121,6 +1256,10 @@ const initializeScene = async () => { console.log('[index.vue] 加载储备中心和预置点数据...') await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat) + // 13. 加载应急统计数据 + console.log('[index.vue] 加载应急统计数据...') + await loadEmergencyBaseAndPreset(DISASTER_CENTER.lon, DISASTER_CENTER.lat) + console.log('[index.vue] 场景初始化完成') }