跨端: 全屏是**窗口级**的 —— 光给 MainPage 让位,等于把黑边换成顶栏压字
上一提交(cac026e)把黑边消掉了,但**只给 `MainPage` 加了避让**。
实测截图硬证:写邮件页的「取消」与时钟「09:49」重叠、「发送」与 wifi/电量图标重叠。
## 根因:`setWindowLayoutFullScreen(true)` 不只作用于当前页
它是**窗口级**的:一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页
(写邮件/会话/收件箱/邮件详情/用户管理)都从 y=0 开始画。
所以上次那个错与更早那次(6861934 只删全屏不留避让)**同源**:
都是"同一件事只做了一半"。上次少的是**步骤**,这次少的是**页面**。
## 改法:把"消费避让"变成每个 @Entry 页都得做的事
- `model/WindowInsets.ts` 加 `topInset(insets)`:取 0 时(未全屏/取不到)
表达式的值与旧代码**逐字相同** ⇒ 没全屏的环境行为不变,不会把谁顶下去。
- 五个页各按自己的形状让位:
- 固定 56vp 顶栏(写邮件/会话/收件箱/用户管理):
`height(56 + topInset(...))` **与** `padding(… top: topInset(...))` 一起加 ——
只加 padding 会把固定的 56 切掉 39(按钮压扁),只加 height 则内容仍贴 y=0。
- 满高容器(邮件详情):`padding({ top })` 加在 `@Entry` 包装层。
★ **不能加在 `MailDetailView` 里面**:它同时被 `MainPage` 的 Navigation 内嵌复用,
而那层已经让过位了 —— 加在里面就变成让两次(39vp 变 78vp)。
这类"同一组件两种入口"的坑与"悬浮加号要放在 Navigation 内部"同源:
**让位的量取决于它被挂在哪一层**。
## 判据(harmony-window 8 → 9 条)
接线⑤ 枚举**所有** `@Entry` 页并要求它们消费避让 —— 口径是
"有人在窗口上开了全屏 ⇒ 每个 @Entry 页都得让",而不是"检查 MainPage 做了没有"。
后者在新增一个推上来的页时会静默逃掉,而"新增一个页"正是最常发生的事。
豁免要带**可机器复核**的理由(不再是"这个页先不管"):
- `Index.ets`:DevEco 模板欢迎页,不在 `main_pages.json` 流程里;
- `LoginPage.ets`:根容器 `.align(Alignment.Center)` ⇒ 结构上碰不到 y=0。
但"居中"是可能被改掉的性质 ⇒ 判据**断言那个居中写法仍然存在**,
谁把它改成贴顶,这条先红,逼他回来重新想这个页要不要避让。
**变异自检两个方向都跑过**:
- 把 ComposePage 避让整个拿掉(重演"只给 MainPage 加")⇒ 红 ✓
- 只加 padding 不加 height(会压扁按钮)⇒ 红 ✓
★ 期间还修掉两处"判据锚在当时的字符串上"(不是放宽,是它把"加一个正当的避让"
与"真犯那个错"判得一模一样):
- `harmony-nav` ④ 的留白断言、`harmony-widescreen` ④ 的 navReserve 断言,
都改成剥注释后验**形状与不变量**,而不是写死字面表达式。改完变异仍咬得住。
This commit is contained in:
@ -30,6 +30,7 @@
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { prose } from './lib/read.mjs';
|
||||
@ -165,6 +166,118 @@ test('★ 接线④:`.ets` 不许 import `WindowInsets` 的 SDK 类型(判
|
||||
'WindowInsets.ts 必须保持纯净(无 SDK 依赖),否则判据连编都编不过');
|
||||
});
|
||||
|
||||
test('★ 接线⑤:**每一个 `@Entry` 页**都要消费避让(全屏是窗口级的,不止 MainPage)', () => {
|
||||
/*
|
||||
* ── 这条为什么是全文件第二重要(2026-09-18 实测踩到)──
|
||||
*
|
||||
* `setWindowLayoutFullScreen(true)` 是**窗口级**的:一旦设上,窗口里**所有**
|
||||
* 用 `router.pushUrl` 推上来的页都从 y=0 开始画,而不只是 `MainPage`。
|
||||
*
|
||||
* 我第一版只给 `MainPage` 加了避让 ⇒ 写邮件页实拍:「取消」与时钟「09:49」重叠、
|
||||
* 「发送」与 wifi/电量图标重叠。**把黑边换成了另一种坏** —— 而且两个错同源:
|
||||
* 都是"同一件事只做了一半"(上次少了避让,这次少了页面)。
|
||||
*
|
||||
* ★ 所以口径是「**有人在窗口上开了全屏 ⇒ 每个 `@Entry` 页都得让**」,
|
||||
* 而不是「检查 MainPage 做了没有」。后者下新增一个推上来的页会静默逃掉 ——
|
||||
* 而"新增一个页"正是最常发生的事。
|
||||
*/
|
||||
const pageDir = join(HARMONY, 'entry', 'src', 'main', 'ets', 'pages');
|
||||
const entries = readdirSync(pageDir)
|
||||
.filter(f => f.endsWith('.ets'))
|
||||
.filter(f => /^@Entry\b/m.test(prose(join(pageDir, f))));
|
||||
/*
|
||||
* 自检:扫到的 `@Entry` 页数要 ≥ 5(`main_pages.json` 里登记 7 个)。
|
||||
* 防的是"目录改名/遍历写错 ⇒ 这条变成空判据而依然全绿"——
|
||||
* 与 `harmony-system-api` 里那条"扫到的文件数 ≥ N"同形。
|
||||
*/
|
||||
assert.ok(entries.length >= 5, `要扫到全部 @Entry 页(实际 ${entries.length}:${entries.join('、')})`);
|
||||
|
||||
/*
|
||||
* 两种合法的"消费"写法:
|
||||
* · `topInset(this.windowInsets)` —— 直白的(顶栏页用这个);
|
||||
* · 直接读 `this.windowInsets.statusBar` —— MainPage 要的,它得把避让嵌进
|
||||
* `isWide ? paneGap : statusBar` 这个条件表达式里,套不进 `topInset()`。
|
||||
* 两者都算"把值用起来了";形状不同而要求相同(与 `harmony-nav` 那条同口径:
|
||||
* 判的是"读到的东西有没有落到布局上",不是"用的哪个函数")。
|
||||
*/
|
||||
const consumes = (src) => /topInset\(\s*this\.windowInsets\s*\)/.test(src)
|
||||
|| /this\.windowInsets\.statusBar/.test(src);
|
||||
|
||||
/*
|
||||
* 豁免名单:每条都必须是**可机器复核的理由**,不是"这个页先不管"。
|
||||
*
|
||||
* `LoginPage` 的根容器是 `.align(Alignment.Center)` 的满高容器 —— 内容是**居中**的,
|
||||
* 最顶上的那个像素也远在状态栏之下,结构上碰不到 y=0。所以它不需要避让。
|
||||
*
|
||||
* ★ 但"居中"是个可能被改掉的性质(哪天有人给它加个贴顶的标题栏,理由就没了)。
|
||||
* 所以这里不只看一次理由字符串 —— 下面**断言那个居中写法仍然存在**:
|
||||
* 谁把它改成贴顶,这条先红,逼他回来重新想这个页要不要避让。
|
||||
* 这正是本仓那条"豁免得带可复核的理由"(`harmony-system-api` 的 ALLOW 同形)。
|
||||
*/
|
||||
const EXEMPT = new Map([
|
||||
['Index.ets', { why: 'DevEco 模板欢迎页,不在 main_pages.json 的流程里,从不被路由到', check: null }],
|
||||
['LoginPage.ets', {
|
||||
why: '根容器 .align(Alignment.Center):内容居中,结构上碰不到 y=0',
|
||||
check: (src) => /\.align\(Alignment\.Center\)/.test(stripped(src))
|
||||
&& !/Alignment\.Top/.test(stripped(src))
|
||||
}]
|
||||
]);
|
||||
|
||||
const missing = [];
|
||||
for (const f of entries) {
|
||||
const src = prose(join(pageDir, f));
|
||||
const exempt = EXEMPT.get(f);
|
||||
if (exempt) {
|
||||
if (exempt.check) {
|
||||
assert.ok(exempt.check(src),
|
||||
`${f} 被豁免的理由已不存在(原理由:${exempt.why})—— ` +
|
||||
'要么恢复那个形状,要么把它当普通页消费避让,不要默默留着豁免');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!consumes(src)) missing.push(f);
|
||||
}
|
||||
assert.deepEqual(missing, [],
|
||||
`这些 @Entry 页没有消费窗口避让:${missing.join('、')} —— ` +
|
||||
'全屏是窗口级的,漏一个页就是一处「顶栏被时钟压住」(实测过:写邮件页的取消/发送压着 09:49 与电量)');
|
||||
|
||||
/*
|
||||
* `\u2605` 顶栏那对"高度 + padding-top"只对**固定高度**的顶栏成立(`height(56)`)。
|
||||
*
|
||||
* `MailDetailPage` 是另一种形状:它的 `@Entry` 包装层是 `height('100%')` 的**满高列**,
|
||||
* 避让用 `padding({ top: … })` 加在满高容器上(内容自然往下,没有"从 56 里切"的问题)——
|
||||
* 这是满高容器的正确写法,不该被要求写成 `56 + topInset(...)`。
|
||||
*
|
||||
* 所以这里只对**真的写了固定 height(56) 顶栏**的页判那对同现,
|
||||
* 其余页(满高容器)只要求"避让落到了布局上"(上面已经判过)。
|
||||
*/
|
||||
const halfDone = [];
|
||||
for (const f of entries) {
|
||||
if (EXEMPT.has(f)) continue;
|
||||
const code_ = stripped(prose(join(pageDir, f)));
|
||||
if (!/topInset\(\s*this\.windowInsets\s*\)/.test(code_)) continue; // 不是顶栏形状(MainPage)
|
||||
/*
|
||||
* 只看**同一个修饰符链**:包含 `padding(top: topInset(...))` 的那一段往前找它的 `.height(...)`。
|
||||
*
|
||||
* 不能满文件搜 `height(56)` —— `MailDetailPage` 里的悬浮回复球是 `.width(56).height(56)`,
|
||||
* 与顶栏毫无关系(我第一版就是这么误报的:把一个 56×56 的圆球当成了"固定 56 顶栏")。
|
||||
* 判据要锚在"这一段的形状"上,而不是"文件里有没有出现过这个数"。
|
||||
*/
|
||||
const pad = /\.padding\(\{[^}]*top:\s*topInset\(\s*this\.windowInsets\s*\)[^}]*\}\)/.exec(code_);
|
||||
if (!pad) continue;
|
||||
const chain = code_.slice(Math.max(0, pad.index - 300), pad.index + pad[0].length);
|
||||
const hasFixed56 = /\.height\(\s*56\s*\)/.test(chain);
|
||||
if (!hasFixed56) continue; // 满高容器形状(MailDetailPage):没有从 56 里切的问题
|
||||
const heightGrew = /\.height\(56 \+ topInset\(\s*this\.windowInsets\s*\)\)/.test(chain);
|
||||
if (!heightGrew) {
|
||||
halfDone.push(f);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(halfDone, [],
|
||||
`这些页的顶栏没把"高度 + padding-top"一起加:${halfDone.join('、')} —— ` +
|
||||
'只加一个会把固定 56vp 切掉一截(按钮压扁)或让位没生效(内容仍贴 y=0)');
|
||||
});
|
||||
|
||||
/* ───────────────────────── ③ 设备行为(有设备才跑)───────────────────────── */
|
||||
|
||||
/*
|
||||
|
||||
@ -82,7 +82,7 @@ const SUITE = [
|
||||
// `setWindowLayoutFullScreen(true)`(消黑边)+ `getWindowAvoidArea`(让开时钟/手势条)。
|
||||
// 只做前半 ⇒ 页签被时钟盖住(上一次就是这样退回去的,黑边于是留了三天);
|
||||
// 只做后半 ⇒ 黑边照旧。**两半缺哪一半都要判红**,所以变异自检两个方向都跑过。
|
||||
['test/harmony-window.test.mjs', ['--experimental-strip-types', '--no-warnings'], 8],
|
||||
['test/harmony-window.test.mjs', ['--experimental-strip-types', '--no-warnings'], 9],
|
||||
// 外观契约:默认值去 Go 源码里读(服务端 DefaultAppearance 是权威)+ 缓存键按账号
|
||||
['test/appearance-defaults.test.mjs', [], 4],
|
||||
['test/build-stamp.test.mjs', [], 7],
|
||||
|
||||
Reference in New Issue
Block a user