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 未签名 ⇒ 只能保证编译通过 +
令牌一致,观感需要设备或签名后由人眼确认。文档里也把这条写进"验收纪律"。
This commit is contained in:
2026-09-14 12:49:40 +08:00
parent cefd96a6a3
commit 5434bc9e4e
10 changed files with 275 additions and 182 deletions

View File

@ -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'));
});

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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 })
}
}

View File

@ -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)
}
}

View File

@ -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<void> {

View File

@ -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)
}

View File

@ -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 })
}

View File

@ -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)
}
}

View File

@ -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. 无法验证的要**如实标注**例如"编译通过视觉未验"不能写成"已完成"。