Commit Graph

30 Commits

Author SHA1 Message Date
dfcf13ff9d 跨端: 邮件正文/日历三处真 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`)
2026-09-21 20:23:46 +08:00
cbb1e1ee62 跨端: 修「深色下玻璃变灰纸板」+「主题选了不生效」(两个真 bug)
用户问「你的玻璃效果呢?」。我上手先取像素,结果是**两个一直存在的真 bug**,
不是观感偏好问题。两个都有实测数据。

## ① 深色下玻璃 alpha 没跟着翻 →「像深色卡片浮在灰纸板上」

### 实测

深色主题 + aurora 壁纸,同一行交替采样(`/tmp/g1.raw`,1008×2232):

    y=670   卡片缝隙 rgb(199,199,199)   卡片内 rgb(27,37,50)
    y=880   卡片缝隙 rgb(201,203,201)   卡片内 rgb(27,36,53)

缝隙是**浅灰 199**、比卡片还亮。而 199 恰好 = `0.78 × 255`
(壁纸层实测 x=4..24 为 rgb(0,1,3),近黑)—— 就是 `glassCardWall` 那层白纱。

### 根因:我把 WebUI 的一句话读漏了后半句

`Theme.glassCard` / `glassCardWall` 是**单一值、不分深浅**。我当初写的理由是
「基色恒为白,主题之间只差 alpha」——但 WebUI 原话是
「**基材恒为白**,主题之间**只差 alpha**」(`index.css:406`、`:1546`)。
我只抄了前半句,把「只差 alpha」误读成「alpha 也不用变」。

WebUI `.dark` 的实际取值:

    --glass-card-a:      0.06     ← 浅色 0.92
    --glass-card-wall-a: 0.04     ← 浅色 0.78

**深浅差 15 倍**:深色下白纱要几乎撤掉让深壁纸透上来,浅色下才用厚白纱盖亮壁纸。
我用同一个 0.92 配两套主题 ⇒ 深色下壁纸被糊成浅灰,**玻璃感与深色同时消失**。

### 修法

`Theme.glassCardFor(wall, dark?)` —— 与 `accentFor`/`textSubtleFor` 同一范式,
深浅从 `Theme.isDarkNow()`(`AppStorage` 那个发布键)读,不靠调用方自觉传。
新增 `glassCardDark #0FFFFFFF` / `glassCardWallDark #0AFFFFFF`。

改后同处实测:缝隙 rgb(3..16),壁纸透上来了;浅色档回归检查未变(244/239,厚白纱仍在)。

## ② 用户选的主题被无条件覆盖 →「选了深色但界面还是浅的」

### 实测

「我的」页选「深色」后:
· 按钮显示选中、服务端也记下 `theme:'dark'`(`GET /me/appearance` 确认);
· 但界面仍浅色,且文字浅色压浅底 —— 实测背景 `rgb(243,243,243)` /
  文字 `rgb(243,243,243)`,**对比度 ≈1.0:1,完全不可读**。

### 根因(两处叠加)

① `EntryAbility.onCreate` 有一句**无条件**的
   `setColorMode(COLOR_MODE_NOT_SET)`(= 跟随系统)—— 每次冷启都把用户的选择重置掉。
   它是目录迁移时抄进来的(`git log -S` 指向 `f9d757b chore: directory migration`),
   **没有注释说明为什么,也没有谁在用**。已删除(连带上游 import)。

② `MainPage.applyAppearance()` 算出了 `isDarkNow`、发布了 `AppStorage`,
   但**从来没调用过 `store.applyTheme(...)`** ⇒ 系统色彩模式根本没被设过。

### 修法

· 删掉 EntryAbility 那句无条件覆盖(附长注释说明为什么由页面负责);
· `applyAppearance()` 补上 `store.applyTheme(ctx, snap.theme)`。

★ **顺序**:`KEY_IS_DARK` 发布必须在 `applyTheme` **之前** ——
  因为 `glassCardFor()` 是从那个键读深浅的,反过来会让卡片用上一轮的深浅画一帧。
  `applyThemeNow()` 里同一处顺序也一并修正。

## 判据

`cross-client-theme` 的两条原本把 `Theme.glassCard`/`glassCardWall` 两个**字面量**钉死,
被我这次正确的改动撞红 —— 我没有放宽它们,而是改成钉**行为**:
· 卡片必须经 `glassCardFor(...)` 取色(入口存在);
· 四个令牌都要在 Theme 里存在;
· **深色档不得与浅色档同值**(同值 = 主题感知是假的)。

变异验证:① 深色档改回与浅色同值 → 3 条红;② 卡片改回不分深浅 → 5 条红;还原 → 21/21 绿。

## 验证状态

✓ 两个修复都在设备上取**像素**验证(不是看截图说"像了")
✓ `cross-client-theme` 21/21、全量套件 545 pass(3 红均为改动前既有:
  `harmony-admin` 两处 = 我上一轮 `SecuritySection` 拆分遗留、`build-stamp` = 产物戳过期)
✓ 编译通过、进程存活、无新 jscrash
2026-09-21 18:46:19 +08:00
e29f38b151 跨端: 页面转场 + 主题切换交叉淡出(用户:「加页面转场,因为是桌面应用了」+「深浅色切换也加动画」)
## ① 页面间转场(`pageTransition`)

用户要的是"因为它是桌面应用了"—— 桌面端的页面切换有转场是基本预期。

官方文档(`ts-page-transition-animation`)给了两条关键信息:
· 「当路由(**router**)进行切换时,可以通过在 `pageTransition` 函数中自定义
  页面入场和页面退场的转场动效」;
· 「为了实现更好的转场效果,**推荐使用 Navigation 组件和模态转场**」。

⇒ 所以**两套机制各归各位**,不混用:
  · 走 `router` 的独立页(管理页 / 邮件详情独立页 / 写信独立页)→ `pageTransition`;
  · 应用内 `Navigation` 跳转(邮件详情 / 写信,都走 `pushPath`)→ 已有
    `geometryTransition`(上一轮加的共享元素转场)。

数值照 WebUI `rise-in` 的关键帧(`index.css:1298-1307`)逐字对齐:
    from { opacity: 0; transform: translateY(4px) }
    to   { opacity: 1; transform: none }
⇒ 淡入 + 上浮 `Theme.riseInOffset`(4vp),时长 `Theme.durRise`(200)。

★ 退场**只淡出、不做位移**:两端都位移会看起来互相推挤。

## ② 主题切换的交叉淡出

这是**平台机制差异**,不是"懒得做":

· WebUI 切主题是**免费**的 —— CSS transition 挂在 `background-color` 上,
  主题换的是 CSS **变量**,于是每个元素各自补间(`index.css:1169` 给
  button/a/input/textarea/select 都挂了);
· ArkUI 的主题走 `app.setColorMode()`,而界面颜色是**系统资源**
  (`$r('sys.color.*')`)。`animateTo` 只能补间**数值属性** ——
  "把一个资源换成另一个资源"没有中间值 ⇒ 实测整屏同时跳变(闪一下)。

⇒ 自己造一个"旧屏淡出":
  ① 切主题**之前**用 `getComponentSnapshot().get(id)` 抓当前界面;
  ② 立刻切主题(底层已是新主题);
  ③ 把位图盖在最上层、`opacity` 1 → 0 补间 ⇒ "旧界面淡出、新界面透出"。

新增:
· `Motion.captureForThemeFade(ui, rootId)` —— 抓图,失败返回 `undefined`;
· `Theme.durThemeFade = 260`(整屏变化比单组件入场慢一点才不刺眼);
· `MainPage` 根 Stack 加 `.id(THEME_FADE_ROOT_ID)`(**常量**,两处拼错不报错、
  只表现为"动画静默不发生")+ `themeFadeImage`/`themeFadeOpacity` 两个状态
  + 最上层那层 `Image`;
· `toggleTheme` 拆成"带动画"与 `applyThemeNow`(**不含动画**)两条路,
  共用同一份状态变更 —— 抓图失败 / 系统关掉动画时直接切主题
  (**功能优先于观感**:不能因为动画失败而切不了主题)。

★ 两个必须做的收尾(都不是可选):
  · 动画结束后**清空 `themeFadeImage`** —— 留着是一张**整屏大小**的 PixelMap
    常驻内存,而且它盖在最上层会**吃掉所有触摸事件**(界面看着正常但点不动);
  · 那层加 `hitTestBehavior(HitTestMode.None)` —— 动画期间用户可能正好在点按钮。

★ 默认跟随系统:**本来就已经是**(`Appearance.theme` 初值 `'system'`,
  `AppearanceStore` 与 `Models` 两处都是),本轮未改,只是确认了。

## 验证状态(如实)

✓ 编译通过;设备上装包后进程存活、无新 jscrash
✗ **动画本体没有在设备上看到**:
  · 页面转场与主题淡出都是 200-260ms,而 `snapshot_display` 往返 1.5-3s
    ⇒ 探针比被测对象慢一个数量级(同 `harmony-morph-unverified-middleframes`);
  · 主题淡出还额外需要"已登录 + 找到主题开关",而我在清 preferences 之后
    登录流程没能用 `uitest uiInput` 走通(`inputText` 追加而非替换,
    把地址栏填成了 `https://exm/api/v1https://mail.jianfgit.`)。

⇒ 这两条的**结构与接线**有判据/编译保障,但"动起来好不好看"只能由真机确认。
   我不声称已验 —— 与既有的 morph 那条同一个诚实口径。
2026-09-21 17:36:40 +08:00
ba16230a63 跨端: 回复球 → 回复条 加共享元素转场(WebUI 的 FLIP「按钮长成面板」)
用户 2026-09-21:「我记得 webui 行为是按钮变成对应的写邮件页面或输入框吧,你做的啥?」

## WebUI 的真实行为(读它的实现,不是凭印象)

`MailView.tsx:990-1010` + `ComposePage.tsx:57-79` 做的是 **FLIP**
(First-Last-Invert-Play):

    const from = 球的 getBoundingClientRect();       // ① 起点
    const to   = 面板的 getBoundingClientRect();      // ② 终点
    const scale = Math.max(0.04, from.width / to.width);
    const dx = from.left + from.width/2 - (to.left + to.width/2);
    const dy = from.top  + from.height/2 - (to.top + to.height/2);
    el.animate([
      { transform:`translate(${dx}px,${dy}px) scale(${scale})`,
        opacity: 0.3, borderRadius: '28px' },        // ← 起点圆角 = 球的半径
      { transform:'none', opacity:1, borderRadius:'0px' }
    ], { duration: 220, easing: 'cubic-bezier(0.22, 0.61, 0.36, 1)' });

观感就是**球从自己的位置和半径"长"成一整幅面板**。用户说的
「按钮变成输入框」正是这一条 —— 而且它有**两处**:回复球→回复条、
以及写信 FAB→写信页。

★ 它还显式尊重无障碍:`prefers-reduced-motion` 时直接 return(不打动画)。

## 鸿蒙的对应能力:`geometryTransition`

官方文档(`ts-transition-animation-geometrytransition`)原文:

  · 「通过安排绑定的 in/out 组件的 frame、position,将视觉焦点由旧视图位置
    引导到新视图位置」—— 就是 FLIP 的系统实现;
  · 「**必须配合 `animateTo` 使用**才有动画效果,动效时长、曲线跟随
    `animateTo` 中的配置,**不支持 `animation` 动画**」;
  · 「同一个 id 只能有**两个**组件绑定,且分别作为 in(新视图)和
    out(旧视图)两种不同类型角色,**不能多个组件绑定同一个 id**」。

⇒ 三条约束逐条满足:
  · 两端绑同一个 id `reply-morph`(且**只有**这两处,判据可查);
  · 状态切换写在 `animateTo` 的**闭包内**(不是 `.animation()`);
  · 用默认 `follow: false`(两端是 `if/else` 互斥出现,不是"始终在树上")。

## 时长/曲线的取值(不是我拍的)

**220ms + `cubic-bezier(0.22, 0.61, 0.36, 1)`** —— 逐字取 WebUI 那行 FLIP 的参数。
那个贝塞尔恰好就是本仓 `Theme.easeRise` 的定义(`easeRise` 当初就是从这行抄来的),
所以代码里直接引它,没有新写一条曲线。

新增令牌 `Theme.durMorph = 220`,**没有**复用 `durRise`(200):两者是不同动效
(元素出入场 vs 共享元素续接),WebUI 那边也是两个值(150 / 220)——
合成一个的话以后想单独调其中一个就得先拆开,拆的时候必漏调用点。

## 设备验证(做了什么 / 没做成什么)

✓ 点球之后**回复条确实展开**(`dumpLayout` 命中「回复给 pi@root.realtest」
  与「输入回复内容」两处)——功能链路通。
✗ **没能在设备上看到中间帧**:`snapshot_display` 一次往返 ~1.5-3s,
  而这条动画 220ms ⇒ 探针比被测对象慢一个数量级。连拍 4 张
  y=1600 的白区跨度全是 `(30,1007)`(即每张都已是终态)。
  ★ 这正是此前"连测七八轮没有中间帧、并编出三个错误理论"的同一个坑
  (那次的结论记在本仓:**探针对被测变化不敏感时,量的是噪声**)。
  ⇒ 这一条的**动画本体**只能由用户在真机上看,我不声称已验。

顺带把「取消」键改成反向 morph(`closeReplyWithMorph()`)——
WebUI `MailView.tsx:945` 专门记过这个不对称:「打开有 morph、收起是瞬间消失 —— 不对称」。
2026-09-21 15:39:44 +08:00
125aec1191 跨端: 2in1 键盘可达(Ctrl+N 写信 / ↑↓ 换补全 / Enter 选中)+ 三处判据被实测改判
用户 2026-09-21:「接下来做一下 2in1 上的快捷键,比如快捷键打开发信页面」
「上下键切换发信目标」「回车展开输入框等」

## ① 打开发信页:Ctrl+N,用官方 `keyboardShortcut`

