@quantabit/leaderboard-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,1280 @@
1
+ import { BaseApiClient } from '@quantabit/sdk-config';
2
+ import React, { useState, useCallback, useEffect, useMemo, useContext, createContext } from 'react';
3
+
4
+ /**
5
+ * Leaderboard SDK - API 客户端
6
+ * 排行榜系统后端接口封装
7
+ *
8
+ * 使用 BaseApiClient 基类简化代码
9
+ */
10
+
11
+
12
+ /**
13
+ * 排行榜 API 客户端
14
+ */
15
+ class LeaderboardApiClient extends BaseApiClient {
16
+ constructor(config = {}) {
17
+ super('/leaderboard', config);
18
+ }
19
+
20
+ // ============ 排行榜查询 ============
21
+
22
+ /**
23
+ * 获取排行榜列表
24
+ */
25
+ async getLeaderboards() {
26
+ return this.get('/list');
27
+ }
28
+
29
+ /**
30
+ * 获取排行榜详情
31
+ * @param {string} leaderboardId - 排行榜 ID
32
+ */
33
+ async getLeaderboard(leaderboardId) {
34
+ return this.get(`/${leaderboardId}`);
35
+ }
36
+
37
+ /**
38
+ * 获取排行榜排名
39
+ */
40
+ async getRanking(leaderboardId, period, params = {}) {
41
+ if (typeof period === 'object') {
42
+ params = period;
43
+ period = undefined;
44
+ }
45
+ const queryParams = {
46
+ ...params
47
+ };
48
+ if (period) queryParams.period = period;
49
+ return this.get(`/${leaderboardId}/rankings`, queryParams);
50
+ }
51
+ async getRankings(leaderboardId, period, params = {}) {
52
+ return this.getRanking(leaderboardId, period, params);
53
+ }
54
+
55
+ /**
56
+ * 获取 Top Rankers
57
+ */
58
+ async getTopRankers(leaderboardId, period, n = 3) {
59
+ if (typeof period === 'number') {
60
+ n = period;
61
+ period = undefined;
62
+ }
63
+ const queryParams = {
64
+ n
65
+ };
66
+ if (period) queryParams.period = period;
67
+ return this.get(`/${leaderboardId}/top`, queryParams);
68
+ }
69
+
70
+ /**
71
+ * 获取 Top N
72
+ */
73
+ async getTopN(leaderboardId, n = 10) {
74
+ return this.getTopRankers(leaderboardId, undefined, n);
75
+ }
76
+
77
+ // ============ 我的排名 ============
78
+
79
+ /**
80
+ * 获取我的排名
81
+ */
82
+ async getMyRank(leaderboardId, period) {
83
+ const queryParams = {};
84
+ if (period) queryParams.period = period;
85
+ return this.get(`/${leaderboardId}/my-rank`, queryParams);
86
+ }
87
+
88
+ /**
89
+ * 获取附近排名
90
+ */
91
+ async getNearbyRanks(leaderboardId, period, range = 5) {
92
+ if (typeof period === 'number') {
93
+ range = period;
94
+ period = undefined;
95
+ }
96
+ const queryParams = {
97
+ range
98
+ };
99
+ if (period) queryParams.period = period;
100
+ return this.get(`/${leaderboardId}/nearby`, queryParams);
101
+ }
102
+
103
+ /**
104
+ * 获取附近用户
105
+ */
106
+ async getNearbyUsers(leaderboardId, range = 5) {
107
+ return this.getNearbyRanks(leaderboardId, undefined, range);
108
+ }
109
+
110
+ /**
111
+ * 获取榜单奖励
112
+ */
113
+ async getPrizes(leaderboardId, period) {
114
+ const queryParams = {};
115
+ if (period) queryParams.period = period;
116
+ return this.get(`/${leaderboardId}/prizes`, queryParams);
117
+ }
118
+
119
+ /**
120
+ * 获取奖励 (别名)
121
+ */
122
+ async getRewards(leaderboardId, period) {
123
+ return this.getPrizes(leaderboardId, period);
124
+ }
125
+
126
+ /**
127
+ * 获取我的历史排名
128
+ * @param {string} leaderboardId - 排行榜 ID
129
+ * @param {Object} params - 查询参数
130
+ */
131
+ async getMyRankHistory(leaderboardId, params = {}) {
132
+ return this.get(`/${leaderboardId}/my/history`, params);
133
+ }
134
+
135
+ // ============ 分数提交 ============
136
+
137
+ /**
138
+ * 提交分数
139
+ * @param {string} leaderboardId - 排行榜 ID
140
+ * @param {number} score - 分数
141
+ * @param {Object} metadata - 元数据
142
+ */
143
+ async submitScore(leaderboardId, score, metadata = {}) {
144
+ return this.post(`/${leaderboardId}/score`, {
145
+ score,
146
+ ...metadata
147
+ });
148
+ }
149
+
150
+ /**
151
+ * 增加分数
152
+ * @param {string} leaderboardId - 排行榜 ID
153
+ * @param {number} increment - 增量
154
+ */
155
+ async incrementScore(leaderboardId, increment) {
156
+ return this.post(`/${leaderboardId}/score/increment`, {
157
+ increment
158
+ });
159
+ }
160
+
161
+ // ============ 时间维度 ============
162
+
163
+ /**
164
+ * 获取日榜
165
+ * @param {string} leaderboardId - 排行榜 ID
166
+ * @param {string} date - 日期
167
+ * @param {Object} params - 查询参数
168
+ */
169
+ async getDailyRankings(leaderboardId, date, params = {}) {
170
+ return this.get(`/${leaderboardId}/daily`, {
171
+ date,
172
+ ...params
173
+ });
174
+ }
175
+
176
+ /**
177
+ * 获取周榜
178
+ * @param {string} leaderboardId - 排行榜 ID
179
+ * @param {string} week - 周(YYYY-WXX)
180
+ * @param {Object} params - 查询参数
181
+ */
182
+ async getWeeklyRankings(leaderboardId, week, params = {}) {
183
+ return this.get(`/${leaderboardId}/weekly`, {
184
+ week,
185
+ ...params
186
+ });
187
+ }
188
+
189
+ /**
190
+ * 获取月榜
191
+ * @param {string} leaderboardId - 排行榜 ID
192
+ * @param {string} month - 月份(YYYY-MM)
193
+ * @param {Object} params - 查询参数
194
+ */
195
+ async getMonthlyRankings(leaderboardId, month, params = {}) {
196
+ return this.get(`/${leaderboardId}/monthly`, {
197
+ month,
198
+ ...params
199
+ });
200
+ }
201
+
202
+ // ============ 好友排名 ============
203
+
204
+ /**
205
+ * 获取好友排名
206
+ * @param {string} leaderboardId - 排行榜 ID
207
+ * @param {Object} params - 查询参数
208
+ */
209
+ async getFriendRankings(leaderboardId, params = {}) {
210
+ return this.get(`/${leaderboardId}/friends`, params);
211
+ }
212
+
213
+ // ============ 管理员操作 ============
214
+
215
+ /**
216
+ * 创建排行榜
217
+ * @param {Object} data - 排行榜数据
218
+ */
219
+ async createLeaderboard(data) {
220
+ return this.post('/admin', data);
221
+ }
222
+
223
+ /**
224
+ * 更新排行榜
225
+ * @param {string} leaderboardId - 排行榜 ID
226
+ * @param {Object} updates - 更新数据
227
+ */
228
+ async updateLeaderboard(leaderboardId, updates) {
229
+ return this.put(`/admin/${leaderboardId}`, updates);
230
+ }
231
+
232
+ /**
233
+ * 删除排行榜
234
+ * @param {string} leaderboardId - 排行榜 ID
235
+ */
236
+ async deleteLeaderboard(leaderboardId) {
237
+ return this.delete(`/admin/${leaderboardId}`);
238
+ }
239
+
240
+ /**
241
+ * 重置排行榜
242
+ * @param {string} leaderboardId - 排行榜 ID
243
+ */
244
+ async resetLeaderboard(leaderboardId) {
245
+ return this.post(`/admin/${leaderboardId}/reset`);
246
+ }
247
+ }
248
+
249
+ // 创建默认实例
250
+ const leaderboardApi = new LeaderboardApiClient();
251
+
252
+ /**
253
+ * Leaderboard SDK - 类型定义
254
+ */
255
+
256
+ // 榜单类型
257
+ const BoardType = {
258
+ DAILY: 'daily',
259
+ WEEKLY: 'weekly',
260
+ MONTHLY: 'monthly',
261
+ TOTAL: 'total',
262
+ SEASON: 'season'
263
+ };
264
+
265
+ // 排行维度
266
+ const RankDimension = {
267
+ POINTS: 'points',
268
+ // 积分
269
+ LEVEL: 'level',
270
+ // 等级
271
+ WEALTH: 'wealth',
272
+ // 财富
273
+ INFLUENCE: 'influence',
274
+ // 影响力
275
+ CONTRIBUTION: 'contribution',
276
+ // 贡献
277
+ ACTIVITY: 'activity' // 活跃度
278
+ };
279
+
280
+ // 排名变化类型
281
+ const RankChangeType = {
282
+ UP: 'up',
283
+ DOWN: 'down',
284
+ SAME: 'same',
285
+ NEW: 'new'
286
+ };
287
+
288
+ // 榜单配置
289
+ const BoardConfig = {
290
+ [BoardType.DAILY]: {
291
+ label: 'boardDaily',
292
+ icon: '📆',
293
+ refresh: '每日更新'
294
+ },
295
+ [BoardType.WEEKLY]: {
296
+ label: 'boardWeekly',
297
+ icon: '📅',
298
+ refresh: '每周更新'
299
+ },
300
+ [BoardType.MONTHLY]: {
301
+ label: 'boardMonthly',
302
+ icon: '🗓️',
303
+ refresh: '每月更新'
304
+ },
305
+ [BoardType.TOTAL]: {
306
+ label: 'boardTotal',
307
+ icon: '🏆',
308
+ refresh: '实时更新'
309
+ },
310
+ [BoardType.SEASON]: {
311
+ label: 'boardSeason',
312
+ icon: '🎖️',
313
+ refresh: '赛季结算'
314
+ }
315
+ };
316
+
317
+ // 维度配置
318
+ const DimensionConfig = {
319
+ [RankDimension.POINTS]: {
320
+ label: 'dimPoints',
321
+ icon: '⭐',
322
+ color: '#faad14'
323
+ },
324
+ [RankDimension.LEVEL]: {
325
+ label: 'dimLevel',
326
+ icon: '📈',
327
+ color: '#52c41a'
328
+ },
329
+ [RankDimension.WEALTH]: {
330
+ label: 'dimWealth',
331
+ icon: '💰',
332
+ color: '#eb2f96'
333
+ },
334
+ [RankDimension.INFLUENCE]: {
335
+ label: 'dimInfluence',
336
+ icon: '👑',
337
+ color: '#722ed1'
338
+ },
339
+ [RankDimension.CONTRIBUTION]: {
340
+ label: 'dimContribution',
341
+ icon: '🎁',
342
+ color: '#1890ff'
343
+ },
344
+ [RankDimension.ACTIVITY]: {
345
+ label: 'dimActivity',
346
+ icon: '🔥',
347
+ color: '#ff4d4f'
348
+ }
349
+ };
350
+
351
+ // 排名样式
352
+ const RankStyle = {
353
+ 1: {
354
+ bg: 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)',
355
+ icon: '🥇'
356
+ },
357
+ 2: {
358
+ bg: 'linear-gradient(135deg, #C0C0C0 0%, #A0A0A0 100%)',
359
+ icon: '🥈'
360
+ },
361
+ 3: {
362
+ bg: 'linear-gradient(135deg, #CD7F32 0%, #8B4513 100%)',
363
+ icon: '🥉'
364
+ }
365
+ };
366
+
367
+ // 格式化分数
368
+ function formatScore(score, dimension) {
369
+ if (typeof score !== 'number') return score;
370
+ if (score >= 1000000) return (score / 1000000).toFixed(1) + 'M';
371
+ if (score >= 1000) return (score / 1000).toFixed(1) + 'K';
372
+ return score.toLocaleString();
373
+ }
374
+
375
+ // 获取排名变化
376
+ function getRankChange(current, previous) {
377
+ if (!previous) return {
378
+ type: RankChangeType.NEW,
379
+ value: 0
380
+ };
381
+ if (current < previous) return {
382
+ type: RankChangeType.UP,
383
+ value: previous - current
384
+ };
385
+ if (current > previous) return {
386
+ type: RankChangeType.DOWN,
387
+ value: current - previous
388
+ };
389
+ return {
390
+ type: RankChangeType.SAME,
391
+ value: 0
392
+ };
393
+ }
394
+ const LeaderboardType = {};
395
+ const Period = {};
396
+
397
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
398
+ const messages = {
399
+ en: {
400
+ 'lb.title': 'Leaderboard',
401
+ 'lb.rank': 'Rank',
402
+ 'lb.score': 'Score'
403
+ },
404
+ zh: {
405
+ 'lb.title': '排行榜',
406
+ 'lb.rank': '排名',
407
+ 'lb.score': '分数'
408
+ },
409
+ ja: {
410
+ 'lb.title': 'ランキング',
411
+ 'lb.rank': '順位',
412
+ 'lb.score': 'スコア'
413
+ },
414
+ ko: {
415
+ 'lb.title': '排행榜',
416
+ 'lb.rank': '순위',
417
+ 'lb.score': '점수'
418
+ }
419
+ };
420
+ let currentLang = 'en';
421
+ function setLanguage(l) {
422
+ if (SUPPORTED_LANGUAGES.includes(l)) currentLang = l;
423
+ }
424
+ function getLanguage() {
425
+ return currentLang;
426
+ }
427
+ function t(k) {
428
+ return messages[currentLang]?.[k] || messages.en?.[k] || k;
429
+ }
430
+
431
+ /**
432
+ * Leaderboard SDK - React Hooks
433
+ * 排行榜相关的状态管理
434
+ */
435
+
436
+
437
+ /**
438
+ * 获取排行榜
439
+ */
440
+ function useLeaderboard$1(type = 'points', period = 'weekly', options = {}) {
441
+ const [rankings, setRankings] = useState([]);
442
+ const [loading, setLoading] = useState(true);
443
+ const [error, setError] = useState(null);
444
+ const [pagination, setPagination] = useState({
445
+ page: 1,
446
+ total: 0,
447
+ hasMore: true
448
+ });
449
+ const {
450
+ limit = 50
451
+ } = options;
452
+ const fetchRankings = useCallback(async (page = 1) => {
453
+ try {
454
+ setLoading(true);
455
+ const response = await leaderboardApi.getRankings(type, period, {
456
+ page,
457
+ limit
458
+ });
459
+ if (page === 1) {
460
+ setRankings(response.data || []);
461
+ } else {
462
+ setRankings(prev => [...prev, ...(response.data || [])]);
463
+ }
464
+ setPagination({
465
+ page,
466
+ total: response.total || 0,
467
+ hasMore: response.hasMore || false
468
+ });
469
+ setError(null);
470
+ } catch (err) {
471
+ setError(err.message);
472
+ } finally {
473
+ setLoading(false);
474
+ }
475
+ }, [type, period, limit]);
476
+ useEffect(() => {
477
+ fetchRankings(1);
478
+ }, [fetchRankings]);
479
+ const loadMore = useCallback(() => {
480
+ if (!loading && pagination.hasMore) {
481
+ fetchRankings(pagination.page + 1);
482
+ }
483
+ }, [loading, pagination, fetchRankings]);
484
+ return {
485
+ rankings,
486
+ loading,
487
+ error,
488
+ pagination,
489
+ loadMore,
490
+ refresh: () => fetchRankings(1)
491
+ };
492
+ }
493
+
494
+ /**
495
+ * 获取我的排名
496
+ */
497
+ function useMyRank(type = 'points', period = 'weekly') {
498
+ const [rank, setRank] = useState(null);
499
+ const [loading, setLoading] = useState(true);
500
+ const [error, setError] = useState(null);
501
+ const fetchRank = useCallback(async () => {
502
+ try {
503
+ setLoading(true);
504
+ const response = await leaderboardApi.getMyRank(type, period);
505
+ setRank(response);
506
+ setError(null);
507
+ } catch (err) {
508
+ setError(err.message);
509
+ } finally {
510
+ setLoading(false);
511
+ }
512
+ }, [type, period]);
513
+ useEffect(() => {
514
+ fetchRank();
515
+ }, [fetchRank]);
516
+ return {
517
+ rank: rank?.rank,
518
+ score: rank?.score,
519
+ change: rank?.change,
520
+ percentile: rank?.percentile,
521
+ loading,
522
+ error,
523
+ refresh: fetchRank
524
+ };
525
+ }
526
+
527
+ /**
528
+ * 排行榜前三名
529
+ */
530
+ function useTopThree(type = 'points', period = 'weekly') {
531
+ const [top, setTop] = useState([]);
532
+ const [loading, setLoading] = useState(true);
533
+ const [error, setError] = useState(null);
534
+ const fetchTop = useCallback(async () => {
535
+ try {
536
+ setLoading(true);
537
+ const response = await leaderboardApi.getTopRankers(type, period, 3);
538
+ setTop(response.data || []);
539
+ setError(null);
540
+ } catch (err) {
541
+ setError(err.message);
542
+ } finally {
543
+ setLoading(false);
544
+ }
545
+ }, [type, period]);
546
+ useEffect(() => {
547
+ fetchTop();
548
+ }, [fetchTop]);
549
+ const [first, second, third] = top;
550
+ return {
551
+ first,
552
+ second,
553
+ third,
554
+ top,
555
+ loading,
556
+ error,
557
+ refresh: fetchTop
558
+ };
559
+ }
560
+
561
+ /**
562
+ * 排行榜周围排名
563
+ */
564
+ function useNearbyRanks(type = 'points', period = 'weekly') {
565
+ const [nearby, setNearby] = useState([]);
566
+ const [currentUser, setCurrentUser] = useState(null);
567
+ const [loading, setLoading] = useState(true);
568
+ const [error, setError] = useState(null);
569
+ const fetchNearby = useCallback(async () => {
570
+ try {
571
+ setLoading(true);
572
+ const response = await leaderboardApi.getNearbyRanks(type, period);
573
+ setNearby(response.data || []);
574
+ setCurrentUser(response.currentUser);
575
+ setError(null);
576
+ } catch (err) {
577
+ setError(err.message);
578
+ } finally {
579
+ setLoading(false);
580
+ }
581
+ }, [type, period]);
582
+ useEffect(() => {
583
+ fetchNearby();
584
+ }, [fetchNearby]);
585
+ return {
586
+ nearby,
587
+ currentUser,
588
+ loading,
589
+ error,
590
+ refresh: fetchNearby
591
+ };
592
+ }
593
+
594
+ /**
595
+ * 排行榜类型切换
596
+ */
597
+ function useLeaderboardTabs(defaultType = 'points', defaultPeriod = 'weekly') {
598
+ const [type, setType] = useState(defaultType);
599
+ const [period, setPeriod] = useState(defaultPeriod);
600
+ const types = useMemo(() => [{
601
+ value: 'points',
602
+ label: '积分榜'
603
+ }, {
604
+ value: 'level',
605
+ label: '等级榜'
606
+ }, {
607
+ value: 'activity',
608
+ label: '活跃榜'
609
+ }, {
610
+ value: 'contribution',
611
+ label: '贡献榜'
612
+ }], []);
613
+ const periods = useMemo(() => [{
614
+ value: 'daily',
615
+ label: '日榜'
616
+ }, {
617
+ value: 'weekly',
618
+ label: '周榜'
619
+ }, {
620
+ value: 'monthly',
621
+ label: '月榜'
622
+ }, {
623
+ value: 'allTime',
624
+ label: '总榜'
625
+ }], []);
626
+ return {
627
+ type,
628
+ setType,
629
+ period,
630
+ setPeriod,
631
+ types,
632
+ periods
633
+ };
634
+ }
635
+
636
+ /**
637
+ * 排行榜倒计时
638
+ */
639
+ function useLeaderboardCountdown(period = 'weekly') {
640
+ const [timeLeft, setTimeLeft] = useState(calculateTimeLeft(period));
641
+ useEffect(() => {
642
+ const timer = setInterval(() => {
643
+ setTimeLeft(calculateTimeLeft(period));
644
+ }, 1000);
645
+ return () => clearInterval(timer);
646
+ }, [period]);
647
+ return timeLeft;
648
+ }
649
+
650
+ /**
651
+ * 排行榜奖励
652
+ */
653
+ function useLeaderboardPrizes(type = 'points', period = 'weekly') {
654
+ const [prizes, setPrizes] = useState([]);
655
+ const [loading, setLoading] = useState(true);
656
+ const [error, setError] = useState(null);
657
+ const fetchPrizes = useCallback(async () => {
658
+ try {
659
+ setLoading(true);
660
+ const response = await leaderboardApi.getPrizes(type, period);
661
+ setPrizes(response.prizes || []);
662
+ setError(null);
663
+ } catch (err) {
664
+ setError(err.message);
665
+ } finally {
666
+ setLoading(false);
667
+ }
668
+ }, [type, period]);
669
+ useEffect(() => {
670
+ fetchPrizes();
671
+ }, [fetchPrizes]);
672
+ return {
673
+ prizes,
674
+ loading,
675
+ error,
676
+ refresh: fetchPrizes
677
+ };
678
+ }
679
+
680
+ // 工具函数
681
+ function calculateTimeLeft(period) {
682
+ const now = new Date();
683
+ let end;
684
+ switch (period) {
685
+ case 'daily':
686
+ end = new Date(now);
687
+ end.setHours(24, 0, 0, 0);
688
+ break;
689
+ case 'weekly':
690
+ end = new Date(now);
691
+ end.setDate(end.getDate() + (7 - end.getDay()));
692
+ end.setHours(0, 0, 0, 0);
693
+ break;
694
+ case 'monthly':
695
+ end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
696
+ break;
697
+ default:
698
+ return null;
699
+ }
700
+ const diff = end - now;
701
+ return {
702
+ days: Math.floor(diff / (1000 * 60 * 60 * 24)),
703
+ hours: Math.floor(diff % (1000 * 60 * 60 * 24) / (1000 * 60 * 60)),
704
+ minutes: Math.floor(diff % (1000 * 60 * 60) / (1000 * 60)),
705
+ seconds: Math.floor(diff % (1000 * 60) / 1000)
706
+ };
707
+ }
708
+
709
+ const MEDALS = {
710
+ 1: {
711
+ emoji: '🥇',
712
+ bg: 'linear-gradient(135deg,#fde68a,#fbbf24)',
713
+ color: '#92400e'
714
+ },
715
+ 2: {
716
+ emoji: '🥈',
717
+ bg: 'linear-gradient(135deg,#e5e7eb,#d1d5db)',
718
+ color: '#374151'
719
+ },
720
+ 3: {
721
+ emoji: '🥉',
722
+ bg: 'linear-gradient(135deg,#fed7aa,#fdba74)',
723
+ color: '#9a3412'
724
+ }
725
+ };
726
+ function RankBadge({
727
+ rank,
728
+ size = 28,
729
+ className = ''
730
+ }) {
731
+ const medal = MEDALS[rank];
732
+ if (medal) return /*#__PURE__*/React.createElement("div", {
733
+ className: `qlb-medal ${className}`,
734
+ style: {
735
+ width: size,
736
+ height: size,
737
+ borderRadius: '50%',
738
+ background: medal.bg,
739
+ display: 'flex',
740
+ alignItems: 'center',
741
+ justifyContent: 'center',
742
+ fontSize: size * 0.5,
743
+ flexShrink: 0,
744
+ boxShadow: '0 2px 6px rgba(0,0,0,0.1)'
745
+ }
746
+ }, medal.emoji);
747
+ return /*#__PURE__*/React.createElement("div", {
748
+ className: className,
749
+ style: {
750
+ width: size,
751
+ height: size,
752
+ borderRadius: '50%',
753
+ background: '#f4f4f5',
754
+ display: 'flex',
755
+ alignItems: 'center',
756
+ justifyContent: 'center',
757
+ fontSize: 11,
758
+ fontWeight: 700,
759
+ color: '#71717a',
760
+ flexShrink: 0
761
+ }
762
+ }, rank);
763
+ }
764
+
765
+ function Leaderboard({
766
+ items = [],
767
+ title = 'Leaderboard',
768
+ valueLabel = 'Score',
769
+ highlightUser,
770
+ showAvatars = true,
771
+ className = ''
772
+ }) {
773
+ return /*#__PURE__*/React.createElement("div", {
774
+ className: `qlb-board ${className}`,
775
+ style: {
776
+ borderRadius: 16,
777
+ border: '1px solid #e4e4e7',
778
+ background: '#fff',
779
+ overflow: 'hidden'
780
+ }
781
+ }, title && /*#__PURE__*/React.createElement("div", {
782
+ style: {
783
+ padding: '16px 20px',
784
+ borderBottom: '1px solid #f4f4f5',
785
+ fontSize: 16,
786
+ fontWeight: 700,
787
+ color: '#18181b'
788
+ }
789
+ }, title), /*#__PURE__*/React.createElement("div", null, items.map((item, i) => {
790
+ const rank = item.rank || i + 1;
791
+ const isHighlight = highlightUser && (item.id === highlightUser || item.name === highlightUser);
792
+ return /*#__PURE__*/React.createElement("div", {
793
+ key: item.id || i,
794
+ className: "qlb-row",
795
+ style: {
796
+ display: 'flex',
797
+ alignItems: 'center',
798
+ gap: 12,
799
+ padding: '10px 20px',
800
+ borderBottom: i < items.length - 1 ? '1px solid #f4f4f5' : 'none',
801
+ background: isHighlight ? 'rgba(59,130,246,0.04)' : 'transparent',
802
+ transition: 'all 0.15s'
803
+ }
804
+ }, /*#__PURE__*/React.createElement(RankBadge, {
805
+ rank: rank
806
+ }), showAvatars && (item.avatar ? /*#__PURE__*/React.createElement("img", {
807
+ src: item.avatar,
808
+ alt: "",
809
+ style: {
810
+ width: 32,
811
+ height: 32,
812
+ borderRadius: '50%',
813
+ objectFit: 'cover',
814
+ flexShrink: 0
815
+ }
816
+ }) : /*#__PURE__*/React.createElement("div", {
817
+ style: {
818
+ width: 32,
819
+ height: 32,
820
+ borderRadius: '50%',
821
+ background: '#eff6ff',
822
+ display: 'flex',
823
+ alignItems: 'center',
824
+ justifyContent: 'center',
825
+ fontSize: 14,
826
+ fontWeight: 700,
827
+ color: '#3b82f6',
828
+ flexShrink: 0
829
+ }
830
+ }, (item.name || '?')[0])), /*#__PURE__*/React.createElement("div", {
831
+ style: {
832
+ flex: 1,
833
+ overflow: 'hidden'
834
+ }
835
+ }, /*#__PURE__*/React.createElement("div", {
836
+ style: {
837
+ fontSize: 14,
838
+ fontWeight: isHighlight ? 700 : 500,
839
+ color: '#18181b',
840
+ overflow: 'hidden',
841
+ textOverflow: 'ellipsis',
842
+ whiteSpace: 'nowrap'
843
+ }
844
+ }, item.name), item.subtitle && /*#__PURE__*/React.createElement("div", {
845
+ style: {
846
+ fontSize: 11,
847
+ color: '#a1a1aa'
848
+ }
849
+ }, item.subtitle)), /*#__PURE__*/React.createElement("div", {
850
+ style: {
851
+ fontSize: 15,
852
+ fontWeight: 700,
853
+ color: rank <= 3 ? '#f59e0b' : '#18181b',
854
+ textAlign: 'right'
855
+ }
856
+ }, typeof item.value === 'number' ? item.value.toLocaleString() : item.value, item.change && /*#__PURE__*/React.createElement("div", {
857
+ style: {
858
+ fontSize: 11,
859
+ color: item.change > 0 ? '#22c55e' : '#ef4444',
860
+ fontWeight: 500
861
+ }
862
+ }, item.change > 0 ? '↑' : '↓', Math.abs(item.change))));
863
+ })));
864
+ }
865
+
866
+ function TopThree({
867
+ items = [],
868
+ className = ''
869
+ }) {
870
+ const ordered = [items[1], items[0], items[2]].filter(Boolean);
871
+ const heights = [100, 140, 80];
872
+ const ranks = [2, 1, 3];
873
+ return /*#__PURE__*/React.createElement("div", {
874
+ className: `qlb-podium ${className}`,
875
+ style: {
876
+ display: 'flex',
877
+ alignItems: 'flex-end',
878
+ justifyContent: 'center',
879
+ gap: 8
880
+ }
881
+ }, ordered.map((item, i) => {
882
+ const rank = ranks[i];
883
+ const h = heights[i];
884
+ return /*#__PURE__*/React.createElement("div", {
885
+ key: i,
886
+ style: {
887
+ display: 'flex',
888
+ flexDirection: 'column',
889
+ alignItems: 'center',
890
+ width: 100
891
+ }
892
+ }, item.avatar ? /*#__PURE__*/React.createElement("img", {
893
+ src: item.avatar,
894
+ alt: "",
895
+ style: {
896
+ width: rank === 1 ? 56 : 44,
897
+ height: rank === 1 ? 56 : 44,
898
+ borderRadius: '50%',
899
+ objectFit: 'cover',
900
+ border: rank === 1 ? '3px solid #fbbf24' : rank === 2 ? '2px solid #d1d5db' : '2px solid #fdba74',
901
+ marginBottom: 6
902
+ }
903
+ }) : /*#__PURE__*/React.createElement("div", {
904
+ style: {
905
+ width: rank === 1 ? 56 : 44,
906
+ height: rank === 1 ? 56 : 44,
907
+ borderRadius: '50%',
908
+ background: '#eff6ff',
909
+ display: 'flex',
910
+ alignItems: 'center',
911
+ justifyContent: 'center',
912
+ fontSize: rank === 1 ? 20 : 16,
913
+ fontWeight: 700,
914
+ color: '#3b82f6',
915
+ marginBottom: 6
916
+ }
917
+ }, (item.name || '?')[0]), /*#__PURE__*/React.createElement("div", {
918
+ style: {
919
+ fontSize: 12,
920
+ fontWeight: 600,
921
+ color: '#18181b',
922
+ textAlign: 'center',
923
+ overflow: 'hidden',
924
+ textOverflow: 'ellipsis',
925
+ whiteSpace: 'nowrap',
926
+ maxWidth: 100
927
+ }
928
+ }, item.name), /*#__PURE__*/React.createElement("div", {
929
+ style: {
930
+ fontSize: 14,
931
+ fontWeight: 800,
932
+ color: '#f59e0b',
933
+ marginTop: 2
934
+ }
935
+ }, typeof item.value === 'number' ? item.value.toLocaleString() : item.value), /*#__PURE__*/React.createElement("div", {
936
+ style: {
937
+ width: '100%',
938
+ height: h,
939
+ marginTop: 8,
940
+ borderRadius: '8px 8px 0 0',
941
+ background: rank === 1 ? 'linear-gradient(180deg,#fbbf24,#d97706)' : rank === 2 ? 'linear-gradient(180deg,#d1d5db,#9ca3af)' : 'linear-gradient(180deg,#fdba74,#f97316)',
942
+ display: 'flex',
943
+ alignItems: 'center',
944
+ justifyContent: 'center'
945
+ }
946
+ }, /*#__PURE__*/React.createElement("span", {
947
+ style: {
948
+ fontSize: 24,
949
+ fontWeight: 800,
950
+ color: 'rgba(255,255,255,0.9)'
951
+ }
952
+ }, "#", rank)));
953
+ }));
954
+ }
955
+
956
+ /**
957
+ * Leaderboard SDK - Context Provider
958
+ */
959
+
960
+ const LeaderboardContext = /*#__PURE__*/createContext(null);
961
+ function useLeaderboard() {
962
+ const context = useContext(LeaderboardContext);
963
+ if (!context) {
964
+ throw new Error('useLeaderboard must be used within a LeaderboardProvider');
965
+ }
966
+ return context;
967
+ }
968
+
969
+ /**
970
+ * Leaderboard SDK - RankingList 组件
971
+ * 排行榜列表
972
+ */
973
+
974
+ function RankingList({
975
+ limit = 20,
976
+ showMyRank = true,
977
+ onUserClick,
978
+ className = ''
979
+ }) {
980
+ const {
981
+ ranking,
982
+ myRank,
983
+ dimension,
984
+ loading,
985
+ loadRanking,
986
+ loadMyRank
987
+ } = useLeaderboard();
988
+ useEffect(() => {
989
+ loadRanking(dimension, undefined, {
990
+ limit
991
+ });
992
+ if (showMyRank) loadMyRank();
993
+ }, [dimension, limit, showMyRank, loadRanking, loadMyRank]);
994
+ const dimConfig = DimensionConfig[dimension] || {};
995
+ const renderRankBadge = rank => {
996
+ const style = RankStyle[rank];
997
+ if (style) {
998
+ return /*#__PURE__*/React.createElement("span", {
999
+ className: "rank-badge top",
1000
+ style: {
1001
+ background: style.bg
1002
+ }
1003
+ }, style.icon);
1004
+ }
1005
+ return /*#__PURE__*/React.createElement("span", {
1006
+ className: "rank-badge"
1007
+ }, rank);
1008
+ };
1009
+ const renderChange = item => {
1010
+ const change = getRankChange(item.rank, item.previous_rank);
1011
+ if (change.type === RankChangeType.SAME) return null;
1012
+ return /*#__PURE__*/React.createElement("span", {
1013
+ className: `rank-change ${change.type}`
1014
+ }, change.type === RankChangeType.UP && `↑${change.value}`, change.type === RankChangeType.DOWN && `↓${change.value}`, change.type === RankChangeType.NEW && t('rankNew'));
1015
+ };
1016
+ return /*#__PURE__*/React.createElement("div", {
1017
+ className: `ranking-list ${className}`
1018
+ }, loading && /*#__PURE__*/React.createElement("div", {
1019
+ className: "ranking-loading"
1020
+ }, t('loading')), /*#__PURE__*/React.createElement("div", {
1021
+ className: "ranking-items"
1022
+ }, ranking.map((item, index) => /*#__PURE__*/React.createElement("div", {
1023
+ key: item.user_id || index,
1024
+ className: `ranking-item ${item.rank <= 3 ? 'top-rank' : ''}`,
1025
+ onClick: () => onUserClick?.(item)
1026
+ }, renderRankBadge(item.rank), /*#__PURE__*/React.createElement("div", {
1027
+ className: "user-avatar"
1028
+ }, item.avatar ? /*#__PURE__*/React.createElement("img", {
1029
+ src: item.avatar,
1030
+ alt: item.nickname
1031
+ }) : /*#__PURE__*/React.createElement("span", null, item.nickname?.charAt(0) || '?')), /*#__PURE__*/React.createElement("div", {
1032
+ className: "user-info"
1033
+ }, /*#__PURE__*/React.createElement("span", {
1034
+ className: "user-name"
1035
+ }, item.nickname), renderChange(item)), /*#__PURE__*/React.createElement("div", {
1036
+ className: "user-score",
1037
+ style: {
1038
+ color: dimConfig.color
1039
+ }
1040
+ }, /*#__PURE__*/React.createElement("span", {
1041
+ className: "score-icon"
1042
+ }, dimConfig.icon), /*#__PURE__*/React.createElement("span", {
1043
+ className: "score-value"
1044
+ }, formatScore(item.score)))))), ranking.length === 0 && !loading && /*#__PURE__*/React.createElement("div", {
1045
+ className: "ranking-empty"
1046
+ }, t('noData')), showMyRank && myRank && /*#__PURE__*/React.createElement("div", {
1047
+ className: "my-rank-bar"
1048
+ }, /*#__PURE__*/React.createElement("span", {
1049
+ className: "my-rank-label"
1050
+ }, t('myRanking')), /*#__PURE__*/React.createElement("div", {
1051
+ className: "my-rank-info"
1052
+ }, /*#__PURE__*/React.createElement("span", {
1053
+ className: "my-rank-num"
1054
+ }, myRank.rank ? `#${myRank.rank}` : t('notRanked')), /*#__PURE__*/React.createElement("span", {
1055
+ className: "my-rank-score",
1056
+ style: {
1057
+ color: dimConfig.color
1058
+ }
1059
+ }, dimConfig.icon, " ", formatScore(myRank.score)))));
1060
+ }
1061
+
1062
+ function RankingItem({
1063
+ item,
1064
+ index,
1065
+ dimension = 'points',
1066
+ onUserClick
1067
+ }) {
1068
+ const rank = item.rank || index + 1;
1069
+ const dimConfig = DimensionConfig[dimension] || {};
1070
+ const change = getRankChange(item.rank, item.previous_rank);
1071
+ return /*#__PURE__*/React.createElement("div", {
1072
+ className: `ranking-item ${rank <= 3 ? 'top-rank' : ''}`,
1073
+ onClick: () => onUserClick?.(item),
1074
+ style: {
1075
+ display: 'flex',
1076
+ alignItems: 'center',
1077
+ gap: 12,
1078
+ padding: '12px 16px',
1079
+ borderBottom: '1px solid #f4f4f5',
1080
+ cursor: onUserClick ? 'pointer' : 'default'
1081
+ }
1082
+ }, /*#__PURE__*/React.createElement("span", {
1083
+ className: "rank-badge"
1084
+ }, rank), /*#__PURE__*/React.createElement("div", {
1085
+ className: "user-avatar",
1086
+ style: {
1087
+ width: 32,
1088
+ height: 32,
1089
+ borderRadius: '50%',
1090
+ background: '#eff6ff',
1091
+ display: 'flex',
1092
+ alignItems: 'center',
1093
+ justifyindex: 'center'
1094
+ }
1095
+ }, item.avatar ? /*#__PURE__*/React.createElement("img", {
1096
+ src: item.avatar,
1097
+ alt: item.nickname,
1098
+ style: {
1099
+ width: '100%',
1100
+ height: '100%',
1101
+ borderRadius: '50%',
1102
+ objectFit: 'cover'
1103
+ }
1104
+ }) : /*#__PURE__*/React.createElement("span", null, item.nickname?.charAt(0) || '?')), /*#__PURE__*/React.createElement("div", {
1105
+ className: "user-info",
1106
+ style: {
1107
+ flex: 1
1108
+ }
1109
+ }, /*#__PURE__*/React.createElement("span", {
1110
+ className: "user-name",
1111
+ style: {
1112
+ fontWeight: 600
1113
+ }
1114
+ }, item.nickname), change.type !== RankChangeType.SAME && /*#__PURE__*/React.createElement("span", {
1115
+ className: `rank-change ${change.type}`,
1116
+ style: {
1117
+ marginLeft: 8,
1118
+ fontSize: 11,
1119
+ color: change.type === RankChangeType.UP ? '#22c55e' : '#ef4444'
1120
+ }
1121
+ }, change.type === RankChangeType.UP ? `↑${change.value}` : `↓${change.value}`)), /*#__PURE__*/React.createElement("div", {
1122
+ className: "user-score",
1123
+ style: {
1124
+ color: dimConfig.color,
1125
+ fontWeight: 700
1126
+ }
1127
+ }, /*#__PURE__*/React.createElement("span", null, dimConfig.icon), " ", formatScore(item.score)));
1128
+ }
1129
+ function MyRankCard({
1130
+ myRank,
1131
+ dimension = 'points'
1132
+ }) {
1133
+ const dimConfig = DimensionConfig[dimension] || {};
1134
+ if (!myRank) return null;
1135
+ return /*#__PURE__*/React.createElement("div", {
1136
+ className: "my-rank-card",
1137
+ style: {
1138
+ padding: '16px',
1139
+ background: '#f8fafc',
1140
+ borderRadius: '12px',
1141
+ marginTop: '16px',
1142
+ display: 'flex',
1143
+ justifyContent: 'space-between',
1144
+ alignItems: 'center'
1145
+ }
1146
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
1147
+ style: {
1148
+ fontSize: 12,
1149
+ color: '#64748b'
1150
+ }
1151
+ }, "My Ranking"), /*#__PURE__*/React.createElement("div", {
1152
+ style: {
1153
+ fontSize: 20,
1154
+ fontWeight: 800,
1155
+ color: '#0f172a'
1156
+ }
1157
+ }, myRank.rank ? `#${myRank.rank}` : 'Not Ranked')), /*#__PURE__*/React.createElement("div", {
1158
+ style: {
1159
+ textAlign: 'right'
1160
+ }
1161
+ }, /*#__PURE__*/React.createElement("div", {
1162
+ style: {
1163
+ fontSize: 12,
1164
+ color: '#64748b'
1165
+ }
1166
+ }, "My Score"), /*#__PURE__*/React.createElement("div", {
1167
+ style: {
1168
+ fontSize: 18,
1169
+ fontWeight: 700,
1170
+ color: dimConfig.color
1171
+ }
1172
+ }, dimConfig.icon, " ", formatScore(myRank.score))));
1173
+ }
1174
+ function LeaderboardTabs({
1175
+ type,
1176
+ setType,
1177
+ period,
1178
+ setPeriod,
1179
+ types = [],
1180
+ periods = []
1181
+ }) {
1182
+ return /*#__PURE__*/React.createElement("div", {
1183
+ className: "leaderboard-tabs",
1184
+ style: {
1185
+ display: 'flex',
1186
+ flexDirection: 'column',
1187
+ gap: 8,
1188
+ marginBottom: 16
1189
+ }
1190
+ }, types.length > 0 && /*#__PURE__*/React.createElement("div", {
1191
+ style: {
1192
+ display: 'flex',
1193
+ gap: 8
1194
+ }
1195
+ }, types.map(t => /*#__PURE__*/React.createElement("button", {
1196
+ key: t.value,
1197
+ onClick: () => setType?.(t.value),
1198
+ style: {
1199
+ padding: '6px 12px',
1200
+ borderRadius: 8,
1201
+ border: 'none',
1202
+ background: type === t.value ? '#3b82f6' : '#f1f5f9',
1203
+ color: type === t.value ? '#fff' : '#475569',
1204
+ cursor: 'pointer',
1205
+ fontSize: 13,
1206
+ fontWeight: 600
1207
+ }
1208
+ }, t.label))), periods.length > 0 && /*#__PURE__*/React.createElement("div", {
1209
+ style: {
1210
+ display: 'flex',
1211
+ gap: 8
1212
+ }
1213
+ }, periods.map(p => /*#__PURE__*/React.createElement("button", {
1214
+ key: p.value,
1215
+ onClick: () => setPeriod?.(p.value),
1216
+ style: {
1217
+ padding: '4px 10px',
1218
+ borderRadius: 6,
1219
+ border: 'none',
1220
+ background: period === p.value ? '#3b82f6' : '#f8fafc',
1221
+ color: period === p.value ? '#fff' : '#64748b',
1222
+ cursor: 'pointer',
1223
+ fontSize: 12
1224
+ }
1225
+ }, p.label))));
1226
+ }
1227
+ function LeaderboardCountdown({
1228
+ period = 'weekly'
1229
+ }) {
1230
+ return /*#__PURE__*/React.createElement("div", {
1231
+ className: "leaderboard-countdown",
1232
+ style: {
1233
+ fontSize: 12,
1234
+ color: '#64748b',
1235
+ background: '#f1f5f9',
1236
+ padding: '6px 12px',
1237
+ borderRadius: 6,
1238
+ display: 'inline-block'
1239
+ }
1240
+ }, "Time Remaining: ", period);
1241
+ }
1242
+ function LeaderboardPanel({
1243
+ title = 'Leaderboard',
1244
+ children
1245
+ }) {
1246
+ return /*#__PURE__*/React.createElement("div", {
1247
+ className: "leaderboard-panel",
1248
+ style: {
1249
+ background: '#fff',
1250
+ borderRadius: 16,
1251
+ border: '1px solid #e2e8f0',
1252
+ padding: 20,
1253
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.05)'
1254
+ }
1255
+ }, /*#__PURE__*/React.createElement("h2", {
1256
+ style: {
1257
+ margin: '0 0 16px 0',
1258
+ fontSize: 18,
1259
+ fontWeight: 700,
1260
+ color: '#0f172a'
1261
+ }
1262
+ }, title), children);
1263
+ }
1264
+
1265
+ // ============ Services & Client ============
1266
+ async function getRankings(leaderboardId, period, params) {
1267
+ return leaderboardApi.getRankings(leaderboardId, period, params);
1268
+ }
1269
+ async function getMyRank(leaderboardId, period) {
1270
+ return leaderboardApi.getMyRank(leaderboardId, period);
1271
+ }
1272
+ async function getTopRankers(leaderboardId, period, n) {
1273
+ return leaderboardApi.getTopRankers(leaderboardId, period, n);
1274
+ }
1275
+ async function getPrizes(leaderboardId, period) {
1276
+ return leaderboardApi.getPrizes(leaderboardId, period);
1277
+ }
1278
+
1279
+ export { BoardConfig, BoardType, DimensionConfig, Leaderboard, LeaderboardApiClient, LeaderboardCountdown, LeaderboardPanel, LeaderboardTabs, LeaderboardType, MyRankCard, Period, RankBadge, RankChangeType, RankDimension, RankStyle, RankingItem, RankingList, SUPPORTED_LANGUAGES, TopThree, formatScore, getLanguage, getMyRank, getPrizes, getRankChange, getRankings, getTopRankers, leaderboardApi, messages, setLanguage, t, useLeaderboard$1 as useLeaderboard, useLeaderboardCountdown, useLeaderboardPrizes, useLeaderboardTabs, useMyRank, useNearbyRanks, useTopThree };
1280
+ //# sourceMappingURL=index.esm.js.map