Browse Source

fix(cockpit): 顶栏右侧留白回调到 15%(票103)

维护者 2026-11-02:「右侧留空回调到 15%」。菜单仍保持上一版定稿的 5% 左移。

同时修正一处我上一版引入的错误:状态栏「不得压住菜单」那条约束的常数,
代数重推后应为 510u 而不是 324u ——
  菜单右缘 mR = 0.5W + 186u + 位移(W 为页宽、位移为负)
  状态左缘 sl = W − 12u − inset − 300u
  要求 sl ≥ mR + 12u
  ⇒ inset ≤ 0.5W − 510u − 位移  (510 = 12内边距 + 300状态栏 + 186半菜单宽 + 12间距)
上一版写成 324u,上界偏大 186u,在 10% 留白下恰好不显现;留白加到 15% 后立刻暴露:
1152×720 处状态栏压住菜单 13px。改正后该处由约束②收敛到 inset=135.4u,
状态栏停在菜单右侧 12u(留白 12.8%,不再重叠)。

闸门 verify/t103-topbar-shift.mjs 已随值同步:55/55。它在 1920×1080 等验收档位
要求留白恰好 15.00%,在可行下限 1152×720 要求收敛为更小值且不重叠。
变异验证(改回 10vw / 常数改回 324u / 去掉出界兜底 / 菜单改回 10vw)全部转红。
按 AGENTS.md 未跑 build/dev/浏览器。
ChenJZ 16 hours ago
parent
commit
f9f234a63b
3 changed files with 36 additions and 31 deletions
  1. 1 1
      docs/cockpit-deployment.md
  2. 19 14
      src/modules/cockpit/CockpitShell.vue
  3. 16 16
      verify/t103-topbar-shift.mjs

+ 1 - 1
docs/cockpit-deployment.md

