跨端: 登录页那个"emoji"是 Unicode 符号 —— 顺手修好图标贴左上角(共 4 处)

用户两条反馈,都是**看着界面**报出来的,而编译器与所有既有判据全绿:
①「为什么登陆页不是app图标,而是一个emojy?」
②「你自己看看那个图标的位置正常吗?」

## ① `Text('✉')` 被系统渲染成彩色 emoji

登录页的品牌标识原先写的是 `Text('✉')`(Unicode U+2709)。HarmonyOS 字体链里有
**彩色 emoji 字体**,U+2709 自带 emoji 字形 ⇒ 渲染成一枚黄白色风信封 emoji:

- `.fontColor(Theme.accent)` 对彩色 emoji **无效**(界面显示的是 emoji 自带颜色);
- 与底栏/侧栏那些 `AmIcon` 线描图标不是同一套视觉语言;
- 实测截图硬证:大屏下那枚 emoji 比旁边的文字还显眼。

而本仓**早就有** `ICON_PATHS.brandMark`(就是 App 图标上那个信封,专为品牌标识画的)。
换成 `AmIcon({ iconName: 'brandMark' })` 即可 —— 走 `Path.stroke()`,跟主题色走。

## ② 图标贴在卡片左上角(量出来的)

`AmIcon` 内部那个 `Stack` 是 `iconSize` 那么大、**默认靠左上**排版。调用方写
`AmIcon({…}).width(48).height(48)` 想要个大点的可上色盒子时,**外层盒子变大、图标不动**。

实测(三折叠 3.5 密度,`dumpLayout` 读的**实际 bounds**):

    卡片 [1523,521][1661,659]  138×138px
    图标 [1526,524][1589,587]   63×63px   ← 左边距/上边距都只有 3px
    ⇒ 图标中心偏 10vp

**不是一处**:全仓扫出 4 个同样写法。修法是套一层
`Stack({ alignContent: Alignment.Center })`(仓里回复球与悬浮加号本来就是这么写的,
所以它们一直是对的):

| 位置 | 图标 | 盒子 | 原状态 |
|---|---|---|---|
| 登录页品牌卡 | brandMark 24 | 48×48 | 贴左上 ✗ |
| 用户管理刷新键 | repeat 20 | 40×40 | 贴左上 ✗ |
| 联系人视图切换 | cardView 18 | 40×40 | 贴左上 ✗ |
| 收件箱组头箭头 | chevronRight 12 | 20 槽位 | 贴左 ✗ |
| 回复球 / 悬浮加号 | chatBubble / compose | 56×56 | 本来就对(尺寸在外层 Button 上) |

修后实测:偏移 **−34.5px → 0.5px**(0.14vp,亚像素级)。

## 判据(harmony-arkts 3 → 5 条)

- **图标不许用 Unicode 符号充当**:扫 `Text('…')` 里单个符号的情况。
  ★ 只框 U+2600–U+27BF / U+2B00–U+2BFF / U+FE0F,**刻意不含基本箭头段**(U+2190–U+21FF)——
  `→` 在正文里是标点不是图标,框进来会误伤大量正常文案。
  (我第一版把箭头段也框了,结果自检自己先红 —— 断言写错就是写错,不靠放宽它来「修」。)
- **尺寸不许直接链在 `AmIcon` 上**:把"图标贴左上角"这个坑的**形状**钉住,
  并自检"套了 `Stack` 的正确写法不许被误判"。

**两个变异方向都跑过**:放回 `Text('✉')` ⇒ 红 ✓;给 `AmIcon` 直接加 `.width(48)` ⇒ 红 ✓。
This commit is contained in:
2026-09-18 11:47:51 +08:00
parent 2da38bba83
commit 0e5eec61bd
6 changed files with 199 additions and 18 deletions

View File

