From 686193458adcc0453d7d480486d13a84b4fa31f6 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 17 Sep 2026 21:15:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(harmony):=20=E5=BA=95=E6=A0=8F=E9=BB=91?= =?UTF-8?q?=E5=B8=A6=20/=20=E8=81=94=E7=B3=BB=E4=BA=BA=E7=82=B9=E4=B8=8D?= =?UTF-8?q?=E5=BC=80=20/=20=E6=88=91=E7=9A=84=E9=A1=B5=E4=B8=8D=E5=AE=88?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=20/=20=E9=A1=B6=E6=A0=8F=E7=A1=AC=E6=88=AA?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户(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 双栏;日历页深度对齐(本轮只补了渐隐)。 --- client/electron/test/harmony-nav.test.mjs | 127 ++++++++-- .../entry/src/main/ets/api/MailApi.ets | 19 ++ .../entry/src/main/ets/common/Theme.ets | 26 +++ .../main/ets/entryability/EntryAbility.ets | 27 +++ .../entry/src/main/ets/model/NavItems.ts | 29 ++- .../src/main/ets/pages/AdminUsersPage.ets | 4 + .../entry/src/main/ets/pages/CalendarPage.ets | 6 + .../entry/src/main/ets/pages/InboxPage.ets | 4 + .../src/main/ets/pages/MailDetailPage.ets | 13 ++ .../entry/src/main/ets/pages/MainPage.ets | 220 +++++++++++++++++- .../entry/src/main/ets/pages/SessionsPage.ets | 4 + .../entry/src/main/ets/pages/SettingsPage.ets | 30 ++- .../resources/base/profile/main_pages.json | 1 - 13 files changed, 468 insertions(+), 42 deletions(-) diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs index 82875b7..3faa914 100644 --- a/client/electron/test/harmony-nav.test.mjs +++ b/client/electron/test/harmony-nav.test.mjs @@ -8,6 +8,7 @@ // ③ 命中区:≥44vp —— 从 `NavItems.ts` **导入数值**判,不拿正则去源码里猜; // ④ 悬浮与让位:条是自绘的浮动层(留白 + 圆角 + 系统材质), // 且内容底部让出的高度 ≥ 条高 + 离底留白(否则最后一行压在条底下:看得见、点不到)。 +import { readdirSync } from 'node:fs'; import { code, prose, stripComments } from './lib/read.mjs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -64,6 +65,7 @@ test('① 点击:每个导航项点下去落到**它自己**那个 index(配 */ const item = builderBody(main, 'NavItem(item: NavItem, index: number) {'); assert.ok(item.length > 100, '要能取到导航项的正文'); + const itemCode = stripComments(item); /* * 点击处理器是 `() => { this.currentIndex = normalizeNavIndex(index); }`。 * 取赋值语句并断言它**同时**用到 `index` 与归一化函数 —— @@ -71,13 +73,31 @@ test('① 点击:每个导航项点下去落到**它自己**那个 index(配 * 所以要求括号里的实参就是 `index`。 */ // 只数**赋值**(`= `),不数三元式里的 `===` —— 外壳入口不赋值,所以正文里仍只能有一次赋值 - const assigns = [...item.matchAll(/(? m[1].trim()); - assert.equal(assigns.length, 1, `导航项里应当恰好一次 currentIndex 赋值(实际 ${assigns.length} 次)—— 外壳入口(route 非空)走路由,不赋 currentIndex`); - assert.match(assigns[0], /normalizeNavIndex\(\s*index\s*\)/, - `点击要落到本项自己的 index 并过归一化,现在赋的是:${assigns[0]}`); - // 外壳入口(route 非空)走路由、内容窗格才赋 currentIndex —— 两者都走在本项的 onClick 里 - assert.match(item, /item\.route.*pushUrl|pushUrl.*item\.route|if \(item\.route[\s\S]{0,80}pushUrl/, - '外壳入口(route 非空)要路由到 @Entry 页(与 WideSidebar 的设置按钮同一行为)'); + /* + * ★ 2026-09-17:赋值改成在 `animateTo` 回调里写一个**局部变量** `target`, + * 而这个 `target` 由 `normalizeNavIndex(index)` 算出来 —— 仍然要求 + * "点第 N 项落到第 N 个",只是多了一层变量。 + * 判据跟着改成:先要求 `target` 出自 `normalizeNavIndex(index)`, + * 再要求赋值写的是 `target`(而不是某个写死的下标)。 + */ + assert.match(itemCode, /const\s+target:\s*number\s*=\s*normalizeNavIndex\(\s*index\s*\)/, + '点击目标要由本项自己的 index 过归一化算出来(`target = normalizeNavIndex(index)`)'); + const assigns = [...itemCode.matchAll(/(? m[1].trim()); + assert.equal(assigns.length, 1, `导航项里应当恰好一次 currentIndex 赋值(实际 ${assigns.length} 次)`); + assert.equal(assigns[0], 'target', + `赋值要写算出来的 target(本项自己的 index),现在写的是:${assigns[0]}`); + /* 切窗格包在 animateTo 里(否则是硬切 —— 用户 2026-09-17「一点动画都没有」) */ + assert.match(itemCode, /getUIContext\(\)\.animateTo\(/, + '★ 切窗格要包在 `getUIContext().animateTo(...)` 里(全局 animateTo 已废弃,判据会红)'); + /* + * ★ 2026-09-17:**不再**要求"route 非空就 pushUrl"。 + * + * 那条正是"我的页面不遵守导航规则"的源头:点底栏「我的」推一个独立 @Entry 页, + * 底部导航整条消失。现在四项都是内容窗格,统一走 currentIndex 分派。 + * 这里反过来钉:导航项里**不该**再有 pushUrl 分支(否则那个形状会回来)。 + */ + assert.ok(!/pushUrl/.test(itemCode), + '★ 导航项里不许再有 pushUrl —— 那会把导航条整个推走(用户 2026-09-17 抱怨的形状)'); // ForEach 要真的把 index 传进来(否则上面那句 index 无从谈起) const bar = builderBody(main, 'NavBar() {'); assert.match(bar, /ForEach\(NAV_ITEMS,\s*\(item: NavItem,\s*index: number\)/, 'ForEach 要带 index 参数'); @@ -111,6 +131,12 @@ test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人 * 写成"否则就挂联系人"的后果:以后再加一项,新索引会被它静默接住(点了显示联系人)。 */ assert.match(root, /else if \(this\.currentIndex === 2\)/, '联系人必须显式绑在 index 2,不许用 else 兜底'); + /* 第 4 项「我的」也是内容窗格(2026-09-17:原来是 pushUrl 推独立页 ⇒ 导航消失) */ + assert.match(root, /else if \(this\.currentIndex === 3\)/, + '「我的」要显式绑在 index 3(内容窗格),不许用 else 兜底'); + assert.match(root, /SettingsPane\(\{\s*bgActive: this\.bgActive,\s*navReserve: this\.navReserve\s*\}\)/, + '★ index 3 要挂 SettingsPane(**窗格**,不是 pushUrl 出去的 @Entry 页)——' + + '用户 2026-09-17:「我的页面完全没有遵守 nav 的导航规则」'); /* * 日历(index 1):**常驻挂载 + visible 由索引驱动 + Visibility 开关**,三样缺一不可。 * · `visible: this.currentIndex === 1` 是 `@Watch` 的触发源 —— 不带它,today 就不会在 @@ -124,20 +150,24 @@ test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人 assert.ok(!/if \(this\.currentIndex === 1\)/.test(root), '日历不该写成 if/else 的一支:它是常驻 pane(卸载重挂会丢掉"在看哪个月",且 today 只能靠挂载重算)'); /* - * 分派必须**穷尽内容窗格**(`NAV_CONTENT_COUNT` = 3:通信/日历/联系人),但 - * **不**把外壳入口(第 4 项「我的」)算进去 —— 它是 `@Entry` 页,点击走路由, - * 不在 currentIndex 分派里挂内容。2026-09-14 日历入口上架时这条**如约变红** + * 分派必须**穷尽内容窗格**。2026-09-14 日历入口上架时这条**如约变红** * (当时是 `length === 2` + 两支 if/else)—— 加**窗格**必须同时加分派。 - * 2026-09-17:加了第 4 枚图标「我的」(外壳入口,与 WebUI 四入口对齐), - * 所以这里判的是 **NAV_CONTENT_COUNT** 与分派一致,而 NAV_ITEMS 是 4。 + * + * ★ 2026-09-17:内容窗格 3 → **4**(「我的」从外壳入口改成窗格)。 + * 用户原话:「我的页面完全没有遵守 nav 的导航规则」—— + * 原来点底栏「我的」是 `pushUrl('pages/SettingsPage')` 推一个独立 @Entry 页, + * 底部导航整条消失;而 WebUI 的 `account` 只是一个 `viewMode`,导航常驻。 + * 所以现在四项**都是内容窗格**,`NAV_ITEMS.length === NAV_CONTENT_COUNT`, + * 不再有"外壳入口"这一类。 */ - assert.equal(N.NAV_CONTENT_COUNT, 3, `内容窗格应为 3(通信/日历/联系人),实际 ${N.NAV_CONTENT_COUNT}`); - assert.equal(N.NAV_ITEMS.length, N.NAV_CONTENT_COUNT + 1, - `NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项;应为 3 个内容窗格 + 1 个外壳入口(我的)`); - // 第 4 项必须是外壳入口(带 route),否则它会被 currentIndex 分派静默漏掉 - const shell = N.NAV_ITEMS[N.NAV_CONTENT_COUNT]; - assert.equal(shell.key, 'me', '第 4 项应是「我的」'); - assert.equal(shell.route, N.NAV_SETTINGS_ROUTE, '「我的」要路由到设置页'); + assert.equal(N.NAV_CONTENT_COUNT, 4, `内容窗格应为 4(通信/日历/联系人/我的),实际 ${N.NAV_CONTENT_COUNT}`); + assert.equal(N.NAV_ITEMS.length, N.NAV_CONTENT_COUNT, + `NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项;应与内容窗格数一致(${N.NAV_CONTENT_COUNT})—— 四项都是窗格`); + /* 第 4 项「我的」**不得**再带 route:带 route 就会被当外壳入口 push 出去(导航消失) */ + const me = N.NAV_ITEMS[3]; + assert.equal(me.key, 'me', '第 4 项应是「我的」'); + assert.equal(me.route, undefined, + '★ 「我的」不许再带 route —— 带 route 就是"点了导航、导航条整个消失"那个形状') // 内容挂在浮动条**下面**(先内容后条),否则条会被内容盖住、点不到 const navAt = stack.indexOf('this.NavBar()'); const contentAt = stack.indexOf('CommPage({ bgActive: this.bgActive })'); @@ -304,8 +334,10 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且 assert.equal(N.normalizeNavIndex(99), 0); assert.equal(N.normalizeNavIndex(1), 1); assert.equal(N.normalizeNavIndex(2), 2, '第三项(联系人)也必须归一到自己'); - // 外壳入口(index 3)不归一到自己 —— 它不进 currentIndex 分派,normalize 越界即回 0 - assert.equal(N.normalizeNavIndex(3), 0, '外壳入口(我的)不该被当作内容窗格下标'); + /* 第 4 项「我的」是内容窗格 ⇒ index 3 要归一到自己(不再是"越界回 0") */ + assert.equal(N.normalizeNavIndex(3), 3, + '「我的」是内容窗格(2026-09-17),index 3 要归一到自己 —— 否则点它会被静默踢回收件箱'); + assert.equal(N.normalizeNavIndex(4), 0, '越界(4)才回 0'); assert.throws(() => N.navKeyAt(1.5) === undefined, undefined, '非整数下标不该悄悄生效'); }); @@ -565,6 +597,59 @@ test('★ 滚动列表上下边缘渐隐:所有列表容器都挂 fadingEdge 'MainPage 要从 @kit.ArkUI import LengthMetrics'); }); +/* + * ★ 渐隐必须覆盖**所有**滚动容器,不只是 MainPage 那一批。 + * + * 用户(2026-09-17):「你的顶栏为什么还是硬截断而不是渐变?」 + * + * 根因正是上一条判据的**扫描范围太窄**:它只看 `pages/MainPage.ets`, + * 于是 `MailDetailPage` / `CalendarPage` / `SettingsPage` / `AdminUsersPage` / + * `SessionsPage` / `InboxPage` 里的滚动容器**一个都没被覆盖** —— + * 那些页面滚起来就是硬切,而判据全绿。 + * 这跟本仓库反复记的"窗口式/范围式判据"是同一个病: + * 判据自己划的圈,正好把出问题的那块划在外面。 + * + * 现在改成**按文件枚举 + 计数配平**:每个页面里 `Scroll()` / `List(` / `Scroll(` + * 的个数,必须等于该文件里 `.fadingEdge(` 的个数。少一个就红, + * 而且报错会指名是哪个文件少了几处。 + */ +test('★ 上下渐隐覆盖**所有**页面的滚动容器(不是只有 MainPage)', () => { + const pages = readdirSync(join(HARMONY_ETS, 'pages')).filter(f => f.endsWith('.ets')); + assert.ok(pages.length >= 8, `pages/ 下应有 ≥8 个页面,实际 ${pages.length}(扫描范围别退化)`); + + /* + * 例外:`LoginPage` 的 `Scroll` 是**整页根节点**(表单内容竖直排布), + * 不是"一列卡片"—— 它的上下就是屏幕边,没有任何内容从它下面穿过, + * 所以渐隐在这里只会把品牌卡与首个输入框洗白,没有任何遮挡要交代。 + * 这与 WebUI 只给 `.overflow-y-auto`(列表容器)加 mask、不给页面根加是同一条判断。 + * **例外必须写明理由**(本仓库纪律),且只有这一个。 + */ + const EXEMPT = new Set(['LoginPage.ets']); + + const offenders = []; + let totalContainers = 0; + for (const f of pages) { + if (EXEMPT.has(f)) continue; + const src = code(join(HARMONY_ETS, 'pages', f)); + const containers = (src.match(/\b(?:Scroll\(\)|(?:Scroll|List)\(\{)/g) ?? []).length; + const fades = (src.match(/\.fadingEdge\(/g) ?? []).length; + totalContainers += containers; + if (fades < containers) { + offenders.push(`${f}(容器 ${containers} / 渐隐 ${fades})`); + } + } + /* + * 自检:扫到的容器总数要合理。写死一个下限是为了防"正则写坏 ⇒ 一个都匹配不到 ⇒ + * 每个文件都是 0/0 ⇒ 判据全绿"。当前仓库是 13 个。 + */ + assert.ok(totalContainers >= 10, + `全仓滚动容器应 ≥10 个,实际扫到 ${totalContainers} —— 正则或目录范围退化了`); + assert.deepEqual(offenders, [], + `这些页面的滚动容器缺 fadingEdge:${offenders.join('、')}\n` + + '现象就是"顶栏/底边硬截断而不是渐变"(用户 2026-09-17 原话)。' + + '每个 `Scroll()` / `List(...)` 都要挂 `.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })`。'); +}); + /* * ★ 悬浮加号必须待在 Navigation 的**列表侧**,不能当 Navigation 的兄弟。 * diff --git a/client/harmony/entry/src/main/ets/api/MailApi.ets b/client/harmony/entry/src/main/ets/api/MailApi.ets index a53360d..7fcad08 100644 --- a/client/harmony/entry/src/main/ets/api/MailApi.ets +++ b/client/harmony/entry/src/main/ets/api/MailApi.ets @@ -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('/contacts'); } + /** + * 一条会话下的全部邮件(`GET /sessions/{id}/mails`)。 + * + * 联系人页点一条 "跟谁在聊" 就是打开这条会话的邮件列表 —— + * 与 WebUI `ContactPanel` 的 `open(c)` → `selectSession(c.session_id)` 同一行为。 + * 返回裸数组(服务端 `{mails: [...]}`),这里只取数组。 + */ + async sessionMails(sessionId: string): Promise { + const resp: SessionMailsResponse = await this.client.get( + '/sessions/' + sessionId + '/mails' + ); + return resp.mails; + } + /** 邮件详情(API 返回裸对象) */ async mailDetail(mailId: string): Promise { return this.client.get('/mail/' + mailId); diff --git a/client/harmony/entry/src/main/ets/common/Theme.ets b/client/harmony/entry/src/main/ets/common/Theme.ets index 4982dc7..9cdd466 100644 --- a/client/harmony/entry/src/main/ets/common/Theme.ets +++ b/client/harmony/entry/src/main/ets/common/Theme.ets @@ -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); } diff --git a/client/harmony/entry/src/main/ets/entryability/EntryAbility.ets b/client/harmony/entry/src/main/ets/entryability/EntryAbility.ets index ea7ee64..90a4ffa 100644 --- a/client/harmony/entry/src/main/ets/entryability/EntryAbility.ets +++ b/client/harmony/entry/src/main/ets/entryability/EntryAbility.ets @@ -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)); diff --git a/client/harmony/entry/src/main/ets/model/NavItems.ts b/client/harmony/entry/src/main/ets/model/NavItems.ts index c1a1ab7..47f2fe2 100644 --- a/client/harmony/entry/src/main/ets/model/NavItems.ts +++ b/client/harmony/entry/src/main/ets/model/NavItems.ts @@ -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 图标) */ diff --git a/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets b/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets index f12bdbb..a16530c 100644 --- a/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets +++ b/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets @@ -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 }) } } diff --git a/client/harmony/entry/src/main/ets/pages/CalendarPage.ets b/client/harmony/entry/src/main/ets/pages/CalendarPage.ets index 42add40..c36035f 100644 --- a/client/harmony/entry/src/main/ets/pages/CalendarPage.ets +++ b/client/harmony/entry/src/main/ets/pages/CalendarPage.ets @@ -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 }) } } diff --git a/client/harmony/entry/src/main/ets/pages/InboxPage.ets b/client/harmony/entry/src/main/ets/pages/InboxPage.ets index 9a68c51..dda1994 100644 --- a/client/harmony/entry/src/main/ets/pages/InboxPage.ets +++ b/client/harmony/entry/src/main/ets/pages/InboxPage.ets @@ -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 }) } diff --git a/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets b/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets index a5354a3..cfb3ba9 100644 --- a/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets @@ -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` 同形同位置。 diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 0924665..301bb0e 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -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 { + 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` 控制显示。 diff --git a/client/harmony/entry/src/main/ets/pages/SessionsPage.ets b/client/harmony/entry/src/main/ets/pages/SessionsPage.ets index 80b406a..77e2d53 100644 --- a/client/harmony/entry/src/main/ets/pages/SessionsPage.ets +++ b/client/harmony/entry/src/main/ets/pages/SessionsPage.ets @@ -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 }) } diff --git a/client/harmony/entry/src/main/ets/pages/SettingsPage.ets b/client/harmony/entry/src/main/ets/pages/SettingsPage.ets index 6da420f..c2648c5 100644 --- a/client/harmony/entry/src/main/ets/pages/SettingsPage.ets +++ b/client/harmony/entry/src/main/ets/pages/SettingsPage.ets @@ -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` 的 `

` 同形:小字、灰、上间距) */ diff --git a/client/harmony/entry/src/main/resources/base/profile/main_pages.json b/client/harmony/entry/src/main/resources/base/profile/main_pages.json index 9f78adb..7a4a1a6 100644 --- a/client/harmony/entry/src/main/resources/base/profile/main_pages.json +++ b/client/harmony/entry/src/main/resources/base/profile/main_pages.json @@ -4,7 +4,6 @@ "pages/MainPage", "pages/MailDetailPage", "pages/ComposePage", - "pages/SettingsPage", "pages/SessionsPage", "pages/AdminUsersPage", "pages/Index"