feat(3d-situational-awareness): 增加对应急基地、物资和储备中心的支持

- 更新调度建议,在ForceDispatch组件中使用基地而非站点
- 在useDisasterData组合式函数中添加物资和基地的新字段
- 实现储备中心和预设点的数据转换,包含距离计算
- 增强updateForcePreset功能,处理物资和基地数量统计,支持仅统计选项
- 修改地图处理逻辑,加载并显示储备中心、人员和设备标记
- 在场景初始化和距离变化时集成应急统计数据加载
This commit is contained in:
Zzc 2025-11-25 18:16:46 +08:00
parent 47074b6d56
commit 8f2326f54c
3 changed files with 333 additions and 59 deletions

View File

@ -26,7 +26,7 @@
<div class="force-dispatch__stat"> <div class="force-dispatch__stat">
<span class="force-dispatch__stat-label">应急基地与预置点</span> <span class="force-dispatch__stat-label">应急基地与预置点</span>
<span class="force-dispatch__stat-value"> <span class="force-dispatch__stat-value">
{{ dispatchSuggestion.stations }} {{ dispatchSuggestion.bases }}
<span class="force-dispatch__stat-unit"></span> <span class="force-dispatch__stat-unit"></span>
</span> </span>
</div> </div>

View File

@ -98,16 +98,19 @@ export function useDisasterData() {
const stationsCount = forcePreset.value.stations?.length || 0 const stationsCount = forcePreset.value.stations?.length || 0
return { 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, stations: stationsCount,
// 挖掘机建议数每2个养护站配置1台挖掘机 // 应急基地
bases: forcePreset.value.bases,
// 挖掘机建议数
excavators: Math.max(1, Math.ceil(stationsCount / 2)), 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 {Object} emergencyResourcesData - 接口返回的应急资源数据
* @param {number} emergencyResourcesData.equipmentCount - 应急装备数量 * @param {number} emergencyResourcesData.equipmentCount - 应急装备数量
* @param {number} emergencyResourcesData.personnelCount - 应急人员数量 * @param {number} emergencyResourcesData.personnelCount - 应急人员数量
* @param {number} emergencyResourcesData.materialCount - 应急物资数量
* @param {number} emergencyResourcesData.baseCount - 应急基地数量
* @param {Array} emergencyResourcesData.stations - 养护站列表 * @param {Array} emergencyResourcesData.stations - 养护站列表
* @param {string} emergencyResourcesData.stations[].stationId - 养护站ID * @param {string} emergencyResourcesData.stations[].stationId - 养护站ID
* @param {string} emergencyResourcesData.stations[].stationName - 养护站名称 * @param {string} emergencyResourcesData.stations[].stationName - 养护站名称
* @param {number} emergencyResourcesData.stations[].distance - 距离(km) * @param {number} emergencyResourcesData.stations[].distance - 距离(km)
* @param {Object} options - 可选配置
* @param {boolean} options.onlyStatistics - 是否仅更新统计数据(不更新stations)
*/ */
const updateForcePreset = (emergencyResourcesData) => { const updateForcePreset = (emergencyResourcesData, options = {}) => {
if (!emergencyResourcesData) { if (!emergencyResourcesData) {
console.warn('[useDisasterData] 应急资源数据为空,跳过更新') console.warn('[useDisasterData] 应急资源数据为空,跳过更新')
return return
} }
const { equipmentCount, personnelCount, stations } = emergencyResourcesData const { onlyStatistics = false } = options
const {
equipmentCount,
personnelCount,
materialCount,
baseCount,
stations
} = emergencyResourcesData
// 更新装备和人员数量 // 更新装备数量
if (typeof equipmentCount === 'number') { if (typeof equipmentCount === 'number') {
forcePreset.value.equipment = equipmentCount forcePreset.value.equipment = equipmentCount
} }
// 更新人员数量
if (typeof personnelCount === 'number') { if (typeof personnelCount === 'number') {
forcePreset.value.personnel = personnelCount forcePreset.value.personnel = personnelCount
} }
// 更新养护站列表 // 更新物资数量 (新增)
if (Array.isArray(stations)) { 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.bases = stations.length
}
forcePreset.value.stations = stations.map((station) => ({ forcePreset.value.stations = stations.map((station) => ({
id: station.stationId, id: station.stationId,
name: station.stationName?.trim() || '未命名养护站', name: station.stationName?.trim() || '未命名养护站',
@ -191,6 +325,7 @@ export function useDisasterData() {
dispatchSuggestion, dispatchSuggestion,
totalResources, totalResources,
updateForcePreset, updateForcePreset,
updateSearchRadius updateSearchRadius,
transformReserveDataToStations
} }
} }

View File

@ -327,6 +327,8 @@ const {
showMarkers, showMarkers,
hideMarkers, hideMarkers,
markerEntities, markerEntities,
reserveCenterEntities,
emergencyResourceEntities,
} = useMapMarkers() } = useMapMarkers()
// 3D Tiles // 3D Tiles
@ -682,19 +684,22 @@ const handleMapToolChange = async ({ tool, active }) => {
const handleDistanceChange = async (newDistance) => { const handleDistanceChange = async (newDistance) => {
console.log(`[index.vue] 距离范围改变为: ${newDistance}km`) console.log(`[index.vue] 距离范围改变为: ${newDistance}km`)
// // 1.
disasterData.updateSearchRadius(newDistance) disasterData.updateSearchRadius(newDistance)
// // 2.
if (mapStore.viewer) { if (mapStore.viewer) {
createOrUpdateRangeCircle(mapStore.viewer, newDistance) createOrUpdateRangeCircle(mapStore.viewer, newDistance)
} }
// // 3.
await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat) // await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
// // 4.
await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat) 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 const viewer = mapStore.viewer
if (!viewer) return if (!viewer) return
// 1. console.log('[index.vue] 开始显示快速响应标记...')
// 1. /
// 1.1 soldier/device
viewer.entities.values.forEach((entity) => { viewer.entities.values.forEach((entity) => {
if (entity.properties) { if (entity.properties) {
const props = entity.properties const props = entity.properties
if (props.type?.getValue() === 'soldier' || const type = props.type?.getValue()
props.type?.getValue() === 'device' || if (type === 'soldier' ||
props.isPathStartMarker?.getValue()) { type === 'device' ||
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' ||
props.isPathStartMarker?.getValue()) { props.isPathStartMarker?.getValue()) {
entity.show = false entity.show = false
} }
} }
}) })
// 2. // 1.2
hideMarkers() 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. // 3.
hideRangeCircle() 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)) { if (response?.data && Array.isArray(response.data)) {
console.log('[index.vue] 储备中心和预置点数据加载成功:', 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) { if (mapStore.viewer) {
console.log('[index.vue] 添加储备中心和预置点地图标记...') console.log('[index.vue] 添加储备中心和预置点地图标记...')
clearReserveCenterMarkers(mapStore.viewer) 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 { try {
const response = await request({ const response = await request({
url: `/snow-ops-platform/yhYjll/statistics`, url: `/snow-ops-platform/yhYjll/statistics`,
@ -893,16 +1013,24 @@ const loadEmergencyBaseAndPreset = async (longitude, latitude, maxDistance) => {
maxDistance: disasterData.forcePreset.value.searchRadius, maxDistance: disasterData.forcePreset.value.searchRadius,
}, },
}) })
if (response?.data) { if (response?.data) {
console.log('[index.vue] 应急基地与预置点、应急装备、应急物资、应急人员的数量加载成功:', response.data) // (,stations)
disasterData.updateForcePreset(response.data, { onlyStatistics: true })
console.log('[index.vue] 应急统计数据加载成功:', response.data)
} else { } else {
console.warn('[index.vue] 应急基地与预置点、应急装备、应急物资、应急人员的数量接口返回数据为空') console.warn('[index.vue] 应急统计数据接口返回数据为空')
} }
return response return response
} catch (error) { } 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 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. // 11.
console.log('[index.vue] 加载应急资源数据...') console.log('[index.vue] 加载应急资源数据...')
await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat) // await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
// () // ()
hideMarkers() hideMarkers()
@ -1121,6 +1256,10 @@ const initializeScene = async () => {
console.log('[index.vue] 加载储备中心和预置点数据...') console.log('[index.vue] 加载储备中心和预置点数据...')
await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat) 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] 场景初始化完成') console.log('[index.vue] 场景初始化完成')
} }