跨端: 顶栏不再自己铺白条 + 日历改左右两栏 + 常驻窗格动画真的会播(用户三处实测指出)
用户三条反馈,逐条对应:
① 「底栏数字为什么显示在图标下面?」
WebUI 的徽标是 `absolute top-1 right-[22%]`(脱离文档流、浮在图标右上角),
我写成了 `Column` 的第三个子节点 ⇒ 参与竖向布局、掉到文字下面。
改用 `Stack({ alignContent: Alignment.TopEnd })` 锚在**图标**上。
(顺带撞了 skill 里明写的坑:Stack 没有 `.justifyContent()`。)
② 「一个横着过去的白条,我真的服了」/「期望:融进背景」
WebUI 的顶栏**自身没有底色** —— 只有 `border-b border-gray-200`
(`ContactPanel.tsx:64`、`CommTabs.tsx:40`、`CalendarView.tsx:295`),
底色由所在面板给;壁纸开启时那层面板是玻璃色(`index.css:876`)。
鸿蒙三个窗格顶栏写死了 `Theme.surface`(实心白)⇒ 无论壁纸开没开,
顶上都是一条不通明白带。改成与**页面底**同一口径
(`bgActive ? Transparent : surface`)+ 补下边框。
③ 「日历页面和webui布局完全不同」
WebUI 是**左右两栏**(`CalendarView.tsx:452-457`):左 `flex-1` 网格、
右 `400px` 常驻面板(日程 / 编辑器 / 小时网格三态互斥)。
鸿蒙原来是**单栏竖堆**。重搭为两栏,`paneWide` 由 `.onAreaChange`
量本页**自己的**宽度(不是屏幕宽度 —— 宽屏下这一页已被侧栏占掉一截);
编辑器改占右栏位置(不再整页盖掉正在看的那个月)。
④ 「最严重的动画问题你一点也不该改」
日历是**常驻挂载**(`visibility` 控制,因为它里面 today 要随时间重算、
也要保住"正在看哪个月"),而 `.transition()` 只在**挂载/卸载**时触发
(SDK 原话 \"when it **appears and disappears**\")⇒ 挂在它上面的
`.transition(paneRiseIn())` **一帧也不会播**,切过去是硬弹。
WebUI 踩过同一个坑并把错法写进了 `index.css:1305-1320`
(「只挂了类,却没让触发窗口出现 ⇒ 类挂着、动画永远不播」),
它的解法是 `html.view-switch` 重放窗口。ArkUI 对应物是
`animateTo` + 显式 `calPaneIn` 属性(`opacity` + `translate`)。
★ reset 必须在 `animateTo` **外**:写进回调里会与同帧的 1 相抵,
渲染层只看得见最终值 ⇒ 动画退化成一个瞬移。
顺带修:
· 宽屏侧栏 = 3 项(通信/日历/**联系**)+ 底部一簇(头像/主题/退出),
与底栏的四项(含「我的」)**不是同一份清单** —— WebUI 的 Sidebar 与
NarrowNav 本就不同(`Sidebar.tsx:26-44` vs `NarrowNav.tsx:37-40`+143)。
新增 `NAV_SIDEBAR_ITEMS` / `ME_PANE_INDEX` / `SIDEBAR_ITEM_*`。
· 退出登录抽成 `api/Logout.ets` 的 `performLogout()`:侧栏底簇与「我的」页
两个入口必须做同一件事(尤其"先注销推送 token"那一步),复制一份就会不一致。
· 徽标 `'plain'` 档底色:WebUI 是石板灰 `--c-chrome-600`(#475569),
我写成与未读共用红色 ⇒ 「联系」的徽标看起来像"有未读"。
· 主题快捷开关(侧栏底簇):对齐 `ThemeToggleButton` —— 从 `system` 翻转时
落到**当前生效值的反面**(不是回 system;那可能毫无变化、让按钮看起来坏了)。
· 「浓度」→「压暗」+ 数值带单位(原先屏上印 `56.000000`;WebUI 是 `suffix="%"`)。
判据(8 个套件全绿:logic 28 / nav 18 / widescreen 7 / window 9 /
arkts 5 / contacts 5 / calendar 30 / system-api 5):
· `harmony-widescreen` ② 重写:**回读 `Sidebar.tsx` 数 `short:` 的个数**
要求鸿蒙同数,并断言底部一簇三键真的被调用(`this.onToggleTheme()` ——
第一版写成 `/onToggleTheme/`,变异测试当场证明它不咬:属性**声明**还在,
正则照样匹上)。新增 ⑧:三档色调各自底色,期望值从 `--c-chrome-600` 读出。
· `harmony-nav` 动画条重写:改判**机制真的存在且被驱动**
(旧断言 `.visibility(...).transition(...)` 锁的正是那个 bug ——
判据引自己写的注释当依据,就会把错误锁死)。新增 reset-在-animateTo-外
这条断言(否则动画退化成瞬移)。
· `harmony-nav` 设备条:`navItemsOf` 的宽屏过滤从"左边缘靠左 1/6"
改成"**整个盒子在侧栏轨道内**" —— 旧条件把日历网格的格子
(实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
新增 `navRailItemsOf`:导航轨贴顶、底部簇在屏底,按位置切一刀。
· `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS`
**各自的数组**里取 label —— 原先把全文件 `label:` 一网打尽,
得到 7 个(4+3 混在一起),任何一边改对了它都会红。
设备实测(HATriple 三折叠,3184×2232):侧栏 3 项 + 底簇 / 底栏徽标回到
图标右上角 / 日历左右两栏与 WebUI 并排同构。
server/go.mod:补 2da38bb 漏提交的 lunar-go 依赖。
This commit is contained in:
@ -460,9 +460,45 @@ test('★ 窗格切换有真的过场动画(transition 挂在会换的那棵
|
||||
assert.ok(branchRoots >= 2,
|
||||
`★ transition 要挂在会被换掉的子树根上(if/else 分支),实际只找到 ${branchRoots} 处。` +
|
||||
'挂在两级之上的稳定父容器上等于没挂(实测过:6 秒取样仍是硬切)。');
|
||||
// 日历那一支(常驻 + visibility)也要有
|
||||
assert.match(mainCode, /\.visibility\(this\.currentIndex === 1[\s\S]{0,120}\.transition\(Theme\.paneRiseIn\(\)\)/,
|
||||
'★ 日历窗格(常驻挂载 + visibility 控制)也要挂过场过渡');
|
||||
|
||||
/*
|
||||
* ★★ 2026-09-19 重写日历那一条(用户:「最严重的动画问题你一点也不该改」)。
|
||||
*
|
||||
* 原断言:`.visibility(...)` 后面跟 `.transition(Theme.paneRiseIn())` ——
|
||||
* 它锚的是**写法**,而不是“动画真的会播”。而那个写法**一帧也不会播**:
|
||||
* `.transition()` 只在**挂载/卸载**时触发(SDK 原话 "when it **appears and
|
||||
* disappears**"),可日历是**常驻**的(用 `visibility` 控制,因为它里面
|
||||
* `today` 要随时间重算、也要保住“正在看哪个月”)—— 永不重挂载。
|
||||
* 所以旧断言是**把 bug 锁住了**(与我上次给侧栏编理由同一个错法)。
|
||||
*
|
||||
* 新断言改成断**机制真的存在且被驱动**:
|
||||
* · 常驻窗格不能用 TransitionEffect,要用可插值的属性
|
||||
* (`opacity` + `translate`);
|
||||
* · 那个属性要有一个 @State 支撑;
|
||||
* · 切窗格时要**真的把它从 0 推到 1**(且 reset 在 animateTo 外 ——
|
||||
* 否则同一帧内 0→1,起点终点都是 1,动画退化成瞬移)。
|
||||
*/
|
||||
assert.ok(!/\.visibility\(this\.currentIndex === 1[\s\S]{0,120}\.transition\(Theme\.paneRiseIn\(\)\)/.test(mainCode),
|
||||
'★ 日历窗格是**常驻挂载**(visibility 控制)—— 它上面的 `.transition()` 永远不会触发。' +
|
||||
'那条断言锁的就是这个 bug,不得恢复。');
|
||||
// 日历容器的入场:用可插值属性,且由 @State 支撑
|
||||
assert.match(mainCode, /calPaneIn/,
|
||||
'★ 日历窗格的入场进度要有一个 @State(`calPaneIn`)支撑 —— 常驻窗格只能靠属性插值做入场');
|
||||
const calContainer = /\.visibility\(this\.currentIndex === 1[\s\S]{0,400}?\.opacity\(this\.calPaneIn\)/.exec(mainCode);
|
||||
assert.ok(calContainer,
|
||||
'★ 日历容器要真的用 `calPaneIn` 驱动透明度(这才是常驻窗格能播的入场)');
|
||||
assert.match(mainCode, /\.translate\(\{[^}]*calPaneIn[^}]*\}\)/,
|
||||
'★ 日历容器要同时驱动位移(对齐 WebUI `rise-in` 的 translateY)');
|
||||
// reset 必须在 animateTo **外** —— 否则起点=终点,动画退化
|
||||
const resetIdx = mainCode.indexOf('this.calPaneIn = 0');
|
||||
const animIdx = mainCode.indexOf('getUIContext().animateTo(');
|
||||
assert.ok(resetIdx > 0, '★ 切到日历前要先把 `calPaneIn` 瞬回 0(否则看不到“从无到有”)');
|
||||
assert.ok(resetIdx < animIdx || animIdx < 0,
|
||||
'★ `calPaneIn = 0` 必须在 `animateTo` **之前**:写进回调里会与同帧的 1 相抵,' +
|
||||
'渲染层只看得见最终值 ⇒ 动画退化成一次瞬移(等于没修)');
|
||||
const intoAnim = /getUIContext\(\)\.animateTo\([\s\S]{0,300}?this\.calPaneIn = 1/.exec(mainCode);
|
||||
assert.ok(intoAnim,
|
||||
'★ 推到 1 要在 `animateTo` 窗口里(与其余窗格的 transition 同时长、同曲线)');
|
||||
|
||||
// animateTo 走 getUIContext(全局 animateTo 已废弃,另行有判据钉)
|
||||
assert.match(mainCode, /getUIContext\(\)\.animateTo\(/,
|
||||
@ -933,24 +969,55 @@ function navItemsOf(root) {
|
||||
const boxW = x2 - x1;
|
||||
const boxH = y2 - y1;
|
||||
/*
|
||||
* ★ 阈值用**比例**而不是绝对 px,且必须容得下真实的 48vp 项。
|
||||
* 实测(密度 2.875):项 138×138px、屏 3184×2232 ⇒
|
||||
* x1=45 < 3184/6=531 ✓、boxW=138 < 3184/4=796 ✓、
|
||||
* 但 boxH=138 需要 > 2232*0.08=178 ✗ ⇒ 四项**全被滤掉**。
|
||||
* 也就是说这个阈值我第一版写大了,只是当时项被压成 39vp 才"恰好"通过
|
||||
* (那是别的 bug,不是阈值对)。
|
||||
* 改成 `screenH * 0.04`(=89px):48vp 项(138px)过得去,
|
||||
* 而内容区那些横向长条(高 43px < 89)仍然被排除。
|
||||
* ★★ 2026-09-19 修(被判据自己抳到):过滤条件从 “靠左 1/6” 改成
|
||||
* “**整个盒子在侧栏轨道内**”。
|
||||
*
|
||||
* ★ 还要**要求有文字标签**:品牌标也是左上的可点方块(实测 `Stack [57,174][172,289]`
|
||||
* 40vp、无文字),不加这一条它会混进来,而它不是导航项 ——
|
||||
* 它是"点它回家"的品牌按钮(WebUI 的 `brand-mark`)。
|
||||
* 原条件 `x1 < screenW / 6` 只要求**左边缘**靠左 —— 而日历网格的格子实测是
|
||||
* `Column [229,511][366,794]`:x1=229 < 531 ✓、高 283 > 89 ✓、带文字 ✓
|
||||
* ⇒ **被当成导航项**。于是一屏日历数出 11~12 个“导航项”,
|
||||
* 而侧栏实际只有 3 项。
|
||||
*
|
||||
* 换成 “`x2`(右边缘)也在轨道内”:
|
||||
* · 导航项 `[45,312][183,450]` ⇒ x2=183 ≤ 255 ✓
|
||||
* · 品牌标 `[57,174][172,289]` ⇒ x2=172 ✓(但无文字,另行排除)
|
||||
* · 日历格子 `[229,511][366,794]` ⇒ x2=366 > 255 ✗ **排除**
|
||||
*
|
||||
* 为什么用比例 `screenW * 0.08`(展开态 = 255px)而不是写死 183/200:
|
||||
* 侧栏是 60vp,px 值随密度变(教训:**密度是第二个真相**,见 `isWideLayout`)。
|
||||
* 0.08 在展开态的 3184px 上给 255,在单屏 1008px 上给 80(而单屏不是宽屏,不走这支)。
|
||||
*/
|
||||
return x1 < screenW / 6 && boxW < screenW / 4 && boxH > screenH * 0.04
|
||||
return x2 <= screenW * 0.08 && boxH > screenH * 0.04
|
||||
&& textsUnder(n).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 宽屏侧栏里的**导航轨**(不含底部那一簇)。
|
||||
*
|
||||
* ★★ 2026-09-19 新增(用户:「你写的app和webui大面积不符」)。
|
||||
*
|
||||
* 侧栏底部那一簇(头像 / 主题 / 退出)也是可点、也有文字/图标,形状与导航项相近 ——
|
||||
* `navItemsOf` 会把它们一起数进来(实测数出 12 个,而导航项应为 3 个)。
|
||||
* 而 WebUI 确实有这一簇(`Sidebar.tsx:183-212`),所以**不能拿它当“多出来的项”判红**;
|
||||
* 也不能因此放宽总项数(那会让“我的又回到侧栏”那个 bug 漏过去)。
|
||||
*
|
||||
* 所以改按**位置**切:导航项一簇**贴顶**(WebUI 实测 y = 70/122/174,`gap-1`),
|
||||
* 下面用 `Blank()` 推到屏幕底部才是那一簇。取两者之间的空档切一刀即可。
|
||||
*
|
||||
* ★ 阈值用“屏幕高度的一半”:导航项在顶部 1/3 以内,底部簇在最后 1/4 以内,
|
||||
* 中间有大段空白(实测展开态导航项在 y≈985-1380、底部簇在 y≈2000+)。
|
||||
* 用比例而不是绝对 px,避开密度那第二个真相(同 `isWideLayout` 的教训)。
|
||||
*/
|
||||
function navRailItemsOf(root) {
|
||||
const items = navItemsOf(root);
|
||||
const screenH = screenHeightOf(root);
|
||||
return items.filter(n => {
|
||||
const m = (n.attributes?.bounds || '').match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
||||
if (!m) return false;
|
||||
return Number(m[2]) < screenH * 0.5;
|
||||
});
|
||||
}
|
||||
|
||||
test('★ 行为(设备):底栏真渲染了可点的导航项(dumpLayout 实测,live ⊆ source)', async (t) => {
|
||||
const CRIT = 'harmony-nav/底栏可点项';
|
||||
const hdc = findHdc();
|
||||
@ -1041,8 +1108,33 @@ test('★ 行为(设备):底栏真渲染了可点的导航项(dumpLayout
|
||||
*
|
||||
* 现在按两侧的**共同事实**判:侧栏项 = 图标(Path)+ 文字标签。
|
||||
* 图标画出 + 文字命中源码清单,两条都要。
|
||||
*
|
||||
* ★★ 2026-09-19 修第二处(与 harmony-widescreen ② 同一个错):
|
||||
* 这一段原来拿 `N.NAV_ITEMS`(**四项**,含「我的」)的 label 去比侧栏渲染出来的文字
|
||||
* —— 但侧栏是**三项**(通信/日历/**联系**),第三项对不上(“联系” ≠ “联系人”),
|
||||
* 而且它反过来证明不了“侧栏多了我的”那个 bug。
|
||||
* 侧栏要比的是 `NAV_SIDEBAR_ITEMS`;两项前两项相同只是巧合,不能因此混用。
|
||||
*/
|
||||
const sourceLabels = new Set(N.NAV_ITEMS.map(i => i.label));
|
||||
const sourceLabels = new Set(N.NAV_SIDEBAR_ITEMS.map(i => i.label));
|
||||
/*
|
||||
* 侧栏**总共**应该正好是 `NAV_SIDEBAR_ITEMS` 那个数。
|
||||
* 只判“每项命中”不够:多出来的一项(例如「我的」)也可能命中一个 label,
|
||||
* 而“多一个我的”本来就是这次报的 bug。所以同时判**个数**。
|
||||
*/
|
||||
/*
|
||||
* ★★ 2026-09-19:改判**导航轨**(`navRailItemsOf`)而不是全部可点项 ——
|
||||
* 侧栏底部那一簇(头像/主题/退出)是 WebUI 就有的(`Sidebar.tsx:183-212`),
|
||||
* 把它数进来会得到 12,而那并不是“多了一个我的”。
|
||||
* 数量仍要卡死(不然“我的回到侧栏”那个 bug 会漏过去),只是卡在**轨道**这一层。
|
||||
*/
|
||||
const railItems = navRailItemsOf(root);
|
||||
assert.equal(railItems.length, N.NAV_SIDEBAR_ITEMS.length,
|
||||
`宽屏侧栏导航轨应有 ${N.NAV_SIDEBAR_ITEMS.length} 项(与 WebUI Sidebar.tsx 的 navItems 同数),` +
|
||||
`实际 ${railItems.length} 项(多出来很可能就是「我的」又回到了侧栏);` +
|
||||
`(全部可点项 ${navItems.length} 个,含底部那一簇)`);
|
||||
/* 后续逐项断言作用在导航轨上 */
|
||||
navItems.length = 0;
|
||||
navItems.push(...railItems);
|
||||
for (const it of navItems) {
|
||||
const paths = [...walk(it)].filter(x => x.attributes?.type === 'Path');
|
||||
assert.ok(paths.length >= 1,
|
||||
@ -1051,7 +1143,7 @@ test('★ 行为(设备):底栏真渲染了可点的导航项(dumpLayout
|
||||
const texts = textsUnder(it);
|
||||
const matched = texts.filter(l => sourceLabels.has(l));
|
||||
assert.ok(matched.length >= 1,
|
||||
`宽屏侧栏项要带文字标签且命中源码 NAV_ITEMS(WebUI Sidebar 有 <span>{short}</span>);` +
|
||||
`宽屏侧栏项要带文字标签且命中源码 NAV_SIDEBAR_ITEMS(WebUI Sidebar 有 <span>{short}</span>);` +
|
||||
`实际文字:${texts.join('、')}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user