@qiqb/message-domain 0.0.0-stage → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -2
- package/build/components/DefaultMessageReminderRenderer.d.ts +10 -0
- package/build/components/DefaultMessageReminderRenderer.js +29 -0
- package/build/components/MessageNavigation.d.ts +7 -0
- package/build/components/MessageNavigation.js +192 -0
- package/build/components/MessageReminderDisplay.d.ts +21 -0
- package/build/components/MessageReminderDisplay.js +353 -0
- package/build/components/MessageWorkspace.d.ts +14 -0
- package/build/components/MessageWorkspace.js +26 -0
- package/build/components/message-navigation.less +89 -0
- package/build/components/message-reminder-display.less +367 -0
- package/build/components/message-workspace.less +150 -0
- package/build/index.d.ts +20 -0
- package/build/index.js +17 -0
- package/build/pages/inbox/InboxPage.d.ts +2 -0
- package/build/pages/inbox/InboxPage.js +257 -0
- package/build/pages/inbox/inbox.less +293 -0
- package/build/pages/subscription/PersonalSubscriptionPage.d.ts +1 -0
- package/build/pages/subscription/PersonalSubscriptionPage.js +6 -0
- package/build/pages/subscription/RoleSubscriptionPage.d.ts +1 -0
- package/build/pages/subscription/RoleSubscriptionPage.js +5 -0
- package/build/pages/subscription/SubscriptionForm.d.ts +16 -0
- package/build/pages/subscription/SubscriptionForm.js +111 -0
- package/build/pages/subscription/SubscriptionPage.d.ts +5 -0
- package/build/pages/subscription/SubscriptionPage.js +273 -0
- package/build/pages/subscription/subscription-form.less +341 -0
- package/build/pages/subscription/subscription.less +250 -0
- package/build/pages/topic/TopicPage.d.ts +1 -0
- package/build/pages/topic/TopicPage.js +275 -0
- package/build/routes.cjs +38 -0
- package/build/routes.d.cts +22 -0
- package/build/routes.d.ts +22 -0
- package/build/routes.js +34 -0
- package/build/runtime/MessageReminderLayout.d.ts +14 -0
- package/build/runtime/MessageReminderLayout.js +25 -0
- package/build/runtime/MessageReminders.d.ts +16 -0
- package/build/runtime/MessageReminders.js +364 -0
- package/build/runtime/index.d.ts +77 -0
- package/build/runtime/index.js +103 -0
- package/build/runtime/message-reminder-layout.less +40 -0
- package/build/runtime/messageRealtime.d.ts +15 -0
- package/build/runtime/messageRealtime.js +99 -0
- package/build/runtime/reminderEvents.d.ts +18 -0
- package/build/runtime/reminderEvents.js +53 -0
- package/build/runtime/reminderOwnership.d.ts +35 -0
- package/build/runtime/reminderOwnership.js +135 -0
- package/build/runtime/reminderPresentation.d.ts +34 -0
- package/build/runtime/reminderPresentation.js +57 -0
- package/build/runtime/reminderQuery.d.ts +18 -0
- package/build/runtime/reminderQuery.js +33 -0
- package/build/runtime/reminderQueue.d.ts +25 -0
- package/build/runtime/reminderQueue.js +88 -0
- package/build/services/MessageController.d.ts +108 -0
- package/build/services/MessageController.js +42 -0
- package/build/types/index.d.ts +12 -0
- package/build/types/index.js +1 -0
- package/package.json +95 -4
package/README.md
CHANGED
|
@@ -1,3 +1,214 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @qiqb/message-domain
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
可发布的 QIQB 通用消息前端领域包,包含消息主题、个人订阅、角色订阅、个人收件箱,以及浮动通知、内嵌轮播、全屏阅读确认、配置预览和账号隔离队列。消息级别与提醒方式独立配置,主题修改只影响后续新消息。
|
|
4
|
+
|
|
5
|
+
包输出 ESM JavaScript、TypeScript 声明和 LESS;不依赖 Umi/Max、宿主 `.umi`、`@prokeeper-bip/shared` 或平台管理页面包。React、React DOM、Ant Design、Pro Components 和图标是 peerDependencies,由消费宿主安装和提供主题。宿主需支持导入包内 LESS,页面和组件已自行引用对应样式。
|
|
6
|
+
|
|
7
|
+
## 运行时装配
|
|
8
|
+
|
|
9
|
+
宿主启动时、消息页面和组件挂载前调用一次配置。注入函数中可以使用宿主的 React hook;不要在挂载后更换 hook 实现。
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { configureMessageDomainRuntime } from '@qiqb/message-domain/runtime';
|
|
13
|
+
|
|
14
|
+
configureMessageDomainRuntime({
|
|
15
|
+
request, // 泛型 request<T>(url, { method, params, data, skipErrorHandler }),返回 WebResp 本体
|
|
16
|
+
navigate: (path) => history.push(path), // 宿主负责 basename/微前端导航
|
|
17
|
+
useLocation, // 返回 { search: string },支持 receiptId 查询参数打开详情
|
|
18
|
+
useAccess, // 返回 { verifyElementPermission(code, projectCode) }
|
|
19
|
+
projectCode: 'qiqb_platform',
|
|
20
|
+
apiPrefix: '/api/qiqb_platform/message',
|
|
21
|
+
inboxPath: '/p/message/inbox',
|
|
22
|
+
receiptQueryParam: 'receiptId',
|
|
23
|
+
storageNamespace: 'qiqb-platform',
|
|
24
|
+
// eventNamespace 不传时跟随 storageNamespace
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`request` 由宿主继续处理 token、超时及鉴权。默认 fetch 只提供同源 JSON 请求,适合无框架预览;正式接入应注入现有请求层。页面使用 `WebResp<T> { success, message?, data? }` 与 `PageInfo<T> { total, records?, current?, size? }`。消息领域自行提示业务失败,传入 `skipErrorHandler: true`,避免宿主重复弹出相同错误。
|
|
29
|
+
|
|
30
|
+
### 可选实时订阅
|
|
31
|
+
|
|
32
|
+
消息包不依赖 Socket.IO 客户端。宿主安装并配置独立 `@qiqb/socketio-domain` 后,将同账户共享连接的
|
|
33
|
+
租约注入 `realtime`;宿主和其它业务模块共用连接,不在每个微应用重新创建 Manager。
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { acquireSocketIoConnection } from '@qiqb/socketio-domain';
|
|
37
|
+
import { configureMessageDomainRuntime } from '@qiqb/message-domain/runtime';
|
|
38
|
+
import type { MessageInboxChange } from '@qiqb/message-domain/runtime';
|
|
39
|
+
|
|
40
|
+
configureMessageDomainRuntime({
|
|
41
|
+
realtime: {
|
|
42
|
+
acquire() {
|
|
43
|
+
const lease = acquireSocketIoConnection();
|
|
44
|
+
return {
|
|
45
|
+
getState: () => lease.getState(),
|
|
46
|
+
onStateChange: (listener, immediate) => lease.onStateChange(listener, immediate),
|
|
47
|
+
onChanged: (listener) => lease.on('message.inbox.changed', (payload) => listener(payload as MessageInboxChange)),
|
|
48
|
+
release: () => lease.release(),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
正式提醒仍由同 `apiPrefix + accountId` 的唯一 owner 查询和展示。连接正常时变化事件立即触发
|
|
56
|
+
HTTP 刷新,同时每 60 秒完整对账;断线或未注入实时能力时沿用 5 秒查询。首次连接、重连与在线
|
|
57
|
+
对账完整扫描未读分页,覆盖超过 300 条的历史收件;只有完整查询成功后才移除服务器投影中缺失的
|
|
58
|
+
队列记录,分页失败保留当前展示。首次已读事件可立即移除对应记录,普通数据接收或传输 ACK 不代表
|
|
59
|
+
业务已读。后台页面暂停查询,恢复可见立即补查;卸载、退出或换账号释放当前订阅租约,不断开其他
|
|
60
|
+
业务模块共享的连接。
|
|
61
|
+
|
|
62
|
+
默认权限项目为 `qiqb_platform`,按钮授权显式传递该项目编码,不随当前导航项目漂移。缺少 `useAccess` 时主题及角色管理操作默认不可用;角色订阅的列表查询及写操作统一要求 `MessageRoleSubscriptionManageBtn`。我的订阅由服务端绑定当前账号,只提供查询、筛选和启停;新增、编辑、删除由主题策略与角色配置维护。页面入口和后端接口权限继续由宿主及服务端校验,按钮隐藏不能替代服务端鉴权。
|
|
63
|
+
|
|
64
|
+
主题使用 `subscriptionMode` 指定订阅策略,默认 `ALL_ACCOUNTS`(全量账户订阅),创建时为有效账户绑定订阅;`ROLE`(角色订阅)主题通过角色配置,在账户绑定角色时生成我的订阅。模式选项由 `/topic/subscription_modes` 动态返回,支持服务端扩展。主题的“同步订阅”调用 `/topic/sync_subscriptions` 补齐账户订阅,复用 `MessageTopicSaveBtn` 权限并显示新增数量。页面不配置可过滤字段或订阅条件,角色订阅保存时传递空条件列表,默认使用站内消息渠道。
|
|
65
|
+
|
|
66
|
+
## 路由
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import {
|
|
70
|
+
messageRoutes,
|
|
71
|
+
createMessageRoutes,
|
|
72
|
+
} from '@qiqb/message-domain/routes';
|
|
73
|
+
// 平台宿主直接装配 messageRoutes;四条 route 均显式 projectCode=qiqb_platform。
|
|
74
|
+
// BIP 保留自己的菜单/页面授权上下文,组件来源仍是同一个可发布包。
|
|
75
|
+
const bipRoutes = createMessageRoutes({ projectCode: 'prokeeper_bip' });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`@qiqb/message-domain/routes` 提供独立的 `import`/`require` 条件入口:浏览器/ESM 使用 `routes.js`,Umi `.umirc.ts` 等 CommonJS 配置使用 `routes.cjs`,类型分别使用 `.d.ts`/`.d.cts`。因此配置可直接 `require('@qiqb/message-domain/routes')`,无需让宿主支持加载整份 ESM 界面包。页面、服务与运行时仍为 ESM。
|
|
79
|
+
|
|
80
|
+
消息是可选领域,默认路径为 `/p/message/topics`、`/p/message/my-subscriptions`、`/p/message/role-subscriptions`、`/p/message/inbox`。`createMessageRoutes({ projectCode, basePath })` 可改变授权上下文或路由基座;路由工厂是纯配置,无框架运行时依赖。宿主据 `component` 字符串或直接导入页面组件完成路由解析,并依据 `permissionCode + projectCode` 授权。
|
|
81
|
+
|
|
82
|
+
平台宿主只装配 `/p/message/*`、`/p/audit/*`,默认路由调整后不注册历史路径别名或重定向入口。builtin-data 仍只补缺失,不覆盖已有菜单路由;历史消息菜单需通过现有菜单管理更新为对应的 `/p/message/*` 路径。
|
|
83
|
+
|
|
84
|
+
## 全局提醒与页面槽位
|
|
85
|
+
|
|
86
|
+
布局中的铃铛导航可以直接使用领域组件,由宿主决定是否显示:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { MessageNavigation } from '@qiqb/message-domain';
|
|
90
|
+
|
|
91
|
+
{showMessageNavigation && <MessageNavigation accountId={currentAccountId} />}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
导航角标读取当前账户的真实未读数量,展开后默认展示未读消息,支持分页、刷新和失败重试。数量、列表或正式提醒查询失败时,铃铛以警告色和断开标识显示连接状态,未读数仍保留;展开面板统一展示一条连接提示与重试入口,重试同时刷新导航并唤醒当前正式提醒 owner。连接状态按消息 API 与账户共享,不参与 owner 选举,也不受预览 namespace 影响。点击条目使用 runtime 的 `openMessageInbox` 打开具体详情,详情成功加载后由收件箱页面确认阅读;展开列表不会标已读。“查看全部消息”跳转 runtime 的 `inboxPath`。无登录账户时不显示也不查询,账户/API/事件上下文变化会清空旧状态并丢弃迟到响应。
|
|
95
|
+
|
|
96
|
+
导航只查询未读数量,接入实时能力时由变化事件唤醒并低频对账,后台页面暂停;阅读成功事件按 runtime namespace 同步数量和列表。列表按展开、分页、刷新与未读变化查询当前账户的有效未读投影,与角标一样排除过期消息;不轮询正式提醒列表、不参与提醒 owner 选举,也不触发自动提醒展示。隐藏铃铛不会改变宿主独立挂载的 `MessageReminders` 行为。
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import {
|
|
100
|
+
MessageReminderProvider,
|
|
101
|
+
MessageReminderLayout,
|
|
102
|
+
} from '@qiqb/message-domain/runtime/MessageReminderLayout';
|
|
103
|
+
import MessageReminders from '@qiqb/message-domain/runtime/MessageReminders';
|
|
104
|
+
|
|
105
|
+
// Provider 覆盖页面内容与全局提醒;Layout 放到宿主内容流而非页面表格内部。
|
|
106
|
+
<MessageReminderProvider>
|
|
107
|
+
<MessageReminderLayout>{children}</MessageReminderLayout>
|
|
108
|
+
<MessageReminders accountId={currentAccountId} priority={100} />
|
|
109
|
+
</MessageReminderProvider>;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
平台宿主使用 `priority={100}`,业务微应用默认 `0`。同一 `document.defaultView` 的 `Symbol.for` registry 按 `apiPrefix + accountId` 选举唯一正式提醒入口;namespace 不参与 owner key,平台宿主与 BIP 不会同时轮询和弹出同一份正式消息。最高优先级入口持有 owner,同级按登记顺序稳定选择;卸载、退出或换账号自动释放并转移所有权。失去 owner 后旧请求不会回写正式队列,也不会导航。当前文档交接仅共享已展示/关闭的收件标识,不共享消息正文、账号凭证或业务属性。
|
|
113
|
+
|
|
114
|
+
非 owner 仍可显示自己的配置预览。预览独立于正式队列、浏览器存储和阅读接口。轮播预览使用独立 body 槽位显示在编辑弹窗之上;正式轮播只进入页面内槽位。没有槽位或槽位不可见时,消息保留排队,不提前计入已经展示。
|
|
115
|
+
|
|
116
|
+
轮播多条每 5 秒切换,悬停、焦点在栏内、后台标签页、全屏遮罩以及栏位不可见时暂停。浮动合并为右下角的一张 Ant Design Card,使用前后按钮切换消息,仅当前可见项计入已展示;轮播使用 Alert 占满二级菜单与页面共同上方的内容区宽度,不悬浮。全屏使用 Ant Design Modal 按级别优先,必须明确确认阅读或查看,遮罩/Escape 不关闭。`onOpen` 可在 runtime 或 `MessageReminders` prop 覆盖(prop 优先);默认跳转 `inboxPath?receiptId=...`。查看与“我已知晓”均先确认服务端阅读成功;展示、预览和关闭浮动/轮播本身不会标已读。
|
|
117
|
+
|
|
118
|
+
账号切换重建隔离队列,旧请求和确认不会导航到新账号页面。普通浮动/轮播真正展示后写入本会话去重;未显示的排队消息不提前记住,全屏未确认时刷新仍可继续提醒。sessionStorage 仅保存收件 ID,禁用或损坏时回退内存。相同 owner key 的已读事实通过登记回调在平台/BIP 之间传递,按后端与账号隔离。
|
|
119
|
+
|
|
120
|
+
## 项目自定义提醒外观
|
|
121
|
+
|
|
122
|
+
未配置 renderer 时使用平台的 `DefaultMessageReminderRenderer`,保留三种模式、内容区轮播槽位与阅读失败反馈;连接状态与重试由 `MessageNavigation` 的铃铛和展开面板展示,不再显示左下角连接提示。宿主需要默认连接反馈时应同时挂载导航。项目可以替换完整展示,也可以仅接管某种 `reminderMode`,将其余消息和回调继续交给默认组件。自定义 renderer 仍可使用 `connectionFailed` 与 `onRetry` 契约。请求、轮询、队列、去重、账号隔离、所有者选举和阅读确认仍由 `MessageReminders` 维护。
|
|
123
|
+
|
|
124
|
+
`configureMessageDomainRuntime({ reminderRenderer: ProjectMessageReminderRenderer })` 配置当前包运行时的默认外观;`<MessageReminders renderer={ProjectMessageReminderRenderer} />` 覆盖该实例,优先级为实例 `renderer`、runtime `reminderRenderer`、平台默认组件。主题预览与该入口的正式提醒使用同一 renderer。
|
|
125
|
+
|
|
126
|
+
下面示例完整保留平台行为,可在包装组件内继续按项目需求调整外观。运行时配置合并到宿主现有启动配置;示例中的业务入口显式使用 `priority={200}` 接管正式提醒,并放在已有 `MessageReminderProvider` 下挂载。
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import {
|
|
130
|
+
configureMessageDomainRuntime,
|
|
131
|
+
DefaultMessageReminderRenderer,
|
|
132
|
+
MessageReminders,
|
|
133
|
+
type MessageReminderRendererProps,
|
|
134
|
+
} from '@qiqb/message-domain';
|
|
135
|
+
|
|
136
|
+
export function ProjectMessageReminderRenderer(
|
|
137
|
+
props: MessageReminderRendererProps,
|
|
138
|
+
) {
|
|
139
|
+
// 包装默认组件时完整透传消息、槽位与回调,继续使用平台的阅读和去重规则。
|
|
140
|
+
return <DefaultMessageReminderRenderer {...props} />;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
configureMessageDomainRuntime({
|
|
144
|
+
reminderRenderer: ProjectMessageReminderRenderer,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export function ProjectMessageReminders({
|
|
148
|
+
accountId,
|
|
149
|
+
}: {
|
|
150
|
+
accountId?: string;
|
|
151
|
+
}) {
|
|
152
|
+
return (
|
|
153
|
+
<MessageReminders
|
|
154
|
+
accountId={accountId}
|
|
155
|
+
priority={200}
|
|
156
|
+
renderer={ProjectMessageReminderRenderer}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
外观配置不改变所有者优先级。平台宿主为 `100`,业务微应用默认 `0`;两者同时挂载且 API/账户相同时,微应用配置的自定义 renderer 只用于自己的预览,正式提醒继续由平台宿主展示。若业务需接管正式展示,可如上显式传 `priority > 100`(例如 `200`),卸载后恢复平台宿主;也可直接在持有 owner 的平台宿主上配置项目 renderer。独立运行且没有更高优先级入口时,默认 `0` 仍可取得正式展示权。
|
|
163
|
+
|
|
164
|
+
展示契约 `MessageReminderRendererProps` 从包根导出,字段如下:
|
|
165
|
+
|
|
166
|
+
| 字段 | 含义 |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| `messages: InboxMessage[]` | 当前入口允许展示的消息与独立预览;不要自行拉取或修改收件状态 |
|
|
169
|
+
| `carouselTarget?: HTMLElement` | 当前轮播槽位;正式消息进入内容区,预览使用独立槽位 |
|
|
170
|
+
| `accountId?: string` | 当前登录账户 |
|
|
171
|
+
| `onDismiss(id: string): void` | 关闭浮动、轮播或预览;正式全屏不允许直接关闭 |
|
|
172
|
+
| `onOpen(record: InboxMessage): void` | 正式消息先确认服务端阅读成功,再调用宿主导航;预览只结束展示 |
|
|
173
|
+
| `onConfirm(record: InboxMessage): Promise<void>` | 明确确认阅读;失败时保留消息并返回拒绝的 Promise |
|
|
174
|
+
| `confirmingIds?: string[]`、`confirmError?: string` | 正在确认的收件 ID 与失败提示;展示层应保留加载和失败反馈 |
|
|
175
|
+
| `onDisplayed(record: InboxMessage): void` | 仅在这条消息实际可见时报告,排队、隐藏或仅传入组件不算已展示 |
|
|
176
|
+
| `connectionFailed: boolean`、`onRetry(): void` | 连接失败状态与立即重试入口 |
|
|
177
|
+
| `onClearError(): void` | 清除失败提示,不关闭消息,也不确认阅读 |
|
|
178
|
+
|
|
179
|
+
自定义组件应使用这些回调完成操作。正式 `FULLSCREEN` 只能在 `onConfirm` 或 `onOpen` 的阅读请求成功后由核心移除,不能隐藏失败消息或将关闭按钮接到 `onDismiss`。轻量消息被关闭仍保留未读记录。回调校验当前账户、消息源、namespace、队列和 owner lease;旧页面、旧账户或失去所有权后的回调无法操作新队列。项目按模式拆分展示时,应将各条消息只交给一个展示组件,并在对应组件真正可见后报告 `onDisplayed`。
|
|
180
|
+
|
|
181
|
+
## BIP 兼容配置
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
configureMessageDomainRuntime({
|
|
185
|
+
request,
|
|
186
|
+
navigate: (path) => history.push(path),
|
|
187
|
+
useLocation,
|
|
188
|
+
useAccess,
|
|
189
|
+
projectCode: 'prokeeper_bip',
|
|
190
|
+
apiPrefix: '/api/qiqb_platform/message', // 消息服务迁入平台;如过渡服务另有地址可显式覆盖
|
|
191
|
+
storageNamespace: 'prokeeper-bip',
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
此配置继续使用旧键 `prokeeper-bip:reminders:<accountId>` 和旧事件命名空间;若需要不同事件隔离,可另传 `eventNamespace`。平台样式统一使用 `qiqb-message-*`/`qiqb-subscription-*`/`qiqb-inbox-*`,不再依赖 BIP CSS。原 `pages/...`、`services/MessageController`、`components/MessageReminderDisplay`、`runtime/MessageReminders`、`runtime/MessageReminderLayout` 子路径保留;替换包名前缀即可迁移大部分既有 import。
|
|
196
|
+
|
|
197
|
+
纯展示入口 `MessageReminderDisplay` 支持 `messages/onDismiss/onOpen/onConfirm/confirmingIds/confirmError/onDisplayed/carouselTarget`,自身不执行接口。独立 `ReminderQueue` 与 owner 注册函数也可按需复用。预览/阅读事件应使用包导出的派发和订阅函数,避免硬编码默认事件名而失去 namespace 隔离。
|
|
198
|
+
|
|
199
|
+
## 构建与本地打包
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
cd frontend
|
|
203
|
+
corepack pnpm --filter @qiqb/message-domain lint
|
|
204
|
+
corepack pnpm --filter @qiqb/message-domain test:reminders
|
|
205
|
+
corepack pnpm --filter @qiqb/message-domain test:runtime
|
|
206
|
+
corepack pnpm --filter @qiqb/message-domain test:presentation
|
|
207
|
+
corepack pnpm build:message-domain
|
|
208
|
+
corepack pnpm --filter @qiqb/message-domain test:routes
|
|
209
|
+
corepack pnpm pack:message-domain
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
打包到 `/private/tmp/qiqb-message-domain-0.1.3.tgz`,可通过 `pnpm add /private/tmp/qiqb-message-domain-0.1.3.tgz` 在消费仓库安装验证。构建仅清理本包 `build/`,输出 ESM 的相对 `.js` 导入、界面 `.d.ts`、独立路由 `.cjs`/`.d.cts` 和按源路径复制的 `.less`。包内容只包含 `build/`、README 和 package.json,不包含源代码、测试、依赖缓存或宿主生成文件。
|
|
213
|
+
|
|
214
|
+
`publish:message-domain` 与包内 `publish:official` 默认发布到 npm 官方仓库,使用公开 scoped 包;`publish:private/publish:local/publish:nexus` 保留为显式私服入口。它们不会在 build/pack 时自动执行;只有明确要发布时再运行。验证和打包不启动宿主应用、不部署后端、不远程发布。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MessageReminderRenderer, MessageReminderRendererProps } from '../runtime/reminderPresentation.js';
|
|
2
|
+
/** 平台默认外观;不接入项目 renderer 时仍使用既有全屏、轮播和浮动提醒。 */
|
|
3
|
+
export default function DefaultMessageReminderRenderer({ accountId, connectionFailed, onRetry, onClearError, ...displayProps }: MessageReminderRendererProps): import("react").JSX.Element;
|
|
4
|
+
/**
|
|
5
|
+
* 实例配置优先于宿主运行时配置;未配置时自动使用平台默认组件。
|
|
6
|
+
* 两种配置都经由同一组件边界,正式消息和主题预览会使用同一套项目展示实现。
|
|
7
|
+
*/
|
|
8
|
+
export declare function MessageReminderPresentation({ renderer, ...props }: MessageReminderRendererProps & {
|
|
9
|
+
renderer?: MessageReminderRenderer;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, theme } from 'antd';
|
|
3
|
+
import { getMessageDomainRuntime } from '../runtime/index.js';
|
|
4
|
+
import MessageReminderDisplay from './MessageReminderDisplay.js';
|
|
5
|
+
/** 平台默认外观;不接入项目 renderer 时仍使用既有全屏、轮播和浮动提醒。 */
|
|
6
|
+
export default function DefaultMessageReminderRenderer({ accountId, connectionFailed, onRetry, onClearError, ...displayProps }) {
|
|
7
|
+
const { token } = theme.useToken();
|
|
8
|
+
const { confirmError, messages } = displayProps;
|
|
9
|
+
// 连接断开与重试由铃铛导航展示;明确阅读失败仍在提醒旁反馈,不能被连接状态吞掉。
|
|
10
|
+
const showStatus = confirmError &&
|
|
11
|
+
!messages.some((record) => record.reminderMode === 'FULLSCREEN');
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsx(MessageReminderDisplay, { ...displayProps }), showStatus && (_jsx("div", { style: {
|
|
13
|
+
position: 'fixed',
|
|
14
|
+
left: 16,
|
|
15
|
+
bottom: 16,
|
|
16
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
17
|
+
zIndex: token.zIndexPopupBase + 100,
|
|
18
|
+
}, children: _jsx(Alert, { type: "error", showIcon: true, message: confirmError, closable: true, onClose: onClearError }) }))] }));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 实例配置优先于宿主运行时配置;未配置时自动使用平台默认组件。
|
|
22
|
+
* 两种配置都经由同一组件边界,正式消息和主题预览会使用同一套项目展示实现。
|
|
23
|
+
*/
|
|
24
|
+
export function MessageReminderPresentation({ renderer, ...props }) {
|
|
25
|
+
const Renderer = renderer ||
|
|
26
|
+
getMessageDomainRuntime().reminderRenderer ||
|
|
27
|
+
DefaultMessageReminderRenderer;
|
|
28
|
+
return _jsx(Renderer, { ...props });
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import './message-navigation.less';
|
|
2
|
+
export interface MessageNavigationProps {
|
|
3
|
+
/** 仅使用宿主当前登录账户;退出登录时不继续查询上一账户的消息。 */
|
|
4
|
+
accountId?: string;
|
|
5
|
+
}
|
|
6
|
+
/** 仅展示消息导航;可见性由宿主布局配置决定,正式提醒继续使用独立的 owner。 */
|
|
7
|
+
export default function MessageNavigation({ accountId }: MessageNavigationProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BellOutlined, DisconnectOutlined, ReloadOutlined, } from '@ant-design/icons';
|
|
3
|
+
import { Alert, Badge, Button, Empty, Pagination, Popover, Skeleton, theme, } from 'antd';
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { getMessageDomainRuntime, navigate, openMessageInbox, } from '../runtime/index.js';
|
|
6
|
+
import { subscribeMessageReceiptRead } from '../runtime/reminderEvents.js';
|
|
7
|
+
import { startMessageRealtimeRefresh } from '../runtime/messageRealtime.js';
|
|
8
|
+
import { retryMessageReminderConnection, subscribeMessageReminderConnection, } from '../runtime/reminderOwnership.js';
|
|
9
|
+
import { getMessageReminders, getUnreadMessageCount, } from '../services/MessageController.js';
|
|
10
|
+
import './message-navigation.less';
|
|
11
|
+
const pageSize = 5;
|
|
12
|
+
const receiptId = (record) => record.receiptId || record.id;
|
|
13
|
+
const errorText = (error, fallback) => error instanceof Error && error.message ? error.message : fallback;
|
|
14
|
+
/** 仅展示消息导航;可见性由宿主布局配置决定,正式提醒继续使用独立的 owner。 */
|
|
15
|
+
export default function MessageNavigation({ accountId }) {
|
|
16
|
+
const { apiPrefix, eventNamespace } = getMessageDomainRuntime();
|
|
17
|
+
if (!accountId)
|
|
18
|
+
return null;
|
|
19
|
+
// 账户、消息服务或事件上下文变化时重建状态,避免短暂展示上一账户的正文/角标。
|
|
20
|
+
return (_jsx(MessageNavigationContent, { accountId: accountId }, JSON.stringify([apiPrefix, accountId, eventNamespace])));
|
|
21
|
+
}
|
|
22
|
+
function MessageNavigationContent({ accountId }) {
|
|
23
|
+
const { apiPrefix, realtime } = getMessageDomainRuntime();
|
|
24
|
+
const { token } = theme.useToken();
|
|
25
|
+
const mounted = useRef(true);
|
|
26
|
+
const listVersion = useRef(0);
|
|
27
|
+
const countVersion = useRef(0);
|
|
28
|
+
const previousCount = useRef();
|
|
29
|
+
const openRef = useRef(false);
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
openRef.current = open;
|
|
32
|
+
const [current, setCurrent] = useState(1);
|
|
33
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
34
|
+
const [records, setRecords] = useState([]);
|
|
35
|
+
const [total, setTotal] = useState(0);
|
|
36
|
+
const [unreadCount, setUnreadCount] = useState();
|
|
37
|
+
const [listLoading, setListLoading] = useState(false);
|
|
38
|
+
const [countLoading, setCountLoading] = useState(false);
|
|
39
|
+
const [listError, setListError] = useState();
|
|
40
|
+
const [countError, setCountError] = useState();
|
|
41
|
+
const [reminderConnectionFailed, setReminderConnectionFailed] = useState(false);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
mounted.current = true;
|
|
44
|
+
return () => {
|
|
45
|
+
// 慢网请求无法取消时也必须失效,旧账户和已卸载入口不再回写数据。
|
|
46
|
+
mounted.current = false;
|
|
47
|
+
++listVersion.current;
|
|
48
|
+
++countVersion.current;
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
useEffect(() => subscribeMessageReminderConnection({ apiPrefix, accountId }, (failed) => {
|
|
52
|
+
if (mounted.current)
|
|
53
|
+
setReminderConnectionFailed(failed);
|
|
54
|
+
}), [apiPrefix, accountId]);
|
|
55
|
+
const refreshCount = useCallback(async () => {
|
|
56
|
+
if (!mounted.current)
|
|
57
|
+
return;
|
|
58
|
+
const version = ++countVersion.current;
|
|
59
|
+
setCountLoading(true);
|
|
60
|
+
try {
|
|
61
|
+
const { data } = await getUnreadMessageCount();
|
|
62
|
+
if (!mounted.current || version !== countVersion.current)
|
|
63
|
+
return;
|
|
64
|
+
const nextCount = data?.count || 0;
|
|
65
|
+
// 新增消息或其它页面阅读后,以服务端数量刷新当前未读页,不按本地数量猜测。
|
|
66
|
+
if (openRef.current &&
|
|
67
|
+
previousCount.current !== undefined &&
|
|
68
|
+
previousCount.current !== nextCount)
|
|
69
|
+
setRefreshKey((value) => value + 1);
|
|
70
|
+
previousCount.current = nextCount;
|
|
71
|
+
setUnreadCount(nextCount);
|
|
72
|
+
setCountError(undefined);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (mounted.current && version === countVersion.current)
|
|
76
|
+
setCountError(errorText(error, '加载未读数量失败'));
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (mounted.current && version === countVersion.current)
|
|
80
|
+
setCountLoading(false);
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
// 导航仍只查询数量,不参与正式提醒所有权;连接租约由宿主统一复用。
|
|
85
|
+
const loop = startMessageRealtimeRefresh({
|
|
86
|
+
realtime,
|
|
87
|
+
refresh: refreshCount,
|
|
88
|
+
onChanged: (change) => {
|
|
89
|
+
if (!mounted.current)
|
|
90
|
+
return;
|
|
91
|
+
if (change?.receiptId && change.readAt) {
|
|
92
|
+
++listVersion.current;
|
|
93
|
+
setRecords((items) => items.filter((record) => receiptId(record) !== change.receiptId));
|
|
94
|
+
}
|
|
95
|
+
if (openRef.current)
|
|
96
|
+
setRefreshKey((value) => value + 1);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return () => loop.release();
|
|
100
|
+
}, [refreshCount, realtime]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!open)
|
|
103
|
+
return;
|
|
104
|
+
const version = ++listVersion.current;
|
|
105
|
+
setListLoading(true);
|
|
106
|
+
// 复用当前账户有效未读投影,与未读数量一样排除已过期消息。
|
|
107
|
+
// 这里只按展开/分页等明确动作查询,不触发提醒展示、队列或自动阅读。
|
|
108
|
+
void getMessageReminders({ current, pageSize })
|
|
109
|
+
.then(({ data }) => {
|
|
110
|
+
if (!mounted.current || version !== listVersion.current)
|
|
111
|
+
return;
|
|
112
|
+
const resultTotal = data?.total || 0;
|
|
113
|
+
// 当前页最后一条被阅读后退回有效页,不把已经没有记录的页保留在导航中。
|
|
114
|
+
const lastPage = Math.max(1, Math.ceil(resultTotal / pageSize));
|
|
115
|
+
if (current > lastPage) {
|
|
116
|
+
setCurrent(lastPage);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
setRecords(data?.records || []);
|
|
120
|
+
setTotal(resultTotal);
|
|
121
|
+
// 重试期间保留断开标识,只有当前列表请求成功才清除它对应的失败。
|
|
122
|
+
setListError(undefined);
|
|
123
|
+
})
|
|
124
|
+
.catch((error) => {
|
|
125
|
+
if (mounted.current && version === listVersion.current)
|
|
126
|
+
setListError(errorText(error, '加载未读消息失败'));
|
|
127
|
+
})
|
|
128
|
+
.finally(() => {
|
|
129
|
+
if (mounted.current && version === listVersion.current)
|
|
130
|
+
setListLoading(false);
|
|
131
|
+
});
|
|
132
|
+
return () => {
|
|
133
|
+
++listVersion.current;
|
|
134
|
+
};
|
|
135
|
+
}, [open, current, refreshKey]);
|
|
136
|
+
useEffect(() => subscribeMessageReceiptRead(({ id }) => {
|
|
137
|
+
if (!mounted.current)
|
|
138
|
+
return;
|
|
139
|
+
// 只响应当前 runtime namespace 的成功阅读事实;展开导航不会发送已读操作。
|
|
140
|
+
// 先使在途列表失效,防止已读广播之后的旧响应又把该消息放回未读列表。
|
|
141
|
+
++listVersion.current;
|
|
142
|
+
setRecords((items) => items.filter((record) => receiptId(record) !== id));
|
|
143
|
+
setRefreshKey((value) => value + 1);
|
|
144
|
+
void refreshCount();
|
|
145
|
+
}), [refreshCount]);
|
|
146
|
+
const refresh = () => {
|
|
147
|
+
if (!mounted.current)
|
|
148
|
+
return;
|
|
149
|
+
setRefreshKey((value) => value + 1);
|
|
150
|
+
void refreshCount();
|
|
151
|
+
retryMessageReminderConnection({ apiPrefix, accountId });
|
|
152
|
+
};
|
|
153
|
+
// 数量、展开列表和正式提醒分别恢复;任一路仍失败时都保留断开提示。
|
|
154
|
+
const connectionFailed = !!countError || !!listError || reminderConnectionFailed;
|
|
155
|
+
const connectionError = countError ||
|
|
156
|
+
listError ||
|
|
157
|
+
(reminderConnectionFailed ? '消息提醒连接暂不可用' : undefined);
|
|
158
|
+
const navigationLabel = `我的消息${unreadCount === undefined ? '' : `,${unreadCount} 条未读`}${connectionFailed ? ',消息连接已断开' : ''}`;
|
|
159
|
+
const content = (_jsxs("section", { className: "qiqb-message-navigation", "aria-label": "\u672A\u8BFB\u6D88\u606F\u5217\u8868", style: {
|
|
160
|
+
'--message-nav-text': token.colorText,
|
|
161
|
+
'--message-nav-muted': token.colorTextSecondary,
|
|
162
|
+
'--message-nav-border': token.colorBorderSecondary,
|
|
163
|
+
'--message-nav-hover': token.colorFillAlter,
|
|
164
|
+
}, children: [_jsxs("header", { className: "qiqb-message-navigation-heading", children: [_jsx("strong", { children: "\u672A\u8BFB\u6D88\u606F" }), _jsx(Button, { type: "text", size: "small", icon: _jsx(ReloadOutlined, {}), loading: listLoading || countLoading, onClick: refresh, children: "\u5237\u65B0" })] }), connectionFailed && (_jsx(Alert, { type: "warning", showIcon: true, message: "\u6D88\u606F\u8FDE\u63A5\u6682\u4E0D\u53EF\u7528", description: connectionError, action: _jsx(Button, { size: "small", loading: listLoading || countLoading, onClick: refresh, children: "\u91CD\u8BD5" }) })), _jsx("div", { className: "qiqb-message-navigation-list", children: listLoading ? (_jsx(Skeleton, { active: true, title: false, paragraph: { rows: 4 } })) : listError ? null : records.length ? (records.map((record) => (_jsxs("button", { type: "button", className: "qiqb-message-navigation-item", onClick: () => {
|
|
165
|
+
if (!mounted.current)
|
|
166
|
+
return;
|
|
167
|
+
setOpen(false);
|
|
168
|
+
// 详情页成功加载后负责确认阅读;导航不提前标记,失败时仍保留未读。
|
|
169
|
+
openMessageInbox(record);
|
|
170
|
+
}, children: [_jsx("span", { className: "qiqb-message-navigation-title", children: record.title }), _jsx("span", { className: "qiqb-message-navigation-summary", children: record.content }), _jsxs("span", { className: "qiqb-message-navigation-meta", children: [record.topicName || record.topicCode, record.createTime ? ` · ${record.createTime}` : ''] })] }, receiptId(record))))) : (_jsx(Empty, { image: Empty.PRESENTED_IMAGE_SIMPLE, description: "\u6682\u65E0\u672A\u8BFB\u6D88\u606F" })) }), !listError && total > pageSize && (_jsx(Pagination, { size: "small", current: current, pageSize: pageSize, total: total, showSizeChanger: false, onChange: setCurrent })), _jsx("footer", { className: "qiqb-message-navigation-footer", children: _jsx(Button, { type: "link", onClick: () => {
|
|
171
|
+
if (!mounted.current)
|
|
172
|
+
return;
|
|
173
|
+
setOpen(false);
|
|
174
|
+
navigate(getMessageDomainRuntime().inboxPath);
|
|
175
|
+
}, children: "\u67E5\u770B\u5168\u90E8\u6D88\u606F" }) })] }));
|
|
176
|
+
return (_jsx(Popover, { trigger: "click", placement: "bottomRight", content: content, open: open, onOpenChange: (value) => {
|
|
177
|
+
if (!mounted.current)
|
|
178
|
+
return;
|
|
179
|
+
setOpen(value);
|
|
180
|
+
if (value)
|
|
181
|
+
void refreshCount();
|
|
182
|
+
}, children: _jsx(Button, { type: "text", title: connectionFailed ? '我的消息(消息连接已断开)' : '我的消息', "aria-label": navigationLabel, "aria-expanded": open,
|
|
183
|
+
// 与宿主帮助入口使用相同的文字按钮和图标尺寸;角标只锚定铃铛,避免撑高导航入口。
|
|
184
|
+
icon: _jsx(Badge, { count: unreadCount, overflowCount: 99, size: "small", children: _jsxs("span", { className: `qiqb-message-navigation-icon${connectionFailed ? ' qiqb-message-navigation-icon-disconnected' : ''}`, style: {
|
|
185
|
+
'--message-nav-warning': token.colorWarning,
|
|
186
|
+
'--message-nav-background': token.colorBgContainer,
|
|
187
|
+
}, children: [_jsx(BellOutlined, {}), connectionFailed && (
|
|
188
|
+
// 断开标识与右上角未读数分开,保留消息数量,同时用图形补充颜色状态。
|
|
189
|
+
_jsx(DisconnectOutlined, { className: "qiqb-message-navigation-disconnected-mark",
|
|
190
|
+
// 宿主按钮会设置图标字号,显式固定小标识尺寸,避免遮住铃铛主体。
|
|
191
|
+
style: { fontSize: 10 }, "aria-hidden": true }))] }) }) }) }));
|
|
192
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { InboxMessage } from '../services/MessageController.js';
|
|
2
|
+
import './message-reminder-display.less';
|
|
3
|
+
export interface MessageReminderDisplayProps {
|
|
4
|
+
messages: InboxMessage[];
|
|
5
|
+
/** 页面提供的内嵌消息栏槽位;没有槽位时不展示轮播,也不消耗提醒机会。 */
|
|
6
|
+
carouselTarget?: HTMLElement;
|
|
7
|
+
/** 关闭浮动/轮播展示,不表示消息已读;数据与生命周期均由外层控制。 */
|
|
8
|
+
onDismiss: (id: string) => void;
|
|
9
|
+
onOpen: (record: InboxMessage) => void;
|
|
10
|
+
/** “我已知晓”仅确认阅读,组件不自行执行接口或产生待办动作。 */
|
|
11
|
+
onConfirm: (record: InboxMessage) => Promise<void>;
|
|
12
|
+
confirmingIds?: string[];
|
|
13
|
+
confirmError?: string;
|
|
14
|
+
/** 只报告用户实际看到的消息,队列中尚未展示的消息不提前消耗提醒机会。 */
|
|
15
|
+
onDisplayed?: (record: InboxMessage) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 纯展示层:三种模式各自维护展示队列,仅轮播 UI 计时,不查询、不轮询、不自动标记已读。
|
|
19
|
+
* 外层决定哪些消息进入队列,以及查看详情、关闭和确认后何时移除消息。
|
|
20
|
+
*/
|
|
21
|
+
export default function MessageReminderDisplay({ messages, carouselTarget, onDismiss, onOpen, onConfirm, confirmingIds, confirmError, onDisplayed, }: MessageReminderDisplayProps): import("react").JSX.Element | null;
|