跨端: 鸿蒙顶栏文案(摘要/一言/签名轮播)在三键左边 —— 位置与字号按实测校准

用户四条需求,逐条落地:
  · 「可以在服务器集成一言与签名,同时 app 本地缓存一部分」
  · 「摘要也应该放在顶部,显示摘要不显示一言,显示一言不显示摘要」
  · 「自动轮播,要有消失出现动画。同时注意,是纯文字不要加底」
  · 「我要的效果是在退出,最大化,最小化三个按键的左边」

客户端(服务端那半见 31939f2)
  · 新增 `common/TopbarStore.ets`:一言 + 签名的取数与**账号级**缓存
    (键 = 前缀 + accountId,本仓纪律;共用一份会让多账号串台)。
    本地缓存先出(秒开、离线可用),再后台拉一次更新;
    拉失败**保留缓存**、不抛异常 —— 装饰性内容不该成为失败点。
  · 轮播:摘要 / 一言 / 签名三选一轮着显示,5.5s 一条、
    淡出淡入各 260ms(停顿明显长于动画,否则观感是"一直在闪")。
  · 纯文字:不设 background、不加玻璃(用户点名「不要加底」),
    `hitTestBehavior(None)` 不吃事件。

★ 位置:为什么自绘而不 `setWindowTitle`
  官方 `setWindowTitle` **实测确实**能在那一行显示文字(截图验过),
  但它三条硬伤:① 必须保持窗口装饰可见 ⇒ 标题栏横带回来,与刚修好的
  「顶栏沉浸」冲突;② 瞬时替换,做不了用户要的消失出现动画;
  ③ 字号颜色跟随系统。⇒ 装饰仍隐藏,文字自绘在装饰带原位。

★★ 两个"按实测校准"的修正(都是用户看出来的)
  1. **位置**:我先做成"右对齐、贴住三键左缘"——用户纠正
     「我要求的是与三键同行,但是在左边啊」。改成靠左(FlexAlign.Start)。
  2. **对齐与字号**(用户:「行没有对齐,大小也偏小」):
     实测(1px ≈ 1.91vp):
       三键 y 290..343 高 53px、中心 316.5
       我原来 y 296..323 高 27px、中心 309.5  ⇒ **中心差 7px、字号小一档**
     改法:字号 12 → 14vp;垂直对齐从写死的 `y: 8` 改成
     `height(windowInsets.windowDecor)` + `VerticalAlign.Center`。
     改后实测:**中心差 0.0px**、文案高 31px(与三键图标同量级)。

★ 顺带修掉一个逻辑漏洞
  摘要三项计数都是 0 时我返回了空串 ⇒ 整块**不渲染**,顶栏右上什么都没有。
  而"没有未读"恰恰是常态(收件箱清干净了)。加兜底文案「暂无待办」。
  那句"三项都是 0 就不显示"是我按"有信息才显示"想当然写的,
  没考虑"零"本身也是信息。

判据(harmony-2in1 新增 4 条 → 19→23,全部变异验证过)
  · 在三键左边且不破坏沉浸 —— 钉的是**两个约束同时成立**
    (只看一件会放过错误的那版:为了三键左边而恢复标题栏)
  · 纯文字:不许 backgroundColor / backgroundBlurStyle,且不吃事件
  · 摘要为零也要有文案(把兜底改回空串即判红)
  · 一言/签名缓存要账号级、失败要吞掉

★ 判据自己的两个坑(都在注释里记了)
  ① 切片锚点不能用常量的**名字**:`TOPBAR_STRIP_VPAD` 先在文件顶部常量区
     出现一次,从那里往后切会一路包进 `InboxTab`(那里有 backgroundColor),
     于是报"文案加了底色"——报的其实是**别人的代码**。改成锚**使用点**。
  ② 位置断言跟着事实改过一轮:第一版给"贴三键"那个错版背书,
     用户纠正后改成断言靠左。

实测凭据(2in1 模拟器 3120×2080)
  · 文案 x 545..652、y 301..332;三键 x 2362..2567、y 290..343
  · 垂直中心差 0.0px
  · 沉浸保留(装饰仍隐藏)
This commit is contained in:
2026-09-25 18:24:08 +08:00
parent 1436fd1fb1
commit 9d50352e7e
4 changed files with 611 additions and 1 deletions

View File

