From 7b3028342a587b27529a25d0cb2b7f3459101c79 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 18 Sep 2026 12:52:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E5=AE=BD=E5=B1=8F?= =?UTF-8?q?=E4=BE=A7=E6=A0=8F=E6=A0=B9=E6=9C=AC=E4=B8=8D=E5=83=8F=20WebUI?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E5=9B=A0=E4=B8=BA=E6=88=91=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E7=89=88"=E5=A4=8D=E5=88=BB"=E7=9A=84=E4=BE=9D?= =?UTF-8?q?=E6=8D=AE=E6=98=AF=E7=BC=96=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「你自己看看宽屏的侧边栏和webui有哪怕一丁点的相似之处嘛?」 并排截图(WebUI 1100×700 @2x vs 三折叠展开态 3184×2232)之后,差异一眼可见: | | WebUI | 鸿蒙(改前) | |---|---|---| | 文字标签 | **有**(通信/日历/联系) | 没有 | | 选中态 | **浅蓝底块** | 只换颜色 | | 「我的」 | 底部头像按钮进入 | 甩给 `onSettings` → **pushUrl 推页** | | 品牌标颜色 | `#475569` 石板灰 | 品牌蓝 | | 项间距 | 48px 项 + 4px gap,**贴顶一簇** | `layoutWeight(1)` 等分铺满(395px/项) | ## 根因:`WideSidebar` 里那段"复刻 WebUI"的注释是**编的** ``` * WebUI 的 `Sidebar`(60px 宽)是**纯图标轨**(无 label 文字)…… * 选中态:图标变色(`navFgActive`),**不加背景块、不加指示条、不加文字** * (用户 2026-09-16:「底部导航栏不允许有文字」⇒ 侧栏同样按纯图标走) ``` 两条都错,而且都能在源码里当场证伪: - `Sidebar.tsx:110` 明明有 `{short}` —— 通信/日历/联系三个标签一直都在; - `index.css:1590` 的 `.nav-item[data-active='true'] { background-color: … }` 就是底块,而且 CSS 注释**专门**说了侧栏必须有它: 「宽屏侧栏是 48px 宽的竖条,图标底下那一块底色是它**唯一的选中线索**, 所以"只变色"不能无差别推广到所有 `.nav-item`。」 我犯的错是**把底栏那条纪律套到了侧栏上**:用户 2026-09-14 说「底部导航栏选中 对应的文字和图标变色即可」、2026-09-16 说「底部导航栏不允许有文字」—— 两句都针对**底部导航栏**,而侧栏是另一种东西(`index.css:1595-1606` 把这个区别 写得很清楚)。更糟的是我把这个错误**写进了判据**(`harmony-widescreen` ②③ 与 `harmony-nav` 的宽屏分支),于是判据锁住的是我编的理由,一路全绿。 ## 修 - 侧栏项 = **图标 + 文字标签 + 选中底块**(`navActiveBg` = `--nav-active-bg` #DBEAFE, 判据**直接读 WebUI 的 CSS** 取值,不写死、更不引自己的注释)。 - 品牌标:`navBrandFg` = `#475569`(**像素取证**:2x 截图里品牌标附近最常见的墨色 是 `rgb(71,85,105) ×206` = `--nav-fg-muted`,即中性石板灰,**不是**品牌蓝); 尺寸/圆角按 WebUI `w-10 h-10 rounded-xl`(40×40、圆角 16);点它回收件箱。 - 项**贴顶一簇**(`Column({ space: 4 })` = WebUI 的 `gap-1`),不再 `layoutWeight(1)`。 - 删掉单列的"设置"入口(`onSettings` 回调一并删除)—— 那正是用户 2026-09-17 报过的 「我的页面完全没有遵守 nav 的导航规则」(push 页 ⇒ 侧栏整条消失)。 「我的」由 `ForEach(NAV_CONTENT_ITEMS)` 覆盖(该常量**含第 4 项**, 走 `onSelect(3)` = 窗格,与底栏同一套)。 - 补避让:侧栏原先**完全没有** `topInset` ⇒ 全屏之后品牌标被状态栏时钟压住。 ## 判据(并修掉它们锁住的错误) - `harmony-widescreen` ②③ **重写**:从"纯图标 / 只变色"改成 "有文字标签 / 有选中底块 / 不许留 `onSettings`",并读 WebUI `index.css` 拿真实色值。 - `harmony-nav` 宽屏分支:原来断言「侧栏项**不该有文字**」—— 同一条编造。 改成"图标(Path)画出来了 **且** 文字命中源码 `NAV_ITEMS`"。 - `harmony-nav` 宽屏形状阈值 `boxH > screenH*0.08` 是**错的**:48vp 项在密度 2.875 下 是 138px,而阈值要求 >178px ⇒ 四项全被滤掉(当时"通过"只是因为项被另一个 bug 压成了 39vp)。改成 `*0.04`,并补一条"必须有文字"把**品牌标**(40vp 无文字的可点方块) 排除在外。 **变异测试 3 个方向全咬**:去掉文字标签 ⇒ 红;去掉选中底块 ⇒ 红;Theme 色值写错 ⇒ 红。 ★ 顺带记一条**我差点犯的错**:我一度按 density 3.5 换算,算出"60vp 侧栏被压成 49.4vp", 去查 flex 压缩、加 `.flexShrink(0)` —— 全是假的。实测密度是 **2.875** (`138px ÷ 48vp = 2.875`),侧栏 173px ÷ 2.875 = **60.2vp**,与声明完全一致。 **没有压缩,是我除错了。** 已撤回那笔改动并把口径写进注释。 harmony-widescreen 6/6、harmony-nav 18/18、harmony-window 9/9、harmony-arkts 5/5、 harmony-contacts 5/5、harmony-calendar 30/30、harmony-system-api 5/5、harmony-logic 28/28。 --- client/electron/test/harmony-nav.test.mjs | 63 ++++++-- .../electron/test/harmony-widescreen.test.mjs | 102 ++++++++---- .../entry/src/main/ets/common/Theme.ets | 26 +++ .../entry/src/main/ets/model/NavItems.ts | 21 ++- .../entry/src/main/ets/pages/MainPage.ets | 9 +- .../entry/src/main/ets/pages/WideSidebar.ets | 151 +++++++++++++----- 6 files changed, 285 insertions(+), 87 deletions(-) diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs index 2ac8ac2..837eead 100644 --- a/client/electron/test/harmony-nav.test.mjs +++ b/client/electron/test/harmony-nav.test.mjs @@ -932,7 +932,22 @@ function navItemsOf(root) { const screenW = screenWidthOf(root); const boxW = x2 - x1; const boxH = y2 - y1; - return x1 < screenW / 6 && boxW < screenW / 4 && boxH > screenH * 0.08; + /* + * ★ 阈值用**比例**而不是绝对 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)仍然被排除。 + * + * ★ 还要**要求有文字标签**:品牌标也是左上的可点方块(实测 `Stack [57,174][172,289]` + * 40vp、无文字),不加这一条它会混进来,而它不是导航项 —— + * 它是"点它回家"的品牌按钮(WebUI 的 `brand-mark`)。 + */ + return x1 < screenW / 6 && boxW < screenW / 4 && boxH > screenH * 0.04 + && textsUnder(n).length > 0; }); } @@ -1016,15 +1031,28 @@ test('★ 行为(设备):底栏真渲染了可点的导航项(dumpLayout * 每个导航项子树里要有 `Path`(`AmIcon` 的画法)且**不该有 Text** * (有文字就说明有人往纯图标轨里塞了标签,那正是被否掉的那个方案)。 */ + /* + * ★★ 2026-09-18 修:这一段原来断言「宽屏侧栏项**不该有文字**」,理由是 + * "用户明确否掉了带文字的方案"。**那是编的**: + * · 用户 2026-09-16 说的是「**底部导航栏**不允许有文字」,不是侧栏; + * · WebUI `Sidebar.tsx:110` 有 `{short}` + * —— 侧栏**一直有**文字标签(通信/日历/联系)。 + * 我据此把鸿蒙侧栏做成了纯图标,还写了判据把它锁死 —— 判据锁住的是我的错误。 + * + * 现在按两侧的**共同事实**判:侧栏项 = 图标(Path)+ 文字标签。 + * 图标画出 + 文字命中源码清单,两条都要。 + */ + const sourceLabels = new Set(N.NAV_ITEMS.map(i => i.label)); for (const it of navItems) { - const kinds = textsUnder(it); // 复用 walk:非空 Text - assert.equal(kinds.length, 0, - `宽屏侧栏项不该有文字(纯图标轨,用户明确否掉了带文字的方案);` + - `实际:${kinds.join('、')}`); const paths = [...walk(it)].filter(x => x.attributes?.type === 'Path'); assert.ok(paths.length >= 1, `宽屏侧栏项(bounds=${it.attributes.bounds})要画出图标(Path 节点)—— ` + '没有 Path 说明图标是空的,侧栏会变成一排摸不着的空白区'); + const texts = textsUnder(it); + const matched = texts.filter(l => sourceLabels.has(l)); + assert.ok(matched.length >= 1, + `宽屏侧栏项要带文字标签且命中源码 NAV_ITEMS(WebUI Sidebar 有 {short});` + + `实际文字:${texts.join('、')}`); } } /* @@ -1091,15 +1119,21 @@ test('★ 判据自检:底栏取值逻辑 —— 好样本取得到、缺底 * * 合成树按实测形状造:3184×2232、侧栏项 `Column [28,985][201,1380]`、子树只有 Path。 */ - const iconRail = (y0) => ({ - // 侧栏项:宽 ~173(< W/4)、高 ~395(> H*0.08)、x1=28(< W/6) - attributes: { type: 'Column', bounds: `[28,${y0}][201,${y0 + 395}]`, clickable: 'true' }, - children: [{ attributes: { type: 'Stack', bounds: `[77,${y0 + 160}][152,${y0 + 235}]`, clickable: 'false' }, - children: [{ attributes: { type: 'Path', bounds: `[79,${y0 + 164}][148,${y0 + 230}]`, clickable: 'false' }, children: [] }] }], + /* + * 侧栏项样本按**实测形状**造(密度 2.875、三折叠展开态): + * Column [45,312][183,450] = 138×138px,子树是 AmIcon(Path) + Text('通信')。 + */ + const iconRail = (y0, label) => ({ + attributes: { type: 'Column', bounds: `[45,${y0}][183,${y0 + 138}]`, clickable: 'true' }, + children: [ + { attributes: { type: 'Stack', bounds: `[73,${y0 + 20}][146,${y0 + 93}]`, clickable: 'false' }, + children: [{ attributes: { type: 'Path', bounds: `[75,${y0 + 22}][144,${y0 + 91}]`, clickable: 'false' }, children: [] }] }, + text(label, `[85,${y0 + 100}][143,${y0 + 134}]`) + ], }); const wideRoot = { attributes: { type: 'Row', bounds: '[0,0][3184,2232]', clickable: 'false' }, - children: [iconRail(985), iconRail(1380), + children: [iconRail(312, '通信'), iconRail(462, '日历'), // 内容区一张宽卡片(可点、有文字)—— 它**不该**被当成导航项 { attributes: { type: 'ListItem', bounds: '[263,212][1115,568]', clickable: 'true' }, children: [text('pi', '[309,245][360,280]')] }], @@ -1107,11 +1141,12 @@ test('★ 判据自检:底栏取值逻辑 —— 好样本取得到、缺底 assert.equal(isWideLayout(wideRoot), true, '自检:3184×2232 要判成宽屏'); const wideItems = navItemsOf(wideRoot); assert.equal(wideItems.length, 2, - `★ 自检失败:宽屏侧栏应当取到 2 个图标项(实际 ${wideItems.length})—— ` + + `★ 自检失败:宽屏侧栏应当取到 2 个导航项(实际 ${wideItems.length})—— ` + '取到 3 个说明内容卡片被误当导航项,"导航没挂"就永远判不出来'); for (const it of wideItems) { - assert.equal(textsUnder(it).length, 0, '自检:侧栏项样本本来就没有文字(纯图标轨)'); - assert.ok([...walk(it)].some(x => x.attributes?.type === 'Path'), '自检:侧栏项要含 Path'); + assert.ok([...walk(it)].some(x => x.attributes?.type === 'Path'), '自检:侧栏项要含 Path(图标画出来了)'); + assert.ok(textsUnder(it).some(l => ['通信','日历','联系人','我的'].includes(l)), + '自检:侧栏项要带命中清单的文字标签(WebUI Sidebar 有 {short})'); } // 窄屏样本不能被误判成宽屏(否则上面那套形状条件会去滤底栏项) assert.equal(isWideLayout(good), false, '自检:1200×2800 要判成窄屏'); diff --git a/client/electron/test/harmony-widescreen.test.mjs b/client/electron/test/harmony-widescreen.test.mjs index d525399..2e16fec 100644 --- a/client/electron/test/harmony-widescreen.test.mjs +++ b/client/electron/test/harmony-widescreen.test.mjs @@ -9,6 +9,7 @@ // 这条判据钉的是 Sidebar 自己的形状:宽 60vp、三项导航 + 设置、选中态只换颜色。 import { code, prose, stripComments } from './lib/read.mjs'; +import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; @@ -40,37 +41,84 @@ test('① 宽度:SIDEBAR_WIDTH = 60vp(WebUI 的 `w-[60px]` 是同一数字 assert.match(main, /WideSidebar\(/, 'MainPage 要真正挂 WideSidebar'); }); -test('② 三项导航 + 设置按钮:内容与 WebUI Sidebar 对齐', () => { - const sidebar = read('pages/WideSidebar.ets'); - // NAV_CONTENT_ITEMS 遍历(**只内容窗格** —— 设置已单列在下面;若遍历 NAV_ITEMS - // 会因第 4 项「我的」出现两个 person 图标) - assert.match(sidebar, /ForEach\(NAV_CONTENT_ITEMS,/, '要遍历内容窗格清单(与底部导航前三项同源)'); - // 品牌标 + 三项导航 + 设置(与 WebUI:brand / nav / settings 的分组一致) - assert.match(sidebar, /品牌标/, '要有品牌标(注释里的分组依据)'); - assert.match(sidebar, /iconName: 'person'/, '设置按钮要用 person 图标(WebUI 侧沿用的同一枚)'); - assert.match(sidebar, /onSettings/, '设置按钮要回调 onSettings(点击后由 MainPage 路由)'); -}); - -test('③ 选中态只换颜色(与底部导航同一套纪律):纯图标、无文字', () => { +test('② 四项导航(与底栏同源):不再有单列的"设置"入口', () => { + /* + * ★★ 2026-09-18 重写。原来这条判的是「三项导航 + 设置按钮」, + * 依据是"设置单列在下面、遍历 NAV_ITEMS 会出现两个 person 图标"。 + * 那个依据**两半都错**: + * · `NAV_CONTENT_ITEMS = NAV_ITEMS.slice(0, NAV_CONTENT_COUNT)` 而 + * `NAV_CONTENT_COUNT = 4` ⇒ **四项全在**(含「我的」),不存在"只有前三项"; + * · 单列的"设置"走 `onSettings → pushUrl('pages/SettingsPage')`, + * 而那正是用户 2026-09-17 报过的「我的页面完全没有遵守 nav 的导航规则」 + * (底栏那一支改成了窗格,侧栏这一支漏了)。 + * + * WebUI `Sidebar.tsx` 的真实形状:`navItems` 三项 + **底部一簇** + * (账号头像 / 主题切换 / 退出)。「我的」在 WebUI 是 `viewMode === 'account'`, + * 由侧栏底部那个**头像按钮**进入 —— 也就是说它是**导航项**,不是"推出去的页"。 + * 鸿蒙的对应物是第 4 项「我的」窗格(与底栏一致)。 + */ const sidebar = read('pages/WideSidebar.ets'); const code_ = stripComments(sidebar); + assert.match(code_, /ForEach\(NAV_CONTENT_ITEMS,/, + '侧栏要遍历 NAV_CONTENT_ITEMS(与底栏同一个清单 —— 两处各留一份会漂移)'); + // 品牌标:WebUI 是 `