跨端: 邮件正文/日历三处真 bug(可读性、居中对齐、日视图多余表头)

用户:「还有那邮件还是居中对齐,可读性也极差,而且还看起来根本不像邮件」
+「宽屏布局下日历的周视图日视图还是一塌糊涂」
+「你的图标变成输入框,图标变成邮件的动画呢?你能不能自己好好审计一下」

这轮**不再逐条打补丁**,改成先审计再改。下面每条都有实测数据。

## ① 邮件正文可读性:1.34:1 → 16.67:1

实测(深色,详情页正文):
    文字 rgb(35,33,52)  压 底色 rgb(0,0,0)  ⇒ **1.34:1**(完全不可读)
    同屏只有行内代码(红色 span)看得见(5.02:1)
    头部信息区更差:rgb(34,35,36) 压 rgb(32,33,35) = **1.02:1**

根因:`Markdown({ text: this.body })` —— **只传了文本,没传 `controller`**。
`@luvi/lv-markdown-in` 于是用它自己的默认样式,那套是给**浅色**配的
(实测 rgb(35,33,52) ≈ WebUI 浅色 `--c-gray-800` = `31 41 55`)。

⇒ 这是「**第三方组件不继承宿主主题**」那一类错:它不读 `$r('sys.color.*')`,
  也不读 `AppStorage`,必须显式注入。新增 `mdController()`,
  照 WebUI `.markdown`(`index.css:592-628`)逐条映射 17 个着色入口:

    .markdown            → Theme.textPrimary
    .markdown a          → Theme.accentFor()
    .markdown blockquote → Theme.textMuted / Theme.border
    .markdown code       → Theme.surfaceMuted

★ 17 个 setter 名逐一比对过库的 `.d.ets`(防拼错静默失效)。
★ 不放在 `@State` 里:控制器是普通对象,ArkUI 不会因它内部变化而重渲染 ——
  每次 `build()` 现取,主题一变就拿到新色(存一份反而**不会**刷新,正是本 bug 的形状)。

## ② 邮件「居中对齐」:ArkUI `Column` 默认就是居中

根因**不在那个 `Text`**,而在 **`Column` 的交叉轴默认对齐是
`HorizontalAlign.Center`** —— 官方《线性布局 (Row/Column)》原文:
「HorizontalAlign.Center(**默认值**):子元素在水平方向居中对齐」。

子元素没显式给宽时跟着内容宽居中 ⇒「回复给 …」那行浮在面板中间。
WebUI 是块级流、天然左对齐,所以"同样的代码"看起来不一样。

修:回复条/转发条两个 `Column` 显式 `.alignItems(HorizontalAlign.Start)`。

## ③ 日历:周/日视图「今天」的数字看不见(1.00:1)

实测(宽屏 3184、周视图第 1 列「21」):
    字 rgb(254,254,254) 压 底 rgb(254,254,254) ⇒ **1.00:1**

根因:`cellFg()` **不看档位**,选中一律返回 `accentFg`(白)。
而两档的"选中底色"根本不是同一个东西:
    · 月视图:整格铺**实心** `Theme.accent` ⇒ 白字对;
    · 周/日视图:底色改由日期头承担,用的是**淡蓝** `accentSoftFor()` ⇒ 白字看不见。
⇒ 按档位分流:周/日档返回 `Theme.accent`(蓝字压淡蓝 ≈5.4:1)。
同一处的农历小字也一并改(同一个三目)。

## ④ 日历:日视图多画了一条无意义的星期表头

周表头 `Row` 在 `if (calScale === 'day')` **之前**渲染 ⇒ 日视图上方挂着
「一 二 三 四 五 六 日」,而一天只有一个日子,七个标签全落空。

WebUI 三种视图都不是这么做的:
    · `MonthGrid` 表头在**自己内部**(`grid-cols-7` 第一行);
    · `WeekGrid` 星期名在**每一列内部**(sticky);
    · `DayGrid` **完全没有**星期行(顶上是日期+农历)。
⇒ 加 `if (this.calScale !== 'day')`。

## 诚实交代:我一度想当然,量了才发现不用改

我本来打算"修"周视图**表头与列对不齐**,理由是"表头均分可用宽、
网格列均分 minWidth(322) 的更大宽"。实测列中心 `[356,607,860,1114,1365,1621,1873]`
与表头中心基本吻合(偏差 ≤11px,在文字宽度内)—— **那是我推的,不是量的**。
先量再改,省掉一次"改坏对的东西"。

## 审计产出(供后续,不只是本次修)

