feat(situational-awareness): 在地图上添加模拟应急资源和范围圆
- 在地图上10公里半径范围内添加10个模拟应急人员和设备点 - 实现动态范围圆可视化,可随搜索半径变化而更新 - 重构距离过滤器下拉菜单为自定义实现,并改进样式 - 更新组件样式,以提高ForceDispatch、ForcePreset、PageHeader和DataField的视觉一致性 - 替换PageHeader中的背景图片,并为UI元素添加新的素材图片
This commit is contained in:
parent
74ce102b67
commit
66ed12b4cb
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 |
@ -150,15 +150,16 @@ const handleStartDispatch = () => {
|
||||
.force-dispatch__top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: vw(12);
|
||||
gap: vw(20);
|
||||
}
|
||||
|
||||
/* 响应等级卡片 */
|
||||
.force-dispatch__level-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: vw(12);
|
||||
padding: vh(10) vw(16);
|
||||
padding: vh(5) vw(16);
|
||||
background: rgba(20, 53, 118, 0.6);
|
||||
border: 1px solid rgba(28, 161, 255, 0.3);
|
||||
border-radius: vw(4);
|
||||
@ -166,13 +167,13 @@ const handleStartDispatch = () => {
|
||||
}
|
||||
|
||||
.force-dispatch__level-label {
|
||||
font-size: fs(13);
|
||||
font-size: fs(18);
|
||||
font-family: SourceHanSansCN-Regular, sans-serif;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.force-dispatch__level-value {
|
||||
font-size: fs(14);
|
||||
font-size: fs(18);
|
||||
font-family: SourceHanSansCN-Bold, sans-serif;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
@ -206,7 +207,7 @@ const handleStartDispatch = () => {
|
||||
}
|
||||
|
||||
.force-dispatch__plan-text {
|
||||
font-size: fs(13);
|
||||
font-size: fs(18);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
|
||||
@ -1,22 +1,36 @@
|
||||
<template>
|
||||
<div class="force-preset">
|
||||
<el-dropdown
|
||||
class="force-preset__filter"
|
||||
trigger="click"
|
||||
@command="handleDistanceChange"
|
||||
>
|
||||
<div class="filter-content">
|
||||
<span class="filter-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span>
|
||||
<img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" class="filter-icon" />
|
||||
<!-- 自定义下拉框 -->
|
||||
<div class="custom-dropdown" v-click-outside="closeDropdown">
|
||||
<!-- 触发器 -->
|
||||
<div class="dropdown-trigger" @click="toggleDropdown">
|
||||
<span class="trigger-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span>
|
||||
<div class="trigger-icon" :class="{ 'is-open': isDropdownOpen }">
|
||||
<img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下拉面板 -->
|
||||
<transition name="dropdown-slide">
|
||||
<div v-if="isDropdownOpen" class="dropdown-panel">
|
||||
<div
|
||||
v-for="option in distanceOptions"
|
||||
:key="option.value"
|
||||
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>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="10" :class="{ 'is-active': forcePreset.searchRadius === 10 }">10km</el-dropdown-item>
|
||||
<el-dropdown-item :command="30" :class="{ 'is-active': forcePreset.searchRadius === 30 }">30km</el-dropdown-item>
|
||||
<el-dropdown-item :command="50" :class="{ 'is-active': forcePreset.searchRadius === 50 }">50km</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<div class="force-preset__summary">
|
||||
<!-- <img src="../../assets/images/SketchPnga96e6ce64e80f6d935217d64400481f3e0361d9e60a7425f6f09c8287716904d.png" alt="background" class="summary-bg" /> -->
|
||||
@ -68,18 +82,58 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue'
|
||||
import { ref, inject } from 'vue'
|
||||
|
||||
const { forcePreset } = inject('disasterData')
|
||||
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) {
|
||||
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>
|
||||
@ -92,34 +146,117 @@ const handleDistanceChange = (distance) => {
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
|
||||
&__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vw(8);
|
||||
padding: vh(10) vw(16);
|
||||
background: rgba(20, 53, 118, 0.5);
|
||||
border-radius: vw(4);
|
||||
margin-bottom: vh(16);
|
||||
cursor: pointer;
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vw(8);
|
||||
width: 100%;
|
||||
// 自定义下拉框
|
||||
.custom-dropdown {
|
||||
position: relative;
|
||||
margin-bottom: vh(8);
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
// 触发器
|
||||
.dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: vh(8) vw(16);
|
||||
background: rgba(28, 70, 130, 0.9);
|
||||
border: 1px solid rgba(28, 161, 255, 0.3);
|
||||
border-radius: vw(8);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(28, 70, 130, 1);
|
||||
border-color: rgba(28, 161, 255, 0.5);
|
||||
}
|
||||
|
||||
.trigger-text {
|
||||
flex: 1;
|
||||
color: var(--text-white);
|
||||
font-size: fs(14);
|
||||
font-size: fs(15);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
width: vw(12);
|
||||
height: vh(12);
|
||||
.trigger-icon {
|
||||
width: 16px;
|
||||
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 {
|
||||
@ -205,7 +342,7 @@ const handleDistanceChange = (distance) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: vh(8);
|
||||
max-height: 100px;
|
||||
max-height: vw(120);
|
||||
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>
|
||||
|
||||
@ -44,7 +44,7 @@ const handleBack = () => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: vh(111);
|
||||
background: url('../assets/images/b149e2d47f8744b5a916eb88fb4115cc_mergeImage.png') no-repeat;
|
||||
background: url('../assets/images/一级标题栏bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -47,13 +47,13 @@ const valueClass = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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-size: 100% 100%;
|
||||
|
||||
&__icon {
|
||||
width: vw(24);
|
||||
height: vh(30);
|
||||
height: vh(24);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -141,7 +141,7 @@ import { request } from "@shared/utils/request";
|
||||
|
||||
// 标记点图标
|
||||
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 deviceIcon from "./assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png";
|
||||
import emergencyBaseIcon from "./assets/images/应急基地.png";
|
||||
@ -156,6 +156,11 @@ const handleDistanceChange = async (newDistance) => {
|
||||
// 更新搜索半径
|
||||
disasterData.updateSearchRadius(newDistance);
|
||||
|
||||
// 更新范围圈
|
||||
if (mapStore.viewer) {
|
||||
createOrUpdateRangeCircle(mapStore.viewer, newDistance);
|
||||
}
|
||||
|
||||
// 重新加载应急资源数据并更新地图标记
|
||||
await loadEmergencyResources(108.011506, 30.175827);
|
||||
};
|
||||
@ -188,6 +193,9 @@ const currentTooltipEntity = ref(null);
|
||||
// 加载动画状态
|
||||
const showLoading = ref(false);
|
||||
|
||||
// 范围圈实体
|
||||
const rangeCircleEntity = ref(null);
|
||||
|
||||
// 3D Tiles加载功能
|
||||
const { load3DTileset, waitForTilesetReady } = use3DTiles();
|
||||
|
||||
@ -405,7 +413,7 @@ onMounted(() => {
|
||||
0
|
||||
),
|
||||
billboard: {
|
||||
image: dangerIcon,
|
||||
image: eventIcon,
|
||||
width: 36,
|
||||
height: 36,
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
@ -415,6 +423,53 @@ onMounted(() => {
|
||||
});
|
||||
viewer.entities.add(defaultPoint);
|
||||
|
||||
// 在默认点附近添加10个模拟点位(应急人员和应急装备),分散在10km范围内
|
||||
// 1度纬度约等于111km,1度经度在30度纬度约等于96km
|
||||
// 10km约等于0.09度纬度,0.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({
|
||||
// ...DEFAULT_CAMERA_VIEW,
|
||||
// });
|
||||
@ -514,6 +569,9 @@ onMounted(() => {
|
||||
// camera.setView(DEFAULT_CAMERA_VIEW)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建初始范围圈(使用当前搜索半径)
|
||||
createOrUpdateRangeCircle(viewer, disasterData.forcePreset.value.searchRadius);
|
||||
});
|
||||
});
|
||||
|
||||
@ -678,6 +736,42 @@ const handleStartDispatch = (payload) => {
|
||||
}, 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
|
||||
*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user