Files
MailUI4Agents/client/harmony/entry/src/main/ets/pages/InboxPage.ets
JianFeeeee fc295893cb 跨端: 深色模式下的品牌浅底不跟随 —— 12 处「选中/未读」底色在深色页上刺眼
## 真 bug(设备实测)

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

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

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

## 修法

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

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

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

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

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

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

## 判据

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

**未验**:日历/登录页在深色下的观感(只逐处改了底色,没逐页截图)。
2026-09-19 16:02:26 +08:00

239 lines
8.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* AgentMail 鸿蒙客户端 — 收件箱页
* 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 { router } from '@kit.ArkUI';
import { AmIcon } from '../common/Icons';
import { LIST_FADE_LENGTH } from '../model/NavItems';
import { LengthMetrics } from '@kit.ArkUI';
import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets';
@Entry
@Component
struct InboxPage {
/*
* 当前是否深色(`MainPage` 经 `AppStorage` 发布)—— 用来选品牌浅底的深浅变体。
*
* ★ 2026-09-19:深色主题下这些"浅蓝底"(未读行 / 选中项 / 分段选中)
* 在深色页面上刺眼。根因是 `Theme.accentSoft` 写死浅色
* (WebUI 靠 CSS 的 `.dark` 段反转发解决,ArkTS 静态常量没有那层机制)。
* 窗格自己算不出深浅色 ⇒ 从 AppStorage 读。
*/
@StorageProp('agentmail.appearance.isDark') isDarkNow: boolean = false;
@State mails: MailSummary[] = [];
/**
* 窗口避让区 —— 由 `EntryAbility.setupFullScreenWindow` 写进 AppStorage。
*
* ★ 推上来的页**也要**消费它:`setWindowLayoutFullScreen(true)` 是**窗口级**的,
* 一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页都从 y=0 开始画。
* 本页顶栏原来贴 y=0 ⇒ 全屏后「取消/发送」会与时钟、wifi/电量图标重叠
* (2026-09-18 实测截图硬证)。只给 `MainPage` 加避让就是把"黑边"换成
* "顶栏被状态栏压住",同属只做一半。
*/
@StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets();
@State loading: boolean = false;
@State limit: number = 20;
@State total: number = 0;
@State unread: number = 0;
@State me: Me = new Me();
@State error: string = '';
private client: ApiClient | null = null;
private mailApi: MailApi | null = null;
aboutToAppear(): void {
const ctx = this.getUIContext().getHostContext();
if (ctx !== undefined) {
const client: ApiClient = ApiClient.getInstance(ctx);
this.client = client;
this.mailApi = new MailApi(client);
this.loadInbox();
}
}
async loadInbox(): Promise<void> {
const c: ApiClient | null = this.client;
const m: MailApi | null = this.mailApi;
if (c === null || m === null) {
return;
}
this.loading = true;
this.error = '';
try {
const resp = await m.inbox('all', this.limit);
this.mails = resp.mails;
this.total = resp.total;
// 客户端统计未读(API 未提供顶层 unread)
let unreadCount: number = 0;
for (let i = 0; i < this.mails.length; i++) {
if (this.mails[i].status === 'unread') {
unreadCount++;
}
}
this.unread = unreadCount;
} catch (e) {
const ae = e as ApiError;
this.error = ae.message.length > 0 ? ae.message : '加载失败';
} finally {
this.loading = false;
}
}
build() {
Column() {
// 顶栏
Row() {
Text('收件箱')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
Blank()
if (this.unread > 0) {
Text(this.unread + ' 未读')
.fontSize(13).fontColor(Theme.surface)
.backgroundColor(Theme.danger)
.borderRadius(10)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
}
}
/* 高度与 padding-top 一起加避让 —— 理由见 `ComposePage` 顶栏(同形状) */
.width('100%')
.height(56 + topInset(this.windowInsets))
.padding({ left: 16, right: 16, top: topInset(this.windowInsets) })
.backgroundColor(Theme.surface)
// 列表
if (this.loading) {
Column() {
LoadingProgress().width(40).height(40)
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(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(Theme.textSubtle)
}
.width('100%').height('80%')
.justifyContent(FlexAlign.Center)
} else {
List({ space: 1 }) {
ForEach(this.mails, (mail: MailSummary) => {
ListItem() {
this.MailItem(mail)
}
.height(80)
.backgroundColor(mail.status === 'unread' ? Theme.accentSoftFor(this.isDarkNow) : Theme.surface)
.onClick(() => {
this.getUIContext().getRouter().pushUrl({
url: 'pages/MailDetailPage',
params: { mail_id: mail.mail_id }
});
})
}, (mail: MailSummary) => mail.mail_id)
}
.width('100%')
.layoutWeight(1)
/* 上下边缘渐隐(2026-09-17 补:原先漏了,同一次滚动里只有部分是渐隐) */
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
.divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 })
}
// 底部 Tab 栏(预留系统导航栏空间)
Column() {
Row() {
Column() {
AmIcon({ iconName: 'inbox', iconSize: 20, iconColor: Theme.accent })
Text('收件箱').fontSize(11).fontColor(Theme.accent)
}
.layoutWeight(1).height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Theme.accentSoftFor(this.isDarkNow))
Column() {
AmIcon({ iconName: 'chatBubble', iconSize: 20, iconColor: Theme.textMuted })
Text('会话').fontSize(11).fontColor(Theme.textMuted)
}
.layoutWeight(1).height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.getUIContext().getRouter().pushUrl({ url: 'pages/SessionsPage' });
})
Column() {
AmIcon({ iconName: 'contacts', iconSize: 20, iconColor: Theme.textMuted })
Text('联系人').fontSize(11).fontColor(Theme.textMuted)
}
.layoutWeight(1).height('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%').height(56)
.backgroundColor(Theme.surface)
}
.width('100%')
}
.width('100%').height('100%')
.backgroundColor(Theme.pageBg)
}
@Builder
MailItem(mail: MailSummary) {
Row() {
// 未读圆点
Column() {
if (mail.status === 'unread') {
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.is_read ? FontWeight.Normal : FontWeight.Bold).fontColor(Theme.textPrimary)
Blank()
Text(mail.created_at).fontSize(11).fontColor(Theme.textSubtle)
}
.width('100%')
Text(mail.subject).fontSize(13).fontColor(Theme.textPrimary)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
Text(mail.body_preview).fontSize(12).fontColor(Theme.textSubtle)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
}
.layoutWeight(1).height('100%')
.alignItems(HorizontalAlign.Start)
.padding({ left: 8 })
// 附件图标
if (mail.has_attachments) {
AmIcon({ iconName: 'paperclip', iconSize: 14, iconColor: Theme.textMuted }).margin({ right: 8 })
}
// 权限档位徽标
if (mail.permission_mode.length > 0) {
Text(mail.permission_mode)
.fontSize(10).fontColor(Theme.textMuted)
.backgroundColor(Theme.border).borderRadius(4)
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
}
}
.width('100%').height('100%')
.padding({ left: 12, right: 12 })
.alignItems(VerticalAlign.Center)
}
}