Przeglądaj źródła

fix(cockpit): 一级菜单固定居中偏左,不随秒针移动(票102)

维护者 2026-11-02:菜单栏按钮要在居中偏左(当前位置略微左移,不与背景装饰线条
重合);现在时间字体长短不平均,秒针一跳按钮就跟着左右移动。

三个能让菜单移动的入口逐个关掉:

- 顶栏右列从 `minmax(min-content, 1fr)` 改回 `minmax(0, 1fr)`。
  票 37 当年为了让状态区不向左溢出压住菜单,把列宽交给内容最小宽度兜底;
  代价是列宽从此随内容变化,中列(一级模式)被推着走。溢出是靠三个诊断读数
  加时钟一起撑爆右列造成的,读数已在上一个提交下屏,等分列宽足够。
- 一级菜单整组 `translateX(-32 * var(--u))`。用 transform 而不是 margin:
  margin 会把让出的空位再分给左右两列,等于把「谁推菜单」的裁决权交回内容;
  translateX 是纯视觉偏移,几何仍居中,位移也不参与 grid 再分配。
- 时钟改成固定宽度数字槽位(右对齐 + tabular-nums + tnum 兜底,flex: 0 0 auto)。
  槽位 128u 大于最宽的一次读数(2384×1160 实测一次读数占 101u,--u≈1.433px),
  覆盖「1 换成 8」与字体回退,秒针跳动只改槽位内字形,不改任何布局宽度。

位置改动不碰语义:MODES / modeLabel / uiStore.setMode 与两个按钮原样。

闸门 verify/t102-topbar-layout.mjs 14/14(对上一个提交的源码为红)。
按 AGENTS.md 未跑 build/dev/浏览器。
ChenJZ 16 godzin temu
rodzic
commit
c435215c55

+ 1 - 0
docs/cockpit-deployment.md