@ -564,3 +564,142 @@ test('2in1|右键菜单的破坏性动作(归档)必须先确认', () => {
/* 真正的请求在确认回调之后(另一个方法里)——确保不是"点了就发" */
assert.match(body, /doArchiveRow/, '确认的回调里才发请求');
});
/* ────────────────────────────────────────────────────────────────
* ⑦ 顶栏文案(摘要 / 一言 / 签名轮播)—— 2026-09-25
*
* 用户四条:
* · 「可以在服务器集成一言与签名,同时 app 本地缓存一部分」
* · 「摘要也应该放在顶部,显示摘要不显示一言,显示一言不显示摘要」
* · 「自动轮播,要有消失出现动画。同时注意,是纯文字不要加底」
* · 「我要的效果是在退出,最大化,最小化三个按键的左边」
* ──────────────────────────────────────────────────────────────── */
test('2in1|顶栏文案在三键左边,且不破坏窗口沉浸', () => {
/*
* ★ 这条钉的是**两个约束同时成立**,因为它们会互相顶:
* 要"在三键左边"最省事的做法是官方 `setWindowTitle`(实测确实能显示),
* 但那个**必须保持窗口装饰可见** —— 于是标题栏那条浅色横带回来,
* 与用户先前要的「顶栏沉浸」直接冲突(那是刚修好的)。
*
* 所以正确解法是:**装饰仍隐藏**(沉浸保留)+ 文字自绘在装饰带原位。
* 判据必须同时看到这两件事,只看一件就会放过错误的那版。
*
* 改坏会红:
* · 去掉 setWindowDecorVisible(false) ⇒ 第一段红(沉浸丢了)
* · 去掉自绘的 position/padding ⇒ 第二段红(文案不在三键左边)
*/
const ABILITY = code(join(ETS, 'entryability/EntryAbility.ets'));
const MAIN = code(join(ETS, 'pages/MainPage.ets'));
/* ① 沉浸必须保留(装饰隐藏) */
assert.match(ABILITY, /win\.setWindowDecorVisible\(false\)/,
'窗口装饰要保持隐藏 —— 否则标题栏横带回来,与「顶栏沉浸」冲突');
/*
* ② 文案自绘,且在**左边**(与三键同一行、左右相称)。
*
* ★★ 这里**改过一次**:第一版我断言的是"贴住三键左缘"
* (右对齐 + TOPBAR_TRIPLE_BTN_WIDTH),用户纠正:
* 「我要求的是与三键同行,但是在左边啊」
* ⇒ 断言随之改成靠左。判据要跟事实走,不是给错版背书。
*/
assert.match(MAIN, /\.justifyContent\(FlexAlign\.Start\)/,
'文案要**靠左**(与右边三键同一行、左右相称)');
assert.match(MAIN, /\.position\(\{ x: 0, y: 0 \}\)/,
'文案要绝对定位到窗口顶部(整窗根 Stack 的最上层)');
assert.match(MAIN, /padding\(\{ left: TOPBAR_STRIP_LEFT \}\)/,
'左侧留出与窗口边缘的距离');
/*
* ★ 垂直对齐要**用装饰带自己的高度**,不许写死 y 偏移。
*
* 我第一版写 `.position({ y: 8 })`(凭感觉给的 8vp),
* 实测文案中心 309.5、三键中心 316.5 —— **差 7px**(用户看出「行没有对齐」)。
* 改成 `height(windowInsets.windowDecor)` + `VerticalAlign.Center` 后
* 实测中心差 **0.0px**。
*
* ⇒ 判据钉"高度取自 windowDecor"这条做法,而不是某个具体数值:
* 它同时保证"带高变了也不跑偏"。
*/
assert.match(MAIN, /\.height\(this\.windowInsets\.windowDecor\)/,
'垂直对齐要用装饰带自己的高度(写死 y 偏移会差几个 px —— 实测过)');
assert.match(MAIN, /\.alignItems\(VerticalAlign\.Center\)/,
'带内垂直居中');
});
test('2in1|顶栏文案必须是纯文字(不加底、不吃事件)', () => {
/*
* 用户原话:「同时注意,是纯文字不要加底」。
*
* ★ 为什么这条要单独钉:给它加个底是最容易的"看起来更清楚"的冲动,
* 而那条会在沉浸顶栏上再压一块色 —— 正是用户点名不要的。
* 同理不能加玻璃材质(那也会形成一块可见的面)。
*
* 改坏会红:给那个 Row 加 backgroundColor / backgroundBlurStyle。
*/
const MAIN = code(join(ETS, 'pages/MainPage.ets'));
/*
* ★ 切片锚点取**使用点**(`.position({ x: 0, y: TOPBAR_STRIP_VPAD })`),
* 不是那个常量的**名字**。
*
* 我第一版就用了名字 —— 而它在**文件顶部的常量区**先出现一次,
* 从那里往后切会一路包进 `InboxTab`(那里有 `backgroundColor`),
* 于是判据报"文案加了底色",报的是**别人的代码**。
* 本仓纪律:判据匹配到的东西必须就是它声称的那个。
*/
const at = MAIN.indexOf('.position({ x: 0, y: 0 })');
assert.ok(at > 0, '顶栏文案块要存在(找不到定位语句)');
/* 往前取到该块的起点、往后取到它的收口,覆盖整块 */
const start = Math.max(0, at - 1200);
const body = MAIN.slice(start, at + 900);
assert.ok(body.length > 0, '要能切出顶栏文案块');
assert.ok(!/\.backgroundColor\(/.test(body),
'顶栏文案不许加底色(用户:「是纯文字不要加底」)');
assert.ok(!/\.backgroundBlurStyle\(/.test(body),
'顶栏文案不许加玻璃材质(同样是"加了一层可见的面")');
assert.match(body, /hitTestBehavior\(HitTestMode\.None\)/,
'文案是装饰性的,不许吃掉底下的点击(三键就在它右边)');
});
test('2in1|顶栏摘要"零"时也要有文案(否则整块消失)', () => {
/*
* ★ 这条钉的是一个**实测撞出来的逻辑漏洞**。
*
* 我第一版写的是"三项计数都是 0 就返回空串"——想当然的"有信息才显示"。
* 实测:那封邮件已读 ⇒ 三项全 0 ⇒ `topbarTexts()` 成空数组 ⇒ 整块**不渲染**,
* 顶栏右上什么都没有。
*
* 而"没有未读"恰恰是**常态**(收件箱清干净了)。那时该说"暂无待办",
* 而不是整块消失让人以为坏了。
*
* 改坏会红:把 `return '暂无待办'` 改回 `return ''`。
*/
const MAIN = code(join(ETS, 'pages/MainPage.ets'));
assert.match(MAIN, /暂无待办/,
'摘要为零时要有兜底文案 —— "零"本身也是信息,不该让整块消失');
});
test('2in1|一言与签名要在客户端缓存(账号级),离线也能轮播', () => {
/*
* 用户:「同时 app 本地缓存一部分」。
*
* ★ 缓存键必须**账号级**(`kind + accountId`)—— 本仓纪律。
* 共用一份会让换个账号看到上一个人的签名;`user_appearance` 当初就是
* 全局键,多账号互相覆盖。
*
* ★ 还要验"拉失败不致命":装饰性内容不该成为失败点
* (服务端那半也有对称的一条判据)。
*
* 改坏会红:去掉 prefKey 的 accountId 拼接、或让 refresh 把异常抛出去。
*/
const STORE = code(join(ETS, 'common/TopbarStore.ets'));
assert.match(STORE, /private prefKey\(accountId: string\)/,
'缓存键要按账号拼(共用一份会让多账号串台)');
assert.match(STORE, /KEY_PREFIX \+ accountId/,
'键的实际拼法要含 accountId');
assert.match(STORE, /catch \(e\)/,
'拉取/缓存失败要吞掉 —— 顶栏是装饰性的,不该把异常抛给调用方');
assert.match(STORE, /'\/me\/topbar'/,
'要打服务端那个端点(/me/topbar)');
});

View File

@ -152,7 +152,7 @@ const SUITE = [
* 2in1 键盘可达(用户 2026-09-21「快捷键打开发信页面 / 上下键切换发信目标 /
* 回车展开输入框」)。见该文件头部说明:为什么单开一个文件、为什么不端到端验。
*/
['test/harmony-2in1.test.mjs', [], 16],
['test/harmony-2in1.test.mjs', [], 23],
['test/harmony-contacts.test.mjs', [], 5],
// ★★ 下面三条是**补接线**,不是新写的判据(2026-09-17)。
//