Zzc a5bd1d3b1e feat(screen): 为人员移动添加红色路径线可视化
- 在 `useEntityAnimation` 可组合项中更新人员路径坐标
- 在 `index.vue` 中实现 `drawRedPathLine` 函数,在地图上渲染红色折线
- 将路径绘制集成到 `handleStartDispatch` 中,以增强态势感知能力
- 调整 `styles` 中加载 GIF 的最大尺寸,以获得更好的显示效果
2025-11-21 11:09:22 +08:00

1600 lines
50 KiB
Vue
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.

<template>
<div class="situational-awareness">
<!-- 顶部导航栏 -->
<PageHeader @back="handleBack" />
<!-- 主内容区域 -->
<div class="situational-awareness__main">
<!-- 地图底层 -->
<div
class="situational-awareness__map-layer"
:class="{ 'is-compare-mode': isCompareMode }"
>
<!-- 左侧地图容器对比模式下显示灾前场景 -->
<div
id="leftCesiumContainer"
class="situational-awareness__left-map"
></div>
<!-- 中间分割线 -->
<div
v-if="isCompareMode"
class="situational-awareness__center-divider"
></div>
<!-- 右侧地图主地图 - 灾后场景 -->
<div class="situational-awareness__right-map">
<MapViewer @tool-change="handleMapToolChange" />
</div>
</div>
<!-- 地图遮罩层 -->
<div class="situational-awareness__map-mask" aria-hidden="true"></div>
<!-- 场景标签层 - 独立层级显示在遮罩层之上 -->
<div class="situational-awareness__scene-labels-layer">
<!-- 灾前现场实景标签 - 在中间分割线左侧 -->
<SceneLabel
v-if="isCompareMode"
text="灾前现场实景"
position="center-left"
/>
<!-- 灾后现场实景标签 - 在右侧面板左边 -->
<SceneLabel
text="灾后现场实景"
position="right-left"
/>
</div>
<!-- 浮动面板层 -->
<div class="situational-awareness__panels-layer">
<Transition name="panel-slide-left">
<div
v-show="!isLeftPanelCollapsed"
class="situational-awareness__panel-column situational-awareness__panel-column--left"
>
<LeftPanel @start-dispatch="handleStartDispatch" />
</div>
</Transition>
<Transition name="panel-slide-right">
<div
v-show="!isRightPanelCollapsed"
class="situational-awareness__panel-column situational-awareness__panel-column--right"
>
<RightPanel />
</div>
</Transition>
</div>
<!-- 折叠按钮层 -->
<div class="situational-awareness__collapse-buttons-layer">
<!-- 左侧折叠按钮 -->
<button
class="situational-awareness__collapse-btn situational-awareness__collapse-btn--left"
:class="{ 'is-collapsed': isLeftPanelCollapsed }"
@click="toggleLeftPanel"
:aria-label="isLeftPanelCollapsed ? '展开左侧面板' : '收起左侧面板'"
>
<img
:src="isLeftPanelCollapsed ? collapseRightArrow : collapseLeftArrow"
alt=""
class="collapse-arrow"
/>
</button>
<!-- 右侧折叠按钮 -->
<button
class="situational-awareness__collapse-btn situational-awareness__collapse-btn--right"
:class="{ 'is-collapsed': isRightPanelCollapsed }"
@click="toggleRightPanel"
:aria-label="isRightPanelCollapsed ? '展开右侧面板' : '收起右侧面板'"
>
<img
:src="isRightPanelCollapsed ? collapseLeftArrow : collapseRightArrow"
alt=""
class="collapse-arrow"
/>
</button>
</div>
<!-- 地图控件层 - 高于遮罩和面板 -->
<div class="situational-awareness__controls-layer">
<div id="sa-controls" class="situational-awareness__controls"></div>
</div>
<!-- 地图 Tooltip - 用于显示地图标记点的轻量级信息提示框 -->
<div class="situational-awareness__tooltip-layer">
<MapTooltip
v-model:visible="mapTooltip.visible"
:x="mapTooltip.x"
:y="mapTooltip.y"
:title="mapTooltip.title"
:icon="mapTooltip.icon"
:z-index="mapTooltip.zIndex"
@close="handleMapTooltipClose"
>
<!-- Tooltip 内容插槽 - 根据实际业务数据渲染 -->
<template v-if="mapTooltip.data && mapTooltip.data.fields">
<div
v-for="(field, index) in mapTooltip.data.fields"
:key="index"
class="tooltip-field-item"
>
<span class="tooltip-field-label">{{ field.label }}</span>
<span class="tooltip-field-value">{{ field.value }}</span>
</div>
</template>
<!-- 操作按钮插槽 -->
<template v-if="mapTooltip.data && mapTooltip.data.actions" #actions>
<button
v-for="(action, index) in mapTooltip.data.actions"
:key="index"
class="tooltip-action-btn"
@click="handleTooltipAction(action)"
>
{{ action.label }}
</button>
</template>
</MapTooltip>
</div>
<!-- 加载动画层 - 一键启动后显示 -->
<div v-if="showLoading" class="situational-awareness__loading-layer">
<img
src="./assets/images/加载.gif"
alt="加载中"
class="situational-awareness__loading-gif"
/>
</div>
</div>
<!-- 弹窗组件 -->
<PersonnelDetail
:visible="showPersonnelDetail"
:personnel-data="selectedPersonnel"
@close="showPersonnelDetail = false"
@link="handlePersonnelLink"
/>
<EmergencyCenterDetail
:visible="showCenterDetail"
:center-data="selectedCenter"
@close="showCenterDetail = false"
/>
</div>
</template>
<script setup>
import { ref, provide, onMounted } from "vue";
import * as Cesium from "cesium";
import { ElMessage } from "element-plus";
import PageHeader from "./components/PageHeader.vue";
import LeftPanel from "./components/LeftPanel/index.vue";
import MapViewer from "./components/MapViewer/index.vue";
import RightPanel from "./components/RightPanel/index.vue";
import PersonnelDetail from "./components/Popups/PersonnelDetail.vue";
import EmergencyCenterDetail from "./components/Popups/EmergencyCenterDetail.vue";
import MapTooltip from "./components/shared/MapTooltip.vue";
import SceneLabel from "./components/SceneLabel.vue";
import { useDisasterData } from "./composables/useDisasterData";
import { useDualMapCompare } from "./composables/useDualMapCompare";
import { useMapMarkers } from "./composables/useMapMarkers";
import { use3DTiles } from "./composables/use3DTiles";
import { useMapTooltip } from "./composables/useMapTooltip";
import { useEntityAnimation } from "./composables/useEntityAnimation";
import { useMapStore } from "@/map";
import { request } from "@shared/utils/request";
// 标记点图标
import emergencyCenterIcon from "./assets/images/应急中心.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";
import reserveCenterIcon from "./assets/images/储备中心.png";
// 折叠按钮图标
import collapseLeftArrow from "./assets/images/折叠面板左箭头.png";
import collapseRightArrow from "./assets/images/折叠面板右箭头.png";
// 使用灾害数据
const disasterData = useDisasterData();
// 处理距离范围变更
const handleDistanceChange = async (newDistance) => {
console.log(`[index.vue] 距离范围改变为: ${newDistance}km`);
// 更新搜索半径
disasterData.updateSearchRadius(newDistance);
// 更新范围圈
if (mapStore.viewer) {
createOrUpdateRangeCircle(mapStore.viewer, newDistance);
}
// 重新加载应急资源数据并更新地图标记
await loadEmergencyResources(108.011506, 30.175827);
// 重新加载储备中心和预置点数据并更新地图标记
await loadReserveCentersAndPresets(108.011506, 30.175827);
};
// 提供给子组件使用
provide("disasterData", disasterData);
provide("onDistanceChange", handleDistanceChange);
// 地图 store
const mapStore = useMapStore();
// 双地图对比功能
const { isCompareMode, toggleCompareMode } = useDualMapCompare();
// 地图标记功能
const {
initializeMarkers,
clearMarkers,
getCollapseCenter,
addEmergencyResourceMarkers,
clearEmergencyResourceMarkers,
addReserveCenterMarkers,
clearReserveCenterMarkers,
} = useMapMarkers();
// 地图 Tooltip 功能
const { tooltipState: mapTooltip, showTooltip, hideTooltip, updateTooltipPosition } = useMapTooltip();
// 实体动画功能
const { startPersonnelMovement, stopPersonnelMovement, isAnimating } = useEntityAnimation();
// 当前显示 tooltip 的实体(用于相机移动时更新位置)
const currentTooltipEntity = ref(null);
// 加载动画状态
const showLoading = ref(false);
// 范围圈实体
const rangeCircleEntity = ref(null);
// 面板折叠状态
const isLeftPanelCollapsed = ref(false);
const isRightPanelCollapsed = ref(false);
// 切换左侧面板
const toggleLeftPanel = () => {
isLeftPanelCollapsed.value = !isLeftPanelCollapsed.value;
};
// 切换右侧面板
const toggleRightPanel = () => {
isRightPanelCollapsed.value = !isRightPanelCollapsed.value;
};
// 3D Tiles加载功能
const { load3DTileset, waitForTilesetReady } = use3DTiles();
/**
* 设置地图点击事件处理器
* 当用户点击地图标记点时,显示 Tooltip
*/
const setupMapClickHandler = (viewer) => {
if (!viewer) return;
// 创建 ScreenSpaceEventHandler 监听点击事件
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((click) => {
// 获取点击位置的实体
const pickedObject = viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
const entity = pickedObject.id;
// 过滤掉范围圈实体(通过检查是否是范围圈)
if (entity === rangeCircleEntity.value) {
console.log('[index.vue] 点击了范围圈,忽略');
// 尝试穿透到下面的实体
const drillPickedObjects = viewer.scene.drillPick(click.position);
let foundMarker = false;
for (const pickedObj of drillPickedObjects) {
if (Cesium.defined(pickedObj.id) && pickedObj.id !== rangeCircleEntity.value) {
const markerEntity = pickedObj.id;
if (markerEntity.properties) {
const type = markerEntity.properties.type?.getValue();
// 找到标记点,显示 Tooltip
if (type === 'soldier') {
showMarkerTooltip(viewer, markerEntity, click.position, soldierIcon);
foundMarker = true;
break;
} else if (type === 'device') {
showMarkerTooltip(viewer, markerEntity, click.position, deviceIcon);
foundMarker = true;
break;
} else if (type === 'emergencyBase' || type === 'station') {
const stationName = markerEntity.properties.name?.getValue() || '';
const icon = stationName === '忠县公路交通应急物资储备中心'
? emergencyCenterIcon
: emergencyBaseIcon;
showMarkerTooltip(viewer, markerEntity, click.position, icon);
foundMarker = true;
break;
} else if (type === 'reserveCenter' || type === 'presetPoint') {
const icon = type === 'reserveCenter' ? reserveCenterIcon : emergencyBaseIcon;
showMarkerTooltip(viewer, markerEntity, click.position, icon);
foundMarker = true;
break;
}
}
}
}
if (!foundMarker) {
hideTooltip();
currentTooltipEntity.value = null;
}
return;
}
// 检查实体是否有 properties标记点才有
if (entity.properties) {
const type = entity.properties.type?.getValue();
// 根据标记类型显示不同的 Tooltip
if (type === 'soldier') {
showMarkerTooltip(viewer, entity, click.position, soldierIcon);
} else if (type === 'device') {
showMarkerTooltip(viewer, entity, click.position, deviceIcon);
} else if (type === 'emergencyBase' || type === 'station') {
// 对于养护站,根据名称判断使用哪个图标
const stationName = entity.properties.name?.getValue() || '';
const icon = stationName === '忠县公路交通应急物资储备中心'
? emergencyCenterIcon
: emergencyBaseIcon;
showMarkerTooltip(viewer, entity, click.position, icon);
} else if (type === 'reserveCenter' || type === 'presetPoint') {
// 储备中心和预置点
const icon = type === 'reserveCenter' ? reserveCenterIcon : emergencyBaseIcon;
showMarkerTooltip(viewer, entity, click.position, icon);
}
}
} else {
// 点击空白区域,隐藏 Tooltip
hideTooltip();
currentTooltipEntity.value = null;
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 监听相机移动事件,更新 tooltip 位置
viewer.scene.postRender.addEventListener(() => {
if (currentTooltipEntity.value && mapTooltip.value.visible) {
updateTooltipPositionForEntity(viewer, currentTooltipEntity.value);
}
});
};
/**
* 显示标记点 Tooltip
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 被点击的实体
* @param {Cesium.Cartesian2} screenPosition - 点击的屏幕坐标(备用)
* @param {string} icon - 图标路径
*/
const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
const properties = entity.properties;
const type = properties.type?.getValue();
// 获取实体的 3D 位置
const position = entity.position?.getValue(Cesium.JulianDate.now());
if (!position) {
console.warn('[Tooltip] 无法获取实体位置');
return;
}
// 对于使用 CLAMP_TO_GROUND 的 billboard需要获取实际的地形高度
const cartographic = Cesium.Cartographic.fromCartesian(position);
let clampedPosition = position;
// 使用 globe.getHeight 获取地形高度
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic);
if (Cesium.defined(height)) {
cartographic.height = height;
clampedPosition = Cesium.Cartographic.toCartesian(cartographic);
}
}
// 将贴地后的 3D 坐标转换为屏幕坐标
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition);
if (!Cesium.defined(canvasPosition)) {
console.warn('[Tooltip] 无法转换坐标到屏幕位置');
return;
}
// 构建 Tooltip 数据
let title = '';
const fields = [];
const actions = [];
if (type === 'soldier') {
// 应急人员
title = '应急人员';
fields.push(
{ label: '姓名', value: properties.name?.getValue() || '-' },
{ label: '部门', value: properties.department?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' },
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
);
actions.push({
label: '联动',
type: 'link',
data: entity
});
} else if (type === 'device') {
// 应急装备
title = '应急装备';
fields.push(
{ label: '设备名称', value: properties.name?.getValue() || '-' },
{ label: '设备类型', value: properties.deviceType?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' },
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
);
} else if (type === 'emergencyBase') {
// 应急基地
title = '应急基地';
fields.push(
{ label: '基地名称', value: properties.name?.getValue() || '-' },
{ label: '路线编号', value: properties.routeNumber?.getValue() || '-' },
{ label: '隶属单位', value: properties.department?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
);
actions.push({
label: '连线',
type: 'connect',
data: entity
});
} else if (type === 'station') {
const stationName = properties.name?.getValue() || '';
const distance = properties.distance?.getValue() || 0;
// 如果是应急中心,显示应急中心信息
if (stationName === '忠县公路交通应急物资储备中心') {
title = '应急中心';
fields.push(
{ label: '名称', value: '忠县应急中心' },
{ label: '行政等级', value: '国道' },
{ label: '隶属单位', value: '交通公路部门' },
{ label: '位置信息', value: `目前为止距离现场${distance}公里` }
);
} else {
// 其他养护站显示 tooltip
title = '养护站';
fields.push(
{ label: '名称', value: stationName || '-' },
{ label: '距离', value: `${distance}公里` }
);
}
} else if (type === 'reserveCenter') {
// 储备中心
title = '储备中心';
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
);
} else if (type === 'presetPoint') {
// 预置点
title = '预置点';
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
);
}
// 显示 Tooltip使用实体的屏幕坐标
showTooltip({
x: canvasPosition.x,
y: canvasPosition.y,
title,
icon,
data: { fields, actions: actions.length > 0 ? actions : undefined }
});
// 保存当前实体,用于相机移动时更新位置
currentTooltipEntity.value = entity;
};
/**
* 更新 Tooltip 位置(当相机移动时)
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 实体对象
*/
const updateTooltipPositionForEntity = (viewer, entity) => {
const position = entity.position?.getValue(Cesium.JulianDate.now());
if (!position) return;
// 对于使用 CLAMP_TO_GROUND 的 billboard需要获取实际的地形高度
const cartographic = Cesium.Cartographic.fromCartesian(position);
let clampedPosition = position;
// 使用 globe.getHeight 获取地形高度
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic);
if (Cesium.defined(height)) {
cartographic.height = height;
clampedPosition = Cesium.Cartographic.toCartesian(cartographic);
}
}
// 将贴地后的 3D 坐标转换为屏幕坐标
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition);
// 如果标记点在视野外,隐藏 tooltip
if (!Cesium.defined(canvasPosition)) {
hideTooltip();
currentTooltipEntity.value = null;
return;
}
updateTooltipPosition(canvasPosition.x, canvasPosition.y);
};
// 初始化地图
onMounted(() => {
// 等待地图就绪后配置初始视图和模型对比图层
mapStore.onReady(async () => {
const { camera } = mapStore.services();
const viewer = mapStore.viewer;
console.log("3D态势感知地图已就绪");
// 默认相机配置
const DEFAULT_CAMERA_VIEW = {
lon: 108.011506,
lat: 30.175827,
height: 5000,
heading: 0,
pitch: -45,
roll: 0,
};
// 默认点加上图标标记,使用图片图标,图标路径为 packages\screen\src\views\3DSituationalAwarenessRefactor\assets\images\应急基地.png
const defaultPoint = new Cesium.Entity({
position: Cesium.Cartesian3.fromDegrees(
DEFAULT_CAMERA_VIEW.lon,
DEFAULT_CAMERA_VIEW.lat,
0
),
billboard: {
image: eventIcon,
width: 36,
height: 36,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
viewer.entities.add(defaultPoint);
// 在默认点附近添加10个模拟点位应急人员和应急装备
const simulatedPoints = [
// 应急人员 (6个)
{ type: 'soldier', name: '张三', department: '应急救援队', lon: 107.97, lat: 30.25, distance: 2.5, estimatedArrival: '10分钟', icon: soldierIcon },
{ type: 'soldier', name: '李四', department: '消防队', lon: 107.971901, lat: 30.251428, distance: 2.3, estimatedArrival: '8分钟', icon: soldierIcon },
{ type: 'soldier', name: '王五', department: '医疗队', lon: 107.974901, lat: 30.241428, distance: 3.1, estimatedArrival: '12分钟', icon: soldierIcon },
{ type: 'soldier', name: '赵六', department: '应急救援队', lon: 108.047344, lat: 30.164313, distance: 4.2, estimatedArrival: '15分钟', icon: soldierIcon },
{ type: 'soldier', name: '刘七', department: '消防队', lon: 108.046344, lat: 30.168313, distance: 3.8, estimatedArrival: '14分钟', icon: soldierIcon },
{ type: 'soldier', name: '陈八', department: '医疗队', lon: 108.050344, lat: 30.170313, distance: 4.5, estimatedArrival: '16分钟', icon: soldierIcon },
// 应急装备 (4个)
{ type: 'device', name: '救援车辆A', deviceType: '消防车', lon: 107.98088, lat: 30.2487, distance: 2.8, estimatedArrival: '11分钟', icon: deviceIcon },
{ type: 'device', name: '救援车辆B', deviceType: '救护车', lon: 107.97898, lat: 30.2502, distance: 2.6, estimatedArrival: '9分钟', icon: deviceIcon },
{ type: 'device', name: '无人机A', deviceType: 'DJI', lon: 108.049344, lat: 30.160313, distance: 4.8, estimatedArrival: '17分钟', icon: deviceIcon },
{ type: 'device', name: '无人机B', deviceType: 'DJI', lon: 108.043344, lat: 30.169313, distance: 3.6, estimatedArrival: '13分钟', 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}公里`,
estimatedArrival: point.estimatedArrival
}
: {
type: 'device',
name: point.name,
deviceType: point.deviceType,
location: `目前为止距离现场${point.distance}公里`,
estimatedArrival: point.estimatedArrival
}
});
});
console.log(`[index.vue] 已添加 ${simulatedPoints.length} 个模拟点位`);
// camera.setView({
// ...DEFAULT_CAMERA_VIEW,
// });
camera.flyTo({
...DEFAULT_CAMERA_VIEW,
duration: 1,
});
// 设置地图点击事件监听 - 显示 Tooltip
setupMapClickHandler(viewer);
// 延迟 1000ms 后设置相机到默认位置
// setTimeout(() => {
// camera.flyTo({
// ...DEFAULT_CAMERA_VIEW,
// duration: 1,
// });
// }, 5000);
// return;
/**
* 设置相机到指定的笛卡尔坐标
* @param {Cesium.Cartesian3 | null} cartesian - 目标位置
* @returns {boolean} 是否成功设置
*/
const focusCameraOnCartesian = (cartesian) => {
if (!cartesian) return false;
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
const centerLon = Cesium.Math.toDegrees(cartographic.longitude);
const centerLat = Cesium.Math.toDegrees(cartographic.latitude);
console.log(
`设置相机对准塌陷区域中心: 经度 ${centerLon.toFixed(
6
)}, 纬度 ${centerLat.toFixed(6)}`
);
camera.setView({
...DEFAULT_CAMERA_VIEW,
lon: centerLon,
lat: centerLat,
});
return true;
};
// 步骤1: 提前获取塌陷区域中心点(不依赖地形加载)
// 如果成功获取,先设置相机对准该区域,让用户看到目标区域
const collapseCenter = getCollapseCenter();
let hasCameraTarget = focusCameraOnCartesian(collapseCenter);
if (!hasCameraTarget) {
console.warn("无法获取塌陷区域中心,使用默认相机位置");
camera.setView(DEFAULT_CAMERA_VIEW);
}
// 步骤2: 加载3D Tiles灾后场景并等待完全就绪
// 这一步至关重要,必须等待瓦片加载完成后才能准确采样地面高度
try {
console.log("[index.vue] 开始加载3D模型...");
const afterTileset = await load3DTileset(viewer, "after", false);
if (afterTileset) {
console.log("[index.vue] 等待3D模型完全就绪包括首批瓦片...");
await waitForTilesetReady(afterTileset);
console.log("[index.vue] 3D模型已完全就绪");
} else {
console.warn("[index.vue] 3D模型加载返回 null");
}
} catch (error) {
console.error("[index.vue] 3D模型加载失败:", error);
}
// 步骤3: 初始化地图标记(单兵、设备、应急基地等)
// 此时3D Tiles已加载完成可以安全添加标记
try {
console.log("[index.vue] 开始初始化地图标记...");
const sampledCollapseCenter = await initializeMarkers(viewer, {
useSampledHeights: true, // 使用采样高度,确保标记位置准确
heightOffset: 100, // 标记相对地面100米与 WuRenJi 保持一致)
});
// 如果之前没有设置相机(配置数据缺失),现在再次尝试
if (!hasCameraTarget && sampledCollapseCenter) {
hasCameraTarget = focusCameraOnCartesian(sampledCollapseCenter);
}
console.log("[index.vue] 地图标记初始化完成");
// camera.setView({
// ...DEFAULT_CAMERA_VIEW,
// })
} catch (error) {
console.error("[index.vue] 地图标记初始化失败:", error);
// 即使标记初始化失败,也要确保相机位置正确
if (!hasCameraTarget) {
console.warn("[index.vue] 标记初始化失败且无相机目标,使用默认位置");
// camera.setView(DEFAULT_CAMERA_VIEW)
}
}
// 创建初始范围圈(使用当前搜索半径)
createOrUpdateRangeCircle(viewer, disasterData.forcePreset.value.searchRadius);
});
});
// 根据经纬度加载养护站数据 /disaster/matchEmergencyResources
const loadEmergencyResources = async (longitude, latitude) => {
try {
const response = await request({
url: `/snow-ops-platform/disaster/matchEmergencyResources`,
method: "GET",
params: {
longitude,
latitude,
maxDistance: disasterData.forcePreset.value.searchRadius,
},
});
if (response?.data) {
// 更新力量预置数据
disasterData.updateForcePreset(response.data);
console.log("[index.vue] 应急资源数据加载成功:", response.data);
// 更新地图标记
if (mapStore.viewer) {
console.log("[index.vue] 更新地图应急资源标记...");
// 清除旧的应急资源标记
clearEmergencyResourceMarkers(mapStore.viewer);
// 添加新的应急资源标记
await addEmergencyResourceMarkers(
mapStore.viewer,
response.data,
{ longitude, latitude },
{ heightOffset: 10 }
);
} else {
console.warn("[index.vue] 地图viewer未就绪跳过标记更新");
}
} else {
console.warn("[index.vue] 应急资源接口返回数据为空");
}
return response;
} catch (error) {
console.error("[index.vue] 加载应急资源数据失败:", error);
ElMessage.warning({
message: "应急资源数据加载失败,使用默认数据",
duration: 3000,
});
return null;
}
};
// 加载储备中心和预置点数据
const loadReserveCentersAndPresets = async (longitude, latitude) => {
try {
const response = await request({
url: `/snow-ops-platform/yhYjll/list`,
method: "GET",
params: {
longitude,
latitude,
maxDistance: disasterData.forcePreset.value.searchRadius,
},
});
if (response?.data && Array.isArray(response.data)) {
console.log("[index.vue] 储备中心和预置点数据加载成功:", response.data);
// 更新地图标记
if (mapStore.viewer) {
console.log("[index.vue] 添加储备中心和预置点地图标记...");
// 清除旧的标记
clearReserveCenterMarkers(mapStore.viewer);
// 添加新的标记
await addReserveCenterMarkers(
mapStore.viewer,
response.data,
{ heightOffset: 10 }
);
} else {
console.warn("[index.vue] 地图viewer未就绪跳过标记更新");
}
} else {
console.warn("[index.vue] 储备中心和预置点接口返回数据为空");
}
return response;
} catch (error) {
console.error("[index.vue] 加载储备中心和预置点数据失败:", error);
ElMessage.warning({
message: "储备中心和预置点数据加载失败",
duration: 3000,
});
return null;
}
};
onMounted(async () => {
// 加载应急资源数据(使用默认灾害点坐标)
// await loadEmergencyResources(108.011506, 30.175827);
const response = await loadEmergencyResources(108.011506, 30.175827);
// 加载储备中心和预置点数据(使用相同的坐标)
await loadReserveCentersAndPresets(108.011506, 30.175827);
});
/**
* 处理地图工具变化事件
* 目前主要处理"模型对比"工具的切换
*
* @param {Object} payload - 工具变化事件载荷
* @param {string} payload.tool - 工具标识
* @param {boolean} payload.active - 工具是否激活
*/
const handleMapToolChange = async ({ tool, active }) => {
console.log(`地图工具变化: ${tool}, 激活状态: ${active}`);
if (tool === "modelCompare") {
try {
// 显示加载提示
const loadingMessage = ElMessage({
message: active ? "正在启用模型对比..." : "正在关闭模型对比...",
type: "info",
duration: 0,
showClose: false,
});
// 使用新的双地图对比模式
await toggleCompareMode(active, mapStore.viewer);
// 关闭加载提示
loadingMessage.close();
// 显示成功提示
ElMessage.success(active ? "模型对比已启用" : "模型对比已关闭");
} catch (error) {
console.error("切换模型对比模式失败:", error);
// 显示错误提示
ElMessage.error({
message: `切换模型对比失败: ${error.message || "未知错误"}`,
duration: 3000,
});
}
}
// 其他工具的处理可以在这里扩展
// if (tool === 'measure') { ... }
};
// 弹窗状态
const showPersonnelDetail = ref(false);
const showCenterDetail = ref(false);
// 选中的数据
const selectedPersonnel = ref({
name: "张强",
department: "安全生产部",
distance: 0.6,
estimatedArrival: 10,
avatar: null,
});
const selectedCenter = ref({
name: "忠县应急中心",
adminLevel: "国道",
department: "交通公路部门",
distance: 0.6,
image: null,
});
// 返回驾驶舱
const handleBack = () => {
console.log("返回驾驶舱");
// 实际实现:路由跳转
// router.push('/cockpit')
};
// 处理人员联动
const handlePersonnelLink = (personnel) => {
console.log("联动人员:", personnel);
// 实际实现:使用 mapStore 的 camera service 飞行到人员位置
// const { camera } = mapStore.services()
// camera.flyTo({ destination: [lon, lat, height] })
showPersonnelDetail.value = false;
};
/**
* 处理 Tooltip 操作按钮点击事件
* @param {Object} action - 操作对象,包含 type 和 data
*/
const handleTooltipAction = (action) => {
console.log('[index.vue] Tooltip 操作按钮点击:', action);
if (action.type === 'link') {
// 应急人员的"联动"操作
const entity = action.data;
const properties = entity.properties;
console.log('[index.vue] 应急人员联动:', {
name: properties.name?.getValue(),
department: properties.department?.getValue()
});
// 可以在这里实现联动功能,比如:
// 1. 飞行到人员位置
// 2. 打开详情弹窗
// 3. 高亮显示相关信息
ElMessage.success(`已联动应急人员: ${properties.name?.getValue() || '未知'}`);
// 关闭 tooltip
hideTooltip();
currentTooltipEntity.value = null;
} else if (action.type === 'connect') {
// 应急基地的"连线"操作
const entity = action.data;
const properties = entity.properties;
console.log('[index.vue] 应急基地连线:', {
name: properties.name?.getValue(),
routeNumber: properties.routeNumber?.getValue()
});
// 可以在这里实现连线功能,比如:
// 1. 在地图上绘制线路
// 2. 显示路径规划
// 3. 计算距离和时间
ElMessage.success(`已连线应急基地: ${properties.name?.getValue() || '未知'}`);
// 关闭 tooltip
hideTooltip();
currentTooltipEntity.value = null;
}
};
/**
* 关闭地图 Tooltip
* 统一的关闭入口,便于后续扩展埋点或联动逻辑
*/
const handleMapTooltipClose = () => {
mapTooltip.value.visible = false;
};
// 路径线实体引用
const pathLineEntity = ref(null);
/**
* 绘制红色路径线
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
*/
const drawRedPathLine = (viewer) => {
if (!viewer) {
console.warn('[index.vue] drawRedPathLine: viewer 为空');
return;
}
// 如果已存在路径线,先移除
if (pathLineEntity.value) {
viewer.entities.remove(pathLineEntity.value);
pathLineEntity.value = null;
}
// 路径坐标点
const pathCoordinates = [
{ x: -1706079.1327424292, y: 5247893.165552528, z: 3187993.9339800295 },
{ x: -1706116.7863268533, y: 5247923.177994122, z: 3187929.297700776 },
{ x: -1706131.4939896727, y: 5247956.7916397555, z: 3187865.1250298577 },
{ x: -1706117.7768181972, y: 5247999.865521995, z: 3187795.4584125844 },
{ x: -1706148.232862157, y: 5248029.100250082, z: 3187735.2203392833 },
{ x: -1706129.4638550146, y: 5248073.941490989, z: 3187662.59740559 },
{ x: -1706131.3071046746, y: 5248086.057462914, z: 3187643.216358425 },
{ x: -1706164.2362053818, y: 5248120.213627388, z: 3187577.1867482658 },
{ x: -1706255.3513903276, y: 5248175.916851786, z: 3187422.819624157 },
{ x: -1706300.2731912779, y: 5248172.011305182, z: 3187397.8767570513 },
{ x: -1706343.1007708232, y: 5248165.925888667, z: 3187382.186124808 }
];
// 将坐标点转换为 Cartesian3 数组
const positions = pathCoordinates.map(coord =>
new Cesium.Cartesian3(coord.x, coord.y, coord.z)
);
// 创建红色路径线实体
pathLineEntity.value = viewer.entities.add({
polyline: {
positions: positions,
width: 5,
material: Cesium.Color.RED,
clampToGround: true,
// 添加发光效果使路径更醒目
// material: new Cesium.PolylineGlowMaterialProperty({
// glowPower: 0.3,
// color: Cesium.Color.RED
// })
}
});
console.log('[index.vue] 红色路径线已绘制');
};
/**
* 处理力量调度启动事件
* 显示加载动画3秒后自动隐藏然后启动人员移动动画
*/
const handleStartDispatch = (payload) => {
console.log('[index.vue] 启动力量调度:', payload);
// 显示加载动画
showLoading.value = true;
// 绘制红色路径线
if (mapStore.viewer) {
drawRedPathLine(mapStore.viewer);
}
// 3秒后隐藏加载动画并启动人员移动
setTimeout(() => {
showLoading.value = false;
// 启动应急人员沿路径移动动画
if (mapStore.viewer) {
console.log('[index.vue] 启动应急人员移动动画...');
startPersonnelMovement(mapStore.viewer, {
duration: 60, // 60秒完成整个路径移动更清晰可见
trackEntity: false, // 不自动跟随相机(可设为 true 启用跟随)
personnelName: '应急救援人员',
department: '应急救援队'
});
} else {
console.warn('[index.vue] 地图viewer未就绪无法启动动画');
}
}, 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
}
});
// 禁用范围圈的鼠标交互,让点击可以穿透到下面的标记点
if (rangeCircleEntity.value) {
rangeCircleEntity.value.allowPicking = false;
}
console.log(`[index.vue] 已创建/更新范围圈: ${radiusKm}km`);
};
/**
* 在指定屏幕坐标显示地图 Tooltip
*
* 使用场景:
* - 当用户点击地图上的标记点时调用此方法
* - 调用方需要将地图实体的经纬度转换为屏幕坐标
*
* @typedef {Object} MapTooltipField
* @property {string} label - 字段标签
* @property {string} value - 字段值
*
* @typedef {Object} MapTooltipData
* @property {MapTooltipField[]} fields - 字段列表
*
* @param {Object} options - Tooltip 配置选项
* @param {number} options.x - 屏幕 X 坐标(像素),相对于地图容器左上角
* @param {number} options.y - 屏幕 Y 坐标(像素),相对于地图容器左上角
* @param {string} [options.title=''] - Tooltip 标题文本
* @param {string} [options.icon=''] - 标题左侧图标的图片路径
* @param {MapTooltipData} [options.data=null] - 业务数据,用于内容插槽渲染
*
* @example
* // 当点击地图实体时
* const screenPos = mapStore.worldToScreen(entity.position)
* showMapTooltip({
* x: screenPos.x,
* y: screenPos.y,
* title: '应急中心',
* icon: emergencyCenterIcon,
* data: {
* fields: [
* { label: '名称', value: '忠县应急中心' },
* { label: '行政等级', value: '国道' },
* { label: '隶属单位', value: '交通公路部门' }
* ]
* }
* })
*/
const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
const state = mapTooltip.value;
state.visible = true;
state.x = x;
state.y = y;
state.title = title;
state.icon = icon;
state.data = data;
// zIndex 保持不变,无需重新赋值
};
// TODO: 实现地图实体点击事件监听
// 当用户点击地图上的标记点时,显示 Tooltip 或详情弹窗
//
// 集成示例(需要先在文件顶部导入图标):
// import personnelIcon from './assets/images/personnel-icon.png'
// import centerIcon from './assets/images/center-icon.png'
//
// mapStore.onReady(() => {
// const { query } = mapStore.services()
//
// // 监听实体点击事件
// query.onEntityClick((entity) => {
// // 1. 将实体位置转换为屏幕坐标
// // 注意:具体 API 取决于你使用的地图引擎Cesium/Mapbox/etc.
// const screenPos = mapStore.worldToScreen(entity.position)
//
// // 2. 显示 Tooltip 展示基本信息
// if (entity.type === 'personnel') {
// showMapTooltip({
// x: screenPos.x,
// y: screenPos.y,
// title: '应急人员',
// icon: personnelIcon,
// data: {
// fields: [
// { label: '姓名', value: entity.properties.name },
// { label: '部门', value: entity.properties.department },
// { label: '距离', value: `${entity.properties.distance}公里` }
// ]
// }
// })
// } else if (entity.type === 'center') {
// showMapTooltip({
// x: screenPos.x,
// y: screenPos.y,
// title: '应急中心',
// icon: centerIcon,
// data: {
// fields: [
// { label: '名称', value: entity.properties.name },
// { label: '行政等级', value: entity.properties.adminLevel },
// { label: '隶属单位', value: entity.properties.department }
// ]
// }
// })
// }
//
// // 3. 如果需要打开详情弹窗,可以在 Tooltip 中添加按钮
// // 或者直接在这里同时打开详情弹窗(根据业务需求)
// // selectedPersonnel.value = entity.properties
// // showPersonnelDetail.value = true
// })
// })
</script>
<style scoped lang="scss">
@use "@/styles/mixins.scss" as *;
@use "./assets/styles/common.scss" as *;
.situational-awareness {
// 容器查询设置,用于嵌入场景的自适应缩放
container-name: situational-awareness;
container-type: size;
// 为旧版浏览器提供视口单位回退,封顶 1920×1080
--cq-inline-100: clamp(0px, 100vw, 1920px);
--cq-block-100: clamp(0px, 100vh, 1080px);
// 当支持容器单位时覆盖为容器单位,同样封顶
@supports (width: 1cqw) {
--cq-inline-100: min(100cqw, 1920px);
}
@supports (height: 1cqh) {
--cq-block-100: min(100cqh, 1080px);
}
// 可配置的布局变量(使用 calc 直接计算,避免函数嵌套)
--sa-left-width: calc(464 / 1920 * var(--cq-inline-100, 100vw));
--sa-right-width: calc(486 / 1920 * var(--cq-inline-100, 100vw));
--sa-gap: calc(16 / 1920 * var(--cq-inline-100, 100vw));
--sa-padding: calc(16 / 1920 * var(--cq-inline-100, 100vw));
--sa-header-height: calc(
131 / 1080 * var(--cq-block-100, 100vh)
); // Header 高度
--sa-min-width: 1280px;
--sa-min-height: 720px;
position: relative;
width: 100%;
height: 100%;
// max-width: 1920px;
// max-height: 1080px;
min-width: var(--sa-min-width);
min-height: var(--sa-min-height);
background-color: var(--bg-dark);
overflow-x: hidden;
overflow-y: auto; // 当宿主尺寸 < 最小尺寸时允许滚动,达到上限时不放大
// PageHeader 浮在顶部
> :first-child {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
}
&__main {
position: absolute;
inset: 0; // 铺满整个容器
background: url(./assets/images/main-bg.png) center/cover no-repeat;
overflow: hidden;
}
// 地图底层 - 填满整个容器
&__map-layer {
position: absolute;
inset: 0;
z-index: 0;
display: flex;
// 默认单地图模式
&:not(.is-compare-mode) {
.situational-awareness__right-map {
width: 100%;
}
.situational-awareness__left-map {
width: 0;
opacity: 0;
pointer-events: none;
overflow: hidden;
}
}
// 双地图对比模式
&.is-compare-mode {
.situational-awareness__left-map {
width: 50%;
opacity: 1;
pointer-events: auto;
transition: width 0.3s ease, opacity 0.3s ease;
}
.situational-awareness__right-map {
width: 50%;
transition: width 0.3s ease;
}
}
}
// 左侧地图容器(灾前场景 - 对比时显示)
&__left-map {
position: relative;
height: 100%;
background: #000;
}
// 右侧地图容器(灾后场景 - 主地图)
&__right-map {
position: relative;
height: 100%;
}
// 中间分割线
&__center-divider {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.5),
rgba(255, 255, 255, 0.1)
);
z-index: 1;
pointer-events: none;
}
// 地图遮罩层 - 覆盖在地图之上,增强视觉效果
&__map-mask {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none; // 不阻挡交互
// 使用 cockpit 的遮罩层图片,保持视觉一致性
background: url(@/views/cockpit/assets/img/遮罩层.png) center/cover
no-repeat;
}
// 场景标签层 - 显示在遮罩层和面板层之上
&__scene-labels-layer {
position: absolute;
inset: 0;
z-index: 10;
pointer-events: none; // 标签不阻挡交互
}
// 浮动面板层 - 使用绝对定位固定面板位置
&__panels-layer {
position: absolute;
inset: 0;
z-index: 2;
height: 100%;
pointer-events: none; // 容器不拦截事件,让中间区域透明
}
// 左右面板列 - 浮动卡片样式
&__panel-column {
position: absolute;
// top: var(--sa-header-height); // 从 header 下方开始
// bottom: 0;
display: flex;
flex-direction: column;
gap: var(--sa-gap); // 列内子面板之间的间距
min-width: 0; // 防止在窄容器中溢出
min-height: 0; // 允许 flex 子元素收缩并启用滚动
pointer-events: auto; // 恢复面板的交互能力
// 左侧面板固定在左边
&--left {
left: 0;
width: var(--sa-left-width);
}
// 右侧面板固定在右边
&--right {
right: 0;
width: var(--sa-right-width);
}
}
// 中间占位区域 - 透明且不可交互,点击穿透到地图
&__center-spacer {
pointer-events: none;
}
// 折叠按钮层 - 独立层级,放置在屏幕两侧
&__collapse-buttons-layer {
position: absolute;
inset: 0;
z-index: 11; // 高于场景标签层
pointer-events: none; // 容器不拦截事件
padding-top: var(--sa-header-height); // 预留 Header 高度
}
// 折叠按钮
&__collapse-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
cursor: pointer;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
z-index: 11;
padding: 0;
&:hover {
opacity: 0.8;
}
&:active {
transform: translateY(-50%) scale(0.95);
}
.collapse-arrow {
width: vw(30);
height: auto;
transition: all 0.3s ease;
}
// 左侧按钮 - 固定在屏幕最左侧
&--left {
left: 0;
}
// 右侧按钮 - 固定在屏幕最右侧
&--right {
right: 0;
}
}
// 面板滑动动画 - 左侧
.panel-slide-left-enter-active,
.panel-slide-left-leave-active {
transition: all 0.3s ease;
}
.panel-slide-left-enter-from {
transform: translateX(-100%);
opacity: 0;
}
.panel-slide-left-leave-to {
transform: translateX(-100%);
opacity: 0;
}
// 面板滑动动画 - 右侧
.panel-slide-right-enter-active,
.panel-slide-right-leave-active {
transition: all 0.3s ease;
}
.panel-slide-right-enter-from {
transform: translateX(100%);
opacity: 0;
}
.panel-slide-right-leave-to {
transform: translateX(100%);
opacity: 0;
}
// 地图控件层 - 高于遮罩和面板,用于放置地图控制工具
&__controls-layer {
position: absolute;
inset: 0;
z-index: 3;
pointer-events: none; // 容器不拦截事件
display: flex;
justify-content: center;
align-items: flex-end;
padding-bottom: 30px; // 临时使用固定值,确保控件显示
}
// 控件容器 - 恢复交互能力
&__controls {
pointer-events: auto;
position: relative;
// 调试:确保控件容器可见
min-height: 56px; // MapControls 的高度
}
// 地图 Tooltip 层 - 覆盖地图和面板,仅 Tooltip 自身可交互
&__tooltip-layer {
position: absolute;
inset: 0;
z-index: 4; // 高于控件层
pointer-events: none; // 容器不拦截事件,点击穿透到地图
}
// 加载动画层 - 一键启动后显示
&__loading-layer {
position: absolute;
top: calc(var(--sa-header-height) + vh(40));
left: 0;
right: 0;
// bottom: 0;
z-index: 5; // 高于 Tooltip 层
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; // 不阻止点击穿透
}
// 加载 GIF 图片
&__loading-gif {
width: auto;
height: auto;
max-width: 320px;
max-height: 55px;
object-fit: contain;
}
}
// Tooltip 内容字段样式
// 用于在 Tooltip 插槽中展示字段列表
.tooltip-field-item {
display: flex;
align-items: baseline;
gap: vw(8);
padding: vh(6) 0;
.tooltip-field-label {
color: var(--text-gray);
font-size: fs(13);
font-family: SourceHanSansCN-Regular, sans-serif;
white-space: nowrap;
flex-shrink: 0;
}
.tooltip-field-value {
color: var(--text-white);
font-size: fs(13);
font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 500;
flex: 1;
word-break: break-all;
}
}
// Tooltip 操作按钮样式
// 用于"连线"、"联动"等交互按钮
.tooltip-action-btn {
min-width: vw(100);
height: vh(36);
padding: 0 vw(24);
background: url('./assets/images/地图tooltip-button.png') no-repeat center/100% 100%;
border: none;
color: var(--text-white);
font-size: fs(14);
font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
filter: brightness(1.2);
transform: translateY(vh(-2));
}
&:active {
filter: brightness(0.9);
transform: translateY(0);
}
}
// 窄容器嵌入的紧凑布局(<1100px 宽度)
.situational-awareness.is-compact {
--sa-left-width: calc(380 / 1920 * var(--cq-inline-100, 100vw));
--sa-right-width: calc(400 / 1920 * var(--cq-inline-100, 100vw));
--sa-gap: calc(12 / 1920 * var(--cq-inline-100, 100vw));
--sa-padding: calc(12 / 1920 * var(--cq-inline-100, 100vw));
}
// 嵌入模式 - 使用更保守的最小尺寸
.situational-awareness.is-embedded {
--sa-min-width: 1024px;
--sa-min-height: 600px;
}
</style>