@quantabit/notification-sdk 1.0.0

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/dist/index.cjs ADDED
@@ -0,0 +1,1731 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var sdkConfig = require('@quantabit/sdk-config');
5
+
6
+ function _extends() {
7
+ return _extends = Object.assign ? Object.assign.bind() : function (n) {
8
+ for (var e = 1; e < arguments.length; e++) {
9
+ var t = arguments[e];
10
+ for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
11
+ }
12
+ return n;
13
+ }, _extends.apply(null, arguments);
14
+ }
15
+
16
+ function NotificationItem({
17
+ title,
18
+ message,
19
+ time,
20
+ avatar,
21
+ read = false,
22
+ type = 'info',
23
+ onRead,
24
+ onClick,
25
+ className = ''
26
+ }) {
27
+ const typeColors = {
28
+ info: '#3b82f6',
29
+ success: '#22c55e',
30
+ warning: '#f59e0b',
31
+ error: '#ef4444'
32
+ };
33
+ return /*#__PURE__*/React.createElement("div", {
34
+ className: `qn-item ${className}`,
35
+ onClick: () => {
36
+ onClick?.();
37
+ onRead?.();
38
+ },
39
+ style: {
40
+ display: 'flex',
41
+ gap: 12,
42
+ padding: '12px 20px',
43
+ cursor: 'pointer',
44
+ transition: 'all 0.15s',
45
+ background: read ? 'transparent' : 'rgba(59,130,246,0.02)',
46
+ borderBottom: '1px solid #f4f4f5'
47
+ }
48
+ }, avatar ? /*#__PURE__*/React.createElement("img", {
49
+ src: avatar,
50
+ style: {
51
+ width: 36,
52
+ height: 36,
53
+ borderRadius: '50%',
54
+ objectFit: 'cover',
55
+ flexShrink: 0
56
+ },
57
+ alt: ""
58
+ }) : /*#__PURE__*/React.createElement("div", {
59
+ style: {
60
+ width: 36,
61
+ height: 36,
62
+ borderRadius: '50%',
63
+ background: typeColors[type] + '18',
64
+ display: 'flex',
65
+ alignItems: 'center',
66
+ justifyContent: 'center',
67
+ flexShrink: 0
68
+ }
69
+ }, /*#__PURE__*/React.createElement("div", {
70
+ style: {
71
+ width: 8,
72
+ height: 8,
73
+ borderRadius: '50%',
74
+ background: typeColors[type]
75
+ }
76
+ })), /*#__PURE__*/React.createElement("div", {
77
+ style: {
78
+ flex: 1,
79
+ overflow: 'hidden'
80
+ }
81
+ }, /*#__PURE__*/React.createElement("div", {
82
+ style: {
83
+ fontSize: 13,
84
+ fontWeight: read ? 400 : 600,
85
+ color: '#18181b',
86
+ overflow: 'hidden',
87
+ textOverflow: 'ellipsis',
88
+ whiteSpace: 'nowrap'
89
+ }
90
+ }, title), message && /*#__PURE__*/React.createElement("div", {
91
+ style: {
92
+ fontSize: 12,
93
+ color: '#71717a',
94
+ marginTop: 2,
95
+ lineHeight: 1.4,
96
+ display: '-webkit-box',
97
+ WebkitLineClamp: 2,
98
+ WebkitBoxOrient: 'vertical',
99
+ overflow: 'hidden'
100
+ }
101
+ }, message), time && /*#__PURE__*/React.createElement("div", {
102
+ style: {
103
+ fontSize: 11,
104
+ color: '#a1a1aa',
105
+ marginTop: 4
106
+ }
107
+ }, time)), !read && /*#__PURE__*/React.createElement("div", {
108
+ style: {
109
+ width: 6,
110
+ height: 6,
111
+ borderRadius: '50%',
112
+ background: '#3b82f6',
113
+ flexShrink: 0,
114
+ marginTop: 4
115
+ }
116
+ }));
117
+ }
118
+
119
+ function NotificationCenter({
120
+ notifications = [],
121
+ onRead,
122
+ onReadAll,
123
+ onClear,
124
+ title = 'Notifications',
125
+ emptyText = 'No notifications',
126
+ className = ''
127
+ }) {
128
+ const unread = notifications.filter(n => !n.read).length;
129
+ return /*#__PURE__*/React.createElement("div", {
130
+ className: `qn-center ${className}`,
131
+ style: {
132
+ width: 360,
133
+ background: '#fff',
134
+ borderRadius: 16,
135
+ boxShadow: '0 12px 48px rgba(0,0,0,0.12)',
136
+ border: '1px solid #e4e4e7',
137
+ overflow: 'hidden',
138
+ fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif"
139
+ }
140
+ }, /*#__PURE__*/React.createElement("div", {
141
+ style: {
142
+ display: 'flex',
143
+ justifyContent: 'space-between',
144
+ alignItems: 'center',
145
+ padding: '16px 20px',
146
+ borderBottom: '1px solid #f4f4f5'
147
+ }
148
+ }, /*#__PURE__*/React.createElement("div", {
149
+ style: {
150
+ display: 'flex',
151
+ alignItems: 'center',
152
+ gap: 8
153
+ }
154
+ }, /*#__PURE__*/React.createElement("span", {
155
+ style: {
156
+ fontSize: 16,
157
+ fontWeight: 700
158
+ }
159
+ }, title), unread > 0 && /*#__PURE__*/React.createElement("span", {
160
+ style: {
161
+ fontSize: 11,
162
+ fontWeight: 700,
163
+ background: '#ef4444',
164
+ color: '#fff',
165
+ borderRadius: 10,
166
+ padding: '1px 7px'
167
+ }
168
+ }, unread)), /*#__PURE__*/React.createElement("div", {
169
+ style: {
170
+ display: 'flex',
171
+ gap: 8
172
+ }
173
+ }, onReadAll && /*#__PURE__*/React.createElement("button", {
174
+ onClick: onReadAll,
175
+ style: {
176
+ border: 'none',
177
+ background: 'none',
178
+ cursor: 'pointer',
179
+ fontSize: 12,
180
+ color: '#3b82f6',
181
+ fontWeight: 500
182
+ }
183
+ }, "Mark all read"), onClear && /*#__PURE__*/React.createElement("button", {
184
+ onClick: onClear,
185
+ style: {
186
+ border: 'none',
187
+ background: 'none',
188
+ cursor: 'pointer',
189
+ fontSize: 12,
190
+ color: '#a1a1aa'
191
+ }
192
+ }, "Clear"))), /*#__PURE__*/React.createElement("div", {
193
+ style: {
194
+ maxHeight: 400,
195
+ overflowY: 'auto'
196
+ }
197
+ }, notifications.length === 0 ? /*#__PURE__*/React.createElement("div", {
198
+ style: {
199
+ padding: 40,
200
+ textAlign: 'center',
201
+ color: '#a1a1aa',
202
+ fontSize: 13
203
+ }
204
+ }, emptyText) : notifications.map((n, i) => /*#__PURE__*/React.createElement(NotificationItem, _extends({
205
+ key: n.id || i
206
+ }, n, {
207
+ onRead: () => onRead?.(n.id || i)
208
+ })))));
209
+ }
210
+
211
+ function NotificationBell({
212
+ notifications = [],
213
+ onRead,
214
+ onReadAll,
215
+ onClear,
216
+ size = 20,
217
+ className = ''
218
+ }) {
219
+ const [open, setOpen] = React.useState(false);
220
+ const unread = notifications.filter(n => !n.read).length;
221
+ return /*#__PURE__*/React.createElement("div", {
222
+ style: {
223
+ position: 'relative',
224
+ display: 'inline-flex'
225
+ }
226
+ }, /*#__PURE__*/React.createElement("button", {
227
+ onClick: () => setOpen(!open),
228
+ className: `qn-bell ${className}`,
229
+ style: {
230
+ position: 'relative',
231
+ border: 'none',
232
+ background: 'transparent',
233
+ cursor: 'pointer',
234
+ padding: 8,
235
+ borderRadius: 8,
236
+ transition: 'all 0.2s'
237
+ }
238
+ }, /*#__PURE__*/React.createElement("svg", {
239
+ width: size,
240
+ height: size,
241
+ viewBox: "0 0 24 24",
242
+ fill: "none",
243
+ stroke: "currentColor",
244
+ strokeWidth: "2"
245
+ }, /*#__PURE__*/React.createElement("path", {
246
+ d: "M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 01-3.46 0"
247
+ })), unread > 0 && /*#__PURE__*/React.createElement("span", {
248
+ style: {
249
+ position: 'absolute',
250
+ top: 2,
251
+ right: 2,
252
+ width: 16,
253
+ height: 16,
254
+ borderRadius: '50%',
255
+ background: '#ef4444',
256
+ color: '#fff',
257
+ fontSize: 10,
258
+ fontWeight: 700,
259
+ display: 'flex',
260
+ alignItems: 'center',
261
+ justifyContent: 'center',
262
+ border: '2px solid #fff'
263
+ }
264
+ }, unread > 9 ? '9+' : unread)), open && /*#__PURE__*/React.createElement("div", {
265
+ style: {
266
+ position: 'absolute',
267
+ top: '100%',
268
+ right: 0,
269
+ marginTop: 8,
270
+ zIndex: 9999
271
+ }
272
+ }, /*#__PURE__*/React.createElement(NotificationCenter, {
273
+ notifications: notifications,
274
+ onRead: onRead,
275
+ onReadAll: onReadAll,
276
+ onClear: onClear
277
+ })));
278
+ }
279
+
280
+ /**
281
+ * Notification SDK - API 客户端
282
+ * 通知系统后端接口封装
283
+ *
284
+ * 使用 BaseApiClient 基类简化代码
285
+ */
286
+
287
+
288
+ /**
289
+ * 通知 API 客户端
290
+ */
291
+ class NotificationApiClient extends sdkConfig.BaseApiClient {
292
+ constructor(config = {}) {
293
+ super('/notification', config);
294
+ }
295
+
296
+ // ============ 通知查询 ============
297
+
298
+ /**
299
+ * 获取通知列表
300
+ * @param {Object} params - 查询参数
301
+ */
302
+ async getNotifications(params = {}) {
303
+ return this.get('/list', params);
304
+ }
305
+
306
+ /**
307
+ * 获取通知详情
308
+ * @param {string} notificationId - 通知 ID
309
+ */
310
+ async getNotification(notificationId) {
311
+ return this.get(`/${notificationId}`);
312
+ }
313
+
314
+ /**
315
+ * 获取未读通知数
316
+ */
317
+ async getUnreadCount() {
318
+ return this.get('/unread-count');
319
+ }
320
+
321
+ /**
322
+ * 按类型获取通知
323
+ * @param {string} type - 通知类型
324
+ * @param {Object} params - 查询参数
325
+ */
326
+ async getNotificationsByType(type, params = {}) {
327
+ return this.get('/list', {
328
+ type,
329
+ ...params
330
+ });
331
+ }
332
+
333
+ // ============ 通知操作 ============
334
+
335
+ /**
336
+ * 标记为已读
337
+ * @param {string} notificationId - 通知 ID
338
+ */
339
+ async markAsRead(notificationId) {
340
+ return this.post(`/${notificationId}/read`);
341
+ }
342
+
343
+ /**
344
+ * 批量标记已读
345
+ * @param {string[]} notificationIds - 通知 ID 列表
346
+ */
347
+ async batchMarkAsRead(notificationIds) {
348
+ return this.post('/batch-read', {
349
+ ids: notificationIds
350
+ });
351
+ }
352
+
353
+ /**
354
+ * 全部标记已读
355
+ */
356
+ async markAllAsRead() {
357
+ return this.post('/read-all');
358
+ }
359
+
360
+ /**
361
+ * 删除通知
362
+ * @param {string} notificationId - 通知 ID
363
+ */
364
+ async deleteNotification(notificationId) {
365
+ return this.delete(`/${notificationId}`);
366
+ }
367
+
368
+ /**
369
+ * 批量删除通知
370
+ * @param {string[]} notificationIds - 通知 ID 列表
371
+ */
372
+ async batchDelete(notificationIds) {
373
+ return this.post('/batch-delete', {
374
+ ids: notificationIds
375
+ });
376
+ }
377
+
378
+ /**
379
+ * 清空所有通知
380
+ */
381
+ async clearAll() {
382
+ return this.delete('/all');
383
+ }
384
+
385
+ // ============ 通知设置 ============
386
+
387
+ /**
388
+ * 获取通知设置
389
+ */
390
+ async getSettings() {
391
+ return this.get('/settings');
392
+ }
393
+
394
+ /**
395
+ * 更新通知设置
396
+ * @param {Object} settings - 设置数据
397
+ */
398
+ async updateSettings(settings) {
399
+ return this.put('/settings', settings);
400
+ }
401
+
402
+ /**
403
+ * 获取各类型通知设置
404
+ */
405
+ async getTypeSettings() {
406
+ return this.get('/settings/types');
407
+ }
408
+
409
+ /**
410
+ * 更新类型通知设置
411
+ * @param {string} type - 通知类型
412
+ * @param {Object} settings - 设置数据
413
+ */
414
+ async updateTypeSettings(type, settings) {
415
+ return this.put(`/settings/types/${type}`, settings);
416
+ }
417
+
418
+ // ============ 设备管理 ============
419
+
420
+ /**
421
+ * 注册设备
422
+ * @param {Object} device - 设备信息
423
+ */
424
+ async registerDevice(device) {
425
+ return this.post('/devices', device);
426
+ }
427
+
428
+ /**
429
+ * 更新设备 Token
430
+ * @param {string} deviceId - 设备 ID
431
+ * @param {string} token - 推送 Token
432
+ */
433
+ async updateDeviceToken(deviceId, token) {
434
+ return this.put(`/devices/${deviceId}/token`, {
435
+ token
436
+ });
437
+ }
438
+
439
+ /**
440
+ * 删除设备
441
+ * @param {string} deviceId - 设备 ID
442
+ */
443
+ async removeDevice(deviceId) {
444
+ return this.delete(`/devices/${deviceId}`);
445
+ }
446
+
447
+ /**
448
+ * 获取我的设备列表
449
+ */
450
+ async getMyDevices() {
451
+ return this.get('/devices');
452
+ }
453
+
454
+ // ============ 推送订阅 ============
455
+
456
+ /**
457
+ * 订阅主题
458
+ * @param {string} topic - 主题
459
+ */
460
+ async subscribeToTopic(topic) {
461
+ return this.post('/topics/subscribe', {
462
+ topic
463
+ });
464
+ }
465
+
466
+ /**
467
+ * 取消订阅主题
468
+ * @param {string} topic - 主题
469
+ */
470
+ async unsubscribeFromTopic(topic) {
471
+ return this.post('/topics/unsubscribe', {
472
+ topic
473
+ });
474
+ }
475
+
476
+ /**
477
+ * 获取已订阅主题
478
+ */
479
+ async getSubscribedTopics() {
480
+ return this.get('/topics/subscribed');
481
+ }
482
+
483
+ // ============ 管理员操作 ============
484
+
485
+ /**
486
+ * 发送通知(管理员)
487
+ * @param {Object} notification - 通知数据
488
+ */
489
+ async sendNotification(notification) {
490
+ return this.post('/admin/send', notification);
491
+ }
492
+
493
+ /**
494
+ * 批量发送通知(管理员)
495
+ * @param {Object} notification - 通知数据
496
+ * @param {string[]} userIds - 用户 ID 列表
497
+ */
498
+ async batchSendNotification(notification, userIds) {
499
+ return this.post('/admin/batch-send', {
500
+ ...notification,
501
+ user_ids: userIds
502
+ });
503
+ }
504
+
505
+ /**
506
+ * 发送广播通知(管理员)
507
+ * @param {Object} notification - 通知数据
508
+ */
509
+ async broadcastNotification(notification) {
510
+ return this.post('/admin/broadcast', notification);
511
+ }
512
+
513
+ // ============ 隐私合规 ============
514
+
515
+ /**
516
+ * 清除所有通知和设备数据 — GDPR 第17条
517
+ * @returns {Promise<object>} 清除结果
518
+ */
519
+ async clearAllUserData() {
520
+ const results = {};
521
+ try {
522
+ results.notifications = await this.clearAll();
523
+ } catch (e) {
524
+ results.notifError = e.message;
525
+ }
526
+ try {
527
+ const devices = await this.getMyDevices();
528
+ for (const d of devices?.data || []) {
529
+ await this.removeDevice(d.id).catch(() => {});
530
+ }
531
+ results.devices = 'cleared';
532
+ } catch (e) {
533
+ results.deviceError = e.message;
534
+ }
535
+ return {
536
+ cleared: true,
537
+ timestamp: new Date().toISOString(),
538
+ ...results
539
+ };
540
+ }
541
+
542
+ /**
543
+ * 获取隐私数据声明
544
+ */
545
+ getDataDisclosure() {
546
+ return {
547
+ sdk: '@quantabit/notification-sdk',
548
+ privacyLevel: 'functional',
549
+ consentRequired: false,
550
+ collected: [{
551
+ type: 'device_info',
552
+ description: 'Device ID, push token, platform',
553
+ retention: 'Until device unregistered'
554
+ }, {
555
+ type: 'notification_history',
556
+ description: 'Sent notification records',
557
+ retention: '90 days'
558
+ }, {
559
+ type: 'notification_preferences',
560
+ description: 'User notification settings',
561
+ retention: 'Until account deletion'
562
+ }, {
563
+ type: 'topic_subscriptions',
564
+ description: 'Subscribed notification topics',
565
+ retention: 'Until unsubscribed'
566
+ }],
567
+ gdprCapabilities: ['delete'],
568
+ note: 'Notification delivery requires device registration. Users can unregister devices anytime.'
569
+ };
570
+ }
571
+ }
572
+
573
+ // 创建默认实例
574
+ const notificationApi = new NotificationApiClient();
575
+
576
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
577
+ const messages = {
578
+ en: {
579
+ 'notif.title': 'Notifications',
580
+ 'notif.empty': 'No notifications',
581
+ 'notif.markAll': 'Mark all read',
582
+ 'notif.clear': 'Clear'
583
+ },
584
+ zh: {
585
+ 'notif.title': '通知',
586
+ 'notif.empty': '暂无通知',
587
+ 'notif.markAll': '全部已读',
588
+ 'notif.clear': '清空'
589
+ },
590
+ ja: {
591
+ 'notif.title': '通知',
592
+ 'notif.empty': '通知はありません',
593
+ 'notif.markAll': 'すべて既読にする',
594
+ 'notif.clear': 'クリア'
595
+ },
596
+ ko: {
597
+ 'notif.title': '알림',
598
+ 'notif.empty': '알림이 없습니다',
599
+ 'notif.markAll': '모두 읽음으로 표시',
600
+ 'notif.clear': '지우기'
601
+ }
602
+ };
603
+ let currentLang = 'en';
604
+ function initLanguage() {
605
+ // 从 localStorage 或浏览器检测初始化语言
606
+ if (typeof window !== 'undefined') {
607
+ const saved = localStorage.getItem('qbit_notification_lang');
608
+ if (saved && SUPPORTED_LANGUAGES.includes(saved)) {
609
+ currentLang = saved;
610
+ }
611
+ }
612
+ return currentLang;
613
+ }
614
+ function setLanguage(l) {
615
+ if (SUPPORTED_LANGUAGES.includes(l)) currentLang = l;
616
+ }
617
+ function getLanguage() {
618
+ return currentLang;
619
+ }
620
+ function t(k, params = {}) {
621
+ let text = messages[currentLang]?.[k] || messages.en?.[k] || k;
622
+ Object.entries(params).forEach(([key, val]) => {
623
+ text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), val);
624
+ });
625
+ return text;
626
+ }
627
+
628
+ /**
629
+ * Notification SDK - React Context
630
+ * 通知状态管理
631
+ */
632
+
633
+ const NotificationContext = /*#__PURE__*/React.createContext(null);
634
+
635
+ /**
636
+ * 通知Provider组件
637
+ */
638
+ function NotificationProvider({
639
+ children,
640
+ apiBaseUrl,
641
+ getToken,
642
+ language,
643
+ enableWebSocket = true,
644
+ wsUrl,
645
+ onError,
646
+ onNewNotification
647
+ }) {
648
+ // 状态
649
+ const [notifications, setNotifications] = React.useState([]);
650
+ const [unreadCount, setUnreadCount] = React.useState(0);
651
+ const [unreadByType, setUnreadByType] = React.useState({});
652
+ const [announcements, setAnnouncements] = React.useState([]);
653
+ const [preferences, setPreferences] = React.useState(null);
654
+ const [loading, setLoading] = React.useState(false);
655
+ const [error, setError] = React.useState(null);
656
+ const [connectionStatus, setConnectionStatus] = React.useState('disconnected');
657
+ const [currentFilter, setCurrentFilter] = React.useState({
658
+ status: 'unread',
659
+ type: null
660
+ });
661
+ const [pagination, setPagination] = React.useState({
662
+ page: 1,
663
+ pageSize: 20,
664
+ total: 0,
665
+ hasMore: true
666
+ });
667
+ const initialized = React.useRef(false);
668
+
669
+ // 初始化
670
+ React.useEffect(() => {
671
+ if (initialized.current) return;
672
+ initialized.current = true;
673
+
674
+ // 配置API
675
+ notificationApi.configure({
676
+ apiBaseUrl: apiBaseUrl || '/api/v1/notifications',
677
+ getToken: getToken || (() => {
678
+ if (typeof localStorage !== 'undefined') {
679
+ return localStorage.getItem('access_token');
680
+ }
681
+ return null;
682
+ }),
683
+ getLanguage: () => getLanguage(),
684
+ onError: err => {
685
+ setError(err.message);
686
+ onError?.(err);
687
+ }
688
+ });
689
+
690
+ // 初始化语言
691
+ if (language) {
692
+ setLanguage(language);
693
+ } else {
694
+ initLanguage();
695
+ }
696
+
697
+ // WebSocket连接状态回调
698
+ notificationApi.setConnectionChangeHandler(setConnectionStatus);
699
+
700
+ // 新通知回调
701
+ notificationApi.setNewNotificationHandler(notification => {
702
+ setNotifications(prev => [notification, ...prev]);
703
+ setUnreadCount(prev => prev + 1);
704
+ onNewNotification?.(notification);
705
+ });
706
+ }, [apiBaseUrl, getToken, language, onError, onNewNotification]);
707
+
708
+ // WebSocket连接管理
709
+ React.useEffect(() => {
710
+ if (enableWebSocket && getToken?.()) {
711
+ notificationApi.connectWebSocket(wsUrl);
712
+ }
713
+ return () => {
714
+ notificationApi.disconnectWebSocket();
715
+ };
716
+ }, [enableWebSocket, wsUrl, getToken]);
717
+
718
+ // 获取通知列表
719
+ const fetchNotifications = React.useCallback(async (options = {}) => {
720
+ setLoading(true);
721
+ setError(null);
722
+ try {
723
+ const result = await notificationApi.getNotifications({
724
+ ...currentFilter,
725
+ ...options,
726
+ page: options.page || 1,
727
+ pageSize: pagination.pageSize
728
+ });
729
+ if (options.page === 1 || !options.page) {
730
+ setNotifications(result.items || result.data || []);
731
+ } else {
732
+ setNotifications(prev => [...prev, ...(result.items || result.data || [])]);
733
+ }
734
+ setPagination({
735
+ page: result.page || options.page || 1,
736
+ pageSize: result.page_size || pagination.pageSize,
737
+ total: result.total || 0,
738
+ hasMore: (result.items || result.data || []).length >= pagination.pageSize
739
+ });
740
+ } catch (err) {
741
+ setError(err.message);
742
+ } finally {
743
+ setLoading(false);
744
+ }
745
+ }, [currentFilter, pagination.pageSize]);
746
+
747
+ // 加载更多
748
+ const loadMore = React.useCallback(async () => {
749
+ if (!pagination.hasMore || loading) return;
750
+ await fetchNotifications({
751
+ page: pagination.page + 1
752
+ });
753
+ }, [pagination, loading, fetchNotifications]);
754
+
755
+ // 获取未读数量
756
+ const fetchUnreadCount = React.useCallback(async () => {
757
+ try {
758
+ const result = await notificationApi.getUnreadCount();
759
+ setUnreadCount(result.count || result.total || 0);
760
+ } catch (err) {
761
+ console.error('Failed to fetch unread count:', err);
762
+ }
763
+ }, []);
764
+
765
+ // 获取分类未读数量
766
+ const fetchUnreadByType = React.useCallback(async () => {
767
+ try {
768
+ const result = await notificationApi.getUnreadCountByType();
769
+ setUnreadByType(result);
770
+ } catch (err) {
771
+ console.error('Failed to fetch unread by type:', err);
772
+ }
773
+ }, []);
774
+
775
+ // 标记已读
776
+ const markAsRead = React.useCallback(async notificationId => {
777
+ try {
778
+ await notificationApi.markAsRead(notificationId);
779
+ setNotifications(prev => prev.map(n => n.id === notificationId ? {
780
+ ...n,
781
+ status: 'read'
782
+ } : n));
783
+ setUnreadCount(prev => Math.max(0, prev - 1));
784
+ } catch (err) {
785
+ setError(err.message);
786
+ throw err;
787
+ }
788
+ }, []);
789
+
790
+ // 全部标记已读
791
+ const markAllAsRead = React.useCallback(async (type = null) => {
792
+ try {
793
+ await notificationApi.markAllAsRead(type);
794
+ setNotifications(prev => prev.map(n => !type || n.type === type ? {
795
+ ...n,
796
+ status: 'read'
797
+ } : n));
798
+ if (type) {
799
+ setUnreadByType(prev => ({
800
+ ...prev,
801
+ [type]: 0
802
+ }));
803
+ } else {
804
+ setUnreadCount(0);
805
+ setUnreadByType({});
806
+ }
807
+ } catch (err) {
808
+ setError(err.message);
809
+ throw err;
810
+ }
811
+ }, []);
812
+
813
+ // 删除通知
814
+ const deleteNotification = React.useCallback(async notificationId => {
815
+ try {
816
+ const notification = notifications.find(n => n.id === notificationId);
817
+ await notificationApi.deleteNotification(notificationId);
818
+ setNotifications(prev => prev.filter(n => n.id !== notificationId));
819
+ if (notification?.status === 'unread') {
820
+ setUnreadCount(prev => Math.max(0, prev - 1));
821
+ }
822
+ } catch (err) {
823
+ setError(err.message);
824
+ throw err;
825
+ }
826
+ }, [notifications]);
827
+
828
+ // 归档通知
829
+ const archiveNotification = React.useCallback(async notificationId => {
830
+ try {
831
+ await notificationApi.archiveNotification(notificationId);
832
+ setNotifications(prev => prev.map(n => n.id === notificationId ? {
833
+ ...n,
834
+ status: 'archived'
835
+ } : n));
836
+ } catch (err) {
837
+ setError(err.message);
838
+ throw err;
839
+ }
840
+ }, []);
841
+
842
+ // 获取公告
843
+ const fetchAnnouncements = React.useCallback(async (options = {}) => {
844
+ try {
845
+ const result = await notificationApi.getAnnouncements(options);
846
+ setAnnouncements(result.items || result.data || []);
847
+ } catch (err) {
848
+ console.error('Failed to fetch announcements:', err);
849
+ }
850
+ }, []);
851
+
852
+ // 获取偏好设置
853
+ const fetchPreferences = React.useCallback(async () => {
854
+ try {
855
+ const result = await notificationApi.getPreferences();
856
+ setPreferences(result);
857
+ } catch (err) {
858
+ console.error('Failed to fetch preferences:', err);
859
+ }
860
+ }, []);
861
+
862
+ // 更新偏好设置
863
+ const updatePreferences = React.useCallback(async newPreferences => {
864
+ try {
865
+ await notificationApi.updatePreferences(newPreferences);
866
+ setPreferences(prev => ({
867
+ ...prev,
868
+ ...newPreferences
869
+ }));
870
+ } catch (err) {
871
+ setError(err.message);
872
+ throw err;
873
+ }
874
+ }, []);
875
+
876
+ // 设置筛选条件
877
+ const setFilter = React.useCallback(filter => {
878
+ setCurrentFilter(prev => ({
879
+ ...prev,
880
+ ...filter
881
+ }));
882
+ setPagination(prev => ({
883
+ ...prev,
884
+ page: 1,
885
+ hasMore: true
886
+ }));
887
+ }, []);
888
+
889
+ // 刷新
890
+ const refresh = React.useCallback(async () => {
891
+ await Promise.all([fetchNotifications({
892
+ page: 1
893
+ }), fetchUnreadCount()]);
894
+ }, [fetchNotifications, fetchUnreadCount]);
895
+
896
+ // Context值
897
+ const value = {
898
+ // 状态
899
+ notifications,
900
+ unreadCount,
901
+ unreadByType,
902
+ announcements,
903
+ preferences,
904
+ loading,
905
+ error,
906
+ connectionStatus,
907
+ currentFilter,
908
+ pagination,
909
+ // 方法
910
+ fetchNotifications,
911
+ loadMore,
912
+ fetchUnreadCount,
913
+ fetchUnreadByType,
914
+ markAsRead,
915
+ markAllAsRead,
916
+ deleteNotification,
917
+ archiveNotification,
918
+ fetchAnnouncements,
919
+ fetchPreferences,
920
+ updatePreferences,
921
+ setFilter,
922
+ refresh,
923
+ // API
924
+ api: notificationApi
925
+ };
926
+ return /*#__PURE__*/React.createElement(NotificationContext.Provider, {
927
+ value: value
928
+ }, children);
929
+ }
930
+
931
+ /**
932
+ * 使用通知Context的Hook
933
+ */
934
+ function useNotification() {
935
+ const context = React.useContext(NotificationContext);
936
+ if (!context) {
937
+ throw new Error('useNotification must be used within a NotificationProvider');
938
+ }
939
+ return context;
940
+ }
941
+
942
+ // 通用图标样式
943
+ const iconStyle = {
944
+ width: '1em',
945
+ height: '1em',
946
+ fill: 'currentColor'
947
+ };
948
+
949
+ // 铃铛图标
950
+ function BellIcon(props) {
951
+ return /*#__PURE__*/React.createElement("svg", _extends({
952
+ viewBox: "0 0 24 24",
953
+ style: iconStyle
954
+ }, props), /*#__PURE__*/React.createElement("path", {
955
+ d: "M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"
956
+ }));
957
+ }
958
+
959
+ // 铃铛带数字图标
960
+ function BellDotIcon(props) {
961
+ return /*#__PURE__*/React.createElement("svg", _extends({
962
+ viewBox: "0 0 24 24",
963
+ style: iconStyle
964
+ }, props), /*#__PURE__*/React.createElement("path", {
965
+ d: "M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"
966
+ }), /*#__PURE__*/React.createElement("circle", {
967
+ cx: "18",
968
+ cy: "5",
969
+ r: "4",
970
+ fill: "currentColor"
971
+ }));
972
+ }
973
+
974
+ // 公告/喇叭图标
975
+ function MegaphoneIcon(props) {
976
+ return /*#__PURE__*/React.createElement("svg", _extends({
977
+ viewBox: "0 0 24 24",
978
+ style: iconStyle
979
+ }, props), /*#__PURE__*/React.createElement("path", {
980
+ d: "M18 11v2h4v-2h-4zm-2 6.61c.96.71 2.21 1.65 3.2 2.39.4-.53.8-1.07 1.2-1.6-.99-.74-2.24-1.68-3.2-2.4-.4.54-.8 1.08-1.2 1.61zM20.4 5.6c-.4-.53-.8-1.07-1.2-1.6-.99.74-2.24 1.68-3.2 2.4.4.53.8 1.07 1.2 1.6.96-.72 2.21-1.65 3.2-2.4zM4 9c-1.1 0-2 .9-2 2v2c0 1.1.9 2 2 2h1v4h2v-4h1l5 3V6L8 9H4zm11.5 3c0-1.33-.58-2.53-1.5-3.35v6.69c.92-.81 1.5-2.01 1.5-3.34z"
981
+ }));
982
+ }
983
+
984
+ // 已读/勾选图标
985
+ function CheckIcon(props) {
986
+ return /*#__PURE__*/React.createElement("svg", _extends({
987
+ viewBox: "0 0 24 24",
988
+ style: iconStyle
989
+ }, props), /*#__PURE__*/React.createElement("path", {
990
+ d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"
991
+ }));
992
+ }
993
+
994
+ // 双勾选图标
995
+ function CheckDoubleIcon(props) {
996
+ return /*#__PURE__*/React.createElement("svg", _extends({
997
+ viewBox: "0 0 24 24",
998
+ style: iconStyle
999
+ }, props), /*#__PURE__*/React.createElement("path", {
1000
+ d: "M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z"
1001
+ }));
1002
+ }
1003
+
1004
+ // 刷新图标
1005
+ function RefreshIcon(props) {
1006
+ return /*#__PURE__*/React.createElement("svg", _extends({
1007
+ viewBox: "0 0 24 24",
1008
+ style: iconStyle
1009
+ }, props), /*#__PURE__*/React.createElement("path", {
1010
+ d: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
1011
+ }));
1012
+ }
1013
+
1014
+ // 关闭图标
1015
+ function CloseIcon(props) {
1016
+ return /*#__PURE__*/React.createElement("svg", _extends({
1017
+ viewBox: "0 0 24 24",
1018
+ style: iconStyle
1019
+ }, props), /*#__PURE__*/React.createElement("path", {
1020
+ d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
1021
+ }));
1022
+ }
1023
+
1024
+ // 筛选图标
1025
+ function FilterIcon(props) {
1026
+ return /*#__PURE__*/React.createElement("svg", _extends({
1027
+ viewBox: "0 0 24 24",
1028
+ style: iconStyle
1029
+ }, props), /*#__PURE__*/React.createElement("path", {
1030
+ d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"
1031
+ }));
1032
+ }
1033
+
1034
+ /**
1035
+ * Notification SDK - 通知列表组件
1036
+ * 显示通知消息列表
1037
+ */
1038
+
1039
+ function NotificationList({
1040
+ showHeader = true,
1041
+ showFilters = true,
1042
+ showMarkAllRead = true,
1043
+ emptyComponent,
1044
+ className = '',
1045
+ onItemClick,
1046
+ maxHeight
1047
+ }) {
1048
+ const {
1049
+ notifications,
1050
+ loading,
1051
+ error,
1052
+ currentFilter,
1053
+ pagination,
1054
+ fetchNotifications,
1055
+ loadMore,
1056
+ markAsRead,
1057
+ markAllAsRead,
1058
+ deleteNotification,
1059
+ archiveNotification,
1060
+ setFilter,
1061
+ refresh
1062
+ } = useNotification();
1063
+ React.useEffect(() => {
1064
+ fetchNotifications();
1065
+ }, [currentFilter]);
1066
+ const handleScroll = React.useCallback(e => {
1067
+ const {
1068
+ scrollTop,
1069
+ scrollHeight,
1070
+ clientHeight
1071
+ } = e.target;
1072
+ if (scrollHeight - scrollTop <= clientHeight + 100 && pagination.hasMore && !loading) {
1073
+ loadMore();
1074
+ }
1075
+ }, [pagination.hasMore, loading, loadMore]);
1076
+ const filters = [{
1077
+ value: 'unread',
1078
+ label: t('unread')
1079
+ }, {
1080
+ value: 'all',
1081
+ label: t('all')
1082
+ }, {
1083
+ value: 'read',
1084
+ label: t('read')
1085
+ }, {
1086
+ value: 'archived',
1087
+ label: t('archived')
1088
+ }];
1089
+ const typeFilters = [{
1090
+ value: null,
1091
+ label: t('all')
1092
+ }, {
1093
+ value: 'system',
1094
+ label: t('system')
1095
+ }, {
1096
+ value: 'announcement',
1097
+ label: t('announcement')
1098
+ }, {
1099
+ value: 'transaction',
1100
+ label: t('transaction')
1101
+ }, {
1102
+ value: 'activity',
1103
+ label: t('activity')
1104
+ }];
1105
+ const isEmpty = !loading && notifications.length === 0;
1106
+ return /*#__PURE__*/React.createElement("div", {
1107
+ className: `eco-notification-list ${className}`
1108
+ }, showHeader && /*#__PURE__*/React.createElement("div", {
1109
+ className: "eco-notification-list__header"
1110
+ }, /*#__PURE__*/React.createElement("h3", {
1111
+ className: "eco-notification-list__title"
1112
+ }, t('notifications')), /*#__PURE__*/React.createElement("div", {
1113
+ className: "eco-notification-list__actions"
1114
+ }, showMarkAllRead && /*#__PURE__*/React.createElement("button", {
1115
+ className: "eco-notification-list__action-btn",
1116
+ onClick: () => markAllAsRead(),
1117
+ title: t('markAllAsRead')
1118
+ }, /*#__PURE__*/React.createElement(CheckDoubleIcon, null)), /*#__PURE__*/React.createElement("button", {
1119
+ className: "eco-notification-list__action-btn",
1120
+ onClick: refresh,
1121
+ title: t('refresh')
1122
+ }, /*#__PURE__*/React.createElement(RefreshIcon, {
1123
+ className: loading ? 'eco-spin' : ''
1124
+ })))), showFilters && /*#__PURE__*/React.createElement("div", {
1125
+ className: "eco-notification-list__filters"
1126
+ }, /*#__PURE__*/React.createElement("div", {
1127
+ className: "eco-notification-list__filter-group"
1128
+ }, filters.map(filter => /*#__PURE__*/React.createElement("button", {
1129
+ key: filter.value,
1130
+ className: `eco-notification-list__filter-btn ${currentFilter.status === filter.value ? 'active' : ''}`,
1131
+ onClick: () => setFilter({
1132
+ status: filter.value
1133
+ })
1134
+ }, filter.label))), /*#__PURE__*/React.createElement("div", {
1135
+ className: "eco-notification-list__type-filter"
1136
+ }, /*#__PURE__*/React.createElement(FilterIcon, null), /*#__PURE__*/React.createElement("select", {
1137
+ value: currentFilter.type || '',
1138
+ onChange: e => setFilter({
1139
+ type: e.target.value || null
1140
+ })
1141
+ }, typeFilters.map(filter => /*#__PURE__*/React.createElement("option", {
1142
+ key: filter.value || 'all',
1143
+ value: filter.value || ''
1144
+ }, filter.label))))), /*#__PURE__*/React.createElement("div", {
1145
+ className: "eco-notification-list__content",
1146
+ onScroll: handleScroll,
1147
+ style: maxHeight ? {
1148
+ maxHeight,
1149
+ overflowY: 'auto'
1150
+ } : undefined
1151
+ }, error && /*#__PURE__*/React.createElement("div", {
1152
+ className: "eco-notification-list__error"
1153
+ }, /*#__PURE__*/React.createElement("span", null, error), /*#__PURE__*/React.createElement("button", {
1154
+ onClick: refresh
1155
+ }, t('tryAgain'))), isEmpty ? emptyComponent || /*#__PURE__*/React.createElement("div", {
1156
+ className: "eco-notification-list__empty"
1157
+ }, /*#__PURE__*/React.createElement("p", null, currentFilter.status === 'unread' ? t('noUnreadMessages') : t('noNotifications'))) : /*#__PURE__*/React.createElement(React.Fragment, null, notifications.map(notification => /*#__PURE__*/React.createElement(NotificationItem, {
1158
+ key: notification.id,
1159
+ notification: notification,
1160
+ onClick: () => {
1161
+ if (notification.status === 'unread') {
1162
+ markAsRead(notification.id);
1163
+ }
1164
+ onItemClick?.(notification);
1165
+ },
1166
+ onDelete: () => deleteNotification(notification.id),
1167
+ onArchive: () => archiveNotification(notification.id)
1168
+ })), loading && /*#__PURE__*/React.createElement("div", {
1169
+ className: "eco-notification-list__loading"
1170
+ }, /*#__PURE__*/React.createElement("span", {
1171
+ className: "eco-loading-spinner"
1172
+ })), !pagination.hasMore && notifications.length > 0 && /*#__PURE__*/React.createElement("div", {
1173
+ className: "eco-notification-list__end"
1174
+ }, t('noMoreMessages')))));
1175
+ }
1176
+
1177
+ /**
1178
+ * Notification SDK - 类型定义
1179
+ * 通知消息中心类型常量
1180
+ */
1181
+
1182
+ // 消息类型
1183
+ const MessageType = {
1184
+ SYSTEM: 'system',
1185
+ // 系统通知
1186
+ ANNOUNCEMENT: 'announcement',
1187
+ // 公告
1188
+ PERSONAL: 'personal',
1189
+ // 私信
1190
+ TRANSACTION: 'transaction',
1191
+ // 交易通知
1192
+ ACTIVITY: 'activity',
1193
+ // 活动通知
1194
+ REMINDER: 'reminder',
1195
+ // 提醒
1196
+ ALERT: 'alert',
1197
+ // 告警
1198
+ MARKETING: 'marketing' // 营销消息
1199
+ };
1200
+
1201
+ // 消息状态
1202
+ const MessageStatus = {
1203
+ UNREAD: 'unread',
1204
+ READ: 'read',
1205
+ ARCHIVED: 'archived',
1206
+ DELETED: 'deleted'
1207
+ };
1208
+
1209
+ // 消息优先级
1210
+ const MessagePriority = {
1211
+ LOW: 'low',
1212
+ NORMAL: 'normal',
1213
+ HIGH: 'high',
1214
+ URGENT: 'urgent'
1215
+ };
1216
+
1217
+ // 通知渠道
1218
+ const NotificationChannel = {
1219
+ IN_APP: 'in_app',
1220
+ // 站内信
1221
+ PUSH: 'push',
1222
+ // APP推送
1223
+ EMAIL: 'email',
1224
+ // 邮件
1225
+ SMS: 'sms',
1226
+ // 短信
1227
+ WEBSOCKET: 'websocket',
1228
+ // WebSocket实时
1229
+ WEBHOOK: 'webhook' // Webhook
1230
+ };
1231
+
1232
+ // 公告范围
1233
+ const AnnouncementScope = {
1234
+ ALL: 'all',
1235
+ // 全体用户
1236
+ SEGMENT: 'segment',
1237
+ // 用户分群
1238
+ VIP: 'vip',
1239
+ // VIP用户
1240
+ NEW_USER: 'new_user',
1241
+ // 新用户
1242
+ ACTIVE: 'active' // 活跃用户
1243
+ };
1244
+
1245
+ // 消息模板类型
1246
+ const TemplateType = {
1247
+ WELCOME: 'welcome',
1248
+ // 欢迎消息
1249
+ VERIFICATION: 'verification',
1250
+ // 验证码
1251
+ PASSWORD_RESET: 'password_reset',
1252
+ // 密码重置
1253
+ ORDER_UPDATE: 'order_update',
1254
+ // 订单更新
1255
+ PAYMENT: 'payment',
1256
+ // 支付通知
1257
+ PROMOTION: 'promotion',
1258
+ // 促销活动
1259
+ CUSTOM: 'custom' // 自定义
1260
+ };
1261
+
1262
+ // 投递状态
1263
+ const DeliveryStatus = {
1264
+ PENDING: 'pending',
1265
+ // 待发送
1266
+ SENDING: 'sending',
1267
+ // 发送中
1268
+ DELIVERED: 'delivered',
1269
+ // 已送达
1270
+ FAILED: 'failed',
1271
+ // 发送失败
1272
+ BOUNCED: 'bounced' // 被退回
1273
+ };
1274
+ const NotificationType = MessageType;
1275
+ const NotificationStatus = MessageStatus;
1276
+ const NotificationPriority = MessagePriority;
1277
+
1278
+ /**
1279
+ * Notification SDK - 通知设置面板
1280
+ * 用户通知偏好设置
1281
+ */
1282
+
1283
+ function NotificationSettings({
1284
+ showChannels = true,
1285
+ showTypes = true,
1286
+ showQuietHours = true,
1287
+ onSave,
1288
+ className = ''
1289
+ }) {
1290
+ const {
1291
+ preferences,
1292
+ fetchPreferences,
1293
+ updatePreferences,
1294
+ loading
1295
+ } = useNotification();
1296
+ const [localPreferences, setLocalPreferences] = React.useState(null);
1297
+ const [saving, setSaving] = React.useState(false);
1298
+ const [quietHoursEnabled, setQuietHoursEnabled] = React.useState(false);
1299
+ const [quietHoursStart, setQuietHoursStart] = React.useState('22:00');
1300
+ const [quietHoursEnd, setQuietHoursEnd] = React.useState('08:00');
1301
+ React.useEffect(() => {
1302
+ fetchPreferences();
1303
+ }, [fetchPreferences]);
1304
+ React.useEffect(() => {
1305
+ if (preferences) {
1306
+ setLocalPreferences(preferences);
1307
+ if (preferences.quiet_hours) {
1308
+ setQuietHoursEnabled(preferences.quiet_hours.enabled);
1309
+ setQuietHoursStart(preferences.quiet_hours.start || '22:00');
1310
+ setQuietHoursEnd(preferences.quiet_hours.end || '08:00');
1311
+ }
1312
+ }
1313
+ }, [preferences]);
1314
+ const handleChannelToggle = channel => {
1315
+ setLocalPreferences(prev => ({
1316
+ ...prev,
1317
+ channels: {
1318
+ ...prev?.channels,
1319
+ [channel]: !prev?.channels?.[channel]
1320
+ }
1321
+ }));
1322
+ };
1323
+ const handleTypeToggle = type => {
1324
+ setLocalPreferences(prev => ({
1325
+ ...prev,
1326
+ types: {
1327
+ ...prev?.types,
1328
+ [type]: !prev?.types?.[type]
1329
+ }
1330
+ }));
1331
+ };
1332
+ const handleSave = async () => {
1333
+ setSaving(true);
1334
+ try {
1335
+ const updatedPreferences = {
1336
+ ...localPreferences,
1337
+ quiet_hours: {
1338
+ enabled: quietHoursEnabled,
1339
+ start: quietHoursStart,
1340
+ end: quietHoursEnd
1341
+ }
1342
+ };
1343
+ await updatePreferences(updatedPreferences);
1344
+ onSave?.(updatedPreferences);
1345
+ } catch (err) {
1346
+ console.error('Failed to save preferences:', err);
1347
+ } finally {
1348
+ setSaving(false);
1349
+ }
1350
+ };
1351
+ if (loading && !localPreferences) {
1352
+ return /*#__PURE__*/React.createElement("div", {
1353
+ className: `eco-notification-settings ${className}`
1354
+ }, /*#__PURE__*/React.createElement("div", {
1355
+ className: "eco-notification-settings__loading"
1356
+ }, /*#__PURE__*/React.createElement("span", {
1357
+ className: "eco-loading-spinner"
1358
+ })));
1359
+ }
1360
+ const channels = [{
1361
+ key: NotificationChannel.IN_APP,
1362
+ label: t('inApp')
1363
+ }, {
1364
+ key: NotificationChannel.PUSH,
1365
+ label: t('push')
1366
+ }, {
1367
+ key: NotificationChannel.EMAIL,
1368
+ label: t('email')
1369
+ }, {
1370
+ key: NotificationChannel.SMS,
1371
+ label: t('sms')
1372
+ }];
1373
+ const types = [{
1374
+ key: MessageType.SYSTEM,
1375
+ label: t('notifyOnSystem')
1376
+ }, {
1377
+ key: MessageType.TRANSACTION,
1378
+ label: t('notifyOnTransaction')
1379
+ }, {
1380
+ key: MessageType.ACTIVITY,
1381
+ label: t('notifyOnActivity')
1382
+ }, {
1383
+ key: MessageType.MARKETING,
1384
+ label: t('notifyOnMarketing')
1385
+ }];
1386
+ return /*#__PURE__*/React.createElement("div", {
1387
+ className: `eco-notification-settings ${className}`
1388
+ }, /*#__PURE__*/React.createElement("h3", {
1389
+ className: "eco-notification-settings__title"
1390
+ }, t('notificationSettings')), showChannels && /*#__PURE__*/React.createElement("section", {
1391
+ className: "eco-notification-settings__section"
1392
+ }, /*#__PURE__*/React.createElement("h4", null, t('pushNotifications')), /*#__PURE__*/React.createElement("div", {
1393
+ className: "eco-notification-settings__options"
1394
+ }, channels.map(({
1395
+ key,
1396
+ label
1397
+ }) => /*#__PURE__*/React.createElement("label", {
1398
+ key: key,
1399
+ className: "eco-notification-settings__option"
1400
+ }, /*#__PURE__*/React.createElement("input", {
1401
+ type: "checkbox",
1402
+ checked: localPreferences?.channels?.[key] ?? true,
1403
+ onChange: () => handleChannelToggle(key)
1404
+ }), /*#__PURE__*/React.createElement("span", {
1405
+ className: "eco-notification-settings__checkbox"
1406
+ }), /*#__PURE__*/React.createElement("span", null, label))))), showTypes && /*#__PURE__*/React.createElement("section", {
1407
+ className: "eco-notification-settings__section"
1408
+ }, /*#__PURE__*/React.createElement("h4", null, t('preferences')), /*#__PURE__*/React.createElement("div", {
1409
+ className: "eco-notification-settings__options"
1410
+ }, types.map(({
1411
+ key,
1412
+ label
1413
+ }) => /*#__PURE__*/React.createElement("label", {
1414
+ key: key,
1415
+ className: "eco-notification-settings__option"
1416
+ }, /*#__PURE__*/React.createElement("input", {
1417
+ type: "checkbox",
1418
+ checked: localPreferences?.types?.[key] ?? true,
1419
+ onChange: () => handleTypeToggle(key)
1420
+ }), /*#__PURE__*/React.createElement("span", {
1421
+ className: "eco-notification-settings__checkbox"
1422
+ }), /*#__PURE__*/React.createElement("span", null, label))))), showQuietHours && /*#__PURE__*/React.createElement("section", {
1423
+ className: "eco-notification-settings__section"
1424
+ }, /*#__PURE__*/React.createElement("h4", null, t('doNotDisturb')), /*#__PURE__*/React.createElement("p", {
1425
+ className: "eco-notification-settings__desc"
1426
+ }, t('doNotDisturbDesc')), /*#__PURE__*/React.createElement("label", {
1427
+ className: "eco-notification-settings__option"
1428
+ }, /*#__PURE__*/React.createElement("input", {
1429
+ type: "checkbox",
1430
+ checked: quietHoursEnabled,
1431
+ onChange: e => setQuietHoursEnabled(e.target.checked)
1432
+ }), /*#__PURE__*/React.createElement("span", {
1433
+ className: "eco-notification-settings__checkbox"
1434
+ }), /*#__PURE__*/React.createElement("span", null, t('quietHours'))), quietHoursEnabled && /*#__PURE__*/React.createElement("div", {
1435
+ className: "eco-notification-settings__time-range"
1436
+ }, /*#__PURE__*/React.createElement("input", {
1437
+ type: "time",
1438
+ value: quietHoursStart,
1439
+ onChange: e => setQuietHoursStart(e.target.value)
1440
+ }), /*#__PURE__*/React.createElement("span", null, "-"), /*#__PURE__*/React.createElement("input", {
1441
+ type: "time",
1442
+ value: quietHoursEnd,
1443
+ onChange: e => setQuietHoursEnd(e.target.value)
1444
+ }))), /*#__PURE__*/React.createElement("div", {
1445
+ className: "eco-notification-settings__actions"
1446
+ }, /*#__PURE__*/React.createElement("button", {
1447
+ className: "eco-notification-settings__save-btn",
1448
+ onClick: handleSave,
1449
+ disabled: saving
1450
+ }, saving ? /*#__PURE__*/React.createElement("span", {
1451
+ className: "eco-loading-spinner"
1452
+ }) : t('settingsSaved').replace('已', ''))));
1453
+ }
1454
+
1455
+ /**
1456
+ * Notification SDK - Toast 通知组件
1457
+ * 实时推送的弹窗通知
1458
+ */
1459
+
1460
+
1461
+ // Toast 管理器
1462
+ const toastQueue = [];
1463
+ let updateCallback = null;
1464
+ function removeToast(id) {
1465
+ const index = toastQueue.findIndex(t => t.id === id);
1466
+ if (index > -1) {
1467
+ toastQueue.splice(index, 1);
1468
+ updateCallback?.([...toastQueue]);
1469
+ }
1470
+ }
1471
+
1472
+ // 单个 Toast 组件
1473
+ function ToastItem({
1474
+ toast,
1475
+ onRemove
1476
+ }) {
1477
+ const [exiting, setExiting] = React.useState(false);
1478
+ const {
1479
+ notification,
1480
+ onClick
1481
+ } = toast;
1482
+ const handleRemove = React.useCallback(() => {
1483
+ setExiting(true);
1484
+ setTimeout(() => {
1485
+ onRemove(toast.id);
1486
+ }, 200);
1487
+ }, [toast.id, onRemove]);
1488
+ const handleClick = () => {
1489
+ onClick?.(notification);
1490
+ handleRemove();
1491
+ };
1492
+ return /*#__PURE__*/React.createElement("div", {
1493
+ className: `eco-toast ${exiting ? 'eco-toast--exiting' : ''}`,
1494
+ onClick: handleClick
1495
+ }, /*#__PURE__*/React.createElement("div", {
1496
+ className: "eco-toast__icon"
1497
+ }, /*#__PURE__*/React.createElement(CheckIcon, null)), /*#__PURE__*/React.createElement("div", {
1498
+ className: "eco-toast__content"
1499
+ }, /*#__PURE__*/React.createElement("div", {
1500
+ className: "eco-toast__title"
1501
+ }, notification.title || t('newNotification')), notification.content && /*#__PURE__*/React.createElement("div", {
1502
+ className: "eco-toast__body"
1503
+ }, notification.content.substring(0, 100))), /*#__PURE__*/React.createElement("button", {
1504
+ className: "eco-toast__close",
1505
+ onClick: e => {
1506
+ e.stopPropagation();
1507
+ handleRemove();
1508
+ }
1509
+ }, /*#__PURE__*/React.createElement(CloseIcon, null)));
1510
+ }
1511
+
1512
+ // Toast 容器
1513
+ function NotificationToast({
1514
+ position = 'top-right',
1515
+ maxToasts = 5
1516
+ }) {
1517
+ const [toasts, setToasts] = React.useState([]);
1518
+ React.useEffect(() => {
1519
+ updateCallback = setToasts;
1520
+ return () => {
1521
+ updateCallback = null;
1522
+ };
1523
+ }, []);
1524
+ const visibleToasts = toasts.filter(t => t.position === position).slice(0, maxToasts);
1525
+ if (visibleToasts.length === 0) return null;
1526
+ return /*#__PURE__*/React.createElement("div", {
1527
+ className: `eco-toast-container eco-toast-container--${position}`
1528
+ }, visibleToasts.map(toast => /*#__PURE__*/React.createElement(ToastItem, {
1529
+ key: toast.id,
1530
+ toast: toast,
1531
+ onRemove: removeToast
1532
+ })));
1533
+ }
1534
+
1535
+ /**
1536
+ * Notification SDK - 通知徽章按钮
1537
+ * 导航栏中的通知图标,带未读数量
1538
+ */
1539
+
1540
+ function NotificationBadge({
1541
+ onClick,
1542
+ showCount = true,
1543
+ maxCount = 99,
1544
+ className = '',
1545
+ size = 'medium',
1546
+ color
1547
+ }) {
1548
+ const {
1549
+ unreadCount,
1550
+ fetchUnreadCount
1551
+ } = useNotification();
1552
+ React.useEffect(() => {
1553
+ fetchUnreadCount();
1554
+ }, [fetchUnreadCount]);
1555
+ const sizeClass = {
1556
+ small: 'eco-notification-badge--small',
1557
+ medium: 'eco-notification-badge--medium',
1558
+ large: 'eco-notification-badge--large'
1559
+ }[size] || 'eco-notification-badge--medium';
1560
+ const displayCount = unreadCount > maxCount ? `${maxCount}+` : unreadCount;
1561
+ const hasUnread = unreadCount > 0;
1562
+ return /*#__PURE__*/React.createElement("button", {
1563
+ className: `eco-notification-badge ${sizeClass} ${className}`,
1564
+ onClick: onClick,
1565
+ title: hasUnread ? t('unreadCount', {
1566
+ n: unreadCount
1567
+ }) : t('notifications'),
1568
+ "aria-label": t('notifications'),
1569
+ style: color ? {
1570
+ color
1571
+ } : undefined
1572
+ }, hasUnread ? /*#__PURE__*/React.createElement(BellDotIcon, null) : /*#__PURE__*/React.createElement(BellIcon, null), showCount && hasUnread && /*#__PURE__*/React.createElement("span", {
1573
+ className: "eco-notification-badge__count"
1574
+ }, displayCount));
1575
+ }
1576
+
1577
+ /**
1578
+ * Notification SDK - 公告横幅组件
1579
+ * 页面顶部的公告条
1580
+ */
1581
+
1582
+ function AnnouncementBanner({
1583
+ autoFetch = true,
1584
+ autoHideDelay = 0,
1585
+ showClose = true,
1586
+ position = 'top',
1587
+ onAnnouncementClick,
1588
+ className = ''
1589
+ }) {
1590
+ const {
1591
+ announcements,
1592
+ fetchAnnouncements
1593
+ } = useNotification();
1594
+ const [currentIndex, setCurrentIndex] = React.useState(0);
1595
+ const [visible, setVisible] = React.useState(true);
1596
+ const [dismissed, setDismissed] = React.useState([]);
1597
+ React.useEffect(() => {
1598
+ if (autoFetch) {
1599
+ fetchAnnouncements({
1600
+ pinned: true
1601
+ });
1602
+ }
1603
+ }, [autoFetch, fetchAnnouncements]);
1604
+
1605
+ // 自动切换公告
1606
+ React.useEffect(() => {
1607
+ if (announcements.length > 1) {
1608
+ const timer = setInterval(() => {
1609
+ setCurrentIndex(prev => (prev + 1) % announcements.length);
1610
+ }, 5000);
1611
+ return () => clearInterval(timer);
1612
+ }
1613
+ }, [announcements.length]);
1614
+
1615
+ // 自动隐藏
1616
+ React.useEffect(() => {
1617
+ if (autoHideDelay > 0) {
1618
+ const timer = setTimeout(() => {
1619
+ setVisible(false);
1620
+ }, autoHideDelay);
1621
+ return () => clearTimeout(timer);
1622
+ }
1623
+ }, [autoHideDelay]);
1624
+
1625
+ // 过滤已关闭的公告
1626
+ const visibleAnnouncements = announcements.filter(a => !dismissed.includes(a.id));
1627
+ if (!visible || visibleAnnouncements.length === 0) {
1628
+ return null;
1629
+ }
1630
+ const current = visibleAnnouncements[currentIndex % visibleAnnouncements.length];
1631
+ const handleDismiss = id => {
1632
+ setDismissed(prev => [...prev, id]);
1633
+ // 保存到localStorage
1634
+ if (typeof localStorage !== 'undefined') {
1635
+ const key = 'eco-dismissed-announcements';
1636
+ const stored = JSON.parse(localStorage.getItem(key) || '[]');
1637
+ stored.push(id);
1638
+ localStorage.setItem(key, JSON.stringify(stored));
1639
+ }
1640
+ };
1641
+ const getPriorityClass = priority => {
1642
+ return {
1643
+ high: 'eco-announcement-banner--high',
1644
+ urgent: 'eco-announcement-banner--urgent'
1645
+ }[priority] || '';
1646
+ };
1647
+ return /*#__PURE__*/React.createElement("div", {
1648
+ className: `eco-announcement-banner eco-announcement-banner--${position} ${getPriorityClass(current.priority)} ${className}`
1649
+ }, /*#__PURE__*/React.createElement("div", {
1650
+ className: "eco-announcement-banner__content"
1651
+ }, /*#__PURE__*/React.createElement(MegaphoneIcon, {
1652
+ className: "eco-announcement-banner__icon"
1653
+ }), /*#__PURE__*/React.createElement("div", {
1654
+ className: "eco-announcement-banner__text",
1655
+ onClick: () => onAnnouncementClick?.(current)
1656
+ }, /*#__PURE__*/React.createElement("span", {
1657
+ className: "eco-announcement-banner__title"
1658
+ }, current.title), current.content && /*#__PURE__*/React.createElement("span", {
1659
+ className: "eco-announcement-banner__desc"
1660
+ }, current.content.substring(0, 100), current.content.length > 100 ? '...' : ''))), visibleAnnouncements.length > 1 && /*#__PURE__*/React.createElement("div", {
1661
+ className: "eco-announcement-banner__indicators"
1662
+ }, visibleAnnouncements.map((_, index) => /*#__PURE__*/React.createElement("span", {
1663
+ key: index,
1664
+ className: `eco-announcement-banner__indicator ${index === currentIndex % visibleAnnouncements.length ? 'active' : ''}`,
1665
+ onClick: () => setCurrentIndex(index)
1666
+ }))), showClose && /*#__PURE__*/React.createElement("button", {
1667
+ className: "eco-announcement-banner__close",
1668
+ onClick: () => handleDismiss(current.id),
1669
+ title: t('close')
1670
+ }, /*#__PURE__*/React.createElement(CloseIcon, null)));
1671
+ }
1672
+
1673
+ function useNotifications(initial = []) {
1674
+ const [notifications, setNotifications] = React.useState(initial);
1675
+ const add = React.useCallback(n => setNotifications(prev => [{
1676
+ ...n,
1677
+ id: n.id || Date.now(),
1678
+ read: false,
1679
+ time: n.time || 'Just now'
1680
+ }, ...prev]), []);
1681
+ const read = React.useCallback(id => setNotifications(prev => prev.map(n => n.id === id ? {
1682
+ ...n,
1683
+ read: true
1684
+ } : n)), []);
1685
+ const readAll = React.useCallback(() => setNotifications(prev => prev.map(n => ({
1686
+ ...n,
1687
+ read: true
1688
+ }))), []);
1689
+ const remove = React.useCallback(id => setNotifications(prev => prev.filter(n => n.id !== id)), []);
1690
+ const clear = React.useCallback(() => setNotifications([]), []);
1691
+ const unreadCount = React.useMemo(() => notifications.filter(n => !n.read).length, [notifications]);
1692
+ return {
1693
+ notifications,
1694
+ add,
1695
+ read,
1696
+ readAll,
1697
+ remove,
1698
+ clear,
1699
+ unreadCount
1700
+ };
1701
+ }
1702
+
1703
+ exports.AnnouncementBanner = AnnouncementBanner;
1704
+ exports.AnnouncementScope = AnnouncementScope;
1705
+ exports.DeliveryStatus = DeliveryStatus;
1706
+ exports.MessagePriority = MessagePriority;
1707
+ exports.MessageStatus = MessageStatus;
1708
+ exports.MessageType = MessageType;
1709
+ exports.NotificationApiClient = NotificationApiClient;
1710
+ exports.NotificationBadge = NotificationBadge;
1711
+ exports.NotificationBell = NotificationBell;
1712
+ exports.NotificationCenter = NotificationCenter;
1713
+ exports.NotificationChannel = NotificationChannel;
1714
+ exports.NotificationItem = NotificationItem;
1715
+ exports.NotificationList = NotificationList;
1716
+ exports.NotificationPriority = NotificationPriority;
1717
+ exports.NotificationProvider = NotificationProvider;
1718
+ exports.NotificationSettings = NotificationSettings;
1719
+ exports.NotificationStatus = NotificationStatus;
1720
+ exports.NotificationToast = NotificationToast;
1721
+ exports.NotificationType = NotificationType;
1722
+ exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
1723
+ exports.TemplateType = TemplateType;
1724
+ exports.getLanguage = getLanguage;
1725
+ exports.messages = messages;
1726
+ exports.notificationApi = notificationApi;
1727
+ exports.setLanguage = setLanguage;
1728
+ exports.t = t;
1729
+ exports.useNotificationContext = useNotification;
1730
+ exports.useNotifications = useNotifications;
1731
+ //# sourceMappingURL=index.cjs.map