diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs index 3e2a961..556ad37 100644 --- a/client/electron/test/harmony-nav.test.mjs +++ b/client/electron/test/harmony-nav.test.mjs @@ -313,17 +313,27 @@ test('★ 玻璃只在两处、且这一处是"背后有可变内容"(GLASS * 跨端对齐:这条与 WebUI 的 `nav-merge.test.mjs ④` 是同一条要求的两端实现, * 两边各自钉住自己的写法(`cross-client-*` 那几条钉的是共享数值,不是这里)。 */ -test('⑤ 选中态只换颜色:图标与文字都变色,且不引入背景/指示条', () => { +test('⑤ 选中态只换颜色:纯图标导航、图标变色,且不引入背景/指示条/文字', () => { const item = builderBody(main, 'NavItem(item: NavItem, index: number) {'); const itemCode = stripComments(item); + /* + * 2026-09-16 用户:「底部导航栏不允许有文字」⇒ 导航项是**纯图标**, + * 只有 1 处选中三元式(AmIcon 的 iconColor)。文字那一半被去掉。 + */ const active = /this\.currentIndex === index \? Theme\.navFgActive : Theme\.navFg/g; const hits = itemCode.match(active) || []; - assert.equal(hits.length, 2, - `图标与文字都要过同一个选中三元式(现在 ${hits.length} 处)—— 只给文字上色就是"选中了一半"`); - // 图标(AmIcon 的 iconColor)与文字(Text 的 fontColor)各自紧跟着那个三元式 - const icon = itemCode.slice(itemCode.indexOf('AmIcon({'), itemCode.indexOf('Text(item.label)')); + assert.equal(hits.length, 1, + `纯图标导航只应有 1 处选中三元式(现在 ${hits.length} 处)— 若有 2 处是文字还留着`); + // 图标上色必须存在(AmIcon 的 iconColor 跟着选中态走) + const icon = itemCode.slice(itemCode.indexOf('AmIcon({'), itemCode.indexOf('AmIcon({') + 160); assert.match(icon, /iconColor:\s*this\.currentIndex === index \? Theme\.navFgActive : Theme\.navFg/, '图标的 iconColor 必须跟着选中态走'); + // 图标不能太小(用户:22 太小) + assert.match(icon, /iconSize: 28/, + '底部导航图标要 28vp(用户嫌 22 太小)'); + // 无文字:导航项里不该有 label 的 Text + assert.ok(!/Text\(item\.label\)/.test(itemCode), + '底部导航是纯图标:不允许有文字 label(2026-09-16 用户明确要求)'); // 反面:选中态不得靠形状表达(背景 / 圆角块 / 下划线元素) assert.ok(!/backgroundColor\([^)]*currentIndex/.test(itemCode), '选中态不许改背景色("变色即可",形状是另一套语言)'); @@ -337,20 +347,19 @@ test('⑤ 选中态只换颜色:图标与文字都变色,且不引入背景/ '未选中色取系统次要文字色(不手写色值)'); }); -/** ⑤ 的变异自检:把图标那行退回"只给文字上色",判据必须红。 */ -test('★ ⑤ 变异自检:图标不上色(改造前的写法)必须被判红', () => { - const oldIconLine = 'AmIcon({ iconName: item.iconKey, iconSize: 22 })'; - const itemCode = `Column() {\n ${oldIconLine}\n Text(item.label)\n .fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)\n }`; - const hits = itemCode.match(/this\.currentIndex === index \? Theme\.navFgActive : Theme\.navFg/g) || []; - assert.equal(hits.length, 1, '旧写法只有 label 上色 ⇒ ⑤ 的"必须两处"会命中它'); - /* - * 判定窗口必须与 ⑤ **完全同一条规则**:从 `AmIcon({` 切到下一个 `Text(`。 - * 我第一版在这里写的是 `Text\(item\.icon\)[\s\S]{0,80}fontColor` —— 那个窗口 - * 把**下一行 label 的** fontColor 也圈进来了,于是"旧写法没有图标上色"这条 - * 自检自己绿了(正是本仓库反复记的"窗口式判据")。判据被它咬了一次。 - */ - const icon = itemCode.slice(itemCode.indexOf('AmIcon({'), itemCode.indexOf('Text(', itemCode.indexOf('AmIcon({') + 1)); - assert.ok(!/iconColor:/.test(icon), `旧写法里图标没有 iconColor ⇒ ⑤ 的图标那一半会命中它(切片=${JSON.stringify(icon)})`); +/** ⑤ 的变异自检:退回"带文字 / 太小 / 图标不上色"必须被判红。 */ +test('★ ⑤ 变异自检:nav 项退回带文字或 22 尺寸或图标不上色必须被判红', () => { + // 变异 1:带文字(Text(item.label))⇒ ⑤ 的"无文字"会命中它 + const withLabel = `Column() {\n AmIcon({ iconName: item.iconKey, iconSize: 28 })\n Text(item.label)\n .fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)\n }`; + assert.ok(/Text\(item\.label\)/.test(withLabel), + '变异 1 自检失败:样本里必须有 label 文字'); + // 变异 2:图标 22(太小)⇒ ⑤ 的"iconSize: 28"会命中它 + const smallIcon = `Column() {\n AmIcon({ iconName: item.iconKey, iconSize: 22, iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg })\n }`; + assert.ok(/iconSize: 22/.test(smallIcon) && !/iconSize: 28/.test(smallIcon), + '变异 2 自检失败:样本里图标必须是 22 且不是 28'); + // 变异 3:图标不上色(没有 iconColor)⇒ ⑤ 的"iconColor 跟着选中态"会命中它 + const noColor = `Column() {\n AmIcon({ iconName: item.iconKey, iconSize: 28 })\n }`; + assert.ok(!/iconColor/.test(noColor), '变异 3 自检失败:样本里不该有 iconColor'); }); /** diff --git a/client/electron/test/harmony-widescreen.test.mjs b/client/electron/test/harmony-widescreen.test.mjs index ab84808..2d3f400 100644 --- a/client/electron/test/harmony-widescreen.test.mjs +++ b/client/electron/test/harmony-widescreen.test.mjs @@ -50,14 +50,23 @@ test('② 三项导航 + 设置按钮:内容与 WebUI Sidebar 对齐', () => { assert.match(sidebar, /onSettings/, '设置按钮要回调 onSettings(点击后由 MainPage 路由)'); }); -test('③ 选中态只换颜色(与底部导航同一套纪律)', () => { +test('③ 选中态只换颜色(与底部导航同一套纪律):纯图标、无文字', () => { const sidebar = read('pages/WideSidebar.ets'); const code_ = stripComments(sidebar); - // 图标与文字都过同一个选中三元式 + /* + * 2026-09-16 用户:「底部导航栏不允许有文字」⇒ 侧栏同样走纯图标。 + * WebUI 的 `Sidebar.tsx` 本就是 60px 纯图标轨(无 label 文字)。 + */ const active = /this\.currentIndex === index \? Theme\.navFgActive : Theme\.navFg/g; const hits = code_.match(active) || []; - assert.equal(hits.length, 2, - `图标与文字都要过同一个选中三元式(现在 ${hits.length} 处)`); + 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_), '选中态不许改背景色(与底部导航同一纪律:变色即可)'); diff --git a/client/harmony/entry/src/main/ets/common/Theme.ets b/client/harmony/entry/src/main/ets/common/Theme.ets index 9e4a214..4982dc7 100644 --- a/client/harmony/entry/src/main/ets/common/Theme.ets +++ b/client/harmony/entry/src/main/ets/common/Theme.ets @@ -106,6 +106,16 @@ export class Theme { /** 控件圆角:系统"按钮"圆角 */ static readonly radiusControl: Resource = $r('sys.float.ohos_id_corner_radius_button'); + /* + * ★ 2026-09-16:宽屏玻璃面板几何(与 WebUI `app-shell` 的 CSS 变量**同值**)。 + * `--radius-card: 0.875rem` = 14px、`--pane-gap: 10px`(`index.css`)。 + * 写死常量而不是 `$r('sys.*')`:因为 WebUI 是 14/10,而系统的 + * `corner_radius_card` 是 16 —— 宽屏面板必须用 WebUI 的值才能"一比一复刻"。 + * 原有的 `radiusCard`/`radiusControl` 保持系统跟随,不动它们。 + */ + static readonly glassRadius: number = 14; + static readonly paneGap: number = 10; + /* * ─────────────── 手写色**登记表**(改这里要一起改判据) ─────────────── * diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 1ff71fb..61b0935 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -1744,23 +1744,18 @@ struct MainPage { NavItem(item: NavItem, index: number) { Column() { /* - * 选中态**只换颜色**,而且**图标与文字都要换**。 + * 选中态**只换颜色**。 * * 用户(2026-09-14):「同时底部导航栏选中对应的文字和图标变色即可」。 - * 这与 WebUI 侧是同一件事:那边删掉了背景块与顶部指示条、只留颜色 - * (`NarrowNav.tsx` / `.narrow-nav .nav-item[data-active='true']`)。 - * 鸿蒙这一侧本来就没有背景 / 指示条(选中态从没用形状表达过), - * 缺的是**图标这一半**:原来只有 label 上了色,图标一直是默认色, - * 看上去像「选中了一半」。两处都过同一个三元式,避免以后只改一处。 + * 2026-09-16:用户进一步明确「底部导航栏不允许有文字」⇒ 去掉 label,只留图标。 + * 这与 WebUI 一致:`NarrowNav.tsx` 的底部导航是**纯图标**(WebUI 图标是 24×24, + * 鸿蒙这边用 28 放大一档 —— 用户嫌 22 太小)。 */ AmIcon({ iconName: item.iconKey, - iconSize: 22, + iconSize: 28, iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg }) - Text(item.label) - .fontSize(Theme.fontTiny) - .fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg) } /* * 命中区:**显式给下限**(44vp),不靠"看起来够大"。 diff --git a/client/harmony/entry/src/main/ets/pages/WideSidebar.ets b/client/harmony/entry/src/main/ets/pages/WideSidebar.ets index 8a15e21..aa51a38 100644 --- a/client/harmony/entry/src/main/ets/pages/WideSidebar.ets +++ b/client/harmony/entry/src/main/ets/pages/WideSidebar.ets @@ -1,12 +1,13 @@ /* * 宽屏侧栏图标轨 —— **一比一复刻 WebUI 的 `Sidebar.tsx`**。 * - * WebUI 的 `Sidebar`(60px 宽)是一列图标按钮: + * WebUI 的 `Sidebar`(60px 宽)是**纯图标轨**(无 label 文字): * · 品牌标(顶部) * · 通信 / 日历 / 联系人(中部) * · 设置(底部) - * 选中态:文字/图标变色 + 品牌浅底(`nav-active-bg`),**不加背景块、不加指示条** - * (用户 2026-09-14:「同时底部导航栏选中对应的文字和图标变色即可」)。 + * 选中态:图标变色(`navFgActive`),**不加背景块、不加指示条、不加文字** + * (用户 2026-09-16:「底部导航栏不允许有文字」⇒ 侧栏同样按纯图标走)。 + * 图标 26vp(WebUI 侧是 24,鸿蒙放大一档 —— 用户嫌 22 太小)。 * * 鸿蒙这一侧原先**完全没有宽屏布局**(只有底部导航条),所以这个文件同时承担 * "宽屏入口"的角色。判据 `harmony-widescreen.test.mjs` 会检查: @@ -32,19 +33,16 @@ export struct WideSidebar { onSettings: () => void = (): void => {}; @Builder - NavItemBuilder(key: string, label: string, iconKey: string, index: number) { + NavItemBuilder(key: string, iconKey: string, index: number) { Column() { AmIcon({ iconName: iconKey, - iconSize: 22, + iconSize: 26, iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg }) - Text(label) - .fontSize(Theme.fontTiny) - .fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg) - .margin({ top: 2 }) } .width('100%') + .layoutWeight(1) .justifyContent(FlexAlign.Center) .onClick(() => { this.onSelect(index); }) } @@ -52,22 +50,22 @@ export struct WideSidebar { build() { Column() { // 品牌标 - AmIcon({ iconName: 'brandMark', iconSize: 26, iconColor: Theme.accent }) + AmIcon({ iconName: 'brandMark', iconSize: 30, iconColor: Theme.accent }) .margin({ bottom: 16 }) // 导航项 ForEach(NAV_ITEMS, (item: NavItem, index: number) => { - this.NavItemBuilder(item.key, item.label, item.iconKey, index) + this.NavItemBuilder(item.key, item.iconKey, index) }, (item: NavItem) => item.key) Blank() // 设置 Column() { - AmIcon({ iconName: 'person', iconSize: 22, iconColor: Theme.navFg }) - Text('我的').fontSize(Theme.fontTiny).fontColor(Theme.navFg).margin({ top: 2 }) + AmIcon({ iconName: 'person', iconSize: 26, iconColor: Theme.navFg }) } .width('100%') + .layoutWeight(1) .justifyContent(FlexAlign.Center) .onClick(() => { this.onSettings(); }) } @@ -75,6 +73,6 @@ export struct WideSidebar { .height('100%') .padding({ top: 12, bottom: 12 }) .backgroundColor(Theme.surface) - .borderRadius(Theme.radiusCard) + .borderRadius(Theme.glassRadius) } }