/** * 「打开写信」的**跨组件一次性意图**。 * * 用户 2026-09-21:「接下来做一下 2in1 上的快捷键,比如快捷键打开发信页面」。 * * ## 为什么需要这个中间层 * * 快捷键必须绑在**根**(`MainPage` 的根 `Stack`)—— 官方 `keyboardShortcut` * 文档说「即使组件未获焦或是在所在页面未展示,只要已经挂载到**获焦窗口**的 * 组件树上就会响应」,而"窗口级生效"只有绑在窗口组件树的根上才成立。 * * 但 `openCompose()` 住在 `CommPage` 上(写信要 `CommPage` 自己的 `navPathStack`, * 宽屏 Split 时才能在右栏开 —— 见 `MainPage.openComposeWith()` 的注释)。 * 于是 `MainPage` 的快捷键够不着那个方法。 * * ## 做法:照抄本仓已经验证过的"两半"形状 * * `api/PushService.ets` 处理"点通知要跳转"时踩过同一个坑,解法写在它的注释里: * · **第一半**:一个静态格子(`pending`)—— 冷启/尚未挂载时,请求先存着; * · **第二半**:一个回调(`listener`)—— 已经挂载时,请求当场送达。 * * 缺任何一半都会漏一种情形: * · 只有格子 ⇒ 用户此刻就在通信页、页面早挂载完了,`aboutToAppear` 不重跑 * ⇒ **按了快捷键什么都没发生**; * · 只有回调 ⇒ 用户此刻在日历页,`CommPage` 因 `if (currentIndex === 0)` * 尚未挂载 ⇒ 没有任何监听者 ⇒ **同样什么都没发生**。 * * ## 谁负责切到通信页 * * `request()` **不管** UI 导航(那是 `MainPage` 的事:它得先把 `currentIndex` * 拨到 0,`CommPage` 才会挂载)。这里只管"把意图存住/交出去"。 * 分开的好处:`MainPage` 里那两行是**唯一**需要读当前 tab 的地方, * 而本类保持无 UI 依赖 —— 可被条件挂载的两侧都安全调用。 */ export class ComposeIntent { /** * 还没人接手的一次请求。 * * 用静态格子而不是 `AppStorage`:这与 `PushService.pendingRoute` 同一理由 —— * 发起方(根组件)和接手方(条件挂载的 `CommPage`)之间只隔一次挂载, * 不需要一个全局可观察的状态;而用了 `@StorageLink` 反而要处理 * "读到之后怎么清"(不清会重放,清了会触发另一轮刷新)。 */ static pending: boolean = false; /** 已经在监听的那一处(同一时刻只可能有一处:`CommPage` 是唯一的写信持有者) */ private static listener: (() => void) | undefined = undefined; /** `CommPage` 挂载时登记 */ static setListener(fn: () => void): void { ComposeIntent.listener = fn; } /** `CommPage` 卸载时摘掉(否则会唤醒一个已销毁的组件) */ static clearListener(): void { ComposeIntent.listener = undefined; } /** * 提"我要写信"。 * * **有人监听就当场交出去;没人监听就存着等挂载后取走** —— 两半都留着, * 因为两种时刻各走一条(用户此刻在通信页 / 在别的页)。 */ static request(): void { const fn: (() => void) | undefined = ComposeIntent.listener; if (fn !== undefined) { fn(); return; } ComposeIntent.pending = true; } /** 取走待处理的请求(取完即清,避免下次挂载时重放) */ static consume(): boolean { if (!ComposeIntent.pending) { return false; } ComposeIntent.pending = false; return true; } } /** * 「打开回复」的跨组件一次性意图 —— 与 `ComposeIntent` **完全同形**。 * * ★★ 2026-09-24 新增。用户:「邮件详情页回车打开回复」。 * * ── 为何需要它(同样是因为"够不着")── * 回车是在**主页根**上收到的(详情页自己的 `onKeyEvent` 因组件不获焦而从不触发, * 实测硬证 —— 详见 `MainPage` 根上那段注释), * 而"开回复"那个动作(`openReplyWithMorph`)住在 `MailDetailView` 里。 * 根组件拿不到它的实例 ⇒ 与 `openCompose()` 当初撞到的是**同一个问题**, * 所以用**同一个解法**:一个格子 + 一个回调,两半都留着。 * * ── 为什么不直接在根上 setState 传下去 ── * `MailDetailView` 是被 `NavDestination` 挂进去的、参数只传 `mail_id/account_id` * 那几个 —— 为这个再加一个 `@Prop showReply` 要改三层(壳→页→回调), * 而且"根改一个值"与"详情正在看哪一封"会分叉(换一封时该清的没清)。 * 意图格子天然是"一次性动作"语义,正好。 * * ★ 与 `ComposeIntent` 分开两个类而不是加个 mode 参数: * 两类意图的**接收方完全不同**(一个在 `CommPage`、一个在 `MailDetailView`), * 合成一个会让"谁该消费它"变得含糊 —— 而含糊正是漏接收的来源。 */ export class ReplyIntent { static pending: boolean = false; private static listener: (() => void) | undefined = undefined; /** `MailDetailView` 挂载时登记 */ static setListener(fn: () => void): void { ReplyIntent.listener = fn; } /** 卸载时摘掉(否则会唤醒一个已销毁的组件) */ static clearListener(): void { ReplyIntent.listener = undefined; } /** 提"我要回复":有人听就当场给,没人听就存着等挂载后取走 */ static request(): void { const fn: (() => void) | undefined = ReplyIntent.listener; if (fn !== undefined) { fn(); return; } ReplyIntent.pending = true; } /** 取走待处理的请求(取完即清,避免下次挂载时重放) */ static consume(): boolean { if (!ReplyIntent.pending) { return false; } ReplyIntent.pending = false; return true; } } /** * 「弹掉通信页导航栈的最上一层」的意图(Esc 用)—— 与另两个同形。 * * ★★ 2026-09-24 新增(用户:「esc返回上一级」)。 * * Esc 在**主页根**上收到(详情/写信自己的 onKeyEvent 因组件不获焦而不触发), * 而 `navPathStack` 只有 `CommPage` 持有 ⇒ 同样的"够不着"问题、同样的解法。 */ export class PopIntent { static pending: boolean = false; private static listener: (() => void) | undefined = undefined; /* * ★ 2026-09-26 曾把回调从 `() => void` 改成 `() => boolean`(为了让 * `UIAbility.onBackPressed` 知道"到底退没退")—— **已随那次一起撤回**, * 原因见 `EntryAbility` 里 `onBackPress` 那段注释(模拟器上按一次系统 * 返回键直接把 UIAbility 销毁了,`harmony-admin` 判据因此挂住)。 * ⇒ 形状回到 `() => void`。等系统返回键那条**真机验过**之后再说。 */ static setListener(fn: () => void): void { PopIntent.listener = fn; } static clearListener(): void { PopIntent.listener = undefined; } static request(): void { const fn: (() => void) | undefined = PopIntent.listener; if (fn !== undefined) { fn(); return; } PopIntent.pending = true; } static consume(): boolean { if (!PopIntent.pending) { return false; } PopIntent.pending = false; return true; } }