先查了官方文档再动手,避开两条静默失败的坑:

  · `keyboardShortcut`(组件快捷键事件,API 10+)「**无论组件是否获焦** ——
    只要窗口获焦,快捷键就会响应」。而 `onKeyEvent` 要求**组件先获焦**,
    邮件列表里焦点落在哪是不确定的(点一下就换)⇒ 用它做全局快捷键会时灵时不灵。
  · 「多个不同组件设置相同组合键 ⇒ 只响应节点树**深度最浅**的那个」。
    ⇒ 再给别处的"写信"按钮补一个 Ctrl+N 不是"多一个入口",而是**让后来那处永久失效**。
    判据因此钉"全仓只许绑一处"。

绑定位置在 `MainPage` 的**根 Stack**(窗口组件树的根)—— 绑在 FAB 上不行,
它在 `if` 分支里、窄屏/宽屏位置也不同,会随分支挂卸。

键位 `Ctrl+N` 避开了官方列出的五个**禁止绑定**组合(Alt+F4/Alt+Tab/Ctrl+Shift+Esc…)。
判据直接解析调用参数校验这三点。

## ② 根够不着 `openCompose()` ⇒ 照 `PushService` 的"两半"形状

`openCompose()` 住在**条件挂载**的 `CommPage` 上(`if (currentIndex === 0)`),
根组件拿不到它。新建 `common/ComposeIntent.ets`:

  · **格子**(`pending`)—— 用户此刻在别的页、`CommPage` 还没实例化;
  · **回调**(`listener`)—— 用户此刻就在通信页、页面早挂载完了。

缺任何一半都是**按键静默失效**:只有格子 ⇒ `aboutToAppear` 不重跑;
只有回调 ⇒ 没有监听者。这形状与 `api/PushService.ets` 处理"点通知跳转"时
踩的是同一个坑,那边注释里已写过解法 —— 这次是照着抄,不是重新踩。

## ③ 收件人三段式补全(↑↓ 切换、Enter/Tab 选中、Esc 收起)

WebUI 的逻辑原先**散在组件闭包里**(`parseParts` 没 export、`apply`/`onKeyDown`
直接改 React state)⇒ 判据根本 import 不到。先把它抽成一对纯逻辑:
`client/electron/src/lib/addressSuggest.ts` ↔ `model/AddressSuggest.ts`,
**抽的时候行为一字不改**(抽出来顺手"改进"会让判据比新行为、线上跑旧行为,两边都错)。
`AddressInput.tsx` 改接这份 lib。

`cross-client-logic` 新增 `AddressSuggest` 一对,22 个用例两边逐例比。
其中 `nextActiveIndex(0,3,-1)` 是**负下标陷阱**:JS 的 `%` 对负数返回负数
(`-1 % 5 === -1`),而负下标在数组访问里**不报错**(返回 `undefined`),
只表现为"按上键后没有任何一项高亮"。直接写 `(i-1) % n` 就会这样静默坏掉。

候选走**内联渲染**而不是 `bindPopup`/`bindMenu`:那两者各有焦点体系,
会先吃掉按键 ⇒ "↑↓ 换候选"落不到 `onToKey` 上。

## ④ 另修一个真 bug:`AddressSuggestion` 字段名整套写错

模型声明 `value`/`kind`,而服务端(`contacts.go:206`)给的是 `alias`/`title`/`source`
—— **从来没返回过** `value`/`kind`。按本仓纪律「声明了服务端从不返回的字段 ⇒ 删掉声明」改正。

顺带守住 `title` 的 `omitempty`:缺键时裸 cast 给 `undefined`(**不是**类里那个 `= ''`),
直接读会 `Cannot read property of undefined` —— 本仓在 `MailDetail.normalize()` 上
踩过同一形状(整页白屏)。判据钉住那句 `typeof … === 'string'` 的守。

## ⑤ 三处判据被**实测**改判(不是我挑一边,是拿数字定的)

### a. 卡片不该有模糊 —— 反转原断言
原判据断言「玻璃卡要走参数化 `backgroundEffect`」。那是**记录旧实现的副作用**
(重言式)。逐字读 WebUI 的 CSS:全仓 `backdrop-filter` **只有两处**
(`.glass-control` 8px/1.1、`.narrow-nav` 18px/1.5),而 `.glass-card`(`index.css:1629`)
**完全没有** —— 它的玻璃感是 `rgb(255 255 255 / 0.92)` 这个 alpha。
留着旧断言更坏:下次谁把卡片改成正确的"白 + alpha"会被判红,然后去**把模糊加回来**。

### b. 我试了"把模糊移到导航条",被实测否掉
推断「真归属是底栏」,于是把底栏改成 `backgroundEffect({radius:18, saturation:1.5})`。
实测(模拟器窄屏 1008×2232,底栏中心列 x=504):

    y      backgroundEffect        backgroundBlurStyle
    1960   rgb(191,199,209)        rgb(234,235,239)      ← 差 -36 亮度
    2060   rgb(206,211,219)        rgb(234,235,239)      ← 差 -24

⇒ `backgroundEffect` **只给模糊、不给底色**,壁纸原样透上来,底栏暗了 24~36 级。
WebUI 的 `.narrow-nav` 是**两条声明**组合的(`background-color` + `backdrop-filter`),
我只搬了后者。系统材质**同时含色调 + 模糊 + 深浅两套** ⇒ 在这里它才是正解
(§7.12 把它判成"有意差异"是对的,我的"改进"是退步)。
判据改成**反向钉住** `backgroundEffect`,并把这段实测数字留在 Theme.ets 里。

### c. 页签条判据记录的是被否掉的"胶囊"
原判据钉 `TAB_BAR_RADIUS`/`TAB_BAR_SIDE`/`TAB_BAR_TOP` —— 那正是用户否掉的形状
(「你又在内部套了一个胶囊」)。CDP 读 WebUI 的实测几何:

    .comm-pane   x=80 w=320 radius=14px overflow=hidden   ← 窗格,裁圆的是它
    tabstrip     x=80 w=320 radius=0px                    ← 条自己无圆角

设备实测(`uitest dumpLayout`):页签条 `[28,140][980,267]`、窗格 `[28,140][980,1957]`
⇒ 左右边缘逐像素相同。判据改为钉"与窗格齐平 + 只有左上角圆角"这两条**不变式**,
`TAB_BAR_RADIUS`/`TAB_BAR_SIDE` 一并**删除**(留着就是孤儿,会邀请人把胶囊拼回来)。

## ⑥ 顺带修两条判据自己的正则
`{6}` 看不见 8 位色 ⇒ 把 `glassCard`/`glassCardWall` 报成"清册过期",
**病因报错了**。改成 `{6}(?:[0-9A-Fa-f]{2})?`(不能写 `{6,8}`,那会连 7 位也放进来)。
半透明禁令改为**枚举白名单**(不是放宽:遮罩那个真实约束原样保留,
`overlay` 写成 8 位单色照样红)。

新增判据 `harmony-2in1.test.mjs` 12 条;`files=34 checks=543 pass=540 fail=2`(收尾前)。
2026-09-21 13:29:24 +08:00
f83c23362a 跨端: 修卡片投影/页签条圆角/日视图/周视图 — 五处对 WebUI 的误读
用户逐条指出后,用 CDP 读 WebUI 的 computed style 与祖先链,发现五处都是我读错/抄错。

## ① 列表底部那条"不知道什么玩意的阴影"(用户原话)

根因:我把 `instance.shadow(Theme.glassShadow)` 挂在 **每张卡片** 上。
WebUI 全仓 `box-shadow` 只有三处 —— `.app-shell > *`(窗格)、
`html[data-bg='on'] .app-shell > *`、`.narrow-nav`(底部导航条)。
**卡片 `.glass-card` 一次都没有**(整个规则块只有 radius/border/bg/transition)。

一屏十几张卡各投一次 ⇒ 叠加成灰雾。设备实测(x=600,卡片底边 y≈1847):
    有投影 y=1846→231、y=1852→230(向下衰减的暗带)
    去掉后 y=1846→255、y=1852→251(消失)
⇒ 投影移到 `PaneModifier`(窗格层),与 WebUI 一致。

★ 判定实验还排除了一个嫌疑:`List` 默认 `edgeEffect(EdgeEffect.Spring)`
  (官方文档「支持弹簧效果和**阴影效果**」)。设 `EdgeEffect.None` 后暗带照样在
  ⇒ 不是它。**弹簧是滚到边缘的正常反馈,不该为遮一个 bug 把它关掉。**

## ② 页签条右上角"那个圆角"(用户原话)

两道弧叠在一起。WebUI 实测(CDP 读祖先链):
    .comm-pane   x=80 w=320 radius=14px overflow=hidden   ← 窗格
      tabstrip   x=80 w=320 radius=0px                    ← 页签条
**两者横向完全齐平**,页签条自己直角、靠窗格裁圆 ⇒ 只有一道弧。
我们内缩 16vp + 自己带角 ⇒ 两道弧,中间一弯月牙。
⇒ 改成与窗格齐平、只留左上角圆角。

★ 期间我犯过两个错,都记在注释里:先把半径设成 `HEADER_HEIGHT/2`=22(条高也是
  44,那就是一个完整胶囊),后来自作主张把整条改成"通栏+下边框+无玻璃"——
  那是重做而不是用户要的小改,已还原。

## ③ 日视图与周视图一样(用户原话)

`DayTimeline()` 我写好了却**从未调用**。日视图一直走 `rows()`(周视图那套七列)。
⇒ 按 scale 分流:日视图 → 24 小时时间轴(对齐 WebUI `DayGrid` 的 `HOURS` 逐行、
  当前小时淡蓝底);周/日格子改用 `WEEK_CELL_HEIGHT`(240) 并画日程标题条
  (对齐 `EventChip`:`HH:mm` + 标题,停用的加删除线 + 灰底)。

## ④ 邮件正文垂直居中(用户原话)

官方 FAQ `faqs-arkui-725` 原文:内容比 `Scroll` 矮时「**默认居中排布**」,
要 `.align(Alignment.Top)` 才是顶部排布。
实测内容块在 `Scroll` 里偏移 731px = (1944−481)/2 —— 正好居中。
⇒ 4 处 Scroll/List 补 `.align(Alignment.Top)`(详情页 y 从 1018 → 287)。

## ⑤ 窄屏圆角/留白全丢(用户:「你的邮件的圆角呢?日历的圆角呢?」)

`borderRadius(this.isWide ? glassRadius : 0)` —— 窄屏恒 0;
`left/right: isWide ? paneGap : 0` —— 把 WebUI「窄屏**底边**留白为 0」
错推广成「四边都为 0」。
WebUI 两条 shell 规则**都给圆角**,差的只是底边留白。
⇒ 圆角两端都开;左右留白两端都给;只有底边分宽窄。

## 连带修的两个真问题

· **底部导航避让**(用户:「为什么不避让底部导航栏」):根是 `NavBar` 与内容
  是 `Stack` 里的**并列兄弟**(互相重叠),所以每个滚动容器都得手写
  `contentEndOffset` —— 我漏了 8 处。照 WebUI 改成 `Column{内容, NavBar}`
  (WebUI 实测 `scroller.bottom=861` / `nav.top=871`,内容停在导航上方)。
· **不透明的那一张卡**(用户:「有一个完全不透明的邮箱项」):`MailRow` 手写
  `Theme.surface`(系统卡片色 α=1),而 `GroupHeader` 用的是 `GlassCardModifier`。
  ⇒ 改用同一件基础件 + 新增 `TintModifier` 合成状态色(`.attributeModifier`
  是单一插槽,色必须参与合成)。实测 `(255,255,255)` → `(195,218,204)`。

判据随之反转 3 条(`harmony-widescreen` 两处把"窄屏 0"改成"两端都要"、
`cross-client-theme` 抓到我新写的 `Theme.border` 当 `fontColor`)。
2026-09-21 12:04:52 +08:00
6f1b4352cd 跨端: 回复/转发改内联底栏 + 入场动画对称化(照鸿蒙文档纠正三处误判)
用户:「点击回复按键与新建邮件部分的动画与 webui 不一致,动画不符合鸿蒙视觉
要求」。两个问题是分开的:结构是覆盖式弹层 vs WebUI 的内联底栏;动画则是我
单方面发明的不对称过渡 + 150ms 低于鸿蒙规范下限。

## 结构:覆盖式弹层 → 底部内联条(回复 / 转发)

WebUI `MailView.tsx:410/1057` 的 `ReplyBar`/`ForwardBar` 是 `border-t` 分出的
**内联底栏**,与正文并列(正文 `flex-1 overflow-y-auto` 保持可见可滚),高度由
内容决定。我们原先是整屏遮罩 + `height('60%')` + `position({x:0,y:0})`。

三条用户可感知的差异:弹层盖住正文(写回复时看不到原文)/固定 60% 高(写一行
也占半屏)/遮罩整屏变暗。结构不用动外层 —— 原版那两处本来就是正文 Stack 的
**兄弟**(同在 `Column` 里 ⇒ 本来竖直排列),错只错在给条加了遮罩/定高/绝对定位。

## 动画

① `paneRiseIn()` / `calendarSlide()` 去 `asymmetric`,改**对称**。
   WebUI 是 `animation: rise-in 150ms … both` —— `both` 就是进出同一条关键帧。
   我原先让出场只做 `opacity` 且更短(120ms),"出现时浮上来、消失时只淡出",
   正是"与 webui 不一致"的来源。当初写不对称的理由(换窗格时两层同时半透明会
   "闪")只对**换窗格**成立,对回复框/转发条不成立 —— 我把两种场景混用了。

② `durRise` 150 → **200ms**(用户选定"折中")。WebUI 是 150(web 常规档),
   鸿蒙官方「元素淡入/位移进入」建议 **200-300ms**,150 比下限还低 25%。

## 照文档纠正三处误判(本轮的真正收获)

我为了搞清"为什么动画不播",先后编出过三个错误理论,读文档后逐条推翻:

① **不是 "NavDestination 吃掉子组件的 `.transition()`"**。
   实测:`ComposeView` 根上的 `.transition()` 一直在播。我之所以连测七八轮都报
   "没有中间帧",是因为**拿平均亮度当探针** —— 白底窗格 50% 透明叠在浅色背景上
   平均亮度几乎不变。换成**位移**探针后,立刻看到"整栏下移 300vp 且半透明"的
   中间帧。教训:**探针对被测变化不敏感时,量的是噪声**。

