From 06c462a57722d408b7cba93c6612ff97e2f25ffd Mon Sep 17 00:00:00 2001
From: Zzc <1373857752@qq.com>
Date: Thu, 20 Nov 2025 18:09:04 +0800
Subject: [PATCH] =?UTF-8?q?feat(screen):=20=E4=B8=BA3D=E6=80=81=E5=8A=BF?=
=?UTF-8?q?=E6=84=9F=E7=9F=A5=E6=B7=BB=E5=8A=A0=E4=BA=BA=E5=91=98=E7=A7=BB?=
=?UTF-8?q?=E5=8A=A8=E5=8A=A8=E7=94=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 引入 `useEntityAnimation` 可组合项,用于在Cesium查看器中沿预定义路径为紧急人员制作动画
- 添加脉冲广告牌效果、发光路径轨迹和相机跟踪选项
- 将动画启动与调度事件集成,支持可配置的时长和实体属性
- 增强地图工具提示,添加人员关联和基地连接的操作按钮
- 更新模拟点,显示预计到达时间,以提高态势感知能力
---
.../composables/useEntityAnimation.js | 261 ++++++++++++++++++
.../3DSituationalAwarenessRefactor/index.vue | 174 ++++++++++--
2 files changed, 408 insertions(+), 27 deletions(-)
create mode 100644 packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js
diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js
new file mode 100644
index 0000000..aa56c92
--- /dev/null
+++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js
@@ -0,0 +1,261 @@
+import { ref } from 'vue'
+import * as Cesium from 'cesium'
+import soldierIcon from '../assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png'
+
+/**
+ * 实体动画管理 Composable
+ * 负责管理地图实体的路径动画,特别是应急人员沿路径移动
+ */
+export function useEntityAnimation() {
+ // 动画实体引用
+ const animatedEntity = ref(null)
+
+ // 动画是否正在运行
+ const isAnimating = ref(false)
+
+ /**
+ * 应急人员路径坐标 (Cartesian3 格式)
+ * 这些坐标定义了人员从起点到终点的移动路径
+ */
+ const PERSONNEL_PATH_COORDINATES = [
+ { x: -1705480.2641142386, y: 5247435.146513505, z: 3189090.667137187 },
+ { x: -1705494.4820981335, y: 5247449.285793587, z: 3189060.452114544 },
+ { x: -1705510.0809808762, y: 5247465.767274618, z: 3189023.050648535 },
+ { x: -1705511.0338125327, y: 5247476.4118378535, z: 3188998.3643177846 },
+ { x: -1705515.8155320068, y: 5247491.504151518, z: 3188970.3225750974 },
+ { x: -1705512.9523099929, y: 5247504.710873116, z: 3188943.7936982675 },
+ { x: -1705519.7649184526, y: 5247519.060354441, z: 3188915.1883725724 },
+ { x: -1705528.241912857, y: 5247539.302819527, z: 3188872.220619207 },
+ { x: -1705530.7649293465, y: 5247548.26353356, z: 3188852.4565014304 },
+ { x: -1705536.847870567, y: 5247562.107401437, z: 3188816.1164027476 },
+ { x: -1705554.2817406887, y: 5247571.234068825, z: 3188789.6105980803 },
+ { x: -1705573.026007999, y: 5247580.50225183, z: 3188770.244426234 },
+ { x: -1705602.018256302, y: 5247597.236114229, z: 3188743.7470805836 }
+ ]
+
+ /**
+ * 启动应急人员沿路径移动动画
+ * @param {Cesium.Viewer} viewer - Cesium viewer 实例
+ * @param {Object} options - 配置选项
+ * @param {number} [options.duration=36] - 动画总时长(秒)
+ * @param {boolean} [options.trackEntity=false] - 是否相机跟随实体
+ * @param {string} [options.personnelName='应急人员'] - 人员名称
+ * @param {string} [options.department='应急救援队'] - 所属部门
+ * @returns {Cesium.Entity} 返回创建的动画实体
+ */
+ const startPersonnelMovement = (viewer, options = {}) => {
+ if (!viewer) {
+ console.warn('[useEntityAnimation] startPersonnelMovement: viewer 为空')
+ return null
+ }
+
+ if (isAnimating.value) {
+ console.warn('[useEntityAnimation] 动画已在运行中,请先停止当前动画')
+ return animatedEntity.value
+ }
+
+ const config = {
+ duration: options.duration ?? 60, // 默认 60 秒,让移动更清晰可见
+ trackEntity: options.trackEntity ?? false,
+ personnelName: options.personnelName ?? '应急人员',
+ department: options.department ?? '应急救援队'
+ }
+
+ console.log('[useEntityAnimation] 开始启动人员移动动画...', config)
+
+ // 设置动画时间范围
+ const startTime = Cesium.JulianDate.now()
+ const stopTime = Cesium.JulianDate.addSeconds(
+ startTime,
+ config.duration,
+ new Cesium.JulianDate()
+ )
+
+ // 配置 viewer 时钟
+ viewer.clock.startTime = startTime.clone()
+ viewer.clock.stopTime = stopTime.clone()
+ viewer.clock.currentTime = startTime.clone()
+ viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP // 动画结束后停止
+ viewer.clock.multiplier = 1 // 实时速度
+ viewer.clock.shouldAnimate = true
+
+ // 创建 SampledPositionProperty 定义路径
+ const positionProperty = new Cesium.SampledPositionProperty()
+
+ // 计算每个路径点的时间间隔
+ const numberOfPoints = PERSONNEL_PATH_COORDINATES.length
+ const timeInterval = config.duration / (numberOfPoints - 1)
+
+ // 添加路径采样点
+ PERSONNEL_PATH_COORDINATES.forEach((coord, index) => {
+ const time = Cesium.JulianDate.addSeconds(
+ startTime,
+ index * timeInterval,
+ new Cesium.JulianDate()
+ )
+ const position = new Cesium.Cartesian3(coord.x, coord.y, coord.z)
+ positionProperty.addSample(time, position)
+ })
+
+ // 创建脉冲缩放效果 - 让图标闪烁更醒目
+ const pulseScale = new Cesium.CallbackProperty((time) => {
+ const elapsed = Cesium.JulianDate.secondsDifference(time, startTime)
+ // 使用正弦波产生脉冲效果,频率 3Hz,幅度 ±30%
+ return 1.0 + Math.sin(elapsed * 3) * 0.3
+ }, false)
+
+ // 创建动画实体
+ const entity = viewer.entities.add({
+ availability: new Cesium.TimeIntervalCollection([
+ new Cesium.TimeInterval({
+ start: startTime,
+ stop: stopTime
+ })
+ ]),
+ position: positionProperty,
+ orientation: new Cesium.VelocityOrientationProperty(positionProperty), // 自动朝向移动方向
+ billboard: {
+ image: soldierIcon,
+ width: 48, // 增大尺寸,从 36 增加到 48
+ height: 56, // 从 40 增加到 56
+ scale: pulseScale, // 使用脉冲缩放效果
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
+ // 添加距离缩放,让图标在不同距离下都清晰可见
+ scaleByDistance: new Cesium.NearFarScalar(
+ 1000, 1.5, // 近距离(1000米)放大到 1.5 倍
+ 50000, 0.8 // 远距离(50000米)缩小到 0.8 倍
+ )
+ },
+ // 添加发光轨迹线
+ path: {
+ resolution: 1,
+ material: new Cesium.PolylineGlowMaterialProperty({
+ glowPower: 0.4, // 发光强度
+ taperPower: 0.5, // 渐变效果
+ color: Cesium.Color.CYAN // 青色轨迹
+ }),
+ width: 8,
+ leadTime: 0, // 前方不显示轨迹
+ trailTime: config.duration // 后方显示完整轨迹
+ },
+ properties: {
+ type: 'animatedSoldier',
+ name: config.personnelName,
+ department: config.department,
+ isAnimating: true
+ }
+ })
+
+ animatedEntity.value = entity
+ isAnimating.value = true
+
+ // 可选: 相机跟随实体
+ if (config.trackEntity) {
+ viewer.trackedEntity = entity
+ // 设置相机视角偏移
+ entity.viewFrom = new Cesium.Cartesian3(-100.0, -100.0, 50.0)
+ }
+
+ console.log('[useEntityAnimation] 人员移动动画已启动')
+
+ // 监听动画结束事件
+ const removeListener = viewer.clock.onStop.addEventListener(() => {
+ console.log('[useEntityAnimation] 动画已结束')
+ isAnimating.value = false
+ removeListener() // 移除监听器
+ })
+
+ return entity
+ }
+
+ /**
+ * 停止人员移动动画
+ * @param {Cesium.Viewer} viewer - Cesium viewer 实例
+ * @param {boolean} [removeEntity=true] - 是否移除动画实体
+ */
+ const stopPersonnelMovement = (viewer, removeEntity = true) => {
+ if (!viewer) {
+ console.warn('[useEntityAnimation] stopPersonnelMovement: viewer 为空')
+ return
+ }
+
+ console.log('[useEntityAnimation] 停止人员移动动画')
+
+ // 停止时钟动画
+ viewer.clock.shouldAnimate = false
+
+ // 取消相机跟随
+ if (viewer.trackedEntity === animatedEntity.value) {
+ viewer.trackedEntity = undefined
+ }
+
+ // 移除实体
+ if (removeEntity && animatedEntity.value) {
+ viewer.entities.remove(animatedEntity.value)
+ animatedEntity.value = null
+ }
+
+ isAnimating.value = false
+ }
+
+ /**
+ * 暂停动画
+ * @param {Cesium.Viewer} viewer - Cesium viewer 实例
+ */
+ const pauseAnimation = (viewer) => {
+ if (!viewer) return
+ viewer.clock.shouldAnimate = false
+ console.log('[useEntityAnimation] 动画已暂停')
+ }
+
+ /**
+ * 恢复动画
+ * @param {Cesium.Viewer} viewer - Cesium viewer 实例
+ */
+ const resumeAnimation = (viewer) => {
+ if (!viewer) return
+ if (isAnimating.value) {
+ viewer.clock.shouldAnimate = true
+ console.log('[useEntityAnimation] 动画已恢复')
+ }
+ }
+
+ /**
+ * 获取路径的起点坐标 (用于初始标记点)
+ * @returns {Cesium.Cartesian3} 起点坐标
+ */
+ const getStartPosition = () => {
+ const firstCoord = PERSONNEL_PATH_COORDINATES[0]
+ return new Cesium.Cartesian3(firstCoord.x, firstCoord.y, firstCoord.z)
+ }
+
+ /**
+ * 将 Cartesian3 坐标转换为经纬度
+ * @param {Cesium.Cartesian3} cartesian - Cartesian3 坐标
+ * @returns {Object} { longitude, latitude, height }
+ */
+ const cartesianToLonLat = (cartesian) => {
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
+ return {
+ longitude: Cesium.Math.toDegrees(cartographic.longitude),
+ latitude: Cesium.Math.toDegrees(cartographic.latitude),
+ height: cartographic.height
+ }
+ }
+
+ return {
+ animatedEntity,
+ isAnimating,
+ startPersonnelMovement,
+ stopPersonnelMovement,
+ pauseAnimation,
+ resumeAnimation,
+ getStartPosition,
+ cartesianToLonLat,
+ PERSONNEL_PATH_COORDINATES
+ }
+}
+
+export default useEntityAnimation
diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue
index 8fc1446..ddfad6e 100644
--- a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue
+++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue
@@ -126,6 +126,18 @@
{{ field.value }}
+
+
+
+
+
@@ -172,6 +184,7 @@ 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";
@@ -233,6 +246,9 @@ const {
// 地图 Tooltip 功能
const { tooltipState: mapTooltip, showTooltip, hideTooltip, updateTooltipPosition } = useMapTooltip();
+// 实体动画功能
+const { startPersonnelMovement, stopPersonnelMovement, isAnimating } = useEntityAnimation();
+
// 当前显示 tooltip 的实体(用于相机移动时更新位置)
const currentTooltipEntity = ref(null);
@@ -403,28 +419,45 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
// 构建 Tooltip 数据
let title = '';
const fields = [];
+ const actions = [];
if (type === 'soldier') {
- title = '单兵信息';
+ // 应急人员
+ title = '应急人员';
fields.push(
{ label: '姓名', value: properties.name?.getValue() || '-' },
{ label: '部门', value: properties.department?.getValue() || '-' },
- { label: '位置', value: properties.location?.getValue() || '-' }
+ { label: '位置信息', value: properties.location?.getValue() || '-' },
+ { label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
);
+ actions.push({
+ label: '联动',
+ type: 'link',
+ data: entity
+ });
} else if (type === 'device') {
- title = '设备信息';
+ // 应急装备
+ title = '应急装备';
fields.push(
{ label: '设备名称', value: properties.name?.getValue() || '-' },
{ label: '设备类型', value: properties.deviceType?.getValue() || '-' },
- { label: '位置', value: properties.location?.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.address?.getValue() || '-' },
- { label: '距离', value: properties.distance?.getValue() || '-' }
+ { 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;
@@ -451,17 +484,14 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
title = '储备中心';
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
- { label: '区县', value: properties.district?.getValue() || '-' },
- { label: '人员数量', value: properties.personnelCount?.getValue() || '0' },
- { label: '占地面积', value: properties.area?.getValue() ? `${properties.area?.getValue()}㎡` : '-' }
+ { label: '位置信息', value: properties.location?.getValue() || '-' }
);
} else if (type === 'presetPoint') {
// 预置点
title = '预置点';
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
- { label: '区县', value: properties.district?.getValue() || '-' },
- { label: '人员数量', value: properties.personnelCount?.getValue() || '0' }
+ { label: '位置信息', value: properties.location?.getValue() || '-' }
);
}
@@ -471,7 +501,7 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
y: canvasPosition.y,
title,
icon,
- data: { fields }
+ data: { fields, actions: actions.length > 0 ? actions : undefined }
});
// 保存当前实体,用于相机移动时更新位置
@@ -554,17 +584,17 @@ onMounted(() => {
// 在默认点附近添加10个模拟点位(应急人员和应急装备)
const simulatedPoints = [
// 应急人员 (6个)
- { type: 'soldier', name: '张三', department: '应急救援队', lon: 107.97, lat: 30.25, distance: 2.5, icon: soldierIcon },
- { type: 'soldier', name: '李四', department: '消防队', lon: 107.971901, lat: 30.251428, distance: 2.3, icon: soldierIcon },
- { type: 'soldier', name: '王五', department: '医疗队', lon: 107.974901, lat: 30.241428, distance: 3.1, icon: soldierIcon },
- { type: 'soldier', name: '赵六', department: '应急救援队', lon: 108.047344, lat: 30.164313, distance: 4.2, icon: soldierIcon },
- { type: 'soldier', name: '刘七', department: '消防队', lon: 108.046344, lat: 30.168313, distance: 3.8, icon: soldierIcon },
- { type: 'soldier', name: '陈八', department: '医疗队', lon: 108.050344, lat: 30.170313, distance: 4.5, icon: soldierIcon },
+ { 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, icon: deviceIcon },
- { type: 'device', name: '救援车辆B', deviceType: '救护车', lon: 107.97898, lat: 30.2502, distance: 2.6, icon: deviceIcon },
- { type: 'device', name: '无人机A', deviceType: 'DJI', lon: 108.049344, lat: 30.160313, distance: 4.8, icon: deviceIcon },
- { type: 'device', name: '无人机B', deviceType: 'DJI', lon: 108.043344, lat: 30.169313, distance: 3.6, icon: deviceIcon }
+ { 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 => {
@@ -583,13 +613,15 @@ onMounted(() => {
type: 'soldier',
name: point.name,
department: point.department,
- location: `目前为止距离现场${point.distance}公里`
+ location: `目前为止距离现场${point.distance}公里`,
+ estimatedArrival: point.estimatedArrival
}
: {
type: 'device',
name: point.name,
deviceType: point.deviceType,
- location: `目前为止距离现场${point.distance}公里`
+ location: `目前为止距离现场${point.distance}公里`,
+ estimatedArrival: point.estimatedArrival
}
});
});
@@ -888,6 +920,55 @@ const handlePersonnelLink = (personnel) => {
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
* 统一的关闭入口,便于后续扩展埋点或联动逻辑
@@ -898,7 +979,7 @@ const handleMapTooltipClose = () => {
/**
* 处理力量调度启动事件
- * 显示加载动画,3秒后自动隐藏
+ * 显示加载动画,3秒后自动隐藏,然后启动人员移动动画
*/
const handleStartDispatch = (payload) => {
console.log('[index.vue] 启动力量调度:', payload);
@@ -906,9 +987,22 @@ const handleStartDispatch = (payload) => {
// 显示加载动画
showLoading.value = true;
- // 3秒后自动隐藏加载动画
+ // 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);
};
@@ -1401,6 +1495,32 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
}
}
+// 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));