fix(harmony): 底栏黑带 / 联系人点不开 / 我的页不守导航 / 顶栏硬截断
用户(2026-09-17)连报五条:「一点动画都没有」「底部那个黑条是啥意思」
「日历页面、我的页面、联系人页面哪个跟 webui 对齐了」「联系人页面连点都点不开」
「你的顶栏为什么还是硬截断而不是渐变」。逐条查证后修:
── ① 底部黑条 = 系统导航栏区域,不是我们画的 ──
根因:窗口默认给系统导航条留位,那块在深色下是黑的;界面于是看起来底下多一条黑带。
修法:`setWindowSystemBarEnable(['status'])` 隐藏系统导航条(底部那条本就是我们自绘的
悬浮玻璃条),内容铺满全高。
★ 刻意**不用** `setWindowLayoutFullScreen(true)`:实测试过,它也能消掉黑带,但会连
状态栏区域一起吃进布局,页签栏被时钟/电量盖住(截图硬证「07:43」与「收件箱」重叠)。
── ② 联系人点不开(用户原话「连点都点不开」)──
根因:`ContactItem` / `WorkCard` **根本没有 onClick** —— 卡片画出来了,
但没有任何点击路径。当时判据只钉了"字段与 WebUI 一致",没钉"点了会发生什么"。
修法:点卡片打开那条会话的邮件列表,就地在**本窗格内**展示(新增 `SessionMailsView`
+ `GET /sessions/{id}/mails`),底部导航保留;每封可再点进详情。
── ③ 我的页不遵守导航规则 ──
根因:点底栏「我的」走 `pushUrl('pages/SettingsPage')` 推**独立 @Entry 页** ⇒
底部导航整条消失,要按返回才能再切窗格。WebUI 里 `account` 只是一个 `viewMode`,
与收件箱同级、导航常驻。
修法:`SettingsPage` → `SettingsPane`(去掉 @Entry 与返回键,从 main_pages.json 摘除),
作为**第 4 个内容窗格**挂到 `currentIndex === 3`;`NAV_CONTENT_COUNT` 3 → 4,
`NAV_ITEMS` 第 4 项去掉 `route`;拨动动画包 `getUIContext().animateTo`。
实测:`我的` 页底部导航可见,可滚到「退出登录」「管理」。
── ④ 顶栏硬截断 ──
根因:`Scroll` 与 `List` 都能挂 `fadingEdge`(同在 `ScrollableCommonMethod` 上),
但**只有 MainPage 的 List 挂了** —— MailDetailPage / CalendarPage / SettingsPage /
AdminUsersPage / SessionsPage / InboxPage 的滚动容器全漏。同一次滚动里
列表是渐隐、正文是硬切,看起来就是"顶栏没做渐变"。
修法:13 个容器全部补齐(LoginPage 例外:它的 Scroll 是整页根、不是列表,
写了理由)。实测:视口上沿被切断的文字最暗 170–196,而下方不透明正文是 **19**。
── ⑤ 一点动画都没有 ──
实测确认改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过**。
补:Theme 加令牌(durFast 120 / durBase 180 / easeOutSoft = cubic-bezier(0.22,1,0.36,1),
与 WebUI `--dur-*`/`--ease-out-soft` 同值同曲线);切窗格包 `animateTo`;
导航项选中变色加 `.animation`。
判据(218 passed / 0 failed):
· 新增「渐隐覆盖**所有**页面滚动容器」:按文件枚举 + 计数配平,
并自检扫到的容器总数(防正则写坏 ⇒ 全绿)。**这条一写就抓到 LoginPage**。
· ①/②/⑤ 与 headless 那几条按新事实改写:内容窗格 3→4、
`normalizeNavIndex(3)` 归一到自己、导航项里**禁止**再出现 `pushUrl`
(那正是导航消失的形状)。
· 修掉自己引入的两处判据缺陷:`itemCode` 未定义;以及一条**窗口式断言**
(从内容层 `}` 往后扫,靠数花括号定界,而注释里就有大量 `.padding({`,
切片长到 1700+ 字符一路扫进 `onAreaChange` ⇒ 假红),改为纯负向断言。
未验:真机观感;横屏 Auto Split 双栏;日历页深度对齐(本轮只补了渐隐)。
This commit is contained in:
@ -8,6 +8,7 @@
|
||||
// ③ 命中区:≥44vp —— 从 `NavItems.ts` **导入数值**判,不拿正则去源码里猜;
|
||||
// ④ 悬浮与让位:条是自绘的浮动层(留白 + 圆角 + 系统材质),
|
||||
// 且内容底部让出的高度 ≥ 条高 + 离底留白(否则最后一行压在条底下:看得见、点不到)。
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { code, prose, stripComments } from './lib/read.mjs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@ -64,6 +65,7 @@ test('① 点击:每个导航项点下去落到**它自己**那个 index(配
|
||||
*/
|
||||
const item = builderBody(main, 'NavItem(item: NavItem, index: number) {');
|
||||
assert.ok(item.length > 100, '要能取到导航项的正文');
|
||||
const itemCode = stripComments(item);
|
||||
/*
|
||||
* 点击处理器是 `() => { this.currentIndex = normalizeNavIndex(index); }`。
|
||||
* 取赋值语句并断言它**同时**用到 `index` 与归一化函数 ——
|
||||
@ -71,13 +73,31 @@ test('① 点击:每个导航项点下去落到**它自己**那个 index(配
|
||||
* 所以要求括号里的实参就是 `index`。
|
||||
*/
|
||||
// 只数**赋值**(`= `),不数三元式里的 `===` —— 外壳入口不赋值,所以正文里仍只能有一次赋值
|
||||
const assigns = [...item.matchAll(/(?<!=)this\.currentIndex\s*=\s*([^;=]+);/g)].map(m => m[1].trim());
|
||||
assert.equal(assigns.length, 1, `导航项里应当恰好一次 currentIndex 赋值(实际 ${assigns.length} 次)—— 外壳入口(route 非空)走路由,不赋 currentIndex`);
|
||||
assert.match(assigns[0], /normalizeNavIndex\(\s*index\s*\)/,
|
||||
`点击要落到本项自己的 index 并过归一化,现在赋的是:${assigns[0]}`);
|
||||
// 外壳入口(route 非空)走路由、内容窗格才赋 currentIndex —— 两者都走在本项的 onClick 里
|
||||
assert.match(item, /item\.route.*pushUrl|pushUrl.*item\.route|if \(item\.route[\s\S]{0,80}pushUrl/,
|
||||
'外壳入口(route 非空)要路由到 @Entry 页(与 WideSidebar 的设置按钮同一行为)');
|
||||
/*
|
||||
* ★ 2026-09-17:赋值改成在 `animateTo` 回调里写一个**局部变量** `target`,
|
||||
* 而这个 `target` 由 `normalizeNavIndex(index)` 算出来 —— 仍然要求
|
||||
* "点第 N 项落到第 N 个",只是多了一层变量。
|
||||
* 判据跟着改成:先要求 `target` 出自 `normalizeNavIndex(index)`,
|
||||
* 再要求赋值写的是 `target`(而不是某个写死的下标)。
|
||||
*/
|
||||
assert.match(itemCode, /const\s+target:\s*number\s*=\s*normalizeNavIndex\(\s*index\s*\)/,
|
||||
'点击目标要由本项自己的 index 过归一化算出来(`target = normalizeNavIndex(index)`)');
|
||||
const assigns = [...itemCode.matchAll(/(?<!=)this\.currentIndex\s*=\s*([^;=]+);/g)].map(m => m[1].trim());
|
||||
assert.equal(assigns.length, 1, `导航项里应当恰好一次 currentIndex 赋值(实际 ${assigns.length} 次)`);
|
||||
assert.equal(assigns[0], 'target',
|
||||
`赋值要写算出来的 target(本项自己的 index),现在写的是:${assigns[0]}`);
|
||||
/* 切窗格包在 animateTo 里(否则是硬切 —— 用户 2026-09-17「一点动画都没有」) */
|
||||
assert.match(itemCode, /getUIContext\(\)\.animateTo\(/,
|
||||
'★ 切窗格要包在 `getUIContext().animateTo(...)` 里(全局 animateTo 已废弃,判据会红)');
|
||||
/*
|
||||
* ★ 2026-09-17:**不再**要求"route 非空就 pushUrl"。
|
||||
*
|
||||
* 那条正是"我的页面不遵守导航规则"的源头:点底栏「我的」推一个独立 @Entry 页,
|
||||
* 底部导航整条消失。现在四项都是内容窗格,统一走 currentIndex 分派。
|
||||
* 这里反过来钉:导航项里**不该**再有 pushUrl 分支(否则那个形状会回来)。
|
||||
*/
|
||||
assert.ok(!/pushUrl/.test(itemCode),
|
||||
'★ 导航项里不许再有 pushUrl —— 那会把导航条整个推走(用户 2026-09-17 抱怨的形状)');
|
||||
// ForEach 要真的把 index 传进来(否则上面那句 index 无从谈起)
|
||||
const bar = builderBody(main, 'NavBar() {');
|
||||
assert.match(bar, /ForEach\(NAV_ITEMS,\s*\(item: NavItem,\s*index: number\)/, 'ForEach 要带 index 参数');
|
||||
@ -111,6 +131,12 @@ test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人
|
||||
* 写成"否则就挂联系人"的后果:以后再加一项,新索引会被它静默接住(点了显示联系人)。
|
||||
*/
|
||||
assert.match(root, /else if \(this\.currentIndex === 2\)/, '联系人必须显式绑在 index 2,不许用 else 兜底');
|
||||
/* 第 4 项「我的」也是内容窗格(2026-09-17:原来是 pushUrl 推独立页 ⇒ 导航消失) */
|
||||
assert.match(root, /else if \(this\.currentIndex === 3\)/,
|
||||
'「我的」要显式绑在 index 3(内容窗格),不许用 else 兜底');
|
||||
assert.match(root, /SettingsPane\(\{\s*bgActive: this\.bgActive,\s*navReserve: this\.navReserve\s*\}\)/,
|
||||
'★ index 3 要挂 SettingsPane(**窗格**,不是 pushUrl 出去的 @Entry 页)——' +
|
||||
'用户 2026-09-17:「我的页面完全没有遵守 nav 的导航规则」');
|
||||
/*
|
||||
* 日历(index 1):**常驻挂载 + visible 由索引驱动 + Visibility 开关**,三样缺一不可。
|
||||
* · `visible: this.currentIndex === 1` 是 `@Watch` 的触发源 —— 不带它,today 就不会在
|
||||
@ -124,20 +150,24 @@ test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人
|
||||
assert.ok(!/if \(this\.currentIndex === 1\)/.test(root),
|
||||
'日历不该写成 if/else 的一支:它是常驻 pane(卸载重挂会丢掉"在看哪个月",且 today 只能靠挂载重算)');
|
||||
/*
|
||||
* 分派必须**穷尽内容窗格**(`NAV_CONTENT_COUNT` = 3:通信/日历/联系人),但
|
||||
* **不**把外壳入口(第 4 项「我的」)算进去 —— 它是 `@Entry` 页,点击走路由,
|
||||
* 不在 currentIndex 分派里挂内容。2026-09-14 日历入口上架时这条**如约变红**
|
||||
* 分派必须**穷尽内容窗格**。2026-09-14 日历入口上架时这条**如约变红**
|
||||
* (当时是 `length === 2` + 两支 if/else)—— 加**窗格**必须同时加分派。
|
||||
* 2026-09-17:加了第 4 枚图标「我的」(外壳入口,与 WebUI 四入口对齐),
|
||||
* 所以这里判的是 **NAV_CONTENT_COUNT** 与分派一致,而 NAV_ITEMS 是 4。
|
||||
*
|
||||
* ★ 2026-09-17:内容窗格 3 → **4**(「我的」从外壳入口改成窗格)。
|
||||
* 用户原话:「我的页面完全没有遵守 nav 的导航规则」——
|
||||
* 原来点底栏「我的」是 `pushUrl('pages/SettingsPage')` 推一个独立 @Entry 页,
|
||||
* 底部导航整条消失;而 WebUI 的 `account` 只是一个 `viewMode`,导航常驻。
|
||||
* 所以现在四项**都是内容窗格**,`NAV_ITEMS.length === NAV_CONTENT_COUNT`,
|
||||
* 不再有"外壳入口"这一类。
|
||||
*/
|
||||
assert.equal(N.NAV_CONTENT_COUNT, 3, `内容窗格应为 3(通信/日历/联系人),实际 ${N.NAV_CONTENT_COUNT}`);
|
||||
assert.equal(N.NAV_ITEMS.length, N.NAV_CONTENT_COUNT + 1,
|
||||
`NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项;应为 3 个内容窗格 + 1 个外壳入口(我的)`);
|
||||
// 第 4 项必须是外壳入口(带 route),否则它会被 currentIndex 分派静默漏掉
|
||||
const shell = N.NAV_ITEMS[N.NAV_CONTENT_COUNT];
|
||||
assert.equal(shell.key, 'me', '第 4 项应是「我的」');
|
||||
assert.equal(shell.route, N.NAV_SETTINGS_ROUTE, '「我的」要路由到设置页');
|
||||
assert.equal(N.NAV_CONTENT_COUNT, 4, `内容窗格应为 4(通信/日历/联系人/我的),实际 ${N.NAV_CONTENT_COUNT}`);
|
||||
assert.equal(N.NAV_ITEMS.length, N.NAV_CONTENT_COUNT,
|
||||
`NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项;应与内容窗格数一致(${N.NAV_CONTENT_COUNT})—— 四项都是窗格`);
|
||||
/* 第 4 项「我的」**不得**再带 route:带 route 就会被当外壳入口 push 出去(导航消失) */
|
||||
const me = N.NAV_ITEMS[3];
|
||||
assert.equal(me.key, 'me', '第 4 项应是「我的」');
|
||||
assert.equal(me.route, undefined,
|
||||
'★ 「我的」不许再带 route —— 带 route 就是"点了导航、导航条整个消失"那个形状')
|
||||
// 内容挂在浮动条**下面**(先内容后条),否则条会被内容盖住、点不到
|
||||
const navAt = stack.indexOf('this.NavBar()');
|
||||
const contentAt = stack.indexOf('CommPage({ bgActive: this.bgActive })');
|
||||
@ -304,8 +334,10 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且
|
||||
assert.equal(N.normalizeNavIndex(99), 0);
|
||||
assert.equal(N.normalizeNavIndex(1), 1);
|
||||
assert.equal(N.normalizeNavIndex(2), 2, '第三项(联系人)也必须归一到自己');
|
||||
// 外壳入口(index 3)不归一到自己 —— 它不进 currentIndex 分派,normalize 越界即回 0
|
||||
assert.equal(N.normalizeNavIndex(3), 0, '外壳入口(我的)不该被当作内容窗格下标');
|
||||
/* 第 4 项「我的」是内容窗格 ⇒ index 3 要归一到自己(不再是"越界回 0") */
|
||||
assert.equal(N.normalizeNavIndex(3), 3,
|
||||
'「我的」是内容窗格(2026-09-17),index 3 要归一到自己 —— 否则点它会被静默踢回收件箱');
|
||||
assert.equal(N.normalizeNavIndex(4), 0, '越界(4)才回 0');
|
||||
assert.throws(() => N.navKeyAt(1.5) === undefined, undefined, '非整数下标不该悄悄生效');
|
||||
});
|
||||
|
||||
@ -565,6 +597,59 @@ test('★ 滚动列表上下边缘渐隐:所有列表容器都挂 fadingEdge
|
||||
'MainPage 要从 @kit.ArkUI import LengthMetrics');
|
||||
});
|
||||
|
||||
/*
|
||||
* ★ 渐隐必须覆盖**所有**滚动容器,不只是 MainPage 那一批。
|
||||
*
|
||||
* 用户(2026-09-17):「你的顶栏为什么还是硬截断而不是渐变?」
|
||||
*
|
||||
* 根因正是上一条判据的**扫描范围太窄**:它只看 `pages/MainPage.ets`,
|
||||
* 于是 `MailDetailPage` / `CalendarPage` / `SettingsPage` / `AdminUsersPage` /
|
||||
* `SessionsPage` / `InboxPage` 里的滚动容器**一个都没被覆盖** ——
|
||||
* 那些页面滚起来就是硬切,而判据全绿。
|
||||
* 这跟本仓库反复记的"窗口式/范围式判据"是同一个病:
|
||||
* 判据自己划的圈,正好把出问题的那块划在外面。
|
||||
*
|
||||
* 现在改成**按文件枚举 + 计数配平**:每个页面里 `Scroll()` / `List(` / `Scroll(`
|
||||
* 的个数,必须等于该文件里 `.fadingEdge(` 的个数。少一个就红,
|
||||
* 而且报错会指名是哪个文件少了几处。
|
||||
*/
|
||||
test('★ 上下渐隐覆盖**所有**页面的滚动容器(不是只有 MainPage)', () => {
|
||||
const pages = readdirSync(join(HARMONY_ETS, 'pages')).filter(f => f.endsWith('.ets'));
|
||||
assert.ok(pages.length >= 8, `pages/ 下应有 ≥8 个页面,实际 ${pages.length}(扫描范围别退化)`);
|
||||
|
||||
/*
|
||||
* 例外:`LoginPage` 的 `Scroll` 是**整页根节点**(表单内容竖直排布),
|
||||
* 不是"一列卡片"—— 它的上下就是屏幕边,没有任何内容从它下面穿过,
|
||||
* 所以渐隐在这里只会把品牌卡与首个输入框洗白,没有任何遮挡要交代。
|
||||
* 这与 WebUI 只给 `.overflow-y-auto`(列表容器)加 mask、不给页面根加是同一条判断。
|
||||
* **例外必须写明理由**(本仓库纪律),且只有这一个。
|
||||
*/
|
||||
const EXEMPT = new Set(['LoginPage.ets']);
|
||||
|
||||
const offenders = [];
|
||||
let totalContainers = 0;
|
||||
for (const f of pages) {
|
||||
if (EXEMPT.has(f)) continue;
|
||||
const src = code(join(HARMONY_ETS, 'pages', f));
|
||||
const containers = (src.match(/\b(?:Scroll\(\)|(?:Scroll|List)\(\{)/g) ?? []).length;
|
||||
const fades = (src.match(/\.fadingEdge\(/g) ?? []).length;
|
||||
totalContainers += containers;
|
||||
if (fades < containers) {
|
||||
offenders.push(`${f}(容器 ${containers} / 渐隐 ${fades})`);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 自检:扫到的容器总数要合理。写死一个下限是为了防"正则写坏 ⇒ 一个都匹配不到 ⇒
|
||||
* 每个文件都是 0/0 ⇒ 判据全绿"。当前仓库是 13 个。
|
||||
*/
|
||||
assert.ok(totalContainers >= 10,
|
||||
`全仓滚动容器应 ≥10 个,实际扫到 ${totalContainers} —— 正则或目录范围退化了`);
|
||||
assert.deepEqual(offenders, [],
|
||||
`这些页面的滚动容器缺 fadingEdge:${offenders.join('、')}\n` +
|
||||
'现象就是"顶栏/底边硬截断而不是渐变"(用户 2026-09-17 原话)。' +
|
||||
'每个 `Scroll()` / `List(...)` 都要挂 `.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })`。');
|
||||
});
|
||||
|
||||
/*
|
||||
* ★ 悬浮加号必须待在 Navigation 的**列表侧**,不能当 Navigation 的兄弟。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user