@ -124,6 +124,24 @@ export struct AmIcon {
.strokeLineJoin(LineJoinStyle.Round)
.scale({ x: this.iconScale(this.iconSize), y: this.iconScale(this.iconSize) })
}
/*
* ★★ 2026-09-18:这里原来是 `.width(this.iconSize).height(this.iconSize)`,
* 现在**显式保持默认尺寸**,同时把“调用方给更大盒子”的做法钉成错误用法。
*
* 曾经的陷阱:调用方写 `AmIcon({…}).width(48).height(48)`(想要一个大点的可点区域)——
* 外层盒子变大了,而**内部这个容器的尺寸不变、且默认靠左上** ⇒ 图标贴在盒子左上角。
*
* 实测(登录页品牌卡,三折叠 3.5 密度,`dumpLayout` 读实际 bounds):
* 卡片 [1523,521][1661,659] 138×138px
* 图标 [1526,524][1589,587] 63×63px
* ⇒ 中心偏了 10vp(用户:「你自己看看那个图标的位置正常吗」)。
* 同样写法在仓里有 4 处(悬浮加号/返回键/刷新键/品牌卡),只是图标小时偏得不明显。
*
* 为什么不在这里写 `width('100%')` 来适配:那会让**所有没显式给尺寸的调用点**
* (绝大多数)在 `Row` 里试图撑满父容器 —— 那是拿一个普遍存在的布局回归
* 换四个特例的方便。正确做法是调用方要“大盒子”就自己套一层居中的容器
* (见 `LoginPage` 品牌卡与 `heroIcon` 的用法),组件本身只管画好这 iconSize 那么大的图标。
*/
.width(this.iconSize)
.height(this.iconSize)
}

View File

@ -346,8 +346,20 @@ struct AdminUsersPage {
Text('管理').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
.layoutWeight(1)
// 刷新:管理页的动作个个改服务端状态,看不到最新值会让人怀疑自己刚才点没点上
AmIcon({ iconName: 'repeat', iconSize: 20, iconColor: Theme.accent }).width(40).height(40)
.onClick(() => { this.load(); })
/*
* ★ 40×40 的命中区,图标居中 —— 尺寸加在**外层 Stack** 上。
*
* 原来写的是 `AmIcon({...}).width(40).height(40)`:内层容器固定 20vp 且靠左上,
* 于是 20vp 的图标贴在 40×40 命中区的左上角(同 `LoginPage` 品牌卡那处,
* 2026-09-18 一起修)。命中区本身是好的(40vp ≥ 44? 不 —— 见下),
* 但视觉上"图标没在按钮中央"是对的观感洁癖,也说明这个盒子才是按钮。
*/
Stack({ alignContent: Alignment.Center }) {
AmIcon({ iconName: 'repeat', iconSize: 20, iconColor: Theme.accent })
}
.width(40)
.height(40)
.onClick(() => { this.load(); })
}
/* 高度与 padding-top 一起加避让 —— 理由见 `ComposePage` 顶栏(同形状) */
.width('100%').height(56 + topInset(this.windowInsets)).padding({ left: 8, right: 8, top: topInset(this.windowInsets) })

View File

