# 鸿蒙客户端与 WebUI 的对齐计划 > 用户(2026-09-14):「安排对齐」「**鸿蒙 ui 应当交给对这一块更熟悉的 dsh 负责**」。 > 这份文档把**差距**与**分期**写清楚,免得每轮都从"感觉还差什么"重新猜。 > > **负责人:dsh**(2026-09-14 起由 jianf 指定)。移交信: > 线索 `harmony-ui-alignment`(`dsh@/home/program/agentmail`), > 信里交代了状态、判据纪律与踩过的坑(判据要点"用户真正会点的那一层"、 > 不许新写死颜色、不要给单个面单独做深色、模糊只由壁纸层负责、 > 列表项每项一张卡、ArkTS 编译坑、视觉不可验要如实标注)。 > > 我(pi)保留 WebUI/Electron 侧:需要两边一起加令牌之类的配合,回信给 pi。 ## 一、现在的差距(有据可查) | 能力 | WebUI | 鸿蒙 | 差距性质 | |---|---|---|---| | 收件箱 | ✅ | ✅ | — | | 会话 | ✅(通信页签) | ✅(独立 tab) | 交互不同 | | 联系人 | ✅ | ✅ | — | | **发件箱** | ✅(通信页签) | ❌ | 缺页面(数据现成) | | **授权(权限决策)** | ✅(通信页签 + 详情内决策) | ❌ | 缺页面(API 现成) | | **日历** | ✅ | ❌ | 缺页面(最大一块) | | **管理(用户管理)** | ✅(我的页底部,管理员可见) | ❌ | 缺页面 + 权限判定 | | **写信** | ✅ 共用 Composer | ✅ 独立实现 | 组件未统一 | | 底部/侧边导航 | ✅ 悬浮玻璃条 | ⚠️ 系统 TabBar | 观感不同 | | 主题 / 壁纸(账号级) | ✅ 服务端同步 | ❌ | 未接 | | 设计令牌 | ✅ `:root` | ✅ `Theme.ets` | **已对齐**(判据钉住) | ## 二、已经做完的(本轮之前) - **设计令牌共用**:`common/Theme.ets` 与 WebUI 的 `:root` 一一对应 (品牌蓝、圆角 14/8、导航玻璃 0.72、语义色、字号),并由 `client/electron/test/cross-client-theme.test.mjs` 钉住取值一致性。 - **旧调色板清除**:页面里的 `#1A73E8`(Google 蓝)/`#333333`/`#F5F7FA` 等 与 WebUI 不同的写死色值,全部换成令牌(203 处)。 - **修掉只剩第一个 tab 高亮的 bug**(`currentIndex === 0` 写死)。 ## 三、分期(按"能独立验收"切) **P1 设计语言(pi 已完成)** —— 令牌 + 颜色替换 + 编译通过。以下 P2–P6 归 dsh。 **P2 发件箱**:复用现有列表组件,接口 `/mail/sent`。验收:能看到已发邮件、 点开进详情;空态有说明。**为什么先做它**:与收件箱同构,风险最低。 **P3 授权页**:导航加一项「授权」(or 通信页内页签,见 5.2③),待决列表走 `GET /permission/pending`,决策走 `POST /permission/decide` (body `{mail_id, decision, note}`);详情内也支持决策。 **(接口名已于 2026-09-14 由 dsh 核对修正,原写的 `/permission/{id}/decide` 不存在。)** 验收:待决列表与 WebUI 同口径(未决 = 无 `permission_result`); 点同意/拒绝后状态立刻变;拒绝时可填备注(备注必须随决策送达模型 —— WebUI 侧踩过这个坑,见 `gateway/handler/permission.go` 的 Note 传递)。 **P4 主题/壁纸同步**:接 `GET/PUT /me/appearance` + 图片走带认证的 fetch。 验收:换账号后外观跟随;服务端"无记录"时以本地为准(不要用默认值覆盖 —— 同 WebUI)。 **P5 玻璃悬浮导航**:自定义底栏(圆角 + 半透明 + `backgroundBlurStyle`), 取代系统 TabBar。**注意**:视觉验收需要设备或签名 HAP, 本机 `hdc list targets` 为空 ⇒ 只能保证编译与结构,观感要人眼确认。 **P6 日历**:网格 + 事件读写 + 左右滑动翻页(复用 WebUI 的手势阈值: 水平 ≥40px、≥1.5× 垂直、<600ms)。这一块最大,单独排。 ## 四、验收纪律(照 WebUI 那套) 1. 每个页面都要有**点它**的判据(WebUI 侧就是因为只验结构没验点击, 漏掉了"侧栏点了不翻页")。 2. 判据不许只看截图:要量几何/对比度/命中区。 3. 无法验证的要**如实标注**(例如"编译通过、视觉未验"),不能写成"已完成"。 --- ## 五、交接核对(dsh,2026-09-14 收到 pi 的移交信后) 核对方式:跑构建、跑判据、读代码,不靠转述。 ### 5.1 移交信里属实的两条 - `hvigorw assembleHap --no-daemon` **BUILD SUCCESSFUL**(改前改后各跑一次)。 - `node --test client/electron/test/cross-client-theme.test.mjs` 6 条全绿。 ### 5.2 三处纠正(照原计划直接做会踩空) **① P3 的接口名写错了。** 计划里写「决策走 `POST /permission/{id}/decide`」, 实际是 `POST /api/v1/permission/decide`,body `{mail_id, decision, note}` (`server/internal/handler/permission.go:301`)。而且待决列表**不要**从收件箱里 筛 `permission_*`:WebUI 用的是 `GET /api/v1/permission/pending` (`client/electron/src/api/client.ts:623`),鸿蒙照这个走才同口径。 理由不只是"更省事":`/me/mail/inbox` 默认只取 50 封,而一个会话能连着产生 十几封权限邮件,从收件箱筛会把别的信挤出视野(WebUI 的 `mailGroups.ts` 开头就是为这件事写的)。**用 pending 接口,与 WebUI 同源。** **②"不许写死颜色"的判据原来只挡得住列出来的那 8 个旧色值。** 判据全绿的同时,`pages/` 里还留着 **14 处**另一套写死的色 —— Google/Material 调色板:`#E8F0FE`、`#E8F5E9`、`#FFF3E0`、`#D93025`、`#777777`×2、 `#555555`、`#444444`、`#cccccc`、`#aaaaaa`、遮罩 `#80000000`×2、 透明 `#00000000`×2。枚举挡不住漂移,只有"类"能挡,已改成: - `pages/` 下**一个裸色值都不许有**(`#RRGGBB` / `#AARRGGBB` 都算),颜色只能来自 `common/Theme.ets`;页面清单也改成**扫目录**(旧版硬编码 7 个文件名, 接下来要加的页面会自动逃出判据)。 - 变异测试确认能判红:往 `InboxPage.ets` 塞一个 `#E8F0FE` → 判据红;撤回 → 绿。 - 顺带把权限档位徽标的配色收进令牌:`Theme.permBg/permFg`,与 WebUI 的 `PermissionChip.tsx` **同一映射**(plan=蓝 / workspace=绿 / full=琥珀)。 原先是 `full ? 绿 : 橙`(Material 色),与 WebUI 反着来。 - 判据从 6 条加到 7 条(新增"两边权限档位配色一致",直接拿 WebUI `:root` 的 `--c-blue-*/green-*/amber-*` 比鸿蒙令牌,防"看起来差不多")。 **③ 导航的差距不止"观感不同",是信息架构不同。** WebUI(2026-09-14 用户 「导航项就只剩通信,日历,联系人」)已是:**通信(收件箱/发件箱/授权,内部页签 + 未读红/待决策橙徽标)/ 日历 / 联系人**,底部另有"我的(含管理)+ 主题 + 退出", 新建是通信页内**悬浮圆形加号**;鸿蒙还是 **收件箱 / 会话 / 联系人** 三个平级 tab, 没有发件、授权、日历,而「会话」在 WebUI 里**没有对应入口**(WebUI 是按会话把 收件箱分组,没有独立会话页)。 ⇒ P2/P3 不是"再加两个页面",得先对齐信息架构,否则加完还是两套导航。 ### 5.3 一条与鸿蒙无关、但该让 pi 知道的 WebUI 侧 `npm test` 在 **HEAD 上就是红的**(`test/background.test.mjs`): - `浅色与深色两套令牌都定义了(自动反色)` —— 期望 `--nav-bg` 有浅/深两套, 但深色那套已在 `faacd3c`(修"导航栏还是黑色")里被有意去掉, 现在 `index.css` 只剩 `--nav-bg: 255 255 255 / 0.72`。 - `导航在壁纸模式下仍参与模糊` —— 期望 `html[data-bg='on'] .nav-rail` 里有 `backdrop-filter: blur(`,而该规则现在**是空的**("模糊由壁纸层负责,这里不叠", 见 `index.css:892`)。 两条判据编码的都是**已被有意回滚的设计**,判据没跟着改。红成了常态, 判据也就不再是判据 —— 这条纪律问题比色值本身更值得先修。 ### 5.4 修订后的分期与"点它"判据 | 期 | 内容 | 判据(必须点到用户会点的那一层) | |---|---|---| | P1.5 ✅ | 裸色值清零 + 判据按类挡 + 权限档位配色统一 | 判据 7/7 绿 + 变异能判红 + 构建成功 | | P2a | 信息架构:「通信」一项,内部页签 收件箱/发件箱/授权(未读红、待决策橙徽标) | **点页签 → 断言落到哪个 pane**(不是断言页签个数);点「通信」回到上次的子页签 | | P2b | 发件箱页:`GET /me/mail/sent`,复用列表项 | 能看到已发邮件、点开进详情、空态有说明 | | P3 | 授权页:`GET /permission/pending` + `POST /permission/decide` | 未决口径与 WebUI 一致(无 `permission_result`);点同意/拒绝后状态立刻变;拒绝可填备注且**备注送达模型**;决策请求已过期(`expired`)必须当场说清"这次批准不会恢复原调用" | | P4 | 主题/壁纸(`/me/appearance`) | 换账号外观跟随;服务端无记录时以本地为准,不用默认值覆盖 | | P5 | 悬浮玻璃导航(取代系统 TabBar) | 模糊只由壁纸层负责;列表项每项一张卡;命中区 ≥44vp | | P6 | 日历(`/calendar/events`,含 ics 导入导出) | 手势阈值与 WebUI 一致(水平 ≥40px、≥1.5× 垂直、<600ms) | **视觉/交互怎么验**:本机有 `harmony-emu`(实例 `HarmonyPhone`,hdc 在 `/opt/harmonyos/ohos-sdk/linux/toolchains`),能起来就能截图 + 真点, 把"编译通过、视觉未验"变成"点过、有截图"。 **实测(2026-09-14)**:当前文件沙箱(workspace-write)下起不来 —— `harmony-emu start` 卡在等一个永远不来的 hdc(脚本要写 `/run/harmony-emulator.pid`), 直启 `Emulator -start HarmonyPhone -noWindow` 则报 `Error opening logfile /root/.Huawei/Emulator/deployed/HarmonyPhone/Log/qemu.log: Permission denied` (实例目录在 `/root` 下,沙箱不许写)。**要跑真机/模拟器判据,得先放宽文件权限 (或换有设备的机器)**;在那之前,鸿蒙侧的判据只能到"构建通过 + 结构/令牌一致", 观感与点击必须如实标注未验 —— 不许写成已完成。 ### 5.5 待定(需要 pi 或用户一句话) 1. **「会话」这个平级入口留不留?** WebUI 没有它(收件箱按会话分组)。 我倾向跟 WebUI 一致 —— 收件箱按会话折叠、去掉平级「会话」tab; 但这是删入口,改之前要一句话确认。 → **已答(见 5.6):不是删功能,是两个 tab 合成一个页面的两种视图;先补视图与折叠,再删 tab。** 2. 新增令牌 `accentStrong / warnBg / warnFg / overlay` 取的都是 WebUI **已有**的 tailwind 值(blue-700 / amber-50 / amber-700),WebUI 侧不需要动代码; `overlay` 是唯一没有对应物的(WebUI 弹层不压遮罩)。若 pi 认为 WebUI 侧也该 立同名令牌,我再改。 → **已答(见 5.6):WebUI 侧不立同名令牌;鸿蒙侧的 `overlay` 拆成 `overlayColor` + `overlayAlpha`。** --- ## 六、pi 的回复已落地(dsh,2026-09-14 下午) ### 6.1 「会话」入口的结论(pi 作为 WebUI 侧负责人) 不是删功能,是两个 tab 合成**一个页面的两种视图**:**联系人页就是会话列表** (`ContactPanel.tsx` 有列表 / 卡片两个视图 —— `ContactRow` ≈ 现在的 ContactsTab; `WorkCard`「工作列表」≈ 现在的 SessionsTab:主题、最新一封摘要、权限档位徽标、 往返预算 = 现在的 `used_rounds/max_rounds`),而**收件箱里会话是组织方式** (`mailGroups.ts` 的 `groupMailsBySession()` 按 `session_id` 折叠、组头取最新一封, `isFlatGroup()` 让单封不成组)。 落地顺序(原样采纳): 1. **先**给「联系人」页补列表 / 卡片切换,卡片视图承接预算条 / `status` / 对方 Agent; 收件箱加**按会话折叠**(组头带未读)。折叠前先看 `total` —— 别让"只取了 50 封" 被折叠伪装成"只有这么多会话"。 2. **两件都到位后**再删平级「会话」tab。在那之前删 = 丢信息:轮次预算、`status`、 `from_agent` 会无处可看,而"预算跑满"正是需要人介入的信号。 合并后导航正好是 **通信 / 日历 / 联系人** 三项,P4–P6 都在这个形态上做。 ⇒ 分期顺序调整为:**P2a 联系人双视图 + 收件箱按会话折叠 → P2b 通信页签(收件/发件/授权) → P3 授权页 → P4 主题壁纸 → P5 悬浮玻璃导航 → P6 日历 → 最后删「会话」tab**。 ### 6.2 遮罩令牌 `overlay` 拆成 `overlayColor` + `overlayAlpha`(WebUI 是 `--bg-scrim` 色 + `--bg-dim` 透明度的两段式;色要能随主题换向,焊死成一个 `#AARRGGBB` 等于把枚举写回代码)。 ArkUI 只认单值,故用 `Theme.overlay()` 组装。**WebUI 侧不立同名令牌**(立了没人用、 判据只能验"它存在",那是自证)。判据第 8 条钉住这个形态。 ### 6.3 那两条红判据的 patch:已落地,且验证过能判红 `test/background.test.mjs` 第 18 组三条按 pi 的 patch 重写:标签改回它真正断言的东西, 两条方向相反的断言取代原两条(`.dark` 不许单独给导航换色 / 模糊只由壁纸层负责)。 变异验证: - 往某个 `.dark { }` 里塞一行 `--nav-bg: 15 23 42 / 0.72;` → **红**; - 往 `html[data-bg='on'] .nav-rail` 里塞 `backdrop-filter: blur(18px);` → **红**; - 撤回 → 绿。背景判据 32 → **34 通过**。 ### 6.4 顺手发现并修掉的真 bug(构建只在日志里警告的那种) `src/background-takeover.generated.css` 的**头注释提前闭合**:生成器在注释里写了 「src」加「/」加两颗星加「/」加「.tsx」,其中那对「星号 + 斜杠」把 CSS 注释就地结束, 剩下的尾巴变成 CSS 正文、并与第一条规则的选择器连在一起 ⇒ 非法选择器 ⇒ **`.bg-amber-100` 那条接管规则被浏览器整条丢掉**(壁纸模式下它不再变半透明)。 构建只给一条 `[WARNING] Unexpected "14" [css-syntax-error]`,不报错、不影响构建。 改在生成器 `scripts/gen-background-takeover.mjs`(注释里只描述、不写 glob 字面量)并 重新生成:压缩输出现在以 `html[data-bg=on] .bg-amber-100{` 起头、14 条规则全在、无告警。 另加两条判据("头注释没提前闭合" + 自检)。 ### 6.5 一条判据从来没被跑过 `cross-client-theme.test.mjs` **不在 `npm test` 链里**(`npm test` 只跑 markdown-xss / narrow-layout / theme / background / packaging + vitest, 而 vitest 只收 `test/components` 与 `test/stores`)—— 那条"防漂移判据"从没在默认套件 里跑过。已加进 `npm test`。 ### 6.6 `npm test` 现在全绿(退出码 0) 窄屏布局全通过、主题 30、背景 34、cross-client 8、packaging 3、vitest 258 用例 / 15 文件全过。 其中 **packaging 第 3 条此前是红的**,但它**不是判据过期,是安装包真的落后于 dist** (`release/linux-unpacked/resources/app.asar` 里装的还是旧前端)。根因有点讽刺: `npm test` 的 `&&` 链一直在 background 那条就中断,**packaging 从来没跑到过**。 已 `npm run build` + `npx electron-builder --linux -c.electronDownload.isVerifyChecksum=false` 重打包。 ⚠️ **deb 目标在本机打不出来**(`fpm` 的 portable ruby 在 `Dir.chdir` 处退出), AppImage 与 `linux-unpacked` 正常 —— 交付前要确认这个环境问题不影响目标平台。 ### 6.7 WebUI 侧同源残留(pi 指出的第三处,已修) `Sidebar.tsx` 底部的**主题切换 / 退出登录**按钮原为 `text-chrome-400 hover:text-white hover:bg-chrome-800`、外层分隔线 `border-chrome-700` —— 那是"框架本来就该深"的假设。导航改白玻璃后 `chrome-400` 在白底上约 2.6:1 (图标要 3:1),hover 还会在白导航上闪出一块近黑。已换成 `.nav-item` + `border-gray-200`。 **未加"Sidebar 里不许有 chrome-*"的判据**:`Sidebar.tsx` 另有一处 `bg-chrome-600 text-chrome-100` 是实心小色块(正常用法),一刀切会误红。 ### 6.8 仍未验的 鸿蒙侧**视觉与点击**仍未验(模拟器在本机沙箱下起不来,见 5.4)—— 本轮的鸿蒙改动只有颜色令牌,判据能覆盖;**到 P2a 一定要有人眼或设备**, 否则"点页签落在哪个 pane"这条判据无法证明。