跨端: 鸿蒙顶栏文案(摘要/一言/签名轮播)在三键左边 —— 位置与字号按实测校准
用户四条需求,逐条落地:
· 「可以在服务器集成一言与签名,同时 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:
@ -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)');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user