@@ -516,6 +516,7 @@ location /gis/model/ {
 | 归位按钮 | 顶栏「归位」可点,平移后点它回到目标视角 | `map.home`、引擎是否就绪 |
 | 配置生效 | 网络面板里 `cockpit/config.json` 返回 200,且页面按它取值(改 `map.home` 后归位落点跟着变) | `config.json` 路径、`__COCKPIT_CONFIG_URL__` |
 | 数据源 | `dataSource: real` 时面板数据来自 `/api/cockpit/v1/**`(网络面板可见),mock 时没有这些请求 | `config.json` 的 `dataSource`、`?dataSource=` 覆盖 |
+| 顶栏菜单稳定 | 一级模式按钮固定「居中偏左」,秒针跳动时按钮**不左右移动**,也不压到背景光路 | 票 102:右列宽度由等分决定、时钟为固定数字槽位 |
 | 全局底图 | 地球有影像(不是黑球) | `map.basemap.source` |
 | 地形 | 浮标含「地形:dem」;拉近后有山体起伏 | nginx `/gis/dem/`、`layer.json` 能直接下载 |
 | 本地影像 | 压到目标区域能看到精细影像 | overlay 的 `url`/`{reverseY}`/`bounds` |

+ 28 - 6
src/modules/cockpit/CockpitShell.vue

@@ -2257,14 +2257,16 @@ const mapDataSourceCredits = computed(() => getDataSourceCredits())
    顶栏(压矮:56 设计像素)
    ============================================================ */
 /* 顶栏三列:品牌 / 一级模式 / 状态区。
-   票 37:右侧状态区改成 `minmax(min-content, 1fr)`,**不再让它被挤到溢出**。
-   原先写死 `minmax(240px, 1fr)`,`justify-content: flex-end` 在内容超宽时会**向左溢出**到
-   模式按钮那一列上 —— 票 37 加了一个「返回全球」chip 后正好越界 46px,
-   实测「归位」压住「预警处置」(x 855–897 vs 799–898)。
-   改成按内容最小宽度兜底后,列宽随内容长,两个 chip 再多也不会压到模式区。 */
+   票 102:右列的最小宽度从「按内容」改回「按等分」—— **列宽由等分决定,不由内容决定**。
+   此前右列按内容最小宽度兜底,于是内容里任何一点宽度变化(时钟秒针的字体长短不平均、
+   读数换字、chip 增减)都会改写列宽,把中列菜单推着左右移动
+   (维护者 2026-11-02 报障:时间一跳,按钮就跟着挪)。
+   右列现在吃等分且内容右对齐,内容再变也只在列内挪,碰不到中列。
+   票 37 当年担心的「右列内容超宽向左溢出压住模式区」,靠的是三个诊断读数 + 时钟
+   一起把右列撑爆;读数已下屏(本票),剩下的内容宽度远小于等分列宽。 */
 .cockpit-shell__topbar {
     display: grid;
-    grid-template-columns: minmax(240px, 1fr) auto minmax(min-content, 1fr);
+    grid-template-columns: minmax(240px, 1fr) auto minmax(0, 1fr);
     align-items: center;
     gap: calc(12 * var(--u));
     height: calc(52 * var(--u));
@@ -2405,6 +2407,13 @@ const mapDataSourceCredits = computed(() => getDataSourceCredits())
     gap: calc(4 * var(--u));
     justify-content: center;
     position: relative;
+    /* 票 102(维护者:菜单栏要在「居中偏左」,在当前位置上略微左移,不与背景装饰线条重合):
+       整组平移,不写回列宽 —— 位移只影响这一格自己,不重新参与 grid 分配,
+       所以选中态切换、读数变化都改不了按钮位置。
+       用 transform 而不是 margin:margin 会把中列让出的空位再分给左右两列,
+       等于把「谁推菜单」的裁决权交还给内容;translateX 是纯视觉偏移,几何仍居中。
+       32u 是按 1920×1080(--u=1px 档)实测「略微左移」的幅度,与顶栏其余尺寸同一套 --u。 */
+    transform: translateX(calc(-32 * var(--u)));
 }
 
 .cockpit-shell__mode {
@@ -2478,7 +2487,20 @@ const mapDataSourceCredits = computed(() => getDataSourceCredits())
     white-space: nowrap;
 }
 
+/* 票 102(维护者:时间字体长短不平均,秒针一跳按钮就跟着左右挪):
+   时钟改为**固定宽度的数字槽位**,槽位内右对齐 + 等宽数字(tabular-nums + tnum 兜底)。
+   宽度按 2384×1160 截图实测:`--u` 在该视口 ≈1.433px(0.1235vh),一次读数 `21:22:22`
+   的字形跨度 145px ≈ 101u,取 128u(+27% 余量)覆盖「把 1 换成 8」的最宽读数与字体回退
+   (Rajdhani → Bahnschrift → Segoe,等宽数字不被支持时)—— 槽位只要不吃到内容宽度,
+   宽度就恒由这条声明决定:秒针跳动只改槽位内的字形,不再改动任何布局宽度。
+   flex: 0 0 auto —— 槽位不参与挤压,右列内容再多也不会压缩它。 */
 .cockpit-shell__clock {
+    display: inline-block;
+    flex: 0 0 auto;
+    min-width: calc(128 * var(--u));
+    text-align: right;
+    font-variant-numeric: tabular-nums;
+    font-feature-settings: 'tnum' 1;
     font-family: var(--num);
     font-size: var(--fs-body, 14px);
     letter-spacing: 0.06em;

+ 44 - 0
verify/t102-topbar-layout.mjs

@@ -0,0 +1,44 @@
+/**
+ * 票 102 静态闸门(二):一级菜单的位置不得由右侧内容决定。
+ *
+ * 维护者 2026-11-02:「按钮在居中偏左的位置(略微左移,不与背景装饰线条重合);
+ * 目前由于时间字体的长短不平均,时间跳动会导致按钮位置左右移动,需避免。」
+ *
+ * 接缝同样是外壳源码契约。这里锁的是**位置的决定权归谁**:
+ * 三条声明分别关掉三个能让菜单移动的入口 —— 列宽随内容、菜单参与再分配、槽位吃内容宽度。
+ * 可传源码路径复核历史版本(提交前的暂存副本必须是红的)。
+ */
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+
+const source = process.argv[2]
+    ? fs.readFileSync(process.argv[2], 'utf8')
+    : fs.readFileSync(new URL('../src/modules/cockpit/CockpitShell.vue', import.meta.url), 'utf8')
+
+// 入口一:右列按内容最小宽度兜底 = 内容改写列宽 = 中列被推着走。
+assert.ok(
+    source.includes('grid-template-columns: minmax(240px, 1fr) auto minmax(0, 1fr);'),
+    '右列必须吃等分,不能再按内容撑宽'
+)
+assert.ok(!source.includes('minmax(min-content, 1fr)'), '按内容撑宽的右列必须撤掉')
+
+// 入口二:整组平移只影响这一格的视觉位置,不重新参与 grid 分配,
+// 所以位置不再有任何「随内容变化」的来源。居中偏左、并避开底边光路与两侧装饰线。
+assert.ok(source.includes('transform: translateX(calc(-32 * var(--u)));'), '一级菜单必须固定居中偏左')
+assert.ok(!/cockpit-shell__modes\s*\{[^}]*margin-left/.test(source), '位移不得用 margin(会被再分配)')
+
+// 入口三:时钟读数宽度必须恒由声明决定,而不是由「这一刻是几点几分几秒」决定。
+// 槽位宽度须大于最宽的一次读数(2384×1160 实测一次读数占 101u,--u≈1.433px)。
+assert.ok(source.includes('display: inline-block'), '时钟槽位要能吃到 min-width')
+assert.ok(source.includes('flex: 0 0 auto'), '时钟槽位不参与挤压')
+assert.ok(source.includes('min-width: calc(128 * var(--u));'), '时钟需要固定宽度的数字槽位')
+assert.ok(source.includes('text-align: right'), '时钟槽位内右对齐')
+assert.ok(source.includes('font-variant-numeric: tabular-nums'), '时钟数字必须等宽')
+
+// 位置改动只许动位置:导航语义与按钮本身不变。
+assert.ok(source.includes('aria-label="一级模式"'), '一级模式导航保持原样')
+assert.ok(source.includes('v-for="item in MODES"'), '一级模式导航保持原样')
+assert.ok(source.includes('cockpit-shell__home'), '归位按钮保持原样')
+assert.ok(source.includes('cockpit-shell__clock'), '时钟保持原样')
+
+console.log('t102-topbar-layout: 14/14')