@quantabit/retention-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1174 @@
1
+ 'use strict';
2
+
3
+ var sdkConfig = require('@quantabit/sdk-config');
4
+ var React = require('react');
5
+
6
+ /**
7
+ * Retention SDK - API 客户端
8
+ * 用户留存分析系统后端接口封装
9
+ *
10
+ * 使用 BaseApiClient 基类简化代码
11
+ */
12
+
13
+
14
+ /**
15
+ * 留存 API 客户端
16
+ */
17
+ class RetentionApiClient extends sdkConfig.BaseApiClient {
18
+ constructor(config = {}) {
19
+ super('/retention', config);
20
+ }
21
+
22
+ // ============ 留存分析 ============
23
+
24
+ /**
25
+ * 获取留存概览
26
+ * @param {Object} params - 查询参数
27
+ */
28
+ async getOverview(params = {}) {
29
+ return this.get('/overview', params);
30
+ }
31
+
32
+ /**
33
+ * 获取留存率
34
+ * @param {Object} params - 分析参数
35
+ */
36
+ async getRetentionRate(params = {}) {
37
+ return this.post('/rate', params);
38
+ }
39
+
40
+ /**
41
+ * 获取留存趋势
42
+ * @param {Object} params - 查询参数
43
+ */
44
+ async getRetentionTrend(params = {}) {
45
+ return this.get('/trend', params);
46
+ }
47
+
48
+ /**
49
+ * 获取日留存
50
+ * @param {Object} params - 查询参数
51
+ */
52
+ async getDailyRetention(params = {}) {
53
+ return this.get('/daily', params);
54
+ }
55
+
56
+ /**
57
+ * 获取周留存
58
+ * @param {Object} params - 查询参数
59
+ */
60
+ async getWeeklyRetention(params = {}) {
61
+ return this.get('/weekly', params);
62
+ }
63
+
64
+ /**
65
+ * 获取月留存
66
+ * @param {Object} params - 查询参数
67
+ */
68
+ async getMonthlyRetention(params = {}) {
69
+ return this.get('/monthly', params);
70
+ }
71
+
72
+ // ============ Cohort 分析 ============
73
+
74
+ /**
75
+ * 获取 Cohort 分析
76
+ * @param {Object} params - 分析参数
77
+ */
78
+ async getCohortAnalysis(params = {}) {
79
+ return this.post('/cohort', params);
80
+ }
81
+
82
+ /**
83
+ * 获取 Cohort 列表
84
+ * @param {Object} params - 查询参数
85
+ */
86
+ async getCohorts(params = {}) {
87
+ return this.get('/cohorts', params);
88
+ }
89
+
90
+ /**
91
+ * 创建 Cohort
92
+ * @param {Object} cohort - Cohort 定义
93
+ */
94
+ async createCohort(cohort) {
95
+ return this.post('/cohorts', cohort);
96
+ }
97
+
98
+ /**
99
+ * 对比 Cohort
100
+ * @param {string[]} cohortIds - Cohort ID 列表
101
+ * @param {Object} params - 对比参数
102
+ */
103
+ async compareCohorts(cohortIds, params = {}) {
104
+ return this.post('/cohorts/compare', {
105
+ cohort_ids: cohortIds,
106
+ ...params
107
+ });
108
+ }
109
+
110
+ // ============ 流失分析 ============
111
+
112
+ /**
113
+ * 获取流失用户
114
+ * @param {Object} params - 查询参数
115
+ */
116
+ async getChurnedUsers(params = {}) {
117
+ return this.get('/churned', params);
118
+ }
119
+
120
+ /**
121
+ * 获取流失原因分析
122
+ * @param {Object} params - 分析参数
123
+ */
124
+ async getChurnReasons(params = {}) {
125
+ return this.get('/churned/reasons', params);
126
+ }
127
+
128
+ /**
129
+ * 获取流失预警
130
+ * @param {Object} params - 查询参数
131
+ */
132
+ async getChurnRiskUsers(params = {}) {
133
+ return this.get('/churn-risk', params);
134
+ }
135
+
136
+ /**
137
+ * 获取流失趋势
138
+ * @param {Object} params - 查询参数
139
+ */
140
+ async getChurnTrend(params = {}) {
141
+ return this.get('/churned/trend', params);
142
+ }
143
+
144
+ // ============ LTV 分析 ============
145
+
146
+ /**
147
+ * 获取 LTV 概览
148
+ * @param {Object} params - 查询参数
149
+ */
150
+ async getLTVOverview(params = {}) {
151
+ return this.get('/ltv/overview', params);
152
+ }
153
+
154
+ /**
155
+ * 获取用户 LTV
156
+ * @param {string} userId - 用户 ID
157
+ */
158
+ async getUserLTV(userId) {
159
+ return this.get(`/ltv/users/${userId}`);
160
+ }
161
+
162
+ /**
163
+ * 获取 LTV 分布
164
+ * @param {Object} params - 查询参数
165
+ */
166
+ async getLTVDistribution(params = {}) {
167
+ return this.get('/ltv/distribution', params);
168
+ }
169
+
170
+ /**
171
+ * 预测 LTV
172
+ * @param {Object} params - 预测参数
173
+ */
174
+ async predictLTV(params = {}) {
175
+ return this.post('/ltv/predict', params);
176
+ }
177
+
178
+ // ============ 分组分析 ============
179
+
180
+ /**
181
+ * 按属性分组分析
182
+ * @param {string} property - 分组属性
183
+ * @param {Object} params - 分析参数
184
+ */
185
+ async analyzeByProperty(property, params = {}) {
186
+ return this.post('/analyze/group', {
187
+ property,
188
+ ...params
189
+ });
190
+ }
191
+
192
+ /**
193
+ * 按人群分析
194
+ * @param {string} segmentId - 人群 ID
195
+ * @param {Object} params - 分析参数
196
+ */
197
+ async analyzeBySegment(segmentId, params = {}) {
198
+ return this.post('/analyze/segment', {
199
+ segment_id: segmentId,
200
+ ...params
201
+ });
202
+ }
203
+
204
+ // ============ 报表导出 ============
205
+
206
+ /**
207
+ * 导出留存报告
208
+ * @param {Object} options - 导出选项
209
+ */
210
+ async exportReport(options) {
211
+ return this.post('/export', options);
212
+ }
213
+ }
214
+
215
+ // 创建默认实例
216
+ const retentionApi = new RetentionApiClient();
217
+
218
+ /**
219
+ * Retention SDK - 类型定义
220
+ */
221
+
222
+ // 留存周期类型
223
+ const RetentionPeriod = {
224
+ DAY_1: 'day_1',
225
+ DAY_3: 'day_3',
226
+ DAY_7: 'day_7',
227
+ DAY_14: 'day_14',
228
+ DAY_30: 'day_30',
229
+ DAY_60: 'day_60',
230
+ DAY_90: 'day_90'
231
+ };
232
+ const CohortType = {};
233
+ const ChurnRisk = {};
234
+
235
+ /**
236
+ * Retention SDK - 国际化
237
+ * Cohort留存分析多语言支持
238
+ */
239
+
240
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
241
+ const messages = {
242
+ zh: {
243
+ // 留存
244
+ retention: '留存',
245
+ retentionRate: '留存率',
246
+ retentionAnalysis: '留存分析',
247
+ cohortAnalysis: 'Cohort分析',
248
+ // 时间
249
+ day1: '次日',
250
+ day3: '3日',
251
+ day7: '7日',
252
+ day14: '14日',
253
+ day30: '30日',
254
+ day60: '60日',
255
+ day90: '90日',
256
+ // 用户群
257
+ cohort: 'Cohort',
258
+ cohorts: '用户群',
259
+ newUsers: '新用户',
260
+ activeUsers: '活跃用户',
261
+ paidUsers: '付费用户',
262
+ // 指标
263
+ users: '用户数',
264
+ retention: '留存',
265
+ churn: '流失',
266
+ churnRate: '流失率',
267
+ lifetime: '生命周期',
268
+ ltv: 'LTV',
269
+ averageLtv: '平均LTV',
270
+ // 分析维度
271
+ byDate: '按日期',
272
+ byChannel: '按渠道',
273
+ byPlatform: '按平台',
274
+ byRegion: '按地区',
275
+ byVersion: '按版本',
276
+ // 图表
277
+ heatmap: '热力图',
278
+ curve: '曲线图',
279
+ table: '表格',
280
+ // 时间范围
281
+ timeRange: '时间范围',
282
+ last7Days: '近7天',
283
+ last14Days: '近14天',
284
+ last30Days: '近30天',
285
+ last90Days: '近90天',
286
+ lastYear: '近一年',
287
+ custom: '自定义',
288
+ // 对比
289
+ comparison: '对比分析',
290
+ benchmark: '基准',
291
+ vsLastPeriod: '环比',
292
+ vsLastYear: '同比',
293
+ // 洞察
294
+ insights: '洞察',
295
+ trend: '趋势',
296
+ improving: '改善中',
297
+ declining: '下降中',
298
+ stable: '稳定',
299
+ // 状态
300
+ loading: '加载中...',
301
+ noData: '暂无数据',
302
+ error: '加载失败',
303
+ // 操作
304
+ export: '导出',
305
+ refresh: '刷新',
306
+ filter: '筛选',
307
+ // 预测
308
+ forecast: '预测',
309
+ predictedRetention: '预测留存',
310
+ confidence: '置信度'
311
+ },
312
+ en: {
313
+ retention: 'Retention',
314
+ retentionRate: 'Retention Rate',
315
+ retentionAnalysis: 'Retention Analysis',
316
+ cohortAnalysis: 'Cohort Analysis',
317
+ day1: 'Day 1',
318
+ day3: 'Day 3',
319
+ day7: 'Day 7',
320
+ day14: 'Day 14',
321
+ day30: 'Day 30',
322
+ day60: 'Day 60',
323
+ day90: 'Day 90',
324
+ cohort: 'Cohort',
325
+ cohorts: 'Cohorts',
326
+ newUsers: 'New Users',
327
+ activeUsers: 'Active Users',
328
+ paidUsers: 'Paid Users',
329
+ users: 'Users',
330
+ churn: 'Churn',
331
+ churnRate: 'Churn Rate',
332
+ lifetime: 'Lifetime',
333
+ ltv: 'LTV',
334
+ averageLtv: 'Average LTV',
335
+ byDate: 'By Date',
336
+ byChannel: 'By Channel',
337
+ byPlatform: 'By Platform',
338
+ byRegion: 'By Region',
339
+ byVersion: 'By Version',
340
+ heatmap: 'Heatmap',
341
+ curve: 'Curve',
342
+ table: 'Table',
343
+ timeRange: 'Time Range',
344
+ last7Days: 'Last 7 Days',
345
+ last14Days: 'Last 14 Days',
346
+ last30Days: 'Last 30 Days',
347
+ last90Days: 'Last 90 Days',
348
+ lastYear: 'Last Year',
349
+ custom: 'Custom',
350
+ comparison: 'Comparison',
351
+ benchmark: 'Benchmark',
352
+ vsLastPeriod: 'vs Last Period',
353
+ vsLastYear: 'vs Last Year',
354
+ insights: 'Insights',
355
+ trend: 'Trend',
356
+ improving: 'Improving',
357
+ declining: 'Declining',
358
+ stable: 'Stable',
359
+ loading: 'Loading...',
360
+ noData: 'No Data',
361
+ error: 'Error',
362
+ export: 'Export',
363
+ refresh: 'Refresh',
364
+ filter: 'Filter',
365
+ forecast: 'Forecast',
366
+ predictedRetention: 'Predicted Retention',
367
+ confidence: 'Confidence'
368
+ },
369
+ ja: {
370
+ retention: 'リテンション',
371
+ retentionRate: 'リテンション率',
372
+ retentionAnalysis: 'リテンション分析',
373
+ cohortAnalysis: 'コホート分析',
374
+ day1: '翌日',
375
+ day3: '3日後',
376
+ day7: '7日後',
377
+ day14: '14日後',
378
+ day30: '30日後',
379
+ day60: '60日後',
380
+ day90: '90日後',
381
+ cohort: 'コホート',
382
+ cohorts: 'コホート',
383
+ newUsers: '新規ユーザー',
384
+ activeUsers: 'アクティブユーザー',
385
+ paidUsers: '課金ユーザー',
386
+ users: 'ユーザー数',
387
+ churn: '離脱',
388
+ churnRate: '離脱率',
389
+ lifetime: 'ライフタイム',
390
+ ltv: 'LTV',
391
+ averageLtv: '平均LTV',
392
+ byDate: '日付別',
393
+ byChannel: 'チャネル別',
394
+ byPlatform: 'プラットフォーム別',
395
+ byRegion: '地域別',
396
+ byVersion: 'バージョン別',
397
+ heatmap: 'ヒートマップ',
398
+ curve: '曲線',
399
+ table: 'テーブル',
400
+ timeRange: '期間',
401
+ last7Days: '過去7日間',
402
+ last14Days: '過去14日間',
403
+ last30Days: '過去30日間',
404
+ last90Days: '過去90日間',
405
+ lastYear: '過去1年間',
406
+ custom: 'カスタム',
407
+ comparison: '比較',
408
+ benchmark: 'ベンチマーク',
409
+ vsLastPeriod: '前期比',
410
+ vsLastYear: '前年比',
411
+ insights: 'インサイト',
412
+ trend: 'トレンド',
413
+ improving: '改善中',
414
+ declining: '低下中',
415
+ stable: '安定',
416
+ loading: '読み込み中...',
417
+ noData: 'データなし',
418
+ error: 'エラー',
419
+ export: 'エクスポート',
420
+ refresh: '更新',
421
+ filter: 'フィルター',
422
+ forecast: '予測',
423
+ predictedRetention: '予測リテンション',
424
+ confidence: '信頼度'
425
+ },
426
+ ko: {
427
+ retention: '리텐션',
428
+ retentionRate: '리텐션율',
429
+ retentionAnalysis: '리텐션 분석',
430
+ cohortAnalysis: '코호트 분석',
431
+ day1: 'D+1',
432
+ day3: 'D+3',
433
+ day7: 'D+7',
434
+ day14: 'D+14',
435
+ day30: 'D+30',
436
+ day60: 'D+60',
437
+ day90: 'D+90',
438
+ cohort: '코호트',
439
+ cohorts: '코호트',
440
+ newUsers: '신규 사용자',
441
+ activeUsers: '활성 사용자',
442
+ paidUsers: '결제 사용자',
443
+ users: '사용자 수',
444
+ churn: '이탈',
445
+ churnRate: '이탈률',
446
+ lifetime: '생애 주기',
447
+ ltv: 'LTV',
448
+ averageLtv: '평균 LTV',
449
+ byDate: '날짜별',
450
+ byChannel: '채널별',
451
+ byPlatform: '플랫폼별',
452
+ byRegion: '지역별',
453
+ byVersion: '버전별',
454
+ heatmap: '히트맵',
455
+ curve: '곡선',
456
+ table: '테이블',
457
+ timeRange: '기간',
458
+ last7Days: '최근 7일',
459
+ last14Days: '최근 14일',
460
+ last30Days: '최근 30일',
461
+ last90Days: '최근 90일',
462
+ lastYear: '최근 1년',
463
+ custom: '사용자 지정',
464
+ comparison: '비교',
465
+ benchmark: '벤치마크',
466
+ vsLastPeriod: '전기 대비',
467
+ vsLastYear: '전년 대비',
468
+ insights: '인사이트',
469
+ trend: '트렌드',
470
+ improving: '개선 중',
471
+ declining: '하락 중',
472
+ stable: '안정',
473
+ loading: '로딩 중...',
474
+ noData: '데이터 없음',
475
+ error: '오류',
476
+ export: '내보내기',
477
+ refresh: '새로고침',
478
+ filter: '필터',
479
+ forecast: '예측',
480
+ predictedRetention: '예측 리텐션',
481
+ confidence: '신뢰도'
482
+ }
483
+ };
484
+ let currentLanguage = 'zh';
485
+ function setLanguage(lang) {
486
+ if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
487
+ }
488
+ function getLanguage() {
489
+ return currentLanguage;
490
+ }
491
+ function t(key) {
492
+ return (messages[currentLanguage] || messages.en)[key] || key;
493
+ }
494
+
495
+ /**
496
+ * Retention SDK - React Hooks
497
+ * 留存分析相关的状态管理
498
+ */
499
+
500
+
501
+ /**
502
+ * 获取留存数据
503
+ */
504
+ function useRetention(options = {}) {
505
+ const [data, setData] = React.useState(null);
506
+ const [loading, setLoading] = React.useState(true);
507
+ const [error, setError] = React.useState(null);
508
+ const {
509
+ timeRange = '30d',
510
+ cohortType = 'new',
511
+ dimension = null
512
+ } = options;
513
+ const fetchRetention = React.useCallback(async () => {
514
+ try {
515
+ setLoading(true);
516
+ const response = await retentionApi.getRetention({
517
+ timeRange,
518
+ cohortType,
519
+ dimension
520
+ });
521
+ setData(response);
522
+ setError(null);
523
+ } catch (err) {
524
+ setError(err.message);
525
+ } finally {
526
+ setLoading(false);
527
+ }
528
+ }, [timeRange, cohortType, dimension]);
529
+ React.useEffect(() => {
530
+ fetchRetention();
531
+ }, [fetchRetention]);
532
+
533
+ // 计算关键指标
534
+ const metrics = React.useMemo(() => {
535
+ if (!data || !data.cohorts) return null;
536
+ const cohorts = data.cohorts;
537
+ const avgDay1 = cohorts.reduce((sum, c) => sum + (c.day1 || 0), 0) / cohorts.length;
538
+ const avgDay7 = cohorts.reduce((sum, c) => sum + (c.day7 || 0), 0) / cohorts.length;
539
+ const avgDay30 = cohorts.reduce((sum, c) => sum + (c.day30 || 0), 0) / cohorts.length;
540
+ return {
541
+ averageDay1: avgDay1,
542
+ averageDay7: avgDay7,
543
+ averageDay30: avgDay30,
544
+ totalCohorts: cohorts.length,
545
+ totalUsers: cohorts.reduce((sum, c) => sum + (c.users || 0), 0)
546
+ };
547
+ }, [data]);
548
+ return {
549
+ data,
550
+ metrics,
551
+ loading,
552
+ error,
553
+ refresh: fetchRetention
554
+ };
555
+ }
556
+
557
+ /**
558
+ * Cohort分析
559
+ */
560
+ function useCohortAnalysis(options = {}) {
561
+ const [cohorts, setCohorts] = React.useState([]);
562
+ const [loading, setLoading] = React.useState(true);
563
+ const [error, setError] = React.useState(null);
564
+ const {
565
+ startDate,
566
+ endDate,
567
+ granularity = 'day'
568
+ } = options;
569
+ const analyze = React.useCallback(async () => {
570
+ try {
571
+ setLoading(true);
572
+ const response = await retentionApi.getCohortAnalysis({
573
+ startDate,
574
+ endDate,
575
+ granularity
576
+ });
577
+ setCohorts(response.cohorts || []);
578
+ setError(null);
579
+ } catch (err) {
580
+ setError(err.message);
581
+ } finally {
582
+ setLoading(false);
583
+ }
584
+ }, [startDate, endDate, granularity]);
585
+ React.useEffect(() => {
586
+ if (startDate && endDate) {
587
+ analyze();
588
+ }
589
+ }, [analyze, startDate, endDate]);
590
+ return {
591
+ cohorts,
592
+ loading,
593
+ error,
594
+ refresh: analyze
595
+ };
596
+ }
597
+
598
+ /**
599
+ * 留存曲线
600
+ */
601
+ function useRetentionCurve(cohortId, options = {}) {
602
+ const [curve, setCurve] = React.useState(null);
603
+ const [loading, setLoading] = React.useState(true);
604
+ const [error, setError] = React.useState(null);
605
+ const {
606
+ days = 30
607
+ } = options;
608
+ const fetchCurve = React.useCallback(async () => {
609
+ if (!cohortId) return;
610
+ try {
611
+ setLoading(true);
612
+ const response = await retentionApi.getRetentionCurve(cohortId, {
613
+ days
614
+ });
615
+ setCurve(response);
616
+ setError(null);
617
+ } catch (err) {
618
+ setError(err.message);
619
+ } finally {
620
+ setLoading(false);
621
+ }
622
+ }, [cohortId, days]);
623
+ React.useEffect(() => {
624
+ fetchCurve();
625
+ }, [fetchCurve]);
626
+ return {
627
+ curve,
628
+ loading,
629
+ error,
630
+ refresh: fetchCurve
631
+ };
632
+ }
633
+
634
+ /**
635
+ * 留存对比
636
+ */
637
+ function useRetentionComparison(options = {}) {
638
+ const [comparison, setComparison] = React.useState(null);
639
+ const [loading, setLoading] = React.useState(true);
640
+ const [error, setError] = React.useState(null);
641
+ const {
642
+ segments = [],
643
+ periods = []
644
+ } = options;
645
+ const compare = React.useCallback(async () => {
646
+ try {
647
+ setLoading(true);
648
+ const response = await retentionApi.compare({
649
+ segments,
650
+ periods
651
+ });
652
+ setComparison(response);
653
+ setError(null);
654
+ } catch (err) {
655
+ setError(err.message);
656
+ } finally {
657
+ setLoading(false);
658
+ }
659
+ }, [segments, periods]);
660
+ React.useEffect(() => {
661
+ if (segments.length > 0 || periods.length > 0) {
662
+ compare();
663
+ }
664
+ }, [compare, segments, periods]);
665
+ return {
666
+ comparison,
667
+ loading,
668
+ error,
669
+ refresh: compare
670
+ };
671
+ }
672
+
673
+ /**
674
+ * 流失分析
675
+ */
676
+ function useChurnAnalysis(options = {}) {
677
+ const [churnData, setChurnData] = React.useState(null);
678
+ const [loading, setLoading] = React.useState(true);
679
+ const [error, setError] = React.useState(null);
680
+ const {
681
+ timeRange = '30d',
682
+ riskThreshold = 0.7
683
+ } = options;
684
+ const analyze = React.useCallback(async () => {
685
+ try {
686
+ setLoading(true);
687
+ const response = await retentionApi.getChurnAnalysis({
688
+ timeRange,
689
+ riskThreshold
690
+ });
691
+ setChurnData(response);
692
+ setError(null);
693
+ } catch (err) {
694
+ setError(err.message);
695
+ } finally {
696
+ setLoading(false);
697
+ }
698
+ }, [timeRange, riskThreshold]);
699
+ React.useEffect(() => {
700
+ analyze();
701
+ }, [analyze]);
702
+
703
+ // 高风险用户
704
+ const atRiskUsers = React.useMemo(() => {
705
+ return churnData?.users?.filter(u => u.churnRisk >= riskThreshold) || [];
706
+ }, [churnData, riskThreshold]);
707
+ return {
708
+ churnData,
709
+ atRiskUsers,
710
+ loading,
711
+ error,
712
+ refresh: analyze
713
+ };
714
+ }
715
+
716
+ /**
717
+ * LTV分析
718
+ */
719
+ function useLtvAnalysis(options = {}) {
720
+ const [ltvData, setLtvData] = React.useState(null);
721
+ const [loading, setLoading] = React.useState(true);
722
+ const [error, setError] = React.useState(null);
723
+ const {
724
+ cohortType = 'all',
725
+ timeRange = '90d'
726
+ } = options;
727
+ const analyze = React.useCallback(async () => {
728
+ try {
729
+ setLoading(true);
730
+ const response = await retentionApi.getLtvAnalysis({
731
+ cohortType,
732
+ timeRange
733
+ });
734
+ setLtvData(response);
735
+ setError(null);
736
+ } catch (err) {
737
+ setError(err.message);
738
+ } finally {
739
+ setLoading(false);
740
+ }
741
+ }, [cohortType, timeRange]);
742
+ React.useEffect(() => {
743
+ analyze();
744
+ }, [analyze]);
745
+ return {
746
+ ltvData,
747
+ loading,
748
+ error,
749
+ refresh: analyze
750
+ };
751
+ }
752
+
753
+ /**
754
+ * 留存预测
755
+ */
756
+ function useRetentionForecast(cohortId, options = {}) {
757
+ const [forecast, setForecast] = React.useState(null);
758
+ const [loading, setLoading] = React.useState(true);
759
+ const [error, setError] = React.useState(null);
760
+ const {
761
+ forecastDays = 30
762
+ } = options;
763
+ const predict = React.useCallback(async () => {
764
+ if (!cohortId) return;
765
+ try {
766
+ setLoading(true);
767
+ const response = await retentionApi.forecast(cohortId, {
768
+ forecastDays
769
+ });
770
+ setForecast(response);
771
+ setError(null);
772
+ } catch (err) {
773
+ setError(err.message);
774
+ } finally {
775
+ setLoading(false);
776
+ }
777
+ }, [cohortId, forecastDays]);
778
+ React.useEffect(() => {
779
+ predict();
780
+ }, [predict]);
781
+ return {
782
+ forecast,
783
+ loading,
784
+ error,
785
+ refresh: predict
786
+ };
787
+ }
788
+
789
+ /**
790
+ * Retention SDK - React 组件
791
+ * 留存分析可视化组件
792
+ */
793
+
794
+
795
+ /**
796
+ * Cohort热力图
797
+ */
798
+ function CohortHeatmap({
799
+ data,
800
+ showLabels = true,
801
+ colorScheme = 'blue'
802
+ }) {
803
+ if (!data || !data.cohorts || data.cohorts.length === 0) {
804
+ return /*#__PURE__*/React.createElement("div", {
805
+ className: "eco-retention-empty"
806
+ }, t('noData'));
807
+ }
808
+ const {
809
+ cohorts,
810
+ days
811
+ } = data;
812
+
813
+ // 获取颜色
814
+ const getColor = value => {
815
+ if (value === null || value === undefined) return 'transparent';
816
+ const intensity = Math.min(1, Math.max(0, value));
817
+ if (colorScheme === 'green') {
818
+ return `rgba(16, 185, 129, ${0.1 + intensity * 0.9})`;
819
+ }
820
+ return `rgba(99, 102, 241, ${0.1 + intensity * 0.9})`;
821
+ };
822
+ return /*#__PURE__*/React.createElement("div", {
823
+ className: "eco-retention-heatmap"
824
+ }, /*#__PURE__*/React.createElement("div", {
825
+ className: "eco-retention-heatmap-header"
826
+ }, /*#__PURE__*/React.createElement("div", {
827
+ className: "eco-retention-heatmap-label"
828
+ }, t('cohort')), days.map((day, i) => /*#__PURE__*/React.createElement("div", {
829
+ key: i,
830
+ className: "eco-retention-heatmap-day"
831
+ }, t(`day${day}`) || `D+${day}`))), /*#__PURE__*/React.createElement("div", {
832
+ className: "eco-retention-heatmap-body"
833
+ }, cohorts.map((cohort, rowIndex) => /*#__PURE__*/React.createElement("div", {
834
+ key: cohort.date || rowIndex,
835
+ className: "eco-retention-heatmap-row"
836
+ }, /*#__PURE__*/React.createElement("div", {
837
+ className: "eco-retention-heatmap-cohort"
838
+ }, /*#__PURE__*/React.createElement("span", {
839
+ className: "eco-retention-cohort-date"
840
+ }, cohort.date), /*#__PURE__*/React.createElement("span", {
841
+ className: "eco-retention-cohort-users"
842
+ }, cohort.users.toLocaleString())), days.map((day, colIndex) => {
843
+ const value = cohort[`day${day}`];
844
+ return /*#__PURE__*/React.createElement("div", {
845
+ key: colIndex,
846
+ className: "eco-retention-heatmap-cell",
847
+ style: {
848
+ backgroundColor: getColor(value)
849
+ },
850
+ title: value !== null ? `${(value * 100).toFixed(1)}%` : ''
851
+ }, showLabels && value !== null && /*#__PURE__*/React.createElement("span", {
852
+ className: "eco-retention-cell-value"
853
+ }, (value * 100).toFixed(0), "%"));
854
+ })))), /*#__PURE__*/React.createElement("div", {
855
+ className: "eco-retention-heatmap-legend"
856
+ }, /*#__PURE__*/React.createElement("span", null, "0%"), /*#__PURE__*/React.createElement("div", {
857
+ className: "eco-retention-legend-gradient"
858
+ }), /*#__PURE__*/React.createElement("span", null, "100%")));
859
+ }
860
+
861
+ /**
862
+ * 留存曲线图
863
+ */
864
+ function RetentionCurve({
865
+ data,
866
+ showBenchmark = false,
867
+ benchmarkData = null
868
+ }) {
869
+ if (!data || !data.points || data.points.length === 0) {
870
+ return /*#__PURE__*/React.createElement("div", {
871
+ className: "eco-retention-empty"
872
+ }, t('noData'));
873
+ }
874
+ const {
875
+ points
876
+ } = data;
877
+ const pathPoints = points.map((p, i) => {
878
+ const x = i / (points.length - 1) * 100;
879
+ const y = 100 - p.value * 100;
880
+ return `${x},${y}`;
881
+ }).join(' ');
882
+ const benchmarkPoints = benchmarkData?.points?.map((p, i) => {
883
+ const x = i / (benchmarkData.points.length - 1) * 100;
884
+ const y = 100 - p.value * 100;
885
+ return `${x},${y}`;
886
+ }).join(' ');
887
+ return /*#__PURE__*/React.createElement("div", {
888
+ className: "eco-retention-curve"
889
+ }, /*#__PURE__*/React.createElement("svg", {
890
+ className: "eco-retention-curve-svg",
891
+ viewBox: "0 0 100 100",
892
+ preserveAspectRatio: "none"
893
+ }, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("linearGradient", {
894
+ id: "retentionGradient",
895
+ x1: "0%",
896
+ y1: "0%",
897
+ x2: "0%",
898
+ y2: "100%"
899
+ }, /*#__PURE__*/React.createElement("stop", {
900
+ offset: "0%",
901
+ stopColor: "#6366f1",
902
+ stopOpacity: "0.3"
903
+ }), /*#__PURE__*/React.createElement("stop", {
904
+ offset: "100%",
905
+ stopColor: "#6366f1",
906
+ stopOpacity: "0"
907
+ }))), [25, 50, 75].map(y => /*#__PURE__*/React.createElement("line", {
908
+ key: y,
909
+ x1: "0",
910
+ y1: y,
911
+ x2: "100",
912
+ y2: y,
913
+ stroke: "#e2e8f0",
914
+ strokeWidth: "0.5",
915
+ vectorEffect: "non-scaling-stroke"
916
+ })), showBenchmark && benchmarkPoints && /*#__PURE__*/React.createElement("polyline", {
917
+ points: benchmarkPoints,
918
+ fill: "none",
919
+ stroke: "#94a3b8",
920
+ strokeWidth: "2",
921
+ strokeDasharray: "4,4",
922
+ vectorEffect: "non-scaling-stroke"
923
+ }), /*#__PURE__*/React.createElement("polygon", {
924
+ points: `0,100 ${pathPoints} 100,100`,
925
+ fill: "url(#retentionGradient)"
926
+ }), /*#__PURE__*/React.createElement("polyline", {
927
+ points: pathPoints,
928
+ fill: "none",
929
+ stroke: "#6366f1",
930
+ strokeWidth: "2",
931
+ vectorEffect: "non-scaling-stroke"
932
+ }), points.map((p, i) => {
933
+ const x = i / (points.length - 1) * 100;
934
+ const y = 100 - p.value * 100;
935
+ return /*#__PURE__*/React.createElement("circle", {
936
+ key: i,
937
+ cx: x,
938
+ cy: y,
939
+ r: "3",
940
+ fill: "#6366f1",
941
+ vectorEffect: "non-scaling-stroke"
942
+ });
943
+ })), /*#__PURE__*/React.createElement("div", {
944
+ className: "eco-retention-curve-labels"
945
+ }, /*#__PURE__*/React.createElement("div", {
946
+ className: "eco-retention-curve-y-labels"
947
+ }, /*#__PURE__*/React.createElement("span", null, "100%"), /*#__PURE__*/React.createElement("span", null, "75%"), /*#__PURE__*/React.createElement("span", null, "50%"), /*#__PURE__*/React.createElement("span", null, "25%"), /*#__PURE__*/React.createElement("span", null, "0%")), /*#__PURE__*/React.createElement("div", {
948
+ className: "eco-retention-curve-x-labels"
949
+ }, points.filter((_, i) => i % 7 === 0 || i === points.length - 1).map((p, i) => /*#__PURE__*/React.createElement("span", {
950
+ key: i
951
+ }, "D+", p.day)))));
952
+ }
953
+
954
+ /**
955
+ * 留存指标卡片
956
+ */
957
+ function RetentionMetricCard({
958
+ title,
959
+ value,
960
+ trend,
961
+ benchmark
962
+ }) {
963
+ const trendClass = trend > 0 ? 'up' : trend < 0 ? 'down' : 'stable';
964
+ const vsBenchmark = benchmark ? value - benchmark : null;
965
+ return /*#__PURE__*/React.createElement("div", {
966
+ className: "eco-retention-metric-card"
967
+ }, /*#__PURE__*/React.createElement("div", {
968
+ className: "eco-retention-metric-title"
969
+ }, title), /*#__PURE__*/React.createElement("div", {
970
+ className: "eco-retention-metric-value"
971
+ }, typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : value), /*#__PURE__*/React.createElement("div", {
972
+ className: "eco-retention-metric-footer"
973
+ }, trend !== undefined && /*#__PURE__*/React.createElement("span", {
974
+ className: `eco-retention-metric-trend ${trendClass}`
975
+ }, trend > 0 ? '↑' : trend < 0 ? '↓' : '–', Math.abs(trend * 100).toFixed(1), "%"), vsBenchmark !== null && /*#__PURE__*/React.createElement("span", {
976
+ className: `eco-retention-metric-benchmark ${vsBenchmark >= 0 ? 'positive' : 'negative'}`
977
+ }, "vs ", t('benchmark'), ": ", vsBenchmark >= 0 ? '+' : '', (vsBenchmark * 100).toFixed(1), "%")));
978
+ }
979
+
980
+ /**
981
+ * 留存概览面板
982
+ */
983
+ function RetentionOverview({
984
+ timeRange = '30d'
985
+ }) {
986
+ const {
987
+ data,
988
+ metrics,
989
+ loading,
990
+ error
991
+ } = useRetention({
992
+ timeRange
993
+ });
994
+ if (loading) {
995
+ return /*#__PURE__*/React.createElement("div", {
996
+ className: "eco-retention-loading"
997
+ }, /*#__PURE__*/React.createElement("div", {
998
+ className: "eco-retention-spinner"
999
+ }), /*#__PURE__*/React.createElement("span", null, t('loading')));
1000
+ }
1001
+ if (error) {
1002
+ return /*#__PURE__*/React.createElement("div", {
1003
+ className: "eco-retention-error"
1004
+ }, error);
1005
+ }
1006
+ if (!metrics) {
1007
+ return /*#__PURE__*/React.createElement("div", {
1008
+ className: "eco-retention-empty"
1009
+ }, t('noData'));
1010
+ }
1011
+ return /*#__PURE__*/React.createElement("div", {
1012
+ className: "eco-retention-overview"
1013
+ }, /*#__PURE__*/React.createElement("h3", {
1014
+ className: "eco-retention-overview-title"
1015
+ }, t('retentionAnalysis')), /*#__PURE__*/React.createElement("div", {
1016
+ className: "eco-retention-metrics-grid"
1017
+ }, /*#__PURE__*/React.createElement(RetentionMetricCard, {
1018
+ title: t('day1'),
1019
+ value: metrics.averageDay1
1020
+ }), /*#__PURE__*/React.createElement(RetentionMetricCard, {
1021
+ title: t('day7'),
1022
+ value: metrics.averageDay7
1023
+ }), /*#__PURE__*/React.createElement(RetentionMetricCard, {
1024
+ title: t('day30'),
1025
+ value: metrics.averageDay30
1026
+ }), /*#__PURE__*/React.createElement(RetentionMetricCard, {
1027
+ title: t('users'),
1028
+ value: metrics.totalUsers.toLocaleString()
1029
+ })));
1030
+ }
1031
+
1032
+ /**
1033
+ * 时间范围选择器
1034
+ */
1035
+ function TimeRangeSelector({
1036
+ value,
1037
+ onChange
1038
+ }) {
1039
+ const options = [{
1040
+ id: 'last7Days',
1041
+ label: t('last7Days')
1042
+ }, {
1043
+ id: 'last14Days',
1044
+ label: t('last14Days')
1045
+ }, {
1046
+ id: 'last30Days',
1047
+ label: t('last30Days')
1048
+ }, {
1049
+ id: 'last90Days',
1050
+ label: t('last90Days')
1051
+ }, {
1052
+ id: 'lastYear',
1053
+ label: t('lastYear')
1054
+ }];
1055
+ return /*#__PURE__*/React.createElement("div", {
1056
+ className: "eco-retention-timerange"
1057
+ }, options.map(opt => /*#__PURE__*/React.createElement("button", {
1058
+ key: opt.id,
1059
+ className: `eco-retention-timerange-btn ${value === opt.id ? 'active' : ''}`,
1060
+ onClick: () => onChange(opt.id)
1061
+ }, opt.label)));
1062
+ }
1063
+
1064
+ /**
1065
+ * 视图切换器
1066
+ */
1067
+ function ViewToggle({
1068
+ value,
1069
+ onChange
1070
+ }) {
1071
+ const views = [{
1072
+ id: 'heatmap',
1073
+ label: t('heatmap'),
1074
+ icon: '▦'
1075
+ }, {
1076
+ id: 'curve',
1077
+ label: t('curve'),
1078
+ icon: '📈'
1079
+ }, {
1080
+ id: 'table',
1081
+ label: t('table'),
1082
+ icon: '☷'
1083
+ }];
1084
+ return /*#__PURE__*/React.createElement("div", {
1085
+ className: "eco-retention-view-toggle"
1086
+ }, views.map(view => /*#__PURE__*/React.createElement("button", {
1087
+ key: view.id,
1088
+ className: `eco-retention-view-btn ${value === view.id ? 'active' : ''}`,
1089
+ onClick: () => onChange(view.id),
1090
+ title: view.label
1091
+ }, /*#__PURE__*/React.createElement("span", null, view.icon))));
1092
+ }
1093
+
1094
+ /**
1095
+ * 留存表格
1096
+ */
1097
+ function RetentionTable({
1098
+ data
1099
+ }) {
1100
+ if (!data || !data.cohorts) {
1101
+ return /*#__PURE__*/React.createElement("div", {
1102
+ className: "eco-retention-empty"
1103
+ }, t('noData'));
1104
+ }
1105
+ const {
1106
+ cohorts,
1107
+ days
1108
+ } = data;
1109
+ return /*#__PURE__*/React.createElement("div", {
1110
+ className: "eco-retention-table-wrapper"
1111
+ }, /*#__PURE__*/React.createElement("table", {
1112
+ className: "eco-retention-table"
1113
+ }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, t('cohort')), /*#__PURE__*/React.createElement("th", null, t('users')), days.map(day => /*#__PURE__*/React.createElement("th", {
1114
+ key: day
1115
+ }, t(`day${day}`) || `D+${day}`)))), /*#__PURE__*/React.createElement("tbody", null, cohorts.map((cohort, index) => /*#__PURE__*/React.createElement("tr", {
1116
+ key: cohort.date || index
1117
+ }, /*#__PURE__*/React.createElement("td", {
1118
+ className: "eco-retention-table-cohort"
1119
+ }, cohort.date), /*#__PURE__*/React.createElement("td", {
1120
+ className: "eco-retention-table-users"
1121
+ }, cohort.users.toLocaleString()), days.map(day => {
1122
+ const value = cohort[`day${day}`];
1123
+ return /*#__PURE__*/React.createElement("td", {
1124
+ key: day,
1125
+ className: "eco-retention-table-value"
1126
+ }, value !== null ? `${(value * 100).toFixed(1)}%` : '-');
1127
+ }))))));
1128
+ }
1129
+
1130
+ /**
1131
+ * @quantabit/retention-sdk
1132
+ * Retention Analytics SDK - Full Version
1133
+ */
1134
+
1135
+ const getRetention = (...args) => retentionApi.getRetention(...args);
1136
+ const getCohortAnalysis = (...args) => retentionApi.getCohortAnalysis(...args);
1137
+ const getRetentionCurve = (...args) => retentionApi.getRetentionCurve(...args);
1138
+ const compare = (...args) => retentionApi.compare(...args);
1139
+ const getChurnAnalysis = (...args) => retentionApi.getChurnAnalysis(...args);
1140
+ const getLtvAnalysis = (...args) => retentionApi.getLtvAnalysis(...args);
1141
+ const forecast = (...args) => retentionApi.forecast(...args);
1142
+
1143
+ exports.ChurnRisk = ChurnRisk;
1144
+ exports.CohortHeatmap = CohortHeatmap;
1145
+ exports.CohortType = CohortType;
1146
+ exports.RetentionApiClient = RetentionApiClient;
1147
+ exports.RetentionCurve = RetentionCurve;
1148
+ exports.RetentionMetricCard = RetentionMetricCard;
1149
+ exports.RetentionOverview = RetentionOverview;
1150
+ exports.RetentionPeriod = RetentionPeriod;
1151
+ exports.RetentionTable = RetentionTable;
1152
+ exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
1153
+ exports.TimeRangeSelector = TimeRangeSelector;
1154
+ exports.ViewToggle = ViewToggle;
1155
+ exports.compare = compare;
1156
+ exports.forecast = forecast;
1157
+ exports.getChurnAnalysis = getChurnAnalysis;
1158
+ exports.getCohortAnalysis = getCohortAnalysis;
1159
+ exports.getLanguage = getLanguage;
1160
+ exports.getLtvAnalysis = getLtvAnalysis;
1161
+ exports.getRetention = getRetention;
1162
+ exports.getRetentionCurve = getRetentionCurve;
1163
+ exports.messages = messages;
1164
+ exports.retentionApi = retentionApi;
1165
+ exports.setLanguage = setLanguage;
1166
+ exports.t = t;
1167
+ exports.useChurnAnalysis = useChurnAnalysis;
1168
+ exports.useCohortAnalysis = useCohortAnalysis;
1169
+ exports.useLtvAnalysis = useLtvAnalysis;
1170
+ exports.useRetention = useRetention;
1171
+ exports.useRetentionComparison = useRetentionComparison;
1172
+ exports.useRetentionCurve = useRetentionCurve;
1173
+ exports.useRetentionForecast = useRetentionForecast;
1174
+ //# sourceMappingURL=index.cjs.map