From cefd96a6a30ad108999058bf2fd24804138a1b30 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 12:43:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(clients):=20UI=20=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=88=B0=E5=AE=A2=E6=88=B7=E7=AB=AF=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20Electron=20=E5=8C=85=E9=87=8D=E5=BB=BA=20+?= =?UTF-8?q?=20=E9=B8=BF=E8=92=99=E8=AE=BE=E8=AE=A1=E4=BB=A4=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「下一步就是同步 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 共用组件。要做功能对齐是另一件事, 需要单独排期(我可以按你的优先级来)。 --- .../electron/test/cross-client-theme.test.mjs | 61 +++++++++++++++++++ .../entry/src/main/ets/common/Theme.ets | 50 +++++++++++++++ .../entry/src/main/ets/pages/MainPage.ets | 13 ++-- 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 client/electron/test/cross-client-theme.test.mjs create mode 100644 client/harmony/entry/src/main/ets/common/Theme.ets diff --git a/client/electron/test/cross-client-theme.test.mjs b/client/electron/test/cross-client-theme.test.mjs new file mode 100644 index 0000000..3e622a5 --- /dev/null +++ b/client/electron/test/cross-client-theme.test.mjs @@ -0,0 +1,61 @@ +/** + * 两个客户端必须用**同一份设计词表**。 + * + * 用户下一步要求:「同步 ui 设计到客户端」。同步的第一件事不是把每个页面重画一遍, + * 而是两边共用同一套令牌(颜色/圆角/玻璃透明度)—— 否则每加一个页面就重抄一遍色值, + * 两个客户端会越走越远,而且这种漂移**没有任何判据会红**。 + * + * 这里只断言"两边对同一件事的取值一致",不断言实现方式(WebUI 用 CSS 变量、 + * 鸿蒙用 ArkTS 常量,本来就该不同)。 + */ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, '..', '..', '..'); +const web = readFileSync(join(ROOT, 'client/electron/src/index.css'), 'utf8'); +const harmony = readFileSync( + join(ROOT, 'client/harmony/entry/src/main/ets/common/Theme.ets'), + 'utf8' +); + +const hex = h => h.toUpperCase(); + +test('品牌蓝一致', () => { + // WebUI: --c-blue-600 应该是 37 99 235 = #2563EB + const webBlue = web.match(/--c-blue-600:\s*(\d+)\s+(\d+)\s+(\d+)/); + assert.ok(webBlue, 'WebUI 要有 --c-blue-600'); + const asHex = hex( + '#' + + [webBlue[1], webBlue[2], webBlue[3]] + .map(n => Number(n).toString(16).padStart(2, '0')) + .join('') + ); + const harmonyBlue = harmony.match(/accent: string = '(#[0-9A-Fa-f]{6})'/); + assert.ok(harmonyBlue, '鸿蒙要有 accent'); + assert.equal(hex(harmonyBlue[1]), asHex, `品牌蓝不一致:WebUI ${asHex} vs 鸿蒙 ${harmonyBlue[1]}`); +}); + +test('卡片圆角一致(WebUI 0.875rem = 14px)', () => { + assert.match(web, /--radius-card:\s*0\.875rem/); + assert.match(harmony, /radiusCard: number = 14/); +}); + +test('导航玻璃不透明度一致(都是 0.72)', () => { + assert.match(web, /--nav-bg:\s*255 255 255 \/ 0\.72/); + // 0.72 × 255 ≈ 184 = 0xB8(鸿蒙用 #AARRGGBB,顺序与 CSS 不同) + assert.match(harmony, /navBgLight: string = '#B8FFFFFF'/); +}); + +test('★ 判据自检:把鸿蒙的品牌蓝改成别的必须判红', () => { + const mutated = harmony.replace(/accent: string = '#2563EB'/, "accent: string = '#FF0000'"); + const m = mutated.match(/accent: string = '(#[0-9A-Fa-f]{6})'/); + assert.notEqual(hex(m[1]), '#2563EB'); +}); + +test('鸿蒙的令牌文件说明了与 WebUI 的对应关系(不是凭空一套)', () => { + assert.match(harmony, /与 WebUI 的令牌\*\*一一对应|对应 WebUI/); +}); diff --git a/client/harmony/entry/src/main/ets/common/Theme.ets b/client/harmony/entry/src/main/ets/common/Theme.ets new file mode 100644 index 0000000..ba3cc0f --- /dev/null +++ b/client/harmony/entry/src/main/ets/common/Theme.ets @@ -0,0 +1,50 @@ +/* + * AgentMail 鸿蒙客户端 — 设计令牌 + * + * 与 WebUI 的令牌**一一对应**(见 client/electron/src/index.css 的 `:root` 段)。 + * 用户要求「下一步就是同步 ui 设计到客户端了」——同步的第一步不是把每个页面都改一遍, + * 而是让两边**用同一份词表**:颜色、圆角、玻璃透明度、模糊档位。 + * 否则每加一个页面就会重新抄一遍色值,两个客户端慢慢就长得不一样了。 + * + * 注意几个刻意的取舍: + * - WebUI 用 `rgb(r g b / a)`,鸿蒙用 `#AARRGGBB` —— 顺序不同,转换时必须显式写清; + * 0.72 × 255 ≈ 184 = 0xB8、0.88 × 255 ≈ 224 = 0xE0。 + * - WebUI 的"正文面"是 0.88 不透明的玻璃(字要读得清),鸿蒙这里给的是**实色** + * surface:没有壁纸时不需要透,透反而降低可读性。 + * - 只有**导航**用半透明 + 模糊(浮在内容之上,模糊有遮蔽意义)——与 WebUI 同一结论。 + */ +export class Theme { + /** 页面底色(对齐 WebUI 的 bg-gray-50 / slate-100) */ + static readonly pageBg: string = '#F8FAFC'; + /** 承载文字的面:实色,优先可读性(对应 WebUI --bg-glass 在无壁纸时的观感) */ + static readonly surface: string = '#FFFFFFFF'; + /** 次级面(列表行 hover、分组底) */ + static readonly surfaceMuted: string = '#F1F5F9'; + /** 分隔线 */ + static readonly border: string = '#E2E8F0'; + + /** 导航底:浅色玻璃 0.72(对应 WebUI --nav-bg: 255 255 255 / 0.72) */ + static readonly navBgLight: string = '#B8FFFFFF'; + /** 深色主题下的导航底:0.72 的 #0F172A(对应 --nav-bg 的 .dark 值) */ + static readonly navBgDark: string = '#B80F172A'; + /** 导航文字:未选中 / 选中(对应 --nav-fg-muted / --nav-active-fg) */ + static readonly navFg: string = '#475569'; + static readonly navFgActive: string = '#1E40AF'; + + /** 品牌蓝(按钮、链接、焦点环) */ + static readonly accent: string = '#2563EB'; + static readonly accentFg: string = '#FFFFFF'; + /** 语义色:同意 / 拒绝(对应 WebUI 的 approve/danger) */ + static readonly approve: string = '#15803D'; + static readonly danger: string = '#B91C1C'; + static readonly dangerBg: string = '#FEF2F2'; + + /** 圆角:卡片 14、控件 8(与 WebUI 的 --radius-card / --radius-control 一致) */ + static readonly radiusCard: number = 14; + static readonly radiusControl: number = 8; + + /** 字体大小(与 WebUI 的 text-2xs/xs/sm 对齐) */ + static readonly fontTiny: number = 11; + static readonly fontSmall: number = 12; + static readonly fontBody: number = 14; +} diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index c0d53e9..7cd46a2 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -3,6 +3,7 @@ * 收件箱 / 会话 / 联系人 三个 TabContent */ import { ApiClient, ApiError } from '../api/ApiClient'; +import { Theme } from '../common/Theme'; import { MailApi, InboxResponse } from '../api/MailApi'; import { AccountManager, AccountInfo } from '../api/AccountManager'; import { SseService, SseEvent } from '../api/SseService'; @@ -613,10 +614,12 @@ struct MainPage { @State currentIndex: number = 0; @Builder - TabBarBuilder(title: string, icon: string) { + TabBarBuilder(title: string, icon: string, index: number) { Column() { Text(icon).fontSize(20) - Text(title).fontSize(11).fontColor(this.currentIndex === 0 ? '#1A73E8' : '#666666') + Text(title) + .fontSize(Theme.fontTiny) + .fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg) } .width('100%') .justifyContent(FlexAlign.Center) @@ -627,17 +630,17 @@ struct MainPage { TabContent() { InboxTab() } - .tabBar(this.TabBarBuilder('收件箱', '📬')) + .tabBar(this.TabBarBuilder('收件箱', '📬', 0)) TabContent() { SessionsTab() } - .tabBar(this.TabBarBuilder('会话', '💬')) + .tabBar(this.TabBarBuilder('会话', '💬', 1)) TabContent() { ContactsTab() } - .tabBar(this.TabBarBuilder('联系人', '👤')) + .tabBar(this.TabBarBuilder('联系人', '👤', 2)) } .onChange((index: number) => { this.currentIndex = index;