② **不 `customTransition` 也能做**。`NavDestination` 确实有 `customTransition`
   (API 15+),但它是**整页转场**,我们要的只是内容块的一次上浮淡入。
   (顺带记一条:`NavDestinationTransition.curve` 的类型是枚举 `Curve`,
   不收 `ICurve` —— 试过用 `curves.cubicBezierCurve` 会编译报错。)

③ **`.opacity()` 在 `NavDestination` 上是生效的**。先前判定"不生效"同样是那个
   废探针害的;换 `opacity(0)` 二元判定后整页消失,证明它一直生效。

## 连带修一个真 bug(判据抓的)

回复/转发改成内联后**失去了"弹层有固定高度"这层键盘保护** —— 官方默认
`KeyboardAvoidMode.OFFSET`(整页上移)会把贴底的「取消/发送/转发」顶出屏幕。
在 `EntryAbility` 里显式设 `RESIZE`(按剩余高度重排)。坑:`@kit.ArkUI` 与全局
作用域各有一个同名 `KeyboardAvoidMode`,**只有前者有 `RESIZE`**。

## 判据(4 条红全部结算,逐条说明为什么不是放宽)

· `harmony-nav` durRise:从"逐字等于 150"改为**区间 200-300**(钉住用户裁定,
  退回 150 与写 800 都红,已变异验证)。
· `harmony-nav` asymmetric:**反转**为"不得 asymmetric"(旧断言把上一版设计锁住,
  而 WebUI 本来就是对称的)。
· `harmony-admin` 弹层高度:原断言数的形状只属于废弃的覆盖式弹层 → 改为
  **新结构下的等价不变式**(键盘避让必须 RESIZE)。这条判据当年抓的是真 bug,
  该 bug 换了形态仍在,所以不能简单删。
· `cross-client-theme`:删掉我中途废弃留下的孤儿令牌 `riseCurveEnum`。

新增 4 条变异条目(全部 `红✓`);`mutants=52 ran=52 skipped=0`。
`files=33 checks=530 pass=530 fail=0`,`baseline=7/7✓`。
设备已验:回复/转发确为内联底栏(正文可见、`border-t` 分隔)。
2026-09-21 02:19:30 +08:00
d9bb4f766b 跨端: 三条判据转绿(登记新令牌 + 把一条断言错实现的判据改成断言真不变式)
上一步(写邮件右栏)之后整套跑红 3 条,逐个查清:

══ ① cross-client-theme —— 判据是对的,我漏登记

新加的 `accentEdge` / `accentEdgeDark` 是**手写色**,而这个仓有一条硬规矩:
`Theme.ets` 里每个 `static readonly X: string = '#……'` 都必须在
`SELF_OWNED_COLORS` 名单里 + 在文件内的「手写色登记表」注释里写一行理由。
(这条规矩本身就是为防"新写死一个色悄悄溜过去"而立的 —— 第二处断言
 会检查名单里没有化石名,所以名单不能只加不改。)

按规矩补两处:
· 名单里登记,并写清它对齐的是 WebUI 的哪个 token
  (`MailList.tsx:280` 的 `border-blue-200`,与 `bg-blue-50` 成对)
· `Theme.ets` 登记表里写一行理由 —— 重点记下**为什么"选中"必须两个 token**:
  选中与未读的底色相同(都是 `accentSoft`),只靠底色的话
  "选中一封未读邮件"看不出任何变化,那圈边才是信息。
  并把表头计数 24 → 26 同步改掉。

══ ② harmony-widescreen —— 判据错了,它断言的是一个 WebUI 明令禁止的实现

