跨端: 登录页那个"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

@ -157,3 +157,109 @@ test('ArkTS 词汇层硬坑:全仓 .ets 不许出现解构 / any / unknown /
}
assert.deepEqual(bad, [], `★ ArkTS 硬坑(编译不过):\n ${bad.join('\n ')}`);
});
/* ─────────────────── 图标:不许用 Unicode 符号,且尺寸不许直接加在 AmIcon 上 ─────────────────── */
/**
* 这两条都是 2026-09-18 用户**看着界面**报出来的,而且都不是「代码不合法」——
* 编译器与所有既有判据全绿,只有屏幕上是错的。
*/
test('★ 图标不许用 Unicode 符号充当(`Text(\'✉\')` 会被系统渲染成彩色 emoji)', () => {
/*
* 登录页原本写的是 `Text('✉')`(U+2709)当品牌图标。
*
* HarmonyOS 的字体链里有**彩色 emoji 字体**,U+2709 自带 emoji 字形 ⇒
* 它被渲染成一枚**彩色 emoji**(黄白色风信封)而不是单色图标:
* ① `.fontColor(Theme.accent)` 对彩色 emoji **无效**(界面显示的是 emoji 自带颜色);
* ② 与底栏/侧栏那些 `AmIcon` 线描图标不是同一套视觉语言。
* 实测截图硬证:大屏下那枚 emoji 比旁边的文字还显眼。
*
* 本仓**早就有** `ICON_PATHS.brandMark`(就是 App 图标那个信封),
* 所以这条纪律的成本是零:界面里所有图标都走 `AmIcon`。
*
* 判据只扫 `Text('…')` 里**单独一个**符号的情况 —— 正文里的标点/箭头不算
* (例如提示语里的「·」或「→」),否则会误伤大量正常文案。
*/
const ETS = join(ROOT, 'client', 'harmony', 'entry', 'src', 'main', 'ets');
/*
* 会被 emoji 字体接管的**典型图标类**符号:
* U+2600–U+27BF(Misc Symbols / Dingbats:☀ ☂ ✈ ✉ ✏ ✔ ❤ …)
* U+2B00–U+2BFF(杂项符号与箭头)
* U+FE0F(变体选择符,把字符变成 emoji 呈现)
* ★ 刻意**不含 U+2190–U+21FF(基本箭头)**:`→`/`←` 在正文里是**标点**,
* 不是图标 —— 把它们也扫进来会误伤大量正常文案。
* (我第一版把箭头段也框了进去,结果自检自己就先红了:
* `!EMOJI_ISH.test('→')` 不成立 —— 断言写错就是断言写错,不能靠放宽它来「修」。)
*/
const EMOJI_ISH = /^[\u2600-\u27BF\u2B00-\u2BFF\uFE0F]$/;
const hits = [];
const walkDir = (dir) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, e.name);
if (e.isDirectory()) { walkDir(full); continue; }
if (!/\.ets$/.test(e.name)) continue;
const src = code(full);
for (const m of src.matchAll(/Text\(\s*'([^']{1,3})'\s*\)/g)) {
const ch = m[1];
if (EMOJI_ISH.test(ch)) {
hits.push(`${e.name}: Text('${ch}')(U+${ch.codePointAt(0).toString(16).toUpperCase()})`);
}
}
}
};
walkDir(ETS);
assert.deepEqual(hits, [],
`用 Unicode 符号当图标会被系统渲染成 emoji,且不吃 fontColor:\n ${hits.join('\n ')}\n` +
'改用 AmIcon(图标表在 common/Icons.ets,品牌标是 brandMark)');
// 自检:探测器要真能认出这个形状(否则"没有命中"与"探测器坏了"结果一样)
assert.ok(EMOJI_ISH.test('\u2709') && EMOJI_ISH.test('\u2764'),
'探测器要认得出 U+2709 / U+2764 这类符号');
assert.ok(!EMOJI_ISH.test('·') && !EMOJI_ISH.test('→'),
'探测器不许把正文标点当成图标(会误伤大量文案)');
});
test('★ 尺寸/底色不许直接链在 `AmIcon` 上(内层容器固定 iconSize 且靠左上 ⇒ 图标贴左上角)', () => {
/*
* `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。用户原话:「你自己看看那个图标的位置正常吗」。
*
* 正确写法是套一层居中的容器(仓里回复球与悬浮加号本来就是这么做的):
* Stack({ alignContent: Alignment.Center }) { AmIcon({…}) }.width(48).height(48)
*
* 判据形状:同一表达式里,`AmIcon({…})` 之后**紧跟着** `.width(` / `.height(`
* 就算命中(中间只允许换行与空白)。套了 `Stack` 的写法中间隔着 `}`,
* 所以不会被误判。
*/
const ETS = join(ROOT, 'client', 'harmony', 'entry', 'src', 'main', 'ets');
const hits = [];
const walkDir = (dir) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, e.name);
if (e.isDirectory()) { walkDir(full); continue; }
if (!/\.ets$/.test(e.name)) continue;
const src = code(full);
/* `AmIcon({ ... })` 后面直接跟 `.width(` 或 `.height(` */
for (const m of src.matchAll(/AmIcon\(\{[\s\S]{0,200}?\}\)\s*\.\s*(width|height)\s*\(/g)) {
const line = src.slice(0, m.index).split('\n').length;
hits.push(`${e.name}:${line} → .${m[1]}(`);
}
}
};
walkDir(ETS);
assert.deepEqual(hits, [],
`尺寸直接加在 AmIcon 上会让图标贴左上角(内层容器固定 iconSize):\n ${hits.join('\n ')}\n` +
'改成 Stack({ alignContent: Alignment.Center }) { AmIcon({…}) }.width(N).height(N)');
// 自检:探测器必须认得这个形状,且不误伤正确的嵌套写法
const bad = "AmIcon({ iconName: 'x', iconSize: 24 }).width(48)";
const good = "Stack({ alignContent: Alignment.Center }) {\n AmIcon({ iconName: 'x', iconSize: 24 })\n}\n.width(48)";
const probe = (t) => [...t.matchAll(/AmIcon\(\{[\s\S]{0,200}?\}\)\s*\.\s*(width|height)\s*\(/g)].length;
assert.equal(probe(bad), 1, '探测器要认得出错误写法');
assert.equal(probe(good), 0, '探测器不许误伤套了 Stack 的正确写法');
});