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