@quantabit/push-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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/index.cjs +2142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +2094 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/styles.css.map +1 -0
- package/package.json +89 -0
- package/types/index.d.ts +48 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sdkConfig = require('@quantabit/sdk-config');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
|
|
6
|
+
/** 浏览器原生 Web Push (Notification API) */
|
|
7
|
+
class WebPushAdapter {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.name = 'webpush';
|
|
10
|
+
this._cbs = {};
|
|
11
|
+
this.swPath = config.serviceWorkerPath || '/sw.js';
|
|
12
|
+
}
|
|
13
|
+
onMessage(cb) {
|
|
14
|
+
this._cbs.message = cb;
|
|
15
|
+
}
|
|
16
|
+
onConnect(cb) {
|
|
17
|
+
this._cbs.connect = cb;
|
|
18
|
+
}
|
|
19
|
+
onDisconnect(cb) {
|
|
20
|
+
this._cbs.disconnect = cb;
|
|
21
|
+
}
|
|
22
|
+
onError(cb) {
|
|
23
|
+
this._cbs.error = cb;
|
|
24
|
+
}
|
|
25
|
+
async connect() {
|
|
26
|
+
if (typeof window === 'undefined') return;
|
|
27
|
+
const perm = await Notification.requestPermission();
|
|
28
|
+
if (perm !== 'granted') {
|
|
29
|
+
this._cbs.error?.(new Error('Permission denied'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if ('serviceWorker' in navigator) {
|
|
33
|
+
try {
|
|
34
|
+
await navigator.serviceWorker.register(this.swPath);
|
|
35
|
+
navigator.serviceWorker.addEventListener('message', e => this._cbs.message?.(e.data));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
this._cbs.error?.(e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this._cbs.connect?.();
|
|
41
|
+
}
|
|
42
|
+
async disconnect() {
|
|
43
|
+
this._cbs.disconnect?.();
|
|
44
|
+
}
|
|
45
|
+
async subscribe(topic) {/* Web Push subscription via PushManager */}
|
|
46
|
+
async send(msg) {
|
|
47
|
+
if (typeof Notification !== 'undefined') new Notification(msg.title || 'Notification', {
|
|
48
|
+
body: msg.body || msg,
|
|
49
|
+
icon: msg.icon
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** WebSocket 自研推送 — 最常用于自建 IM */
|
|
55
|
+
class WebSocketAdapter {
|
|
56
|
+
constructor(config = {}) {
|
|
57
|
+
this.name = 'websocket';
|
|
58
|
+
if (!config.url) throw new Error('[WebSocketAdapter] config.url is required');
|
|
59
|
+
this.url = config.url;
|
|
60
|
+
this.protocols = config.protocols;
|
|
61
|
+
this._ws = null;
|
|
62
|
+
this._cbs = {};
|
|
63
|
+
this._pingInterval = config.pingInterval || 30000;
|
|
64
|
+
this._pingTimer = null;
|
|
65
|
+
}
|
|
66
|
+
onMessage(cb) {
|
|
67
|
+
this._cbs.message = cb;
|
|
68
|
+
}
|
|
69
|
+
onConnect(cb) {
|
|
70
|
+
this._cbs.connect = cb;
|
|
71
|
+
}
|
|
72
|
+
onDisconnect(cb) {
|
|
73
|
+
this._cbs.disconnect = cb;
|
|
74
|
+
}
|
|
75
|
+
onError(cb) {
|
|
76
|
+
this._cbs.error = cb;
|
|
77
|
+
}
|
|
78
|
+
async connect() {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
this._ws = new WebSocket(this.url, this.protocols);
|
|
81
|
+
this._ws.onopen = () => {
|
|
82
|
+
this._cbs.connect?.();
|
|
83
|
+
this._startPing();
|
|
84
|
+
resolve();
|
|
85
|
+
};
|
|
86
|
+
this._ws.onmessage = e => {
|
|
87
|
+
try {
|
|
88
|
+
const d = JSON.parse(e.data);
|
|
89
|
+
if (d.type === 'pong') return;
|
|
90
|
+
this._cbs.message?.(d);
|
|
91
|
+
} catch {
|
|
92
|
+
this._cbs.message?.(e.data);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
this._ws.onclose = () => {
|
|
96
|
+
this._stopPing();
|
|
97
|
+
this._cbs.disconnect?.();
|
|
98
|
+
};
|
|
99
|
+
this._ws.onerror = e => {
|
|
100
|
+
this._cbs.error?.(e);
|
|
101
|
+
reject(e);
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async disconnect() {
|
|
106
|
+
this._stopPing();
|
|
107
|
+
this._ws?.close();
|
|
108
|
+
}
|
|
109
|
+
async subscribe(topic) {
|
|
110
|
+
this.send({
|
|
111
|
+
type: 'subscribe',
|
|
112
|
+
topic
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async unsubscribe(topic) {
|
|
116
|
+
this.send({
|
|
117
|
+
type: 'unsubscribe',
|
|
118
|
+
topic
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async send(msg) {
|
|
122
|
+
if (this._ws?.readyState === WebSocket.OPEN) this._ws.send(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
123
|
+
}
|
|
124
|
+
_startPing() {
|
|
125
|
+
this._pingTimer = setInterval(() => this.send({
|
|
126
|
+
type: 'ping'
|
|
127
|
+
}), this._pingInterval);
|
|
128
|
+
}
|
|
129
|
+
_stopPing() {
|
|
130
|
+
clearInterval(this._pingTimer);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Server-Sent Events 推送 — 单向实时流 */
|
|
135
|
+
class SSEAdapter {
|
|
136
|
+
constructor(config = {}) {
|
|
137
|
+
this.name = 'sse';
|
|
138
|
+
this.url = config.url || '/api/events';
|
|
139
|
+
this._source = null;
|
|
140
|
+
this._cbs = {};
|
|
141
|
+
}
|
|
142
|
+
onMessage(cb) {
|
|
143
|
+
this._cbs.message = cb;
|
|
144
|
+
}
|
|
145
|
+
onConnect(cb) {
|
|
146
|
+
this._cbs.connect = cb;
|
|
147
|
+
}
|
|
148
|
+
onDisconnect(cb) {
|
|
149
|
+
this._cbs.disconnect = cb;
|
|
150
|
+
}
|
|
151
|
+
onError(cb) {
|
|
152
|
+
this._cbs.error = cb;
|
|
153
|
+
}
|
|
154
|
+
async connect() {
|
|
155
|
+
this._source = new EventSource(this.url);
|
|
156
|
+
this._source.onopen = () => this._cbs.connect?.();
|
|
157
|
+
this._source.onmessage = e => {
|
|
158
|
+
try {
|
|
159
|
+
this._cbs.message?.(JSON.parse(e.data));
|
|
160
|
+
} catch {
|
|
161
|
+
this._cbs.message?.(e.data);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
this._source.onerror = e => {
|
|
165
|
+
this._cbs.error?.(e);
|
|
166
|
+
if (this._source.readyState === EventSource.CLOSED) this._cbs.disconnect?.();
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async disconnect() {
|
|
170
|
+
this._source?.close();
|
|
171
|
+
this._cbs.disconnect?.();
|
|
172
|
+
}
|
|
173
|
+
async subscribe(topic) {/* SSE 通过 URL 参数区分 topic */}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Firebase Cloud Messaging 适配器 */
|
|
177
|
+
class FCMAdapter {
|
|
178
|
+
constructor(config = {}) {
|
|
179
|
+
this.name = 'fcm';
|
|
180
|
+
this.config = config;
|
|
181
|
+
this._cbs = {};
|
|
182
|
+
}
|
|
183
|
+
onMessage(cb) {
|
|
184
|
+
this._cbs.message = cb;
|
|
185
|
+
}
|
|
186
|
+
onConnect(cb) {
|
|
187
|
+
this._cbs.connect = cb;
|
|
188
|
+
}
|
|
189
|
+
onDisconnect(cb) {
|
|
190
|
+
this._cbs.disconnect = cb;
|
|
191
|
+
}
|
|
192
|
+
onError(cb) {
|
|
193
|
+
this._cbs.error = cb;
|
|
194
|
+
}
|
|
195
|
+
async connect() {
|
|
196
|
+
try {
|
|
197
|
+
// 需要用户自行引入 firebase/messaging
|
|
198
|
+
if (typeof window !== 'undefined' && window.firebase) {
|
|
199
|
+
const messaging = window.firebase.messaging();
|
|
200
|
+
const token = await messaging.getToken({
|
|
201
|
+
vapidKey: this.config.vapidKey
|
|
202
|
+
});
|
|
203
|
+
messaging.onMessage(payload => this._cbs.message?.(payload));
|
|
204
|
+
this._cbs.connect?.();
|
|
205
|
+
return token;
|
|
206
|
+
}
|
|
207
|
+
this._cbs.connect?.();
|
|
208
|
+
} catch (e) {
|
|
209
|
+
this._cbs.error?.(e);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async disconnect() {
|
|
213
|
+
this._cbs.disconnect?.();
|
|
214
|
+
}
|
|
215
|
+
async subscribe(topic) {/* FCM 通过服务端管理 topic */}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** OneSignal 推送适配器 */
|
|
219
|
+
class OneSignalAdapter {
|
|
220
|
+
constructor(config = {}) {
|
|
221
|
+
this.name = 'onesignal';
|
|
222
|
+
this.appId = config.appId;
|
|
223
|
+
this._cbs = {};
|
|
224
|
+
}
|
|
225
|
+
onMessage(cb) {
|
|
226
|
+
this._cbs.message = cb;
|
|
227
|
+
}
|
|
228
|
+
onConnect(cb) {
|
|
229
|
+
this._cbs.connect = cb;
|
|
230
|
+
}
|
|
231
|
+
onDisconnect(cb) {
|
|
232
|
+
this._cbs.disconnect = cb;
|
|
233
|
+
}
|
|
234
|
+
onError(cb) {
|
|
235
|
+
this._cbs.error = cb;
|
|
236
|
+
}
|
|
237
|
+
async connect() {
|
|
238
|
+
if (typeof window === 'undefined') return;
|
|
239
|
+
try {
|
|
240
|
+
// 加载 OneSignal SDK (如果未加载)
|
|
241
|
+
if (!window.OneSignal) {
|
|
242
|
+
const script = document.createElement('script');
|
|
243
|
+
script.src = 'https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js';
|
|
244
|
+
script.async = true;
|
|
245
|
+
document.head.appendChild(script);
|
|
246
|
+
await new Promise(r => script.onload = r);
|
|
247
|
+
}
|
|
248
|
+
window.OneSignal = window.OneSignal || [];
|
|
249
|
+
window.OneSignal.push(() => {
|
|
250
|
+
window.OneSignal.init({
|
|
251
|
+
appId: this.appId
|
|
252
|
+
});
|
|
253
|
+
window.OneSignal.on('notificationDisplay', n => this._cbs.message?.(n));
|
|
254
|
+
this._cbs.connect?.();
|
|
255
|
+
});
|
|
256
|
+
} catch (e) {
|
|
257
|
+
this._cbs.error?.(e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async disconnect() {
|
|
261
|
+
this._cbs.disconnect?.();
|
|
262
|
+
}
|
|
263
|
+
async subscribe() {
|
|
264
|
+
window.OneSignal?.push(() => window.OneSignal.showSliderPrompt());
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 极光推送 JPush Web Push 适配器 */
|
|
269
|
+
class JPushAdapter {
|
|
270
|
+
constructor(config = {}) {
|
|
271
|
+
this.name = 'jpush';
|
|
272
|
+
this.appKey = config.appKey || config.appId;
|
|
273
|
+
this._cbs = {};
|
|
274
|
+
}
|
|
275
|
+
onMessage(cb) {
|
|
276
|
+
this._cbs.message = cb;
|
|
277
|
+
}
|
|
278
|
+
onConnect(cb) {
|
|
279
|
+
this._cbs.connect = cb;
|
|
280
|
+
}
|
|
281
|
+
onDisconnect(cb) {
|
|
282
|
+
this._cbs.disconnect = cb;
|
|
283
|
+
}
|
|
284
|
+
onError(cb) {
|
|
285
|
+
this._cbs.error = cb;
|
|
286
|
+
}
|
|
287
|
+
async connect() {
|
|
288
|
+
if (typeof window === 'undefined') return;
|
|
289
|
+
try {
|
|
290
|
+
// 极光 Web Push SDK
|
|
291
|
+
if (window.JAnalyticsInterface) {
|
|
292
|
+
window.JAnalyticsInterface.init({
|
|
293
|
+
appkey: this.appKey
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
this._cbs.connect?.();
|
|
297
|
+
} catch (e) {
|
|
298
|
+
this._cbs.error?.(e);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async disconnect() {
|
|
302
|
+
this._cbs.disconnect?.();
|
|
303
|
+
}
|
|
304
|
+
async subscribe(alias) {/* JPush 通过 alias/tag 订阅 */}
|
|
305
|
+
async send(msg) {/* JPush 通过服务端 REST API 发送 */}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Mock 适配器 — 用于开发和测试,模拟推送消息 */
|
|
309
|
+
class MockAdapter {
|
|
310
|
+
constructor(config = {}) {
|
|
311
|
+
this.name = 'mock';
|
|
312
|
+
this._cbs = {};
|
|
313
|
+
this._timer = null;
|
|
314
|
+
this.interval = config.mockInterval || 5000;
|
|
315
|
+
}
|
|
316
|
+
onMessage(cb) {
|
|
317
|
+
this._cbs.message = cb;
|
|
318
|
+
}
|
|
319
|
+
onConnect(cb) {
|
|
320
|
+
this._cbs.connect = cb;
|
|
321
|
+
}
|
|
322
|
+
onDisconnect(cb) {
|
|
323
|
+
this._cbs.disconnect = cb;
|
|
324
|
+
}
|
|
325
|
+
onError(cb) {
|
|
326
|
+
this._cbs.error = cb;
|
|
327
|
+
}
|
|
328
|
+
async connect() {
|
|
329
|
+
this._cbs.connect?.();
|
|
330
|
+
this._timer = setInterval(() => {
|
|
331
|
+
this._cbs.message?.({
|
|
332
|
+
id: Date.now(),
|
|
333
|
+
type: 'mock',
|
|
334
|
+
title: 'Test Notification',
|
|
335
|
+
body: 'This is a mock push message',
|
|
336
|
+
time: new Date().toISOString()
|
|
337
|
+
});
|
|
338
|
+
}, this.interval);
|
|
339
|
+
}
|
|
340
|
+
async disconnect() {
|
|
341
|
+
clearInterval(this._timer);
|
|
342
|
+
this._cbs.disconnect?.();
|
|
343
|
+
}
|
|
344
|
+
async subscribe() {}
|
|
345
|
+
async send(msg) {
|
|
346
|
+
setTimeout(() => this._cbs.message?.({
|
|
347
|
+
...msg,
|
|
348
|
+
echo: true
|
|
349
|
+
}), 100);
|
|
350
|
+
}
|
|
351
|
+
// 手动触发消息 (测试用)
|
|
352
|
+
emit(msg) {
|
|
353
|
+
this._cbs.message?.(msg);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* PushManager — 统一推送管理器 (隐私合规增强版)
|
|
359
|
+
* 适配器模式:所有推送服务共享同一接口,可随时切换
|
|
360
|
+
*
|
|
361
|
+
* ⚠️ 推送权限请求和订阅需要 functional 级别隐私同意
|
|
362
|
+
* 连接和消息处理不受限制(属于 essential 功能)
|
|
363
|
+
*/
|
|
364
|
+
|
|
365
|
+
// 尝试导入 ConsentManager(可选依赖,不强制)
|
|
366
|
+
let consentManager = null;
|
|
367
|
+
try {
|
|
368
|
+
const sdkConfig = require('@quantabit/sdk-config');
|
|
369
|
+
consentManager = sdkConfig.consentManager;
|
|
370
|
+
} catch {
|
|
371
|
+
// sdk-config 不可用时静默降级
|
|
372
|
+
}
|
|
373
|
+
const ADAPTERS = {
|
|
374
|
+
webpush: WebPushAdapter,
|
|
375
|
+
websocket: WebSocketAdapter,
|
|
376
|
+
sse: SSEAdapter,
|
|
377
|
+
fcm: FCMAdapter,
|
|
378
|
+
onesignal: OneSignalAdapter,
|
|
379
|
+
jpush: JPushAdapter,
|
|
380
|
+
mock: MockAdapter
|
|
381
|
+
};
|
|
382
|
+
class PushManager {
|
|
383
|
+
constructor(config = {}) {
|
|
384
|
+
this.config = config;
|
|
385
|
+
this.debug = config.debug || false;
|
|
386
|
+
this._listeners = new Map();
|
|
387
|
+
this._middleware = [];
|
|
388
|
+
this._connected = false;
|
|
389
|
+
this._messageQueue = [];
|
|
390
|
+
this._retryCount = 0;
|
|
391
|
+
this._maxRetries = config.maxRetries || 5;
|
|
392
|
+
this._retryDelay = config.retryDelay || 3000;
|
|
393
|
+
this._initAdapter(config);
|
|
394
|
+
}
|
|
395
|
+
_initAdapter(config) {
|
|
396
|
+
if (typeof config.adapter === 'string') {
|
|
397
|
+
const Cls = ADAPTERS[config.adapter];
|
|
398
|
+
if (!Cls) throw new Error(`[PushManager] 未知适配器: ${config.adapter}, 可选: ${Object.keys(ADAPTERS).join(', ')}`);
|
|
399
|
+
this._adapter = new Cls(config);
|
|
400
|
+
} else if (config.adapter && typeof config.adapter === 'object') {
|
|
401
|
+
this._adapter = config.adapter;
|
|
402
|
+
} else {
|
|
403
|
+
this._adapter = new MockAdapter(config);
|
|
404
|
+
}
|
|
405
|
+
this._adapter.onMessage?.(msg => this._handleMessage(msg));
|
|
406
|
+
this._adapter.onConnect?.(() => {
|
|
407
|
+
this._connected = true;
|
|
408
|
+
this._retryCount = 0;
|
|
409
|
+
this._emit('connect');
|
|
410
|
+
this._flushQueue();
|
|
411
|
+
});
|
|
412
|
+
this._adapter.onDisconnect?.(() => {
|
|
413
|
+
this._connected = false;
|
|
414
|
+
this._emit('disconnect');
|
|
415
|
+
this._autoReconnect();
|
|
416
|
+
});
|
|
417
|
+
this._adapter.onError?.(err => {
|
|
418
|
+
this._emit('error', err);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async connect() {
|
|
422
|
+
try {
|
|
423
|
+
await this._adapter.connect?.();
|
|
424
|
+
this._connected = true;
|
|
425
|
+
} catch (e) {
|
|
426
|
+
this._autoReconnect();
|
|
427
|
+
}
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
async disconnect() {
|
|
431
|
+
this._connected = false;
|
|
432
|
+
await this._adapter.disconnect?.();
|
|
433
|
+
this._emit('disconnect');
|
|
434
|
+
return this;
|
|
435
|
+
}
|
|
436
|
+
async subscribe(topic = 'default') {
|
|
437
|
+
await this._adapter.subscribe?.(topic);
|
|
438
|
+
this._emit('subscribe', topic);
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
async unsubscribe(topic = 'default') {
|
|
442
|
+
await this._adapter.unsubscribe?.(topic);
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
async send(message) {
|
|
446
|
+
if (!this._connected) {
|
|
447
|
+
this._messageQueue.push(message);
|
|
448
|
+
return this;
|
|
449
|
+
}
|
|
450
|
+
await this._adapter.send?.(message);
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
453
|
+
onMessage(cb) {
|
|
454
|
+
return this._on('message', cb);
|
|
455
|
+
}
|
|
456
|
+
onConnect(cb) {
|
|
457
|
+
return this._on('connect', cb);
|
|
458
|
+
}
|
|
459
|
+
onDisconnect(cb) {
|
|
460
|
+
return this._on('disconnect', cb);
|
|
461
|
+
}
|
|
462
|
+
onError(cb) {
|
|
463
|
+
return this._on('error', cb);
|
|
464
|
+
}
|
|
465
|
+
use(mw) {
|
|
466
|
+
this._middleware.push(mw);
|
|
467
|
+
return this;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 请求推送权限 — 需要 functional 隐私同意
|
|
472
|
+
* 如果用户未授予 functional 同意,返回 'consent_required'
|
|
473
|
+
*/
|
|
474
|
+
async requestPermission() {
|
|
475
|
+
// 隐私同意检查:推送权限需要 functional 级别同意
|
|
476
|
+
if (consentManager && !consentManager.hasConsent('functional')) {
|
|
477
|
+
if (this.debug) console.log('[PushManager] 推送权限请求被阻止(需要 functional 隐私同意)');
|
|
478
|
+
return 'consent_required';
|
|
479
|
+
}
|
|
480
|
+
if (typeof Notification !== 'undefined') return await Notification.requestPermission();
|
|
481
|
+
return 'unsupported';
|
|
482
|
+
}
|
|
483
|
+
getPermissionStatus() {
|
|
484
|
+
if (consentManager && !consentManager.hasConsent('functional')) return 'consent_required';
|
|
485
|
+
return typeof Notification !== 'undefined' ? Notification.permission : 'unsupported';
|
|
486
|
+
}
|
|
487
|
+
get isConnected() {
|
|
488
|
+
return this._connected;
|
|
489
|
+
}
|
|
490
|
+
get adapterName() {
|
|
491
|
+
return this._adapter?.name || 'unknown';
|
|
492
|
+
}
|
|
493
|
+
_handleMessage(raw) {
|
|
494
|
+
let msg = raw;
|
|
495
|
+
for (const mw of this._middleware) {
|
|
496
|
+
msg = mw(msg);
|
|
497
|
+
if (!msg) return;
|
|
498
|
+
}
|
|
499
|
+
this._emit('message', msg);
|
|
500
|
+
}
|
|
501
|
+
_flushQueue() {
|
|
502
|
+
while (this._messageQueue.length) this.send(this._messageQueue.shift());
|
|
503
|
+
}
|
|
504
|
+
_autoReconnect() {
|
|
505
|
+
if (this._retryCount >= this._maxRetries) {
|
|
506
|
+
this._emit('error', new Error('Max retries'));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this._retryCount++;
|
|
510
|
+
setTimeout(() => this.connect(), this._retryDelay * Math.pow(1.5, this._retryCount - 1));
|
|
511
|
+
}
|
|
512
|
+
_on(ev, cb) {
|
|
513
|
+
if (!this._listeners.has(ev)) this._listeners.set(ev, []);
|
|
514
|
+
this._listeners.get(ev).push(cb);
|
|
515
|
+
return () => {
|
|
516
|
+
const a = this._listeners.get(ev);
|
|
517
|
+
a?.splice(a.indexOf(cb), 1);
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
_emit(ev, data) {
|
|
521
|
+
this._listeners.get(ev)?.forEach(fn => {
|
|
522
|
+
try {
|
|
523
|
+
fn(data);
|
|
524
|
+
} catch (e) {}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Push SDK - API 客户端
|
|
531
|
+
* 推送通知系统后端接口封装
|
|
532
|
+
*
|
|
533
|
+
* 使用 BaseApiClient 基类简化代码
|
|
534
|
+
*/
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 推送 API 客户端
|
|
539
|
+
*/
|
|
540
|
+
class PushApiClient extends sdkConfig.BaseApiClient {
|
|
541
|
+
constructor(config = {}) {
|
|
542
|
+
super('/push', config);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ============ 设备管理 ============
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* 注册设备
|
|
549
|
+
* @param {Object} deviceInfo - 设备信息
|
|
550
|
+
*/
|
|
551
|
+
async registerDevice(deviceInfo) {
|
|
552
|
+
return this.post('/devices', deviceInfo);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* 更新设备 Token
|
|
557
|
+
* @param {string} deviceId - 设备 ID
|
|
558
|
+
* @param {string} token - 推送 Token
|
|
559
|
+
*/
|
|
560
|
+
async updateDeviceToken(deviceId, token) {
|
|
561
|
+
return this.put(`/devices/${deviceId}/token`, {
|
|
562
|
+
token
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 获取已注册设备
|
|
568
|
+
*/
|
|
569
|
+
async getMyDevices() {
|
|
570
|
+
return this.get('/devices/my');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* 注销设备
|
|
575
|
+
* @param {string} deviceId - 设备 ID
|
|
576
|
+
*/
|
|
577
|
+
async unregisterDevice(deviceId) {
|
|
578
|
+
return this.delete(`/devices/${deviceId}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ============ 推送设置 ============
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 获取推送偏好
|
|
585
|
+
*/
|
|
586
|
+
async getPreferences() {
|
|
587
|
+
return this.get('/preferences');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* 更新推送偏好
|
|
592
|
+
* @param {Object} preferences - 偏好设置
|
|
593
|
+
*/
|
|
594
|
+
async updatePreferences(preferences) {
|
|
595
|
+
return this.put('/preferences', preferences);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 获取免打扰设置
|
|
600
|
+
*/
|
|
601
|
+
async getQuietHours() {
|
|
602
|
+
return this.get('/quiet-hours');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* 设置免打扰时段
|
|
607
|
+
* @param {Object} settings - 免打扰设置
|
|
608
|
+
*/
|
|
609
|
+
async setQuietHours(settings) {
|
|
610
|
+
return this.put('/quiet-hours', settings);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ============ 推送话题 ============
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 订阅话题
|
|
617
|
+
* @param {string} topic - 话题名称
|
|
618
|
+
*/
|
|
619
|
+
async subscribeTopic(topic) {
|
|
620
|
+
return this.post(`/topics/${topic}/subscribe`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* 取消订阅话题
|
|
625
|
+
* @param {string} topic - 话题名称
|
|
626
|
+
*/
|
|
627
|
+
async unsubscribeTopic(topic) {
|
|
628
|
+
return this.post(`/topics/${topic}/unsubscribe`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* 获取已订阅话题
|
|
633
|
+
*/
|
|
634
|
+
async getSubscribedTopics() {
|
|
635
|
+
return this.get('/topics/subscribed');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 获取可用话题
|
|
640
|
+
*/
|
|
641
|
+
async getAvailableTopics() {
|
|
642
|
+
return this.get('/topics');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ============ 推送历史 ============
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 获取推送历史
|
|
649
|
+
* @param {Object} params - 查询参数
|
|
650
|
+
*/
|
|
651
|
+
async getHistory(params = {}) {
|
|
652
|
+
return this.get('/history', params);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* 标记已读
|
|
657
|
+
* @param {string} pushId - 推送 ID
|
|
658
|
+
*/
|
|
659
|
+
async markAsRead(pushId) {
|
|
660
|
+
return this.post(`/history/${pushId}/read`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* 批量标记已读
|
|
665
|
+
* @param {string[]} pushIds - 推送 ID 列表
|
|
666
|
+
*/
|
|
667
|
+
async batchMarkAsRead(pushIds) {
|
|
668
|
+
return this.post('/history/batch-read', {
|
|
669
|
+
push_ids: pushIds
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ============ 管理员操作 ============
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* 发送推送(管理员)
|
|
677
|
+
* @param {Object} data - 推送数据
|
|
678
|
+
*/
|
|
679
|
+
async send(data) {
|
|
680
|
+
return this.post('/send', data);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* 发送给指定用户(管理员)
|
|
685
|
+
* @param {string[]} userIds - 用户 ID 列表
|
|
686
|
+
* @param {Object} message - 消息内容
|
|
687
|
+
*/
|
|
688
|
+
async sendToUsers(userIds, message) {
|
|
689
|
+
return this.post('/send/users', {
|
|
690
|
+
user_ids: userIds,
|
|
691
|
+
...message
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* 发送给话题订阅者(管理员)
|
|
697
|
+
* @param {string} topic - 话题
|
|
698
|
+
* @param {Object} message - 消息内容
|
|
699
|
+
*/
|
|
700
|
+
async sendToTopic(topic, message) {
|
|
701
|
+
return this.post('/send/topic', {
|
|
702
|
+
topic,
|
|
703
|
+
...message
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 全量推送(管理员)
|
|
709
|
+
* @param {Object} message - 消息内容
|
|
710
|
+
*/
|
|
711
|
+
async broadcast(message) {
|
|
712
|
+
return this.post('/send/broadcast', message);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* 定时推送(管理员)
|
|
717
|
+
* @param {Object} data - 推送数据
|
|
718
|
+
* @param {string} scheduleAt - 发送时间
|
|
719
|
+
*/
|
|
720
|
+
async schedule(data, scheduleAt) {
|
|
721
|
+
return this.post('/schedule', {
|
|
722
|
+
...data,
|
|
723
|
+
schedule_at: scheduleAt
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* 取消定时推送(管理员)
|
|
729
|
+
* @param {string} scheduleId - 定时任务 ID
|
|
730
|
+
*/
|
|
731
|
+
async cancelSchedule(scheduleId) {
|
|
732
|
+
return this.delete(`/schedule/${scheduleId}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ============ 统计 ============
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 获取推送统计(管理员)
|
|
739
|
+
* @param {Object} params - 统计参数
|
|
740
|
+
*/
|
|
741
|
+
async getStats(params = {}) {
|
|
742
|
+
return this.get('/stats', params);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* 获取送达率统计(管理员)
|
|
747
|
+
* @param {Object} params - 统计参数
|
|
748
|
+
*/
|
|
749
|
+
async getDeliveryStats(params = {}) {
|
|
750
|
+
return this.get('/stats/delivery', params);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ============ 隐私合规 ============
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* 清除所有推送数据 — GDPR 第17条
|
|
757
|
+
* 注销所有设备 + 清除订阅 + 清除历史
|
|
758
|
+
*/
|
|
759
|
+
async clearAllUserData() {
|
|
760
|
+
const results = {};
|
|
761
|
+
try {
|
|
762
|
+
const devices = await this.getMyDevices();
|
|
763
|
+
for (const d of devices?.data || []) {
|
|
764
|
+
await this.unregisterDevice(d.id).catch(() => {});
|
|
765
|
+
}
|
|
766
|
+
results.devices = 'cleared';
|
|
767
|
+
} catch (e) {
|
|
768
|
+
results.deviceError = e.message;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const topics = await this.getSubscribedTopics();
|
|
772
|
+
for (const t of topics?.data || []) {
|
|
773
|
+
await this.unsubscribeTopic(t.topic || t.name).catch(() => {});
|
|
774
|
+
}
|
|
775
|
+
results.topics = 'cleared';
|
|
776
|
+
} catch (e) {
|
|
777
|
+
results.topicError = e.message;
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
cleared: true,
|
|
781
|
+
timestamp: new Date().toISOString(),
|
|
782
|
+
...results
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* 导出推送数据 — GDPR 第20条
|
|
788
|
+
*/
|
|
789
|
+
async exportPushData() {
|
|
790
|
+
try {
|
|
791
|
+
const [prefs, topics, history] = await Promise.allSettled([this.getPreferences(), this.getSubscribedTopics(), this.getHistory({
|
|
792
|
+
page_size: 50
|
|
793
|
+
})]);
|
|
794
|
+
return {
|
|
795
|
+
exportDate: new Date().toISOString(),
|
|
796
|
+
format: 'QBit Push Export (GDPR Art. 20)',
|
|
797
|
+
preferences: prefs.status === 'fulfilled' ? prefs.value?.data : null,
|
|
798
|
+
subscribedTopics: topics.status === 'fulfilled' ? topics.value?.data : [],
|
|
799
|
+
recentHistory: history.status === 'fulfilled' ? history.value?.data?.items : []
|
|
800
|
+
};
|
|
801
|
+
} catch (e) {
|
|
802
|
+
return {
|
|
803
|
+
error: e.message,
|
|
804
|
+
exportDate: new Date().toISOString()
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* 获取隐私数据声明
|
|
811
|
+
*/
|
|
812
|
+
getDataDisclosure() {
|
|
813
|
+
return {
|
|
814
|
+
sdk: '@quantabit/push-sdk',
|
|
815
|
+
privacyLevel: 'functional',
|
|
816
|
+
consentRequired: true,
|
|
817
|
+
collected: [{
|
|
818
|
+
type: 'device_token',
|
|
819
|
+
description: 'Push notification token (APNs/FCM)',
|
|
820
|
+
retention: 'Until unregistered',
|
|
821
|
+
encrypted: true
|
|
822
|
+
}, {
|
|
823
|
+
type: 'device_info',
|
|
824
|
+
description: 'OS, version, app version',
|
|
825
|
+
retention: 'Until unregistered'
|
|
826
|
+
}, {
|
|
827
|
+
type: 'push_preferences',
|
|
828
|
+
description: 'Notification preferences and quiet hours',
|
|
829
|
+
retention: 'Until account deletion'
|
|
830
|
+
}, {
|
|
831
|
+
type: 'push_history',
|
|
832
|
+
description: 'Delivered notification records',
|
|
833
|
+
retention: '30 days'
|
|
834
|
+
}, {
|
|
835
|
+
type: 'topic_subscriptions',
|
|
836
|
+
description: 'Push topic subscriptions',
|
|
837
|
+
retention: 'Until unsubscribed'
|
|
838
|
+
}],
|
|
839
|
+
gdprCapabilities: ['delete', 'export'],
|
|
840
|
+
note: 'Push notifications require explicit user consent (device permission). Users can revoke at OS level.'
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// 创建默认实例
|
|
846
|
+
const pushApi = new PushApiClient();
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Push SDK - 类型定义
|
|
850
|
+
*/
|
|
851
|
+
|
|
852
|
+
// 推送渠道
|
|
853
|
+
const PushChannel = {
|
|
854
|
+
WEB_PUSH: 'web_push',
|
|
855
|
+
FCM: 'fcm',
|
|
856
|
+
// Firebase Cloud Messaging
|
|
857
|
+
APNS: 'apns',
|
|
858
|
+
// Apple Push Notification
|
|
859
|
+
EMAIL: 'email',
|
|
860
|
+
SMS: 'sms',
|
|
861
|
+
WECHAT: 'wechat',
|
|
862
|
+
IN_APP: 'in_app'
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// 推送状态
|
|
866
|
+
const PushStatus = {
|
|
867
|
+
PENDING: 'pending',
|
|
868
|
+
SENT: 'sent',
|
|
869
|
+
DELIVERED: 'delivered',
|
|
870
|
+
FAILED: 'failed',
|
|
871
|
+
CLICKED: 'clicked',
|
|
872
|
+
DISMISSED: 'dismissed'
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// 推送优先级
|
|
876
|
+
const PushPriority = {
|
|
877
|
+
LOW: 'low',
|
|
878
|
+
NORMAL: 'normal',
|
|
879
|
+
HIGH: 'high'
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
// 订阅状态
|
|
883
|
+
const SubscriptionStatus = {
|
|
884
|
+
ACTIVE: 'active',
|
|
885
|
+
EXPIRED: 'expired',
|
|
886
|
+
UNSUBSCRIBED: 'unsubscribed',
|
|
887
|
+
DENIED: 'denied'
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// 权限状态
|
|
891
|
+
const PermissionStatus = {
|
|
892
|
+
GRANTED: 'granted',
|
|
893
|
+
DENIED: 'denied',
|
|
894
|
+
DEFAULT: 'default',
|
|
895
|
+
PROMPT: 'prompt'
|
|
896
|
+
};
|
|
897
|
+
const NotificationStatus = PushStatus;
|
|
898
|
+
const NotificationType = {
|
|
899
|
+
SYSTEM: 'system',
|
|
900
|
+
ANNOUNCEMENT: 'announcement',
|
|
901
|
+
TRANSACTION: 'transaction',
|
|
902
|
+
ACTIVITY: 'activity',
|
|
903
|
+
REMINDER: 'reminder',
|
|
904
|
+
MARKETING: 'marketing'
|
|
905
|
+
};
|
|
906
|
+
const NotificationChannel = PushChannel;
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Push SDK - Web Push 客户端
|
|
910
|
+
* 使用 BaseApiClient 基类,继承统一的配置、Token 管理和错误处理
|
|
911
|
+
*/
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Web Push 客户端
|
|
916
|
+
*/
|
|
917
|
+
class WebPushClientClass extends sdkConfig.BaseApiClient {
|
|
918
|
+
constructor(config = {}) {
|
|
919
|
+
super('/push', config);
|
|
920
|
+
this.vapidPublicKey = config.vapidPublicKey || '';
|
|
921
|
+
this.onMessage = config.onMessage;
|
|
922
|
+
this.subscription = null;
|
|
923
|
+
this.registration = null;
|
|
924
|
+
this.permission = PermissionStatus.DEFAULT;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* 初始化
|
|
929
|
+
*/
|
|
930
|
+
async init(config = {}) {
|
|
931
|
+
if (config.vapidPublicKey) this.vapidPublicKey = config.vapidPublicKey;
|
|
932
|
+
if (!this.isSupported()) {
|
|
933
|
+
console.warn('Web Push is not supported');
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// 检查权限状态
|
|
938
|
+
this.permission = Notification.permission;
|
|
939
|
+
|
|
940
|
+
// 注册 Service Worker
|
|
941
|
+
try {
|
|
942
|
+
this.registration = await navigator.serviceWorker.register('/sw.js');
|
|
943
|
+
console.log('Service Worker registered');
|
|
944
|
+
} catch (err) {
|
|
945
|
+
console.error('Service Worker registration failed:', err);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// 检查现有订阅
|
|
950
|
+
this.subscription = await this.registration.pushManager.getSubscription();
|
|
951
|
+
|
|
952
|
+
// 监听消息
|
|
953
|
+
this.setupMessageListener();
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* 检查是否支持 Web Push
|
|
959
|
+
*/
|
|
960
|
+
isSupported() {
|
|
961
|
+
return typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* 请求权限
|
|
966
|
+
*/
|
|
967
|
+
async requestPermission() {
|
|
968
|
+
if (!this.isSupported()) {
|
|
969
|
+
return PermissionStatus.DENIED;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const permission = await Notification.requestPermission();
|
|
973
|
+
this.permission = permission;
|
|
974
|
+
if (permission === 'granted') {
|
|
975
|
+
await this.subscribe();
|
|
976
|
+
}
|
|
977
|
+
return permission;
|
|
978
|
+
} catch (err) {
|
|
979
|
+
console.error('Permission request failed:', err);
|
|
980
|
+
this.onError?.(err);
|
|
981
|
+
return PermissionStatus.DENIED;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* 订阅推送
|
|
987
|
+
*/
|
|
988
|
+
async subscribe() {
|
|
989
|
+
if (!this.registration) {
|
|
990
|
+
await this.init();
|
|
991
|
+
}
|
|
992
|
+
if (!this.vapidPublicKey) {
|
|
993
|
+
throw new Error('VAPID public key not configured');
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
this.subscription = await this.registration.pushManager.subscribe({
|
|
997
|
+
userVisibleOnly: true,
|
|
998
|
+
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// 将订阅信息发送到服务器
|
|
1002
|
+
await this.saveSubscription(this.subscription);
|
|
1003
|
+
console.log('Push subscription successful');
|
|
1004
|
+
return this.subscription;
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
console.error('Push subscription failed:', err);
|
|
1007
|
+
this.onError?.(err);
|
|
1008
|
+
throw err;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* 取消订阅
|
|
1014
|
+
*/
|
|
1015
|
+
async unsubscribe() {
|
|
1016
|
+
if (!this.subscription) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
await this.subscription.unsubscribe();
|
|
1021
|
+
await this.removeSubscription();
|
|
1022
|
+
this.subscription = null;
|
|
1023
|
+
console.log('Push unsubscribed');
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
console.error('Unsubscribe failed:', err);
|
|
1026
|
+
this.onError?.(err);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* 保存订阅到服务器
|
|
1032
|
+
*/
|
|
1033
|
+
async saveSubscription(subscription) {
|
|
1034
|
+
return this.post('/subscriptions', {
|
|
1035
|
+
subscription: subscription.toJSON(),
|
|
1036
|
+
channel: 'web_push'
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* 从服务器移除订阅
|
|
1042
|
+
*/
|
|
1043
|
+
async removeSubscription() {
|
|
1044
|
+
return this.delete('/subscriptions/current');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* 监听消息
|
|
1049
|
+
*/
|
|
1050
|
+
setupMessageListener() {
|
|
1051
|
+
if (!('serviceWorker' in navigator)) return;
|
|
1052
|
+
navigator.serviceWorker.addEventListener('message', event => {
|
|
1053
|
+
if (event.data && event.data.type === 'PUSH_RECEIVED') {
|
|
1054
|
+
this.onMessage?.(event.data.payload);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* 获取订阅状态
|
|
1061
|
+
*/
|
|
1062
|
+
getSubscriptionStatus() {
|
|
1063
|
+
if (!this.isSupported()) {
|
|
1064
|
+
return SubscriptionStatus.DENIED;
|
|
1065
|
+
}
|
|
1066
|
+
if (this.permission === 'denied') {
|
|
1067
|
+
return SubscriptionStatus.DENIED;
|
|
1068
|
+
}
|
|
1069
|
+
if (this.subscription) {
|
|
1070
|
+
return SubscriptionStatus.ACTIVE;
|
|
1071
|
+
}
|
|
1072
|
+
return SubscriptionStatus.UNSUBSCRIBED;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* 获取权限状态
|
|
1077
|
+
*/
|
|
1078
|
+
getPermissionStatus() {
|
|
1079
|
+
if (!this.isSupported()) {
|
|
1080
|
+
return PermissionStatus.DENIED;
|
|
1081
|
+
}
|
|
1082
|
+
return Notification.permission;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* 显示本地通知
|
|
1087
|
+
*/
|
|
1088
|
+
async showNotification(title, options = {}) {
|
|
1089
|
+
if (this.permission !== 'granted') {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (this.registration) {
|
|
1093
|
+
return this.registration.showNotification(title, {
|
|
1094
|
+
icon: options.icon || '/icon-192.png',
|
|
1095
|
+
badge: options.badge || '/badge-72.png',
|
|
1096
|
+
...options
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// 降级到 Notification API
|
|
1101
|
+
return new Notification(title, options);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Base64 转 Uint8Array
|
|
1106
|
+
*/
|
|
1107
|
+
urlBase64ToUint8Array(base64String) {
|
|
1108
|
+
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
1109
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
1110
|
+
const rawData = atob(base64);
|
|
1111
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
1112
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
1113
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
1114
|
+
}
|
|
1115
|
+
return outputArray;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const webPushClient = new WebPushClientClass();
|
|
1119
|
+
const WebPushClient = WebPushClientClass;
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Push SDK - 国际化
|
|
1123
|
+
* 消息推送多语言支持
|
|
1124
|
+
*/
|
|
1125
|
+
|
|
1126
|
+
const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
|
|
1127
|
+
const messages = {
|
|
1128
|
+
zh: {
|
|
1129
|
+
// 推送
|
|
1130
|
+
push: '推送',
|
|
1131
|
+
notification: '通知',
|
|
1132
|
+
notifications: '通知',
|
|
1133
|
+
pushNotification: '推送通知',
|
|
1134
|
+
// 类型
|
|
1135
|
+
type: '类型',
|
|
1136
|
+
system: '系统通知',
|
|
1137
|
+
marketing: '营销推送',
|
|
1138
|
+
transactional: '交易通知',
|
|
1139
|
+
social: '社交通知',
|
|
1140
|
+
reminder: '提醒',
|
|
1141
|
+
alert: '警报',
|
|
1142
|
+
// 渠道
|
|
1143
|
+
channel: '渠道',
|
|
1144
|
+
webPush: 'Web推送',
|
|
1145
|
+
appPush: 'App推送',
|
|
1146
|
+
email: '邮件',
|
|
1147
|
+
sms: '短信',
|
|
1148
|
+
inApp: '站内信',
|
|
1149
|
+
// 状态
|
|
1150
|
+
status: '状态',
|
|
1151
|
+
pending: '待发送',
|
|
1152
|
+
sent: '已发送',
|
|
1153
|
+
delivered: '已送达',
|
|
1154
|
+
read: '已读',
|
|
1155
|
+
clicked: '已点击',
|
|
1156
|
+
failed: '发送失败',
|
|
1157
|
+
// 权限
|
|
1158
|
+
permission: '权限',
|
|
1159
|
+
granted: '已授权',
|
|
1160
|
+
denied: '已拒绝',
|
|
1161
|
+
default: '未设置',
|
|
1162
|
+
requestPermission: '开启通知权限',
|
|
1163
|
+
permissionRequired: '需要通知权限',
|
|
1164
|
+
// 设置
|
|
1165
|
+
settings: '设置',
|
|
1166
|
+
enabled: '已开启',
|
|
1167
|
+
disabled: '已关闭',
|
|
1168
|
+
enablePush: '开启推送',
|
|
1169
|
+
disablePush: '关闭推送',
|
|
1170
|
+
// 偏好
|
|
1171
|
+
preferences: '偏好设置',
|
|
1172
|
+
muteAll: '全部静音',
|
|
1173
|
+
muteDuration: '静音时长',
|
|
1174
|
+
quietHours: '免打扰时段',
|
|
1175
|
+
// 频率
|
|
1176
|
+
frequency: '频率',
|
|
1177
|
+
realtime: '实时',
|
|
1178
|
+
daily: '每日摘要',
|
|
1179
|
+
weekly: '每周摘要',
|
|
1180
|
+
// 操作
|
|
1181
|
+
send: '发送',
|
|
1182
|
+
schedule: '定时发送',
|
|
1183
|
+
cancel: '取消',
|
|
1184
|
+
resend: '重试',
|
|
1185
|
+
markAsRead: '标记已读',
|
|
1186
|
+
markAllRead: '全部已读',
|
|
1187
|
+
delete: '删除',
|
|
1188
|
+
clear: '清空',
|
|
1189
|
+
// 时间
|
|
1190
|
+
now: '刚刚',
|
|
1191
|
+
minutesAgo: '{n}分钟前',
|
|
1192
|
+
hoursAgo: '{n}小时前',
|
|
1193
|
+
yesterday: '昨天',
|
|
1194
|
+
daysAgo: '{n}天前',
|
|
1195
|
+
// 状态
|
|
1196
|
+
loading: '加载中...',
|
|
1197
|
+
noNotifications: '暂无通知',
|
|
1198
|
+
error: '加载失败',
|
|
1199
|
+
// 标题
|
|
1200
|
+
unread: '未读',
|
|
1201
|
+
all: '全部',
|
|
1202
|
+
newNotification: '新通知'
|
|
1203
|
+
},
|
|
1204
|
+
en: {
|
|
1205
|
+
push: 'Push',
|
|
1206
|
+
notification: 'Notification',
|
|
1207
|
+
notifications: 'Notifications',
|
|
1208
|
+
pushNotification: 'Push Notification',
|
|
1209
|
+
type: 'Type',
|
|
1210
|
+
system: 'System',
|
|
1211
|
+
marketing: 'Marketing',
|
|
1212
|
+
transactional: 'Transactional',
|
|
1213
|
+
social: 'Social',
|
|
1214
|
+
reminder: 'Reminder',
|
|
1215
|
+
alert: 'Alert',
|
|
1216
|
+
channel: 'Channel',
|
|
1217
|
+
webPush: 'Web Push',
|
|
1218
|
+
appPush: 'App Push',
|
|
1219
|
+
email: 'Email',
|
|
1220
|
+
sms: 'SMS',
|
|
1221
|
+
inApp: 'In-App',
|
|
1222
|
+
status: 'Status',
|
|
1223
|
+
pending: 'Pending',
|
|
1224
|
+
sent: 'Sent',
|
|
1225
|
+
delivered: 'Delivered',
|
|
1226
|
+
read: 'Read',
|
|
1227
|
+
clicked: 'Clicked',
|
|
1228
|
+
failed: 'Failed',
|
|
1229
|
+
permission: 'Permission',
|
|
1230
|
+
granted: 'Granted',
|
|
1231
|
+
denied: 'Denied',
|
|
1232
|
+
default: 'Default',
|
|
1233
|
+
requestPermission: 'Enable Notifications',
|
|
1234
|
+
permissionRequired: 'Permission Required',
|
|
1235
|
+
settings: 'Settings',
|
|
1236
|
+
enabled: 'Enabled',
|
|
1237
|
+
disabled: 'Disabled',
|
|
1238
|
+
enablePush: 'Enable Push',
|
|
1239
|
+
disablePush: 'Disable Push',
|
|
1240
|
+
preferences: 'Preferences',
|
|
1241
|
+
muteAll: 'Mute All',
|
|
1242
|
+
muteDuration: 'Mute Duration',
|
|
1243
|
+
quietHours: 'Quiet Hours',
|
|
1244
|
+
frequency: 'Frequency',
|
|
1245
|
+
realtime: 'Realtime',
|
|
1246
|
+
daily: 'Daily Digest',
|
|
1247
|
+
weekly: 'Weekly Digest',
|
|
1248
|
+
send: 'Send',
|
|
1249
|
+
schedule: 'Schedule',
|
|
1250
|
+
cancel: 'Cancel',
|
|
1251
|
+
resend: 'Resend',
|
|
1252
|
+
markAsRead: 'Mark as Read',
|
|
1253
|
+
markAllRead: 'Mark All Read',
|
|
1254
|
+
delete: 'Delete',
|
|
1255
|
+
clear: 'Clear',
|
|
1256
|
+
now: 'Just now',
|
|
1257
|
+
minutesAgo: '{n}m ago',
|
|
1258
|
+
hoursAgo: '{n}h ago',
|
|
1259
|
+
yesterday: 'Yesterday',
|
|
1260
|
+
daysAgo: '{n}d ago',
|
|
1261
|
+
loading: 'Loading...',
|
|
1262
|
+
noNotifications: 'No Notifications',
|
|
1263
|
+
error: 'Error',
|
|
1264
|
+
unread: 'Unread',
|
|
1265
|
+
all: 'All',
|
|
1266
|
+
newNotification: 'New Notification'
|
|
1267
|
+
},
|
|
1268
|
+
ja: {
|
|
1269
|
+
push: 'プッシュ',
|
|
1270
|
+
notification: '通知',
|
|
1271
|
+
notifications: '通知',
|
|
1272
|
+
pushNotification: 'プッシュ通知',
|
|
1273
|
+
type: 'タイプ',
|
|
1274
|
+
system: 'システム',
|
|
1275
|
+
marketing: 'マーケティング',
|
|
1276
|
+
transactional: '取引',
|
|
1277
|
+
social: 'ソーシャル',
|
|
1278
|
+
reminder: 'リマインダー',
|
|
1279
|
+
alert: 'アラート',
|
|
1280
|
+
channel: 'チャネル',
|
|
1281
|
+
webPush: 'Webプッシュ',
|
|
1282
|
+
appPush: 'アプリプッシュ',
|
|
1283
|
+
email: 'メール',
|
|
1284
|
+
sms: 'SMS',
|
|
1285
|
+
inApp: 'アプリ内',
|
|
1286
|
+
status: 'ステータス',
|
|
1287
|
+
pending: '待機中',
|
|
1288
|
+
sent: '送信済み',
|
|
1289
|
+
delivered: '配信済み',
|
|
1290
|
+
read: '既読',
|
|
1291
|
+
clicked: 'クリック済み',
|
|
1292
|
+
failed: '失敗',
|
|
1293
|
+
permission: '権限',
|
|
1294
|
+
granted: '許可済み',
|
|
1295
|
+
denied: '拒否',
|
|
1296
|
+
default: '未設定',
|
|
1297
|
+
requestPermission: '通知を許可',
|
|
1298
|
+
permissionRequired: '権限が必要です',
|
|
1299
|
+
settings: '設定',
|
|
1300
|
+
enabled: 'オン',
|
|
1301
|
+
disabled: 'オフ',
|
|
1302
|
+
enablePush: 'プッシュを有効化',
|
|
1303
|
+
disablePush: 'プッシュを無効化',
|
|
1304
|
+
preferences: '通知設定',
|
|
1305
|
+
muteAll: 'すべてミュート',
|
|
1306
|
+
muteDuration: 'ミュート期間',
|
|
1307
|
+
quietHours: 'おやすみ時間',
|
|
1308
|
+
frequency: '頻度',
|
|
1309
|
+
realtime: 'リアルタイム',
|
|
1310
|
+
daily: '日次まとめ',
|
|
1311
|
+
weekly: '週次まとめ',
|
|
1312
|
+
send: '送信',
|
|
1313
|
+
schedule: '予約送信',
|
|
1314
|
+
cancel: 'キャンセル',
|
|
1315
|
+
resend: '再送信',
|
|
1316
|
+
markAsRead: '既読にする',
|
|
1317
|
+
markAllRead: 'すべて既読',
|
|
1318
|
+
delete: '削除',
|
|
1319
|
+
clear: 'クリア',
|
|
1320
|
+
now: 'たった今',
|
|
1321
|
+
minutesAgo: '{n}分前',
|
|
1322
|
+
hoursAgo: '{n}時間前',
|
|
1323
|
+
yesterday: '昨日',
|
|
1324
|
+
daysAgo: '{n}日前',
|
|
1325
|
+
loading: '読み込み中...',
|
|
1326
|
+
noNotifications: '通知はありません',
|
|
1327
|
+
error: 'エラー',
|
|
1328
|
+
unread: '未読',
|
|
1329
|
+
all: 'すべて',
|
|
1330
|
+
newNotification: '新しい通知'
|
|
1331
|
+
},
|
|
1332
|
+
ko: {
|
|
1333
|
+
push: '푸시',
|
|
1334
|
+
notification: '알림',
|
|
1335
|
+
notifications: '알림',
|
|
1336
|
+
pushNotification: '푸시 알림',
|
|
1337
|
+
type: '유형',
|
|
1338
|
+
system: '시스템',
|
|
1339
|
+
marketing: '마케팅',
|
|
1340
|
+
transactional: '거래',
|
|
1341
|
+
social: '소셜',
|
|
1342
|
+
reminder: '리마인더',
|
|
1343
|
+
alert: '경고',
|
|
1344
|
+
channel: '채널',
|
|
1345
|
+
webPush: '웹 푸시',
|
|
1346
|
+
appPush: '앱 푸시',
|
|
1347
|
+
email: '이메일',
|
|
1348
|
+
sms: 'SMS',
|
|
1349
|
+
inApp: '인앱',
|
|
1350
|
+
status: '상태',
|
|
1351
|
+
pending: '대기 중',
|
|
1352
|
+
sent: '전송됨',
|
|
1353
|
+
delivered: '전달됨',
|
|
1354
|
+
read: '읽음',
|
|
1355
|
+
clicked: '클릭됨',
|
|
1356
|
+
failed: '실패',
|
|
1357
|
+
permission: '권한',
|
|
1358
|
+
granted: '허용됨',
|
|
1359
|
+
denied: '거부됨',
|
|
1360
|
+
default: '미설정',
|
|
1361
|
+
requestPermission: '알림 허용',
|
|
1362
|
+
permissionRequired: '권한이 필요합니다',
|
|
1363
|
+
settings: '설정',
|
|
1364
|
+
enabled: '사용',
|
|
1365
|
+
disabled: '사용 안 함',
|
|
1366
|
+
enablePush: '푸시 사용',
|
|
1367
|
+
disablePush: '푸시 사용 안 함',
|
|
1368
|
+
preferences: '알림 설정',
|
|
1369
|
+
muteAll: '모두 음소거',
|
|
1370
|
+
muteDuration: '음소거 기간',
|
|
1371
|
+
quietHours: '방해 금지 시간',
|
|
1372
|
+
frequency: '빈도',
|
|
1373
|
+
realtime: '실시간',
|
|
1374
|
+
daily: '일일 다이제스트',
|
|
1375
|
+
weekly: '주간 다이제스트',
|
|
1376
|
+
send: '보내기',
|
|
1377
|
+
schedule: '예약',
|
|
1378
|
+
cancel: '취소',
|
|
1379
|
+
resend: '재전송',
|
|
1380
|
+
markAsRead: '읽음으로 표시',
|
|
1381
|
+
markAllRead: '모두 읽음',
|
|
1382
|
+
delete: '삭제',
|
|
1383
|
+
clear: '비우기',
|
|
1384
|
+
now: '방금',
|
|
1385
|
+
minutesAgo: '{n}분 전',
|
|
1386
|
+
hoursAgo: '{n}시간 전',
|
|
1387
|
+
yesterday: '어제',
|
|
1388
|
+
daysAgo: '{n}일 전',
|
|
1389
|
+
loading: '로딩 중...',
|
|
1390
|
+
noNotifications: '알림 없음',
|
|
1391
|
+
error: '오류',
|
|
1392
|
+
unread: '읽지 않음',
|
|
1393
|
+
all: '전체',
|
|
1394
|
+
newNotification: '새 알림'
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
let currentLanguage = 'zh';
|
|
1398
|
+
function setLanguage(lang) {
|
|
1399
|
+
if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
|
|
1400
|
+
}
|
|
1401
|
+
function getLanguage() {
|
|
1402
|
+
return currentLanguage;
|
|
1403
|
+
}
|
|
1404
|
+
function t(key, params = {}) {
|
|
1405
|
+
let text = (messages[currentLanguage] || messages.en)[key] || key;
|
|
1406
|
+
Object.keys(params).forEach(k => {
|
|
1407
|
+
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), params[k]);
|
|
1408
|
+
});
|
|
1409
|
+
return text;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function usePush(config = {}) {
|
|
1413
|
+
const mgr = React.useRef(null);
|
|
1414
|
+
const [connected, setConnected] = React.useState(false);
|
|
1415
|
+
const [messages, setMessages] = React.useState([]);
|
|
1416
|
+
const [lastMessage, setLastMessage] = React.useState(null);
|
|
1417
|
+
const [permission, setPermission] = React.useState('default');
|
|
1418
|
+
React.useEffect(() => {
|
|
1419
|
+
mgr.current = new PushManager(config);
|
|
1420
|
+
mgr.current.onConnect(() => setConnected(true));
|
|
1421
|
+
mgr.current.onDisconnect(() => setConnected(false));
|
|
1422
|
+
mgr.current.onMessage(msg => {
|
|
1423
|
+
setLastMessage(msg);
|
|
1424
|
+
setMessages(prev => [msg, ...prev].slice(0, 100));
|
|
1425
|
+
});
|
|
1426
|
+
mgr.current.connect();
|
|
1427
|
+
setPermission(mgr.current.getPermissionStatus());
|
|
1428
|
+
return () => mgr.current?.disconnect();
|
|
1429
|
+
}, []);
|
|
1430
|
+
const send = React.useCallback(msg => mgr.current?.send(msg), []);
|
|
1431
|
+
const subscribe = React.useCallback(topic => mgr.current?.subscribe(topic), []);
|
|
1432
|
+
const requestPermission = React.useCallback(async () => {
|
|
1433
|
+
const p = await mgr.current?.requestPermission();
|
|
1434
|
+
setPermission(p);
|
|
1435
|
+
return p;
|
|
1436
|
+
}, []);
|
|
1437
|
+
const clearMessages = React.useCallback(() => setMessages([]), []);
|
|
1438
|
+
return {
|
|
1439
|
+
connected,
|
|
1440
|
+
messages,
|
|
1441
|
+
lastMessage,
|
|
1442
|
+
send,
|
|
1443
|
+
subscribe,
|
|
1444
|
+
requestPermission,
|
|
1445
|
+
permission,
|
|
1446
|
+
clearMessages,
|
|
1447
|
+
manager: mgr.current
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function usePushPermission() {
|
|
1451
|
+
const supported = typeof window !== 'undefined' && 'Notification' in window;
|
|
1452
|
+
const [permission, setPermission] = React.useState(supported ? window.Notification.permission : 'denied');
|
|
1453
|
+
const requestPermission = React.useCallback(async () => {
|
|
1454
|
+
if (!supported) return 'denied';
|
|
1455
|
+
const result = await window.Notification.requestPermission();
|
|
1456
|
+
setPermission(result);
|
|
1457
|
+
return result;
|
|
1458
|
+
}, [supported]);
|
|
1459
|
+
return {
|
|
1460
|
+
supported,
|
|
1461
|
+
permission,
|
|
1462
|
+
requestPermission
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function useNotifications(options = {}) {
|
|
1466
|
+
const [notifications, setNotifications] = React.useState([]);
|
|
1467
|
+
const [unreadCount, setUnreadCount] = React.useState(0);
|
|
1468
|
+
const [loading, setLoading] = React.useState(false);
|
|
1469
|
+
const [error, setError] = React.useState(null);
|
|
1470
|
+
const refresh = React.useCallback(async () => {
|
|
1471
|
+
setLoading(true);
|
|
1472
|
+
setError(null);
|
|
1473
|
+
try {
|
|
1474
|
+
const [historyResult, statsResult] = await Promise.allSettled([pushApi.getHistory(options), pushApi.getStats({
|
|
1475
|
+
scope: 'unread'
|
|
1476
|
+
})]);
|
|
1477
|
+
const history = historyResult.status === 'fulfilled' ? historyResult.value?.data?.items || historyResult.value?.data || [] : [];
|
|
1478
|
+
setNotifications(Array.isArray(history) ? history : []);
|
|
1479
|
+
if (statsResult.status === 'fulfilled') {
|
|
1480
|
+
setUnreadCount(statsResult.value?.data?.unread_count || statsResult.value?.data?.unread || 0);
|
|
1481
|
+
}
|
|
1482
|
+
return historyResult.status === 'fulfilled' ? historyResult.value : null;
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
setError(err.message);
|
|
1485
|
+
return null;
|
|
1486
|
+
} finally {
|
|
1487
|
+
setLoading(false);
|
|
1488
|
+
}
|
|
1489
|
+
}, [options.filter, options.limit, options.page]);
|
|
1490
|
+
React.useEffect(() => {
|
|
1491
|
+
refresh();
|
|
1492
|
+
}, [refresh]);
|
|
1493
|
+
const markAsRead = React.useCallback(async pushId => {
|
|
1494
|
+
const result = await pushApi.markAsRead(pushId);
|
|
1495
|
+
await refresh();
|
|
1496
|
+
return result;
|
|
1497
|
+
}, [refresh]);
|
|
1498
|
+
const markAllAsRead = React.useCallback(async () => {
|
|
1499
|
+
const ids = notifications.map(item => item.id).filter(Boolean);
|
|
1500
|
+
if (ids.length === 0) return null;
|
|
1501
|
+
const result = await pushApi.batchMarkAsRead(ids);
|
|
1502
|
+
await refresh();
|
|
1503
|
+
return result;
|
|
1504
|
+
}, [notifications, refresh]);
|
|
1505
|
+
const deleteNotification = React.useCallback(async () => {
|
|
1506
|
+
// Push history deletion is not exposed yet; keep the hook API stable.
|
|
1507
|
+
await refresh();
|
|
1508
|
+
return null;
|
|
1509
|
+
}, [refresh]);
|
|
1510
|
+
const clearAll = React.useCallback(() => {
|
|
1511
|
+
setNotifications([]);
|
|
1512
|
+
setUnreadCount(0);
|
|
1513
|
+
}, []);
|
|
1514
|
+
return {
|
|
1515
|
+
notifications,
|
|
1516
|
+
unreadCount,
|
|
1517
|
+
loading,
|
|
1518
|
+
error,
|
|
1519
|
+
refresh,
|
|
1520
|
+
markAsRead,
|
|
1521
|
+
markAllAsRead,
|
|
1522
|
+
deleteNotification,
|
|
1523
|
+
clearAll
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function useRealtimeNotifications(config = {}) {
|
|
1527
|
+
return usePush(config);
|
|
1528
|
+
}
|
|
1529
|
+
function useNotificationPreferences() {
|
|
1530
|
+
const [preferences, setPreferences] = React.useState({});
|
|
1531
|
+
const [loading, setLoading] = React.useState(false);
|
|
1532
|
+
const [saving, setSaving] = React.useState(false);
|
|
1533
|
+
const [error, setError] = React.useState(null);
|
|
1534
|
+
const refresh = React.useCallback(async () => {
|
|
1535
|
+
setLoading(true);
|
|
1536
|
+
setError(null);
|
|
1537
|
+
try {
|
|
1538
|
+
const result = await pushApi.getPreferences();
|
|
1539
|
+
setPreferences(result.data || {});
|
|
1540
|
+
return result;
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
setError(err.message);
|
|
1543
|
+
return null;
|
|
1544
|
+
} finally {
|
|
1545
|
+
setLoading(false);
|
|
1546
|
+
}
|
|
1547
|
+
}, []);
|
|
1548
|
+
React.useEffect(() => {
|
|
1549
|
+
refresh();
|
|
1550
|
+
}, [refresh]);
|
|
1551
|
+
const updatePreferences = React.useCallback(async nextPreferences => {
|
|
1552
|
+
setSaving(true);
|
|
1553
|
+
try {
|
|
1554
|
+
const result = await pushApi.updatePreferences(nextPreferences);
|
|
1555
|
+
setPreferences(nextPreferences);
|
|
1556
|
+
return result;
|
|
1557
|
+
} finally {
|
|
1558
|
+
setSaving(false);
|
|
1559
|
+
}
|
|
1560
|
+
}, []);
|
|
1561
|
+
const toggleChannel = React.useCallback(channel => {
|
|
1562
|
+
const next = {
|
|
1563
|
+
...preferences,
|
|
1564
|
+
channels: {
|
|
1565
|
+
...(preferences.channels || {}),
|
|
1566
|
+
[channel]: !preferences.channels?.[channel]
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
return updatePreferences(next);
|
|
1570
|
+
}, [preferences, updatePreferences]);
|
|
1571
|
+
const toggleType = React.useCallback(type => {
|
|
1572
|
+
const next = {
|
|
1573
|
+
...preferences,
|
|
1574
|
+
types: {
|
|
1575
|
+
...(preferences.types || {}),
|
|
1576
|
+
[type]: !preferences.types?.[type]
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
return updatePreferences(next);
|
|
1580
|
+
}, [preferences, updatePreferences]);
|
|
1581
|
+
return {
|
|
1582
|
+
preferences,
|
|
1583
|
+
loading,
|
|
1584
|
+
saving,
|
|
1585
|
+
error,
|
|
1586
|
+
refresh,
|
|
1587
|
+
updatePreferences,
|
|
1588
|
+
toggleChannel,
|
|
1589
|
+
toggleType
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function usePushSubscription() {
|
|
1593
|
+
const [topics, setTopics] = React.useState([]);
|
|
1594
|
+
const refresh = React.useCallback(async () => {
|
|
1595
|
+
const result = await pushApi.getSubscribedTopics();
|
|
1596
|
+
setTopics(result.data || []);
|
|
1597
|
+
return result;
|
|
1598
|
+
}, []);
|
|
1599
|
+
React.useEffect(() => {
|
|
1600
|
+
refresh();
|
|
1601
|
+
}, [refresh]);
|
|
1602
|
+
const subscribe = React.useCallback(async topic => {
|
|
1603
|
+
const result = await pushApi.subscribeTopic(topic);
|
|
1604
|
+
await refresh();
|
|
1605
|
+
return result;
|
|
1606
|
+
}, [refresh]);
|
|
1607
|
+
const unsubscribe = React.useCallback(async topic => {
|
|
1608
|
+
const result = await pushApi.unsubscribeTopic(topic);
|
|
1609
|
+
await refresh();
|
|
1610
|
+
return result;
|
|
1611
|
+
}, [refresh]);
|
|
1612
|
+
return {
|
|
1613
|
+
topics,
|
|
1614
|
+
refresh,
|
|
1615
|
+
subscribe,
|
|
1616
|
+
unsubscribe
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function useSendNotification() {
|
|
1620
|
+
const [loading, setLoading] = React.useState(false);
|
|
1621
|
+
const [error, setError] = React.useState(null);
|
|
1622
|
+
const send = React.useCallback(async message => {
|
|
1623
|
+
setLoading(true);
|
|
1624
|
+
setError(null);
|
|
1625
|
+
try {
|
|
1626
|
+
return await pushApi.send(message);
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
setError(err.message);
|
|
1629
|
+
throw err;
|
|
1630
|
+
} finally {
|
|
1631
|
+
setLoading(false);
|
|
1632
|
+
}
|
|
1633
|
+
}, []);
|
|
1634
|
+
const schedule = React.useCallback(async (message, scheduleAt) => {
|
|
1635
|
+
setLoading(true);
|
|
1636
|
+
setError(null);
|
|
1637
|
+
try {
|
|
1638
|
+
return await pushApi.schedule(message, scheduleAt);
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
setError(err.message);
|
|
1641
|
+
throw err;
|
|
1642
|
+
} finally {
|
|
1643
|
+
setLoading(false);
|
|
1644
|
+
}
|
|
1645
|
+
}, []);
|
|
1646
|
+
return {
|
|
1647
|
+
send,
|
|
1648
|
+
schedule,
|
|
1649
|
+
loading,
|
|
1650
|
+
error
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const PushContext = /*#__PURE__*/React.createContext(null);
|
|
1655
|
+
function PushProvider({
|
|
1656
|
+
config = {},
|
|
1657
|
+
children
|
|
1658
|
+
}) {
|
|
1659
|
+
const push = usePush(config);
|
|
1660
|
+
return /*#__PURE__*/React.createElement(PushContext.Provider, {
|
|
1661
|
+
value: push
|
|
1662
|
+
}, children);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function PushPermissionBanner({
|
|
1666
|
+
onAllow,
|
|
1667
|
+
onDismiss,
|
|
1668
|
+
title = 'Enable Notifications',
|
|
1669
|
+
message = 'Stay updated with real-time push notifications',
|
|
1670
|
+
className = ''
|
|
1671
|
+
}) {
|
|
1672
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1673
|
+
className: `qps-banner ${className}`,
|
|
1674
|
+
style: {
|
|
1675
|
+
display: 'flex',
|
|
1676
|
+
alignItems: 'center',
|
|
1677
|
+
gap: 12,
|
|
1678
|
+
padding: '12px 16px',
|
|
1679
|
+
background: 'linear-gradient(135deg,#eff6ff,#eef2ff)',
|
|
1680
|
+
borderRadius: 12,
|
|
1681
|
+
border: '1px solid #bfdbfe'
|
|
1682
|
+
}
|
|
1683
|
+
}, /*#__PURE__*/React.createElement("svg", {
|
|
1684
|
+
width: "24",
|
|
1685
|
+
height: "24",
|
|
1686
|
+
viewBox: "0 0 24 24",
|
|
1687
|
+
fill: "none",
|
|
1688
|
+
stroke: "#3b82f6",
|
|
1689
|
+
strokeWidth: "2"
|
|
1690
|
+
}, /*#__PURE__*/React.createElement("path", {
|
|
1691
|
+
d: "M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"
|
|
1692
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
1693
|
+
d: "M13.73 21a2 2 0 01-3.46 0"
|
|
1694
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
1695
|
+
style: {
|
|
1696
|
+
flex: 1
|
|
1697
|
+
}
|
|
1698
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1699
|
+
style: {
|
|
1700
|
+
fontSize: 14,
|
|
1701
|
+
fontWeight: 600,
|
|
1702
|
+
color: '#1e40af'
|
|
1703
|
+
}
|
|
1704
|
+
}, title), /*#__PURE__*/React.createElement("div", {
|
|
1705
|
+
style: {
|
|
1706
|
+
fontSize: 12,
|
|
1707
|
+
color: '#3b82f6',
|
|
1708
|
+
marginTop: 2
|
|
1709
|
+
}
|
|
1710
|
+
}, message)), /*#__PURE__*/React.createElement("button", {
|
|
1711
|
+
onClick: onAllow,
|
|
1712
|
+
style: {
|
|
1713
|
+
padding: '6px 16px',
|
|
1714
|
+
borderRadius: 8,
|
|
1715
|
+
border: 'none',
|
|
1716
|
+
background: '#3b82f6',
|
|
1717
|
+
color: '#fff',
|
|
1718
|
+
cursor: 'pointer',
|
|
1719
|
+
fontSize: 13,
|
|
1720
|
+
fontWeight: 600
|
|
1721
|
+
}
|
|
1722
|
+
}, "Allow"), onDismiss && /*#__PURE__*/React.createElement("button", {
|
|
1723
|
+
onClick: onDismiss,
|
|
1724
|
+
style: {
|
|
1725
|
+
border: 'none',
|
|
1726
|
+
background: 'none',
|
|
1727
|
+
cursor: 'pointer',
|
|
1728
|
+
color: '#93c5fd',
|
|
1729
|
+
fontSize: 16
|
|
1730
|
+
}
|
|
1731
|
+
}, "\u2715"));
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Push SDK - React 组件
|
|
1736
|
+
* 消息推送可视化组件
|
|
1737
|
+
*/
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* 通知中心
|
|
1742
|
+
*/
|
|
1743
|
+
function NotificationCenter({
|
|
1744
|
+
onClose
|
|
1745
|
+
}) {
|
|
1746
|
+
const [filter, setFilter] = React.useState('all');
|
|
1747
|
+
const {
|
|
1748
|
+
notifications,
|
|
1749
|
+
unreadCount,
|
|
1750
|
+
loading,
|
|
1751
|
+
markAsRead,
|
|
1752
|
+
markAllAsRead,
|
|
1753
|
+
deleteNotification,
|
|
1754
|
+
clearAll
|
|
1755
|
+
} = useNotifications({
|
|
1756
|
+
filter
|
|
1757
|
+
});
|
|
1758
|
+
const filteredNotifications = filter === 'unread' ? notifications.filter(n => !n.read) : notifications;
|
|
1759
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1760
|
+
className: "eco-push-center"
|
|
1761
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1762
|
+
className: "eco-push-header"
|
|
1763
|
+
}, /*#__PURE__*/React.createElement("h3", null, t('notifications')), unreadCount > 0 && /*#__PURE__*/React.createElement("span", {
|
|
1764
|
+
className: "eco-push-badge"
|
|
1765
|
+
}, unreadCount), onClose && /*#__PURE__*/React.createElement("button", {
|
|
1766
|
+
className: "eco-push-close",
|
|
1767
|
+
onClick: onClose
|
|
1768
|
+
}, "\xD7")), /*#__PURE__*/React.createElement("div", {
|
|
1769
|
+
className: "eco-push-tabs"
|
|
1770
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
1771
|
+
className: `eco-push-tab ${filter === 'all' ? 'active' : ''}`,
|
|
1772
|
+
onClick: () => setFilter('all')
|
|
1773
|
+
}, t('all')), /*#__PURE__*/React.createElement("button", {
|
|
1774
|
+
className: `eco-push-tab ${filter === 'unread' ? 'active' : ''}`,
|
|
1775
|
+
onClick: () => setFilter('unread')
|
|
1776
|
+
}, t('unread'), " ", unreadCount > 0 && `(${unreadCount})`)), /*#__PURE__*/React.createElement("div", {
|
|
1777
|
+
className: "eco-push-actions"
|
|
1778
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
1779
|
+
className: "eco-push-action-btn",
|
|
1780
|
+
onClick: markAllAsRead
|
|
1781
|
+
}, t('markAllRead')), /*#__PURE__*/React.createElement("button", {
|
|
1782
|
+
className: "eco-push-action-btn",
|
|
1783
|
+
onClick: clearAll
|
|
1784
|
+
}, t('clear'))), /*#__PURE__*/React.createElement("div", {
|
|
1785
|
+
className: "eco-push-list"
|
|
1786
|
+
}, loading ? /*#__PURE__*/React.createElement("div", {
|
|
1787
|
+
className: "eco-push-loading"
|
|
1788
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1789
|
+
className: "eco-push-spinner"
|
|
1790
|
+
})) : filteredNotifications.length === 0 ? /*#__PURE__*/React.createElement("div", {
|
|
1791
|
+
className: "eco-push-empty"
|
|
1792
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1793
|
+
className: "eco-push-empty-icon"
|
|
1794
|
+
}, "\uD83D\uDD14"), /*#__PURE__*/React.createElement("span", null, t('noNotifications'))) : filteredNotifications.map(notification => /*#__PURE__*/React.createElement(NotificationItem, {
|
|
1795
|
+
key: notification.id,
|
|
1796
|
+
notification: notification,
|
|
1797
|
+
onRead: () => markAsRead(notification.id),
|
|
1798
|
+
onDelete: () => deleteNotification(notification.id)
|
|
1799
|
+
}))));
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* 单个通知项
|
|
1804
|
+
*/
|
|
1805
|
+
function NotificationItem({
|
|
1806
|
+
notification,
|
|
1807
|
+
onRead,
|
|
1808
|
+
onDelete
|
|
1809
|
+
}) {
|
|
1810
|
+
const handleClick = () => {
|
|
1811
|
+
if (!notification.read) {
|
|
1812
|
+
onRead?.();
|
|
1813
|
+
}
|
|
1814
|
+
if (notification.link) {
|
|
1815
|
+
window.location.href = notification.link;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1819
|
+
className: `eco-push-item ${notification.read ? '' : 'unread'}`,
|
|
1820
|
+
onClick: handleClick
|
|
1821
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1822
|
+
className: "eco-push-item-icon"
|
|
1823
|
+
}, getTypeIcon(notification.type)), /*#__PURE__*/React.createElement("div", {
|
|
1824
|
+
className: "eco-push-item-content"
|
|
1825
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1826
|
+
className: "eco-push-item-header"
|
|
1827
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1828
|
+
className: "eco-push-item-title"
|
|
1829
|
+
}, notification.title), /*#__PURE__*/React.createElement("span", {
|
|
1830
|
+
className: "eco-push-item-time"
|
|
1831
|
+
}, formatTime(notification.createdAt))), /*#__PURE__*/React.createElement("p", {
|
|
1832
|
+
className: "eco-push-item-body"
|
|
1833
|
+
}, notification.body), notification.image && /*#__PURE__*/React.createElement("img", {
|
|
1834
|
+
className: "eco-push-item-image",
|
|
1835
|
+
src: notification.image,
|
|
1836
|
+
alt: ""
|
|
1837
|
+
})), /*#__PURE__*/React.createElement("button", {
|
|
1838
|
+
className: "eco-push-item-delete",
|
|
1839
|
+
onClick: e => {
|
|
1840
|
+
e.stopPropagation();
|
|
1841
|
+
onDelete?.();
|
|
1842
|
+
}
|
|
1843
|
+
}, "\xD7"));
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* 通知铃铛按钮
|
|
1848
|
+
*/
|
|
1849
|
+
function NotificationBell({
|
|
1850
|
+
onClick
|
|
1851
|
+
}) {
|
|
1852
|
+
const {
|
|
1853
|
+
unreadCount
|
|
1854
|
+
} = useNotifications({
|
|
1855
|
+
limit: 0
|
|
1856
|
+
});
|
|
1857
|
+
return /*#__PURE__*/React.createElement("button", {
|
|
1858
|
+
className: "eco-push-bell",
|
|
1859
|
+
onClick: onClick
|
|
1860
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1861
|
+
className: "eco-push-bell-icon"
|
|
1862
|
+
}, "\uD83D\uDD14"), unreadCount > 0 && /*#__PURE__*/React.createElement("span", {
|
|
1863
|
+
className: "eco-push-bell-badge"
|
|
1864
|
+
}, unreadCount > 99 ? '99+' : unreadCount));
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* 推送权限请求
|
|
1869
|
+
*/
|
|
1870
|
+
function PushPermissionPrompt({
|
|
1871
|
+
onGranted,
|
|
1872
|
+
onDenied
|
|
1873
|
+
}) {
|
|
1874
|
+
const {
|
|
1875
|
+
permission,
|
|
1876
|
+
supported,
|
|
1877
|
+
requestPermission
|
|
1878
|
+
} = usePushPermission();
|
|
1879
|
+
const [requesting, setRequesting] = React.useState(false);
|
|
1880
|
+
if (!supported) return null;
|
|
1881
|
+
if (permission === 'granted') return null;
|
|
1882
|
+
if (permission === 'denied') {
|
|
1883
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1884
|
+
className: "eco-push-permission eco-push-permission-denied"
|
|
1885
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1886
|
+
className: "eco-push-permission-icon"
|
|
1887
|
+
}, "\uD83D\uDD15"), /*#__PURE__*/React.createElement("p", null, t('permissionRequired')));
|
|
1888
|
+
}
|
|
1889
|
+
const handleRequest = async () => {
|
|
1890
|
+
setRequesting(true);
|
|
1891
|
+
const result = await requestPermission();
|
|
1892
|
+
setRequesting(false);
|
|
1893
|
+
if (result === 'granted') {
|
|
1894
|
+
onGranted?.();
|
|
1895
|
+
} else {
|
|
1896
|
+
onDenied?.();
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1900
|
+
className: "eco-push-permission"
|
|
1901
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1902
|
+
className: "eco-push-permission-icon"
|
|
1903
|
+
}, "\uD83D\uDD14"), /*#__PURE__*/React.createElement("div", {
|
|
1904
|
+
className: "eco-push-permission-content"
|
|
1905
|
+
}, /*#__PURE__*/React.createElement("h4", null, t('requestPermission')), /*#__PURE__*/React.createElement("p", null, "\u63A5\u6536\u91CD\u8981\u7684\u901A\u77E5\u548C\u66F4\u65B0")), /*#__PURE__*/React.createElement("button", {
|
|
1906
|
+
className: "eco-push-permission-btn",
|
|
1907
|
+
onClick: handleRequest,
|
|
1908
|
+
disabled: requesting
|
|
1909
|
+
}, requesting ? '...' : t('enablePush')));
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* 通知设置面板
|
|
1914
|
+
*/
|
|
1915
|
+
function NotificationSettings() {
|
|
1916
|
+
const {
|
|
1917
|
+
preferences,
|
|
1918
|
+
loading,
|
|
1919
|
+
saving,
|
|
1920
|
+
toggleChannel,
|
|
1921
|
+
toggleType,
|
|
1922
|
+
updatePreferences
|
|
1923
|
+
} = useNotificationPreferences();
|
|
1924
|
+
if (loading) {
|
|
1925
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1926
|
+
className: "eco-push-settings eco-push-loading"
|
|
1927
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1928
|
+
className: "eco-push-spinner"
|
|
1929
|
+
}));
|
|
1930
|
+
}
|
|
1931
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1932
|
+
className: "eco-push-settings"
|
|
1933
|
+
}, /*#__PURE__*/React.createElement("h3", null, t('preferences')), /*#__PURE__*/React.createElement("div", {
|
|
1934
|
+
className: "eco-push-settings-section"
|
|
1935
|
+
}, /*#__PURE__*/React.createElement("h4", null, t('channel')), /*#__PURE__*/React.createElement("div", {
|
|
1936
|
+
className: "eco-push-settings-list"
|
|
1937
|
+
}, Object.entries(preferences.channels || {}).map(([channel, enabled]) => /*#__PURE__*/React.createElement(ToggleItem, {
|
|
1938
|
+
key: channel,
|
|
1939
|
+
label: t(channel),
|
|
1940
|
+
checked: enabled,
|
|
1941
|
+
onChange: () => toggleChannel(channel),
|
|
1942
|
+
disabled: saving
|
|
1943
|
+
})))), /*#__PURE__*/React.createElement("div", {
|
|
1944
|
+
className: "eco-push-settings-section"
|
|
1945
|
+
}, /*#__PURE__*/React.createElement("h4", null, t('type')), /*#__PURE__*/React.createElement("div", {
|
|
1946
|
+
className: "eco-push-settings-list"
|
|
1947
|
+
}, Object.entries(preferences.types || {}).map(([type, enabled]) => /*#__PURE__*/React.createElement(ToggleItem, {
|
|
1948
|
+
key: type,
|
|
1949
|
+
label: t(type),
|
|
1950
|
+
checked: enabled,
|
|
1951
|
+
onChange: () => toggleType(type),
|
|
1952
|
+
disabled: saving
|
|
1953
|
+
})))), /*#__PURE__*/React.createElement("div", {
|
|
1954
|
+
className: "eco-push-settings-section"
|
|
1955
|
+
}, /*#__PURE__*/React.createElement("h4", null, t('quietHours')), /*#__PURE__*/React.createElement(ToggleItem, {
|
|
1956
|
+
label: t('enabled'),
|
|
1957
|
+
checked: preferences.quietHours?.enabled,
|
|
1958
|
+
onChange: () => updatePreferences({
|
|
1959
|
+
quietHours: {
|
|
1960
|
+
...preferences.quietHours,
|
|
1961
|
+
enabled: !preferences.quietHours?.enabled
|
|
1962
|
+
}
|
|
1963
|
+
}),
|
|
1964
|
+
disabled: saving
|
|
1965
|
+
}), preferences.quietHours?.enabled && /*#__PURE__*/React.createElement("div", {
|
|
1966
|
+
className: "eco-push-quiet-hours"
|
|
1967
|
+
}, /*#__PURE__*/React.createElement("input", {
|
|
1968
|
+
type: "time",
|
|
1969
|
+
value: preferences.quietHours?.start || '22:00',
|
|
1970
|
+
onChange: e => updatePreferences({
|
|
1971
|
+
quietHours: {
|
|
1972
|
+
...preferences.quietHours,
|
|
1973
|
+
start: e.target.value
|
|
1974
|
+
}
|
|
1975
|
+
})
|
|
1976
|
+
}), /*#__PURE__*/React.createElement("span", null, "-"), /*#__PURE__*/React.createElement("input", {
|
|
1977
|
+
type: "time",
|
|
1978
|
+
value: preferences.quietHours?.end || '08:00',
|
|
1979
|
+
onChange: e => updatePreferences({
|
|
1980
|
+
quietHours: {
|
|
1981
|
+
...preferences.quietHours,
|
|
1982
|
+
end: e.target.value
|
|
1983
|
+
}
|
|
1984
|
+
})
|
|
1985
|
+
}))));
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* 开关项
|
|
1990
|
+
*/
|
|
1991
|
+
function ToggleItem({
|
|
1992
|
+
label,
|
|
1993
|
+
checked,
|
|
1994
|
+
onChange,
|
|
1995
|
+
disabled
|
|
1996
|
+
}) {
|
|
1997
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1998
|
+
className: "eco-push-toggle-item"
|
|
1999
|
+
}, /*#__PURE__*/React.createElement("span", null, label), /*#__PURE__*/React.createElement("button", {
|
|
2000
|
+
className: `eco-push-toggle ${checked ? 'active' : ''}`,
|
|
2001
|
+
onClick: onChange,
|
|
2002
|
+
disabled: disabled
|
|
2003
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
2004
|
+
className: "eco-push-toggle-handle"
|
|
2005
|
+
})));
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Toast通知
|
|
2010
|
+
*/
|
|
2011
|
+
function NotificationToast({
|
|
2012
|
+
notification,
|
|
2013
|
+
onClose,
|
|
2014
|
+
duration = 5000
|
|
2015
|
+
}) {
|
|
2016
|
+
React.useEffect(() => {
|
|
2017
|
+
if (duration > 0) {
|
|
2018
|
+
const timer = setTimeout(onClose, duration);
|
|
2019
|
+
return () => clearTimeout(timer);
|
|
2020
|
+
}
|
|
2021
|
+
}, [duration, onClose]);
|
|
2022
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
2023
|
+
className: "eco-push-toast"
|
|
2024
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
2025
|
+
className: "eco-push-toast-icon"
|
|
2026
|
+
}, getTypeIcon(notification.type)), /*#__PURE__*/React.createElement("div", {
|
|
2027
|
+
className: "eco-push-toast-content"
|
|
2028
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
2029
|
+
className: "eco-push-toast-title"
|
|
2030
|
+
}, notification.title), /*#__PURE__*/React.createElement("div", {
|
|
2031
|
+
className: "eco-push-toast-body"
|
|
2032
|
+
}, notification.body)), /*#__PURE__*/React.createElement("button", {
|
|
2033
|
+
className: "eco-push-toast-close",
|
|
2034
|
+
onClick: onClose
|
|
2035
|
+
}, "\xD7"));
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// 工具函数
|
|
2039
|
+
function getTypeIcon(type) {
|
|
2040
|
+
const icons = {
|
|
2041
|
+
system: '⚙️',
|
|
2042
|
+
marketing: '📢',
|
|
2043
|
+
transactional: '💳',
|
|
2044
|
+
social: '👥',
|
|
2045
|
+
reminder: '⏰',
|
|
2046
|
+
alert: '⚠️'
|
|
2047
|
+
};
|
|
2048
|
+
return icons[type] || '🔔';
|
|
2049
|
+
}
|
|
2050
|
+
function formatTime(timestamp) {
|
|
2051
|
+
const now = Date.now();
|
|
2052
|
+
const diff = now - new Date(timestamp).getTime();
|
|
2053
|
+
const minutes = Math.floor(diff / 60000);
|
|
2054
|
+
const hours = Math.floor(diff / 3600000);
|
|
2055
|
+
const days = Math.floor(diff / 86400000);
|
|
2056
|
+
if (minutes < 1) return t('now');
|
|
2057
|
+
if (minutes < 60) return t('minutesAgo', {
|
|
2058
|
+
n: minutes
|
|
2059
|
+
});
|
|
2060
|
+
if (hours < 24) return t('hoursAgo', {
|
|
2061
|
+
n: hours
|
|
2062
|
+
});
|
|
2063
|
+
if (days === 1) return t('yesterday');
|
|
2064
|
+
if (days < 7) return t('daysAgo', {
|
|
2065
|
+
n: days
|
|
2066
|
+
});
|
|
2067
|
+
return new Date(timestamp).toLocaleDateString();
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/**
|
|
2071
|
+
* @quantabit/push-sdk — 通用推送通知适配层
|
|
2072
|
+
*
|
|
2073
|
+
* 统一 API 接入多种推送服务:
|
|
2074
|
+
* - 浏览器原生 Web Push (Notification API + Service Worker)
|
|
2075
|
+
* - Firebase Cloud Messaging (FCM)
|
|
2076
|
+
* - OneSignal
|
|
2077
|
+
* - 极光推送 JPush
|
|
2078
|
+
* - WebSocket 自研推送
|
|
2079
|
+
* - Server-Sent Events (SSE)
|
|
2080
|
+
* - 自定义适配器
|
|
2081
|
+
*
|
|
2082
|
+
* 使用方式:
|
|
2083
|
+
* const push = new PushManager({ adapter: 'websocket', url: 'wss://...' });
|
|
2084
|
+
* push.subscribe();
|
|
2085
|
+
* push.onMessage(msg => console.log(msg));
|
|
2086
|
+
*/
|
|
2087
|
+
const getNotifications = pushApi.getHistory.bind(pushApi);
|
|
2088
|
+
const markAsRead = pushApi.markAsRead.bind(pushApi);
|
|
2089
|
+
const markAllAsRead = async () => pushApi.batchMarkAsRead([]);
|
|
2090
|
+
const send = pushApi.send.bind(pushApi);
|
|
2091
|
+
const schedule = pushApi.schedule.bind(pushApi);
|
|
2092
|
+
const getPreferences = pushApi.getPreferences.bind(pushApi);
|
|
2093
|
+
const updatePreferences = pushApi.updatePreferences.bind(pushApi);
|
|
2094
|
+
|
|
2095
|
+
exports.FCMAdapter = FCMAdapter;
|
|
2096
|
+
exports.JPushAdapter = JPushAdapter;
|
|
2097
|
+
exports.MockAdapter = MockAdapter;
|
|
2098
|
+
exports.NotificationBell = NotificationBell;
|
|
2099
|
+
exports.NotificationCenter = NotificationCenter;
|
|
2100
|
+
exports.NotificationChannel = NotificationChannel;
|
|
2101
|
+
exports.NotificationItem = NotificationItem;
|
|
2102
|
+
exports.NotificationSettings = NotificationSettings;
|
|
2103
|
+
exports.NotificationStatus = NotificationStatus;
|
|
2104
|
+
exports.NotificationToast = NotificationToast;
|
|
2105
|
+
exports.NotificationType = NotificationType;
|
|
2106
|
+
exports.OneSignalAdapter = OneSignalAdapter;
|
|
2107
|
+
exports.PermissionStatus = PermissionStatus;
|
|
2108
|
+
exports.PushApiClient = PushApiClient;
|
|
2109
|
+
exports.PushChannel = PushChannel;
|
|
2110
|
+
exports.PushManager = PushManager;
|
|
2111
|
+
exports.PushPermissionBanner = PushPermissionBanner;
|
|
2112
|
+
exports.PushPermissionPrompt = PushPermissionPrompt;
|
|
2113
|
+
exports.PushPriority = PushPriority;
|
|
2114
|
+
exports.PushProvider = PushProvider;
|
|
2115
|
+
exports.PushStatus = PushStatus;
|
|
2116
|
+
exports.SSEAdapter = SSEAdapter;
|
|
2117
|
+
exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
|
|
2118
|
+
exports.SubscriptionStatus = SubscriptionStatus;
|
|
2119
|
+
exports.WebPushAdapter = WebPushAdapter;
|
|
2120
|
+
exports.WebPushClient = WebPushClient;
|
|
2121
|
+
exports.WebSocketAdapter = WebSocketAdapter;
|
|
2122
|
+
exports.getLanguage = getLanguage;
|
|
2123
|
+
exports.getNotifications = getNotifications;
|
|
2124
|
+
exports.getPreferences = getPreferences;
|
|
2125
|
+
exports.markAllAsRead = markAllAsRead;
|
|
2126
|
+
exports.markAsRead = markAsRead;
|
|
2127
|
+
exports.messages = messages;
|
|
2128
|
+
exports.pushApi = pushApi;
|
|
2129
|
+
exports.schedule = schedule;
|
|
2130
|
+
exports.send = send;
|
|
2131
|
+
exports.setLanguage = setLanguage;
|
|
2132
|
+
exports.t = t;
|
|
2133
|
+
exports.updatePreferences = updatePreferences;
|
|
2134
|
+
exports.useNotificationPreferences = useNotificationPreferences;
|
|
2135
|
+
exports.useNotifications = useNotifications;
|
|
2136
|
+
exports.usePush = usePush;
|
|
2137
|
+
exports.usePushPermission = usePushPermission;
|
|
2138
|
+
exports.usePushSubscription = usePushSubscription;
|
|
2139
|
+
exports.useRealtimeNotifications = useRealtimeNotifications;
|
|
2140
|
+
exports.useSendNotification = useSendNotification;
|
|
2141
|
+
exports.webPushClient = webPushClient;
|
|
2142
|
+
//# sourceMappingURL=index.cjs.map
|