跨端: 宽屏侧栏根本不像 WebUI —— 因为我上一版"复刻"的依据是编的
用户:「你自己看看宽屏的侧边栏和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` 明明有 `<span className="text-3xs leading-none">{short}</span>`
—— 通信/日历/联系三个标签一直都在;
- `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。
This commit is contained in:
@ -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` 有 `<span className="text-3xs">{short}</span>`
|
||||
* —— 侧栏**一直有**文字标签(通信/日历/联系)。
|
||||
* 我据此把鸿蒙侧栏做成了纯图标,还写了判据把它锁死 —— 判据锁住的是我的错误。
|
||||
*
|
||||
* 现在按两侧的**共同事实**判:侧栏项 = 图标(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 有 <span>{short}</span>);` +
|
||||
`实际文字:${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 有 <span>{short}</span>)');
|
||||
}
|
||||
// 窄屏样本不能被误判成宽屏(否则上面那套形状条件会去滤底栏项)
|
||||
assert.equal(isWideLayout(good), false, '自检:1200×2800 要判成窄屏');
|
||||
|
||||
@ -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 是 `<button data-testid="brand-mark"><BrandMarkIcon/>` —— 可点 + brandMark 图标
|
||||
assert.match(code_, /iconName: 'brandMark'/, '要有品牌标(WebUI `brand-mark` 的对应物,图标是 brandMark)');
|
||||
// 「我的」必须在侧栏里作为一个**导航项**出现,且走 onSelect 而不是推页
|
||||
assert.ok(!/onSettings/.test(code_),
|
||||
'侧栏不许留 onSettings(它推 `pages/SettingsPage` ⇒ 侧栏整条消失,正是用户报过的形状)');
|
||||
/*
|
||||
* 2026-09-16 用户:「底部导航栏不允许有文字」⇒ 侧栏同样走纯图标。
|
||||
* WebUI 的 `Sidebar.tsx` 本就是 60px 纯图标轨(无 label 文字)。
|
||||
* 「我的」由上面那个 `ForEach(NAV_CONTENT_ITEMS)` 覆盖(`NAV_CONTENT_ITEMS`
|
||||
* 含第 4 项)—— 所以这里判的是"没有另起一个推页入口",
|
||||
* 以及 onSelect 能把 index 传到第 4 项(`normalizeNavIndex` 不截到 3 项)。
|
||||
*/
|
||||
const active = /this\.currentIndex === index \? Theme\.navFgActive : Theme\.navFg/g;
|
||||
const hits = code_.match(active) || [];
|
||||
assert.equal(hits.length, 1,
|
||||
`侧栏导航项只应有 1 处选中三元式(现在 ${hits.length} 处)— 纯图标,无文字`);
|
||||
// 无文字(label 的 Text)
|
||||
assert.ok(!/Text\(label\)/.test(code_),
|
||||
'侧栏是纯图标轨:不允许有 label 文字(WebUI Sidebar 无文字)');
|
||||
// 图标不能太小(26vp)
|
||||
assert.match(code_, /iconSize: 26/,
|
||||
'侧栏导航图标要 26vp(WebUI 是 24,鸿蒙放大一档 —— 用户嫌 22 太小)');
|
||||
// 不加背景块 / 指示条
|
||||
assert.ok(!/currentIndex[^{;]{0,40}backgroundColor/.test(code_),
|
||||
'选中态不许改背景色(与底部导航同一纪律:变色即可)');
|
||||
assert.match(code_, /onClick\(\(\) => \{ this\.onSelect\(index\); \}\)/,
|
||||
'导航项点击要原样传 index(第 4 项「我的」因此走 onSelect(3) = 窗格)');
|
||||
// 品牌标点它要回第一项(WebUI `onClick={() => setViewMode('inbox')}`)
|
||||
assert.match(code_, /onClick\(\(\) => \{ this\.onSelect\(0\); \}\)/,
|
||||
'品牌标点击要回收件箱(WebUI brand-mark 的一致习惯:点左上角 logo 回家)');
|
||||
});
|
||||
|
||||
test('③ 侧栏选中态 = **浅蓝底块** + 变色(与底栏"只变色"是两条纪律)', () => {
|
||||
/*
|
||||
* ★★ 2026-09-18 重写。原来这条写着「选中态只换颜色……不加背景块」,
|
||||
* 并引"WebUI Sidebar 本就是纯图标轨"为依据 —— **依据是编的**。
|
||||
*
|
||||
* `client/electron/src/index.css:1595-1606` 的原话:
|
||||
* 「底部导航的选中态**只换颜色**……只作用在 `.narrow-nav` 上:宽屏侧栏是
|
||||
* 48px 宽的竖条,图标底下那一块底色是它**唯一的选中线索**,
|
||||
* 所以"只变色"不能无差别推广到所有 `.nav-item`。」
|
||||
* 而 `.nav-item[data-active='true'] { background-color: rgb(var(--nav-active-bg)) }`
|
||||
* 就是那块底色(`--nav-active-bg: 219 234 254` = #DBEAFE)。
|
||||
*
|
||||
* ★ 这条判据现在**直接读 WebUI 的 CSS 拿真实值**,而不是引自己写的注释 ——
|
||||
* 上一版之所以能一直绿,就是因为"依据"是一句话,没人拿它跟 WebUI 对。
|
||||
*/
|
||||
const sidebar = read('pages/WideSidebar.ets');
|
||||
const code_ = stripComments(sidebar);
|
||||
|
||||
// 从 WebUI 的真实源码里取侧栏选中底色(不写死,改了两边一起动)
|
||||
const css = readFileSync(
|
||||
join(ROOT, 'client', 'electron', 'src', 'index.css'), 'utf8');
|
||||
const m = css.match(/--nav-active-bg:\s*(\d+ \d+ \d+)/);
|
||||
assert.ok(m, 'WebUI index.css 要有 --nav-active-bg(侧栏选中底色)');
|
||||
const [r, g, b] = m[1].split(' ').map(Number);
|
||||
const hex = '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('').toUpperCase();
|
||||
const themeSrc = readFileSync(join(HARMONY_ETS, 'common', 'Theme.ets'), 'utf8');
|
||||
assert.match(themeSrc, new RegExp(`navActiveBg: string = '${hex}'`, 'i'),
|
||||
`Theme.navActiveBg 要等于 WebUI 的 --nav-active-bg(${hex})`);
|
||||
|
||||
// 选中态必须**有底块**(WebUI 侧栏唯一的选中线索)
|
||||
assert.match(code_, /backgroundColor\(this\.currentIndex === index \? Theme\.navActiveBg : Color\.Transparent\)/,
|
||||
'导航项选中要有底块(WebUI `.nav-item[data-active]` 的 --nav-active-bg)—— 侧栏不是底栏,不能只变色');
|
||||
|
||||
// 文字标签:WebUI `Sidebar.tsx:110` 有 <span>{short}</span>,所以侧栏**有** label
|
||||
assert.match(code_, /Text\(label\)/,
|
||||
'侧栏导航项要有文字标签(WebUI Sidebar 有 <span>{short}</span>)');
|
||||
|
||||
// ★ 自检:这三条都必须能判红
|
||||
assert.ok(!/backgroundColor\(this\.currentIndex === index \? Theme\.navActiveBg : Color\.Transparent\)/.test(
|
||||
'backgroundColor(this.currentIndex === index ? Color.Transparent : Theme.navActiveBg)'),
|
||||
'自检:三元式写反了必须判红');
|
||||
});
|
||||
|
||||
test('④ MainPage 接线:宽屏才挂侧栏、宽屏藏底部条、断点 768', () => {
|
||||
|
||||
@ -178,6 +178,32 @@ export class Theme {
|
||||
/** 导航文字:未选中 / 选中(对应 --nav-fg-muted / --nav-active-fg) */
|
||||
static readonly navFg: Resource = $r('sys.color.ohos_id_color_text_secondary');
|
||||
static readonly navFgActive: string = Theme.accentStrong;
|
||||
/*
|
||||
* 侧栏**选中块的底色**(WebUI `--nav-active-bg: 219 234 254`)。
|
||||
*
|
||||
* ★ 为什么侧栏有底块、底栏没有(这是两种导航**纪律不同**,不是不一致):
|
||||
* `index.css:1595-1606` 的原话:「底部导航的选中态**只换颜色**……
|
||||
* 只作用在 `.narrow-nav` 上:宽屏侧栏是 48px 宽的竖条,图标底下那一块底色
|
||||
* 是它**唯一的选中线索**,所以"只变色"不能无差别推广到所有 `.nav-item`。」
|
||||
* 我 2026-09-18 之前把底栏那条纪律套到了侧栏上,还在 `WideSidebar` 的注释里
|
||||
* 写成「WebUI 的 Sidebar 是纯图标轨(无 label 文字)」—— 那是**编的**:
|
||||
* `Sidebar.tsx:110` 明明有 `<span className="text-3xs">{short}</span>`,
|
||||
* `nav-item[data-active='true']` 也明明有底色。并排截图一眼就能看出来。
|
||||
*/
|
||||
static readonly navActiveBg: string = '#DBEAFE';
|
||||
/*
|
||||
* 侧栏**品牌标**的墨色(WebUI `--nav-fg-muted: 71 85 105` = #475569)。
|
||||
*
|
||||
* ★ 用像素取证过,不是照着色阶猜的:在 2x 缩放的 WebUI 侧栏截图上统计
|
||||
* 品牌标附近最常见的墨色 → `rgb(71,85,105) ×206`,正是 `--nav-fg-muted`。
|
||||
* 也就是说 WebUI 的品牌标是**中性石板灰**(`.nav-item` 默认色),
|
||||
* **不是**品牌蓝。鸿蒙这边原来画成 `Theme.accent`(蓝)—— 并排一看就不同色。
|
||||
*
|
||||
* 深色主题下 `--nav-fg-muted: 148 158 175`(见 index.css:1559)。
|
||||
* 鸿蒙暂未跟:`$r('sys.color.*')` 那套只在系统语义色上跟随,而这个值
|
||||
* 是 WebUI 自己定的中性色。这一条**记在案上**,不假装已经跟了。
|
||||
*/
|
||||
static readonly navBrandFg: string = '#475569';
|
||||
|
||||
// ─────────────── 业务语义色:系统没有对应物,继续自己写 ───────────────
|
||||
|
||||
|
||||
@ -132,11 +132,26 @@ export const NAV_CONTENT_RESERVE: number = NAV_BAR_HEIGHT + NAV_BAR_BOTTOM + NAV
|
||||
/** 内容窗格的数量(四项:通信/日历/联系人/我的)—— `MainPage` 的 currentIndex 分派按它穷尽 */
|
||||
export const NAV_CONTENT_COUNT: number = 4;
|
||||
|
||||
/** 内容窗格的清单(前三项:通信/日历/联系人)—— 宽屏侧栏与 currentIndex 分派都用它
|
||||
* (第 4 项「我的」是外壳入口,不在内 —— 否则宽屏侧栏会出现两个 person 图标) */
|
||||
/**
|
||||
* 内容窗格的清单 —— 宽屏侧栏与 `currentIndex` 分派**都用它**。
|
||||
*
|
||||
* ★ 2026-09-18 修注释:这里原来写着「前三项……第 4 项「我的」是外壳入口,不在内」,
|
||||
* 但代码写的是 `slice(0, NAV_CONTENT_COUNT)` 而 `NAV_CONTENT_COUNT = 4`
|
||||
* ⇒ **四项全在**,注释与代码互相矛盾。宽屏侧栏因此多出一个「我的」,
|
||||
* 与它下面那个"推页进设置"的入口并排(两个 person 图标),
|
||||
* 而 `MainPage` 的 `currentIndex === 3` 分支本来就是窗格 —— 三处口径不一。
|
||||
* 现在统一:**四项都是内容窗格**,注释按代码写。
|
||||
*/
|
||||
export const NAV_CONTENT_ITEMS: NavItem[] = NAV_ITEMS.slice(0, NAV_CONTENT_COUNT);
|
||||
|
||||
/** 外壳入口(第 4 项「我的」)的路由目标 —— 与 WideSidebar 的 onSettings 同一地址 */
|
||||
/**
|
||||
* 「我的」的路由目标(`SettingsPage` 仍然存在,供别处按需推页)。
|
||||
*
|
||||
* ★ 侧栏/底栏**不再**用它 —— 第 4 项是内容窗格(`currentIndex === 3`)。
|
||||
* 留着这个常量不是死代码:`SettingsPage` 作为 @Entry 仍可被推到
|
||||
* (例如将来做"从通知点开设置"),但**导航栏不许拿它当入口**
|
||||
* (用户 2026-09-17:「我的页面完全没有遵守 nav 的导航规则」)。
|
||||
*/
|
||||
export const NAV_SETTINGS_ROUTE: string = 'pages/SettingsPage';
|
||||
|
||||
/**
|
||||
|
||||
@ -2563,8 +2563,13 @@ struct MainPage {
|
||||
WideSidebar({
|
||||
currentIndex: this.currentIndex,
|
||||
bgActive: this.bgActive,
|
||||
onSelect: (index: number) => { this.currentIndex = normalizeNavIndex(index); },
|
||||
onSettings: () => { this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' }); }
|
||||
windowInsets: this.windowInsets,
|
||||
/*
|
||||
* 「我的」走 `onSelect(3)` 而不是推页 —— 与底栏同一套窗格机制。
|
||||
* 原先是 `onSettings: pushUrl('pages/SettingsPage')`,那正是用户
|
||||
* 2026-09-17 报过的"我的页面没有遵守 nav 导航规则"(侧栏这一支当时漏改)。
|
||||
*/
|
||||
onSelect: (index: number) => { this.currentIndex = normalizeNavIndex(index); }
|
||||
})
|
||||
}
|
||||
Column() {
|
||||
|
||||
@ -1,24 +1,36 @@
|
||||
/*
|
||||
* 宽屏侧栏图标轨 —— **一比一复刻 WebUI 的 `Sidebar.tsx`**。
|
||||
* 宽屏侧栏 —— 对齐 WebUI 的 `Sidebar.tsx`。
|
||||
*
|
||||
* WebUI 的 `Sidebar`(60px 宽)是**纯图标轨**(无 label 文字):
|
||||
* · 品牌标(顶部)
|
||||
* · 通信 / 日历 / 联系人(中部)
|
||||
* · 设置(底部)
|
||||
* 选中态:图标变色(`navFgActive`),**不加背景块、不加指示条、不加文字**
|
||||
* (用户 2026-09-16:「底部导航栏不允许有文字」⇒ 侧栏同样按纯图标走)。
|
||||
* 图标 26vp(WebUI 侧是 24,鸿蒙放大一档 —— 用户嫌 22 太小)。
|
||||
* WebUI 的侧栏(60px 宽)实际是:
|
||||
* · 品牌标(顶部,点它回收件箱)
|
||||
* · 通信 / 日历 / 联系 —— **每项 = 图标 + 文字标签**
|
||||
* · 选中态:**浅蓝底块**(`--nav-active-bg: 219 234 254`)+ 文字/图标变色
|
||||
* · 底部一簇:账号头像(带连接状态点)/ 主题切换 / 退出
|
||||
*
|
||||
* 鸿蒙这一侧原先**完全没有宽屏布局**(只有底部导航条),所以这个文件同时承担
|
||||
* "宽屏入口"的角色。判据 `harmony-widescreen.test.mjs` 会检查:
|
||||
* · 60vp 宽(与 WebUI 的 60px 一致)
|
||||
* · 三项导航 + 设置按钮
|
||||
* · 选中态只换颜色(与底部导航同一套纪律)
|
||||
* ★★ 2026-09-18 修:这个文件的上一版写着「WebUI 的 Sidebar 是**纯图标轨**
|
||||
* (无 label 文字)……选中态**不加背景块**」,并据此把侧栏做成了纯图标 + 只变色。
|
||||
* **那段理由是编的**,两条都错:
|
||||
* · `Sidebar.tsx:110` 有 `<span className="text-3xs leading-none">{short}</span>`
|
||||
* —— 通信 / 日历 / 联系 三个标签一直都在;
|
||||
* · `index.css:1590` 的 `.nav-item[data-active='true'] { background-color: … }`
|
||||
* 就是底块,而且 CSS 注释**专门**说了侧栏必须有它:
|
||||
* 「宽屏侧栏是 48px 宽的竖条,图标底下那一块底色是它**唯一的选中线索**,
|
||||
* 所以"只变色"不能无差别推广到所有 `.nav-item`」。
|
||||
* 我把底栏那条纪律(用户 2026-09-14「选中对应的文字和图标变色即可」)套到了侧栏上,
|
||||
* 而用户 2026-09-16 说的「底部导航栏不允许有文字」针对的是**底部导航栏**。
|
||||
* ⇒ 并排截图一眼可见的差异,被一段自己写的注释盖住了。
|
||||
*
|
||||
* 保留的差异(**有理由的**,不是漏做):WebUI 底部那一簇(头像/主题/退出)
|
||||
* 鸿蒙放在「我的」窗格与底栏里 —— 这里只放导航项,避免同一动作两个入口。
|
||||
*
|
||||
* 判据 `harmony-widescreen.test.mjs` 检查:60vp 宽、三项导航 + 设置、
|
||||
* 选中态有底块、导航项带文字标签。
|
||||
*/
|
||||
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { NAV_CONTENT_ITEMS, NavItem } from '../model/NavItems';
|
||||
import { Insets, topInset } from '../model/WindowInsets';
|
||||
|
||||
/** 侧栏宽度(vp)。WebUI 的 `Sidebar` 用 `w-[60px]` */
|
||||
export const SIDEBAR_WIDTH: number = 60;
|
||||
@ -27,54 +39,111 @@ export const SIDEBAR_WIDTH: number = 60;
|
||||
export struct WideSidebar {
|
||||
/** 当前选中的导航下标 */
|
||||
@Prop currentIndex: number = 0;
|
||||
/**
|
||||
* 窗口避让区(vp)。
|
||||
*
|
||||
* ★★ 2026-09-18 修:侧栏原先**完全没有避让** —— 全屏之后它的画布也从 y=0 开始,
|
||||
* 于是品牌标被状态栏时钟压住(并排截图里时钟正好落在信封上)。
|
||||
* 窄屏的顶部窗格当时补了 `topInset`,侧栏这一支漏了;而宽屏此前从未真正运行过。
|
||||
*/
|
||||
@Prop windowInsets: Insets = new Insets();
|
||||
/** 背景是否开启:开着=玻璃面板(blur + 0.88 白),关着=不透明白 */
|
||||
@Prop bgActive: boolean = false;
|
||||
/** 选中回调 */
|
||||
onSelect: (index: number) => void = (): void => {};
|
||||
/** 设置回调 */
|
||||
onSettings: () => void = (): void => {};
|
||||
|
||||
/**
|
||||
* 侧栏导航项 = **图标 + 文字标签 + 选中底块**(三项都与 WebUI `Sidebar` 一致)。
|
||||
*
|
||||
* 底块尺寸对齐 WebUI:`w-12 h-12 rounded-lg` = 48×48vp、圆角 8。
|
||||
*/
|
||||
@Builder
|
||||
NavItemBuilder(key: string, iconKey: string, index: number) {
|
||||
NavItemBuilder(key: string, label: string, iconKey: string, index: number) {
|
||||
Column() {
|
||||
AmIcon({
|
||||
iconName: iconKey,
|
||||
iconSize: 26,
|
||||
iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.onClick(() => { this.onSelect(index); })
|
||||
AmIcon({
|
||||
iconName: iconKey,
|
||||
iconSize: 22,
|
||||
iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg
|
||||
})
|
||||
Text(label)
|
||||
.fontSize(10)
|
||||
.fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)
|
||||
.margin({ top: 2 })
|
||||
}
|
||||
.width(48)
|
||||
.height(48)
|
||||
// 侧栏宽 60、项 48 ⇒ 两侧各 6vp,**不需要**再压缩;显式声明避免被父级按比例压掉
|
||||
.flexShrink(0)
|
||||
.borderRadius(12)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
// 选中底块:WebUI `.nav-item[data-active='true']` 的 `--nav-active-bg`
|
||||
.backgroundColor(this.currentIndex === index ? Theme.navActiveBg : Color.Transparent)
|
||||
.onClick(() => { this.onSelect(index); })
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 品牌标
|
||||
AmIcon({ iconName: 'brandMark', iconSize: 30, iconColor: Theme.accent })
|
||||
.margin({ bottom: 16 })
|
||||
/*
|
||||
* 品牌标(点它回收件箱 —— 与 WebUI 的 `brand-mark` 一致的习惯)。
|
||||
*
|
||||
* ★ 颜色用 `navBrandFg`(#475569 中性石板灰),**不是** `Theme.accent`:
|
||||
* WebUI 的品牌标走 `.nav-item` 默认色 `--nav-fg-muted`,像素取证见 Theme 的注释。
|
||||
* ★ 尺寸/圆角对齐 WebUI:`w-10 h-10 rounded-xl` = 40×40vp、圆角 16,
|
||||
* 图标 20vp。原来写的是裸 30vp 图标无底盒。
|
||||
*/
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
AmIcon({ iconName: 'brandMark', iconSize: 20, iconColor: Theme.navBrandFg })
|
||||
}
|
||||
.width(40).height(40)
|
||||
.borderRadius(16)
|
||||
.margin({ bottom: 8 })
|
||||
.onClick(() => { this.onSelect(0); })
|
||||
|
||||
// 导航项:**只内容窗格**(前三项)—— 设置已单列在下面,
|
||||
// 若遍历 NAV_ITEMS 会因第 4 项「我的」出现**两个 person 图标**。
|
||||
ForEach(NAV_CONTENT_ITEMS, (item: NavItem, index: number) => {
|
||||
this.NavItemBuilder(item.key, item.iconKey, index)
|
||||
}, (item: NavItem) => item.key)
|
||||
/*
|
||||
* 导航项**一簇贴顶**,不是等分铺满。
|
||||
*
|
||||
* WebUI 实测:三项各 48px 高、y = 70 / 122 / 174,间距 4(`gap-1`),
|
||||
* 剩下的空间由 `<div className="flex-1" />` 吃掉。
|
||||
* 我上一版给每项 `.layoutWeight(1)` ⇒ 在三折叠展开态下每项被拉成 395px 高,
|
||||
* 三个图标散在 1200px 里 —— 并排一看就知道不是同一个东西。
|
||||
*/
|
||||
Column({ space: 4 }) {
|
||||
ForEach(NAV_CONTENT_ITEMS, (item: NavItem, index: number) => {
|
||||
this.NavItemBuilder(item.key, item.label, item.iconKey, index)
|
||||
}, (item: NavItem) => item.key)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Blank()
|
||||
|
||||
// 设置
|
||||
Column() {
|
||||
AmIcon({ iconName: 'person', iconSize: 26, iconColor: Theme.navFg })
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.onClick(() => { this.onSettings(); })
|
||||
/*
|
||||
* ★★ 2026-09-18 修:下面那一簇(原「设置」单列项)删掉了。
|
||||
*
|
||||
* 它原来是 `onSettings → pushUrl('pages/SettingsPage')` —— 那正是用户
|
||||
* 2026-09-17 报过的形状:「我的页面完全没有遵守 nav 的导航规则」
|
||||
* (push 一个独立 @Entry 页 ⇒ 侧栏/底栏整条消失,要先「返回」才能再切窗格)。
|
||||
* 底栏那一支当时改成了窗格(`currentIndex === 3`),**侧栏这一支漏了**,
|
||||
* 于是宽屏下同一个毛病还在(而宽屏此前从未真正运行过,所以没人看见)。
|
||||
*
|
||||
* 现在两侧统一:`NAV_CONTENT_ITEMS` 是**全部四项**(含「我的」),
|
||||
* 上面的 `ForEach` 已经把四项都渲染出来(含「我的」= `onSelect(3)` = 窗格),
|
||||
* 所以这里**不需要**再单列一个入口 —— 单列那个会与第 4 项重复,
|
||||
* 而重复的那一个还恰好是"推页"的那条老路。
|
||||
*/
|
||||
}
|
||||
/*
|
||||
* ★ 2026-09-18 补一条**实测口径**(差点被我读错):
|
||||
* 三折叠展开态 dumpLayout 给的是**像素**,而这块设备的密度是 **2.875**(不是 3.5)。
|
||||
* 侧栏 173px ÷ 2.875 = **60.2vp**,导航项 138px ÷ 2.875 = **48.0vp** ——
|
||||
* 与声明完全一致。我一开始按 3.5 除,算出"60vp 被压成 49.4",差点去查
|
||||
* flex 压缩、去加 `.flexShrink(0)`(**都是假的**:没有压缩,是我除错了)。
|
||||
* 密度从声明值与实测值反推最稳:`138px / 48vp = 2.875`。
|
||||
*/
|
||||
.width(SIDEBAR_WIDTH)
|
||||
.height('100%')
|
||||
.padding({ top: 12, bottom: 12 })
|
||||
.padding({ top: 12 + topInset(this.windowInsets), bottom: 12 + this.windowInsets.navIndicator })
|
||||
/*
|
||||
* ★ 2026-09-16:玻璃面板(对齐 WebUI app-shell)。
|
||||
* bgActive(自定义壁纸开)时:系统材质 BlurStyle(半透明由系统给,不许手写 alpha)
|
||||
|
||||
Reference in New Issue
Block a user