feat(situational-awareness): 在地图上添加模拟应急资源和范围圆

- 在地图上10公里半径范围内添加10个模拟应急人员和设备点
- 实现动态范围圆可视化,可随搜索半径变化而更新
- 重构距离过滤器下拉菜单为自定义实现,并改进样式
- 更新组件样式,以提高ForceDispatch、ForcePreset、PageHeader和DataField的视觉一致性
- 替换PageHeader中的背景图片,并为UI元素添加新的素材图片
This commit is contained in:
Zzc 2025-11-19 15:23:17 +08:00
parent 74ce102b67
commit 66ed12b4cb
9 changed files with 280 additions and 73 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -150,15 +150,16 @@ const handleStartDispatch = () => {
.force-dispatch__top { .force-dispatch__top {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: vw(12); gap: vw(20);
} }
/* 响应等级卡片 */ /* 响应等级卡片 */
.force-dispatch__level-card { .force-dispatch__level-card {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: vw(12); gap: vw(12);
padding: vh(10) vw(16); padding: vh(5) vw(16);
background: rgba(20, 53, 118, 0.6); background: rgba(20, 53, 118, 0.6);
border: 1px solid rgba(28, 161, 255, 0.3); border: 1px solid rgba(28, 161, 255, 0.3);
border-radius: vw(4); border-radius: vw(4);
@ -166,13 +167,13 @@ const handleStartDispatch = () => {
} }
.force-dispatch__level-label { .force-dispatch__level-label {
font-size: fs(13); font-size: fs(18);
font-family: SourceHanSansCN-Regular, sans-serif; font-family: SourceHanSansCN-Regular, sans-serif;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
.force-dispatch__level-value { .force-dispatch__level-value {
font-size: fs(14); font-size: fs(18);
font-family: SourceHanSansCN-Bold, sans-serif; font-family: SourceHanSansCN-Bold, sans-serif;
font-weight: 600; font-weight: 600;
color: rgba(255, 255, 255, 0.95); color: rgba(255, 255, 255, 0.95);
@ -206,7 +207,7 @@ const handleStartDispatch = () => {
} }
.force-dispatch__plan-text { .force-dispatch__plan-text {
font-size: fs(13); font-size: fs(18);
font-family: SourceHanSansCN-Medium, sans-serif; font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 500; font-weight: 500;
color: rgba(255, 255, 255, 0.95); color: rgba(255, 255, 255, 0.95);

View File

@ -1,22 +1,36 @@
<template> <template>
<div class="force-preset"> <div class="force-preset">
<el-dropdown <!-- 自定义下拉框 -->
class="force-preset__filter" <div class="custom-dropdown" v-click-outside="closeDropdown">
trigger="click" <!-- 触发器 -->
@command="handleDistanceChange" <div class="dropdown-trigger" @click="toggleDropdown">
> <span class="trigger-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span>
<div class="filter-content"> <div class="trigger-icon" :class="{ 'is-open': isDropdownOpen }">
<span class="filter-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span> <img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" />
<img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" class="filter-icon" /> </div>
</div> </div>
<template #dropdown>
<el-dropdown-menu> <!-- 下拉面板 -->
<el-dropdown-item :command="10" :class="{ 'is-active': forcePreset.searchRadius === 10 }">10km</el-dropdown-item> <transition name="dropdown-slide">
<el-dropdown-item :command="30" :class="{ 'is-active': forcePreset.searchRadius === 30 }">30km</el-dropdown-item> <div v-if="isDropdownOpen" class="dropdown-panel">
<el-dropdown-item :command="50" :class="{ 'is-active': forcePreset.searchRadius === 50 }">50km</el-dropdown-item> <div
</el-dropdown-menu> v-for="option in distanceOptions"
</template> :key="option.value"
</el-dropdown> class="dropdown-item"
:class="{ 'is-active': forcePreset.searchRadius === option.value }"
@click="selectOption(option.value)"
>
<span class="item-text">{{ option.label }}</span>
<div v-if="forcePreset.searchRadius === option.value" class="item-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M8 12L11 15L16 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</div>
</transition>
</div>
<div class="force-preset__summary"> <div class="force-preset__summary">
<!-- <img src="../../assets/images/SketchPnga96e6ce64e80f6d935217d64400481f3e0361d9e60a7425f6f09c8287716904d.png" alt="background" class="summary-bg" /> --> <!-- <img src="../../assets/images/SketchPnga96e6ce64e80f6d935217d64400481f3e0361d9e60a7425f6f09c8287716904d.png" alt="background" class="summary-bg" /> -->
@ -68,18 +82,58 @@
</template> </template>
<script setup> <script setup>
import { inject } from 'vue' import { ref, inject } from 'vue'
const { forcePreset } = inject('disasterData') const { forcePreset } = inject('disasterData')
const onDistanceChange = inject('onDistanceChange') const onDistanceChange = inject('onDistanceChange')
//
const isDropdownOpen = ref(false)
//
const distanceOptions = [
{ value: 10, label: '距离灾害点10km范围内' },
{ value: 30, label: '距离灾害点30km范围内' },
{ value: 50, label: '距离灾害点50km范围内' }
]
/** /**
* 处理距离范围选择变更 * 切换下拉框显示/隐藏
* @param {number} distance - 选中的距离范围(km)
*/ */
const handleDistanceChange = (distance) => { const toggleDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value
}
/**
* 关闭下拉框
*/
const closeDropdown = () => {
isDropdownOpen.value = false
}
/**
* 选择选项
* @param {number} value - 选中的距离值
*/
const selectOption = (value) => {
if (onDistanceChange) { if (onDistanceChange) {
onDistanceChange(distance) onDistanceChange(value)
}
closeDropdown()
}
//
const vClickOutside = {
mounted(el, binding) {
el.clickOutsideEvent = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value()
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideEvent)
} }
} }
</script> </script>
@ -92,36 +146,119 @@ const handleDistanceChange = (distance) => {
padding: 0; padding: 0;
margin-top: 0; margin-top: 0;
&__filter { //
.custom-dropdown {
position: relative;
margin-bottom: vh(8);
}
//
.dropdown-trigger {
display: flex; display: flex;
align-items: center; align-items: center;
gap: vw(8); justify-content: space-between;
padding: vh(10) vw(16); padding: vh(8) vw(16);
background: rgba(20, 53, 118, 0.5); background: rgba(28, 70, 130, 0.9);
border-radius: vw(4); border: 1px solid rgba(28, 161, 255, 0.3);
margin-bottom: vh(16); border-radius: vw(8);
cursor: pointer; cursor: pointer;
transition: all 0.3s ease;
.filter-content { &:hover {
display: flex; background: rgba(28, 70, 130, 1);
align-items: center; border-color: rgba(28, 161, 255, 0.5);
gap: vw(8);
width: 100%;
} }
.filter-text { .trigger-text {
flex: 1; flex: 1;
color: var(--text-white); color: var(--text-white);
font-size: fs(14); font-size: fs(15);
font-family: SourceHanSansCN-Medium, sans-serif; font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 500;
} }
.filter-icon { .trigger-icon {
width: vw(12); width: 16px;
height: vh(12); height: 16px;
flex-shrink: 0;
transition: transform 0.3s ease;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
&.is-open {
transform: rotate(180deg);
}
} }
} }
//
.dropdown-panel {
position: absolute;
top: calc(100% + vh(4));
left: 0;
right: 0;
background: rgba(15, 35, 75, 0.98);
border: 1px solid rgba(28, 161, 255, 0.3);
border-radius: vw(8);
overflow: hidden;
z-index: 100;
box-shadow: 0 vh(4) vh(16) rgba(0, 0, 0, 0.5);
}
//
.dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: vh(12) vw(16);
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background: rgba(28, 161, 255, 0.1);
}
&.is-active {
background: rgba(28, 161, 255, 0.15);
}
.item-text {
flex: 1;
color: rgba(255, 255, 255, 0.9);
font-size: fs(15);
font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 400;
}
.item-icon {
width: vw(20);
height: vh(20);
flex-shrink: 0;
color: var(--primary-color);
svg {
width: 100%;
height: 100%;
}
}
}
//
.dropdown-slide-enter-active,
.dropdown-slide-leave-active {
transition: all 0.3s ease;
}
.dropdown-slide-enter-from,
.dropdown-slide-leave-to {
opacity: 0;
transform: translateY(vh(-10));
}
&__summary { &__summary {
position: relative; position: relative;
display: flex; display: flex;
@ -205,7 +342,7 @@ const handleDistanceChange = (distance) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: vh(8); gap: vh(8);
max-height: 100px; max-height: vw(120);
overflow-y: auto; overflow-y: auto;
// //
@ -259,29 +396,4 @@ const handleDistanceChange = (distance) => {
} }
} }
} }
// Dropdown
:deep(.el-dropdown-menu) {
background: rgba(20, 53, 118, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: vh(4) 0;
.el-dropdown-menu__item {
color: var(--text-white);
font-size: fs(14);
font-family: SourceHanSansCN-Medium, sans-serif;
padding: vh(8) vw(16);
&:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-white);
}
&.is-active {
background: rgba(255, 255, 255, 0.15);
color: var(--el-color-primary);
font-weight: 600;
}
}
}
</style> </style>

