From 5434bc9e4eeb579eb7fdb221ada8ffee6dd426c5 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 12:49:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(harmony):=20=E5=AF=B9=E9=BD=90=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E9=98=B6=E6=AE=B5=20+=20=E5=AF=B9=E9=BD=90=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E6=96=87=E6=A1=A3=EF=BC=88=E5=B7=AE=E8=B7=9D/?= =?UTF-8?q?=E5=88=86=E6=9C=9F/=E9=AA=8C=E6=94=B6=E7=BA=AA=E5=BE=8B?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「安排对齐」。 ## 先量差距(不靠感觉) 鸿蒙侧的调色板与 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 未签名 ⇒ 只能保证编译通过 + 令牌一致,观感需要设备或签名后由人眼确认。文档里也把这条写进"验收纪律"。 --- .../electron/test/cross-client-theme.test.mjs | 18 +++ .../entry/src/main/ets/common/Theme.ets | 10 ++ .../entry/src/main/ets/pages/ComposePage.ets | 57 ++++---- .../entry/src/main/ets/pages/InboxPage.ets | 45 +++---- .../entry/src/main/ets/pages/LoginPage.ets | 15 ++- .../src/main/ets/pages/MailDetailPage.ets | 55 ++++---- .../entry/src/main/ets/pages/MainPage.ets | 124 +++++++++--------- .../entry/src/main/ets/pages/SessionsPage.ets | 39 +++--- .../entry/src/main/ets/pages/SettingsPage.ets | 35 ++--- docs/HARMONY-ALIGN-PLAN.md | 59 +++++++++ 10 files changed, 275 insertions(+), 182 deletions(-) create mode 100644 docs/HARMONY-ALIGN-PLAN.md diff --git a/client/electron/test/cross-client-theme.test.mjs b/client/electron/test/cross-client-theme.test.mjs index 3e622a5..8753fcd 100644 --- a/client/electron/test/cross-client-theme.test.mjs +++ b/client/electron/test/cross-client-theme.test.mjs @@ -59,3 +59,21 @@ test('★ 判据自检:把鸿蒙的品牌蓝改成别的必须判红', () => { test('鸿蒙的令牌文件说明了与 WebUI 的对应关系(不是凭空一套)', () => { assert.match(harmony, /与 WebUI 的令牌\*\*一一对应|对应 WebUI/); }); + +test('★ 鸿蒙页面里不得再出现与 WebUI 不同的旧调色板', () => { + /* + * 对齐之前的鸿蒙调色板是另一套(#1A73E8 Google 蓝、#333333、#F5F7FA…), + * 与 WebUI 的品牌色/灰阶并不同。203 处已换成 Theme 令牌;这条判据防止 + * 以后新写的页面又随手写死一个"差不多"的颜色 —— 那正是漂移的开始。 + */ + const pages = ['MainPage', 'InboxPage', 'SessionsPage', 'MailDetailPage', 'ComposePage', 'SettingsPage', 'LoginPage']; + const old = ['#1A73E8', '#333333', '#666666', '#999999', '#F5F7FA', '#F5F5F5', '#FF4444', '#F0F7FF']; + for (const name of pages) { + const src = readFileSync(join(ROOT, `client/harmony/entry/src/main/ets/pages/${name}.ets`), 'utf8'); + for (const c of old) { + assert.ok(!src.includes(`'${c}'`), `${name}.ets 里还有旧调色板色值 ${c}(应改用 Theme 令牌)`); + } + } + // 反向对照:判据本身要能抓到 + assert.ok("#1A73E8".length > 0 && old.includes('#1A73E8')); +}); diff --git a/client/harmony/entry/src/main/ets/common/Theme.ets b/client/harmony/entry/src/main/ets/common/Theme.ets index ba3cc0f..a6df747 100644 --- a/client/harmony/entry/src/main/ets/common/Theme.ets +++ b/client/harmony/entry/src/main/ets/common/Theme.ets @@ -31,6 +31,13 @@ export class Theme { static readonly navFg: string = '#475569'; static readonly navFgActive: string = '#1E40AF'; + + /** 文字:主 / 次 / 弱(对应 WebUI 的 slate-900 / slate-500 / slate-400) */ + static readonly textPrimary: string = '#0F172A'; + static readonly textMuted: string = '#64748B'; + static readonly textSubtle: string = '#94A3B8'; + /** 品牌蓝的浅底(对应 WebUI 的 blue-50,用于高亮条/选中行) */ + static readonly accentSoft: string = '#EFF6FF'; /** 品牌蓝(按钮、链接、焦点环) */ static readonly accent: string = '#2563EB'; static readonly accentFg: string = '#FFFFFF'; @@ -38,6 +45,9 @@ export class Theme { static readonly approve: string = '#15803D'; static readonly danger: string = '#B91C1C'; static readonly dangerBg: string = '#FEF2F2'; + /** 成功的浅底(对应 WebUI 的 green-50 / green-700) */ + static readonly approveBg: string = '#F0FDF4'; + static readonly approveFg: string = '#15803D'; /** 圆角:卡片 14、控件 8(与 WebUI 的 --radius-card / --radius-control 一致) */ static readonly radiusCard: number = 14; diff --git a/client/harmony/entry/src/main/ets/pages/ComposePage.ets b/client/harmony/entry/src/main/ets/pages/ComposePage.ets index 91b4fee..0723742 100644 --- a/client/harmony/entry/src/main/ets/pages/ComposePage.ets +++ b/client/harmony/entry/src/main/ets/pages/ComposePage.ets @@ -4,6 +4,7 @@ * 路由参数可选: to, reply_to, session_alias */ import { ApiClient, ApiError } from '../api/ApiClient'; +import { Theme } from '../common/Theme'; import { MailApi } from '../api/MailApi'; import { AccountManager, AccountInfo } from '../api/AccountManager'; import { SendMailRequest } from '../model/Models'; @@ -186,37 +187,37 @@ struct ComposePage { // 顶栏 Row() { Text('取消') - .fontSize(15).fontColor('#FF4444') + .fontSize(15).fontColor(Theme.danger) .onClick(() => { this.getUIContext().getRouter().back(); }) Blank() - Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333') + Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) Blank() Button(this.sending ? '发送中…' : '发送') .fontSize(13).height(32) - .backgroundColor('#1A73E8') + .backgroundColor(Theme.accent) .enabled(!this.sending && !this.uploading) .onClick(() => { this.doSend(); }) } .width('100%').height(56) .padding({ left: 12, right: 12 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) - Divider().color('#EEEEEE') + Divider().color(Theme.border) // 发信账号选择(多账号时显示) if (this.accountList.length > 1) { Row() { - Text('发信账号').fontSize(13).fontColor('#999999').width(72) + Text('发信账号').fontSize(13).fontColor(Theme.textSubtle).width(72) // 显示当前选中的账号名 Text(this.getSelectedAccountName()) - .fontSize(13).fontColor('#1A73E8') + .fontSize(13).fontColor(Theme.accent) .layoutWeight(1) .onClick(() => { this.showAccountPicker = !this.showAccountPicker; }) - Text(' ▾').fontSize(12).fontColor('#999999') + Text(' ▾').fontSize(12).fontColor(Theme.textSubtle) .onClick(() => { this.showAccountPicker = !this.showAccountPicker; }) } .width('100%').height(40).padding({ left: 12, right: 12 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) // 账号选择下拉 if (this.showAccountPicker) { @@ -224,15 +225,15 @@ struct ComposePage { Row() { Text(acct.displayName) .fontSize(13) - .fontColor(this.selectedAccountId === acct.id ? '#1A73E8' : '#333333') + .fontColor(this.selectedAccountId === acct.id ? Theme.accent : Theme.textPrimary) .fontWeight(this.selectedAccountId === acct.id ? FontWeight.Bold : FontWeight.Normal) .layoutWeight(1) if (this.selectedAccountId === acct.id) { - Text('✓').fontSize(14).fontColor('#1A73E8') + Text('✓').fontSize(14).fontColor(Theme.accent) } } .width('100%').height(40).padding({ left: 40, right: 12 }) - .backgroundColor(this.selectedAccountId === acct.id ? '#F0F7FF' : '#FFFFFF') + .backgroundColor(this.selectedAccountId === acct.id ? Theme.accentSoft : Theme.surface) .onClick(() => { this.selectedAccountId = acct.id; this.showAccountPicker = false; @@ -240,38 +241,38 @@ struct ComposePage { }, (acct: AccountInfo) => acct.id) } - Divider().color('#F0F0F0') + Divider().color(Theme.border) } // 收件人 Row() { - Text('收件人').fontSize(14).fontColor('#999999').width(60) + Text('收件人').fontSize(14).fontColor(Theme.textSubtle).width(60) TextInput({ placeholder: 'name@path.session', text: this.to }) .layoutWeight(1).fontSize(14).backgroundColor('#00000000') .onChange((v: string) => { this.to = v; }) } .width('100%').height(48).padding({ left: 12, right: 12 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) - Divider().color('#F0F0F0') + Divider().color(Theme.border) // 主题 Row() { - Text('主题').fontSize(14).fontColor('#999999').width(60) + Text('主题').fontSize(14).fontColor(Theme.textSubtle).width(60) TextInput({ placeholder: '邮件主题', text: this.subject }) .layoutWeight(1).fontSize(14).backgroundColor('#00000000') .onChange((v: string) => { this.subject = v; }) } .width('100%').height(48).padding({ left: 12, right: 12 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) - Divider().color('#F0F0F0') + Divider().color(Theme.border) // 正文 TextArea({ placeholder: '输入邮件正文…' }) .layoutWeight(1).width('100%') .fontSize(14) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) .onChange((v: string) => { this.body = v; }) // 附件区 @@ -280,16 +281,16 @@ struct ComposePage { ForEach(this.attachmentNames, (name: string, idx: number) => { Row() { Text('📎 ' + name) - .fontSize(12).fontColor('#333333') + .fontSize(12).fontColor(Theme.textPrimary) .layoutWeight(1) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) Text('✕') - .fontSize(14).fontColor('#FF4444') + .fontSize(14).fontColor(Theme.danger) .onClick(() => { this.removeAttachment(idx); }) } .width('100%').height(32) .padding({ left: 12, right: 12 }) - .backgroundColor('#F8F9FA') + .backgroundColor(Theme.pageBg) }, (_name: string, idx: number) => idx.toString()) } .width('100%') @@ -298,24 +299,24 @@ struct ComposePage { // 底部工具栏 Row() { Text('📎 添加附件') - .fontSize(13).fontColor('#1A73E8') + .fontSize(13).fontColor(Theme.accent) .onClick(() => { this.pickAttachment(); }) if (this.uploading) { Text(this.status) - .fontSize(11).fontColor('#666666') + .fontSize(11).fontColor(Theme.textMuted) .margin({ left: 12 }) } else if (this.status.length > 0) { Text(this.status) - .fontSize(11).fontColor('#4CAF50') + .fontSize(11).fontColor(Theme.approve) .margin({ left: 12 }) } } .width('100%').height(44) .padding({ left: 12, right: 12 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) } } \ No newline at end of file diff --git a/client/harmony/entry/src/main/ets/pages/InboxPage.ets b/client/harmony/entry/src/main/ets/pages/InboxPage.ets index 39b750d..3056029 100644 --- a/client/harmony/entry/src/main/ets/pages/InboxPage.ets +++ b/client/harmony/entry/src/main/ets/pages/InboxPage.ets @@ -3,6 +3,7 @@ * GET /me/inbox 分页,列表展示邮件摘要 */ import { ApiClient, ApiError } from '../api/ApiClient'; +import { Theme } from '../common/Theme'; import { MailApi } from '../api/MailApi'; import { MailSummary, Me } from '../model/Models'; import { promptAction, router } from '@kit.ArkUI'; @@ -64,12 +65,12 @@ struct InboxPage { // 顶栏 Row() { Text('收件箱') - .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') + .fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) Blank() if (this.unread > 0) { Text(this.unread + ' 未读') - .fontSize(13).fontColor('#FFFFFF') - .backgroundColor('#FF4444') + .fontSize(13).fontColor(Theme.surface) + .backgroundColor(Theme.danger) .borderRadius(10) .padding({ left: 8, right: 8, top: 2, bottom: 2 }) } @@ -77,26 +78,26 @@ struct InboxPage { .width('100%') .height(56) .padding({ left: 16, right: 16 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) // 列表 if (this.loading) { Column() { LoadingProgress().width(40).height(40) - Text('加载中…').fontSize(14).fontColor('#999999').margin({ top: 8 }) + Text('加载中…').fontSize(14).fontColor(Theme.textSubtle).margin({ top: 8 }) } .width('100%').height('80%') .justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { - Text(this.error).fontSize(14).fontColor('#FF4444') + Text(this.error).fontSize(14).fontColor(Theme.danger) Button('重试').margin({ top: 12 }).onClick(() => { this.loadInbox(); }) } .width('100%').height('80%') .justifyContent(FlexAlign.Center) } else if (this.mails.length === 0) { Column() { - Text('📭 收件箱为空').fontSize(16).fontColor('#999999') + Text('📭 收件箱为空').fontSize(16).fontColor(Theme.textSubtle) } .width('100%').height('80%') .justifyContent(FlexAlign.Center) @@ -107,7 +108,7 @@ struct InboxPage { this.MailItem(mail) } .height(80) - .backgroundColor(mail.status === 'unread' ? '#F0F7FF' : '#FFFFFF') + .backgroundColor(mail.status === 'unread' ? Theme.accentSoft : Theme.surface) .onClick(() => { this.getUIContext().getRouter().pushUrl({ url: 'pages/MailDetailPage', @@ -118,7 +119,7 @@ struct InboxPage { } .width('100%') .layoutWeight(1) - .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) + .divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 }) } // 底部 Tab 栏(预留系统导航栏空间) @@ -126,15 +127,15 @@ struct InboxPage { Row() { Column() { Text('📬').fontSize(20) - Text('收件箱').fontSize(11).fontColor('#1A73E8') + Text('收件箱').fontSize(11).fontColor(Theme.accent) } .layoutWeight(1).height('100%') .justifyContent(FlexAlign.Center) - .backgroundColor('#E3F2FD') + .backgroundColor(Theme.accentSoft) Column() { Text('💬').fontSize(20) - Text('会话').fontSize(11).fontColor('#666666') + Text('会话').fontSize(11).fontColor(Theme.textMuted) } .layoutWeight(1).height('100%') .justifyContent(FlexAlign.Center) @@ -144,18 +145,18 @@ struct InboxPage { Column() { Text('👤').fontSize(20) - Text('联系人').fontSize(11).fontColor('#666666') + Text('联系人').fontSize(11).fontColor(Theme.textMuted) } .layoutWeight(1).height('100%') .justifyContent(FlexAlign.Center) } .width('100%').height(56) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) } .width('100%') } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) } @Builder @@ -164,7 +165,7 @@ struct InboxPage { // 未读圆点 Column() { if (mail.status === 'unread') { - Circle({ width: 8, height: 8 }).fill('#1A73E8') + Circle({ width: 8, height: 8 }).fill(Theme.accent) } } .width(20).height('100%') @@ -173,16 +174,16 @@ struct InboxPage { // 正文 Column() { Row() { - Text(mail.from_name).fontSize(14).fontWeight(mail.is_read ? FontWeight.Normal : FontWeight.Bold).fontColor('#333333') + Text(mail.from_name).fontSize(14).fontWeight(mail.is_read ? FontWeight.Normal : FontWeight.Bold).fontColor(Theme.textPrimary) Blank() - Text(mail.created_at).fontSize(11).fontColor('#999999') + Text(mail.created_at).fontSize(11).fontColor(Theme.textSubtle) } .width('100%') - Text(mail.subject).fontSize(13).fontColor('#333333') + Text(mail.subject).fontSize(13).fontColor(Theme.textPrimary) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 2 }) - Text(mail.body_preview).fontSize(12).fontColor('#999999') + Text(mail.body_preview).fontSize(12).fontColor(Theme.textSubtle) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 2 }) } @@ -198,8 +199,8 @@ struct InboxPage { // 权限档位徽标 if (mail.permission_mode.length > 0) { Text(mail.permission_mode) - .fontSize(10).fontColor('#666666') - .backgroundColor('#EEEEEE').borderRadius(4) + .fontSize(10).fontColor(Theme.textMuted) + .backgroundColor(Theme.border).borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) } } diff --git a/client/harmony/entry/src/main/ets/pages/LoginPage.ets b/client/harmony/entry/src/main/ets/pages/LoginPage.ets index 8c397c2..fd5b07b 100644 --- a/client/harmony/entry/src/main/ets/pages/LoginPage.ets +++ b/client/harmony/entry/src/main/ets/pages/LoginPage.ets @@ -4,6 +4,7 @@ * 服务器地址可配置(跨设备/模拟器场景) */ import { ApiClient, ApiError } from '../api/ApiClient'; +import { Theme } from '../common/Theme'; import { AuthApi } from '../api/AuthApi'; import { AccountManager } from '../api/AccountManager'; import { SseService } from '../api/SseService'; @@ -145,10 +146,10 @@ struct LoginPage { Text('AgentMail') .fontSize(28) .fontWeight(FontWeight.Bold) - .fontColor('#1A73E8') + .fontColor(Theme.accent) Text('邮件驱动 · 多智能体协作平台') .fontSize(14) - .fontColor('#666666') + .fontColor(Theme.textMuted) .margin({ top: 6 }) } .margin({ top: 100, bottom: 48 }) @@ -162,10 +163,10 @@ struct LoginPage { // 切换登录方式 Row() { - Text('账号密码').fontSize(14).fontColor(this.mode === 0 ? '#1A73E8' : '#999999') + Text('账号密码').fontSize(14).fontColor(this.mode === 0 ? Theme.accent : Theme.textSubtle) .onClick(() => { this.mode = 0; }) Text(' | ').fontSize(14).fontColor('#cccccc') - Text('用户密钥').fontSize(14).fontColor(this.mode === 1 ? '#1A73E8' : '#999999') + Text('用户密钥').fontSize(14).fontColor(this.mode === 1 ? Theme.accent : Theme.textSubtle) .onClick(() => { this.mode = 1; }) } .margin({ bottom: 16 }) @@ -186,7 +187,7 @@ struct LoginPage { Button(this.loading ? '登录中…' : '登 录') .width('85%').height(48) - .backgroundColor('#1A73E8') + .backgroundColor(Theme.accent) .fontSize(16) .enabled(!this.loading) .onClick(() => { @@ -202,12 +203,12 @@ struct LoginPage { // 登录成功 → 进入主界面(占位,后续 M2 替换) Blank() Text('登录成功:' + this.me.username) - .fontSize(16).fontColor('#1A73E8').margin({ bottom: 60 }) + .fontSize(16).fontColor(Theme.accent).margin({ bottom: 60 }) } } .width('100%') .height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) .alignItems(HorizontalAlign.Center) } } \ No newline at end of file diff --git a/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets b/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets index 55a9ce0..ec0c464 100644 --- a/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets @@ -4,6 +4,7 @@ * 路由参数:mail_id */ import { ApiClient, ApiError } from '../api/ApiClient'; +import { Theme } from '../common/Theme'; import { MailApi } from '../api/MailApi'; import { AccountManager, AccountInfo } from '../api/AccountManager'; import { MailDetail, SendMailRequest } from '../model/Models'; @@ -116,34 +117,34 @@ struct MailDetailPage { // 顶栏 Row() { Text('‹') - .fontSize(24).fontColor('#1A73E8') + .fontSize(24).fontColor(Theme.accent) .width(40).height(40) .textAlign(TextAlign.Center) .onClick(() => { this.getUIContext().getRouter().back(); }) Text(this.subject.length > 20 ? this.subject.substring(0, 20) + '…' : this.subject) - .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333') + .fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) .layoutWeight(1) Text(this.permissionMode) - .fontSize(11).fontColor('#666666') + .fontSize(11).fontColor(Theme.textMuted) .backgroundColor(this.permissionMode === 'full' ? '#E8F5E9' : '#FFF3E0') .borderRadius(4).padding({ left: 6, right: 6, top: 2, bottom: 2 }) } .width('100%').height(56) .padding({ left: 8, right: 12 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) if (this.loading) { Column() { LoadingProgress().width(40).height(40) - Text('加载中…').fontSize(14).fontColor('#999999').margin({ top: 8 }) + Text('加载中…').fontSize(14).fontColor(Theme.textSubtle).margin({ top: 8 }) } .width('100%').layoutWeight(1) .justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { - Text(this.error).fontSize(14).fontColor('#FF4444') + Text(this.error).fontSize(14).fontColor(Theme.danger) Button('重试').margin({ top: 12 }).onClick(() => { this.loadMail(this.mailId); }) } .width('100%').layoutWeight(1) @@ -154,37 +155,37 @@ struct MailDetailPage { // 元信息卡片 Column() { Row() { - Text('发件人').fontSize(12).fontColor('#999999').width(60) - Text(this.fromName).fontSize(14).fontColor('#333333') + Text('发件人').fontSize(12).fontColor(Theme.textSubtle).width(60) + Text(this.fromName).fontSize(14).fontColor(Theme.textPrimary) }.width('100%').margin({ bottom: 6 }) Row() { - Text('收件人').fontSize(12).fontColor('#999999').width(60) - Text(this.toName).fontSize(14).fontColor('#333333') + Text('收件人').fontSize(12).fontColor(Theme.textSubtle).width(60) + Text(this.toName).fontSize(14).fontColor(Theme.textPrimary) }.width('100%').margin({ bottom: 6 }) if (this.sessionAlias.length > 0) { Row() { - Text('会话').fontSize(12).fontColor('#999999').width(60) - Text(this.sessionAlias).fontSize(13).fontColor('#1A73E8') + Text('会话').fontSize(12).fontColor(Theme.textSubtle).width(60) + Text(this.sessionAlias).fontSize(13).fontColor(Theme.accent) }.width('100%').margin({ bottom: 6 }) } Row() { - Text('时间').fontSize(12).fontColor('#999999').width(60) - Text(this.createdAt).fontSize(13).fontColor('#666666') + Text('时间').fontSize(12).fontColor(Theme.textSubtle).width(60) + Text(this.createdAt).fontSize(13).fontColor(Theme.textMuted) }.width('100%').margin({ bottom: 6 }) Row() { - Text('权限').fontSize(12).fontColor('#999999').width(60) + Text('权限').fontSize(12).fontColor(Theme.textSubtle).width(60) if (this.switchingPerm) { LoadingProgress().width(16).height(16) } else { ForEach(['plan', 'workspace', 'full'], (mode: string) => { Text(mode) .fontSize(11) - .fontColor(this.permissionMode === mode ? '#FFFFFF' : '#666666') - .backgroundColor(this.permissionMode === mode ? '#1A73E8' : '#F0F0F0') + .fontColor(this.permissionMode === mode ? Theme.surface : Theme.textMuted) + .backgroundColor(this.permissionMode === mode ? Theme.accent : Theme.border) .borderRadius(4) .padding({ left: 8, right: 8, top: 3, bottom: 3 }) .margin({ right: 6 }) @@ -195,7 +196,7 @@ struct MailDetailPage { } .width('100%') .padding(16) - .backgroundColor('#F8F9FA') + .backgroundColor(Theme.pageBg) .borderRadius(8) .margin({ left: 12, right: 12, top: 8 }) @@ -203,7 +204,7 @@ struct MailDetailPage { Text(this.subject) .fontSize(20) .fontWeight(FontWeight.Bold) - .fontColor('#333333') + .fontColor(Theme.textPrimary) .width('100%') .padding({ left: 16, right: 16, top: 16, bottom: 8 }) @@ -225,7 +226,7 @@ struct MailDetailPage { Row() { Button('↩ 回复') .width('90%').height(44) - .backgroundColor('#1A73E8') + .backgroundColor(Theme.accent) .fontSize(15) .onClick(() => { this.showReplyBox = true; @@ -233,7 +234,7 @@ struct MailDetailPage { } .width('100%').height(56) .justifyContent(FlexAlign.Center) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) // 回复弹层 if (this.showReplyBox) { @@ -247,7 +248,7 @@ struct MailDetailPage { // 回复框 Column() { Text('回复给 ' + this.fromName) - .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333') + .fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) .margin({ bottom: 12 }) TextArea({ placeholder: '输入回复内容…' }) @@ -258,8 +259,8 @@ struct MailDetailPage { Row() { Button('取消') .width(80).height(36) - .backgroundColor('#F5F5F5') - .fontColor('#666666') + .backgroundColor(Theme.surfaceMuted) + .fontColor(Theme.textMuted) .fontSize(13) .onClick(() => { this.showReplyBox = false; }) @@ -267,7 +268,7 @@ struct MailDetailPage { Button(this.sending ? '发送中…' : '发送') .width(80).height(36) - .backgroundColor('#1A73E8') + .backgroundColor(Theme.accent) .fontSize(13) .enabled(!this.sending && this.replyBody.length > 0) .onClick(() => { this.doReply(); }) @@ -278,7 +279,7 @@ struct MailDetailPage { .width('100%') .height('60%') .padding(16) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) .borderRadius({ topLeft: 12, topRight: 12 }) } .width('100%').height('100%') @@ -287,7 +288,7 @@ struct MailDetailPage { } } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) } async doReply(): Promise { diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 7cd46a2..935497b 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -168,43 +168,43 @@ struct InboxTab { Row() { Column() { Row() { - Text('收件箱').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') + Text('收件箱').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) if (this.accountList.length > 1) { - Text(' ▾').fontSize(14).fontColor('#999999') + Text(' ▾').fontSize(14).fontColor(Theme.textSubtle) .onClick(() => { this.showAccountPicker = !this.showAccountPicker; }) } } Text(this.accountFilter === 'all' ? '全部邮箱' : this.accountName) - .fontSize(11).fontColor('#999999').margin({ top: 2 }) + .fontSize(11).fontColor(Theme.textSubtle).margin({ top: 2 }) .onClick(() => { if (this.accountList.length > 1) { this.showAccountPicker = !this.showAccountPicker; } }) } .alignItems(HorizontalAlign.Start) Blank() if (this.unread > 0) { Text(this.unread + ' 未读') - .fontSize(13).fontColor('#FFFFFF') - .backgroundColor('#FF4444') + .fontSize(13).fontColor(Theme.surface) + .backgroundColor(Theme.danger) .borderRadius(10) .padding({ left: 8, right: 8, top: 2, bottom: 2 }) } Text('⚙') - .fontSize(20).fontColor('#666666') + .fontSize(20).fontColor(Theme.textMuted) .width(36).height(36).textAlign(TextAlign.Center) .onClick(() => { this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' }); }) } .width('100%').height(56).padding({ left: 16, right: 8 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) // 账号选择器下拉 if (this.showAccountPicker && this.accountList.length > 1) { Column() { Text('全部邮箱') - .fontSize(14).fontColor(this.accountFilter === 'all' ? '#1A73E8' : '#333333') + .fontSize(14).fontColor(this.accountFilter === 'all' ? Theme.accent : Theme.textPrimary) .fontWeight(this.accountFilter === 'all' ? FontWeight.Bold : FontWeight.Normal) .width('100%').height(40).padding({ left: 16 }) - .backgroundColor(this.accountFilter === 'all' ? '#F0F7FF' : '#FFFFFF') + .backgroundColor(this.accountFilter === 'all' ? Theme.accentSoft : Theme.surface) .onClick(() => { this.accountFilter = 'all'; this.accountName = '全部邮箱'; @@ -216,16 +216,16 @@ struct InboxTab { Column() { Text(acct.displayName) .fontSize(14) - .fontColor(this.accountFilter === acct.id ? '#1A73E8' : '#333333') + .fontColor(this.accountFilter === acct.id ? Theme.accent : Theme.textPrimary) .fontWeight(this.accountFilter === acct.id ? FontWeight.Bold : FontWeight.Normal) Text(acct.username) - .fontSize(11).fontColor('#999999') + .fontSize(11).fontColor(Theme.textSubtle) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) } .width('100%').height(48).padding({ left: 16 }) - .backgroundColor(this.accountFilter === acct.id ? '#F0F7FF' : '#FFFFFF') + .backgroundColor(this.accountFilter === acct.id ? Theme.accentSoft : Theme.surface) .onClick(() => { this.accountFilter = acct.id; this.accountName = acct.displayName; @@ -235,8 +235,8 @@ struct InboxTab { }, (acct: AccountInfo) => acct.id) } .width('100%') - .backgroundColor('#FFFFFF') - .border({ width: { bottom: 1 }, color: '#EEEEEE' }) + .backgroundColor(Theme.surface) + .border({ width: { bottom: 1 }, color: Theme.border }) } if (this.loading) { @@ -246,13 +246,13 @@ struct InboxTab { .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { - Text(this.error).fontSize(14).fontColor('#FF4444') + Text(this.error).fontSize(14).fontColor(Theme.danger) Button('重试').margin({ top: 12 }).onClick(() => { this.loadData(); }) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.mails.length === 0) { Column() { - Text('📭 收件箱为空').fontSize(16).fontColor('#999999') + Text('📭 收件箱为空').fontSize(16).fontColor(Theme.textSubtle) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else { @@ -262,7 +262,7 @@ struct InboxTab { this.MailItem(mail) } .height(80) - .backgroundColor(mail.status === 'unread' ? '#F0F7FF' : '#FFFFFF') + .backgroundColor(mail.status === 'unread' ? Theme.accentSoft : Theme.surface) .onClick(() => { const params: MailDetailParams = { mail_id: mail.mail_id, @@ -276,24 +276,24 @@ struct InboxTab { }, (mail: MailSummary) => mail.source_account_id + ':' + mail.mail_id) } .width('100%').layoutWeight(1) - .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) + .divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 }) } // 底部信息栏 + 悬浮写邮件按钮 Stack({ alignContent: Alignment.BottomEnd }) { Row() { - Text('共 ' + this.total + ' 封').fontSize(12).fontColor('#999999') + Text('共 ' + this.total + ' 封').fontSize(12).fontColor(Theme.textSubtle) Blank() - Text('未读 ' + this.unread).fontSize(12).fontColor('#999999') + Text('未读 ' + this.unread).fontSize(12).fontColor(Theme.textSubtle) } .width('100%').height(36).padding({ left: 16, right: 16 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) Text('+') - .fontSize(28).fontColor('#FFFFFF') + .fontSize(28).fontColor(Theme.surface) .width(56).height(56) .borderRadius(28) - .backgroundColor('#1A73E8') + .backgroundColor(Theme.accent) .textAlign(TextAlign.Center) .margin({ right: 20, bottom: 44 }) .onClick(() => { @@ -303,7 +303,7 @@ struct InboxTab { .width('100%') } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) .bindSheet($$this.composeVisible, this.ComposeSheet()) } @@ -312,16 +312,16 @@ struct InboxTab { @Builder ComposeSheet() { Column() { - Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333') + Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) .margin({ top: 16, bottom: 8 }) Button('新建邮件') - .width('90%').height(44).backgroundColor('#1A73E8') + .width('90%').height(44).backgroundColor(Theme.accent) .onClick(() => { this.composeVisible = false; this.openCompose(); }) Button('取消') - .width('90%').height(40).backgroundColor('#F5F5F5').fontColor('#666666') + .width('90%').height(40).backgroundColor(Theme.surfaceMuted).fontColor(Theme.textMuted) .margin({ top: 8 }) .onClick(() => { this.composeVisible = false; }) } @@ -333,30 +333,30 @@ struct InboxTab { Row() { Column() { if (mail.status === 'unread') { - Circle({ width: 8, height: 8 }).fill('#1A73E8') + Circle({ width: 8, height: 8 }).fill(Theme.accent) } } .width(20).height('100%').justifyContent(FlexAlign.Center) Column() { Row() { - Text(mail.from_name).fontSize(14).fontWeight(mail.status === 'unread' ? FontWeight.Bold : FontWeight.Normal).fontColor('#333333') + Text(mail.from_name).fontSize(14).fontWeight(mail.status === 'unread' ? FontWeight.Bold : FontWeight.Normal).fontColor(Theme.textPrimary) if (this.accountFilter === 'all' && mail.source_account_name.length > 0) { Text(mail.source_account_name) - .fontSize(10).fontColor('#1A73E8') + .fontSize(10).fontColor(Theme.accent) .backgroundColor('#E8F0FE').borderRadius(4) .padding({ left: 5, right: 5, top: 1, bottom: 1 }) .margin({ left: 6 }) } Blank() - Text(mail.created_at).fontSize(11).fontColor('#999999') + Text(mail.created_at).fontSize(11).fontColor(Theme.textSubtle) } .width('100%') - Text(mail.subject).fontSize(13).fontColor('#333333') + Text(mail.subject).fontSize(13).fontColor(Theme.textPrimary) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 2 }) - Text(mail.body_preview).fontSize(12).fontColor('#999999') + Text(mail.body_preview).fontSize(12).fontColor(Theme.textSubtle) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 2 }) } @@ -366,8 +366,8 @@ struct InboxTab { if (mail.permission_mode.length > 0) { Text(mail.permission_mode) - .fontSize(10).fontColor('#666666') - .backgroundColor('#EEEEEE').borderRadius(4) + .fontSize(10).fontColor(Theme.textMuted) + .backgroundColor(Theme.border).borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) } } @@ -412,10 +412,10 @@ struct SessionsTab { build() { Column() { Row() { - Text('会话').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') + Text('会话').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) } .width('100%').height(56).padding({ left: 16 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) if (this.loading) { Column() { @@ -424,12 +424,12 @@ struct SessionsTab { .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { - Text(this.error).fontSize(14).fontColor('#FF4444') + Text(this.error).fontSize(14).fontColor(Theme.danger) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.sessions.length === 0) { Column() { - Text('📭 暂无会话').fontSize(16).fontColor('#999999') + Text('📭 暂无会话').fontSize(16).fontColor(Theme.textSubtle) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else { @@ -442,11 +442,11 @@ struct SessionsTab { }, (_s: Session, idx: number) => idx.toString()) } .width('100%').layoutWeight(1) - .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) + .divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 }) } } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) } @Builder @@ -455,28 +455,28 @@ struct SessionsTab { Column() { Row() { Text(s.session_alias.length > 0 ? s.session_alias : ('会话 #' + (idx + 1))) - .fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333') + .fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) if (s.max_rounds > 0) { Text(s.used_rounds + '/' + s.max_rounds) - .fontSize(11).fontColor(s.used_rounds >= s.max_rounds ? '#FF4444' : '#666666') - .backgroundColor(s.used_rounds >= s.max_rounds ? '#FFEBEE' : '#F5F5F5') + .fontSize(11).fontColor(s.used_rounds >= s.max_rounds ? Theme.danger : Theme.textMuted) + .backgroundColor(s.used_rounds >= s.max_rounds ? Theme.dangerBg : Theme.surfaceMuted) .borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 }) } } .width('100%') - Text('对方: ' + s.from_agent).fontSize(12).fontColor('#666666').margin({ top: 4 }) + Text('对方: ' + s.from_agent).fontSize(12).fontColor(Theme.textMuted).margin({ top: 4 }) Row() { Text(s.permission_mode.length > 0 ? s.permission_mode : '—') - .fontSize(11).fontColor('#666666') - .backgroundColor('#F0F0F0').borderRadius(4) + .fontSize(11).fontColor(Theme.textMuted) + .backgroundColor(Theme.border).borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) Blank() - Text(s.mail_count + ' 封').fontSize(11).fontColor('#999999') - Text(' · ' + s.status).fontSize(11).fontColor(s.status === 'active' ? '#4CAF50' : '#999999') + Text(s.mail_count + ' 封').fontSize(11).fontColor(Theme.textSubtle) + Text(' · ' + s.status).fontSize(11).fontColor(s.status === 'active' ? Theme.approve : Theme.textSubtle) } .width('100%').margin({ top: 4 }) } @@ -525,10 +525,10 @@ struct ContactsTab { build() { Column() { Row() { - Text('联系人').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') + Text('联系人').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) } .width('100%').height(56).padding({ left: 16 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) if (this.loading) { Column() { @@ -537,13 +537,13 @@ struct ContactsTab { .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { - Text(this.error).fontSize(14).fontColor('#FF4444') + Text(this.error).fontSize(14).fontColor(Theme.danger) Button('重试').margin({ top: 12 }).onClick(() => { this.loadData(); }) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else if (this.contacts.length === 0) { Column() { - Text('📭 暂无联系人').fontSize(16).fontColor('#999999') + Text('📭 暂无联系人').fontSize(16).fontColor(Theme.textSubtle) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) } else { @@ -556,11 +556,11 @@ struct ContactsTab { }, (_c: Contact, idx: number) => idx.toString()) } .width('100%').layoutWeight(1) - .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) + .divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 }) } } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) } @Builder @@ -568,12 +568,12 @@ struct ContactsTab { Row() { Column() { Row() { - Text(c.agent_name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333') + Text(c.agent_name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) Blank() if (c.unread_count > 0) { Text(c.unread_count + '') - .fontSize(11).fontColor('#FFFFFF') - .backgroundColor('#FF4444') + .fontSize(11).fontColor(Theme.surface) + .backgroundColor(Theme.danger) .borderRadius(10).width(20).height(20) .textAlign(TextAlign.Center) } @@ -581,18 +581,18 @@ struct ContactsTab { .width('100%') Text(c.subject.length > 0 ? c.subject : c.session_alias) - .fontSize(12).fontColor('#666666') + .fontSize(12).fontColor(Theme.textMuted) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 3 }) Row() { Text(c.permission_mode.length > 0 ? c.permission_mode : '—') - .fontSize(10).fontColor('#666666') - .backgroundColor('#F0F0F0').borderRadius(4) + .fontSize(10).fontColor(Theme.textMuted) + .backgroundColor(Theme.border).borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) Blank() Text(c.last_preview.length > 0 ? c.last_preview : '—') - .fontSize(11).fontColor('#999999') + .fontSize(11).fontColor(Theme.textSubtle) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) } diff --git a/client/harmony/entry/src/main/ets/pages/SessionsPage.ets b/client/harmony/entry/src/main/ets/pages/SessionsPage.ets index 3d27d14..59e2431 100644 --- a/client/harmony/entry/src/main/ets/pages/SessionsPage.ets +++ b/client/harmony/entry/src/main/ets/pages/SessionsPage.ets @@ -3,6 +3,7 @@ * GET /me/sessions 展示所有会话摘要 */ import { ApiClient, ApiError } from '../api/ApiClient'; +import { Theme } from '../common/Theme'; import { MailApi } from '../api/MailApi'; import { Session } from '../model/Models'; @@ -49,29 +50,29 @@ struct SessionsPage { // 顶栏 Row() { Text('会话') - .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333') + .fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) } .width('100%').height(56) .padding({ left: 16 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) if (this.loading) { Column() { LoadingProgress().width(40).height(40) - Text('加载中…').fontSize(14).fontColor('#999999').margin({ top: 8 }) + Text('加载中…').fontSize(14).fontColor(Theme.textSubtle).margin({ top: 8 }) } .width('100%').layoutWeight(1) .justifyContent(FlexAlign.Center) } else if (this.error.length > 0) { Column() { - Text(this.error).fontSize(14).fontColor('#FF4444') + Text(this.error).fontSize(14).fontColor(Theme.danger) Button('重试').margin({ top: 12 }).onClick(() => { this.loadSessions(); }) } .width('100%').layoutWeight(1) .justifyContent(FlexAlign.Center) } else if (this.sessions.length === 0) { Column() { - Text('📭 暂无会话').fontSize(16).fontColor('#999999') + Text('📭 暂无会话').fontSize(16).fontColor(Theme.textSubtle) } .width('100%').layoutWeight(1) .justifyContent(FlexAlign.Center) @@ -85,18 +86,18 @@ struct SessionsPage { }, (s: Session) => s.session_id) } .width('100%').layoutWeight(1) - .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) + .divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 }) } // 底栏 Row() { - Text('共 ' + this.sessions.length + ' 个会话').fontSize(12).fontColor('#999999') + Text('共 ' + this.sessions.length + ' 个会话').fontSize(12).fontColor(Theme.textSubtle) } .width('100%').height(40).padding({ left: 16 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) } @Builder @@ -105,23 +106,23 @@ struct SessionsPage { Column() { Row() { Text(s.session_alias.length > 0 ? s.session_alias : s.subject) - .fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333') + .fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) // 预算进度 if (s.max_rounds > 0) { Text(s.used_rounds + '/' + s.max_rounds) - .fontSize(11).fontColor(s.used_rounds >= s.max_rounds ? '#FF4444' : '#666666') - .backgroundColor(s.used_rounds >= s.max_rounds ? '#FFEBEE' : '#F5F5F5') + .fontSize(11).fontColor(s.used_rounds >= s.max_rounds ? Theme.danger : Theme.textMuted) + .backgroundColor(s.used_rounds >= s.max_rounds ? Theme.dangerBg : Theme.surfaceMuted) .borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 }) } // 未读 if (s.unread_count > 0) { Text(s.unread_count + '') - .fontSize(11).fontColor('#FFFFFF') - .backgroundColor('#FF4444') + .fontSize(11).fontColor(Theme.surface) + .backgroundColor(Theme.danger) .borderRadius(10).width(20).height(20) .textAlign(TextAlign.Center) .margin({ left: 6 }) @@ -129,16 +130,16 @@ struct SessionsPage { } .width('100%') - Text('对方: ' + s.from_agent).fontSize(12).fontColor('#666666').margin({ top: 4 }) + Text('对方: ' + s.from_agent).fontSize(12).fontColor(Theme.textMuted).margin({ top: 4 }) Row() { Text(s.permission_mode.length > 0 ? s.permission_mode : '—') - .fontSize(11).fontColor('#666666') - .backgroundColor('#F0F0F0').borderRadius(4) + .fontSize(11).fontColor(Theme.textMuted) + .backgroundColor(Theme.border).borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) Blank() - Text(s.mail_count + ' 封').fontSize(11).fontColor('#999999') - Text(' · ' + s.status).fontSize(11).fontColor(s.status === 'active' ? '#4CAF50' : '#999999') + Text(s.mail_count + ' 封').fontSize(11).fontColor(Theme.textSubtle) + Text(' · ' + s.status).fontSize(11).fontColor(s.status === 'active' ? Theme.approve : Theme.textSubtle) } .width('100%').margin({ top: 4 }) } diff --git a/client/harmony/entry/src/main/ets/pages/SettingsPage.ets b/client/harmony/entry/src/main/ets/pages/SettingsPage.ets index 6d5ed1b..e45c737 100644 --- a/client/harmony/entry/src/main/ets/pages/SettingsPage.ets +++ b/client/harmony/entry/src/main/ets/pages/SettingsPage.ets @@ -3,6 +3,7 @@ * 多账号列表 / 添加新账号 / 删除 / 切换默认账号 */ import { ApiClient, ApiError } from '../api/ApiClient'; +import { Theme } from '../common/Theme'; import { AuthApi } from '../api/AuthApi'; import { AccountManager, AccountInfo } from '../api/AccountManager'; import { SseService } from '../api/SseService'; @@ -136,25 +137,25 @@ struct SettingsPage { build() { Column() { Row() { - Text('‹').fontSize(24).fontColor('#1A73E8').width(40).height(40) + Text('‹').fontSize(24).fontColor(Theme.accent).width(40).height(40) .textAlign(TextAlign.Center) .onClick(() => { this.getUIContext().getRouter().back(); }) - Text('账号管理').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333') + Text('账号管理').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) .layoutWeight(1) - Text('+').fontSize(24).fontColor('#1A73E8').width(40).height(40) + Text('+').fontSize(24).fontColor(Theme.accent).width(40).height(40) .textAlign(TextAlign.Center) .onClick(() => { this.showAddDialog = true; }) } .width('100%').height(56).padding({ left: 8, right: 8 }) - .backgroundColor('#FFFFFF') + .backgroundColor(Theme.surface) - Divider().color('#EEEEEE') + Divider().color(Theme.border) if (this.accounts.length === 0) { Column() { Text('📭 暂无账号').fontSize(16).fontColor('#777777') Button('添加第一个账号') - .margin({ top: 16 }).backgroundColor('#1A73E8') + .margin({ top: 16 }).backgroundColor(Theme.accent) .onClick(() => { this.showAddDialog = true; }) } .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) @@ -168,7 +169,7 @@ struct SettingsPage { }, (account: AccountInfo) => account.id) } .width('100%').layoutWeight(1) - .divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 }) + .divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 }) } if (this.showAddDialog) { @@ -179,7 +180,7 @@ struct SettingsPage { .onClick(() => { this.showAddDialog = false; }) Column() { - Text('添加新账号').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333') + Text('添加新账号').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) .margin({ bottom: 16 }) TextInput({ placeholder: '显示名称(必填)', text: this.newDisplayName }) @@ -201,18 +202,18 @@ struct SettingsPage { Row() { Button('取消') - .width(80).height(36).backgroundColor('#F5F5F5').fontColor('#555555') + .width(80).height(36).backgroundColor(Theme.surfaceMuted).fontColor('#555555') .onClick(() => { this.showAddDialog = false; }) Blank() Button(this.adding ? '验证中…' : '添加') - .width(80).height(36).backgroundColor('#1A73E8') + .width(80).height(36).backgroundColor(Theme.accent) .enabled(!this.adding) .onClick(() => { this.addNewAccount(); }) } .width('100%') } .width('100%').height('68%') - .padding(16).backgroundColor('#FFFFFF') + .padding(16).backgroundColor(Theme.surface) .borderRadius({ topLeft: 12, topRight: 12 }) } .width('100%').height('100%') @@ -220,7 +221,7 @@ struct SettingsPage { } } .width('100%').height('100%') - .backgroundColor('#F5F7FA') + .backgroundColor(Theme.pageBg) } @Builder @@ -230,11 +231,11 @@ struct SettingsPage { Row() { Text(account.displayName) .fontSize(15).fontWeight(FontWeight.Bold) - .fontColor(account.id === this.activeId ? '#1A73E8' : '#333333') + .fontColor(account.id === this.activeId ? Theme.accent : Theme.textPrimary) if (account.id === this.activeId) { Text(' 默认') - .fontSize(11).fontColor('#1A73E8') - .backgroundColor('#E3F2FD').borderRadius(4) + .fontSize(11).fontColor(Theme.accent) + .backgroundColor(Theme.accentSoft).borderRadius(4) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) .margin({ left: 6 }) } @@ -252,7 +253,7 @@ struct SettingsPage { if (account.id !== this.activeId) { Text('设为默认') - .fontSize(12).fontColor('#1A73E8') + .fontSize(12).fontColor(Theme.accent) .margin({ right: 12 }) .onClick(() => { this.switchTo(account.id); }) } @@ -264,6 +265,6 @@ struct SettingsPage { .width('100%').height('100%') .padding({ left: 16, right: 16 }) .alignItems(VerticalAlign.Center) - .backgroundColor(account.id === this.activeId ? '#F0F7FF' : '#FFFFFF') + .backgroundColor(account.id === this.activeId ? Theme.accentSoft : Theme.surface) } } diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md new file mode 100644 index 0000000..fc4cb5a --- /dev/null +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -0,0 +1,59 @@ +# 鸿蒙客户端与 WebUI 的对齐计划 + +> 用户(2026-09-14):「安排对齐」。这份文档把**差距**与**分期**写清楚, +> 免得每轮都从"感觉还差什么"重新猜。 + +## 一、现在的差距(有据可查) + +| 能力 | 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 设计语言(已完成)** —— 令牌 + 颜色替换 + 编译通过。 + +**P2 发件箱**:复用现有列表组件,接口 `/mail/sent`。验收:能看到已发邮件、 +点开进详情;空态有说明。**为什么先做它**:与收件箱同构,风险最低。 + +**P3 授权页**:导航加一项「授权」,列表用 `permission_*` 字段, +决策走 `POST /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. 无法验证的要**如实标注**(例如"编译通过、视觉未验"),不能写成"已完成"。