@ -14,6 +14,7 @@ import { ApiBaseCheck, validateApiBase } from '../model/ApiBase';
import { PushService } from '../api/PushService';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AmIcon } from '../common/Icons';
@Entry
@Component
@ -202,11 +203,39 @@ struct LoginPage {
Scroll() {
Column() {
Column() {
// 与 WebUI 登录页一致:品牌标识不是巨幅文字,而是一枚紧凑的浅蓝图标卡。
Stack() {
Text('✉')
.fontSize(22)
.fontColor(Theme.accent)
/*
* 品牌标识(与 WebUI 登录页一致:一枚紧凑的浅蓝图标卡,不是巨幅文字)。
*
* ★★ 2026-09-18 修:这里原先是 `Text('✉')`(Unicode U+2709)。
* HarmonyOS 的字体链里有**彩色 emoji 字体**,U+2709 自带 emoji 字形
* ⇒ 它被渲染成一枚**彩色 emoji**(黄白色风信封),而不是单色图标:
* 既不跟主题色走(`fontColor(Theme.accent)` 对彩色 emoji **无效** —— 界面看着
* 就是那个 emoji 自带的颜色),也和底栏/侧栏那些 `AmIcon` 线描图标不是同一套视觉语言。
* 实测截图硬证:大屏下那枚 emoji 比旁边的文字还显眼。
*
* 正确做法是 `AmIcon({ iconName: 'brandMark' })` —— 本仓**早就有**这个图标
* (`common/Icons.ets` 的 `ICON_PATHS.brandMark`,就是 App 图标上那个信封),
* 而它走 `Path.stroke()`,跟主题色走、与其余图标同一套描边。
*
* ★ 纪律:**界面里不许用 Unicode 符号当中图标**。
* 它在某些字体链上会被渲染成 emoji,而 emoji 不吃 `fontColor`。
*/
/*
* ★ 尺寸/底色加在**外层 `Stack`** 上,**不加在 `AmIcon` 上**。
*
* 直接把 `.width(48)` 链在 `AmIcon` 后面是个陷阱:它内部那个容器固定是
* `iconSize` 那么大、且**默认靠左上**,外层盒子变大而图标不动 ⇒
* 图标贴在卡片左上角。实测(三折叠 3.5 密度,dumpLayout 读的实际 bounds):
* 卡片 [1523,521][1661,659] 138×138px
* 图标 [1526,524][1589,587] 63×63px ← 左边距 3px、上边距 3px
* 中心偏了 10vp(用户:「你自己看看那个图标的位置正常吗」)。
*
* `Stack({ alignContent: Alignment.Center })` 是"给图标一个更大的、
* 居中的、可上色的盒子"的正确写法 —— 仓里另外两处(回复球、悬浮加号)
* 就是这么做的(它们把尺寸加在外层 `Button` 上,所以一直是对的)。
*/
Stack({ alignContent: Alignment.Center }) {
AmIcon({ iconName: 'brandMark', iconSize: 24, iconColor: Theme.accent })
}
.width(48)
.height(48)

View File

@ -537,9 +537,17 @@ struct InboxTab {
@Builder
GroupHeader(g: SessionGroup) {
Row() {
AmIcon({ iconName: 'chevronRight', iconSize: 12, iconColor: Theme.textSubtle })
.rotate({ angle: this.isExpanded(g.key) ? 90 : 0 })
.width(20)
/*
* 展开箭头:`.width(20)` 是**给它一个 20vp 的槽位**(与下面的标题对齐),
* 而图标只有 12vp —— 尺寸加在**外层 Stack** 上才会居中;
* 直接链在 `AmIcon` 上会让箭头贴在槽位左侧(2026-09-18 与另外两处一起修)。
* `rotate` 留在图标上(转的是箭头本身,不是那 20vp 的槽位)。
*/
Stack({ alignContent: Alignment.Center }) {
AmIcon({ iconName: 'chevronRight', iconSize: 12, iconColor: Theme.textSubtle })
.rotate({ angle: this.isExpanded(g.key) ? 90 : 0 })
}
.width(20)
Column() {
Row() {
Text(g.latest?.from_name ?? '')
@ -1508,13 +1516,21 @@ struct ContactsTab {
Row() {
Text(contactViewTitle(this.contactView)).fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
Blank()
AmIcon({
iconName: this.contactView === 'list' ? 'cardView' : 'listView',
iconSize: 18,
iconColor: Theme.textMuted
})
.width(40).height(40)
.onClick(() => { this.switchView(); })
/*
* 40×40 命中区 + 图标居中:尺寸加在**外层 Stack** 上,不加在 `AmIcon` 上。
* 内层容器固定 `iconSize`(18) 且靠左上 —— 直接链 `.width(40)` 会让
* 18vp 的图标贴在 40×40 命中区左上角(同 `LoginPage` 品牌卡、
* `AdminUsersPage` 刷新键,2026-09-18 一起修)。
*/
Stack({ alignContent: Alignment.Center }) {
AmIcon({
iconName: this.contactView === 'list' ? 'cardView' : 'listView',
iconSize: 18,
iconColor: Theme.textMuted
})
}
.width(40).height(40)
.onClick(() => { this.switchView(); })
}
.width('100%').height(56).padding({ left: 16, right: 8 })
.backgroundColor(Theme.surface)