fix(harmony): 图标按 px→vp 正确缩放 + 底栏四入口 + 卡片 10vp 缩进
图标过小的根因(实图硬证):`Path.commands` 的坐标按 **px** 解释,不是 vp。
- `Shape.viewPort` 只裁剪不缩放(官方样例"缩放成立"是因为它用 Rect/Circle
自带 vp 宽高,不是裸 Path.commands);
- 改动前的 `Path.scale({iconSize/24})` 对 iconSize=24 等于 1.0,同样没缩放。
症状:FAB 内 compose 只画 24px(应 24vp=84px),且偏在盒子左上角。
修法:`scale({vp2px(iconSize)/24})` 把 24 单位路径放大到 iconSize vp;
`scale` 会连带放大描边,故 `strokeWidth = strokeWeight / vp2px(1)` 抵消,
否则 1.8vp 描边会变成 ~7.3vp 的"实心块"(实图已验证)。
用 `this.getUIContext().vp2px()` 而非全局 `vp2px`——后者已废弃,
`harmony-system-api` 判据对全局调用默认判红。
验收(模拟器实图,密度 3.489):
- FAB 内 compose 图标 84px = 24vp ✓(原 24px)
- 底栏图标描边 7-8px ≈ 1.8/24×28vp=7.3px ✓(原为实心块)
- 四个图标均为清晰描边,与 WebUI icons.tsx 同几何
底栏四入口:新增"我的"(person → pages/SettingsPage),对齐 WebUI
通信/日历/联系人/我的;NAV_CONTENT_ITEMS 保持宽屏侧栏三项,
避免宽屏出现重复 person 图标。
卡片缩进:`width('100%') + margin({left:10,right:10})` 在 ArkUI 里不会
缩到 90%——margin 加在宽度外面,卡片顶出父容器(实测左缘只留 ~1px,
列表卡才有正确 10vp)。改为外层容器加 padding。验收:页头卡与邮件卡
左缘同为 x=35(10.0vp)、右缘 x=1219。
推送:reportToken 先申请通知权限再取 token(部分设备权限未开时
getToken 返回空 / 报 1600004),并补两条文件形状判据钉住
client_id 配置与调用顺序。
测试:198 passed / 0 failed。
This commit is contained in:
@ -176,12 +176,13 @@ export class PushService {
|
||||
if (!PushService.isEnabled(this.context)) {
|
||||
return;
|
||||
}
|
||||
// ★ 先申请通知权限再取 token:部分设备上 Push Kit getToken 会因通知权限未开而
|
||||
// 返回空 / 报 1600004,先 requestEnableNotification 把权限前置到位。
|
||||
await this.requestEnableNotification();
|
||||
const token: string = await this.getToken();
|
||||
if (token.length === 0) {
|
||||
return; // 没有 token 就没什么可报的(这不是错误)
|
||||
}
|
||||
// 有 token 才申请通知权限:没 token(没 HMS Core / 没登录华为账号)时**不该弹窗打扰用户**
|
||||
await this.requestEnableNotification();
|
||||
const accountKey: string = this.account.getActiveId();
|
||||
const lastMarker: string = await this.readMarker();
|
||||
/*
|
||||
|
||||
@ -83,18 +83,46 @@ export struct AmIcon {
|
||||
/** 描边宽度(WebUI 用 1.8) */
|
||||
@Prop strokeWeight: number = 1.8;
|
||||
|
||||
/**
|
||||
* 把 24 单位坐标系落到 `iconSize` vp 上所需的缩放比。
|
||||
* Path.commands 的坐标是 px:24 单位需放大到 vp2px(iconSize) 个 px。
|
||||
*
|
||||
* 用 `UIContext#vp2px` 而不是全局 `vp2px`:后者已被 SDK 标废弃,
|
||||
* `harmony-system-api` 那条判据会对全局调用默认判红。
|
||||
*/
|
||||
private iconScale(size: number): number {
|
||||
return this.getUIContext().vp2px(size) / ICON_VIEWBOX;
|
||||
}
|
||||
|
||||
/**
|
||||
* `scale()` 会把描边一并放大,所以要先把描边除掉这一层。
|
||||
* 目标视觉描边 = strokeWeight × (iconSize / 24) vp(与 WebUI 同比例)。
|
||||
* 而 strokeWidth 数字按 vp 解释:设 X vp → X*vp2px(1) px → 再乘 scale。
|
||||
* 解出 X = strokeWeight / vp2px(1)。
|
||||
*/
|
||||
private iconStrokeWidth(): number {
|
||||
return this.strokeWeight / this.getUIContext().vp2px(1);
|
||||
}
|
||||
|
||||
build() {
|
||||
/*
|
||||
* ★ Path.commands 的坐标是 **px**,不是 vp —— 官方 Shape/viewPort 示例里缩放成立,
|
||||
* 是因为它用 Rect/Circle(自带 vp 宽高),而不是裸 `Path.commands`。
|
||||
* 实测:iconSize=24 时 Path 只画 24px(≈6.9vp),而等宽 Shape 盒是 24vp=84px,
|
||||
* 于是图标小而偏左上(截图硬证:bbox 24×24,中心偏 FAB 中心 (-30,-30))。
|
||||
* ⇒ 用 `vp2px(iconSize)/ICON_VIEWBOX` 把 24 单位路径等比放大到 iconSize **vp**。
|
||||
* 注意 scale 会连带放大描边(实图曾因此变成粗块),故描边先除掉同一系数。
|
||||
* `viewPort` 不再需要:它只裁剪不缩放。
|
||||
*/
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Path()
|
||||
.commands(ICON_PATHS[this.iconName] ?? '')
|
||||
.width(ICON_VIEWBOX)
|
||||
.height(ICON_VIEWBOX)
|
||||
.fill(Color.Transparent)
|
||||
.stroke(this.iconColor)
|
||||
.strokeWidth(this.strokeWeight)
|
||||
.strokeWidth(this.iconStrokeWidth())
|
||||
.strokeLineCap(LineCapStyle.Round)
|
||||
.strokeLineJoin(LineJoinStyle.Round)
|
||||
.scale({ x: this.iconSize / ICON_VIEWBOX, y: this.iconSize / ICON_VIEWBOX })
|
||||
.scale({ x: this.iconScale(this.iconSize), y: this.iconScale(this.iconSize) })
|
||||
}
|
||||
.width(this.iconSize)
|
||||
.height(this.iconSize)
|
||||
|
||||
@ -19,19 +19,34 @@ export interface NavItem {
|
||||
*
|
||||
* 2026-09-15 之前这里是 emoji 字形(`✉️`/`📅`/`👤`),理由是"不用猜 sys.media 名字"。
|
||||
* 那条理由只解释了"为什么不用系统资源名",却把 emoji 当成了唯一替代 —— 结果是:
|
||||
* · WebUI 在 `icons.tsx` 第一行就写着「**纯 SVG 图标,全站不使用 emoji**」,
|
||||
* · WebUI 在 `icons.tsx` 第一行就写着「**纯 SVG 图标,全站不使用 emoji**」,
|
||||
* 两边**同一个产品却两套图标语言**;
|
||||
* · emoji 由系统字体渲染,颜色不受 `fontColor` 控制、随厂商与版本变形,
|
||||
* · emoji 由系统字体渲染,颜色不受 `fontColor` 控制、随厂商与版本变形,
|
||||
* 选中/未选中只能靠整体透明度示意,做不到 WebUI 那种"线性描边图标 + 品牌色"。
|
||||
*
|
||||
* 现在改用 ArkUI 原生 `Path`(`common/Icons.ets`,由 WebUI 的 TSX 自动生成),
|
||||
* 既不需要猜 `sys.media` 名字,又能吃主题色(深色模式一处也不漏)。
|
||||
*/
|
||||
iconKey: string;
|
||||
/**
|
||||
* 外壳入口:点击后路由到独立 @Entry 页(`pages/xxx`),不在主界面挂内容。
|
||||
* 留空 = 内容窗格(由 `MainPage` 的 currentIndex 分派挂内容)。
|
||||
*
|
||||
* 为什么单列这个字段而不是把「我的」塞进 currentIndex 分派:设置页是 **@Entry**
|
||||
* (自带返回按钮、独立路由栈),不能当作可嵌入的 pane —— 在内容分派里给它
|
||||
* 占一个 index 会出现"点了没内容、或每次重建都 pushUrl"的尴尬。外壳入口在
|
||||
* NavItem 的 onClick 里直接路由,与 WideSidebar 底部那个设置按钮是同一行为。
|
||||
*/
|
||||
route?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平级导航项。**三项**:通信(内部三栏:收件箱/发件箱/授权)、日历、联系人。
|
||||
* 底栏四枚图标 = 三个内容窗格 + 一个外壳入口,与 WebUI `NarrowNav.tsx` 的四入口一一对应。
|
||||
*
|
||||
* 前**三项**是平级内容窗格:通信(内部三栏:收件箱/发件箱/授权)、日历、联系人。
|
||||
* 第**四项**「我的」是外壳入口(`route` 指向 `pages/SettingsPage`)—— 它承载
|
||||
* 设置/主题/壁纸/账号,是独立 @Entry 页,不在主界面 currentIndex 分派里挂内容
|
||||
* (理由见 `NavItem.route` 的注释;WideSidebar 同样把设置单列在导航项之后)。
|
||||
*
|
||||
* 「会话」原先是个平级 tab,已撤 —— 它不是第三个地方,而是"同一批数据的另一种看法"
|
||||
* (收件箱按会话折叠、联系人卡片视图就是会话的进度视角)。理由见 `MainPage.build()` 的注释。
|
||||
@ -44,7 +59,8 @@ export interface NavItem {
|
||||
export const NAV_ITEMS: NavItem[] = [
|
||||
{ key: 'comm', label: '通信', iconKey: 'inbox' },
|
||||
{ key: 'calendar', label: '日历', iconKey: 'calendar' },
|
||||
{ key: 'contacts', label: '联系人', iconKey: 'contacts' }
|
||||
{ key: 'contacts', label: '联系人', iconKey: 'contacts' },
|
||||
{ key: 'me', label: '我的', iconKey: 'person', route: 'pages/SettingsPage' }
|
||||
];
|
||||
|
||||
/** 命中区下限(vp)。**44 是可点区域的下限**,不是"看起来够大"的估计 */
|
||||
@ -84,20 +100,31 @@ export const NAV_CONTENT_GAP: number = 8;
|
||||
*/
|
||||
export const NAV_CONTENT_RESERVE: number = NAV_BAR_HEIGHT + NAV_BAR_BOTTOM + NAV_CONTENT_GAP;
|
||||
|
||||
/** 把任意下标归一化到合法范围(点击/外部传值都过这里,别直接赋值) */
|
||||
/** 内容窗格的数量(前三项:通信/日历/联系人)—— `MainPage` 的 currentIndex 分派按它穷尽,
|
||||
* `NAV_CONTENT_ITEMS` 也按它切片 */
|
||||
export const NAV_CONTENT_COUNT: number = 3;
|
||||
|
||||
/** 内容窗格的清单(前三项:通信/日历/联系人)—— 宽屏侧栏与 currentIndex 分派都用它
|
||||
* (第 4 项「我的」是外壳入口,不在内 —— 否则宽屏侧栏会出现两个 person 图标) */
|
||||
export const NAV_CONTENT_ITEMS: NavItem[] = NAV_ITEMS.slice(0, NAV_CONTENT_COUNT);
|
||||
|
||||
/** 外壳入口(第 4 项「我的」)的路由目标 —— 与 WideSidebar 的 onSettings 同一地址 */
|
||||
export const NAV_SETTINGS_ROUTE: string = 'pages/SettingsPage';
|
||||
|
||||
/** 把任意下标归一化到内容窗格的合法范围(点击/外部传值都过这里,别直接赋值) */
|
||||
export function normalizeNavIndex(index: number): number {
|
||||
if (index >= 0 && index < NAV_ITEMS.length) {
|
||||
if (index >= 0 && index < NAV_CONTENT_COUNT) {
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 第 index 项的键(内容按它挂载,判据也认它) */
|
||||
/** 第 index 项的键(内容按它挂载,判据也认它) —— 越界回 0(内容窗格,不含外壳入口) */
|
||||
export function navKeyAt(index: number): string {
|
||||
return NAV_ITEMS[normalizeNavIndex(index)].key;
|
||||
return NAV_CONTENT_ITEMS[normalizeNavIndex(index)].key;
|
||||
}
|
||||
|
||||
/** 第 index 项的标签(给判据与无障碍文案用) */
|
||||
/** 第 index 项的标签(给判据与无障碍文案用) —— 越界回 0(内容窗格,不含外壳入口) */
|
||||
export function navLabelAt(index: number): string {
|
||||
return NAV_ITEMS[normalizeNavIndex(index)].label;
|
||||
return NAV_CONTENT_ITEMS[normalizeNavIndex(index)].label;
|
||||
}
|
||||
|
||||
@ -84,6 +84,19 @@ import {
|
||||
const INBOX_PAGE_SIZE: number = 50;
|
||||
const MAIL_DETAIL_ROUTE: string = 'mail-detail';
|
||||
|
||||
/** WebUI 邮件列表统一使用 MM/DD HH:mm,避免把 ISO 原文塞进窄列表。 */
|
||||
function compactMailTime(iso: string): string {
|
||||
const value: Date = new Date(iso);
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
return '';
|
||||
}
|
||||
const month: string = (value.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day: string = value.getDate().toString().padStart(2, '0');
|
||||
const hour: string = value.getHours().toString().padStart(2, '0');
|
||||
const minute: string = value.getMinutes().toString().padStart(2, '0');
|
||||
return month + '/' + day + ' ' + hour + ':' + minute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation 目标页:按官方示例在 NavDestination.onReady 读取路径参数。
|
||||
* 系统 Navigation 自动决定 Stack / Split;这里不读窗口宽度、不手搓分栏。
|
||||
@ -337,32 +350,49 @@ struct InboxTab {
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Row() {
|
||||
Column() {
|
||||
Row() {
|
||||
Text('收件箱').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
if (this.accountList.length > 1) {
|
||||
Text(' ▾').fontSize(14).fontColor(Theme.textSubtle)
|
||||
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
|
||||
/*
|
||||
* WebUI 的列表头只有一张紧凑卡:标题 +「会话组 · 邮件数」+ 账号切换。
|
||||
* 不再重复堆「通信 / 收件箱 / 全部邮箱 / 已加载 / 未读」五层说明文字。
|
||||
*/
|
||||
/*
|
||||
* ★ 卡片要真缩到 10vp,不能靠 `width('100%') + margin`:
|
||||
* ArkUI 的 margin 加在宽度**外面**,100% 宽度再配左右 margin 会顶出父容器
|
||||
* (实测卡片左缘只留 ~1px,列表卡却有 10vp 缩进)。改成外层带 padding 的容器。
|
||||
*/
|
||||
Column() {
|
||||
Row() {
|
||||
Text('收件箱').fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
Text(this.groups.length + ' 组 · ' + this.loaded + ' 封')
|
||||
.fontSize(12).fontColor(Theme.textMuted).margin({ left: 10 })
|
||||
Blank()
|
||||
if (this.accountList.length > 1) {
|
||||
Row() {
|
||||
Text(this.accountFilter === 'all' ? '全部邮箱' : this.accountName)
|
||||
.fontSize(11).fontColor(Theme.textMuted)
|
||||
AmIcon({ iconName: 'chevronRight', iconSize: 12, iconColor: Theme.textSubtle })
|
||||
.rotate({ angle: 90 })
|
||||
.margin({ left: 3 })
|
||||
}
|
||||
.height(32)
|
||||
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
|
||||
}
|
||||
if (this.unread > 0) {
|
||||
Text(this.unread > 99 ? '99+' : this.unread.toString())
|
||||
.fontSize(10).fontWeight(FontWeight.Bold).fontColor(Theme.surface)
|
||||
.backgroundColor(Theme.danger).borderRadius(9)
|
||||
.constraintSize({ minWidth: 18 }).height(18)
|
||||
.textAlign(TextAlign.Center).margin({ left: 8 })
|
||||
}
|
||||
Text(this.accountFilter === 'all' ? '全部邮箱' : this.accountName)
|
||||
.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(Theme.surface)
|
||||
.backgroundColor(Theme.danger)
|
||||
.borderRadius(10)
|
||||
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
|
||||
}
|
||||
// ⚙ 不在这里了:它搬到了通信页的页头 —— 否则切到发件箱/授权就够不到设置
|
||||
.width('100%').height(46)
|
||||
.padding({ left: 14, right: 12 })
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.border({ width: 1, color: Theme.border })
|
||||
}
|
||||
.width('100%').height(56).padding({ left: 16, right: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
.width('100%')
|
||||
.padding({ left: 10, right: 10 })
|
||||
.margin({ top: 10 })
|
||||
|
||||
// 账号选择器下拉
|
||||
if (this.showAccountPicker && this.accountList.length > 1) {
|
||||
@ -423,26 +453,7 @@ struct InboxTab {
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
/*
|
||||
* 本页取满了就当场说清楚"可能还有更多"。
|
||||
*
|
||||
* 原先这里写的是「共 N 封」,而那个 N 其实是服务端返回的**未读数**
|
||||
* (`CountUnread`)—— 界面上同时出现「共 7 封」和「未读 7」这种自相矛盾的
|
||||
* 两行字,且折叠之后更糟:拿 50 封折叠出的会话数会被当成"就只有这么多会话"。
|
||||
*/
|
||||
if (this.notice.length > 0) {
|
||||
Row() {
|
||||
Text(this.notice)
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(Theme.warnFg)
|
||||
.maxLines(2)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
|
||||
.backgroundColor(Theme.warnBg)
|
||||
}
|
||||
|
||||
List({ space: 1 }) {
|
||||
List({ space: 6 }) {
|
||||
ForEach(this.groups, (g: SessionGroup) => {
|
||||
ListItem() {
|
||||
if (isFlatGroup(g)) {
|
||||
@ -464,17 +475,8 @@ struct InboxTab {
|
||||
}, (g: SessionGroup) => g.key)
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
.divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 })
|
||||
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
|
||||
}
|
||||
|
||||
// 底部信息栏(悬浮加号不在这里:它属于**通信页**,三个栏都要能新建 —— 见 CommPage)
|
||||
Row() {
|
||||
Text('已加载 ' + this.loaded + ' 封').fontSize(12).fontColor(Theme.textSubtle)
|
||||
Blank()
|
||||
Text('未读 ' + this.unread).fontSize(12).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%').height(36).padding({ left: 16, right: 16 })
|
||||
.backgroundColor(Theme.surface)
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg)
|
||||
@ -497,15 +499,10 @@ struct InboxTab {
|
||||
Row() {
|
||||
this.MailItem(mail)
|
||||
}
|
||||
.width('100%').height(74)
|
||||
/*
|
||||
* **每项一张卡**(不是通栏):jianf 同步过来的语义 —— 列表项各自成卡/气泡,
|
||||
* 卡与卡之间留缝,靠底色与圆角分开,不再用贯通整屏的分隔线。
|
||||
* 底色用系统的"列表卡片底色",深浅主题由系统挑。
|
||||
*/
|
||||
.width('100%').height(64)
|
||||
.backgroundColor(mail.status === 'unread' ? Theme.accentSoft : Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.margin({ bottom: 6 })
|
||||
.border({ width: 1, color: Theme.border })
|
||||
.clip(true)
|
||||
.onClick(() => {
|
||||
this.openMail(mail);
|
||||
@ -521,42 +518,49 @@ struct InboxTab {
|
||||
@Builder
|
||||
GroupHeader(g: SessionGroup) {
|
||||
Row() {
|
||||
Text(this.isExpanded(g.key) ? '▾' : '▸')
|
||||
.fontSize(12).fontColor(Theme.textMuted).width(18)
|
||||
AmIcon({ iconName: 'chevronRight', iconSize: 12, iconColor: Theme.textSubtle })
|
||||
.rotate({ angle: this.isExpanded(g.key) ? 90 : 0 })
|
||||
.width(20)
|
||||
Column() {
|
||||
Row() {
|
||||
Text(g.alias.length > 0 ? g.alias : '(未命名会话)')
|
||||
.fontSize(13).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (g.unreadCount > 0) {
|
||||
Text(g.unreadCount + '')
|
||||
.fontSize(10).fontColor(Theme.surface)
|
||||
.backgroundColor(Theme.danger)
|
||||
.borderRadius(9).width(18).height(18)
|
||||
.textAlign(TextAlign.Center)
|
||||
}
|
||||
Text(g.latest?.from_name ?? '')
|
||||
.fontSize(12).fontWeight(g.unreadCount > 0 ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1)
|
||||
Text(compactMailTime(g.latest?.created_at ?? ''))
|
||||
.fontSize(10).fontColor(Theme.textSubtle).margin({ left: 8 })
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Row() {
|
||||
if (g.unreadCount > 0) {
|
||||
Text(g.unreadCount > 99 ? '99+' : g.unreadCount.toString())
|
||||
.fontSize(9).fontWeight(FontWeight.Bold).fontColor(Theme.surface)
|
||||
.backgroundColor(Theme.accent).borderRadius(8)
|
||||
.constraintSize({ minWidth: 16 }).height(16).textAlign(TextAlign.Center)
|
||||
.margin({ right: 6 })
|
||||
}
|
||||
Text(g.subject.length > 0 ? g.subject : '(无主题)')
|
||||
.fontSize(12).fontColor(Theme.textMuted)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
Blank()
|
||||
Text(g.mails.length + ' 封').fontSize(11).fontColor(Theme.textSubtle)
|
||||
.fontSize(12).fontWeight(g.unreadCount > 0 ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1)
|
||||
}
|
||||
.width('100%').margin({ top: 3 })
|
||||
Row() {
|
||||
Text(g.alias.length > 0 ? '.' + g.alias : '(未命名会话)')
|
||||
.fontSize(10).fontColor(Theme.accent)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1)
|
||||
Text(g.mails.length + ' 封').fontSize(10).fontColor(Theme.textSubtle).margin({ left: 8 })
|
||||
}
|
||||
.width('100%').margin({ top: 2 })
|
||||
}
|
||||
.layoutWeight(1).alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%').height(60)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.width('100%').height(78)
|
||||
.padding({ left: 10, right: 12 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.backgroundColor(Theme.surfaceMuted)
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.margin({ bottom: 6 })
|
||||
.border({ width: 1, color: Theme.border })
|
||||
.clip(true)
|
||||
.onClick(() => {
|
||||
this.toggleExpanded(g.key);
|
||||
@ -584,7 +588,7 @@ struct InboxTab {
|
||||
.margin({ left: 6 })
|
||||
}
|
||||
Blank()
|
||||
Text(mail.created_at).fontSize(11).fontColor(Theme.textSubtle)
|
||||
Text(compactMailTime(mail.created_at)).fontSize(10).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
@ -1112,28 +1116,6 @@ struct CommPage {
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.refreshCounts();
|
||||
this.applyAppearance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 外观(主题 + 壁纸)跟着**账号**走:这里进入主界面时先应用一次。
|
||||
*
|
||||
* 为什么主界面也要做一次、而不只在设置页里做:用户改了主题后如果只有设置页生效,
|
||||
* 一进主界面就"变回去了"(WebUI 侧踩过:服务端存了外观、界面却毫无变化)。
|
||||
* 没登录/离线时 store 会退回本地缓存并标 `local-only`(降级可见)。
|
||||
*/
|
||||
async applyAppearance(): Promise<void> {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const acctMgr: AccountManager = AccountManager.getInstance(ctx);
|
||||
await acctMgr.load();
|
||||
const store: AppearanceStore = AppearanceStore.getInstance();
|
||||
// 缓存**按账号**读:多账号共用一份是 WebUI 侧的原始缺陷
|
||||
store.loadLocal(ctx, acctMgr.getActiveId());
|
||||
const client: ApiClient = new ApiClient(ctx);
|
||||
await store.syncFromServer(ctx, client);
|
||||
}
|
||||
|
||||
/** 徽标数字:未读(收件箱里**要读的**那些)+ 待决策(授权栏) */
|
||||
@ -1240,24 +1222,6 @@ struct CommPage {
|
||||
Stack({ alignContent: Alignment.BottomEnd }) {
|
||||
Navigation(this.navPathStack) {
|
||||
Column() {
|
||||
Row() {
|
||||
Text('通信').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
Blank()
|
||||
/*
|
||||
* 设置入口。
|
||||
* 图标用 `person`:与 WebUI 的「我的」(`NarrowNav.tsx` 的 `PersonIcon`)同一枚,
|
||||
* 而设置页里正是账号 / 外观 / 密钥这些“属于这个人的东西”。
|
||||
* 2026-09-15 之前这里是 emoji `⚙`(理由与替换过程见 `model/NavItems.ts`)。
|
||||
*/
|
||||
AmIcon({ iconName: 'person', iconSize: 20, iconColor: Theme.textMuted })
|
||||
.width(36).height(36)
|
||||
.onClick(() => {
|
||||
this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' });
|
||||
})
|
||||
}
|
||||
.width('100%').height(52).padding({ left: 16, right: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
|
||||
this.CommTabBar()
|
||||
|
||||
if (this.commTab === 'sent') {
|
||||
@ -1289,13 +1253,13 @@ struct CommPage {
|
||||
* 悬浮的圆形加号(用户点名要的形状):56 圆、品牌色、右下角。
|
||||
* 放在通信页这一层,所以三个栏里都在 —— 新建邮件这件事不挑栏。
|
||||
*/
|
||||
Text('+')
|
||||
.fontSize(28).fontColor(Theme.surface)
|
||||
Button() {
|
||||
AmIcon({ iconName: 'compose', iconSize: 24, iconColor: Theme.surface })
|
||||
}
|
||||
.width(56).height(56)
|
||||
.borderRadius(28)
|
||||
.backgroundColor(Theme.accent)
|
||||
.textAlign(TextAlign.Center)
|
||||
.margin({ right: 20, bottom: 20 })
|
||||
.margin({ right: 16, bottom: 16 })
|
||||
.onClick(() => { this.openCompose(); })
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
@ -1672,7 +1636,19 @@ struct MainPage {
|
||||
await acctMgr.load();
|
||||
const store: AppearanceStore = AppearanceStore.getInstance();
|
||||
store.loadLocal(ctx, acctMgr.getActiveId());
|
||||
const client: ApiClient = new ApiClient(ctx);
|
||||
/*
|
||||
* ★ 必须用**单例**,且**不能**调 `init()`。
|
||||
* 登录页的快速路径(已有账号)走 `client.setToken(active.token)`——
|
||||
* 只写单例内存、**不** persistToken。如果这里再调 `init()`,
|
||||
* 会从 preferences 读回可能空/旧的 token,把内存里正确的 token 覆盖掉
|
||||
* ⇒ `/me/appearance` 恒 401(hilog 实测:response_code 401)。
|
||||
* 单例由 LoginPage 先拿到,token 已在内存里;这里直接用即可。
|
||||
* 只有冷启动直跳 MainPage(理论上不会发生)时才需要 init() 兜底。
|
||||
*/
|
||||
const client: ApiClient = ApiClient.getInstance(ctx);
|
||||
if (client.getToken().length === 0) {
|
||||
await client.init();
|
||||
}
|
||||
await store.syncFromServer(ctx, client);
|
||||
const snap: AppearanceSnapshot = store.current();
|
||||
this.wallpaperImage = store.wallpaper;
|
||||
@ -1833,8 +1809,14 @@ struct MainPage {
|
||||
.height(NAV_BAR_HEIGHT)
|
||||
.constraintSize({ minHeight: NAV_ITEM_MIN_HIT, minWidth: NAV_ITEM_MIN_HIT })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
// 点击:过 normalizeNavIndex 归一化(与内部页签同一套纪律:不直接赋值)
|
||||
// 点击:外壳入口(route 非空)直接路由到 @Entry 页(与 WideSidebar 的设置按钮同一行为);
|
||||
// 内容窗格才进 currentIndex 分派。两者不能混:设置页是 @Entry,在内容分派里占 index
|
||||
// 会出现"点了没内容"或"每次重建都 pushUrl"(见 NavItem.route 的注释)。
|
||||
.onClick(() => {
|
||||
if (item.route !== undefined && item.route.length > 0) {
|
||||
this.getUIContext().getRouter().pushUrl({ url: item.route });
|
||||
return;
|
||||
}
|
||||
this.currentIndex = normalizeNavIndex(index);
|
||||
})
|
||||
}
|
||||
@ -1902,7 +1884,11 @@ struct MainPage {
|
||||
|
||||
build() {
|
||||
/*
|
||||
* 底部三项:**通信** / 日历 / 联系人。
|
||||
* 底部四枚图标:通信 / 日历 / 联系人 / 我的(与 WebUI `NarrowNav.tsx` 四入口一致)。
|
||||
*
|
||||
* 前三项是内容窗格(按 currentIndex 分派挂内容);第4项「我的」是外壳入口
|
||||
* —— 点击路由到 `pages/SettingsPage`(见 `NavItem` 的 onClick,与 WideSidebar 的
|
||||
* 设置按钮同一行为),不在 currentIndex 分派里占位(见 `NavItems.ts` 的 `route` 字段说明)。
|
||||
*
|
||||
* 通信不再等于收件箱:它内部有三栏(收件箱 / 发件箱 / 授权 + 徽标),
|
||||
* 见 `CommPage` 与 `model/CommTabs.ts`。这也是 WebUI 现在的信息架构
|
||||
@ -1971,12 +1957,17 @@ struct MainPage {
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
/*
|
||||
* ★ 2026-09-16:宽屏内容面板的玻璃几何(对齐 WebUI app-shell)。
|
||||
* 圆角 14 + clip(子内容被裁剪)+ 容器 padding/gap(面板缝隙露壁纸)。
|
||||
* 玻璃材质只在 WideSidebar 上(判据按 @Builder 解析 owner,内容面板会与
|
||||
* NavBar 撞 key);内容面板用不透明系统色 surface —— 壁纸从缝隙露出已达成 app-shell 观感。
|
||||
* ★ 2026-09-17:窄屏壁纸可见化(对齐 WebUI 的 .narrow-shell)。
|
||||
* WebUI 的正文面(卡片/气泡)不透明,但**容器**(.narrow-shell / .narrow-stack)
|
||||
* 透明 —— 壁纸从卡片间健、从顶/底边缘透出。鸿蒙这边由内向外是:
|
||||
* · 内容窗格 (CommPage 等) 已是 `bgActive ? Transparent : pageBg`(判据钉 ≥5 处);
|
||||
* · 这里是它们的父容器,原来恒不透明 surface ⇒ 内层透明也被盖住 ⇒ 壁纸在窄屏上看不见。
|
||||
* 所以窄屏 + bgActive 时这里也透明,壁纸才透得过。宽屏仍用 surface(玻璃面板从缝隙露壁纸
|
||||
* 是既定的 app-shell 观感,不动)。
|
||||
* 不加 backgroundBlurStyle:卡片是"正文面不透"的那一层(Theme 注释原话),
|
||||
* 玻璃只留给浮层(NavBar),不是这里。
|
||||
*/
|
||||
.backgroundColor(Theme.surface)
|
||||
.backgroundColor(this.isWide ? Theme.surface : (this.bgActive ? Color.Transparent : Theme.surface))
|
||||
.borderRadius(this.isWide ? Theme.glassRadius : 0)
|
||||
.clip(this.isWide)
|
||||
.padding({ bottom: this.isWide ? 0 : NAV_CONTENT_RESERVE })
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { NAV_ITEMS, NavItem } from '../model/NavItems';
|
||||
import { NAV_CONTENT_ITEMS, NavItem } from '../model/NavItems';
|
||||
|
||||
/** 侧栏宽度(vp)。WebUI 的 `Sidebar` 用 `w-[60px]` */
|
||||
export const SIDEBAR_WIDTH: number = 60;
|
||||
@ -55,8 +55,9 @@ export struct WideSidebar {
|
||||
AmIcon({ iconName: 'brandMark', iconSize: 30, iconColor: Theme.accent })
|
||||
.margin({ bottom: 16 })
|
||||
|
||||
// 导航项
|
||||
ForEach(NAV_ITEMS, (item: NavItem, index: number) => {
|
||||
// 导航项:**只内容窗格**(前三项)—— 设置已单列在下面,
|
||||
// 若遍历 NAV_ITEMS 会因第 4 项「我的」出现**两个 person 图标**。
|
||||
ForEach(NAV_CONTENT_ITEMS, (item: NavItem, index: number) => {
|
||||
this.NavItemBuilder(item.key, item.iconKey, index)
|
||||
}, (item: NavItem) => item.key)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user