@qiqb/message-domain 0.0.0-stage → 0.1.4
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 +220 -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 +16 -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 +265 -0
- package/build/pages/inbox/inbox.less +430 -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 +291 -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 +79 -0
- package/build/runtime/index.js +105 -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 +96 -4
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BellOutlined, CloseOutlined, ExclamationCircleOutlined, InfoCircleOutlined, LeftOutlined, RightOutlined, WarningOutlined, } from '@ant-design/icons';
|
|
3
|
+
import { Alert, Button, Card, Modal, Tag, theme } from 'antd';
|
|
4
|
+
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import './message-reminder-display.less';
|
|
7
|
+
const levelLabels = {
|
|
8
|
+
NORMAL: '普通',
|
|
9
|
+
IMPORTANT: '重要',
|
|
10
|
+
SEVERE: '严重',
|
|
11
|
+
};
|
|
12
|
+
const levelPriority = {
|
|
13
|
+
NORMAL: 0,
|
|
14
|
+
IMPORTANT: 1,
|
|
15
|
+
SEVERE: 2,
|
|
16
|
+
};
|
|
17
|
+
function receiptId(record) {
|
|
18
|
+
return record.receiptId || record.id;
|
|
19
|
+
}
|
|
20
|
+
/** 预览消息由外层使用独立标识传入,可抢先展示但不会改变真实消息的严重程度。 */
|
|
21
|
+
function isPreview(record) {
|
|
22
|
+
return receiptId(record).startsWith('preview:');
|
|
23
|
+
}
|
|
24
|
+
/** 历史消息及未知扩展值采用兼容默认值,级别不参与提醒方式的选择。 */
|
|
25
|
+
function reminderMode(record) {
|
|
26
|
+
return record.reminderMode === 'FULLSCREEN' ||
|
|
27
|
+
record.reminderMode === 'CAROUSEL'
|
|
28
|
+
? record.reminderMode
|
|
29
|
+
: 'FLOATING';
|
|
30
|
+
}
|
|
31
|
+
function messageLevel(record) {
|
|
32
|
+
return record.messageLevel === 'SEVERE' || record.messageLevel === 'IMPORTANT'
|
|
33
|
+
? record.messageLevel
|
|
34
|
+
: 'NORMAL';
|
|
35
|
+
}
|
|
36
|
+
function levelType(level) {
|
|
37
|
+
return level === 'SEVERE'
|
|
38
|
+
? 'error'
|
|
39
|
+
: level === 'IMPORTANT'
|
|
40
|
+
? 'warning'
|
|
41
|
+
: 'info';
|
|
42
|
+
}
|
|
43
|
+
// Tag 的普通状态使用预设蓝色;Alert 的 info 不是 Tag 支持的预设状态。
|
|
44
|
+
function levelTagColor(level) {
|
|
45
|
+
return level === 'NORMAL' ? 'processing' : levelType(level);
|
|
46
|
+
}
|
|
47
|
+
function LevelIcon({ level }) {
|
|
48
|
+
if (level === 'SEVERE')
|
|
49
|
+
return _jsx(WarningOutlined, {});
|
|
50
|
+
if (level === 'IMPORTANT')
|
|
51
|
+
return _jsx(ExclamationCircleOutlined, {});
|
|
52
|
+
return _jsx(InfoCircleOutlined, {});
|
|
53
|
+
}
|
|
54
|
+
function displayTime(record) {
|
|
55
|
+
const value = record.occurredAt || record.createTime;
|
|
56
|
+
if (!value)
|
|
57
|
+
return '时间未提供';
|
|
58
|
+
const date = new Date(value);
|
|
59
|
+
return Number.isNaN(date.getTime())
|
|
60
|
+
? value
|
|
61
|
+
: new Intl.DateTimeFormat('zh-CN', {
|
|
62
|
+
dateStyle: 'medium',
|
|
63
|
+
timeStyle: 'short',
|
|
64
|
+
}).format(date);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 纯展示层:三种模式各自维护展示队列,仅轮播 UI 计时,不查询、不轮询、不自动标记已读。
|
|
68
|
+
* 外层决定哪些消息进入队列,以及查看详情、关闭和确认后何时移除消息。
|
|
69
|
+
*/
|
|
70
|
+
export default function MessageReminderDisplay({ messages, carouselTarget, onDismiss, onOpen, onConfirm, confirmingIds = [], confirmError, onDisplayed, }) {
|
|
71
|
+
const { token } = theme.useToken();
|
|
72
|
+
const [portalTarget, setPortalTarget] = useState();
|
|
73
|
+
const [carouselId, setCarouselId] = useState();
|
|
74
|
+
const [floatingId, setFloatingId] = useState();
|
|
75
|
+
const lastPreviewFloatingId = useRef();
|
|
76
|
+
const [departingCarousel, setDepartingCarousel] = useState();
|
|
77
|
+
const previousCarouselRef = useRef();
|
|
78
|
+
const [visibleCarouselTarget, setVisibleCarouselTarget] = useState();
|
|
79
|
+
// 页面槽位与预览槽位切换时,可见性只对原槽位有效,不能沿用旧节点的观察结果。
|
|
80
|
+
const carouselVisible = !!carouselTarget && visibleCarouselTarget === carouselTarget;
|
|
81
|
+
const [carouselHovered, setCarouselHovered] = useState(false);
|
|
82
|
+
const [carouselFocused, setCarouselFocused] = useState(false);
|
|
83
|
+
const [documentHidden, setDocumentHidden] = useState(() => typeof document !== 'undefined' && document.hidden);
|
|
84
|
+
const lastPreviewCarouselId = useRef();
|
|
85
|
+
const [localConfirmingId, setLocalConfirmingId] = useState();
|
|
86
|
+
const [localConfirmError, setLocalConfirmError] = useState();
|
|
87
|
+
const carouselRef = useRef(null);
|
|
88
|
+
const confirmingRef = useRef();
|
|
89
|
+
const displayedIds = useRef(new Set());
|
|
90
|
+
const mounted = useRef(true);
|
|
91
|
+
const bodyId = useId();
|
|
92
|
+
const floating = useMemo(() => messages.filter((record) => reminderMode(record) === 'FLOATING'), [messages]);
|
|
93
|
+
const carousel = useMemo(() => messages.filter((record) => reminderMode(record) === 'CAROUSEL'), [messages]);
|
|
94
|
+
const fullscreen = useMemo(() => messages
|
|
95
|
+
.filter((record) => reminderMode(record) === 'FULLSCREEN')
|
|
96
|
+
// 预览先于真实队列,确保低级别预览也能马上看见;其余同级保留外层次序。
|
|
97
|
+
.sort((a, b) => Number(isPreview(b)) - Number(isPreview(a)) ||
|
|
98
|
+
levelPriority[messageLevel(b)] - levelPriority[messageLevel(a)]), [messages]);
|
|
99
|
+
const previewFloating = floating.find(isPreview);
|
|
100
|
+
const previewFloatingId = previewFloating
|
|
101
|
+
? receiptId(previewFloating)
|
|
102
|
+
: undefined;
|
|
103
|
+
// 多条浮动消息合成一个面板;只上报当前可见项,切换前的消息仍保留提醒机会。
|
|
104
|
+
const selectedFloatingId = previewFloatingId && previewFloatingId !== lastPreviewFloatingId.current
|
|
105
|
+
? previewFloatingId
|
|
106
|
+
: floatingId;
|
|
107
|
+
const floatingIndex = Math.max(0, floating.findIndex((record) => receiptId(record) === selectedFloatingId));
|
|
108
|
+
const floatingMessage = floating[floatingIndex];
|
|
109
|
+
const visibleFloatingId = floatingMessage
|
|
110
|
+
? receiptId(floatingMessage)
|
|
111
|
+
: undefined;
|
|
112
|
+
const previewCarousel = carousel.find(isPreview);
|
|
113
|
+
const previewCarouselId = previewCarousel
|
|
114
|
+
? receiptId(previewCarousel)
|
|
115
|
+
: undefined;
|
|
116
|
+
// 新预览在当前渲染便抢先显示,后续仍允许用户手动切换或自动轮播。
|
|
117
|
+
const selectedCarouselId = previewCarouselId && previewCarouselId !== lastPreviewCarouselId.current
|
|
118
|
+
? previewCarouselId
|
|
119
|
+
: carouselId;
|
|
120
|
+
const carouselIndex = Math.max(0, carousel.findIndex((record) => receiptId(record) === selectedCarouselId));
|
|
121
|
+
const carouselMessage = carousel[carouselIndex];
|
|
122
|
+
const carouselMessageRef = useRef(carouselMessage);
|
|
123
|
+
carouselMessageRef.current = carouselMessage;
|
|
124
|
+
const visibleCarouselId = carouselMessage
|
|
125
|
+
? receiptId(carouselMessage)
|
|
126
|
+
: undefined;
|
|
127
|
+
// 外层轮询会创建新对象;只按标识序列重建计时器,避免相同队列不断刷新后永不轮播。
|
|
128
|
+
const carouselOrderKey = JSON.stringify(carousel.map(receiptId));
|
|
129
|
+
const fullscreenMessage = fullscreen[0];
|
|
130
|
+
const fullscreenId = fullscreenMessage
|
|
131
|
+
? receiptId(fullscreenMessage)
|
|
132
|
+
: undefined;
|
|
133
|
+
const fullscreenIdRef = useRef(fullscreenId);
|
|
134
|
+
fullscreenIdRef.current = fullscreenId;
|
|
135
|
+
const hasFullscreen = !!fullscreenMessage;
|
|
136
|
+
const confirmBusy = !!localConfirmingId ||
|
|
137
|
+
(!!fullscreenId && confirmingIds.includes(fullscreenId));
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
mounted.current = true;
|
|
140
|
+
// 浮动与全屏 Portal 放在 body,避免应用容器裁剪;轮播单独进入页面布局槽位。
|
|
141
|
+
const container = document.createElement('div');
|
|
142
|
+
container.className = 'qiqb-message-reminder-portal';
|
|
143
|
+
document.body.appendChild(container);
|
|
144
|
+
setPortalTarget(container);
|
|
145
|
+
return () => {
|
|
146
|
+
mounted.current = false;
|
|
147
|
+
// 微应用卸载时清理自行创建的节点,不残留遮罩或通知卡片。
|
|
148
|
+
container.remove();
|
|
149
|
+
};
|
|
150
|
+
}, []);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!portalTarget || !onDisplayed || documentHidden)
|
|
153
|
+
return;
|
|
154
|
+
// 全屏遮罩覆盖其他两种提醒时,只报告前景全屏消息;确认结束后再报告可见的卡片和轮播。
|
|
155
|
+
const visibleRecords = fullscreenMessage
|
|
156
|
+
? [fullscreenMessage]
|
|
157
|
+
: [
|
|
158
|
+
...(floatingMessage ? [floatingMessage] : []),
|
|
159
|
+
...(carouselTarget && carouselVisible && carouselMessage
|
|
160
|
+
? [carouselMessage]
|
|
161
|
+
: []),
|
|
162
|
+
];
|
|
163
|
+
for (const record of visibleRecords) {
|
|
164
|
+
const id = receiptId(record);
|
|
165
|
+
if (!displayedIds.current.has(id)) {
|
|
166
|
+
displayedIds.current.add(id);
|
|
167
|
+
onDisplayed(record);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, [
|
|
171
|
+
portalTarget,
|
|
172
|
+
onDisplayed,
|
|
173
|
+
floatingMessage,
|
|
174
|
+
documentHidden,
|
|
175
|
+
carouselVisible,
|
|
176
|
+
carouselMessage,
|
|
177
|
+
carouselTarget,
|
|
178
|
+
fullscreenMessage,
|
|
179
|
+
]);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
// 预览抢先一次;用户切换、关闭当前项后继续按浮动队列顺序展示。
|
|
182
|
+
lastPreviewFloatingId.current = previewFloatingId;
|
|
183
|
+
if (floatingId !== visibleFloatingId)
|
|
184
|
+
setFloatingId(visibleFloatingId);
|
|
185
|
+
}, [floatingId, visibleFloatingId, previewFloatingId]);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
// 新预览只抢先一次;真实消息刷新、手动切换均不会把当前项强制切回预览。
|
|
188
|
+
lastPreviewCarouselId.current = previewCarouselId;
|
|
189
|
+
if (!visibleCarouselId) {
|
|
190
|
+
setCarouselId(undefined);
|
|
191
|
+
setCarouselHovered(false);
|
|
192
|
+
setCarouselFocused(false);
|
|
193
|
+
}
|
|
194
|
+
else if (visibleCarouselId !== carouselId) {
|
|
195
|
+
setCarouselId(visibleCarouselId);
|
|
196
|
+
}
|
|
197
|
+
}, [visibleCarouselId, carouselId, previewCarouselId]);
|
|
198
|
+
useLayoutEffect(() => {
|
|
199
|
+
const record = carouselMessageRef.current;
|
|
200
|
+
const previous = previousCarouselRef.current;
|
|
201
|
+
previousCarouselRef.current = carouselTarget ? record : undefined;
|
|
202
|
+
// 仅在收件标识切换时保留短暂离场内容,接口返回同一条的新对象不会重复播放动画。
|
|
203
|
+
if (!carouselTarget ||
|
|
204
|
+
!record ||
|
|
205
|
+
!previous ||
|
|
206
|
+
receiptId(previous) === receiptId(record)) {
|
|
207
|
+
setDepartingCarousel(undefined);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
setDepartingCarousel(previous);
|
|
211
|
+
const timer = window.setTimeout(() => setDepartingCarousel(undefined), 260);
|
|
212
|
+
return () => window.clearTimeout(timer);
|
|
213
|
+
}, [visibleCarouselId, carouselTarget]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
// 槽位切换会重建 Portal;分页按钮移除也可能没有 blur,按当前 DOM 校准暂停状态。
|
|
216
|
+
const element = carouselRef.current;
|
|
217
|
+
setCarouselHovered(!!element?.matches(':hover'));
|
|
218
|
+
setCarouselFocused(!!element?.contains(document.activeElement));
|
|
219
|
+
}, [carouselTarget, carouselOrderKey]);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
const updateVisibility = () => setDocumentHidden(document.hidden);
|
|
222
|
+
updateVisibility();
|
|
223
|
+
document.addEventListener('visibilitychange', updateVisibility);
|
|
224
|
+
return () => document.removeEventListener('visibilitychange', updateVisibility);
|
|
225
|
+
}, []);
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
const element = carouselRef.current;
|
|
228
|
+
setVisibleCarouselTarget(undefined);
|
|
229
|
+
if (!carouselTarget || !element)
|
|
230
|
+
return;
|
|
231
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
232
|
+
setVisibleCarouselTarget(carouselTarget);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let active = true;
|
|
236
|
+
// 页面已滚到下方时,新消息只排队;回到消息栏后才记录展示并开始自动切换。
|
|
237
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
238
|
+
if (active)
|
|
239
|
+
setVisibleCarouselTarget(entry.isIntersecting && entry.intersectionRatio >= 0.5
|
|
240
|
+
? carouselTarget
|
|
241
|
+
: undefined);
|
|
242
|
+
}, { threshold: 0.5 });
|
|
243
|
+
observer.observe(element);
|
|
244
|
+
return () => {
|
|
245
|
+
active = false;
|
|
246
|
+
observer.disconnect();
|
|
247
|
+
};
|
|
248
|
+
}, [carouselTarget, !!carouselMessage, portalTarget]);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const ids = JSON.parse(carouselOrderKey);
|
|
251
|
+
// 悬停、键盘操作、后台标签页以及全屏确认期间暂停,恢复后给用户完整的五秒阅读时间。
|
|
252
|
+
if (!portalTarget ||
|
|
253
|
+
!carouselTarget ||
|
|
254
|
+
!carouselVisible ||
|
|
255
|
+
ids.length < 2 ||
|
|
256
|
+
carouselHovered ||
|
|
257
|
+
carouselFocused ||
|
|
258
|
+
documentHidden ||
|
|
259
|
+
hasFullscreen)
|
|
260
|
+
return;
|
|
261
|
+
const timer = window.setTimeout(() => {
|
|
262
|
+
const currentIndex = Math.max(0, ids.indexOf(visibleCarouselId || ''));
|
|
263
|
+
setCarouselId(ids[(currentIndex + 1) % ids.length]);
|
|
264
|
+
}, 5000);
|
|
265
|
+
return () => window.clearTimeout(timer);
|
|
266
|
+
}, [
|
|
267
|
+
carouselOrderKey,
|
|
268
|
+
visibleCarouselId,
|
|
269
|
+
carouselHovered,
|
|
270
|
+
carouselFocused,
|
|
271
|
+
documentHidden,
|
|
272
|
+
hasFullscreen,
|
|
273
|
+
carouselVisible,
|
|
274
|
+
portalTarget,
|
|
275
|
+
carouselTarget,
|
|
276
|
+
]);
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
setLocalConfirmError(undefined);
|
|
279
|
+
}, [fullscreenId]);
|
|
280
|
+
// 遮罩、滚动锁定与键盘焦点交给 Ant Design Modal,阅读确认仍由外层业务回调控制。
|
|
281
|
+
const confirm = async () => {
|
|
282
|
+
if (!fullscreenMessage || confirmBusy || confirmingRef.current)
|
|
283
|
+
return;
|
|
284
|
+
const id = receiptId(fullscreenMessage);
|
|
285
|
+
confirmingRef.current = id;
|
|
286
|
+
setLocalConfirmingId(id);
|
|
287
|
+
setLocalConfirmError(undefined);
|
|
288
|
+
try {
|
|
289
|
+
await onConfirm(fullscreenMessage);
|
|
290
|
+
// 展示层不主动关闭;外层确认成功后更新 messages,失败时保留当前提醒。
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (mounted.current && fullscreenIdRef.current === id) {
|
|
294
|
+
setLocalConfirmError(error instanceof Error && error.message
|
|
295
|
+
? error.message
|
|
296
|
+
: '确认失败,请重试。');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
confirmingRef.current = undefined;
|
|
301
|
+
if (mounted.current)
|
|
302
|
+
setLocalConfirmingId(undefined);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const changeCarousel = (step) => {
|
|
306
|
+
if (!carousel.length)
|
|
307
|
+
return;
|
|
308
|
+
const index = (carouselIndex + step + carousel.length) % carousel.length;
|
|
309
|
+
setCarouselId(receiptId(carousel[index]));
|
|
310
|
+
};
|
|
311
|
+
const changeFloating = (step) => {
|
|
312
|
+
if (!floating.length)
|
|
313
|
+
return;
|
|
314
|
+
const index = (floatingIndex + step + floating.length) % floating.length;
|
|
315
|
+
setFloatingId(receiptId(floating[index]));
|
|
316
|
+
};
|
|
317
|
+
const themeStyle = {
|
|
318
|
+
'--reminder-surface': token.colorBgElevated,
|
|
319
|
+
'--reminder-text': token.colorText,
|
|
320
|
+
'--reminder-muted': token.colorTextSecondary,
|
|
321
|
+
'--reminder-border': token.colorBorderSecondary,
|
|
322
|
+
'--reminder-shadow': token.boxShadowSecondary,
|
|
323
|
+
'--reminder-mask': token.colorBgMask,
|
|
324
|
+
'--reminder-radius': `${token.borderRadiusLG}px`,
|
|
325
|
+
'--reminder-normal': token.colorInfo,
|
|
326
|
+
'--reminder-normal-bg': token.colorInfoBg,
|
|
327
|
+
'--reminder-normal-border': token.colorInfoBorder,
|
|
328
|
+
'--reminder-important': token.colorWarning,
|
|
329
|
+
'--reminder-important-bg': token.colorWarningBg,
|
|
330
|
+
'--reminder-important-border': token.colorWarningBorder,
|
|
331
|
+
'--reminder-severe': token.colorError,
|
|
332
|
+
'--reminder-severe-bg': token.colorErrorBg,
|
|
333
|
+
'--reminder-severe-border': token.colorErrorBorder,
|
|
334
|
+
'--reminder-floating-z': token.zIndexPopupBase + 20,
|
|
335
|
+
'--reminder-fullscreen-z': token.zIndexPopupBase + 100,
|
|
336
|
+
};
|
|
337
|
+
// 空队列仍保留已关闭的 Modal 实例,让关闭动画和焦点归还完整执行。
|
|
338
|
+
if (!portalTarget)
|
|
339
|
+
return null;
|
|
340
|
+
const renderCarouselContent = (record) => (_jsxs(_Fragment, { children: [_jsx("strong", { title: record.title, children: record.title }), _jsx("span", { className: "qiqb-message-reminder-carousel-separator", "aria-hidden": true, children: "\u00B7" }), _jsx("span", { className: "qiqb-message-reminder-carousel-summary", title: record.content, children: record.content })] }));
|
|
341
|
+
return (_jsxs(_Fragment, { children: [carouselTarget &&
|
|
342
|
+
carouselMessage &&
|
|
343
|
+
createPortal(_jsx("div", { className: "qiqb-message-reminders qiqb-message-reminders-inline", style: themeStyle, children: _jsx("section", { className: "qiqb-message-reminder-carousel-region", ref: carouselRef, "data-level": messageLevel(carouselMessage), "aria-label": "\u8F6E\u64AD\u6D88\u606F\u63D0\u9192", onMouseEnter: () => setCarouselHovered(true), onMouseLeave: () => setCarouselHovered(false), onFocusCapture: () => setCarouselFocused(true), onBlurCapture: (event) => {
|
|
344
|
+
// 焦点在条带内部按钮间移动时保持暂停,离开整个条带才恢复。
|
|
345
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
346
|
+
setCarouselFocused(false);
|
|
347
|
+
}
|
|
348
|
+
}, children: _jsx(Alert, { className: "qiqb-message-reminder-carousel", type: levelType(messageLevel(carouselMessage)), banner: true, showIcon: true, icon: _jsx(LevelIcon, { level: messageLevel(carouselMessage) }), message: _jsxs("div", { className: "qiqb-message-reminder-carousel-content", children: [_jsx("span", { className: "qiqb-message-reminder-carousel-level", children: levelLabels[messageLevel(carouselMessage)] }), _jsxs("div", { className: "qiqb-message-reminder-carousel-viewport", "aria-live": "polite", "aria-atomic": "true", children: [_jsx("div", { className: "qiqb-message-reminder-carousel-message is-current", children: renderCarouselContent(carouselMessage) }, receiptId(carouselMessage)), departingCarousel && (_jsx("div", { className: "qiqb-message-reminder-carousel-message is-departing", "aria-hidden": "true", children: renderCarouselContent(departingCarousel) }, `departing:${receiptId(departingCarousel)}`))] })] }), action: _jsxs("div", { className: "qiqb-message-reminder-carousel-actions", children: [_jsx(Button, { className: "qiqb-message-reminder-carousel-view", type: "text", size: "small", loading: confirmingIds.includes(receiptId(carouselMessage)), disabled: confirmingIds.includes(receiptId(carouselMessage)), onClick: () => onOpen(carouselMessage), children: "\u67E5\u770B" }), carousel.length > 1 && (_jsxs("div", { className: "qiqb-message-reminder-carousel-navigation", "aria-label": "\u5207\u6362\u6D88\u606F", children: [_jsx(Button, { type: "text", size: "small", "aria-label": "\u4E0A\u4E00\u6761\u63D0\u9192", icon: _jsx(LeftOutlined, {}), onClick: () => changeCarousel(-1) }), _jsxs("span", { className: "qiqb-message-reminder-counter", children: [carouselIndex + 1, " / ", carousel.length] }), _jsx(Button, { type: "text", size: "small", "aria-label": "\u4E0B\u4E00\u6761\u63D0\u9192", icon: _jsx(RightOutlined, {}), onClick: () => changeCarousel(1) })] })), _jsx(Button, { className: "qiqb-message-reminder-carousel-close", type: "text", size: "small", "aria-label": "\u5173\u95ED\u5F53\u524D\u8F6E\u64AD\u63D0\u9192", icon: _jsx(CloseOutlined, {}), onClick: () => onDismiss(receiptId(carouselMessage)) })] }) }) }) }), carouselTarget), createPortal(_jsxs("div", { className: "qiqb-message-reminders", style: themeStyle, children: [floatingMessage && (_jsx("section", { className: "qiqb-message-reminder-floating-stack", "aria-label": "\u6D6E\u52A8\u6D88\u606F\u63D0\u9192", hidden: hasFullscreen, children: _jsxs(Card, { className: "qiqb-message-reminder-floating-card", size: "small", "data-level": messageLevel(floatingMessage), title: _jsxs(_Fragment, { children: [_jsx(BellOutlined, {}), " \u6D88\u606F\u63D0\u9192"] }), extra: _jsx(Button, { type: "text", size: "small", "aria-label": `关闭提醒:${floatingMessage.title}`, icon: _jsx(CloseOutlined, {}), onClick: () => onDismiss(receiptId(floatingMessage)) }), children: [_jsxs("article", { role: "status", "aria-live": "polite", children: [_jsxs("div", { className: "qiqb-message-reminder-floating-header", children: [_jsxs(Tag, { color: levelTagColor(messageLevel(floatingMessage)), children: [_jsx(LevelIcon, { level: messageLevel(floatingMessage) }), ' ', levelLabels[messageLevel(floatingMessage)]] }), _jsx("h3", { children: floatingMessage.title })] }), _jsx("p", { className: "qiqb-message-reminder-floating-summary", children: floatingMessage.content })] }, visibleFloatingId), _jsxs("div", { className: "qiqb-message-reminder-floating-footer", children: [_jsx("span", { children: floatingMessage.sourceSystem || '业务消息' }), _jsx(Button, { type: "link", size: "small", loading: confirmingIds.includes(receiptId(floatingMessage)), disabled: confirmingIds.includes(receiptId(floatingMessage)), onClick: () => onOpen(floatingMessage), children: "\u67E5\u770B\u6D88\u606F" })] }), floating.length > 1 && (_jsxs("div", { className: "qiqb-message-reminder-floating-navigation", "aria-label": "\u5207\u6362\u6D6E\u52A8\u6D88\u606F", children: [_jsx(Button, { type: "text", size: "small", "aria-label": "\u4E0A\u4E00\u6761\u6D6E\u52A8\u63D0\u9192", icon: _jsx(LeftOutlined, {}), onClick: () => changeFloating(-1) }), _jsxs("span", { className: "qiqb-message-reminder-counter", children: [floatingIndex + 1, " / ", floating.length] }), _jsx(Button, { type: "text", size: "small", "aria-label": "\u4E0B\u4E00\u6761\u6D6E\u52A8\u63D0\u9192", icon: _jsx(RightOutlined, {}), onClick: () => changeFloating(1) })] }))] }) })), _jsx(Modal, { open: hasFullscreen, centered: true, width: 640, rootClassName: "qiqb-message-reminders",
|
|
349
|
+
// Ant Design 5 读取 rootStyle,6 的语义样式以 styles.root 为准。
|
|
350
|
+
rootStyle: themeStyle, styles: { root: themeStyle }, className: "qiqb-message-reminder-fullscreen", zIndex: token.zIndexPopupBase + 100,
|
|
351
|
+
// 正式全屏不能用关闭、遮罩或 Escape 绕过阅读确认。
|
|
352
|
+
closable: false, maskClosable: false, keyboard: false, "aria-describedby": bodyId, title: fullscreenMessage && (_jsxs("div", { className: "qiqb-message-reminder-fullscreen-header", children: [_jsx(BellOutlined, {}), " \u6D88\u606F\u63D0\u9192", _jsx(Tag, { color: levelTagColor(messageLevel(fullscreenMessage)), children: levelLabels[messageLevel(fullscreenMessage)] }), _jsxs("span", { className: "qiqb-message-reminder-fullscreen-count", children: ["\u5F85\u786E\u8BA4 ", fullscreen.length, " \u6761"] })] })), footer: fullscreenMessage ? (_jsxs("div", { className: "qiqb-message-reminder-fullscreen-footer", children: [_jsx("span", { children: "\u786E\u8BA4\u4EC5\u8868\u793A\u4F60\u5DF2\u9605\u8BFB\u8FD9\u6761\u6D88\u606F\u3002" }), _jsxs("div", { children: [_jsx(Button, { loading: confirmingIds.includes(receiptId(fullscreenMessage)), disabled: confirmBusy, onClick: () => onOpen(fullscreenMessage), children: "\u67E5\u770B\u8BE6\u60C5" }), _jsx(Button, { type: "primary", loading: confirmBusy, onClick: () => void confirm(), children: "\u6211\u5DF2\u77E5\u6653" })] })] })) : null, children: fullscreenMessage && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qiqb-message-reminder-fullscreen-scroll", tabIndex: 0, role: "document", children: [_jsx("h2", { children: fullscreenMessage.title }), _jsxs("div", { className: "qiqb-message-reminder-fullscreen-meta", children: [_jsxs("span", { children: ["\u6765\u6E90\uFF1A", fullscreenMessage.sourceSystem || '业务消息'] }), _jsxs("span", { children: ["\u65F6\u95F4\uFF1A", displayTime(fullscreenMessage)] })] }), _jsx("div", { className: "qiqb-message-reminder-fullscreen-body", id: bodyId, children: fullscreenMessage.content })] }), (confirmError || localConfirmError) && (_jsx(Alert, { className: "qiqb-message-reminder-confirm-error", type: "error", showIcon: true, message: confirmError || localConfirmError }))] })) })] }), portalTarget)] }));
|
|
353
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import './message-workspace.less';
|
|
3
|
+
/** 消息页和抽屉共用宿主主题色,切换平台明暗主题时不留下固定白底。 */
|
|
4
|
+
export declare function useMessageTheme(): CSSProperties;
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
extra?: ReactNode;
|
|
10
|
+
/** 页面可在领域主题下调整自身布局,不影响主题/订阅等其他消息页面。 */
|
|
11
|
+
className?: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/** 只在消息领域内统一页面框架,避免把订阅语义放进跨领域 shared 包。 */
|
|
15
|
+
export default function MessageWorkspace({ title, description, icon, extra, className, children, }: Props): import("react").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BellOutlined } from '@ant-design/icons';
|
|
3
|
+
import { PageContainer } from '@ant-design/pro-components';
|
|
4
|
+
import { theme } from 'antd';
|
|
5
|
+
import './message-workspace.less';
|
|
6
|
+
/** 消息页和抽屉共用宿主主题色,切换平台明暗主题时不留下固定白底。 */
|
|
7
|
+
export function useMessageTheme() {
|
|
8
|
+
const { token } = theme.useToken();
|
|
9
|
+
return {
|
|
10
|
+
'--message-bg': token.colorBgLayout,
|
|
11
|
+
'--message-surface': token.colorBgContainer,
|
|
12
|
+
'--message-fill': token.colorFillAlter,
|
|
13
|
+
'--message-text': token.colorText,
|
|
14
|
+
'--message-muted': token.colorTextSecondary,
|
|
15
|
+
'--message-border': token.colorBorderSecondary,
|
|
16
|
+
'--message-primary': token.colorPrimary,
|
|
17
|
+
'--message-primary-bg': token.colorPrimaryBg,
|
|
18
|
+
'--message-success': token.colorSuccess,
|
|
19
|
+
'--message-radius': `${token.borderRadiusLG}px`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** 只在消息领域内统一页面框架,避免把订阅语义放进跨领域 shared 包。 */
|
|
23
|
+
export default function MessageWorkspace({ title, description, icon = _jsx(BellOutlined, {}), extra, className, children, }) {
|
|
24
|
+
const messageTheme = useMessageTheme();
|
|
25
|
+
return (_jsx(PageContainer, { pageHeaderRender: false, className: "qiqb-message-page", children: _jsxs("div", { className: ['qiqb-message', className].filter(Boolean).join(' '), style: messageTheme, children: [_jsxs("header", { className: "qiqb-message-heading", children: [_jsxs("div", { className: "qiqb-message-heading-main", children: [_jsx("div", { className: "qiqb-message-heading-icon", "aria-hidden": true, children: icon }), _jsxs("div", { children: [_jsx("h1", { children: title }), _jsx("p", { children: description })] })] }), extra && _jsx("div", { className: "qiqb-message-heading-actions", children: extra })] }), children] }) }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
.qiqb-message-navigation {
|
|
2
|
+
// 导航弹层适配窄屏和宿主主题,正文作为纯文本预览,不执行消息中的 HTML。
|
|
3
|
+
width: 360px;
|
|
4
|
+
max-width: calc(100vw - 48px);
|
|
5
|
+
color: var(--message-nav-text);
|
|
6
|
+
|
|
7
|
+
&-heading {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
padding-bottom: 8px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&-list {
|
|
15
|
+
max-height: min(420px, 55vh);
|
|
16
|
+
overflow-y: auto;
|
|
17
|
+
margin: 8px 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&-item {
|
|
21
|
+
display: block;
|
|
22
|
+
width: 100%;
|
|
23
|
+
padding: 12px 8px;
|
|
24
|
+
border: 0;
|
|
25
|
+
border-bottom: 1px solid var(--message-nav-border);
|
|
26
|
+
border-radius: 4px;
|
|
27
|
+
color: inherit;
|
|
28
|
+
background: transparent;
|
|
29
|
+
font: inherit;
|
|
30
|
+
text-align: left;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
|
|
33
|
+
&:hover,
|
|
34
|
+
&:focus-visible {
|
|
35
|
+
background: var(--message-nav-hover);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&-title,
|
|
40
|
+
&-summary,
|
|
41
|
+
&-meta {
|
|
42
|
+
display: block;
|
|
43
|
+
overflow-wrap: anywhere;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&-title {
|
|
47
|
+
margin-bottom: 4px;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&-summary {
|
|
52
|
+
display: -webkit-box;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
color: var(--message-nav-muted);
|
|
55
|
+
-webkit-line-clamp: 2;
|
|
56
|
+
-webkit-box-orient: vertical;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&-meta {
|
|
60
|
+
margin-top: 6px;
|
|
61
|
+
color: var(--message-nav-muted);
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&-footer {
|
|
66
|
+
margin-top: 8px;
|
|
67
|
+
border-top: 1px solid var(--message-nav-border);
|
|
68
|
+
text-align: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&-icon {
|
|
72
|
+
position: relative;
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
|
|
75
|
+
&-disconnected {
|
|
76
|
+
color: var(--message-nav-warning);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&-disconnected-mark {
|
|
81
|
+
// 右上角继续显示真实未读数,右下角插头标识表示连接断开。
|
|
82
|
+
position: absolute;
|
|
83
|
+
right: -5px;
|
|
84
|
+
bottom: -4px;
|
|
85
|
+
padding: 1px;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
background: var(--message-nav-background);
|
|
88
|
+
}
|
|
89
|
+
}
|