View File

@ -44,7 +44,7 @@ const handleBack = () => {
position: relative; position: relative;
width: 100%; width: 100%;
height: vh(111); height: vh(111);
background: url('../assets/images/b149e2d47f8744b5a916eb88fb4115cc_mergeImage.png') no-repeat; background: url('../assets/images/一级标题栏bg.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -47,13 +47,13 @@ const valueClass = computed(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: vw(8); gap: vw(8);
padding: vh(8) vw(10); padding: vh(4) vw(10);
background: url('../../assets/images/DataField/快速感知_bg.png') no-repeat center center; background: url('../../assets/images/DataField/快速感知_bg.png') no-repeat center center;
background-size: 100% 100%; background-size: 100% 100%;
&__icon { &__icon {
width: vw(24); width: vw(24);
height: vh(30); height: vh(24);
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -141,7 +141,7 @@ import { request } from "@shared/utils/request";
// //
import emergencyCenterIcon from "./assets/images/应急中心.png"; import emergencyCenterIcon from "./assets/images/应急中心.png";
import dangerIcon from "./assets/images/danger.png"; import eventIcon from "./assets/images/事件icon.png";
import soldierIcon from "./assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png"; import soldierIcon from "./assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png";
import deviceIcon from "./assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png"; import deviceIcon from "./assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png";
import emergencyBaseIcon from "./assets/images/应急基地.png"; import emergencyBaseIcon from "./assets/images/应急基地.png";
@ -156,6 +156,11 @@ const handleDistanceChange = async (newDistance) => {
// //
disasterData.updateSearchRadius(newDistance); disasterData.updateSearchRadius(newDistance);
//
if (mapStore.viewer) {
createOrUpdateRangeCircle(mapStore.viewer, newDistance);
}
// //
await loadEmergencyResources(108.011506, 30.175827); await loadEmergencyResources(108.011506, 30.175827);
}; };
@ -188,6 +193,9 @@ const currentTooltipEntity = ref(null);
// //
const showLoading = ref(false); const showLoading = ref(false);
//
const rangeCircleEntity = ref(null);
// 3D Tiles // 3D Tiles
const { load3DTileset, waitForTilesetReady } = use3DTiles(); const { load3DTileset, waitForTilesetReady } = use3DTiles();
@ -405,7 +413,7 @@ onMounted(() => {
0 0
), ),
billboard: { billboard: {
image: dangerIcon, image: eventIcon,
width: 36, width: 36,
height: 36, height: 36,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
@ -415,6 +423,53 @@ onMounted(() => {
}); });
viewer.entities.add(defaultPoint); viewer.entities.add(defaultPoint);
// 1010km
// 1111km13096km
// 10km0.090.104
const simulatedPoints = [
// -
{ type: 'soldier', name: '张三', department: '应急救援队', lon: 108.051, lat: 30.205, distance: 4.2, icon: soldierIcon },
{ type: 'soldier', name: '李四', department: '消防队', lon: 107.975, lat: 30.195, distance: 5.8, icon: soldierIcon },
{ type: 'soldier', name: '王五', department: '医疗队', lon: 108.025, lat: 30.155, distance: 3.5, icon: soldierIcon },
{ type: 'soldier', name: '赵六', department: '应急救援队', lon: 108.085, lat: 30.168, distance: 7.2, icon: soldierIcon },
{ type: 'soldier', name: '刘七', department: '消防队', lon: 107.945, lat: 30.182, distance: 8.5, icon: soldierIcon },
// -
{ type: 'device', name: '救援车辆A', deviceType: '消防车', lon: 108.065, lat: 30.185, distance: 6.3, icon: deviceIcon },
{ type: 'device', name: '救援车辆B', deviceType: '救护车', lon: 107.960, lat: 30.165, distance: 6.8, icon: deviceIcon },
{ type: 'device', name: '无人机A', deviceType: 'DJI', lon: 108.035, lat: 30.225, distance: 5.5, icon: deviceIcon },
{ type: 'device', name: '无人机B', deviceType: 'DJI', lon: 108.095, lat: 30.195, distance: 9.2, icon: deviceIcon },
{ type: 'device', name: '通讯设备', deviceType: '卫星电话', lon: 107.930, lat: 30.175, distance: 9.8, icon: deviceIcon }
];
simulatedPoints.forEach(point => {
const entity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat, 0),
billboard: {
image: point.icon,
width: 36,
height: 40,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
properties: point.type === 'soldier'
? {
type: 'soldier',
name: point.name,
department: point.department,
location: `目前为止距离现场${point.distance}公里`
}
: {
type: 'device',
name: point.name,
deviceType: point.deviceType,
location: `目前为止距离现场${point.distance}公里`
}
});
});
console.log(`[index.vue] 已添加 ${simulatedPoints.length} 个模拟点位`);
// camera.setView({ // camera.setView({
// ...DEFAULT_CAMERA_VIEW, // ...DEFAULT_CAMERA_VIEW,
// }); // });
@ -514,6 +569,9 @@ onMounted(() => {
// camera.setView(DEFAULT_CAMERA_VIEW) // camera.setView(DEFAULT_CAMERA_VIEW)
} }
} }
// 使
createOrUpdateRangeCircle(viewer, disasterData.forcePreset.value.searchRadius);
}); });
}); });
@ -678,6 +736,42 @@ const handleStartDispatch = (payload) => {
}, 3000); }, 3000);
}; };
/**
* 创建或更新范围圈
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {number} radiusKm - 半径公里
*/
const createOrUpdateRangeCircle = (viewer, radiusKm) => {
if (!viewer) return;
const centerLon = 108.011506;
const centerLat = 30.175827;
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('#1CA1FF').withAlpha(0.2),
outline: true,
outlineColor: Cesium.Color.fromCssColorString('#1CA1FF').withAlpha(0.8),
outlineWidth: 2,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
}
});
console.log(`[index.vue] 已创建/更新范围圈: ${radiusKm}km`);
};
/** /**
* 在指定屏幕坐标显示地图 Tooltip * 在指定屏幕坐标显示地图 Tooltip
* *