@quantabit/segment-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,1317 @@
1
+ 'use strict';
2
+
3
+ var sdkConfig = require('@quantabit/sdk-config');
4
+ var React = require('react');
5
+
6
+ /**
7
+ * Segment SDK - API 客户端
8
+ * 用户分群系统后端接口封装
9
+ *
10
+ * 使用 BaseApiClient 基类简化代码
11
+ */
12
+
13
+
14
+ /**
15
+ * 用户分群 API 客户端
16
+ */
17
+ class SegmentApiClient extends sdkConfig.BaseApiClient {
18
+ constructor(config = {}) {
19
+ super('/segment', config);
20
+ }
21
+
22
+ // ============ 分群管理 ============
23
+
24
+ /**
25
+ * 获取分群列表
26
+ * @param {Object} params - 查询参数
27
+ */
28
+ async getSegments(params = {}) {
29
+ return this.get('/list', params);
30
+ }
31
+
32
+ /**
33
+ * 获取分群详情
34
+ * @param {string} segmentId - 分群 ID
35
+ */
36
+ async getSegment(segmentId) {
37
+ return this.get(`/${segmentId}`);
38
+ }
39
+
40
+ /**
41
+ * 创建分群(管理员)
42
+ * @param {Object} data - 分群数据
43
+ */
44
+ async createSegment(data) {
45
+ return this.post('/', data);
46
+ }
47
+
48
+ /**
49
+ * 更新分群(管理员)
50
+ * @param {string} segmentId - 分群 ID
51
+ * @param {Object} updates - 更新数据
52
+ */
53
+ async updateSegment(segmentId, updates) {
54
+ return this.put(`/${segmentId}`, updates);
55
+ }
56
+
57
+ /**
58
+ * 删除分群(管理员)
59
+ * @param {string} segmentId - 分群 ID
60
+ */
61
+ async deleteSegment(segmentId) {
62
+ return this.delete(`/${segmentId}`);
63
+ }
64
+
65
+ // ============ 分群规则 ============
66
+
67
+ /**
68
+ * 获取分群规则
69
+ * @param {string} segmentId - 分群 ID
70
+ */
71
+ async getSegmentRules(segmentId) {
72
+ return this.get(`/${segmentId}/rules`);
73
+ }
74
+
75
+ /**
76
+ * 更新分群规则(管理员)
77
+ * @param {string} segmentId - 分群 ID
78
+ * @param {Object} rules - 规则配置
79
+ */
80
+ async updateSegmentRules(segmentId, rules) {
81
+ return this.put(`/${segmentId}/rules`, rules);
82
+ }
83
+
84
+ /**
85
+ * 预估分群规模
86
+ * @param {Object} rules - 规则配置
87
+ */
88
+ async estimateSize(rules) {
89
+ return this.post('/estimate', rules);
90
+ }
91
+
92
+ // ============ 分群用户 ============
93
+
94
+ /**
95
+ * 获取分群用户
96
+ * @param {string} segmentId - 分群 ID
97
+ * @param {Object} params - 查询参数
98
+ */
99
+ async getSegmentUsers(segmentId, params = {}) {
100
+ return this.get(`/${segmentId}/users`, params);
101
+ }
102
+
103
+ /**
104
+ * 手动添加用户到分群(管理员)
105
+ * @param {string} segmentId - 分群 ID
106
+ * @param {string[]} userIds - 用户 ID 列表
107
+ */
108
+ async addUsersToSegment(segmentId, userIds) {
109
+ return this.post(`/${segmentId}/users`, {
110
+ user_ids: userIds
111
+ });
112
+ }
113
+
114
+ /**
115
+ * 从分群移除用户(管理员)
116
+ * @param {string} segmentId - 分群 ID
117
+ * @param {string[]} userIds - 用户 ID 列表
118
+ */
119
+ async removeUsersFromSegment(segmentId, userIds) {
120
+ return this.post(`/${segmentId}/users/remove`, {
121
+ user_ids: userIds
122
+ });
123
+ }
124
+
125
+ /**
126
+ * 检查用户是否在分群中
127
+ * @param {string} segmentId - 分群 ID
128
+ * @param {string} userId - 用户 ID
129
+ */
130
+ async checkUserInSegment(segmentId, userId) {
131
+ return this.get(`/${segmentId}/users/${userId}/check`);
132
+ }
133
+
134
+ // ============ 分群同步 ============
135
+
136
+ /**
137
+ * 刷新分群(重新计算)
138
+ * @param {string} segmentId - 分群 ID
139
+ */
140
+ async refreshSegment(segmentId) {
141
+ return this.post(`/${segmentId}/refresh`);
142
+ }
143
+
144
+ /**
145
+ * 获取刷新状态
146
+ * @param {string} segmentId - 分群 ID
147
+ */
148
+ async getRefreshStatus(segmentId) {
149
+ return this.get(`/${segmentId}/refresh/status`);
150
+ }
151
+
152
+ // ============ 我的分群 ============
153
+
154
+ /**
155
+ * 获取我所属的分群
156
+ */
157
+ async getMySegments() {
158
+ return this.get('/my');
159
+ }
160
+
161
+ // ============ 统计 ============
162
+
163
+ /**
164
+ * 获取分群统计
165
+ * @param {string} segmentId - 分群 ID
166
+ * @param {Object} params - 统计参数
167
+ */
168
+ async getSegmentStats(segmentId, params = {}) {
169
+ return this.get(`/${segmentId}/stats`, params);
170
+ }
171
+
172
+ /**
173
+ * 获取分群趋势
174
+ * @param {string} segmentId - 分群 ID
175
+ * @param {Object} params - 查询参数
176
+ */
177
+ async getSegmentTrend(segmentId, params = {}) {
178
+ return this.get(`/${segmentId}/trend`, params);
179
+ }
180
+
181
+ /**
182
+ * 导出分群用户
183
+ * @param {string} segmentId - 分群 ID
184
+ * @param {Object} options - 导出选项
185
+ */
186
+ async exportSegmentUsers(segmentId, options = {}) {
187
+ return this.post(`/${segmentId}/export`, options);
188
+ }
189
+
190
+ // ============ 兼容方法 ============
191
+
192
+ async estimate(rules) {
193
+ return this.estimateSize(rules);
194
+ }
195
+ async cloneSegment(segmentId, data) {
196
+ return this.post(`/${segmentId}/clone`, data);
197
+ }
198
+ async getProperties() {
199
+ return this.get('/properties');
200
+ }
201
+ }
202
+
203
+ // 创建默认实例
204
+ const segmentApi = new SegmentApiClient();
205
+
206
+ /**
207
+ * Segment SDK - 类型定义
208
+ */
209
+
210
+ // 分群类型
211
+ const SegmentType = {
212
+ STATIC: 'static',
213
+ // 静态分群
214
+ DYNAMIC: 'dynamic',
215
+ // 动态分群
216
+ REALTIME: 'realtime' // 实时分群
217
+ };
218
+
219
+ // 属性类型
220
+ const PropertyType = {
221
+ USER: 'user',
222
+ BEHAVIOR: 'behavior',
223
+ TRANSACTION: 'transaction',
224
+ DEVICE: 'device'
225
+ };
226
+ const ConditionOperator = {};
227
+
228
+ /**
229
+ * Segment SDK - 国际化
230
+ * 用户分群多语言支持
231
+ */
232
+
233
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
234
+ const messages = {
235
+ zh: {
236
+ // 分群基础
237
+ segment: '分群',
238
+ segments: '用户分群',
239
+ createSegment: '创建分群',
240
+ editSegment: '编辑分群',
241
+ deleteSegment: '删除分群',
242
+ segmentName: '分群名称',
243
+ segmentDesc: '分群描述',
244
+ // 分群类型
245
+ static: '静态分群',
246
+ dynamic: '动态分群',
247
+ realtime: '实时分群',
248
+ // 条件
249
+ condition: '条件',
250
+ conditions: '条件',
251
+ addCondition: '添加条件',
252
+ removeCondition: '移除条件',
253
+ and: '且',
254
+ or: '或',
255
+ // 属性
256
+ property: '属性',
257
+ userProperty: '用户属性',
258
+ eventProperty: '事件属性',
259
+ // 运算符
260
+ equals: '等于',
261
+ notEquals: '不等于',
262
+ contains: '包含',
263
+ notContains: '不包含',
264
+ startsWith: '开头是',
265
+ endsWith: '结尾是',
266
+ greaterThan: '大于',
267
+ lessThan: '小于',
268
+ between: '介于',
269
+ in: '在列表中',
270
+ notIn: '不在列表中',
271
+ isSet: '已设置',
272
+ isNotSet: '未设置',
273
+ // 时间
274
+ inLast: '在过去',
275
+ days: '天',
276
+ weeks: '周',
277
+ months: '月',
278
+ // 行为
279
+ didEvent: '执行过事件',
280
+ didNotEvent: '未执行事件',
281
+ eventCount: '事件次数',
282
+ firstTime: '首次',
283
+ lastTime: '最近',
284
+ // 分群结果
285
+ users: '用户数',
286
+ percentage: '占比',
287
+ estimated: '预估',
288
+ calculating: '计算中...',
289
+ // 操作
290
+ save: '保存',
291
+ cancel: '取消',
292
+ preview: '预览',
293
+ refresh: '刷新',
294
+ export: '导出',
295
+ clone: '复制',
296
+ // 使用场景
297
+ useFor: '用于',
298
+ marketing: '营销活动',
299
+ analytics: '数据分析',
300
+ targeting: '精准投放',
301
+ // 状态
302
+ active: '活跃',
303
+ inactive: '未激活',
304
+ processing: '处理中',
305
+ ready: '就绪',
306
+ // 提示
307
+ noResults: '暂无结果',
308
+ loading: '加载中...',
309
+ error: '加载失败',
310
+ saved: '保存成功',
311
+ deleted: '删除成功',
312
+ // 预设分群
313
+ allUsers: '全部用户',
314
+ newUsers: '新用户',
315
+ activeUsers: '活跃用户',
316
+ churned: '流失用户',
317
+ highValue: '高价值用户',
318
+ atRisk: '流失风险用户'
319
+ },
320
+ en: {
321
+ segment: 'Segment',
322
+ segments: 'Segments',
323
+ createSegment: 'Create Segment',
324
+ editSegment: 'Edit Segment',
325
+ deleteSegment: 'Delete Segment',
326
+ segmentName: 'Segment Name',
327
+ segmentDesc: 'Description',
328
+ static: 'Static',
329
+ dynamic: 'Dynamic',
330
+ realtime: 'Realtime',
331
+ condition: 'Condition',
332
+ conditions: 'Conditions',
333
+ addCondition: 'Add Condition',
334
+ removeCondition: 'Remove',
335
+ and: 'AND',
336
+ or: 'OR',
337
+ property: 'Property',
338
+ userProperty: 'User Property',
339
+ eventProperty: 'Event Property',
340
+ equals: 'equals',
341
+ notEquals: 'not equals',
342
+ contains: 'contains',
343
+ notContains: 'not contains',
344
+ startsWith: 'starts with',
345
+ endsWith: 'ends with',
346
+ greaterThan: 'greater than',
347
+ lessThan: 'less than',
348
+ between: 'between',
349
+ in: 'in',
350
+ notIn: 'not in',
351
+ isSet: 'is set',
352
+ isNotSet: 'is not set',
353
+ inLast: 'in last',
354
+ days: 'days',
355
+ weeks: 'weeks',
356
+ months: 'months',
357
+ didEvent: 'did event',
358
+ didNotEvent: 'did not event',
359
+ eventCount: 'event count',
360
+ firstTime: 'first time',
361
+ lastTime: 'last time',
362
+ users: 'Users',
363
+ percentage: 'Percentage',
364
+ estimated: 'Estimated',
365
+ calculating: 'Calculating...',
366
+ save: 'Save',
367
+ cancel: 'Cancel',
368
+ preview: 'Preview',
369
+ refresh: 'Refresh',
370
+ export: 'Export',
371
+ clone: 'Clone',
372
+ useFor: 'Use for',
373
+ marketing: 'Marketing',
374
+ analytics: 'Analytics',
375
+ targeting: 'Targeting',
376
+ active: 'Active',
377
+ inactive: 'Inactive',
378
+ processing: 'Processing',
379
+ ready: 'Ready',
380
+ noResults: 'No Results',
381
+ loading: 'Loading...',
382
+ error: 'Error',
383
+ saved: 'Saved',
384
+ deleted: 'Deleted',
385
+ allUsers: 'All Users',
386
+ newUsers: 'New Users',
387
+ activeUsers: 'Active Users',
388
+ churned: 'Churned',
389
+ highValue: 'High Value',
390
+ atRisk: 'At Risk'
391
+ },
392
+ ja: {
393
+ segment: 'セグメント',
394
+ segments: 'セグメント',
395
+ createSegment: 'セグメントを作成',
396
+ editSegment: 'セグメントを編集',
397
+ deleteSegment: 'セグメントを削除',
398
+ segmentName: 'セグメント名',
399
+ segmentDesc: '説明',
400
+ static: '静的',
401
+ dynamic: '動的',
402
+ realtime: 'リアルタイム',
403
+ condition: '条件',
404
+ conditions: '条件',
405
+ addCondition: '条件を追加',
406
+ removeCondition: '削除',
407
+ and: 'かつ',
408
+ or: 'または',
409
+ property: 'プロパティ',
410
+ userProperty: 'ユーザープロパティ',
411
+ eventProperty: 'イベントプロパティ',
412
+ equals: '等しい',
413
+ notEquals: '等しくない',
414
+ contains: '含む',
415
+ notContains: '含まない',
416
+ startsWith: 'で始まる',
417
+ endsWith: 'で終わる',
418
+ greaterThan: 'より大きい',
419
+ lessThan: 'より小さい',
420
+ between: 'の間',
421
+ in: 'に含まれる',
422
+ notIn: 'に含まれない',
423
+ isSet: '設定済み',
424
+ isNotSet: '未設定',
425
+ inLast: '過去',
426
+ days: '日間',
427
+ weeks: '週間',
428
+ months: 'ヶ月間',
429
+ didEvent: 'イベント実行済み',
430
+ didNotEvent: 'イベント未実行',
431
+ eventCount: 'イベント回数',
432
+ firstTime: '初回',
433
+ lastTime: '最終',
434
+ users: 'ユーザー数',
435
+ percentage: '割合',
436
+ estimated: '推定',
437
+ calculating: '計算中...',
438
+ save: '保存',
439
+ cancel: 'キャンセル',
440
+ preview: 'プレビュー',
441
+ refresh: '更新',
442
+ export: 'エクスポート',
443
+ clone: '複製',
444
+ useFor: '用途',
445
+ marketing: 'マーケティング',
446
+ analytics: '分析',
447
+ targeting: 'ターゲティング',
448
+ active: 'アクティブ',
449
+ inactive: '非アクティブ',
450
+ processing: '処理中',
451
+ ready: '準備完了',
452
+ noResults: '結果なし',
453
+ loading: '読み込み中...',
454
+ error: 'エラー',
455
+ saved: '保存しました',
456
+ deleted: '削除しました',
457
+ allUsers: '全ユーザー',
458
+ newUsers: '新規ユーザー',
459
+ activeUsers: 'アクティブユーザー',
460
+ churned: '離脱ユーザー',
461
+ highValue: '高価値ユーザー',
462
+ atRisk: 'リスクユーザー'
463
+ },
464
+ ko: {
465
+ segment: '세그먼트',
466
+ segments: '세그먼트',
467
+ createSegment: '세그먼트 생성',
468
+ editSegment: '세그먼트 편집',
469
+ deleteSegment: '세그먼트 삭제',
470
+ segmentName: '세그먼트 이름',
471
+ segmentDesc: '설명',
472
+ static: '정적',
473
+ dynamic: '동적',
474
+ realtime: '실시간',
475
+ condition: '조건',
476
+ conditions: '조건',
477
+ addCondition: '조건 추가',
478
+ removeCondition: '제거',
479
+ and: '그리고',
480
+ or: '또는',
481
+ property: '속성',
482
+ userProperty: '사용자 속성',
483
+ eventProperty: '이벤트 속성',
484
+ equals: '같음',
485
+ notEquals: '같지 않음',
486
+ contains: '포함',
487
+ notContains: '포함하지 않음',
488
+ startsWith: '로 시작',
489
+ endsWith: '로 끝남',
490
+ greaterThan: '보다 큼',
491
+ lessThan: '보다 작음',
492
+ between: '사이',
493
+ in: '포함됨',
494
+ notIn: '포함되지 않음',
495
+ isSet: '설정됨',
496
+ isNotSet: '설정되지 않음',
497
+ inLast: '최근',
498
+ days: '일',
499
+ weeks: '주',
500
+ months: '개월',
501
+ didEvent: '이벤트 수행',
502
+ didNotEvent: '이벤트 미수행',
503
+ eventCount: '이벤트 횟수',
504
+ firstTime: '처음',
505
+ lastTime: '마지막',
506
+ users: '사용자 수',
507
+ percentage: '비율',
508
+ estimated: '예상',
509
+ calculating: '계산 중...',
510
+ save: '저장',
511
+ cancel: '취소',
512
+ preview: '미리보기',
513
+ refresh: '새로고침',
514
+ export: '내보내기',
515
+ clone: '복제',
516
+ useFor: '용도',
517
+ marketing: '마케팅',
518
+ analytics: '분석',
519
+ targeting: '타겟팅',
520
+ active: '활성',
521
+ inactive: '비활성',
522
+ processing: '처리 중',
523
+ ready: '준비 완료',
524
+ noResults: '결과 없음',
525
+ loading: '로딩 중...',
526
+ error: '오류',
527
+ saved: '저장됨',
528
+ deleted: '삭제됨',
529
+ allUsers: '전체 사용자',
530
+ newUsers: '신규 사용자',
531
+ activeUsers: '활성 사용자',
532
+ churned: '이탈 사용자',
533
+ highValue: '고가치 사용자',
534
+ atRisk: '위험 사용자'
535
+ }
536
+ };
537
+ let currentLanguage = 'zh';
538
+ function setLanguage(lang) {
539
+ if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
540
+ }
541
+ function getLanguage() {
542
+ return currentLanguage;
543
+ }
544
+ function t(key) {
545
+ return (messages[currentLanguage] || messages.en)[key] || key;
546
+ }
547
+
548
+ /**
549
+ * Segment SDK - React Hooks
550
+ * 用户分群相关的状态管理
551
+ */
552
+
553
+
554
+ /**
555
+ * 获取分群列表
556
+ */
557
+ function useSegments(params = {}) {
558
+ const [segments, setSegments] = React.useState([]);
559
+ const [loading, setLoading] = React.useState(true);
560
+ const [error, setError] = React.useState(null);
561
+ const fetchSegments = React.useCallback(async () => {
562
+ try {
563
+ setLoading(true);
564
+ const response = await segmentApi.getSegments(params);
565
+ setSegments(response.data || []);
566
+ setError(null);
567
+ } catch (err) {
568
+ setError(err.message);
569
+ } finally {
570
+ setLoading(false);
571
+ }
572
+ }, [params]);
573
+ React.useEffect(() => {
574
+ fetchSegments();
575
+ }, []);
576
+ return {
577
+ segments,
578
+ loading,
579
+ error,
580
+ refresh: fetchSegments
581
+ };
582
+ }
583
+
584
+ /**
585
+ * 获取单个分群详情
586
+ */
587
+ function useSegment(segmentId) {
588
+ const [segment, setSegment] = React.useState(null);
589
+ const [loading, setLoading] = React.useState(true);
590
+ const [error, setError] = React.useState(null);
591
+ const fetchSegment = React.useCallback(async () => {
592
+ if (!segmentId) return;
593
+ try {
594
+ setLoading(true);
595
+ const response = await segmentApi.getSegment(segmentId);
596
+ setSegment(response);
597
+ setError(null);
598
+ } catch (err) {
599
+ setError(err.message);
600
+ } finally {
601
+ setLoading(false);
602
+ }
603
+ }, [segmentId]);
604
+ React.useEffect(() => {
605
+ fetchSegment();
606
+ }, [fetchSegment]);
607
+ return {
608
+ segment,
609
+ loading,
610
+ error,
611
+ refresh: fetchSegment
612
+ };
613
+ }
614
+
615
+ /**
616
+ * 分群预估
617
+ */
618
+ function useSegmentEstimate(conditions) {
619
+ const [estimate, setEstimate] = React.useState(null);
620
+ const [loading, setLoading] = React.useState(false);
621
+ const [error, setError] = React.useState(null);
622
+ const calculateEstimate = React.useCallback(async () => {
623
+ if (!conditions || conditions.length === 0) {
624
+ setEstimate(null);
625
+ return;
626
+ }
627
+ try {
628
+ setLoading(true);
629
+ const response = await segmentApi.estimate({
630
+ conditions
631
+ });
632
+ setEstimate(response);
633
+ setError(null);
634
+ } catch (err) {
635
+ setError(err.message);
636
+ } finally {
637
+ setLoading(false);
638
+ }
639
+ }, [conditions]);
640
+ React.useEffect(() => {
641
+ const debounce = setTimeout(() => {
642
+ calculateEstimate();
643
+ }, 500);
644
+ return () => clearTimeout(debounce);
645
+ }, [calculateEstimate]);
646
+ return {
647
+ estimate,
648
+ loading,
649
+ error,
650
+ refresh: calculateEstimate
651
+ };
652
+ }
653
+
654
+ /**
655
+ * 分群用户列表
656
+ */
657
+ function useSegmentUsers(segmentId, options = {}) {
658
+ const [users, setUsers] = React.useState([]);
659
+ const [loading, setLoading] = React.useState(true);
660
+ const [error, setError] = React.useState(null);
661
+ const [pagination, setPagination] = React.useState({
662
+ page: 1,
663
+ pageSize: options.pageSize || 20,
664
+ total: 0
665
+ });
666
+ const fetchUsers = React.useCallback(async (page = 1) => {
667
+ if (!segmentId) return;
668
+ try {
669
+ setLoading(true);
670
+ const response = await segmentApi.getSegmentUsers(segmentId, {
671
+ page,
672
+ pageSize: pagination.pageSize
673
+ });
674
+ setUsers(response.data || []);
675
+ setPagination(prev => ({
676
+ ...prev,
677
+ page,
678
+ total: response.total || 0
679
+ }));
680
+ setError(null);
681
+ } catch (err) {
682
+ setError(err.message);
683
+ } finally {
684
+ setLoading(false);
685
+ }
686
+ }, [segmentId, pagination.pageSize]);
687
+ React.useEffect(() => {
688
+ fetchUsers(1);
689
+ }, [segmentId]);
690
+ return {
691
+ users,
692
+ loading,
693
+ error,
694
+ pagination,
695
+ fetchPage: fetchUsers,
696
+ refresh: () => fetchUsers(pagination.page)
697
+ };
698
+ }
699
+
700
+ /**
701
+ * 分群操作(创建、更新、删除)
702
+ */
703
+ function useSegmentActions() {
704
+ const [loading, setLoading] = React.useState(false);
705
+ const [error, setError] = React.useState(null);
706
+ const createSegment = React.useCallback(async data => {
707
+ try {
708
+ setLoading(true);
709
+ setError(null);
710
+ const result = await segmentApi.createSegment(data);
711
+ return result;
712
+ } catch (err) {
713
+ setError(err.message);
714
+ throw err;
715
+ } finally {
716
+ setLoading(false);
717
+ }
718
+ }, []);
719
+ const updateSegment = React.useCallback(async (segmentId, data) => {
720
+ try {
721
+ setLoading(true);
722
+ setError(null);
723
+ const result = await segmentApi.updateSegment(segmentId, data);
724
+ return result;
725
+ } catch (err) {
726
+ setError(err.message);
727
+ throw err;
728
+ } finally {
729
+ setLoading(false);
730
+ }
731
+ }, []);
732
+ const deleteSegment = React.useCallback(async segmentId => {
733
+ try {
734
+ setLoading(true);
735
+ setError(null);
736
+ await segmentApi.deleteSegment(segmentId);
737
+ } catch (err) {
738
+ setError(err.message);
739
+ throw err;
740
+ } finally {
741
+ setLoading(false);
742
+ }
743
+ }, []);
744
+ const cloneSegment = React.useCallback(async (segmentId, newName) => {
745
+ try {
746
+ setLoading(true);
747
+ setError(null);
748
+ const result = await segmentApi.cloneSegment(segmentId, {
749
+ name: newName
750
+ });
751
+ return result;
752
+ } catch (err) {
753
+ setError(err.message);
754
+ throw err;
755
+ } finally {
756
+ setLoading(false);
757
+ }
758
+ }, []);
759
+ return {
760
+ loading,
761
+ error,
762
+ createSegment,
763
+ updateSegment,
764
+ deleteSegment,
765
+ cloneSegment
766
+ };
767
+ }
768
+
769
+ /**
770
+ * 条件构建器状态管理
771
+ */
772
+ function useConditionBuilder(initialConditions = []) {
773
+ const [conditions, setConditions] = React.useState(initialConditions);
774
+ const addCondition = React.useCallback(condition => {
775
+ setConditions(prev => [...prev, {
776
+ id: Date.now().toString(),
777
+ ...condition
778
+ }]);
779
+ }, []);
780
+ const updateCondition = React.useCallback((id, updates) => {
781
+ setConditions(prev => prev.map(c => c.id === id ? {
782
+ ...c,
783
+ ...updates
784
+ } : c));
785
+ }, []);
786
+ const removeCondition = React.useCallback(id => {
787
+ setConditions(prev => prev.filter(c => c.id !== id));
788
+ }, []);
789
+ const clearConditions = React.useCallback(() => {
790
+ setConditions([]);
791
+ }, []);
792
+ const moveCondition = React.useCallback((fromIndex, toIndex) => {
793
+ setConditions(prev => {
794
+ const result = [...prev];
795
+ const [removed] = result.splice(fromIndex, 1);
796
+ result.splice(toIndex, 0, removed);
797
+ return result;
798
+ });
799
+ }, []);
800
+ return {
801
+ conditions,
802
+ setConditions,
803
+ addCondition,
804
+ updateCondition,
805
+ removeCondition,
806
+ clearConditions,
807
+ moveCondition
808
+ };
809
+ }
810
+
811
+ /**
812
+ * 获取可用属性
813
+ */
814
+ function useSegmentProperties() {
815
+ const [properties, setProperties] = React.useState({
816
+ user: [],
817
+ event: []
818
+ });
819
+ const [loading, setLoading] = React.useState(true);
820
+ const [error, setError] = React.useState(null);
821
+ React.useEffect(() => {
822
+ const fetchProperties = async () => {
823
+ try {
824
+ const response = await segmentApi.getProperties();
825
+ setProperties(response);
826
+ setError(null);
827
+ } catch (err) {
828
+ setError(err.message);
829
+ } finally {
830
+ setLoading(false);
831
+ }
832
+ };
833
+ fetchProperties();
834
+ }, []);
835
+ return {
836
+ properties,
837
+ loading,
838
+ error
839
+ };
840
+ }
841
+
842
+ /**
843
+ * 预设分群
844
+ */
845
+ function usePresetSegments() {
846
+ const presets = React.useMemo(() => [{
847
+ id: 'all',
848
+ name: 'allUsers',
849
+ icon: '👥'
850
+ }, {
851
+ id: 'new',
852
+ name: 'newUsers',
853
+ icon: '🆕'
854
+ }, {
855
+ id: 'active',
856
+ name: 'activeUsers',
857
+ icon: '✅'
858
+ }, {
859
+ id: 'churned',
860
+ name: 'churned',
861
+ icon: '👋'
862
+ }, {
863
+ id: 'highValue',
864
+ name: 'highValue',
865
+ icon: '💎'
866
+ }, {
867
+ id: 'atRisk',
868
+ name: 'atRisk',
869
+ icon: '⚠️'
870
+ }], []);
871
+ return {
872
+ presets
873
+ };
874
+ }
875
+
876
+ /**
877
+ * Segment SDK - React 组件
878
+ * 用户分群可视化组件
879
+ */
880
+
881
+
882
+ /**
883
+ * 分群卡片
884
+ */
885
+ function SegmentCard({
886
+ segment,
887
+ onClick,
888
+ onEdit,
889
+ onDelete,
890
+ onClone
891
+ }) {
892
+ const statusClass = segment.status === 'active' ? 'active' : 'inactive';
893
+ return /*#__PURE__*/React.createElement("div", {
894
+ className: "eco-segment-card",
895
+ onClick: () => onClick?.(segment)
896
+ }, /*#__PURE__*/React.createElement("div", {
897
+ className: "eco-segment-card-header"
898
+ }, /*#__PURE__*/React.createElement("h4", {
899
+ className: "eco-segment-card-title"
900
+ }, segment.name), /*#__PURE__*/React.createElement("span", {
901
+ className: `eco-segment-status eco-segment-status-${statusClass}`
902
+ }, t(segment.status))), segment.description && /*#__PURE__*/React.createElement("p", {
903
+ className: "eco-segment-card-desc"
904
+ }, segment.description), /*#__PURE__*/React.createElement("div", {
905
+ className: "eco-segment-card-stats"
906
+ }, /*#__PURE__*/React.createElement("div", {
907
+ className: "eco-segment-stat"
908
+ }, /*#__PURE__*/React.createElement("span", {
909
+ className: "eco-segment-stat-value"
910
+ }, segment.userCount?.toLocaleString() || 0), /*#__PURE__*/React.createElement("span", {
911
+ className: "eco-segment-stat-label"
912
+ }, t('users'))), /*#__PURE__*/React.createElement("div", {
913
+ className: "eco-segment-stat"
914
+ }, /*#__PURE__*/React.createElement("span", {
915
+ className: "eco-segment-stat-value"
916
+ }, segment.conditionCount || 0), /*#__PURE__*/React.createElement("span", {
917
+ className: "eco-segment-stat-label"
918
+ }, t('conditions')))), /*#__PURE__*/React.createElement("div", {
919
+ className: "eco-segment-card-type"
920
+ }, /*#__PURE__*/React.createElement("span", {
921
+ className: `eco-segment-type-badge eco-segment-type-${segment.type}`
922
+ }, t(segment.type))), /*#__PURE__*/React.createElement("div", {
923
+ className: "eco-segment-card-actions",
924
+ onClick: e => e.stopPropagation()
925
+ }, /*#__PURE__*/React.createElement("button", {
926
+ className: "eco-segment-action-btn",
927
+ onClick: () => onEdit?.(segment)
928
+ }, "\u270F\uFE0F"), /*#__PURE__*/React.createElement("button", {
929
+ className: "eco-segment-action-btn",
930
+ onClick: () => onClone?.(segment)
931
+ }, "\uD83D\uDCCB"), /*#__PURE__*/React.createElement("button", {
932
+ className: "eco-segment-action-btn danger",
933
+ onClick: () => onDelete?.(segment)
934
+ }, "\uD83D\uDDD1\uFE0F")));
935
+ }
936
+
937
+ /**
938
+ * 分群列表
939
+ */
940
+ function SegmentList({
941
+ onSelect,
942
+ onEdit,
943
+ onDelete
944
+ }) {
945
+ const {
946
+ segments,
947
+ loading,
948
+ error
949
+ } = useSegments();
950
+ const {
951
+ presets
952
+ } = usePresetSegments();
953
+ if (loading) {
954
+ return /*#__PURE__*/React.createElement("div", {
955
+ className: "eco-segment-loading"
956
+ }, /*#__PURE__*/React.createElement("div", {
957
+ className: "eco-segment-spinner"
958
+ }), /*#__PURE__*/React.createElement("span", null, t('loading')));
959
+ }
960
+ if (error) {
961
+ return /*#__PURE__*/React.createElement("div", {
962
+ className: "eco-segment-error"
963
+ }, error);
964
+ }
965
+ return /*#__PURE__*/React.createElement("div", {
966
+ className: "eco-segment-list"
967
+ }, /*#__PURE__*/React.createElement("div", {
968
+ className: "eco-segment-presets"
969
+ }, /*#__PURE__*/React.createElement("h4", null, t('segments')), /*#__PURE__*/React.createElement("div", {
970
+ className: "eco-segment-preset-grid"
971
+ }, presets.map(preset => /*#__PURE__*/React.createElement("button", {
972
+ key: preset.id,
973
+ className: "eco-segment-preset-btn",
974
+ onClick: () => onSelect?.({
975
+ id: preset.id,
976
+ name: t(preset.name)
977
+ })
978
+ }, /*#__PURE__*/React.createElement("span", {
979
+ className: "eco-segment-preset-icon"
980
+ }, preset.icon), /*#__PURE__*/React.createElement("span", null, t(preset.name)))))), /*#__PURE__*/React.createElement("div", {
981
+ className: "eco-segment-custom"
982
+ }, /*#__PURE__*/React.createElement("h4", null, t('segments')), segments.length === 0 ? /*#__PURE__*/React.createElement("div", {
983
+ className: "eco-segment-empty"
984
+ }, t('noResults')) : /*#__PURE__*/React.createElement("div", {
985
+ className: "eco-segment-grid"
986
+ }, segments.map(segment => /*#__PURE__*/React.createElement(SegmentCard, {
987
+ key: segment.id,
988
+ segment: segment,
989
+ onClick: onSelect,
990
+ onEdit: onEdit,
991
+ onDelete: onDelete
992
+ })))));
993
+ }
994
+
995
+ /**
996
+ * 条件选择器
997
+ */
998
+ function ConditionSelector({
999
+ condition,
1000
+ onChange,
1001
+ onRemove,
1002
+ properties
1003
+ }) {
1004
+ const operators = [{
1005
+ id: 'equals',
1006
+ label: t('equals')
1007
+ }, {
1008
+ id: 'notEquals',
1009
+ label: t('notEquals')
1010
+ }, {
1011
+ id: 'contains',
1012
+ label: t('contains')
1013
+ }, {
1014
+ id: 'greaterThan',
1015
+ label: t('greaterThan')
1016
+ }, {
1017
+ id: 'lessThan',
1018
+ label: t('lessThan')
1019
+ }, {
1020
+ id: 'between',
1021
+ label: t('between')
1022
+ }, {
1023
+ id: 'in',
1024
+ label: t('in')
1025
+ }, {
1026
+ id: 'isSet',
1027
+ label: t('isSet')
1028
+ }, {
1029
+ id: 'isNotSet',
1030
+ label: t('isNotSet')
1031
+ }];
1032
+ return /*#__PURE__*/React.createElement("div", {
1033
+ className: "eco-segment-condition"
1034
+ }, /*#__PURE__*/React.createElement("select", {
1035
+ className: "eco-segment-select",
1036
+ value: condition.propertyType || 'user',
1037
+ onChange: e => onChange({
1038
+ ...condition,
1039
+ propertyType: e.target.value
1040
+ })
1041
+ }, /*#__PURE__*/React.createElement("option", {
1042
+ value: "user"
1043
+ }, t('userProperty')), /*#__PURE__*/React.createElement("option", {
1044
+ value: "event"
1045
+ }, t('eventProperty'))), /*#__PURE__*/React.createElement("select", {
1046
+ className: "eco-segment-select",
1047
+ value: condition.property || '',
1048
+ onChange: e => onChange({
1049
+ ...condition,
1050
+ property: e.target.value
1051
+ })
1052
+ }, /*#__PURE__*/React.createElement("option", {
1053
+ value: ""
1054
+ }, t('property')), (properties?.[condition.propertyType] || []).map(prop => /*#__PURE__*/React.createElement("option", {
1055
+ key: prop.id,
1056
+ value: prop.id
1057
+ }, prop.name))), /*#__PURE__*/React.createElement("select", {
1058
+ className: "eco-segment-select",
1059
+ value: condition.operator || 'equals',
1060
+ onChange: e => onChange({
1061
+ ...condition,
1062
+ operator: e.target.value
1063
+ })
1064
+ }, operators.map(op => /*#__PURE__*/React.createElement("option", {
1065
+ key: op.id,
1066
+ value: op.id
1067
+ }, op.label))), !['isSet', 'isNotSet'].includes(condition.operator) && /*#__PURE__*/React.createElement("input", {
1068
+ type: "text",
1069
+ className: "eco-segment-input",
1070
+ value: condition.value || '',
1071
+ onChange: e => onChange({
1072
+ ...condition,
1073
+ value: e.target.value
1074
+ }),
1075
+ placeholder: "Value"
1076
+ }), /*#__PURE__*/React.createElement("button", {
1077
+ className: "eco-segment-remove-btn",
1078
+ onClick: onRemove
1079
+ }, "\u2715"));
1080
+ }
1081
+
1082
+ /**
1083
+ * 分群构建器
1084
+ */
1085
+ function SegmentBuilder({
1086
+ initialData = null,
1087
+ onSave,
1088
+ onCancel
1089
+ }) {
1090
+ const [name, setName] = React.useState(initialData?.name || '');
1091
+ const [description, setDescription] = React.useState(initialData?.description || '');
1092
+ const [type, setType] = React.useState(initialData?.type || 'dynamic');
1093
+ const {
1094
+ conditions,
1095
+ addCondition,
1096
+ updateCondition,
1097
+ removeCondition
1098
+ } = useConditionBuilder(initialData?.conditions || []);
1099
+ const {
1100
+ estimate,
1101
+ loading: estimating
1102
+ } = useSegmentEstimate(conditions);
1103
+ const {
1104
+ properties,
1105
+ loading: loadingProps
1106
+ } = useSegmentProperties();
1107
+ const {
1108
+ loading: saving,
1109
+ createSegment,
1110
+ updateSegment
1111
+ } = useSegmentActions();
1112
+ const handleSave = async () => {
1113
+ try {
1114
+ const data = {
1115
+ name,
1116
+ description,
1117
+ type,
1118
+ conditions
1119
+ };
1120
+ if (initialData?.id) {
1121
+ await updateSegment(initialData.id, data);
1122
+ } else {
1123
+ await createSegment(data);
1124
+ }
1125
+ onSave?.();
1126
+ } catch (err) {
1127
+ console.error('Save error:', err);
1128
+ }
1129
+ };
1130
+ const handleAddCondition = () => {
1131
+ addCondition({
1132
+ propertyType: 'user',
1133
+ property: '',
1134
+ operator: 'equals',
1135
+ value: ''
1136
+ });
1137
+ };
1138
+ return /*#__PURE__*/React.createElement("div", {
1139
+ className: "eco-segment-builder"
1140
+ }, /*#__PURE__*/React.createElement("div", {
1141
+ className: "eco-segment-builder-header"
1142
+ }, /*#__PURE__*/React.createElement("input", {
1143
+ type: "text",
1144
+ className: "eco-segment-name-input",
1145
+ placeholder: t('segmentName'),
1146
+ value: name,
1147
+ onChange: e => setName(e.target.value)
1148
+ }), /*#__PURE__*/React.createElement("textarea", {
1149
+ className: "eco-segment-desc-input",
1150
+ placeholder: t('segmentDesc'),
1151
+ value: description,
1152
+ onChange: e => setDescription(e.target.value),
1153
+ rows: 2
1154
+ })), /*#__PURE__*/React.createElement("div", {
1155
+ className: "eco-segment-type-selector"
1156
+ }, /*#__PURE__*/React.createElement("label", {
1157
+ className: type === 'static' ? 'active' : ''
1158
+ }, /*#__PURE__*/React.createElement("input", {
1159
+ type: "radio",
1160
+ value: "static",
1161
+ checked: type === 'static',
1162
+ onChange: () => setType('static')
1163
+ }), t('static')), /*#__PURE__*/React.createElement("label", {
1164
+ className: type === 'dynamic' ? 'active' : ''
1165
+ }, /*#__PURE__*/React.createElement("input", {
1166
+ type: "radio",
1167
+ value: "dynamic",
1168
+ checked: type === 'dynamic',
1169
+ onChange: () => setType('dynamic')
1170
+ }), t('dynamic')), /*#__PURE__*/React.createElement("label", {
1171
+ className: type === 'realtime' ? 'active' : ''
1172
+ }, /*#__PURE__*/React.createElement("input", {
1173
+ type: "radio",
1174
+ value: "realtime",
1175
+ checked: type === 'realtime',
1176
+ onChange: () => setType('realtime')
1177
+ }), t('realtime'))), /*#__PURE__*/React.createElement("div", {
1178
+ className: "eco-segment-conditions"
1179
+ }, /*#__PURE__*/React.createElement("h4", null, t('conditions')), conditions.length === 0 ? /*#__PURE__*/React.createElement("div", {
1180
+ className: "eco-segment-empty-conditions"
1181
+ }, t('addCondition')) : /*#__PURE__*/React.createElement("div", {
1182
+ className: "eco-segment-condition-list"
1183
+ }, conditions.map((condition, index) => /*#__PURE__*/React.createElement("div", {
1184
+ key: condition.id,
1185
+ className: "eco-segment-condition-row"
1186
+ }, index > 0 && /*#__PURE__*/React.createElement("span", {
1187
+ className: "eco-segment-logic"
1188
+ }, t('and')), /*#__PURE__*/React.createElement(ConditionSelector, {
1189
+ condition: condition,
1190
+ onChange: updated => updateCondition(condition.id, updated),
1191
+ onRemove: () => removeCondition(condition.id),
1192
+ properties: properties
1193
+ })))), /*#__PURE__*/React.createElement("button", {
1194
+ className: "eco-segment-add-condition-btn",
1195
+ onClick: handleAddCondition
1196
+ }, "+ ", t('addCondition'))), /*#__PURE__*/React.createElement("div", {
1197
+ className: "eco-segment-estimate"
1198
+ }, /*#__PURE__*/React.createElement("div", {
1199
+ className: "eco-segment-estimate-content"
1200
+ }, /*#__PURE__*/React.createElement("span", {
1201
+ className: "eco-segment-estimate-label"
1202
+ }, t('estimated'), " ", t('users'), ":"), /*#__PURE__*/React.createElement("span", {
1203
+ className: "eco-segment-estimate-value"
1204
+ }, estimating ? t('calculating') : estimate?.userCount?.toLocaleString() || 0)), estimate?.percentage && /*#__PURE__*/React.createElement("span", {
1205
+ className: "eco-segment-estimate-percentage"
1206
+ }, "(", (estimate.percentage * 100).toFixed(1), "% ", t('percentage'), ")")), /*#__PURE__*/React.createElement("div", {
1207
+ className: "eco-segment-builder-actions"
1208
+ }, /*#__PURE__*/React.createElement("button", {
1209
+ className: "eco-segment-btn",
1210
+ onClick: onCancel
1211
+ }, t('cancel')), /*#__PURE__*/React.createElement("button", {
1212
+ className: "eco-segment-btn eco-segment-btn-secondary",
1213
+ onClick: handleAddCondition
1214
+ }, t('preview')), /*#__PURE__*/React.createElement("button", {
1215
+ className: "eco-segment-btn eco-segment-btn-primary",
1216
+ onClick: handleSave,
1217
+ disabled: saving || !name
1218
+ }, saving ? t('loading') : t('save'))));
1219
+ }
1220
+
1221
+ /**
1222
+ * 分群选择下拉框
1223
+ */
1224
+ function SegmentSelect({
1225
+ value,
1226
+ onChange,
1227
+ placeholder
1228
+ }) {
1229
+ const {
1230
+ segments,
1231
+ loading
1232
+ } = useSegments();
1233
+ const {
1234
+ presets
1235
+ } = usePresetSegments();
1236
+ return /*#__PURE__*/React.createElement("select", {
1237
+ className: "eco-segment-dropdown",
1238
+ value: value || '',
1239
+ onChange: e => onChange(e.target.value),
1240
+ disabled: loading
1241
+ }, /*#__PURE__*/React.createElement("option", {
1242
+ value: ""
1243
+ }, placeholder || t('segment')), /*#__PURE__*/React.createElement("optgroup", {
1244
+ label: t('segments')
1245
+ }, presets.map(preset => /*#__PURE__*/React.createElement("option", {
1246
+ key: preset.id,
1247
+ value: preset.id
1248
+ }, preset.icon, " ", t(preset.name)))), segments.length > 0 && /*#__PURE__*/React.createElement("optgroup", {
1249
+ label: t('segments')
1250
+ }, segments.map(segment => /*#__PURE__*/React.createElement("option", {
1251
+ key: segment.id,
1252
+ value: segment.id
1253
+ }, segment.name))));
1254
+ }
1255
+
1256
+ /**
1257
+ * 用户数量显示
1258
+ */
1259
+ function UserCountBadge({
1260
+ segmentId
1261
+ }) {
1262
+ const {
1263
+ estimate,
1264
+ loading
1265
+ } = useSegmentEstimate([{
1266
+ segmentId
1267
+ }]);
1268
+ return /*#__PURE__*/React.createElement("span", {
1269
+ className: "eco-segment-user-count"
1270
+ }, loading ? '...' : estimate?.userCount?.toLocaleString() || 0, " ", t('users'));
1271
+ }
1272
+
1273
+ /**
1274
+ * @quantabit/segment-sdk
1275
+ * User Segmentation SDK - Full Version
1276
+ */
1277
+
1278
+ const getSegments = params => segmentApi.getSegments(params);
1279
+ const getSegment = segmentId => segmentApi.getSegment(segmentId);
1280
+ const createSegment = data => segmentApi.createSegment(data);
1281
+ const updateSegment = (segmentId, updates) => segmentApi.updateSegment(segmentId, updates);
1282
+ const deleteSegment = segmentId => segmentApi.deleteSegment(segmentId);
1283
+ const estimate = rules => segmentApi.estimate(rules);
1284
+ const getSegmentUsers = (segmentId, params) => segmentApi.getSegmentUsers(segmentId, params);
1285
+
1286
+ exports.ConditionOperator = ConditionOperator;
1287
+ exports.ConditionSelector = ConditionSelector;
1288
+ exports.PropertyType = PropertyType;
1289
+ exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
1290
+ exports.SegmentApiClient = SegmentApiClient;
1291
+ exports.SegmentBuilder = SegmentBuilder;
1292
+ exports.SegmentCard = SegmentCard;
1293
+ exports.SegmentList = SegmentList;
1294
+ exports.SegmentSelect = SegmentSelect;
1295
+ exports.SegmentType = SegmentType;
1296
+ exports.UserCountBadge = UserCountBadge;
1297
+ exports.createSegment = createSegment;
1298
+ exports.deleteSegment = deleteSegment;
1299
+ exports.estimate = estimate;
1300
+ exports.getLanguage = getLanguage;
1301
+ exports.getSegment = getSegment;
1302
+ exports.getSegmentUsers = getSegmentUsers;
1303
+ exports.getSegments = getSegments;
1304
+ exports.messages = messages;
1305
+ exports.segmentApi = segmentApi;
1306
+ exports.setLanguage = setLanguage;
1307
+ exports.t = t;
1308
+ exports.updateSegment = updateSegment;
1309
+ exports.useConditionBuilder = useConditionBuilder;
1310
+ exports.usePresetSegments = usePresetSegments;
1311
+ exports.useSegment = useSegment;
1312
+ exports.useSegmentActions = useSegmentActions;
1313
+ exports.useSegmentEstimate = useSegmentEstimate;
1314
+ exports.useSegmentProperties = useSegmentProperties;
1315
+ exports.useSegmentUsers = useSegmentUsers;
1316
+ exports.useSegments = useSegments;
1317
+ //# sourceMappingURL=index.cjs.map