跨端: 宽屏侧栏根本不像 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:
2026-09-18 12:52:48 +08:00
parent 009ea172b7
commit 7b3028342a
6 changed files with 285 additions and 87 deletions

View File

@ -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)