fix(harmony): 底栏黑带 / 联系人点不开 / 我的页不守导航 / 顶栏硬截断
用户(2026-09-17)连报五条:「一点动画都没有」「底部那个黑条是啥意思」
「日历页面、我的页面、联系人页面哪个跟 webui 对齐了」「联系人页面连点都点不开」
「你的顶栏为什么还是硬截断而不是渐变」。逐条查证后修:
── ① 底部黑条 = 系统导航栏区域,不是我们画的 ──
根因:窗口默认给系统导航条留位,那块在深色下是黑的;界面于是看起来底下多一条黑带。
修法:`setWindowSystemBarEnable(['status'])` 隐藏系统导航条(底部那条本就是我们自绘的
悬浮玻璃条),内容铺满全高。
★ 刻意**不用** `setWindowLayoutFullScreen(true)`:实测试过,它也能消掉黑带,但会连
状态栏区域一起吃进布局,页签栏被时钟/电量盖住(截图硬证「07:43」与「收件箱」重叠)。
── ② 联系人点不开(用户原话「连点都点不开」)──
根因:`ContactItem` / `WorkCard` **根本没有 onClick** —— 卡片画出来了,
但没有任何点击路径。当时判据只钉了"字段与 WebUI 一致",没钉"点了会发生什么"。
修法:点卡片打开那条会话的邮件列表,就地在**本窗格内**展示(新增 `SessionMailsView`
+ `GET /sessions/{id}/mails`),底部导航保留;每封可再点进详情。
── ③ 我的页不遵守导航规则 ──
根因:点底栏「我的」走 `pushUrl('pages/SettingsPage')` 推**独立 @Entry 页** ⇒
底部导航整条消失,要按返回才能再切窗格。WebUI 里 `account` 只是一个 `viewMode`,
与收件箱同级、导航常驻。
修法:`SettingsPage` → `SettingsPane`(去掉 @Entry 与返回键,从 main_pages.json 摘除),
作为**第 4 个内容窗格**挂到 `currentIndex === 3`;`NAV_CONTENT_COUNT` 3 → 4,
`NAV_ITEMS` 第 4 项去掉 `route`;拨动动画包 `getUIContext().animateTo`。
实测:`我的` 页底部导航可见,可滚到「退出登录」「管理」。
── ④ 顶栏硬截断 ──
根因:`Scroll` 与 `List` 都能挂 `fadingEdge`(同在 `ScrollableCommonMethod` 上),
但**只有 MainPage 的 List 挂了** —— MailDetailPage / CalendarPage / SettingsPage /
AdminUsersPage / SessionsPage / InboxPage 的滚动容器全漏。同一次滚动里
列表是渐隐、正文是硬切,看起来就是"顶栏没做渐变"。
修法:13 个容器全部补齐(LoginPage 例外:它的 Scroll 是整页根、不是列表,
写了理由)。实测:视口上沿被切断的文字最暗 170–196,而下方不透明正文是 **19**。
── ⑤ 一点动画都没有 ──
实测确认改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过**。
补:Theme 加令牌(durFast 120 / durBase 180 / easeOutSoft = cubic-bezier(0.22,1,0.36,1),
与 WebUI `--dur-*`/`--ease-out-soft` 同值同曲线);切窗格包 `animateTo`;
导航项选中变色加 `.animation`。
判据(218 passed / 0 failed):
· 新增「渐隐覆盖**所有**页面滚动容器」:按文件枚举 + 计数配平,
并自检扫到的容器总数(防正则写坏 ⇒ 全绿)。**这条一写就抓到 LoginPage**。
· ①/②/⑤ 与 headless 那几条按新事实改写:内容窗格 3→4、
`normalizeNavIndex(3)` 归一到自己、导航项里**禁止**再出现 `pushUrl`
(那正是导航消失的形状)。
· 修掉自己引入的两处判据缺陷:`itemCode` 未定义;以及一条**窗口式断言**
(从内容层 `}` 往后扫,靠数花括号定界,而注释里就有大量 `.padding({`,
切片长到 1700+ 字符一路扫进 `onAreaChange` ⇒ 假红),改为纯负向断言。
未验:真机观感;横屏 Auto Split 双栏;日历页深度对齐(本轮只补了渐隐)。
This commit is contained in:
@ -21,6 +21,11 @@ export class SessionListResponse {
|
||||
total: number = 0;
|
||||
}
|
||||
|
||||
/** `GET /sessions/{id}/mails` 的响应 */
|
||||
export class SessionMailsResponse {
|
||||
mails: MailSummary[] = [];
|
||||
}
|
||||
|
||||
/** 联系人列表响应 */
|
||||
export class ContactListResponse {
|
||||
contacts: Contact[] = [];
|
||||
@ -131,6 +136,20 @@ export class MailApi {
|
||||
return this.client.get<ContactListResponse>('/contacts');
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条会话下的全部邮件(`GET /sessions/{id}/mails`)。
|
||||
*
|
||||
* 联系人页点一条 "跟谁在聊" 就是打开这条会话的邮件列表 ——
|
||||
* 与 WebUI `ContactPanel` 的 `open(c)` → `selectSession(c.session_id)` 同一行为。
|
||||
* 返回裸数组(服务端 `{mails: [...]}`),这里只取数组。
|
||||
*/
|
||||
async sessionMails(sessionId: string): Promise<MailSummary[]> {
|
||||
const resp: SessionMailsResponse = await this.client.get<SessionMailsResponse>(
|
||||
'/sessions/' + sessionId + '/mails'
|
||||
);
|
||||
return resp.mails;
|
||||
}
|
||||
|
||||
/** 邮件详情(API 返回裸对象) */
|
||||
async mailDetail(mailId: string): Promise<MailDetail> {
|
||||
return this.client.get<MailDetail>('/mail/' + mailId);
|
||||
|
||||
@ -32,6 +32,8 @@
|
||||
* 两边的版式意图是同一套)。
|
||||
*/
|
||||
|
||||
import { curves } from '@kit.ArkUI';
|
||||
|
||||
export class Theme {
|
||||
// ─────────────── 表面:系统语义色 ───────────────
|
||||
|
||||
@ -263,4 +265,28 @@ export class Theme {
|
||||
static readonly fontTiny: number = 11;
|
||||
static readonly fontSmall: number = 12;
|
||||
static readonly fontBody: number = 14;
|
||||
|
||||
/*
|
||||
* ── 动画令牌(对照 WebUI `index.css` 的 `--dur-*` / `--ease-out-soft`)──
|
||||
*
|
||||
* 用户(2026-09-17):「一方面一点动画都没有」。
|
||||
* 实测确认:改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过** ——
|
||||
* 所有页面切换、详情推入、面板展开都是硬切。
|
||||
*
|
||||
* 三个数**与 WebUI 逐字一致**(`index.css:280-282`):
|
||||
* --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1)
|
||||
* --dur-fast: 120ms (控件变色:hover / active)
|
||||
* --dur-base: 180ms (面板与内容入场)
|
||||
* 这样两端的“快/慢”手感是同一个尺子,而不是各自拍一个数。
|
||||
*/
|
||||
static readonly durFast: number = 120;
|
||||
static readonly durBase: number = 180;
|
||||
|
||||
/**
|
||||
* 缓动曲线:`cubic-bezier(0.22, 1, 0.36, 1)` —— 与 WebUI `--ease-out-soft` 同一根曲线。
|
||||
*
|
||||
* 用 `curves.cubicBezierCurve` 而不是 `Curve.EaseOut` 之类的枚举:
|
||||
* 枚举是系统预设的**另一根**曲线,观感与 WebUI 对不上(而“对齐 WebUI”正是本项要求)。
|
||||
*/
|
||||
static readonly easeOutSoft: ICurve = curves.cubicBezierCurve(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@ -101,6 +101,33 @@ export default class EntryAbility extends UIAbility {
|
||||
// Main window is created, set main page for this ability
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
|
||||
|
||||
/*
|
||||
* ★ 底部黑带 = 系统的**导航栏区域**(手势条),不是我们画的。
|
||||
*
|
||||
* 症状(用户 2026-09-17 真机截图):屏幕最下面一条纯黑。
|
||||
* 根因:窗口默认给系统导航条留了位置,而那块区域在深色主题下是黑的;
|
||||
* 界面于是看起来底下多出一条黑带,壁纸/面板到不了屏幕底部。
|
||||
*
|
||||
* 做法:**隐藏系统导航条**(`setWindowSystemBarEnable(['status'])`)——
|
||||
* 底部那条本来就是我们自绘的悬浮玻璃条(用户要求「悬浮 + 四周留白」),
|
||||
* 系统那条黑底手势条不是设计的一部分。隐藏后内容铺满全高(到屏幕底)。
|
||||
*
|
||||
* ★ **刻意不用 `setWindowLayoutFullScreen(true)`。**
|
||||
* 实测过:它确实也消掉黑带,但会连状态栏区域一起吃进布局,
|
||||
* 于是页签栏被时钟/电量盖住(截图硬证:「07:43」与「收件箱」重叠)。
|
||||
* 而我们并不需要内容跑到状态栏底下 —— 那要多一层 AvoidArea 避让才收得住,
|
||||
* 为一个没人要求的效果引入一层布局风险不划算。
|
||||
* 状态栏**保留**(时间/电量要看得见),且它占的位置由系统留好。
|
||||
*/
|
||||
try {
|
||||
const win: window.Window = windowStage.getMainWindowSync();
|
||||
win.setWindowSystemBarEnable(['status']).catch((err: Object) => {
|
||||
hilog.error(DOMAIN, 'testTag', 'setWindowSystemBarEnable failed: %{public}s', JSON.stringify(err));
|
||||
});
|
||||
} catch (err) {
|
||||
hilog.error(DOMAIN, 'testTag', 'window setup failed: %{public}s', JSON.stringify(err));
|
||||
}
|
||||
|
||||
windowStage.loadContent('pages/LoginPage', (err) => {
|
||||
if (err.code) {
|
||||
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
|
||||
|
||||
@ -37,6 +37,14 @@ export interface NavItem {
|
||||
* 占一个 index 会出现"点了没内容、或每次重建都 pushUrl"的尴尬。外壳入口在
|
||||
* NavItem 的 onClick 里直接路由,与 WideSidebar 底部那个设置按钮是同一行为。
|
||||
*/
|
||||
/**
|
||||
* 外壳入口的路由目标。
|
||||
*
|
||||
* ★ 2026-09-17 之后**没有导航项再用它** —— 第 4 项「我的」已改为内容窗格
|
||||
* (见 `NAV_ITEMS` 的说明)。字段保留是因为宽屏侧栏的“设置”按钮与
|
||||
* `WideSidebar.onSettings` 仍走同一套路由语义;**新加的导航项不要再填它**,
|
||||
* 否则又会出现“点了导航、导航条整个消失”的形状。
|
||||
*/
|
||||
route?: string;
|
||||
}
|
||||
|
||||
@ -44,9 +52,11 @@ export interface NavItem {
|
||||
* 底栏四枚图标 = 三个内容窗格 + 一个外壳入口,与 WebUI `NarrowNav.tsx` 的四入口一一对应。
|
||||
*
|
||||
* 前**三项**是平级内容窗格:通信(内部三栏:收件箱/发件箱/授权)、日历、联系人。
|
||||
* 第**四项**「我的」是外壳入口(`route` 指向 `pages/SettingsPage`)—— 它承载
|
||||
* 设置/主题/壁纸/账号,是独立 @Entry 页,不在主界面 currentIndex 分派里挂内容
|
||||
* (理由见 `NavItem.route` 的注释;WideSidebar 同样把设置单列在导航项之后)。
|
||||
* 第**四项**「我的」也是**内容窗格**(与前三项同一套机制,`currentIndex === 3`)——
|
||||
* 它承载设置/主题/壁纸/账号。★ 2026-09-17 修正:原先它是外壳入口(`route` 走
|
||||
* `pushUrl('pages/SettingsPage')` 推独立页),导致底部导航整条消失 —— 用户原话
|
||||
* 「我的页面完全没有遵守 nav 的导航规则」。WebUI 的 `account` 只是一个 `viewMode`,
|
||||
* 导航常驻,所以这里改成同一套窗格机制。
|
||||
*
|
||||
* 「会话」原先是个平级 tab,已撤 —— 它不是第三个地方,而是"同一批数据的另一种看法"
|
||||
* (收件箱按会话折叠、联系人卡片视图就是会话的进度视角)。理由见 `MainPage.build()` 的注释。
|
||||
@ -60,7 +70,15 @@ export const NAV_ITEMS: NavItem[] = [
|
||||
{ key: 'comm', label: '通信', iconKey: 'inbox' },
|
||||
{ key: 'calendar', label: '日历', iconKey: 'calendar' },
|
||||
{ key: 'contacts', label: '联系人', iconKey: 'contacts' },
|
||||
{ key: 'me', label: '我的', iconKey: 'person', route: 'pages/SettingsPage' }
|
||||
/*
|
||||
* ★ 2026-09-17:第 4 项**不再带 `route`** —— 它现在是**内容窗格**。
|
||||
*
|
||||
* 原形状是外壳入口:`route: 'pages/SettingsPage'`,点击 `pushUrl` 推一个
|
||||
* 独立 @Entry 页 ⇒ 底部导航整条消失(用户:「我的页面完全没有遵守 nav 的导航规则」)。
|
||||
* WebUI 的 `account` 只是一个 `viewMode`(与 inbox/calendar/contacts 同级),
|
||||
* 底部导航**无条件渲染** —— 所以这里改成同一套窗格机制。
|
||||
*/
|
||||
{ key: 'me', label: '我的', iconKey: 'person' }
|
||||
];
|
||||
|
||||
/** 命中区下限(vp)。**44 是可点区域的下限**,不是"看起来够大"的估计 */
|
||||
@ -111,7 +129,8 @@ export const NAV_CONTENT_RESERVE: number = NAV_BAR_HEIGHT + NAV_BAR_BOTTOM + NAV
|
||||
|
||||
/** 内容窗格的数量(前三项:通信/日历/联系人)—— `MainPage` 的 currentIndex 分派按它穷尽,
|
||||
* `NAV_CONTENT_ITEMS` 也按它切片 */
|
||||
export const NAV_CONTENT_COUNT: number = 3;
|
||||
/** 内容窗格的数量(四项:通信/日历/联系人/我的)—— `MainPage` 的 currentIndex 分派按它穷尽 */
|
||||
export const NAV_CONTENT_COUNT: number = 4;
|
||||
|
||||
/** 内容窗格的清单(前三项:通信/日历/联系人)—— 宽屏侧栏与 currentIndex 分派都用它
|
||||
* (第 4 项「我的」是外壳入口,不在内 —— 否则宽屏侧栏会出现两个 person 图标) */
|
||||
|
||||
@ -34,6 +34,8 @@ import {
|
||||
} from '../model/Models';
|
||||
import { toggled, lastLoginLabel, messageOfApiError, isAdminRole, isRestricted } from '../model/AdminUsers';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { LIST_FADE_LENGTH } from '../model/NavItems';
|
||||
import { LengthMetrics } from '@kit.ArkUI';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -313,6 +315,8 @@ struct AdminUsersPage {
|
||||
}, (user: AdminUser) => user.user_id)
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
/* 上下边缘渐隐(2026-09-17 补:原先漏了,同一次滚动里只有部分是渐隐) */
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
.padding({ left: 12, right: 12, top: 8, bottom: 12 })
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,8 @@ import {
|
||||
isoTimestampOfLocal,
|
||||
localDateOf
|
||||
} from '../model/Calendar';
|
||||
import { LIST_FADE_LENGTH } from '../model/NavItems';
|
||||
import { LengthMetrics } from '@kit.ArkUI';
|
||||
|
||||
/** 一格的高度(vp)。7 列等分 + 固定高度才画得成整齐的网格 */
|
||||
const CELL_HEIGHT: number = 52;
|
||||
@ -693,6 +695,8 @@ export struct CalendarPage {
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
/* 上下边缘渐隐(2026-09-17 补:这一处原先漏了,同一次滚动里只有列表是渐隐) */
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
@ -798,6 +802,8 @@ export struct CalendarPage {
|
||||
.width('100%')
|
||||
/* 末尾让位:最后一条日程要能滚出悬浮条之下(不是缩短列表) */
|
||||
.contentEndOffset(this.navReserve)
|
||||
/* 上下边缘渐隐(2026-09-17 补:原先漏了,同一次滚动里只有部分是渐隐) */
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
.padding({ left: 10, right: 10 })
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ 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';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -120,6 +122,8 @@ struct InboxPage {
|
||||
}
|
||||
.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 })
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ import { MailDetail, SendMailRequest, Address } from '../model/Models';
|
||||
import { MailDetailParams } from '../model/RouteParams';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { permissionLabel } from '../model/MailGrouping';
|
||||
import { LIST_FADE_LENGTH } from '../model/NavItems';
|
||||
import { LengthMetrics } from '@kit.ArkUI';
|
||||
import {
|
||||
participantAddress,
|
||||
mailReplyTarget,
|
||||
@ -427,6 +429,17 @@ export struct MailDetailView {
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
/*
|
||||
* ★ 上下边缘渐隐(2026-09-17 补)。
|
||||
*
|
||||
* 用户:「你的顶栏为什么还是硬截断而不是渐变?」
|
||||
* 根因:`Scroll` 与 `List` 都能挂 `fadingEdge`(都在 `ScrollableCommonMethod`
|
||||
* 上),但**详情页这里一直没挂** —— 而列表页挂了。于是同一次滚动里
|
||||
* 列表是渐隐、正文是硬切,看起来就像"顶栏没做渐变"。
|
||||
* 同一轮盘点还查出 CalendarPage / SettingsPage / AdminUsersPage 的
|
||||
* 滚动容器也漏了,一并补上(见 harmony-nav 的判据)。
|
||||
*/
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
|
||||
/*
|
||||
* 回复入口:**右下角悬浮球**,与 WebUI 的 `reply-fab` 同形同位置。
|
||||
|
||||
@ -69,6 +69,7 @@ import { PushService, PushRoute } from '../api/PushService';
|
||||
* 这条注释留在 **import 区**而不是跟着表走,因为要守的是"import 在最前"这个位置本身。
|
||||
*/
|
||||
import { CalendarPage } from './CalendarPage';
|
||||
import { SettingsPane } from './SettingsPage';
|
||||
import { LengthMetrics } from '@kit.ArkUI';
|
||||
import {
|
||||
LIST_FADE_LENGTH,
|
||||
@ -1387,6 +1388,27 @@ struct ContactsTab {
|
||||
@State contacts: Contact[] = [];
|
||||
@State loading: boolean = false;
|
||||
@State error: string = '';
|
||||
/*
|
||||
* 打开的会话(空串 = 没开)—— 与 WebUI `ContactPanel` 的 `selectSession` 同一行为:
|
||||
* 点一条“跟谁在聊”就是打开**这条会话的邮件列表**。
|
||||
*
|
||||
* ★ 2026-09-17 用户:「联系人页面连点都点不开」。
|
||||
* 根因:`ContactItem` / `WorkCard` 两个 builder **根本没有 `onClick`** ——
|
||||
* 卡片画出来了、但没有任何点击路径(判据当时只钉了“字段与 WebUI 一致”,
|
||||
* 没钉“点了会发生什么”)。现在补上:点卡片 → 在**本 pane 内**打开会话,
|
||||
* 底部导航保留(与 WebUI 的 pane 模型一致,不跳 @Entry 页)。
|
||||
*/
|
||||
@State openSessionId: string = '';
|
||||
@State openSessionTitle: string = '';
|
||||
@State sessionMails: MailSummary[] = [];
|
||||
@State sessionLoading: boolean = false;
|
||||
/**
|
||||
* 联系人自己的导航栈 —— 与 `CommPage` 同一模式(每个有「列表→详情」的窗格自带一个
|
||||
* `Navigation`)。这样点联系人卡片打开邮件详情时,**底部导航一直可见**
|
||||
* (详情挂在窗格内部),而不是推一个盖住导航的 @Entry 页。
|
||||
* 这也正是用户说的「我的页面完全没有遵守 nav 的导航规则」那条的同源问题。
|
||||
*/
|
||||
private navPathStack: NavPathStack = new NavPathStack();
|
||||
/**
|
||||
* 视图:'list'(跟谁在聊)/ 'card'(在聊什么、进展如何)。
|
||||
*
|
||||
@ -1429,7 +1451,58 @@ struct ContactsTab {
|
||||
this.contactView = nextContactView(this.contactView);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一条会话的邮件列表(点联系人卡片就走这里)。
|
||||
*
|
||||
* 与 WebUI `ContactPanel.open()` 同构:`selectSession(c.session_id)` 后
|
||||
* 内容栏切到会话。这里把“会话的邮件”拉下来就地展示在**本 pane 内** ——
|
||||
* 不推 @Entry 页,因为 WebUI 的会话是**同一个 pane 的另一种内容**,
|
||||
* 底部导航一直可见(推页会把导航盖掉,那是另一种信息架构)。
|
||||
*/
|
||||
async openSession(c: Contact): Promise<void> {
|
||||
const m: MailApi | null = this.mailApi;
|
||||
if (m === null) {
|
||||
return;
|
||||
}
|
||||
this.openSessionId = c.session_id;
|
||||
this.openSessionTitle = c.subject.length > 0
|
||||
? c.subject
|
||||
: (c.session_alias.length > 0 ? c.session_alias : c.agent_name);
|
||||
this.sessionMails = [];
|
||||
this.sessionLoading = true;
|
||||
try {
|
||||
this.sessionMails = await m.sessionMails(c.session_id);
|
||||
} catch (e) {
|
||||
const ae = e as ApiError;
|
||||
this.getUIContext().getPromptAction().showToast({ message: ae.message.length > 0 ? ae.message : '打开会话失败' });
|
||||
} finally {
|
||||
this.sessionLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 回到联系人列表 */
|
||||
closeSession(): void {
|
||||
this.openSessionId = '';
|
||||
this.openSessionTitle = '';
|
||||
this.sessionMails = [];
|
||||
}
|
||||
|
||||
/** 点一封邮件 → 推进本窗格的导航栈(与收件箱同一条详情路径) */
|
||||
openMail(mailId: string, accountId: string): void {
|
||||
const params: MailDetailParams = { mail_id: mailId, account_id: accountId };
|
||||
this.navPathStack.pushPath({ name: MAIL_DETAIL_ROUTE, param: params });
|
||||
}
|
||||
|
||||
@Builder
|
||||
DestinationBuilder(name: string, param: Object) {
|
||||
if (name === MAIL_DETAIL_ROUTE) {
|
||||
MailDetailDestination({ navReserve: this.navReserve })
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
/* 本窗格自带 Navigation(与 CommPage 同一模式):列表 → 邮件详情,底部导航始终可见 */
|
||||
Navigation(this.navPathStack) {
|
||||
Column() {
|
||||
Row() {
|
||||
Text(contactViewTitle(this.contactView)).fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
@ -1445,7 +1518,13 @@ struct ContactsTab {
|
||||
.width('100%').height(56).padding({ left: 16, right: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
|
||||
if (this.loading) {
|
||||
/*
|
||||
* ★ 会话视图:点联系人卡片后**在本 pane 内**展示那条会话的邮件。
|
||||
* 不另开 @Entry 页 —— 与 WebUI 的 pane 模型一致(底部导航一直可见)。
|
||||
*/
|
||||
if (this.openSessionId.length > 0) {
|
||||
this.SessionMailsView()
|
||||
} else if (this.loading) {
|
||||
Column() {
|
||||
LoadingProgress().width(40).height(40)
|
||||
}
|
||||
@ -1469,6 +1548,8 @@ struct ContactsTab {
|
||||
this.WorkCard(c)
|
||||
}
|
||||
.width('100%')
|
||||
/* 点卡片 = 打开这条会话(WebUI `ContactPanel` 的 onOpen) */
|
||||
.onClick(() => { this.openSession(c); })
|
||||
}, (_c: Contact, idx: number) => idx.toString())
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
@ -1486,6 +1567,8 @@ struct ContactsTab {
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.clip(true)
|
||||
/* 点卡片 = 打开这条会话(WebUI `ContactPanel` 的 onOpen) */
|
||||
.onClick(() => { this.openSession(c); })
|
||||
}, (_c: Contact, idx: number) => idx.toString())
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
@ -1496,6 +1579,91 @@ struct ContactsTab {
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg)
|
||||
}
|
||||
.navDestination(this.DestinationBuilder)
|
||||
.mode(NavigationMode.Auto)
|
||||
/* 会话列表栏宽度与通信页同一口径(320,范围 280–360) */
|
||||
.navBarWidth(320)
|
||||
.navBarWidthRange([280, 360])
|
||||
.minContentWidth(360)
|
||||
.hideTitleBar(true)
|
||||
.width('100%').height('100%')
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话视图:一条会话下的全部邮件(点联系人卡片后显示)。
|
||||
*
|
||||
* 与 WebUI 的会话内容区同构:顶部一行返回 + 会话标题,下面是那几封邮件。
|
||||
* 每封点开推进本窗格的 `Navigation`(与收件箱同一条详情路径)。
|
||||
*/
|
||||
@Builder
|
||||
SessionMailsView() {
|
||||
Column() {
|
||||
/* 返回行:与 WebUI 的 `BackButton label="会话"` 同一语义 */
|
||||
Row() {
|
||||
Text('‹').fontSize(24).fontColor(Theme.accent).width(36).height(36)
|
||||
.textAlign(TextAlign.Center)
|
||||
.onClick(() => { this.closeSession(); })
|
||||
Text(this.openSessionTitle)
|
||||
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
Text(this.sessionMails.length + ' 封')
|
||||
.fontSize(11).fontColor(Theme.textMuted)
|
||||
}
|
||||
.width('100%').height(44).padding({ left: 4, right: 12 })
|
||||
.backgroundColor(Theme.surface)
|
||||
|
||||
if (this.sessionLoading) {
|
||||
Column() { LoadingProgress().width(32).height(32) }
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.sessionMails.length === 0) {
|
||||
Column() {
|
||||
Text('这条会话没有邮件').fontSize(14).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List({ space: 6 }) {
|
||||
ForEach(this.sessionMails, (m: MailSummary) => {
|
||||
ListItem() {
|
||||
Column() {
|
||||
Row() {
|
||||
Text(m.from_name.length > 0 ? m.from_name : '—')
|
||||
.fontSize(12).fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Blank()
|
||||
Text(compactMailTime(m.created_at))
|
||||
.fontSize(10).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%')
|
||||
Text(m.subject)
|
||||
.fontSize(14)
|
||||
.fontWeight(m.status === 'unread' ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%').margin({ top: 4 })
|
||||
Text(m.body_preview)
|
||||
.fontSize(11).fontColor(Theme.textSubtle)
|
||||
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%').margin({ top: 2 })
|
||||
}
|
||||
.width('100%').alignItems(HorizontalAlign.Start)
|
||||
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.border({ width: 1, color: Theme.border })
|
||||
}
|
||||
.width('100%')
|
||||
.onClick(() => { this.openMail(m.mail_id, m.source_account_id); })
|
||||
}, (m: MailSummary) => m.mail_id)
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
.contentEndOffset(this.navReserve)
|
||||
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
|
||||
}
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1949,15 +2117,42 @@ struct MainPage {
|
||||
.height(NAV_BAR_HEIGHT)
|
||||
.constraintSize({ minHeight: NAV_ITEM_MIN_HIT, minWidth: NAV_ITEM_MIN_HIT })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
// 点击:外壳入口(route 非空)直接路由到 @Entry 页(与 WideSidebar 的设置按钮同一行为);
|
||||
// 内容窗格才进 currentIndex 分派。两者不能混:设置页是 @Entry,在内容分派里占 index
|
||||
// 会出现"点了没内容"或"每次重建都 pushUrl"(见 NavItem.route 的注释)。
|
||||
/*
|
||||
* 选中/取消选中时图标与文字**变色**要有过渡(不是硬切)。
|
||||
*
|
||||
* 用户(2026-09-17):「一方面一点动画都没有」。
|
||||
* 与 WebUI `.nav-item { transition: color 150ms }` 同一语义;
|
||||
* 时长用令牌(120ms, `--dur-fast`),不另拍一个数。
|
||||
*/
|
||||
.animation({ duration: Theme.durFast, curve: Theme.easeOutSoft })
|
||||
.onClick(() => {
|
||||
if (item.route !== undefined && item.route.length > 0) {
|
||||
this.getUIContext().getRouter().pushUrl({ url: item.route });
|
||||
/*
|
||||
* ★ 四项**都是内容窗格**(用户 2026-09-17:「我的页面完全没有遵守 nav 的导航规则」)。
|
||||
*
|
||||
* 原先第 4 项走 `pushUrl('pages/SettingsPage')` —— 推一个独立 @Entry 页,
|
||||
* 底部导航整条消失。WebUI 的 `account` 只是一个 `viewMode`,与收件箱同级、
|
||||
* 导航常驻。所以现在统一按 `currentIndex` 分派(normalizeNavIndex 的上界
|
||||
* 也跟着放宽到四项)。
|
||||
*
|
||||
* ★ 切窗格走 `animateTo`:用户(2026-09-17)「一方面一点动画都没有」。
|
||||
* 改 `currentIndex` 会换掉整块内容 —— 不包一层过渡就是硬切。
|
||||
* WebUI 的对应物是 `html.view-switch .pane-rise { animation: rise-in 150ms }`
|
||||
* (只给**刚出现的面板**做 4px 上浮 + 淡入,骨架不动 —— 用户对"闪"很敏感,
|
||||
* 所以不做整屏淡入、不做缩放)。
|
||||
* 时长/曲线用令牌(180ms + cubic-bezier(0.22,1,0.36,1)),与 WebUI 同一根尺子。
|
||||
* 必须走 `getUIContext().animateTo`:全局 `animateTo` 已被 SDK 标废弃,
|
||||
* 判据(harmony-system-api)对全局调用默认判红。
|
||||
*/
|
||||
const target: number = normalizeNavIndex(index);
|
||||
if (target === this.currentIndex) {
|
||||
return;
|
||||
}
|
||||
this.currentIndex = normalizeNavIndex(index);
|
||||
this.getUIContext().animateTo({
|
||||
duration: Theme.durBase,
|
||||
curve: Theme.easeOutSoft
|
||||
}, () => {
|
||||
this.currentIndex = target;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@ -2091,6 +2286,17 @@ struct MainPage {
|
||||
CommPage({ bgActive: this.bgActive, navReserve: this.navReserve })
|
||||
} else if (this.currentIndex === 2) {
|
||||
ContactsTab({ bgActive: this.bgActive, navReserve: this.navReserve })
|
||||
} else if (this.currentIndex === 3) {
|
||||
/*
|
||||
* ★ 第四项「我的」现在是**内容窗格**(不再是 push 出去的 @Entry 页)。
|
||||
*
|
||||
* 用户 2026-09-17:「我的页面完全没有遵守 nav 的导航规则」。
|
||||
* 原形状:点底栏「我的」→ `pushUrl('pages/SettingsPage')` 推开一个独立页面
|
||||
* ⇒ 底部导航整条消失,要按「返回」才能再切窗格。
|
||||
* WebUI 里 `account` 只是一个 `viewMode`,与收件箱/日历同级、导航常驻。
|
||||
* 所以这里按窗格挂载(与前三项同一套机制)。
|
||||
*/
|
||||
SettingsPane({ bgActive: this.bgActive, navReserve: this.navReserve })
|
||||
}
|
||||
/*
|
||||
* 日历与另外两个 pane 不同:**常驻挂载**,用 `visibility` 控制显示。
|
||||
|
||||
@ -6,6 +6,8 @@ import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { MailApi } from '../api/MailApi';
|
||||
import { Session } from '../model/Models';
|
||||
import { LIST_FADE_LENGTH } from '../model/NavItems';
|
||||
import { LengthMetrics } from '@kit.ArkUI';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -86,6 +88,8 @@ struct SessionsPage {
|
||||
}, (s: Session) => s.session_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 })
|
||||
}
|
||||
|
||||
|
||||
@ -29,9 +29,12 @@ import { BackgroundPicker } from '../common/BackgroundPicker';
|
||||
import { PushService } from '../api/PushService';
|
||||
import { PushTokenList } from '../model/PushContract';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct SettingsPage {
|
||||
export struct SettingsPane {
|
||||
/** 背景开关(与其它窗格同一约定:开着就让出页面底) */
|
||||
@Prop bgActive: boolean = false;
|
||||
/** 底部悬浮条高度(窄屏非 0)—— 本页滚动容器的内容末尾要让出它 */
|
||||
@Prop navReserve: number = 0;
|
||||
@State accounts: AccountInfo[] = [];
|
||||
@State activeId: string = '';
|
||||
@State showAddDialog: boolean = false;
|
||||
@ -565,17 +568,25 @@ struct SettingsPage {
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
/*
|
||||
* ★ 「我的」是**第四个内容窗格**,不是另开的 @Entry 页(2026-09-17 修正)。
|
||||
*
|
||||
* 用户:「我的页面完全没有遵守 nav 的导航规则」—— 指的就是原先的形状:
|
||||
* 点底栏「我的」会 `pushUrl('pages/SettingsPage')` 推一个**独立页面**,
|
||||
* 于是底部导航整条消失,用户在那页里要先「返回」才能再切换窗格。
|
||||
* 而 WebUI 里 `account` 只是一个 `viewMode` —— 它和收件箱、日历一样
|
||||
* 是**同一个外壳里的一个窗格**,底部导航**一直可见**(`NarrowNav` 无条件渲染)。
|
||||
*
|
||||
* 所以这里去掉返回键(导航条就是返回路径),标题保留。
|
||||
*/
|
||||
Row() {
|
||||
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(Theme.textPrimary)
|
||||
Text('我的').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.layoutWeight(1)
|
||||
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 })
|
||||
.width('100%').height(56).padding({ left: 16, right: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
|
||||
Divider().color(Theme.border)
|
||||
@ -648,6 +659,8 @@ struct SettingsPage {
|
||||
.scrollBar(BarState.Off)
|
||||
/* WebUI `.overflow-y-auto` 的上下渐隐:内容滑到边缘不硬截断 */
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
/* 末尾让位:最后一段(退出登录)要能滚出悬浮条之下 */
|
||||
.contentEndOffset(this.navReserve)
|
||||
|
||||
// 新增账号弹层(留在最外层,不被滚动容器裁掉)
|
||||
if (this.showAddDialog) {
|
||||
@ -699,7 +712,8 @@ struct SettingsPage {
|
||||
}
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.backgroundColor(Theme.pageBg)
|
||||
/* 与其它窗格同一约定:背景开着时让出页面底,壁纸才透得过 */
|
||||
.backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg)
|
||||
}
|
||||
|
||||
/** 分区标题(与 WebUI `AccountPage` 的 `<h3>` 同形:小字、灰、上间距) */
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
"pages/MainPage",
|
||||
"pages/MailDetailPage",
|
||||
"pages/ComposePage",
|
||||
"pages/SettingsPage",
|
||||
"pages/SessionsPage",
|
||||
"pages/AdminUsersPage",
|
||||
"pages/Index"
|
||||
|
||||
Reference in New Issue
Block a user