@@ -517,7 +517,7 @@ location /gis/model/ {
 | 配置生效 | 网络面板里 `cockpit/config.json` 返回 200,且页面按它取值(改 `map.home` 后归位落点跟着变) | `config.json` 路径、`__COCKPIT_CONFIG_URL__` |
 | 数据源 | `dataSource: real` 时面板数据来自 `/api/cockpit/v1/**`(网络面板可见),mock 时没有这些请求 | `config.json` 的 `dataSource`、`?dataSource=` 覆盖 |
 | 顶栏菜单稳定 | 一级模式按钮固定「居中偏左」(在票 102 基础上再左移整页宽 5%);盯 10 秒,秒针跳动时按钮**不左右移动** | 票 102/103 闸门 `verify/t102-topbar-position.mjs`、`verify/t103-topbar-shift.mjs`;源码看 `.cockpit-shell__modes` 的 translateX 与 `.cockpit-shell__clock` 数字槽位 |
-| 顶栏状态栏位置 | 「归位 / 返回全球 / 时钟」整栏左移,右缘停在 90% 处(右侧留 10% 空白);窄于 ~1152u 时自动收敛,不压菜单也不出界 | 票 103:`--topbar-status-inset` 的三条 min() 约束 |
+| 顶栏状态栏位置 | 「归位 / 返回全球 / 时钟」整栏左移,右缘停在 85% 处(右侧留 15% 空白);较窄视口自动收敛,不压菜单也不出界 | 票 103:`--topbar-status-inset` 的三条 min() 约束 |
 | 全局底图 | 地球有影像(不是黑球) | `map.basemap.source` |
 | 地形 | 浮标含「地形:dem」;拉近后有山体起伏 | nginx `/gis/dem/`、`layer.json` 能直接下载 |
 | 本地影像 | 压到目标区域能看到精细影像 | overlay 的 `url`/`{reverseY}`/`bounds` |

+ 19 - 14
src/modules/cockpit/CockpitShell.vue

@@ -2330,8 +2330,8 @@ const mapDataSourceCredits = computed(() => getDataSourceCredits())
     height: calc(52 * var(--u));
     /* 票 103(维护者 2026-11-02):
        ① 一级菜单在票 102 的 -32u 基础上**再左移整页宽的 5%**(5vw,按页面宽度算);
-       ② 「归位 / 返回全球 / 时钟」整栏左移,**右侧留出整页宽的 10% 空白**。
-       (2026-11-02 二次反馈:右侧留白由 25% 压到 10%,菜单位移由 10% 回调到 5%。)
+       ② 「归位 / 返回全球 / 时钟」整栏左移,**右侧留出整页宽的 15% 空白**。
+       (2026-11-02 三轮反馈:留白 25% → 10% → 定稿 15%;菜单位移 10% → 定稿 5%。)
        两个位移会互相牵制:菜单往左跑的同时状态栏也在往左跑,窄屏上就会撞在一起
        (票 37 记录的「归位压住预警处置」正是这个形状)。所以两条位移都由这里
        统一声明,并各自带**收敛条件**——宁可少移,也不许两栏重叠:
@@ -2339,25 +2339,30 @@ const mapDataSourceCredits = computed(() => getDataSourceCredits())
        --topbar-nav-shift:取「-32u - 5vw」,但当它会让菜单左缘撞进品牌标题文字带时,
        收敛到标题右缘。506.6u = 标题图起点 6u + 404u×92%(图内最右不透明列 90.4%,
        再留 2u 余量)+ 顶栏 12u 列间距;0.5*w 是菜单几何中心。
-       --topbar-status-inset:状态栏右缘离页面右缘的距离,取 10vw(再让 12u 内边距),
+       --topbar-status-inset:状态栏右缘离页面右缘的距离,取 15vw(再让 12u 内边距),
        但当它会让状态栏左缘压到菜单右缘时,收敛到「菜单右缘 + 12u 间距」。 */
     --topbar-nav-shift: max(
         calc(-5vw - 32 * var(--u)),
         calc(506.6 * var(--u) - 50vw)
     );
-    /* 状态栏内缩的三条约束,取其中**最小**的一条(min() 天然就是「谁最紧听谁」):
-       ① 10vw - 12u:要求值的常态,右缘停在 90% 处;
-       ② 50vw - 324u - 位移:不得压住菜单。推导:状态右缘 sr = W-12u-inset,要求
-          sr-300u ≥ 菜单右缘+12u,而菜单右缘 = 0.5W+186u+位移,
-          解得 inset ≤ 0.5W - 324u - 位移(12+300+12 内边距与间距,再减 186u 半宽)。
-          常数若写成 510u(多算一个 186u 半宽),窄屏会得到负值、把状态栏推出页面右缘
-          (800×600 实测右缘被钉在 1005u、超出 205px 被 overflow:hidden 裁掉)。
+    /* 状态栏内缩的三条约束,取其中**最小**的一条(min() 天然就是「谁最紧听谁」)。
+       注意 inset 语义:inset 越大,状态栏越靠左。所以「不压菜单」给出的是 inset 的
+       **上界**,正好对应 min() 里取更小的那个:
+       ① 15vw - 12u:要求值的常态,右缘停在 85% 处;
+       ② 50vw - 510u - 位移:不得压住菜单。代数推导(全部换成 u 单位):
+          菜单右缘 mR = 0.5W + 186u + 位移(W 为页宽、位移为负)
+          状态右缘 sr = W - 12u - inset,状态左缘 sl = sr - 300u
+          要求 sl ≥ mR + 12u
+          ⇒ W - 12u - inset - 300u ≥ 0.5W + 186u + 位移 + 12u
+          ⇒ inset ≤ 0.5W - 510u - 位移    (510 = 12 内边距 + 300 状态栏 + 186 半菜单宽 + 12 间距)
+          1152×720 校验:上界 = 135.4u,15% 要 160.8u,min 取 135.4u ⇒ 状态栏停在菜单右侧
+          12u 处(而非压上去 13px)。
        ③ 50vw - 180u:不得超出页面右缘(= 半页宽减内边距),窄屏兜底。
-       三条取 min 之后,「不压菜单」与「不出界」都不会被违反,代价只是极窄屏上
-       右侧留白达不到 10% —— 两条要求冲突时以不重叠、不出界为先。 */
+       三条取 min 之后,「不压菜单」与「不出界」都不会被违反,代价只是较窄的视口上
+       右侧留白达不到 15% —— 两条要求冲突时以不重叠、不出界为先。 */
     --topbar-status-inset: min(
-        calc(10vw - 12 * var(--u)),
-        calc(50vw - 324 * var(--u) - var(--topbar-nav-shift)),
+        calc(15vw - 12 * var(--u)),
+        calc(50vw - 510 * var(--u) - var(--topbar-nav-shift)),
         calc(50vw - 180 * var(--u))
     );
     /* 票 17 第八轮 R2:基线(标题光带最低线 = path 最低线,即顶栏底)之上留 10u 保护区——

+ 16 - 16
verify/t103-topbar-shift.mjs

@@ -1,10 +1,10 @@
 /**
- * 票 103 静态闸门:菜单再左移整页宽的 5%,状态栏整体左移、右侧留出整页宽的 10% 空白。
+ * 票 103 静态闸门:菜单再左移整页宽的 5%,状态栏整体左移、右侧留出整页宽的 15% 空白。
  *
  * 维护者 2026-11-02:
  *   1. 菜单按钮再略微向左移动(按整体页面宽度计算),2026-11-02 二次反馈定稿 5%;
- *   2. 「归位 / 返回全球 / 时间」这一栏整体向左移动,右侧留出空隙,定稿 10%
- *      (初版 25% 被维护者判定「留空太多」,压到 10%)。
+ *   2. 「归位 / 返回全球 / 时间」这一栏整体向左移动,右侧留出空隙,定稿 15%
+ *      (维护者三轮反馈:25% → 10% → 定稿 15%)。
  *
  * 这两条会互相牵制:菜单右缘左移的同时状态栏也在左移,窄屏上会撞在一起
  * (票 37 记录过的「归位压住预警处置」就是这个形状)。所以本闸门不只查字面量:
@@ -39,16 +39,16 @@ check(/position:\s*absolute/.test(meta), '状态栏须脱离 grid 居中流程
 check(/right:\s*var\(--topbar-status-inset\)/.test(meta), '状态栏右缘须引用 --topbar-status-inset')
 check(/top:\s*calc\(/.test(meta) && /translateY\(-50%\)/.test(meta), '绝对定位后仍须与按钮行同水平居中')
 
-// 10vw 与三条收敛条件都在顶栏那个变量里声明(单一来源),此处对变量本体验。
+// 15vw 与三条收敛条件都在顶栏那个变量里声明(单一来源),此处对变量本体验。
 check(/--topbar-status-inset:\s*min\(/.test(topbar), '状态栏内缩须用 min() 收敛(窄屏不得压住菜单)')
-check(/--topbar-status-inset:\s*min\([^;]*10vw/.test(topbar), '状态栏右侧须留整页宽的 10% 空白(10vw)')
-check(!/--topbar-status-inset:\s*min\([^;]*25vw/.test(topbar), '状态栏留白不得仍是 25vw(维护者已压到 10%)')
+check(/--topbar-status-inset:\s*min\([^;]*15vw/.test(topbar), '状态栏右侧须留整页宽的 15% 空白(15vw)')
+check(!/--topbar-status-inset:\s*min\([^;]*25vw/.test(topbar), '状态栏留白不得仍是 25vw(定稿 15%)')
 check(/--topbar-status-inset:\s*min\([^;]*var\(--topbar-nav-shift\)/.test(topbar), '状态栏收敛条件必须与菜单位移同源')
-// 约束②的常数必须与几何推导一致(324u = 12u内边距 + 300u状态栏 + 12u间距 + 186u半菜单宽)。
-// 写成 510u 在可行视口上恰好不改变结果(10% 留白总是最紧的那条),但它是错的常数:
-// 一旦 10vw 那条被调大,错误常数就会提前触发、把状态栏推出页面。所以直接锁常数。
-check(/--topbar-status-inset:\s*min\([^;]*calc\(50vw - 324\s*\*\s*var\(--u\) - var\(--topbar-nav-shift\)\)/.test(topbar), '「不得压菜单」的常数须为几何推导值 324u(12+300+12-186)')
-check(!/--topbar-status-inset:\s*min\([^;]*calc\(50vw - 510/.test(topbar), '不得用 510u 这个错常数(多算一个半菜单宽)')
+// 约束②的常数必须与代数推导一致:510u = 12u 内边距 + 300u 状态栏 + 186u 半菜单宽 + 12u 间距。
+// 写成 324u(漏算半菜单宽)会让上界偏大,窄屏挡不住重叠:
+// 15% 在 1152×720 会压到菜单 13px,此时必须由 510u 这条把 inset 收敛到 135.4u。
+check(/--topbar-status-inset:\s*min\([^;]*calc\(50vw - 510\s*\*\s*var\(--u\) - var\(--topbar-nav-shift\)\)/.test(topbar), '「不得压菜单」的常数须为代数推导值 510u(12+300+186+12)')
+check(!/--topbar-status-inset:\s*min\([^;]*calc\(50vw - 324/.test(topbar), '不得用 324u 这个错常数(漏算半菜单宽,窄屏挡不住重叠)')
 check(/--topbar-status-inset:\s*min\([^;]*50vw - 180\s*\*\s*var\(--u\)/.test(topbar), '状态栏须有「不得超出页面右缘」的兜底(窄屏不被 overflow 裁掉)')
 
 // ---- 复算:按 CSS 里的同一套公式算几何(单位全换算到 px)----
@@ -65,8 +65,8 @@ const navShift = (w, h) => Math.max(-0.05 * w - 32 * unit(h), 506.6 * unit(h) -
 // 写成 510u 会让窄屏得到负值、把状态栏推到页面外。
 const statusInset = (w, h) =>
     Math.min(
-        0.10 * w - 12 * unit(h),
-        0.5 * w - 324 * unit(h) - navShift(w, h),
+        0.15 * w - 12 * unit(h),
+        0.5 * w - 510 * unit(h) - navShift(w, h),
         0.5 * w - 180 * unit(h)
     )
 function geometry(w, h) {
@@ -100,14 +100,14 @@ check(
     '1920×1080:菜单须恰好再左移整页宽的 5%(叠加票 102 的 -32u)'
 )
 check(
-    Math.abs(target.blank - 0.10) < 0.002,
-    `1920×1080:状态栏右缘须恰好停在 90vw,实测留白 ${(target.blank * 100).toFixed(1)}%`
+    Math.abs(target.blank - 0.15) < 0.002,
+    `1920×1080:状态栏右缘须恰好停在 85vw,实测留白 ${(target.blank * 100).toFixed(1)}%`
 )
 
 // 窄屏:两条要求冲突时必须收敛(宁可少移,也不许两栏重叠、不许出界)。
 // 1152×720 是可行下限:此处菜单已被标题顶住(位移收敛到标题右缘),留白也收敛。
 const narrow = geometry(1152, 720)
-check(narrow.blank >= 0.10, '1152×720:留白按收敛值执行(不小于 10%,同时不越界)')
+check(narrow.blank > 0 && narrow.blank < 0.15, '1152×720:15% 会压到菜单 13px,须由「不压菜单」那条约束收敛成更小的留白')
 check(narrow.statusLeft >= narrow.navRight + GAP * narrow.u - 0.5, '1152×720:收敛后仍不得重叠')
 check(narrow.statusRight <= 1152 + 0.5, '1152×720:收敛后仍不得出界')