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