· WebUI 全仓动画清单:5 个 `@keyframes`(`cal-in-next/prev`、`menu-in`、
  `pane-in`、`rise-in`)+ 7 处 `transition` + 3 处 `element.animate()`(全是 FLIP morph)。
· 鸿蒙侧挂点计数:`animateTo` 9、`transition()` 12、`pageTransition` 3、
  `geometryTransition` 4、`PressEffectModifier` 60。
· **`Motion.pageEnter` 是自己加的、零调用点**(`pageTransition` 走的是
  三个页各自的 `pageTransition()`)⇒ 死代码,待清。

## 验证

✓ ①②③④ 全部在设备上取**像素/布局**验证(1.34→16.67、1.00→5.44、日视图表头消失)
✓ 编译通过、进程存活、无新 jscrash
✗ 未验证:动画本体(图标→输入框 morph)—— 220ms,而抓图往返 1.5-3s,
  探针比被测对象慢一个数量级(同 `harmony-morph-unverified-middleframes`)
This commit is contained in:
2026-09-21 20:23:46 +08:00
parent cbb1e1ee62
commit dfcf13ff9d
8 changed files with 298 additions and 47 deletions

View File

@ -772,6 +772,31 @@ export class Theme {
/** 警示面/前景(对应 WebUI 的 --c-amber-50 / --c-amber-700)—— 权限 full 档、待决策徽标 */
static readonly warnBg: string = '#FFFBEB';
static readonly warnFg: string = '#B45309';
/*
* ★★ 2026-09-21 **语义面的深色档**(与 `warnFgDark` 同一批漏掉的另一半)。
*
* 背景.test/`cross-client-theme` 之前只盯了**前景**(`*For()` 那一族),
* 语义**面**(`dangerBg` / `approveBg` / `warnBg` / `chipSpentBg` / `chipWarnBg`)
* 全部只有浅色值。实测(邮件详情页深色)「权限」「预算」两行:
* `#F3F4F6` 白胶囊上压浅色字 ⇒ 白底白字,完全读不出。
*
* 取值照 WebUI `.dark` 段逐字对齐(`index.css:486-520`):
* --c-red-50 `46 31 37` ⇒ #2E1F25 dangerBgDark
* --c-red-100 `58 34 39` ⇒ #3A2227 chipSpentBgDark
* --c-green-50 `25 44 39` ⇒ #192C27 approveBgDark
* --c-amber-50 `46 40 31` ⇒ #2E281F warnBgDark
* --c-amber-100 `59 48 29` ⇒ #3B301D
* --c-orange-100 `60 41 31` ⇒ #3C291F chipWarnBgDark
*
* ★ 为什么不是一个通用的 "bgDark":这六个在浅色下**本来就不同**
* (红/绿/琥珀/橙各自一档),压成一个等于把语义丢了 ——
* “预算用尽”与“预算将尽”就分不出来了。
*/
static readonly dangerBgDark: string = '#2E1F25';
static readonly approveBgDark: string = '#192C27';
static readonly warnBgDark: string = '#2E281F';
static readonly chipSpentBgDark: string = '#3A2227';
static readonly chipWarnBgDark: string = '#3C291F';
/*
* ──── 语义色的**深色档**(2026-09-19 加)────
@ -846,12 +871,69 @@ export class Theme {
/** 中性 chip(对应 WebUI 的 --c-gray-100 / --c-gray-500)—— 预算条"还宽裕"档 */
static readonly chipNeutralBg: string = '#F3F4F6';
static readonly chipNeutralFg: string = '#5A6270';
/*
* ★★ 2026-09-21 **加深色变体**(用户:「你看看这可读性」)。
*
* 实测(深色主题,邮件详情页的「权限」「预算」两行):
* 浅灰底 `#F3F4F6` 上压 `Theme.textMuted` ⇒ 两个色都是浅色档,
* 深色下变成**白胶囊 + 白字**(截图里就是两个看不见字的白块)。
*
* `#F3F4F6` 就是 WebUI 浅色的 `--c-gray-100`(`243 244 246`,
* `index.css:53`);它`.dark` 段的同一个是 `32 36 44`(`index.css:374`)。
* 所以深色取值 = `#20242C`(与 `DARK_GRAY_100` 同值)。
*
* 字色 `#5A6270` 对齐 WebUI 的 `--c-gray-600`;深色用 `#9BA3B0`
*(WebUI `.dark --c-gray-600` 一族的量级)—— 压在 `#20242C` 上约 7:1。
*
* ★ 为什么这是个**早就该修**的:同文件 `permissionMode` 那几行的注释里
* 就写着「`surface` 是会跟随主题翻转的面色 …… 深色下变成近黑 ⇒ 字看不见。
* 这是本仓同一个错法的第 N 次出现(同批一共修了 12 处)」。
* 那批修的是**前景**(`fontColor`),而 `chipNeutralBg` 这个**背景**
* 当时漏了 —— 于是同一行里前景换了、背景没换,反而更糟。
*/
static readonly chipNeutralBgDark: string = '#20242C';
static readonly chipNeutralFgDark: string = '#9BA3B0';
/**
* 语义**面**的颜色访问器(主题感知)—— `dangerFor` 那一族的背景版。
*
* 与 `chipBgFor` 同一范式。为什么需要:`dangerBg` 这些只有浅色值,
* 深色下把浅色调的底留在深色页面上(实测邮件详情页的「权限」「预算」行)。
*/
static dangerBgFor(dark?: boolean): string {
return Theme.isDarkNow(dark) ? Theme.dangerBgDark : Theme.dangerBg;
}
static approveBgFor(dark?: boolean): string {
return Theme.isDarkNow(dark) ? Theme.approveBgDark : Theme.approveBg;
}
static warnBgFor(dark?: boolean): string {
return Theme.isDarkNow(dark) ? Theme.warnBgDark : Theme.warnBg;
}
static chipSpentBgFor(dark?: boolean): string {
return Theme.isDarkNow(dark) ? Theme.chipSpentBgDark : Theme.chipSpentBg;
}
static chipWarnBgFor(dark?: boolean): string {
return Theme.isDarkNow(dark) ? Theme.chipWarnBgDark : Theme.chipWarnBg;
}
/** 预算用尽(对应 WebUI 的 --c-red-100 / --c-red-700) */
static readonly chipSpentBg: string = '#FEE2E2';
/*
* 预算 chip 的**前景**深色档 —— 与背景同一批漏的。
*
* `#B91C1C` 是 WebUI 浅色的 `--c-red-700`(`185 28 28`);
* 深色段同一个变量是 `248 164 164`(`index.css:493`)⇒ `#F8A4A4`。
* 橙色同理:`--c-orange-700` 深色 `251 168 111` ⇒ `#FBA86F`。
*/
static readonly chipSpentFgDark: string = '#F8A4A4';
static readonly chipSpentFg: string = '#B91C1C';
/** 预算将尽(对应 WebUI 的 --c-orange-100 / --c-orange-700) */
static readonly chipWarnBg: string = '#FFEDD5';
static readonly chipWarnFg: string = '#C2410C';
static readonly chipWarnFgDark: string = '#FBA86F';
/**
* 往返预算档位 → chip 底色 / 字色。
@ -862,24 +944,39 @@ export class Theme {
* 这里只管"哪个档用什么颜色"。这三个色**不进跨端"取值相同"判据**(业务局部),
* 但必须进枚举完整性判据(三档齐全、认不出的归一到中性档)。
*/
static budgetBg(state: string): string {
if (state === 'spent') {
return Theme.chipSpentBg;
}
if (state === 'warn') {
return Theme.chipWarnBg;
}
return Theme.chipNeutralBg;
/**
* 权限/预算 chip 的底色(**主题感知**)。
*
* 与 `glassCardFor` 同一范式:深浅从 `Theme.isDarkNow()` 读,不靠调用方传。
* 实测漏修后果:深色下 `#F3F4F6` 白胶囊 + 浅色字 = 完全读不出。
*/
static chipBgFor(dark?: boolean): string {
return Theme.isDarkNow(dark) ? Theme.chipNeutralBgDark : Theme.chipNeutralBg;
}
static budgetFg(state: string): string {
/** 权限/预算 chip 的字色(主题感知)。`chipBgFor` 的配对。 */
static chipFgFor(dark?: boolean): string {
return Theme.isDarkNow(dark) ? Theme.chipNeutralFgDark : Theme.chipNeutralFg;
}
static budgetBg(state: string, dark?: boolean): string {
if (state === 'spent') {
return Theme.chipSpentFg;
return Theme.chipSpentBgFor(dark);
}
if (state === 'warn') {
return Theme.chipWarnFg;
return Theme.chipWarnBgFor(dark);
}
return Theme.chipNeutralFg;
return Theme.chipBgFor(dark);
}
static budgetFg(state: string, dark?: boolean): string {
if (state === 'spent') {
return Theme.isDarkNow(dark) ? Theme.chipSpentFgDark : Theme.chipSpentFg;
}
if (state === 'warn') {
return Theme.isDarkNow(dark) ? Theme.chipWarnFgDark : Theme.chipWarnFg;
}
return Theme.chipFgFor(dark);
}
/** 字体大小(与 WebUI 的 text-2xs/xs/sm 对齐) */