原断言:`assert.match(code_, /clip\(this\.isWide\)/, '面板内容要被圆角裁剪')`
—— 它把"圆角"和"裁切"当成**同一件事**。而 WebUI `index.css:968`
在**同一个规则块**里就写明了这条教训:

    .app-shell > * {
      border-radius: var(--radius-card);
      // ★ 这里**不能**写 overflow: hidden(2026-09-14 用户:
      //   「通信页面完全无法上下滑动」)。
      // 面板自己就是滚动容器,而这条规则的特异性比 Tailwind 的 .overflow-y-auto
      // 高 ⇒ 滚动被静默干掉:实测当时**一个可滚动容器都不存在**(scrollerCount=0)。
      // 圆角仍然生效(border-radius 不影响滚动)

鸿蒙这边同一个形状:那层是内容列的根、`Navigation` 的 `List` 在它里面,
写了 `.clip(this.isWide)` 就是 `List` 节点还在(高 1750px)但**滚不动**
(实测 fling 后第一封仍在 y=526)——这正是用户当时报的"列表没法滚动"。

⇒ 判据改成断言**真正的不变式**:宽屏「圆角开(14)+ 面来自设计令牌 +
**不写** `.clip(this.isWide)`」。原来那句测的是"某句实现写着没写着",
而且那个实现是错的 —— 判据跟着错误实现一起钉住了 bug。

══ ③ build-stamp —— 判据要求重构建,照做(**没有**去改 BUILD_INFO.json)

产物记的是 `c0ab3f5`,HEAD 已是新提交。这条判据自己的文本写得很清楚:
> **别去改 BUILD_INFO.json 里的 gitRev / srcHash 了事** —— 那是把这条判据废掉。
> 正确修法只有一个:重跑构建。

所以 `npm run build` 重跑(rev 0)。注意 srcHash 本来就没变
(`6d1195a4008ebca7`)—— 因为 TRACKED 只扫 electron 侧(`src`/`index.html`/
`vite.config.ts`/…),我这几步改的全是 harmony;**变的只有 gitRev**。
这正说明这条判据在比对"产物来自哪个提交",而不是"文件内容有没有动"。

★ 三条各自的性质不同,值得分开记:① 是判据对、我漏登记(补);
② 是判据错、钉住了一个反向实现(改判据);③ 是判据对且给了唯一正确修法(照办)。
2026-09-20 13:05:59 +08:00
44e277e0b6 跨端: 补「当前选中邮件」高亮 —— 鸿蒙原来完全没有这个概念
用户:「你自己看看跟 webui 相比,观感真的差很多」。逐项比对后找到的**行为缺口**
(不是配色问题,是少了一个状态)。

WebUI 一直有:`MailList.tsx:104/116/229` 把 `currentMail?.mail_id` 传进卡片,
卡片据此上 `active ? 'bg-blue-50 border-blue-200'`。
鸿蒙**一个都没有** —— 点开一封邮件后,左侧列表那一行和旁边几行长得一模一样:
你不知道自己正在读哪一封、读完了该往哪回。

修法:
· `CommPage` 拥有 `@State currentMailId`,`openMail()` 里记下,以 `@Prop` 下发给
  `InboxTab`/`SentTab`。**单点写、多点读** —— 两个 tab 各存一份必然会分叉
  (收件箱和发件箱都能触发同一个动作)。
· 存 `mail_id` 而非索引/组键:SSE 会让列表重排,索引会错位。
· 三级优先 **选中 > 未读 > 普通**。★ 这里有个必须成对的理由:
  选中与未读底色**相同**(都是 `accentSoft`),只靠底色的话
  "选中一封未读邮件"看不出任何变化 ⇒ 选中必须额外加那圈边。
  这正是 WebUI 两个 token 并存的原因,不是随手加的边框。
· 新增 `Theme.accentEdge/accentEdgeDark/accentEdgeFor()`(= tailwind blue-200
  及其深色值)——我第一版只搬了底、忘了边,选中态淡到几乎看不见。

★ 顺带记一个工具坑:`devecocli ui tap` 在这台模拟器上**点了不生效**
(截图前后一样、布局树无变化),而 `hdc shell "uitest uiInput click X Y"`
有效。以后点不动就先换这条,别以为是代码没生效 —— 我为此白跑了两轮。
2026-09-20 12:24:17 +08:00
eb4e413c67 跨端: 玻璃走基础组件(透明度/磨砂/投影)+ 判据去芜存菁(修 4 个判据自身缺陷)
用户两条:「玻璃不透明/不够磨砂炫酷,要更基础组件化」+「判据去掉无意义的、保留有用的」。

★ 玻璃的透明度与质感(逐层调出来的,不是拍的)
  · 透明度:`BlurStyle` 是固定档、没有 saturate 旋钮 ⇒ 观感偏灰。
    改用参数化 `backgroundEffect({ radius, saturation })`,字段与 CSS `backdrop-filter`
    一一对应,于是可以**照抄 WebUI 的数**:`blur(18px) saturate(1.5)`
    (WebUI `.narrow-nav`,那里最"玻璃"的一档)。**saturate 才是玻璃感的主要来源**。
  · 质感:只模糊+饱和仍然"像一块平的白方块" —— 玻璃板之所以像板靠的是**边缘**。
    WebUI 的完整配方是 `box-shadow: 0 8px 28px rgb(0 0 0 / 0.16)` + 发丝描边。
    只有模糊没有投影 ⇒ 一片雾;加上投影 ⇒ 一块板。**这是"不够炫酷"的主因**
    (我先前两个方向都调了模糊与饱和,方向就偏了)。
  · 投影用**系统预置档** `ShadowStyle.OUTER_FLOATING_SM`("浮起的小面板"),
    不是手写 `{radius, color, offsetY}` —— 手写色要么是 `#AARRGGBB`、
    要么是 `rgba()`,两者都被判据明令禁止,而那条禁令**是对的**:
    手写阴影色在深色下看不见(深底叠深影 = 无变化),WebUI 在 `.dark` 里
    要把同一处从 0.16 加重到 0.55 —— 那正是"替系统猜深浅"。

★ 基础组件(用户:「定义一个基础玻璃容器给各个组件引用」)
  · `GlassCardModifier` / `PaneModifier`(`AttributeModifier<CommonAttribute>`):
    **一处定义、处处引用**。页面里的玻璃从 15 处内联三连收敛成
    `.attributeModifier(...of(this.bgActive))`。
    ★ 为什么不是 `@Styles`:全局 `@Styles` 读不到 `this`(而玻璃要知道 `bgActive`);
      成员级 `@Styles` 能读 `this` 但**每个 struct 各一份**,MainPage 有 7 个 struct
      ⇒ 只是把"每处抄 3 行"换成"每 struct 抄 1 份",没解决问题。
  · `Motion.dur()`:接系统「减弱动态效果」(`isAnimationReduceEnabledSync()`,API 23)。
    WebUI 有 `prefers-reduced-motion` 硬约束,鸿蒙这边**改造前一次都没调过**。

★ 途中撞出并修掉的**真 bug**(都是我自己的)
  · 把 `@Styles glassCard()` 里也替换成 `attributeModifier` ⇒
    「我的」页 ProfileSection/AppearanceSection/PushSection **整块不渲染**
    (设备实测:Scroll 直接跳到「客户端连接密钥」)。删掉那个死 `@Styles` 即恢复。
  · `Theme.shadowColor/hairline` 用了 `rgba()` + `#AARRGGBB` ⇒ 违反本仓两条既有禁令
    ("鸿蒙侧不写 CSS 颜色函数"、"半透明属于系统材质/职责")。改用系统阴影档。

★ 判据去芜存菁:修了 4 个**判据自身的缺陷**(都是它声称在管、实际漏判/误判)
  · 材质来源只扫 `backgroundBlurStyle` —— 玻璃改成 `backgroundEffect` 后**整类漏判**。
    实测:往 Modifier 里塞 `backgroundEffect({radius:99,saturation:9.9})` 照样全绿。
    ⇒ 补上这一类,并加"参数必须引 Theme 令牌"(否则只是换个旋钮写死)。
  · 参数抽取用 `[^)]*` —— 对 `this.materialOf()` 与 `{...}` 对象字面量都提前截断,
    **误伤合法写法**(本次被它误红两次)。改成配对括号 + 去外层花括号。
  · 嵌套判定**没比文件**:拿 A 文件的字符偏移去比 B 文件 ⇒ 判出
    "MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。
    假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
  · `harmony-appearance` 数的是**内联字面量**("三元出现 ≥5 次"),
    收敛成组件后写法变了、行为没变却变红 ⇒ 改成判不变式
    (让位只有一条路径 + 每个窗格都收到开关)。
  · 顺带:设备判据找「背景」时只在首屏可见节点里找(而 dumpLayout 只给可见节点),
    且我的第一版只朝一个方向滑(越滑越远)⇒ 改成"先回顶、再逐屏向下扫"。

判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;baseline 7/7✓(第 7 次,逐个取证)。
2026-09-20 08:16:10 +08:00
5103e0aee3 跨端: 抽出基础面/玻璃组件(Motion + Surface)+ 三处硬弹的弹层补过渡
用户两条要求,各自都指向"决策被复制到各处"这个病根:
  ① 「应该定义一个基础玻璃容器给各个组件引用」
  ② 「很多页面还是不够精致……封装为基础组件供所有页面使用。基础组件包含动画」

★ 玻璃不透明的真根因(前几轮都没找到,这次是像素证据定的)
  实测模拟器 3184×2232、壁纸 aurora + 压暗 37:
      Navigation [229,140,3156,2204]   ← 255,255,255(整块内容区)
      y=2210(Navigation 之外那条缝)    ← 226,225,235(壁纸清楚可见)
  那条缝是决定性的:**壁纸层本身是好的**,白是因为上面盖了不透明的壳。
  链路上一共三层,逐层修:
    · `Navigation` 外壳:不设背景 ⇒ 系统默认不透明白,把里面已透明的窗格整片盖住;
    · navBar 内容层(列表栏根容器):同样没有背景;
    · **卡片**:铺的是 `Theme.surface`(实体系统色)。
  修后:缝隙 203,213,228 / 卡片 191,204,220 —— 壁纸透得出来。

★ 新增基础组件(common/)
  · `Surface.ets`  —— `GlassPane`(正文面)/ `GlassCard`(嵌套面)/ `PageHeader` / `Pressable`
  · `Motion.ets`   —— `Motion.dur()`:接系统「减弱动态效果」
    (`accessibility.isAnimationReduceEnabledSync()`,API 23 正好够用)。
    WebUI 侧有 `prefers-reduced-motion` 硬约束(index.css:1266),
    鸿蒙这边**改造前一次都没调过** —— 系统里关了动画,我们照样动。这是无障碍义务。
  · `Theme.cardMaterial` —— 卡片材质令牌。不是拍脑袋选的:
    `COMPONENT_THIN` 实测卡片 252,254,254(吃掉 94% 壁纸,就是用户看到的"白卡");
    `BACKGROUND_THIN` → 185,190,201,壁纸透得出来且文字对比度仍够。

★ 判定形状按"玻璃从例外变成默认"重写(判据 C 条)
  原来是"允许出现玻璃的位置"白名单,逐处登记。那个形状在"玻璃是少数例外"时成立,
  但用户要的是**全部玻璃化** ⇒ 白名单退化成"把每处抄一遍",且挡不住新写的裸枚举。
  改成**规则**:材质档次只能来自 `Theme.navMaterial` / `Theme.cardMaterial`
  (或 `BlurStyle.NONE`),页面里出现裸 `BlurStyle.XXX` 即红;
  辅助方法(`this.materialOf()`)体内也查,否则等于开了后门。两条变异都验证咬得住。

★ 判据 C 条自身的两个 bug(都被本次触发)
  · 嵌套判定**没比文件**:`c.at`/`o.at` 是各文件自己的字符下标,直接比大小
    于是判出"MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。
    假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
  · 参数抽取用 `[^)]*`:`this.materialOf()` 里含一层 `()`,在内层括号处截断,
    把合法写法读成 `this.materialOf(` 判红。改成配对括号抽取。

★ 出现/消失动画:用户点名的三处硬弹
  这些挂在 `if (cond)` 上,条件一变整块出现/消失,原来一帧过渡都没有 ——
  而代码上"看着像挂了动画"(外层页面有 transition),所以最容易漏。
  · `MainPage` 账号选择器下拉 → `menuIn()`(从上往下落的浮层档)
  · `AdminUsersPage` 新建用户表单 → `paneRiseIn()`(就地展开档)
    ★ 过渡必须挂在**调用点的 Column**:`@Builder` 返回 void,链不上修饰符。
      第一版写进了 `CreateForm()` 内部,是新判据抓出来的。
  · `SettingsPage` 新增账号弹层 → `paneRiseIn()`(与回复/转发弹层同一挂法)
  新增判据「出现/消失的那类元素真的挂了过渡」:**枚举所有条件挂载的浮层/展开块**
  (不是数数 —— 数数挡不住"新增第四处又漏了"),变异验证过。

★ 顺手修的两处真嵌套玻璃
  `SettingsPage` 的账号列表与密钥列表都是"卡里面的一段列表",两层都铺材质 =
  WebUI 用 `.bg-white .bg-white { backdrop-filter: none }` 明确禁止的嵌套
  (alpha 相乘 0.82×0.82=0.97 把壁纸吃光)。去掉内层材质,玻璃只留一层。

★ 判据放宽一处(不是放水)
  `harmony-contacts` 那句写死 `pushUrl`,判的是"tryRestore 有没有跳转",
  与用哪个原语无关。修 LoginPage 返回栈时改成 `replaceUrl` 后它误报了;
  改成 `(pushUrl|replaceUrl)`,原语该用哪个由 `harmony-nav` 那条专门判。

判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;
baseline 7/7✓(第 6 次重算,两个文件逐个 `git diff --quiet HEAD` 取证为有意编辑)。
2026-09-20 07:37:16 +08:00
110de79401 跨端: 三级文字**浅色也一样不够**(12 处 2.85:1)+ 判据不再只验一半
## 一、这是"我自己推的理由"被实测推翻

上一条我把 `textSubtleFor()` 写成**只改深色**,理由是:

> 「浅色下这些令牌本来就是为浅底配的,量它们没有信息量」

**那个理由是错的。** 切到浅色主题复扫,立刻抓到 12 处:

```
"用户名 / 显示名 / 角色 / 状态 / 创建时间 / 最后登录"  2.85:1
"已同步" / "37%" / "3px"(外观区)                    2.85:1
"这一天没有日程"                                      2.85:1
```
`ink rgb(153,153,153)` 压白底 —— 全是 `textSubtle`。

对照 WebUI 浅色(`index.css` `:root`):
- `gray-400` = `107 114 128` = **4.83:1**
- `gray-500` = `90 98 112`  = **6.15:1**

⇒ 系统三级色**两个主题下都不够**。它的语义只是"比二级更淡",
**不保证任何可读性下限**;WebUI 那边两套主题都做过适配,这边一档都没有。

修:`textSubtleFor()` 两个主题都返回 `textMuted`
(系统二级色,语义就是"次要文字",且**跟随主题** ——
不必再维护两个手写常量、不必登记)。

## 二、判据从"只验深色"改成"两个主题都验"

原来那条设备判据**在浅色下直接 skip**,skip 的理由正是上面那句错话。
现在两个主题都扫,断言消息里带上主题名(`深色主题下…` / `浅色主题下…`)。

★ 纪律:**色令牌的可读性是两个主题各自的事,不能只验一半。**
  "另一半没问题"若没量过,就只是推测。

**变异验证**:把 `textSubtleFor()` 改回"两主题都用 textSubtle"
(此时设备正在浅色主题)⇒ **判红**;还原 ⇒ 绿。

## 三、设备复扫(浅色主题)

```
[通信]   扫 41 段,低对比 0
[日历]   扫 84 段,低对比 0
[联系人] 扫 50 段,低对比 0
[我的]   扫 40 段,低对比 0
```
**215 段文字全部 ≥3:1。**

两个主题合起来:深色 219 段 + 浅色 215 段,全绿。

## 四、顺手改掉的

测试名从「深色下短文本必须都读得动」改成「**当前主题**下…」
—— 名字得跟着它实际判的东西走(否则下一个人会以为它只管深色)。

`run-all.mjs` → `checks=515 pass=515 fail=0 skip=0 red=0 broken=0 unreported=0`。

**当前设备主题**:浅色(本轮为验另一半切过去的;服务端 `theme=light`)。
2026-09-19 20:58:59 +08:00
4ecddfc2b6 跨端: 语义色/三级文字在深色下没提亮(设备扫出 12 处)+ 判据取样法修到第三版
## 一、判据基建先修对,否则是在听噪声

深色可读性扫描的**取样法迭代了三版**,每版都因为**假红**才改的
(记在这里,因为"判据自己错"比"界面错"更费时间):

1. **取中心一个像素** ⇒ 落在笔画之间、读到的是底色 ⇒ 一片 ratio=1.00 假红。
2. **取全局最暗/最亮** ⇒ 会采到**两个不同的东西**:细字的抗锯齿中间色。
   实例:segmented「日」报 ink `rgb(32,34,35)` / bg `rgb(98,100,101)` 2.69:1 ——
   而截图里它要么蓝底白字、要么深底浅字,两种都远高于 3:1。
   **反复核对才确认是判据的错,不是界面的错。**
3. **步长采样 + 众数** ⇒ 仍然假红:周表头「一」报 2.03:1,
   而逐像素的真实墨是 `rgb(166,167,167)`(**6.6:1**)——
   稀疏网格**整个跳过了笔画**。这不是调参能修好的(字越小越糟)。
4. **逐像素 + 众数=底 + 离底最远者=墨**(最终版)。
   理由:一个文字框里**面积最大的一定是底**,而"离底色最远"就是笔画最实那部分。
   ⚠️ 逐像素是**负担得起的**(整屏已解码在内存,一个框最多 9 万像素)——
   **不要再为了省开销退回抽样**,前两版都因此假红。

## 二、真 bug 三批(都是"深色下没提亮")

### ① 语义色前景(12 处 → 修完设备复扫 0)
WebUI 的语义色也是**双通道**,`.dark` 段把 `red/green/amber-700` 换成浅色
(`248/118/249 → 164/219/195` 那组)。鸿蒙只有一个深色值:
- 「归档」按钮 `Theme.danger` #B91C1C 压深面 ⇒ **2.47:1**(联系人页 7 处)
- 「今天」/「日」等 ⇒ **2.38–2.77:1**

加 `dangerDark/approveDark/warnFgDark`(逐字对齐 WebUI `.dark`)
+ `dangerFor()/approveFor()/warnFgFor()` 入口,22 处接入。

### ② 日历工具条整块配色抄错(不是"深色档选错")
对照 WebUI `CalendarView.tsx:313-331`:

| 元素 | WebUI | 鸿蒙(改前) |
|---|---|---|
| 「今天」 | `border-gray-300` + `text-gray-700`(**中性**) | `accentSoft` 底 + `accentStrong` 字 |
| 月/周/日 未选中 | `bg-white` + `text-gray-700` | `accentSoft` 底 |

鸿蒙把**次要的中性按钮**画成了**品牌色按钮** —— 视觉重量跟主操作一样,
三个档看起来"都像选中"。已按 WebUI 改成中性。

### ③ 三级文字在深色下 2.82:1(**71 处**,全局最大的一个)
WebUI 的 `.dark` 段注释逐字写着这个坑:

> 「`gray-400/500`(次要文字)→ **提亮**。深底上的浅色 gray-400
>  只有约 2:1 对比度,远低于 WCAG AA 的 4.5:1 —— **看得见但读不动**。」

鸿蒙用的系统三级色 `ohos_id_color_text_tertiary` 深色下是 `rgb(102,103,105)`
压深面 **2.82–3.05:1**,而它被用在 10–11px 的**小字**上
(时间戳、农历、空态、辅助说明)。

**不换掉系统令牌**("系统拥有的维度"纪律照旧),加 `textSubtleFor()`:
深色下改用二级色(实测 `rgb(166,167,167)` ≈ 7.15:1,
且与 WebUI 的 `gray-500` 同档位)。71 处接入。

**设备复扫(修后)**:通信/日历/联系三页 **176 段文字全部 ≥3:1**。

## 三、欠账与底本

- `baseline.sha` 第 4 次重算。仍**先逐个取证**:
  `git diff --quiet HEAD -- <f> && echo STALE || echo INTENTIONAL`
  ⇒ 三个全是 `INTENTIONAL`(我改的),不是变异残留。
  把这条取证命令也写进文件,下次照抄。
- `dangerDark/approveDark/warnFgDark` 已登记进 `SELF_OWNED_COLORS`
  + `Theme.ets` 登记表(各带理由与 WebUI 出处)。

`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。

**未验**:①「我的」页(底部头像按钮,扫描脚本没找到入口)没扫;
② 浅色下的对比度没扫(本轮全部针对深色)。
2026-09-19 20:27:09 +08:00
13b557a742 跨端: 品牌蓝深色下没提亮(24 处字/图标看不见)+ 补深色可读性设备判据
## 一、真 bug:WebUI 深色下把品牌蓝**提亮**了,这边没有

WebUI 的强调色是**双通道**(`tailwind.config.js` 的 `backgroundColor`
/`textColor` 覆盖 + `index.css` 两段定义):

| 通道 | 用途 | 浅色 | 深色 |
|---|---|---|---|
| `--s-blue-600` | **实心按钮底** | `37 99 235` | `37 99 235`(**同值**)|
| `--c-blue-600` | 内容/交互的**蓝字与图标** | `37 99 235` | **`128 175 249`** |

`index.css:353` 写了理由:主按钮底跟着变「会让主按钮在深色页面上
失去『这是主操作』的视觉重量」;而蓝字必须提亮,否则深底上读不动。

鸿蒙只有一个 `Theme.accent = '#2563EB'` ⇒ 24 处字/图标在深色下
对比度 **2.61:1**(设备实测:管理页返回箭头 `‹`),低于 WCAG 图形下限 3:1。

**取证方式**:在跑着的 WebUI 上用 CDP 读**计算样式**(不是读 CSS 源)——
浅色 `37 99 235` / 深色 `128 175 249`,实测确认。

## 二、修法:加前景专用的深色档 + 唯一入口

- `Theme.accentDark = '#80AFF9'`(= WebUI `.dark --c-blue-600`)
- `Theme.accentFor(dark?)` 作为**前景**唯一入口
- **当背景的 20 处保持 `Theme.accent` 不动**(跟 WebUI 的 `--s-*` 一致)

24 处 `.fontColor/.iconColor(Theme.accent)` → `Theme.accentFor()`。

## 三、顺手修掉「转述一层就会漏」这个结构问题

`accentSoftFor(isDark)` 原本的约定是"页面算好深浅色传进来"。
给 `accentFor` 做准备时一数:**8 处**直接用了 `Theme.accentSoft`(没走入口)
—— 约定**已经漏了**,而漏掉的症状正是上一轮那个"深色下白底卡片刺眼"。

于是把两个 `For()` 的参数都改成**可选**:`AppStorage` 是 ArkTS 全局键值存储,
静态类可以直接读(原来"拿不到 Context"的理由不成立)。
⇒ `Theme.isDarkNow()` 成为唯一判断点,调用方不必再各自转述。

## 四、设备判据:深色可读性**扫一屏**

原来只有悬浮球那一条(单个点)。这个 bug 类一天撞到**两批**(12 处 + 24 处),
逐处写判据追不上 ⇒ 改成把当前页所有小段文字都量一遍对比度。

三个实现要点(第一版全踩了,都写进注释):
- **不能只取中心一个像素**:中心多半落在笔画之间 ⇒ 读到的是底色,
  报出一片 ratio=1.00 的假红。改成**框内网格扫描取极值**(最亮=底/最暗=墨)。
- **整屏解码一次**:每点 spawn 一次 ffmpeg 太慢 ⇒ 新增
  `readPixels()`(157ms 解整屏,比逐点快三个数量级)。
- **只判"有真实墨迹"的框**(`hi.L - lo.L >= 0.02`),否则跳过而不是判红。

实测:修前 1 处低对比(2.61:1),修后 **40 段文字全部 ≥3:1**。

## 五、判据自身的三个修正

- `accentSoftFor(dark:)` 的签名断言跟着放宽成 `dark?`,并**补上 `accentFor` 的**。
- **孤儿令牌判据从"一跳"改成"走整条链"**:`KEY_IS_DARK` ← `isDarkNow()`
  ← `accentFor()` ← 24 处页面。只查一跳时它假红 ——
  ★ **"有没有人用"是可达性问题,不是邻接问题**;加中间层(抽 `For()` 入口)
  恰恰是我们鼓励的写法,而旧判据会因此假红。
- 手写色登记表补 `accentDark` 一行理由。

## 六、`baseline.sha` 重算(先核过不是残留)

三个文件哈希对不上。逐个 `git diff --quiet HEAD -- <f>` 取证:
- `AdminUsersPage.ets` / `SettingsPage.ets` —— 本次**有意编辑**;
- `api/AppearanceApi.ets` —— **与 HEAD 逐字节相同** ⇒ 底本取完后被**合法改过**
  (提交 `f811c98`),属 `stale` 不是 `residue`。

按该文件自己那条纪律(「重算必须是一次有记录的动作」)在文件里记了理由。

`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。
2026-09-19 20:05:33 +08:00
21132647bc 跨端: 12 处「深色下字看不见」的真 bug + 判据基建补上「看像素」这一层
## 一、判据基建:本目录终于能**看像素**了

此前只能靠 `dumpLayout` —— 那是**结构化描述**,报的是"组件声明了什么",
不是"屏幕上画成什么样"。两者会分叉,而观感类结论只能在像素上得出来。

新增 `lib/harmony-device.mjs`:`screenshot()` / `pixelAt()` /
`hexToRgb()` / `closeColor()`(用 ffmpeg 转 1×1 原始 RGB,不引依赖)。

**它当场证明了它的价值**:`cross-client-theme` 新增的设备判据
用真实像素抓到下面这个 bug —— 静态判据全绿时它藏得好好的。

## 二、真 bug:**12 处**把 `Theme.surface` 当前景色用

`Theme.surface` 是 `sys.color.ohos_id_color_list_card_bg` ——
一个**跟随系统主题翻转**的 Resource:浅色近白、**深色近黑**。

- 浅色下当白字用**碰巧对**(白字压蓝底)
- **深色下字变成黑的**,压在品牌蓝 / danger 红 / warn 琥珀上**几乎看不见**

设备现场:写邮件悬浮球是品牌蓝 `#2563EB`,截图里那个铅笔图标**几乎是隐形的**;
读圆心像素得到 `rgb(32,34,36)`。往左偏 50px 读到底色才见 `rgb(36,99,235)`。

`Theme.accentFg`(`#FFFFFF`)的注释原话就是「品牌底上的文字」—— 为这个场景存在,
却**一处都没用**。

修:12 处 `fontColor/iconColor(Theme.surface)` → `Theme.accentFg`
(`MainPage` 10 + `InboxPage` 1 + `SessionsPage` 1)。改完全仓 0 处残留。
另在 `Theme.ets` 给 `surface` / `accentFg` 都补上"能当什么、不能当什么"的注释。

## 三、判据(两条,都做了变异验证)

1. **设备条**(`cross-client-theme`):读悬浮球像素 ——
   ① 品牌色**真的画成** `#2563EB`(声明 ≠ 渲染);
   ② 球上图标与底色 **WCAG 对比度 ≥3:1**(压在上面的东西得看得见)。
   把 `.accentFg` 改回 `.surface` ⇒ **判红**;还原 ⇒ 绿。
2. **静态防线**(同文件):全局 grep「`fontColor/iconColor(Theme.surface)`」一处不许有。
   设备条只能看一处,而这个错法有 12 处 —— 静态防线管住整类。

## 四、判据自身踩的三个坑(都写进注释了)

- **采样点撞上图标**:第一版取球心,读到 `rgb(32,34,36)`,差点当成"品牌色没渲染"。
  截图一看球是蓝的,深色那点是**铅笔图标**。⇒ 往中心左偏 30% 球宽。
- **假设错了 FAB 的位置**:按"屏幕右下角"找(`x1 > 屏宽*0.6`),
  实测 `[942,1997]`(`x1=942` vs 阈值 1910)⇒ 永远找不到、**静默跳过**。
  原因是列表窗格是**左栏**,球在"左栏的右下角"。⇒ 形状只用站得住的那部分(下半部)。
- **设备判据要自己搭现场**:不加自导航时它**永远跳过**(前面的判据把前台留在管理页),
  而那看起来像"功能没了"。加自导航后立刻开始工作并抓到 bug。

## 五、欠账

- `harmony-maildetail-missing-three` → **count 0(结算)**:三块都做完了
  (转发 `b7c5d8b` / 改名建议 `c2f35d1`+`e79a86a` / 往返预算 `ac62daf`)。
  如实记着**未验**的那点:预算条的**点击**没在设备上走通
  (模拟器顶部 155px 是系统手势区,折叠头部恰在其中)。
- `static-criteria` 5:`cross-client-theme` **升级了一半**,仍留在名单里 ——
  `.ets` 那半只有悬浮球这一处上了设备,其余令牌仍是静态对齐。
- `debt-visibility` 登记 `cross-client-theme` 1 处边界声明(带出处)。

`run-all.mjs` → `checks=513 pass=513 fail=0 skip=0 red=0 broken=0 unreported=0`;
Go 侧 `./internal/repo/...` 通过。
2026-09-19 19:29:49 +08:00
fc295893cb 跨端: 深色模式下的品牌浅底不跟随 —— 12 处「选中/未读」底色在深色页上刺眼
## 真 bug(设备实测)

深色主题下,「我的」页**选中**的那张账号卡片仍是接近纯白的浅蓝
(实测像素 `(255,255,255)` 级别的浅底压在 `(32,34,36)` 的深色页上)。

根因:`Theme.accentSoft = '#EFF6FF'` 是**写死的浅色**,而它被当作
「选中态背景」用在 12 处(未读邮件行、选中账号、分段选中、登录页模式切换…)。

**为什么只有 WebUI 没这个问题**:它有 CSS 变量的**反转发**机制 ——
`index.css:113` 的 `--c-blue-50: 239 246 255` 在 `.dark` 段(`:475`)被换成
`28 37 54`(深蓝黑)。ArkTS 的 `static readonly` **一个常量一个值**,
没有那层机制 ⇒ 静态常量必须自己提供两个取值。

## 修法

1. `Theme.accentSoftDark = '#1C2536'`(对齐 WebUI `.dark --c-blue-50` 的 `28 37 54`)
2. `Theme.accentSoftFor(dark)` 作为**唯一入口** —— 不在页面里各自
   `isDark ? a : b`:那样每处都会各写一遍,迟早漏一处
   (WebUI 那条"由 test/theme.test.mjs 逐档断言"就是为防这个)
3. **深浅色从哪来**:只有 `MainPage` 算得出(它读 `resourceManager` 的
   `colorMode`)。所以走 `AppStorage` 单向发布(与徽标、windowInsets 同一套):
   MainPage 算 → 写 `agentmail.appearance.isDark` → 各窗格 `@StorageProp` 读。
   6 个文件、12 处,全部改用 `accentSoftFor(this.isDarkNow)`。

**设备实测**:深色下「我的」页账号卡片与收件箱未读行都变成深蓝底,
像素 `(32,34,36)` 与页面底一致(不再刺眼)。

## 判据(这条是新加的,形状值得记)

`cross-client-theme` 新增:**品牌浅底必须有深色变体**。断三件事:
1. 深色变体存在,且**取值从 WebUI 的 `.dark` 段反推**(不是随手挑一个深色);
2. 有按主题选值的**入口**(防"页面各自写三元");
3. **用到它的地方真的走那个入口** —— 扫描所有 `backgroundColor(… accentSoft …)`
   并排除 `accentSoftFor`,把漏改的位置**逐行报出来**。

第 3 条在我改到一半时**当场列出了剩下 9 处**(`CalendarPage:1172`、
`ComposePage:255`、`LoginPage:327`…)—— 这就是它该有的样子:
不是"断言存在某个常量",而是"断言没有一处漏改"。

★ 写这条判据时踩了自己一次:JS 模板串里嵌了反引号包围的标识符
(`` `accentSoft` ``),直接 SyntaxError。改用字符串拼接。

## 判据

`run-all.mjs` → `checks=506 pass=506 fail=0 skip=0 red=0 broken=0 unreported=0`。
`hvigorw assembleHap` 成功;前端重建。

**未验**:日历/登录页在深色下的观感(只逐处改了底色,没逐页截图)。
2026-09-19 16:02:26 +08:00
28e3da76d4 跨端: 左右滑动翻页(P6 第 3 步)+ 还 gesture-semantics 债 + 修跑不起来的判据基建
★ 这一轮从用户一句「滑动手势呢?」开始。查下去发现它不是"顺手加个手势",
  而是 `docs/DEBTS.json` 里挂着的一笔债 —— `gesture-semantics` 的原话是:

    「P6 第 3 步:鸿蒙侧出现滑动手势代码时**立即建**判据
      (此前建 = 只有一端存在的假判据)」

  也就是**先有手势、再钉语义**。WebUI 2026-09-14 就有滑动翻页(用户当时
  亲口提的),鸿蒙一直没有 ⇒ 之前建判据会是空真(∀x∈∅)。

## 一、手势本体(两端语义逐项对齐,数值各自定)

按 `HARMONY-ALIGN-PLAN.md:118-128` 显式选的 **(b) 口径**:
「手势的物理量本来就不该强求同值……该对齐的是**语义层**」。

· 判定逻辑放**纯逻辑层** `model/Calendar.ts` 的 `judgeSwipe`(可被 node 直跑,
  写在 .ets 里就跑不了判据,语义没法被单测钉住)。四道门:位移 / 纵向优先 /
  快滑窗口 / 方向。
· 阈值**不引用** WebUI 的 40 / 1.5 / 600(那是把巧合当契约),
  各自定为 56vp / 1.4× / 700ms,并在注释里写出取值依据。
· 接线在 `CalendarPage.ets`:`PanGesture({direction: Horizontal})` +
  `onActionStart`(记时 —— `GestureEvent` **没有时间戳字段**,我查了 SDK
  的 gesture.d.ts 确认)+ `onActionEnd`(读 offsetX/offsetY)。
· ★ 挂在**网格列**上而不是整页:右栏(日程/编辑器)里有输入框与可滚内容,
  整页挂会让「在表单里横划一下」变成翻月。
· ★ 翻页复用 `shiftRange()`(与 ‹ › 按钮**同一个来源**)—— WebUI 的注释
  专门交代过:各写一套的话,阈值、边界、三档行为迟早分叉。
  手势回调里**不准**直接改 year/month(锚点是唯一真相,年月只能由
  `shiftRange → syncYearMonthFrom` 派生)。

**有意差异(记录在案,不是漏做)**:WebUI 在周/日档会额外检查「触点是否落在
可横向滚动的区域里」,是则让给滚动条(用户 2026-09-14 报过这个冲突)。
鸿蒙周档是「一行 7 格按 layoutWeight 等分」、**不横滚** ⇒ 该条件不适用。
哪天加了横滚必须同时补上它。

## 二、判据(8 条语义契约 + 行为层)

新增 `cross-client-gesture.test.mjs`:两端都真有手势 / 方向映射逐项相同 /
纵向优先 / 快滑窗口 / 复用同一翻页函数 / 无边界回弹 / 有意差异被记录 / 自检。
**只比语义、不比数值**,并反向断言鸿蒙的阈值常量不得直接取 WebUI 的那三个数。

`harmony-logic.test.mjs` 加行为判据:真跑 `judgeSwipe`,把四道门各自验一遍
(只钉字符串的话,一个 return 写漏了照样全绿)。

## 三、顺手修掉的三处**判据基建**缺陷(不修就没法验证上面这些)

1. `run-all.mjs` 只认 `# pass N`,而 node v24 打的是 `ℹ pass N`
   ⇒ **21 个文件被记成"没自报条数"**、套件在 HEAD 就恒红(memory 里记过这条,
   修法也记过,今天终于落进代码:4 个正则加 `(?:#|ℹ)`)。修完 `unreported` 27 → 0。
   代价是暴露出一批此前被"没自报"掩盖的真实问题(下面 4~6 条)。
2. `harmony-nav` 的设备判据 `navItemsOf`:宽屏过滤条件从「左边缘靠左 1/6」
   改成「**整个盒子在侧栏轨道内**」。旧条件把**日历网格的格子**
   (实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
   这是设备实测抓出来的 —— 我第一版还以为是"底部簇混进来了",
   打印真实数据才发现是隔壁页面的格子。
3. `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS` **各自的数组**里取
   label。原先把全文件 `label:` 一网打尽 ⇒ 得到 7 个(4+3 混在一起),
   任何一边改对了它都会红。

## 四、被判据拦住后的正经修法(每条都按判据自己给的方向改,不改判据迁就代码)

· `cross-client-theme` A2 拦住我:新增的 `navActiveBg`/`navBrandFg`/`badgePlain`
  未登记;又拦住我:`sseColorOf` 里四个裸色值。→ 抽成 `Theme.sseConnected` 等
  四个令牌(取值对齐 WebUI 的 Tailwind 类)+ 登记 + 在 Theme.ets 的表里写理由。
· 同一条判据的"死令牌"检出:`Theme.durBase` 声明了却从没人读。
  **查 WebUI 才发现壁纸淡入是真有的动效**(`index.css:742-752` 的
  `.app-backdrop` 从 opacity:0 → 1,180ms)⇒ 补上而不是删令牌
  (删掉等于把差异抹平、还说成"清理")。reset 在 `animateTo` **外**,
  与 `calPaneIn` 同一条纪律。
· 玻璃登记:`NavItemBuilder` → `SidebarItem`(重构后按最近的 @Builder 命名),
  登记同步跟上。
· `harmony-admin` 的退出判据:退出逻辑抽成 `api/Logout.ets` 的 `performLogout()`
  (两个入口——「我的」页与侧栏底簇——必须做同一件事,尤其"先注销推送 token"
  那一步)。判据相应改成**追到实际执行处**(两半都断:按钮调了 + 函数真清了全部),
  并写明"别再退回直接匹配 SETTINGS_PAGE 的写法"(那会随重构假红,
  下一个人只会去改判据而不看行为)。
· `harmony-widescreen` 的连接点色值:色值搬进 Theme 后,判据改成断
  「令牌定义对了 + 侧栏真的用了它」两半(只断任一半都有假绿形态)。
· `align-refs`:`CalendarView.tsx` 变了,按判据要求**读一遍差异**再更新登记
  (差异只有农历小字的灰阶档位 gray-300→gray-400/500,**骨架未变**)。
· `criteria-hygiene`:`harmony-contacts` 自造了一个 `code()`、`harmony-widescreen`
  裸用 `readFileSync` ⇒ 都改用 `lib/read.mjs` 的共享入口。
  途中撞出一个**判据自己的 bug**:`code()` 的块注释正则
  `/\*[\s\S]*?\*\//` 会把注释里出现的 `/*`(如 `/」**` 这种中文夹星号)
  当成块注释起点,一路吃到几十行后的 `*/`,把中间的 import 全吞掉 ——
  于是 hygiene 判据假红"没 import"。改掉那处写法后正常。

## 五、验证

判据面:`run-all.mjs` → `files=32 ran=32 checks=497 pass=497 fail=0
skip=0 red=0 broken=0 unreported=0`。
其中新/改判据:gesture 8、nav 18、widescreen 7、logic 30、admin 27、
cross-client-theme 15、criteria-hygiene 6。
构建:`hvigorw assembleHap` 成功;前端 `npm run build` + 重新打 AppImage/deb
(`build-stamp` 7/7、`packaging` 5/5)。

**设备实测(HATriple 三折叠 3184×2232,hdc 连 127.0.0.1:5555)**:
· 农历在格子里真的显示(1=二十 / 7=廿六 / 19=**初九** / 11=八月),与 WebUI 一致;
  这条同时验证了**服务端农历路由已部署**(之前线上是 404)。
· 日历左右两栏:编辑器出现在**右栏**、左栏月份仍可见(单栏模式下编辑器会整页盖掉它)。
· 侧栏 3 项 + 底部一簇;徽标回到图标右上角。

**仍未验(如实标注)**:滑动翻页的**手感**(阈值 56vp/1.4×/700ms 是否合适)
只能真人滑过才知道;我只验了判定逻辑与接线形态。动画同理 ——
机制已验证(`animateTo` 驱动 + reset 在窗口外),但"看起来顺不顺"未做取样验证。

docs:`DEBTS.json` 销掉 `gesture-semantics`(并记结算说明)、
`HARMONY-ALIGN-PLAN.md` P6 从「✅(滑动翻页除外)」改为 ✅。
2026-09-19 14:01:21 +08:00
36ef15a8f2 跨端: 顶栏不再自己铺白条 + 日历改左右两栏 + 常驻窗格动画真的会播(用户三处实测指出)
用户三条反馈,逐条对应:

① 「底栏数字为什么显示在图标下面?」
   WebUI 的徽标是 `absolute top-1 right-[22%]`(脱离文档流、浮在图标右上角),
   我写成了 `Column` 的第三个子节点 ⇒ 参与竖向布局、掉到文字下面。
   改用 `Stack({ alignContent: Alignment.TopEnd })` 锚在**图标**上。
   (顺带撞了 skill 里明写的坑:Stack 没有 `.justifyContent()`。)

② 「一个横着过去的白条,我真的服了」/「期望:融进背景」
   WebUI 的顶栏**自身没有底色** —— 只有 `border-b border-gray-200`
   (`ContactPanel.tsx:64`、`CommTabs.tsx:40`、`CalendarView.tsx:295`),
   底色由所在面板给;壁纸开启时那层面板是玻璃色(`index.css:876`)。
   鸿蒙三个窗格顶栏写死了 `Theme.surface`(实心白)⇒ 无论壁纸开没开,
   顶上都是一条不通明白带。改成与**页面底**同一口径
   (`bgActive ? Transparent : surface`)+ 补下边框。

③ 「日历页面和webui布局完全不同」
   WebUI 是**左右两栏**(`CalendarView.tsx:452-457`):左 `flex-1` 网格、
   右 `400px` 常驻面板(日程 / 编辑器 / 小时网格三态互斥)。
   鸿蒙原来是**单栏竖堆**。重搭为两栏,`paneWide` 由 `.onAreaChange`
   量本页**自己的**宽度(不是屏幕宽度 —— 宽屏下这一页已被侧栏占掉一截);
   编辑器改占右栏位置(不再整页盖掉正在看的那个月)。

④ 「最严重的动画问题你一点也不该改」
   日历是**常驻挂载**(`visibility` 控制,因为它里面 today 要随时间重算、
   也要保住"正在看哪个月"),而 `.transition()` 只在**挂载/卸载**时触发
   (SDK 原话 \"when it **appears and disappears**\")⇒ 挂在它上面的
   `.transition(paneRiseIn())` **一帧也不会播**,切过去是硬弹。
   WebUI 踩过同一个坑并把错法写进了 `index.css:1305-1320`
   (「只挂了类,却没让触发窗口出现 ⇒ 类挂着、动画永远不播」),
   它的解法是 `html.view-switch` 重放窗口。ArkUI 对应物是
   `animateTo` + 显式 `calPaneIn` 属性(`opacity` + `translate`)。
   ★ reset 必须在 `animateTo` **外**:写进回调里会与同帧的 1 相抵,
     渲染层只看得见最终值 ⇒ 动画退化成一个瞬移。

顺带修:
· 宽屏侧栏 = 3 项(通信/日历/**联系**)+ 底部一簇(头像/主题/退出),
  与底栏的四项(含「我的」)**不是同一份清单** —— WebUI 的 Sidebar 与
  NarrowNav 本就不同(`Sidebar.tsx:26-44` vs `NarrowNav.tsx:37-40`+143)。
  新增 `NAV_SIDEBAR_ITEMS` / `ME_PANE_INDEX` / `SIDEBAR_ITEM_*`。
· 退出登录抽成 `api/Logout.ets` 的 `performLogout()`:侧栏底簇与「我的」页
  两个入口必须做同一件事(尤其"先注销推送 token"那一步),复制一份就会不一致。
· 徽标 `'plain'` 档底色:WebUI 是石板灰 `--c-chrome-600`(#475569),
  我写成与未读共用红色 ⇒ 「联系」的徽标看起来像"有未读"。
· 主题快捷开关(侧栏底簇):对齐 `ThemeToggleButton` —— 从 `system` 翻转时
  落到**当前生效值的反面**(不是回 system;那可能毫无变化、让按钮看起来坏了)。
· 「浓度」→「压暗」+ 数值带单位(原先屏上印 `56.000000`;WebUI 是 `suffix="%"`)。

判据(8 个套件全绿:logic 28 / nav 18 / widescreen 7 / window 9 /
arkts 5 / contacts 5 / calendar 30 / system-api 5):
· `harmony-widescreen` ② 重写:**回读 `Sidebar.tsx` 数 `short:` 的个数**
  要求鸿蒙同数,并断言底部一簇三键真的被调用(`this.onToggleTheme()` ——
  第一版写成 `/onToggleTheme/`,变异测试当场证明它不咬:属性**声明**还在,
  正则照样匹上)。新增 ⑧:三档色调各自底色,期望值从 `--c-chrome-600` 读出。
· `harmony-nav` 动画条重写:改判**机制真的存在且被驱动**
  (旧断言 `.visibility(...).transition(...)` 锁的正是那个 bug ——
  判据引自己写的注释当依据,就会把错误锁死)。新增 reset-在-animateTo-外
  这条断言(否则动画退化成瞬移)。
· `harmony-nav` 设备条:`navItemsOf` 的宽屏过滤从"左边缘靠左 1/6"
  改成"**整个盒子在侧栏轨道内**" —— 旧条件把日历网格的格子
  (实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
  新增 `navRailItemsOf`:导航轨贴顶、底部簇在屏底,按位置切一刀。
· `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS`
  **各自的数组**里取 label —— 原先把全文件 `label:` 一网打尽,
  得到 7 个(4+3 混在一起),任何一边改对了它都会红。

设备实测(HATriple 三折叠,3184×2232):侧栏 3 项 + 底簇 / 底栏徽标回到
图标右上角 / 日历左右两栏与 WebUI 并排同构。

server/go.mod:补 2da38bb 漏提交的 lunar-go 依赖。
2026-09-19 12:30:05 +08:00
7b3028342a 跨端: 宽屏侧栏根本不像 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。
2026-09-18 12:52:48 +08:00
af2d2b5cad 跨端: 鸿蒙动画补齐 —— 并且发现原来的「逐字一致」是假的(令牌存在 ≠ 动画用了它)
用户:「A,同时把鸿蒙app的动画补齐」。

## 先纠一条错的前提(这是本轮最有价值的发现)

`Theme.ets` 的注释与 `harmony-nav` 的判据**都**写着:「三个数与 WebUI **逐字一致**:
`--ease-out-soft: cubic-bezier(0.22,1,0.36,1)`、`--dur-fast: 120ms`、`--dur-base: 180ms`」,
`paneRiseIn()` 就按 `durBase(180)` + `easeOutSoft` 做。

三个令牌**确实存在**(`index.css:310-312`)—— 但这句话把「令牌存在」当成了「动画用了它」:

| WebUI 里 | 真实用途 |
|---|---|
| `--dur-base: 180ms` | **只**用在壁纸淡入(`:747`) |
| `--ease-out-soft (0.22,1,0.36,1)` | **只**用在 transition(壁纸、控件变色) |
| **所有 @keyframes 动画** | 硬编码 **150ms / 200ms** + `cubic-bezier(0.22, 0.61, 0.36, 1)` |

全仓 `(0.22,0.61,0.36,1)` 出现 **5 次**(rise-in×3 / cal-in×2),`(0.22,1,0.36,1)` 只出现
**1 次**(令牌定义处)。**是两根不同的曲线** —— 旧代码的面板入场比 WebUI 慢 30ms 且曲线偏软。

所以令牌拆成两组,各对齐各的:控件类 `durFast + easeOutSoft`;动画类
`durRise(150)/durMenu(140)/durCal(200) + easeRise(0.22,0.61,0.36,1)`。

## 补的动画(对齐 WebUI 三个 @keyframes)

- `menuIn()` —— 弹层:下移 4vp + 缩到 0.985 + 淡入(`@keyframes menu-in`)。
  ★ 适用范围照搬 WebUI 注释那条窄口径(「只给真正是弹层的东西」):那条规则曾挂着
  `glass-control`,于是每次切视图**所有按钮与输入框一起淡入位移**,09-15 被摘掉。
  挂到写邮件页的账号候选列表上。
- `calendarSlide(forward)` —— 日历翻月:从 ±12% 横向滑入(`cal-in-next/prev`)。
  方向由新增 `@State slideForward` 带进 `animateTo` 闭包;网格键加 `monthKey()` 前缀
  保证月份一变所有键全变(否则"跨年同名月"那类边角会静默不播)。
- 写信页整页 `rise-in`、回复框 `rise-in`(WebUI 挂在回复框本身,不是外层遮罩 ——
  挂遮罩上会让整个屏幕一起位移,看起来是"页面在动"而不是"框弹出来")。

**实测确认真的会播**(不是"编译过了"):按本仓记录的手法把 `durCal` 临时改成 8000
做慢动作,连拍三帧 —— 截图硬证**两张月历同时在屏**(九月淡出、十月从右侧 12% 滑入),
验完还原成 200ms。

## 判据:从「令牌存在」改成「动画真的用了那个令牌」

`harmony-nav` 那条判据原文锚在令牌上,所以它对上面那个 bug **一辈子全绿**。
重写为:
- 从 WebUI `index.css` **读出** `rise-in` 的真实时长与曲线(`150ms` + 那条 bezier),
  再断言鸿蒙的 `durRise` 与曲线字面量与之逐字一致 —— 而不是"仓库里有没有 180 这个数";
- ★ 把 `paneRiseIn()` 的**函数体抠出来**单独断言它引的是 `durRise/easeRise`,
  且**不得出现** `durBase/easeOutSoft`。

**这一步不能省 —— 我第一版就漏了它**:断言了令牌存在、也断言了曲线字面量存在,
但没断言函数用了它们;于是把函数体换回旧的错值后判据**仍然全绿**。
变异自检抓住了这一点,补上函数体断言后同一个变异 ⇒ 判红 ✓。
与今天修的另一处同源:**判据要锚在"这个东西被用在哪",不是"它存在"**。
2026-09-18 10:56:57 +08:00
2166f0ed81 fix(harmony): 补齐窗格切换动画(transition 挂在会换的那棵子树上)
用户(2026-09-17):「一方面一点动画都没有」。

★ 第一版写错了,这里记下来 —— 它的错法很典型,下次还会踩:
  只在 onClick 里把 `currentIndex = ...` 包了一层 `getUIContext().animateTo(...)`,
  并把 `.transition(...)` 挂在**内容容器(稳定父节点)**上。
  编译过、判据(当时只钉"有 animateTo")全绿,**但动画是静默失效的**:
  `animateTo` 只负责开一个动画窗口,被换掉的子树自己不声明 transition 就什么都不会动;
  而挂 transition 的那个 Column 在新旧两种状态下**都是同一个节点**,永远不会触发。
  实测取证:把时长临时改成 20s,6 秒后截图仍是硬切(收件箱整版清晰、没有叠影)。

修法(对齐 WebUI `.pane-rise` / `.rise-in`,`index.css:1208`):
· `Theme.paneRiseIn()` —— 4vp 上浮 + 淡入;`riseInOffset = 4` 与
  `@keyframes rise-in { from { opacity:0; transform: translateY(4px) } }` 逐字一致。
· 挂到**if/else 各自的子树根**上(CommPage / ContactsTab / SettingsPane,
  以及常驻+visibility 的日历那一支)—— 这才是会被插入/移除的节点。
· `TransitionEffect.asymmetric`:入场 180ms(durBase)、出场 120ms(durFast)。
  出场更快是刻意的:同长会让新旧两层半透明地叠着,看起来像"闪一下",
  而用户对"闪"敏感(09-14 否掉过整屏淡入)。
· 续用令牌 `durFast=120` / `durBase=180` / `easeOutSoft=cubic-bezier(0.22,1,0.36,1)`。

判据(219 passed / 0 failed):
· 新增「窗格切换有真的过场动画」6 条:令牌数值逐个钉死(120 / 180 /
  cubic-bezier(0.22,1,0.36,1));`transition` 必须挂在 if/else 分支根(≥2 处,
  含日历那一支);必须 asymmetric。
· **变异自检**:删掉那 4 处 `.transition(...)` ⇒ 该条立刻变红;还原 ⇒ 全绿。

留给以后的坑(写进 Theme 注释):`snapshot_display` 单次往返约 1s,
180ms 的过渡**抓不到**(连拍三帧像素级一致,会得出"动画没做"的错误结论)。
要取证就得把时长临时调到 20s 再取样,验完还原。本文件注释里留了这条与那次实测值。

未验:真机手感(模拟器已确认过渡链路生效);出场动画与详情 push 的叠加观感。
2026-09-17 21:33:15 +08:00
686193458a fix(harmony): 底栏黑带 / 联系人点不开 / 我的页不守导航 / 顶栏硬截断
用户(2026-09-17)连报五条:「一点动画都没有」「底部那个黑条是啥意思」
「日历页面、我的页面、联系人页面哪个跟 webui 对齐了」「联系人页面连点都点不开」
「你的顶栏为什么还是硬截断而不是渐变」。逐条查证后修:

── ① 底部黑条 = 系统导航栏区域,不是我们画的 ──
根因:窗口默认给系统导航条留位,那块在深色下是黑的;界面于是看起来底下多一条黑带。
修法:`setWindowSystemBarEnable(['status'])` 隐藏系统导航条(底部那条本就是我们自绘的
悬浮玻璃条),内容铺满全高。
★ 刻意**不用** `setWindowLayoutFullScreen(true)`:实测试过,它也能消掉黑带,但会连
状态栏区域一起吃进布局,页签栏被时钟/电量盖住(截图硬证「07:43」与「收件箱」重叠)。

── ② 联系人点不开(用户原话「连点都点不开」)──
根因:`ContactItem` / `WorkCard` **根本没有 onClick** —— 卡片画出来了,
但没有任何点击路径。当时判据只钉了"字段与 WebUI 一致",没钉"点了会发生什么"。
修法:点卡片打开那条会话的邮件列表,就地在**本窗格内**展示(新增 `SessionMailsView`
+ `GET /sessions/{id}/mails`),底部导航保留;每封可再点进详情。

── ③ 我的页不遵守导航规则 ──
根因:点底栏「我的」走 `pushUrl('pages/SettingsPage')` 推**独立 @Entry 页** ⇒
底部导航整条消失,要按返回才能再切窗格。WebUI 里 `account` 只是一个 `viewMode`,
与收件箱同级、导航常驻。
修法:`SettingsPage` → `SettingsPane`(去掉 @Entry 与返回键,从 main_pages.json 摘除),
作为**第 4 个内容窗格**挂到 `currentIndex === 3`;`NAV_CONTENT_COUNT` 3 → 4,
`NAV_ITEMS` 第 4 项去掉 `route`;拨动动画包 `getUIContext().animateTo`。
实测:`我的` 页底部导航可见,可滚到「退出登录」「管理」。

── ④ 顶栏硬截断 ──
根因:`Scroll` 与 `List` 都能挂 `fadingEdge`(同在 `ScrollableCommonMethod` 上),
但**只有 MainPage 的 List 挂了** —— MailDetailPage / CalendarPage / SettingsPage /
AdminUsersPage / SessionsPage / InboxPage 的滚动容器全漏。同一次滚动里
列表是渐隐、正文是硬切,看起来就是"顶栏没做渐变"。
修法:13 个容器全部补齐(LoginPage 例外:它的 Scroll 是整页根、不是列表,
写了理由)。实测:视口上沿被切断的文字最暗 170–196,而下方不透明正文是 **19**。

── ⑤ 一点动画都没有 ──
实测确认改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过**。
补:Theme 加令牌(durFast 120 / durBase 180 / easeOutSoft = cubic-bezier(0.22,1,0.36,1),
与 WebUI `--dur-*`/`--ease-out-soft` 同值同曲线);切窗格包 `animateTo`;
导航项选中变色加 `.animation`。

判据(218 passed / 0 failed):
· 新增「渐隐覆盖**所有**页面滚动容器」:按文件枚举 + 计数配平,
  并自检扫到的容器总数(防正则写坏 ⇒ 全绿)。**这条一写就抓到 LoginPage**。
· ①/②/⑤ 与 headless 那几条按新事实改写:内容窗格 3→4、
  `normalizeNavIndex(3)` 归一到自己、导航项里**禁止**再出现 `pushUrl`
  (那正是导航消失的形状)。
· 修掉自己引入的两处判据缺陷:`itemCode` 未定义;以及一条**窗口式断言**
  (从内容层 `}` 往后扫,靠数花括号定界,而注释里就有大量 `.padding({`,
  切片长到 1700+ 字符一路扫进 `onAreaChange` ⇒ 假红),改为纯负向断言。

未验:真机观感;横屏 Auto Split 双栏;日历页深度对齐(本轮只补了渐隐)。
2026-09-17 21:15:11 +08:00
fd4b475920 ke: 鸿蒙导航纯图标 + 图标加大 + 玻璃几何令牌
用户反馈:① 图标太小 ② 底部导航栏不允许有文字。

- NavItem(底部导航):去掉 Text(item.label) 纯图标,AmIcon 22→28vp
- WideSidebar(宽屏侧栏):去掉 label 文字(WebUI Sidebar 本就是 60px 纯图标轨),
  导航图标 22→26vp、品牌标 26→30vp;NavItemBuilder 去掉 label 参数
- Theme:新增 glassRadius=14 / paneGap=10(与 WebUI app-shell CSS 变量同值,
  用于宽屏玻璃面板几何;原有 radiusCard 保持系统跟随不动)
- 判据同步:harmony-nav ⑤ 改纯图标(1 处三元式 + 无文字 + 28vp)+ 变异自检三项;
  harmony-widescreen ③ 同样改纯图标(1 处 + 无文字 + 26vp)
2026-09-16 07:54:44 +08:00
d1e0ba32f9 跨端: 壁纸遮盖色改用页面底色系(mask 两套主题都深,浅色下方向与 WebUI 相反);模态遮罩保持 mask
pi 提了一个**方向性**疑问,让我先把值读出来再定 —— 读完确认**他是对的**,而且这是"机制上确定不同"。

## 实测(两张 SDK 表交叉验证,不靠记忆)

`ets/build-tools/ets-loader/sysResource.js` 给名字→id,`previewer/.../resources.txt` 给 id→值:

| 令牌 | 浅色主题 | 深色主题 |
|---|---|---|
| `ohos_id_color_mask_regular`(原 `Theme.overlay`) | `#99182431` **深蓝灰** | `#b2000000` 黑 |
| `ohos_id_color_background` | `#ffffffff` **白** | `#ff18181a` 近黑 |

⇒ mask 的 light/regular/thick 是**浓度档**(同一深色的三个 alpha),不是深浅两套值:
它在**两套主题下都是深色**(模态遮罩语义)。而 WebUI 的 `--bg-scrim` 浅色是**白**
(原话「浅色下用白把花哨的图案洗淡」)—— 壁纸遮盖用 mask 就是**反方向**:
浅色主题下把预设压暗,而 WebUI 是把它洗淡。

## 改法

- 新增 `Theme.wallpaperScrim = $r('sys.color.ohos_id_color_background')`(页面底色系:
  浅色白、深色近黑,**自动换向**,与"朝底色淡化"同一意图);预设档与图片档的遮盖层都用它。
- `Theme.overlay`(mask)**只留给模态弹层**(那里语义确实是压暗背后)。
- 不复用 `pageBg` 的原因写进注释:页面底在背景开启时会被换成**透明**
  (`bgActive ? Color.Transparent : Theme.pageBg`),而遮盖层**永远要一个真实颜色** ——
  两个用途生命周期不同,共用一个名字迟早坏一头(这个仓库撞过四次的模式)。

## 判据(从 SDK 读真值再判,不写死结论)

断言「mask 两套主题都深」「页面底色系浅白深黑」「WebUI 的 `--bg-scrim` 浅色是白」,
再落到代码:两处遮盖层必须用 `wallpaperScrim`、弹层仍用 `overlay`、两个令牌不许混用。
**这样"令牌选错方向"以后不能靠记性避免。**

变异:遮盖退回 mask → 红 1 条;只改一层 → 红 2 条。

## 文档

§7.17b-2 记这次读数(表格 + 两张表怎么读 + 为什么不复用 pageBg + 变异结果);
§7.12 有意差异表加一行(遮盖色令牌:两个语义两个令牌,"不允许混用")。

## 验证

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(12 个判据文件全绿 + vitest 258/258;harmony-appearance 23 → 24 条)。
2026-09-14 15:10:15 +08:00
a6b5e52f45 test(harmony): 判据按 pi 复核意见补强 —— 手写色登记表、玻璃叠用形状、按标题取小节、遮罩必须被用、废弃 API 清单从 SDK 生成
pi 逐行读了 `cross-client-theme.test.mjs` 与 `Theme.ets` 后指出五处(第一处是真缺口),
外加一条建议。全部处理,并且**每一处都用变异验证过**。

## 一(真缺口):枚举 11 个"必须是系统资源"的名字,挡不住第 12 个新写死的手写色

`static readonly brandSecondary: string = '#123456'` 这种新增**三条判据都碰不到**:
A(不在名单里)、B(六位、不是半透明)、裸色值那条(只管 `pages/`)。
原理与当初 14 处 Google 色逃出去是同一条 —— **枚举挡实例,类才挡漂移**,
只是这次枚举的单位是**名字**。补 `A2` 条:

- 枚举 `Theme.ets` 里所有 `static readonly X: string = '#……'`,未登记的 → 红;
- 名单里已不存在的名字 → 红(名单不能烂成化石);
- `Theme.ets` 里的「手写色登记表」段必须逐个列出这些名字(理由不能只存在于记忆里);
- 自检:把一个**新的**手写色塞进源码字符串,确认这条抓得到。
  `Theme.ets` 因此新增登记表(17 项,每项一行理由,按品牌 / 业务语义 / 档位胶囊分组)。

变异:Theme.ets 加 `brandSecondary` → 红。

## 二:C 的"玻璃只有一处"**说错了自己断言的东西**

`assert.equal(glassCalls.length, 1)` 断言的是"全仓共一处",**不是**"没有嵌套":
同页两个**并列**玻璃面(没问题)会让它红,真嵌套它没在判。P5 正是悬浮玻璃导航,
到时若把 1 改成 2 就等于不判。改成形状判据:

- 收集每个调用点所作用的**组件块**(按括号配对回溯;注意 ArkUI 修饰符是链式的,
  `X.blur(A).blur(B)` 前面是 `)` 不是 `}` —— 第一版只看一个字符,对这种写法**静默失效**,
  是判据自检抓出来的);
- 判两种叠法:同一组件上叠多次(同一块)、以及套在另一层玻璃的子树里;
- 每一处玻璃都要在 `GLASS_REGISTRY` 里登记(附一句为什么),名单里的位置若已不存在 → 红。

变异:链式叠两层 → 红;通信页多开一处未登记玻璃 → 红;导航条块内嵌玻璃 → 红。

## 三:文档小节用 `indexOf('有意差异')` 会**拿错段落**

别的段落正文里出现这四个字,切片就从那里开始,后面所有断言都在**别的段落**上判。
改成**按标题**定位(正则匹配 `^#{2,4}…有意差异…$`),并加了表头列名断言
(WebUI / 鸿蒙 / 为什么)—— 拿错段落时表头不会是这个形状,于是它自己会红。

顺带修掉一个**我自己的**同类毛病:品牌色那行原来用"关键词 + 80 字符窗口"判,
窗口宽度在赌表格单元格字符数(该行两格之和 > 80)。改成**按行取**那一行再断言。
另外把偏移算术的切片换成**按行**切片(偏移差一个字符就会把最后一行拦腰截断,
现象是"品牌色那行只剩 57 字符"这种看着像文案、其实像切片的怪事)。

变异:文件前面插入含「有意差异」的段落 → 仍绿(按标题定位生效);
再改坏真表里的品牌色行 → 红(确实读的是那一张表)。

## 四:`Theme.overlay` 只判了"存在",没判"被用"

没使用点的令牌是自证。补断言:它必须在 `Theme.ets` **之外**有真实使用点
(实测 `SettingsPage.ets` / `MailDetailPage.ets` 两处自绘弹层在用),
并把 `overlayColor`/`overlayAlpha` 消失的理由写进 §7.12 —— 否则下一个人会当成漏改补回来。

## 五:材质档次是这次替换里唯一"判据绿但可能观感错"的地方

`COMPONENT_THICK` 的依据(对 THIN / BACKGROUND_* / ULTRA_THICK 的取舍)写进 §7.12,
并把"材质档次在导航条上的实际观感"列为**模拟器起来后第一个要看的项**(间距是数字,材质是判断)。

## 六(建议):废弃 API 判据**类化** —— 清单从 SDK 生成

原来是手写"不得再用全局 `promptAction.showToast`"(只挡已踩过的那个)。
现在从 SDK 生成:顶层(花括号深度 0)被标 `@deprecated` 的 `declare function`
—— 实测 68 个名字(含 `animateTo` / `getContext` / `px2vp`),配一份**空**的 allow-list。
深度判定是必要的:`declare namespace fileIo { declare function open() }` 里的 `open`
是命名空间成员,算进来会造一堆假红。只算全局调用(排除 `x.name(`)。

变异:调 `px2vp(10)` → 红;`this.px2vp(10)`(成员调用)→ 绿(假阳性自检)。

## 验证

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(11 个判据文件全绿 + vitest 258/258)。cross-client-theme 13 → 14 条,harmony-system-api 4 → 5 条。
2026-09-14 14:26:45 +08:00
36f3183bba feat(harmony): 系统方案第一批 —— 表面/文字/圆角交给系统、删手写玻璃、每项一张卡;跨端判据改"意图相同"
jianf:「鸿蒙也同步,但是鸿蒙要求用系统方案」。按 pi 对齐的形状(A/B/C + 品牌色防线)落地。

## 鸿蒙侧改了什么

- `Theme.ets` 的表面/文字/分隔/遮罩/圆角**来源换成系统**:
  `pageBg→sys.color.ohos_id_color_background`、`surface→…_list_card_bg`、
  `surfaceMuted→…_sub_background`、`border→…_list_separator`、三级文字 `→…_text_primary/secondary/tertiary`、
  `overlay→…_mask_regular`、`radiusCard/Control→sys.float.ohos_id_corner_radius_card/button`。
  于是这些维度自动跟随深色模式与无障碍设置 —— 这正是"手抄 WebUI 色值"做不到的事。
- **删掉手写玻璃** `#B8FFFFFF`/`#B80F172A`:那两个值等于"我们替系统猜了深色该怎么做"。
  换成一个**档次**声明 `navMaterial: BlurStyle = BlurStyle.COMPONENT_THICK` + 导航条上的
  `.backgroundBlurStyle(...)`;深浅两套颜色与模糊半径由系统按主题给。
  遮罩的两段式(色 + 透明度)同样删掉:拆两段本就是为了"随主题换向",而这件事系统已经做了。
- **每项一张卡/气泡**:收件箱行、会话组头、联系人列表行改成卡片(圆角 + 卡片底色 + 行间距),
  联系人列表那条贯通分隔线删除。
- 仍然自己写的只有两类:**品牌色**(`accent = #2563EB`,跨客户端身份)与**业务语义色**
  (权限三档、预算三档 —— 系统只有 warning/alert 两个情绪色,凑不出三档,硬套会丢语义)。

## 判据:从"取值相同"改"意图相同"(两侧一起改)

- 品牌蓝**唯一保留取值钉**,并新增防线:不得退化成 `$r('sys.color.*')`
  (系统强调色随主题/厂商皮肤变,"两个客户端是同一个产品"就靠不住了)。
- 圆角/材质/遮罩:改成"WebUI 自声明令牌 + 鸿蒙来自系统 + 差异被记录"(§7.12 有意差异表)。
- 新增 A(系统拥有的维度唯一来源是 `$r('sys.*')`,且不得退回 string/number)、
  B(旧机制不得回来:`navBg*`、8 位半透明色、`rgb(`/`rgba(`、写死的 14/8)、
  C(玻璃位置必须调 `backgroundBlurStyle` 且**只许一层**)。
- pi 指出的洞已补:裸色值判据原来只扫 `#RRGGBB(AA)`,抓不到 `rgba(`/`0xRRGGBBAA` ——
  而这几种恰是"改用系统材质"时最容易混进来的形态。现在四种一起扫,且**先剥注释**
  (注释里正当地引用旧写法不该被judged红)。
- pi 的 §5 建议也已落地:新增"版本库不得跟踪缓存/构建产物"判据(`.tmp/` 那次 554 个文件的事故判据化),
  并放行 `server/internal/static/static/placeholder.html`(go:embed 落点的有意占位,删了 Go 侧编不过)。

## 变异验证(能红,且红在对的地方)

| 变异 | 结果 |
|---|---|
| 品牌蓝 → 系统强调色 | 红 3 条 |
| 手写玻璃 `navBgLight` 回来 | 红 2 条 |
| 导航改用写死半透明色、不调 `backgroundBlurStyle` | 红 2 条 |
| 卡片上再开一层模糊 | 红 1 条("玻璃应只出现在一处,实际 2 处") |

## 验证 / 未验

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(10 个判据文件全绿:跨端 13 条、系统资源名 4 条、harmony-logic 19 条… + vitest 258/258)。
**未验**:观感(卡片间距、系统材质在导航条上的实际效果、深色模式)—— 需真机/模拟器;
模拟器要人在命令行跑一次 `harmony-emu start`。`sys.*` 名字全部对着 SDK 名表核过,且判据持续盯着。
2026-09-14 14:04:04 +08:00
3729afd96f feat(harmony): P2a —— 收件箱按会话折叠 + 联系人页卡片视图(判据直接跑同一份逻辑)
按 pi 的结论落地 P2a 的前一半:**先补视图与折叠,再删平级「会话」tab**(tab 本轮保留)。

## 判据怎么"点用户真正会点的那一层"

鸿蒙侧没有设备(`hdc list targets` 为空、模拟器在本机沙箱下起不来),"点一下"暂时
无法自动验。应对不是编个能过的新判据,而是把会点的那一层的内核抽成纯逻辑:
`entry/src/main/ets/model/MailGrouping.ts`(无 UI 依赖),判据用 node 的
`--experimental-strip-types` **执行同一份代码**(`test/harmony-logic.test.mjs`,14 条),
断言的是行为而不是"源码里出现过某个字符串":

- 折叠后组头是不是**最新一封**、组内是否时间倒序、组间排序、同一时刻用 `mail_id` 倒序兜底;
- 时间解析失败**不能让顺序依赖入参**(WebUI 侧踩过的 NaN 比较坑);
- 多账号合并下同名 `session_id` 不能被错并成一组;`session_id` 缺失时各自成组;
- 预算档位与 WebUI `BudgetChip` 完全一致(剩 0 用尽 / ≤1 将尽 / 上限 0 不显示);
- 视图切换与卡片上"最新一封是人还是 Agent"的判据。

页面那一层另用源码判据钉"确实调了这些函数",两层合起来覆盖「逻辑对」+「页面接上了」。
**变异验证 4 处全部判红**:去掉组内排序(2 条红)、预算阈值 `<=1` 改 `<1`、
分组键去掉账号前缀、页面不再区分单封组。

## 收件箱折叠

- 组头取组内最新一封的别名与主题,带未读数徽标与「N 封」,点它展开/收起;
- **单封不成组、平铺**(与 WebUI `isFlatGroup` 同结论:给孤立一封信套组头只是多一次点击);
- 多账号是鸿蒙特有:分组键带账号前缀;`session_id` 缺失按 `mail:<id>` 各自成组。

## 顺带修掉一个"看起来是总数、其实是未读数"的显示

`/me/mail/inbox` 的 `total` 是 **`CountUnread`(未读总数)**,不是总封数
(`server/internal/handler/me.go`)。鸿蒙底部原写「共 N 封」⇒ 同一屏出现
「共 7 封」和「未读 7」两行自相矛盾的字。改成:未读数用服务端 `total`(权威,
原来数这一页会少报);「共 N 封」→「已加载 N 封」;**这一页取满时如实提示
「已加载 50 封(本页上限 50,可能还有更多)」** —— 客户端没有可信总封数,
就不能把 50 封说成全部(pi 提醒的"别让只取 50 封伪装成只有这么多会话")。
WebUI 侧不读这个字段,故只影响鸿蒙。

## 联系人页补卡片视图(撤 tab 的前置)

- 右上角切列表/卡片,标题「联系人」/「工作列表」(与 WebUI 同词),切换规则在
  `nextContactView()`;
- 卡片对应 WebUI 的 `WorkCard`:Agent 名 + 工作目录 + 未读徽标、会话别名、
  **主题当主角**、最新摘要 + 人/Agent 标记、`N 封 · 时间`、权限档位徽标、
  **往返预算条**(同一档位判据)。
- 平级「会话」tab 暂留:撤 tab 按 pi 的顺序排在后面单独一步(撤早了预算/status/from_agent 没处看)。

## 验证

- `hvigorw assembleHap` **BUILD SUCCESSFUL**(`.ts` 纯逻辑模块被 `.ets` 引用,实测可行)。
- `npm test` **退出码 0**:窄屏布局全通过、主题 30、背景 34、cross-client 8、
  harmony-logic 14、packaging 3、vitest 258/258;新判据已接进 `npm test`。
- **视觉与点击仍未验**(无设备):展开手感、卡片间距、组头命中区没有任何自动判据
  能代替人眼 —— 交付按"结构/逻辑已验证、观感未验"写,未写成已完成。
2026-09-14 13:36:05 +08:00
b041ea51e4 fix(harmony): 令牌收尾 —— 裸色值清零、遮罩拆两段式、权限档位配色对齐 WebUI
接手核对时发现:「不许写死颜色」那条判据**只挡得住枚举的 8 个旧值** —— 判据全绿的
同期,pages/ 里还留着 14 处另一套写死的色(Google/Material:#E8F0FE、#E8F5E9、
#FFF3E0、#D93025、#777777×2、#555555、#444444、#cccccc、#aaaaaa、
遮罩 #80000000×2、透明 #00000000×2)。枚举挡不住漂移,只有"类"能挡。

## 改法

- 14 处全部换成令牌。新增 accentStrong / warnBg / warnFg(取值对齐 WebUI `:root`
  的 blue-700 / amber-50 / amber-700)与 `Theme.permBg/permFg` —— 权限档位徽标与
  WebUI 的 `PermissionChip.tsx` **同一映射**(plan=蓝 / workspace=绿 / full=琥珀);
  原先写成 `full ? 绿 : 橙`(Material 色),与 WebUI **反着来**。
- 遮罩拆成 `overlayColor` + `overlayAlpha`(照 WebUI 的 `--bg-scrim` + `--bg-dim`
  两段式):遮罩色要能随主题换向,色与透明度焊死成一个 `#AARRGGBB` 等于把枚举写回
  代码。ArkUI 只认单值,故由 `Theme.overlay()` 组装。
- 判据从"枚举旧值"改成"按类挡":pages/ 下**一个裸色值都不许有**(含 8 位
  `#AARRGGBB`),页面清单从硬编码 7 个文件名改成**扫目录** —— 旧写法下,
  接下来要加的发件箱/授权/日历会自动逃出判据。另加一条判据:权限档位配色与
  WebUI `:root` 变量逐一比对,防"看起来差不多"。

## 验证

- 变异测试:往 `InboxPage.ets` 塞一个 `#E8F0FE` → 判据红;撤回 → 绿。
- cross-client 判据 6 → 8 条全绿;`hvigorw assembleHap` BUILD SUCCESSFUL。
- **视觉未验**(模拟器在本机文件沙箱下起不来,见 `docs/HARMONY-ALIGN-PLAN.md` 5.4),
  未写成"已完成"。
2026-09-14 13:29:50 +08:00
5434bc9e4e feat(harmony): 对齐第一阶段 + 对齐计划文档(差距/分期/验收纪律)
用户:「安排对齐」。

## 先量差距(不靠感觉)

鸿蒙侧的调色板与 WebUI **根本不同**:`#1A73E8`(Google 蓝)vs 品牌 `#2563EB`、
`#333333` vs slate-900 `#0F172A`、`#F5F7FA` vs `#F8FAFC`、`#FF4444` vs red-600…
共 217 处硬编码色值散在 7 个页面里。

功能面:鸿蒙是 收件箱/会话/联系人 三个 tab,**缺 发件箱 / 授权 / 日历 / 管理**,
也没有玻璃悬浮底栏、主题壁纸同步、Composer 共用组件。

## 第一阶段(已完成并可验收)

- `common/Theme.ets` 补齐文字/浅底/语义令牌,页面里的旧调色板**全量替换为令牌**
  (共 203 处),`hvigorw assembleHap` **BUILD SUCCESSFUL**。
- 判据:`cross-client-theme.test.mjs` 新增"鸿蒙页面里不得再出现旧调色板色值"
  (6 条全绿,含扰动自检)。这条防的是**新页面又随手写个"差不多"的颜色** ——
  漂移就是这么开始的,而此前没有任何判据会红。

## 计划文档:docs/HARMONY-ALIGN-PLAN.md

写清两件事,免得每轮重新猜"还差什么":

- **差距表**(逐项,标出"缺页面/交互不同/观感不同"的性质);
- **分期**:P2 发件箱(与收件箱同构,风险最低)→ P3 授权页(备注必须随决策送达模型
  —— WebUI 侧踩过这个坑)→ P4 主题/壁纸同步(服务端"无记录"时以本地为准)
  → P5 玻璃悬浮导航 → P6 日历(最大,单独排)。

## 如实说明

鸿蒙**视觉未验证**:本机 `hdc list targets` 为空、HAP 未签名 ⇒ 只能保证编译通过 +
令牌一致,观感需要设备或签名后由人眼确认。文档里也把这条写进"验收纪律"。
2026-09-14 12:49:40 +08:00
cefd96a6a3 feat(clients): UI 设计同步到客户端 —— Electron 包重建 + 鸿蒙设计令牌
用户:「下一步就是同步 ui 设计到客户端了」。

## ① Electron 客户端(之前严重滞后)

打包产物停在 **09:11**,而前端 dist 是 **12:23** ⇒ 今天所有 UI 工作(导航合并、悬浮玻璃、
圆桌语言、Composer、动画、模糊分层…)**一个都不在包里**。已重建 AppImage + deb。

**验证**(关键:AppImage/deb 是压缩容器,`grep` 直接扫是扫不到的 ——
我第一次就差点因此得出"包里没有"的结论):把 deb 解到 /tmp 再查 `app.asar`:

    comm-tabs=2  compose-fab=1  nav-rail=2  glass-control=2  cal-slide-next=2
    构建戳 "5ce25f6·0914-1240"(与当前提交一致)

## ② 鸿蒙客户端

它是**原生 ArkTS 应用**(22 个 .ets、自带 API 层),不是 WebView 壳 ⇒ 设计要移植。

第一步做的是**共用词表**:新增 `common/Theme.ets`(品牌蓝、页面底、面、分隔线、
导航玻璃不透明度、圆角 14/8、语义色、字号),全部注明与 WebUI 令牌的对应关系
(含一个易错点:CSS 是 `rgb(r g b / a)`,鸿蒙是 `#AARRGGBB`,0.72×255≈184=0xB8)。

顺带修掉一个真 bug:TabBar 的选中色写成 `this.currentIndex === 0` ⇒
**只有第一个 tab 会高亮**。现在按每个 tab 自己的下标判断。

**编译验证**:`hvigorw assembleHap` → **BUILD SUCCESSFUL**(HAP 已打包)。
(无法在设备上跑:本机 `hdc list targets` 为空、HAP 未签名 ⇒ 视觉未验证,如实说明。)

## ③ 判据:跨客户端令牌一致性

新增 `test/cross-client-theme.test.mjs` 5 条:品牌蓝、卡片圆角(0.875rem=14px)、
导航玻璃 0.72 —— 断言的是"两边对同一件事取值一致",不约束实现方式
(CSS 变量 vs ArkTS 常量本来就该不同),并带一条扰动自检。
这种漂移**没有任何判据会红**,所以必须显式钉住。

## 还没做的(如实说明)

鸿蒙端只同步了**设计语言**,功能面不对等:鸿蒙是 收件箱/会话/联系人 三个 tab,
没有 日历/授权/管理;也没有玻璃悬浮底栏与 Compose 共用组件。要做功能对齐是另一件事,
需要单独排期(我可以按你的优先级来)。
2026-09-14 12:43:08 +08:00