@quantabit/level-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/index.cjs +1367 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +1340 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +83 -0
- package/types/index.d.ts +102 -0
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useContext, createContext } from 'react';
|
|
2
|
+
import { BaseApiClient } from '@quantabit/sdk-config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Level SDK - API 客户端
|
|
6
|
+
* 等级系统后端接口封装
|
|
7
|
+
*
|
|
8
|
+
* 使用 BaseApiClient 基类简化代码
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 等级 API 客户端
|
|
14
|
+
*/
|
|
15
|
+
class LevelApiClient extends BaseApiClient {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
super('/level', config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============ 等级查询 ============
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 获取我的等级信息
|
|
24
|
+
*/
|
|
25
|
+
async getMyLevel() {
|
|
26
|
+
return this.get('/my');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 获取等级详情
|
|
31
|
+
* @param {number} level - 等级
|
|
32
|
+
*/
|
|
33
|
+
async getLevelInfo(level) {
|
|
34
|
+
return this.get(`/${level}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取所有等级列表
|
|
39
|
+
*/
|
|
40
|
+
async getAllLevels() {
|
|
41
|
+
return this.get('/list');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 获取升级进度
|
|
46
|
+
*/
|
|
47
|
+
async getProgress() {
|
|
48
|
+
return this.get('/my/progress');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============ 等级权益 ============
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 获取当前等级权益
|
|
55
|
+
*/
|
|
56
|
+
async getMyBenefits() {
|
|
57
|
+
return this.get('/my/benefits');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取指定等级权益
|
|
62
|
+
* @param {number} level - 等级
|
|
63
|
+
*/
|
|
64
|
+
async getLevelBenefits(level) {
|
|
65
|
+
return this.get(`/${level}/benefits`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 领取等级奖励
|
|
70
|
+
* @param {number} level - 等级
|
|
71
|
+
*/
|
|
72
|
+
async claimLevelReward(level) {
|
|
73
|
+
return this.post(`/${level}/claim`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 获取待领取的等级奖励
|
|
78
|
+
*/
|
|
79
|
+
async getPendingRewards() {
|
|
80
|
+
return this.get('/my/rewards/pending');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============ 经验值 ============
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 获取经验值明细
|
|
87
|
+
* @param {Object} params - 查询参数
|
|
88
|
+
*/
|
|
89
|
+
async getExpHistory(params = {}) {
|
|
90
|
+
return this.get('/my/exp/history', params);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 获取经验值来源
|
|
95
|
+
*/
|
|
96
|
+
async getExpSources() {
|
|
97
|
+
return this.get('/exp/sources');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============ 等级排行 ============
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 获取等级排行榜
|
|
104
|
+
* @param {Object} params - 查询参数
|
|
105
|
+
*/
|
|
106
|
+
async getLeaderboard(params = {}) {
|
|
107
|
+
return this.get('/leaderboard', params);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 获取我的排名
|
|
112
|
+
*/
|
|
113
|
+
async getMyRank() {
|
|
114
|
+
return this.get('/my/rank');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============ 特权对比 ============
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 对比两个等级权益
|
|
121
|
+
* @param {number} level1 - 等级1
|
|
122
|
+
* @param {number} level2 - 等级2
|
|
123
|
+
*/
|
|
124
|
+
async compareLevels(level1, level2) {
|
|
125
|
+
return this.get('/compare', {
|
|
126
|
+
level1,
|
|
127
|
+
level2
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 获取升级建议
|
|
133
|
+
*/
|
|
134
|
+
async getUpgradeSuggestions() {
|
|
135
|
+
return this.get('/my/upgrade-tips');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 创建默认实例
|
|
140
|
+
const levelApi = new LevelApiClient();
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Level SDK - 国际化 i18n
|
|
144
|
+
* 等级体系多语言支持
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
|
|
148
|
+
const messages = {
|
|
149
|
+
zh: {
|
|
150
|
+
// 通用
|
|
151
|
+
loading: '加载中...',
|
|
152
|
+
level: '等级',
|
|
153
|
+
experience: '经验值',
|
|
154
|
+
currentLevel: '当前等级',
|
|
155
|
+
nextLevel: '下一等级',
|
|
156
|
+
maxLevel: '满级',
|
|
157
|
+
// 等级信息
|
|
158
|
+
levelUp: '升级啦!',
|
|
159
|
+
levelUpDesc: '恭喜您升级到 Lv.{level}',
|
|
160
|
+
expProgress: '经验进度',
|
|
161
|
+
expNeeded: '还需要 {exp} 经验升级',
|
|
162
|
+
expToNext: '距离下一级还需',
|
|
163
|
+
// 权益
|
|
164
|
+
privileges: '等级权益',
|
|
165
|
+
unlockAt: '等级 {level} 解锁',
|
|
166
|
+
unlocked: '已解锁',
|
|
167
|
+
locked: '未解锁',
|
|
168
|
+
// 统计
|
|
169
|
+
totalExp: '累计经验',
|
|
170
|
+
todayExp: '今日获得',
|
|
171
|
+
weeklyExp: '本周获得',
|
|
172
|
+
monthlyExp: '本月获得',
|
|
173
|
+
// 经验来源
|
|
174
|
+
expSource: '经验来源',
|
|
175
|
+
expFromTask: '完成任务',
|
|
176
|
+
expFromLogin: '每日登录',
|
|
177
|
+
expFromContent: '发布内容',
|
|
178
|
+
expFromInteraction: '互动奖励',
|
|
179
|
+
expFromInvite: '邀请奖励',
|
|
180
|
+
expFromPurchase: '消费奖励',
|
|
181
|
+
// 排行榜
|
|
182
|
+
leaderboard: '等级排行',
|
|
183
|
+
rank: '排名',
|
|
184
|
+
myRank: '我的排名',
|
|
185
|
+
top100: 'TOP 100',
|
|
186
|
+
// 成就
|
|
187
|
+
achievements: '成就',
|
|
188
|
+
achievementUnlocked: '成就解锁',
|
|
189
|
+
achievementProgress: '成就进度',
|
|
190
|
+
// 勋章
|
|
191
|
+
badges: '勋章',
|
|
192
|
+
badgeEarned: '获得勋章',
|
|
193
|
+
// 历史
|
|
194
|
+
expHistory: '经验记录',
|
|
195
|
+
noHistory: '暂无记录',
|
|
196
|
+
// 错误
|
|
197
|
+
errorLoad: '加载失败',
|
|
198
|
+
errorRetry: '点击重试'
|
|
199
|
+
},
|
|
200
|
+
ja: {
|
|
201
|
+
// 通用
|
|
202
|
+
loading: '読み込み中...',
|
|
203
|
+
level: 'レベル',
|
|
204
|
+
experience: '経験値',
|
|
205
|
+
currentLevel: '現在のレベル',
|
|
206
|
+
nextLevel: '次のレベル',
|
|
207
|
+
maxLevel: '最大レベル',
|
|
208
|
+
// 等级信息
|
|
209
|
+
levelUp: 'レベルアップ!',
|
|
210
|
+
levelUpDesc: 'おめでとうございます!Lv.{level}にレベルアップしました',
|
|
211
|
+
expProgress: '経験値の進捗',
|
|
212
|
+
expNeeded: '次のレベルまであと {exp} EXP',
|
|
213
|
+
expToNext: '次のレベルまで',
|
|
214
|
+
// 权益
|
|
215
|
+
privileges: 'レベル特典',
|
|
216
|
+
unlockAt: 'Lv.{level}でアンロック',
|
|
217
|
+
unlocked: 'アンロック済み',
|
|
218
|
+
locked: '未アンロック',
|
|
219
|
+
// 统计
|
|
220
|
+
totalExp: '累計経験値',
|
|
221
|
+
todayExp: '本日獲得',
|
|
222
|
+
weeklyExp: '今週獲得',
|
|
223
|
+
monthlyExp: '今月獲得',
|
|
224
|
+
// 经验来源
|
|
225
|
+
expSource: '経験値の獲得元',
|
|
226
|
+
expFromTask: 'タスク完了',
|
|
227
|
+
expFromLogin: 'デイリーログイン',
|
|
228
|
+
expFromContent: 'コンテンツ公開',
|
|
229
|
+
expFromInteraction: 'インタラクションボーナス',
|
|
230
|
+
expFromInvite: '招待ボーナス',
|
|
231
|
+
expFromPurchase: '購入ボーナス',
|
|
232
|
+
// 排行榜
|
|
233
|
+
leaderboard: 'レベルランキング',
|
|
234
|
+
rank: '順位',
|
|
235
|
+
myRank: '私の順位',
|
|
236
|
+
top100: 'TOP 100',
|
|
237
|
+
// 成就
|
|
238
|
+
achievements: '実績',
|
|
239
|
+
achievementUnlocked: '実績アンロック',
|
|
240
|
+
achievementProgress: '実績の進捗',
|
|
241
|
+
// 勋章
|
|
242
|
+
badges: 'バッジ',
|
|
243
|
+
badgeEarned: 'バッジ獲得',
|
|
244
|
+
// 历史
|
|
245
|
+
expHistory: '経験値履歴',
|
|
246
|
+
noHistory: '履歴がありません',
|
|
247
|
+
// 错误
|
|
248
|
+
errorLoad: '読み込みに失敗しました',
|
|
249
|
+
errorRetry: 'クリックして再試行'
|
|
250
|
+
},
|
|
251
|
+
ko: {
|
|
252
|
+
// 通用
|
|
253
|
+
loading: '로딩 중...',
|
|
254
|
+
level: '레벨',
|
|
255
|
+
experience: '경험치',
|
|
256
|
+
currentLevel: '현재 레벨',
|
|
257
|
+
nextLevel: '다음 레벨',
|
|
258
|
+
maxLevel: '만렙',
|
|
259
|
+
// 等级信息
|
|
260
|
+
levelUp: '레벨 업!',
|
|
261
|
+
levelUpDesc: '축하합니다! Lv.{level}(으)로 레벨 업하셨습니다',
|
|
262
|
+
expProgress: '경험치 진행률',
|
|
263
|
+
expNeeded: '레벨업까지 {exp} 경험치 필요',
|
|
264
|
+
expToNext: '다음 레벨까지',
|
|
265
|
+
// 权益
|
|
266
|
+
privileges: '레벨 혜택',
|
|
267
|
+
unlockAt: '레벨 {level}에서 잠금 해제',
|
|
268
|
+
unlocked: '잠금 해제됨',
|
|
269
|
+
locked: '잠김',
|
|
270
|
+
// 统计
|
|
271
|
+
totalExp: '누적 경험치',
|
|
272
|
+
todayExp: '오늘 획득',
|
|
273
|
+
weeklyExp: '이번 주 획득',
|
|
274
|
+
monthlyExp: '이번 달 획득',
|
|
275
|
+
// 经验来源
|
|
276
|
+
expSource: '경험치 획득처',
|
|
277
|
+
expFromTask: '미션 완료',
|
|
278
|
+
expFromLogin: '출석 체크',
|
|
279
|
+
expFromContent: '콘텐츠 등록',
|
|
280
|
+
expFromInteraction: '상호작용 보너스',
|
|
281
|
+
expFromInvite: '초대 보너스',
|
|
282
|
+
expFromPurchase: '구매 보너스',
|
|
283
|
+
// 排行榜
|
|
284
|
+
leaderboard: '레벨 랭킹',
|
|
285
|
+
rank: '순위',
|
|
286
|
+
myRank: '내 순위',
|
|
287
|
+
top100: 'TOP 100',
|
|
288
|
+
// 成就
|
|
289
|
+
achievements: '업적',
|
|
290
|
+
achievementUnlocked: '업적 달성',
|
|
291
|
+
achievementProgress: '업적 진행률',
|
|
292
|
+
// 勋章
|
|
293
|
+
badges: '배지',
|
|
294
|
+
badgeEarned: '배지 획득',
|
|
295
|
+
// 历史
|
|
296
|
+
expHistory: '경험치 내역',
|
|
297
|
+
noHistory: '내역이 없습니다',
|
|
298
|
+
// 错误
|
|
299
|
+
errorLoad: '불러오기 실패',
|
|
300
|
+
errorRetry: '클릭하여 다시 시도'
|
|
301
|
+
},
|
|
302
|
+
en: {
|
|
303
|
+
// General
|
|
304
|
+
loading: 'Loading...',
|
|
305
|
+
level: 'Level',
|
|
306
|
+
experience: 'Experience',
|
|
307
|
+
currentLevel: 'Current Level',
|
|
308
|
+
nextLevel: 'Next Level',
|
|
309
|
+
maxLevel: 'Max Level',
|
|
310
|
+
// Level Info
|
|
311
|
+
levelUp: 'Level Up!',
|
|
312
|
+
levelUpDesc: 'Congratulations! You reached Lv.{level}',
|
|
313
|
+
expProgress: 'EXP Progress',
|
|
314
|
+
expNeeded: '{exp} EXP needed to level up',
|
|
315
|
+
expToNext: 'To next level',
|
|
316
|
+
// Privileges
|
|
317
|
+
privileges: 'Privileges',
|
|
318
|
+
unlockAt: 'Unlock at Level {level}',
|
|
319
|
+
unlocked: 'Unlocked',
|
|
320
|
+
locked: 'Locked',
|
|
321
|
+
// Statistics
|
|
322
|
+
totalExp: 'Total EXP',
|
|
323
|
+
todayExp: 'Today',
|
|
324
|
+
weeklyExp: 'This Week',
|
|
325
|
+
monthlyExp: 'This Month',
|
|
326
|
+
// EXP Sources
|
|
327
|
+
expSource: 'EXP Sources',
|
|
328
|
+
expFromTask: 'Task Completion',
|
|
329
|
+
expFromLogin: 'Daily Login',
|
|
330
|
+
expFromContent: 'Content Creation',
|
|
331
|
+
expFromInteraction: 'Interaction Bonus',
|
|
332
|
+
expFromInvite: 'Referral Bonus',
|
|
333
|
+
expFromPurchase: 'Purchase Bonus',
|
|
334
|
+
// Leaderboard
|
|
335
|
+
leaderboard: 'Level Leaderboard',
|
|
336
|
+
rank: 'Rank',
|
|
337
|
+
myRank: 'My Rank',
|
|
338
|
+
top100: 'TOP 100',
|
|
339
|
+
// Achievements
|
|
340
|
+
achievements: 'Achievements',
|
|
341
|
+
achievementUnlocked: 'Achievement Unlocked',
|
|
342
|
+
achievementProgress: 'Progress',
|
|
343
|
+
// Badges
|
|
344
|
+
badges: 'Badges',
|
|
345
|
+
badgeEarned: 'Badge Earned',
|
|
346
|
+
// History
|
|
347
|
+
expHistory: 'EXP History',
|
|
348
|
+
noHistory: 'No records yet',
|
|
349
|
+
// Errors
|
|
350
|
+
errorLoad: 'Failed to load',
|
|
351
|
+
errorRetry: 'Click to retry'
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
let currentLanguage = 'zh';
|
|
355
|
+
function setLanguage(lang) {
|
|
356
|
+
if (SUPPORTED_LANGUAGES.includes(lang)) {
|
|
357
|
+
currentLanguage = lang;
|
|
358
|
+
if (typeof window !== 'undefined') {
|
|
359
|
+
localStorage.setItem('qbit_did_level_language', lang);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function getLanguage() {
|
|
364
|
+
if (typeof window !== 'undefined') {
|
|
365
|
+
const saved = localStorage.getItem('qbit_did_level_language');
|
|
366
|
+
if (saved && SUPPORTED_LANGUAGES.includes(saved)) {
|
|
367
|
+
currentLanguage = saved;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return currentLanguage;
|
|
371
|
+
}
|
|
372
|
+
function t(key, params) {
|
|
373
|
+
const msgs = messages[getLanguage()] || messages.zh;
|
|
374
|
+
let text = msgs[key] || key;
|
|
375
|
+
if (params) {
|
|
376
|
+
Object.keys(params).forEach(k => {
|
|
377
|
+
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), params[k]);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return text;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Level SDK - Context Provider
|
|
385
|
+
* 等级体系状态管理
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
const LevelContext = /*#__PURE__*/createContext(null);
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Level Provider 组件
|
|
392
|
+
*/
|
|
393
|
+
function LevelProvider({
|
|
394
|
+
children,
|
|
395
|
+
apiUrl,
|
|
396
|
+
token,
|
|
397
|
+
language = 'zh',
|
|
398
|
+
onLevelUp,
|
|
399
|
+
onError
|
|
400
|
+
}) {
|
|
401
|
+
const [userLevel, setUserLevel] = useState(null);
|
|
402
|
+
const [levelConfigs, setLevelConfigs] = useState([]);
|
|
403
|
+
const [privileges, setPrivileges] = useState([]);
|
|
404
|
+
const [achievements, setAchievements] = useState([]);
|
|
405
|
+
const [badges, setBadges] = useState([]);
|
|
406
|
+
const [loading, setLoading] = useState(false);
|
|
407
|
+
const [error, setError] = useState(null);
|
|
408
|
+
|
|
409
|
+
// 初始化配置
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (apiUrl) levelApi.setBaseUrl(apiUrl);
|
|
412
|
+
if (token) levelApi.setToken(token);
|
|
413
|
+
if (language) setLanguage(language);
|
|
414
|
+
}, [apiUrl, token, language]);
|
|
415
|
+
const handleError = useCallback(err => {
|
|
416
|
+
setError(err.message);
|
|
417
|
+
onError?.(err);
|
|
418
|
+
}, [onError]);
|
|
419
|
+
|
|
420
|
+
// ============ 加载数据 ============
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 加载用户等级信息
|
|
424
|
+
*/
|
|
425
|
+
const loadUserLevel = useCallback(async () => {
|
|
426
|
+
setLoading(true);
|
|
427
|
+
setError(null);
|
|
428
|
+
try {
|
|
429
|
+
const result = await levelApi.getMyLevel();
|
|
430
|
+
const prevLevel = userLevel?.level;
|
|
431
|
+
setUserLevel(result);
|
|
432
|
+
// 检查是否升级
|
|
433
|
+
if (prevLevel && result.level > prevLevel) {
|
|
434
|
+
onLevelUp?.(result);
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
} catch (err) {
|
|
438
|
+
handleError(err);
|
|
439
|
+
throw err;
|
|
440
|
+
} finally {
|
|
441
|
+
setLoading(false);
|
|
442
|
+
}
|
|
443
|
+
}, [userLevel, onLevelUp, handleError]);
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 加载等级配置
|
|
447
|
+
*/
|
|
448
|
+
const loadLevelConfigs = useCallback(async () => {
|
|
449
|
+
try {
|
|
450
|
+
const result = await levelApi.getLevelConfigs();
|
|
451
|
+
const configs = result.items || result.data || result || [];
|
|
452
|
+
setLevelConfigs(configs);
|
|
453
|
+
return configs;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
handleError(err);
|
|
456
|
+
throw err;
|
|
457
|
+
}
|
|
458
|
+
}, [handleError]);
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 加载权益列表
|
|
462
|
+
*/
|
|
463
|
+
const loadPrivileges = useCallback(async () => {
|
|
464
|
+
try {
|
|
465
|
+
const result = await levelApi.getPrivileges();
|
|
466
|
+
const items = result.items || result.data || result || [];
|
|
467
|
+
setPrivileges(items);
|
|
468
|
+
return items;
|
|
469
|
+
} catch (err) {
|
|
470
|
+
handleError(err);
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
}, [handleError]);
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 加载成就列表
|
|
477
|
+
*/
|
|
478
|
+
const loadAchievements = useCallback(async () => {
|
|
479
|
+
try {
|
|
480
|
+
const result = await levelApi.getAchievements();
|
|
481
|
+
const items = result.items || result.data || result || [];
|
|
482
|
+
setAchievements(items);
|
|
483
|
+
return items;
|
|
484
|
+
} catch (err) {
|
|
485
|
+
handleError(err);
|
|
486
|
+
throw err;
|
|
487
|
+
}
|
|
488
|
+
}, [handleError]);
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 加载勋章列表
|
|
492
|
+
*/
|
|
493
|
+
const loadBadges = useCallback(async () => {
|
|
494
|
+
try {
|
|
495
|
+
const result = await levelApi.getBadges();
|
|
496
|
+
const items = result.items || result.data || result || [];
|
|
497
|
+
setBadges(items);
|
|
498
|
+
return items;
|
|
499
|
+
} catch (err) {
|
|
500
|
+
handleError(err);
|
|
501
|
+
throw err;
|
|
502
|
+
}
|
|
503
|
+
}, [handleError]);
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 初始化加载所有数据
|
|
507
|
+
*/
|
|
508
|
+
const initialize = useCallback(async () => {
|
|
509
|
+
setLoading(true);
|
|
510
|
+
try {
|
|
511
|
+
await Promise.all([loadUserLevel(), loadLevelConfigs(), loadPrivileges()]);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
// 错误已在各自的 load 函数中处理
|
|
514
|
+
} finally {
|
|
515
|
+
setLoading(false);
|
|
516
|
+
}
|
|
517
|
+
}, [loadUserLevel, loadLevelConfigs, loadPrivileges]);
|
|
518
|
+
|
|
519
|
+
// ============ 辅助方法 ============
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 获取等级配置
|
|
523
|
+
*/
|
|
524
|
+
const getLevelConfig = useCallback(level => {
|
|
525
|
+
return levelConfigs.find(c => c.level === level);
|
|
526
|
+
}, [levelConfigs]);
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 检查权益是否已解锁
|
|
530
|
+
*/
|
|
531
|
+
const isPrivilegeUnlocked = useCallback(privilegeId => {
|
|
532
|
+
if (!userLevel) return false;
|
|
533
|
+
const privilege = privileges.find(p => p.id === privilegeId);
|
|
534
|
+
return privilege && userLevel.level >= privilege.unlock_level;
|
|
535
|
+
}, [userLevel, privileges]);
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 获取已解锁的权益
|
|
539
|
+
*/
|
|
540
|
+
const getUnlockedPrivileges = useCallback(() => {
|
|
541
|
+
if (!userLevel) return [];
|
|
542
|
+
return privileges.filter(p => userLevel.level >= p.unlock_level);
|
|
543
|
+
}, [userLevel, privileges]);
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* 获取下一等级的权益
|
|
547
|
+
*/
|
|
548
|
+
const getNextLevelPrivileges = useCallback(() => {
|
|
549
|
+
if (!userLevel) return [];
|
|
550
|
+
return privileges.filter(p => p.unlock_level === userLevel.level + 1);
|
|
551
|
+
}, [userLevel, privileges]);
|
|
552
|
+
const value = {
|
|
553
|
+
// 状态
|
|
554
|
+
userLevel,
|
|
555
|
+
levelConfigs,
|
|
556
|
+
privileges,
|
|
557
|
+
achievements,
|
|
558
|
+
badges,
|
|
559
|
+
loading,
|
|
560
|
+
error,
|
|
561
|
+
// 加载方法
|
|
562
|
+
loadUserLevel,
|
|
563
|
+
loadLevelConfigs,
|
|
564
|
+
loadPrivileges,
|
|
565
|
+
loadAchievements,
|
|
566
|
+
loadBadges,
|
|
567
|
+
initialize,
|
|
568
|
+
// 辅助方法
|
|
569
|
+
getLevelConfig,
|
|
570
|
+
isPrivilegeUnlocked,
|
|
571
|
+
getUnlockedPrivileges,
|
|
572
|
+
getNextLevelPrivileges,
|
|
573
|
+
// API 实例
|
|
574
|
+
api: levelApi
|
|
575
|
+
};
|
|
576
|
+
return /*#__PURE__*/React.createElement(LevelContext.Provider, {
|
|
577
|
+
value: value
|
|
578
|
+
}, children);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* 使用 Level Context
|
|
583
|
+
*/
|
|
584
|
+
function useLevel() {
|
|
585
|
+
const context = useContext(LevelContext);
|
|
586
|
+
if (!context) {
|
|
587
|
+
throw new Error('useLevel must be used within a LevelProvider');
|
|
588
|
+
}
|
|
589
|
+
return context;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Level SDK - useExpHistory Hook
|
|
594
|
+
* 经验历史记录 Hook
|
|
595
|
+
*/
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 经验历史 Hook
|
|
600
|
+
*/
|
|
601
|
+
function useExpHistory() {
|
|
602
|
+
const {
|
|
603
|
+
api
|
|
604
|
+
} = useLevel();
|
|
605
|
+
const [history, setHistory] = useState([]);
|
|
606
|
+
const [loading, setLoading] = useState(false);
|
|
607
|
+
const [error, setError] = useState(null);
|
|
608
|
+
const [pagination, setPagination] = useState({
|
|
609
|
+
page: 1,
|
|
610
|
+
pageSize: 20,
|
|
611
|
+
total: 0,
|
|
612
|
+
totalPages: 0
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 加载经验历史
|
|
617
|
+
*/
|
|
618
|
+
const load = useCallback(async (params = {}) => {
|
|
619
|
+
setLoading(true);
|
|
620
|
+
setError(null);
|
|
621
|
+
try {
|
|
622
|
+
const result = await api.getExpHistory({
|
|
623
|
+
page: pagination.page,
|
|
624
|
+
page_size: pagination.pageSize,
|
|
625
|
+
...params
|
|
626
|
+
});
|
|
627
|
+
const items = result.items || result.data || [];
|
|
628
|
+
setHistory(items);
|
|
629
|
+
setPagination(prev => ({
|
|
630
|
+
...prev,
|
|
631
|
+
total: result.total || items.length,
|
|
632
|
+
totalPages: result.total_pages || Math.ceil((result.total || items.length) / prev.pageSize)
|
|
633
|
+
}));
|
|
634
|
+
return result;
|
|
635
|
+
} catch (err) {
|
|
636
|
+
setError(err.message);
|
|
637
|
+
throw err;
|
|
638
|
+
} finally {
|
|
639
|
+
setLoading(false);
|
|
640
|
+
}
|
|
641
|
+
}, [api, pagination.page, pagination.pageSize]);
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* 按来源筛选
|
|
645
|
+
*/
|
|
646
|
+
const filterBySource = useCallback(async source => {
|
|
647
|
+
return load({
|
|
648
|
+
source,
|
|
649
|
+
page: 1
|
|
650
|
+
});
|
|
651
|
+
}, [load]);
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* 按日期范围筛选
|
|
655
|
+
*/
|
|
656
|
+
const filterByDateRange = useCallback(async (startDate, endDate) => {
|
|
657
|
+
return load({
|
|
658
|
+
start_date: startDate,
|
|
659
|
+
end_date: endDate,
|
|
660
|
+
page: 1
|
|
661
|
+
});
|
|
662
|
+
}, [load]);
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* 设置页码
|
|
666
|
+
*/
|
|
667
|
+
const setPage = useCallback(page => {
|
|
668
|
+
setPagination(prev => ({
|
|
669
|
+
...prev,
|
|
670
|
+
page
|
|
671
|
+
}));
|
|
672
|
+
}, []);
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* 刷新
|
|
676
|
+
*/
|
|
677
|
+
const refresh = useCallback(() => {
|
|
678
|
+
return load();
|
|
679
|
+
}, [load]);
|
|
680
|
+
return {
|
|
681
|
+
history,
|
|
682
|
+
loading,
|
|
683
|
+
error,
|
|
684
|
+
pagination,
|
|
685
|
+
load,
|
|
686
|
+
filterBySource,
|
|
687
|
+
filterByDateRange,
|
|
688
|
+
setPage,
|
|
689
|
+
refresh
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Level SDK - useLeaderboard Hook
|
|
695
|
+
* 等级排行榜 Hook
|
|
696
|
+
*/
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* 排行榜 Hook
|
|
701
|
+
*/
|
|
702
|
+
function useLeaderboard(autoLoad = true) {
|
|
703
|
+
const {
|
|
704
|
+
api
|
|
705
|
+
} = useLevel();
|
|
706
|
+
const [leaderboard, setLeaderboard] = useState([]);
|
|
707
|
+
const [myRank, setMyRank] = useState(null);
|
|
708
|
+
const [loading, setLoading] = useState(false);
|
|
709
|
+
const [error, setError] = useState(null);
|
|
710
|
+
const [timeRange, setTimeRange] = useState('all'); // all, weekly, monthly
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* 加载排行榜
|
|
714
|
+
*/
|
|
715
|
+
const load = useCallback(async (params = {}) => {
|
|
716
|
+
setLoading(true);
|
|
717
|
+
setError(null);
|
|
718
|
+
try {
|
|
719
|
+
const [boardResult, rankResult] = await Promise.all([api.getLeaderboard({
|
|
720
|
+
time_range: timeRange,
|
|
721
|
+
limit: 100,
|
|
722
|
+
...params
|
|
723
|
+
}), api.getMyRank()]);
|
|
724
|
+
const items = boardResult.items || boardResult.data || boardResult || [];
|
|
725
|
+
setLeaderboard(items);
|
|
726
|
+
setMyRank(rankResult);
|
|
727
|
+
return {
|
|
728
|
+
leaderboard: items,
|
|
729
|
+
myRank: rankResult
|
|
730
|
+
};
|
|
731
|
+
} catch (err) {
|
|
732
|
+
setError(err.message);
|
|
733
|
+
throw err;
|
|
734
|
+
} finally {
|
|
735
|
+
setLoading(false);
|
|
736
|
+
}
|
|
737
|
+
}, [api, timeRange]);
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 切换时间范围
|
|
741
|
+
*/
|
|
742
|
+
const changeTimeRange = useCallback(range => {
|
|
743
|
+
setTimeRange(range);
|
|
744
|
+
}, []);
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* 刷新
|
|
748
|
+
*/
|
|
749
|
+
const refresh = useCallback(() => {
|
|
750
|
+
return load();
|
|
751
|
+
}, [load]);
|
|
752
|
+
|
|
753
|
+
// 自动加载
|
|
754
|
+
useEffect(() => {
|
|
755
|
+
if (autoLoad) {
|
|
756
|
+
load();
|
|
757
|
+
}
|
|
758
|
+
}, [autoLoad, timeRange]);
|
|
759
|
+
return {
|
|
760
|
+
leaderboard,
|
|
761
|
+
myRank,
|
|
762
|
+
loading,
|
|
763
|
+
error,
|
|
764
|
+
timeRange,
|
|
765
|
+
load,
|
|
766
|
+
changeTimeRange,
|
|
767
|
+
refresh
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Level SDK - LevelBadge 组件
|
|
773
|
+
* 等级徽章展示
|
|
774
|
+
*/
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* 等级徽章组件
|
|
779
|
+
*/
|
|
780
|
+
function LevelBadge({
|
|
781
|
+
level,
|
|
782
|
+
name,
|
|
783
|
+
icon,
|
|
784
|
+
color = '#6366f1',
|
|
785
|
+
size = 'medium',
|
|
786
|
+
// small, medium, large
|
|
787
|
+
showName = true,
|
|
788
|
+
animate = false,
|
|
789
|
+
className = ''
|
|
790
|
+
}) {
|
|
791
|
+
const sizeClasses = {
|
|
792
|
+
small: 'level-badge-sm',
|
|
793
|
+
medium: 'level-badge-md',
|
|
794
|
+
large: 'level-badge-lg'
|
|
795
|
+
};
|
|
796
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
797
|
+
className: `level-badge ${sizeClasses[size]} ${animate ? 'level-badge-animate' : ''} ${className}`,
|
|
798
|
+
style: {
|
|
799
|
+
'--level-color': color
|
|
800
|
+
}
|
|
801
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
802
|
+
className: "level-badge-icon"
|
|
803
|
+
}, icon ? /*#__PURE__*/React.createElement("img", {
|
|
804
|
+
src: icon,
|
|
805
|
+
alt: `Level ${level}`
|
|
806
|
+
}) : /*#__PURE__*/React.createElement("span", {
|
|
807
|
+
className: "level-badge-number"
|
|
808
|
+
}, "Lv.", level)), showName && name && /*#__PURE__*/React.createElement("span", {
|
|
809
|
+
className: "level-badge-name"
|
|
810
|
+
}, name));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Level SDK - 类型定义
|
|
815
|
+
* 等级体系的核心类型
|
|
816
|
+
*/
|
|
817
|
+
|
|
818
|
+
// 经验来源类型
|
|
819
|
+
const ExpSource = {
|
|
820
|
+
TASK: 'task',
|
|
821
|
+
// 完成任务
|
|
822
|
+
LOGIN: 'login',
|
|
823
|
+
// 每日登录
|
|
824
|
+
CONTENT: 'content',
|
|
825
|
+
// 发布内容
|
|
826
|
+
INTERACTION: 'interaction',
|
|
827
|
+
// 互动奖励
|
|
828
|
+
INVITE: 'invite',
|
|
829
|
+
// 邀请奖励
|
|
830
|
+
PURCHASE: 'purchase',
|
|
831
|
+
// 消费奖励
|
|
832
|
+
SYSTEM: 'system',
|
|
833
|
+
// 系统奖励
|
|
834
|
+
ADMIN: 'admin' // 管理员调整
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// 权益类型
|
|
838
|
+
const PrivilegeType = {
|
|
839
|
+
FEATURE: 'feature',
|
|
840
|
+
// 功能解锁
|
|
841
|
+
DISCOUNT: 'discount',
|
|
842
|
+
// 折扣优惠
|
|
843
|
+
BONUS: 'bonus',
|
|
844
|
+
// 额外奖励
|
|
845
|
+
BADGE: 'badge',
|
|
846
|
+
// 专属徽章
|
|
847
|
+
LIMIT_INCREASE: 'limit',
|
|
848
|
+
// 额度提升
|
|
849
|
+
PRIORITY: 'priority',
|
|
850
|
+
// 优先权
|
|
851
|
+
EXCLUSIVE: 'exclusive' // 专属特权
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// 成就类型
|
|
855
|
+
const AchievementType = {
|
|
856
|
+
LEVEL: 'level',
|
|
857
|
+
// 等级成就
|
|
858
|
+
EXP: 'exp',
|
|
859
|
+
// 经验成就
|
|
860
|
+
TASK: 'task',
|
|
861
|
+
// 任务成就
|
|
862
|
+
SOCIAL: 'social',
|
|
863
|
+
// 社交成就
|
|
864
|
+
CONTENT: 'content',
|
|
865
|
+
// 内容成就
|
|
866
|
+
TRADING: 'trading',
|
|
867
|
+
// 交易成就
|
|
868
|
+
SPECIAL: 'special' // 特殊成就
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* 创建默认等级配置
|
|
873
|
+
* @param {Object} overrides - 覆盖字段
|
|
874
|
+
* @returns {Object}
|
|
875
|
+
*/
|
|
876
|
+
function createDefaultLevelConfig(overrides = {}) {
|
|
877
|
+
return {
|
|
878
|
+
id: null,
|
|
879
|
+
level: 1,
|
|
880
|
+
name: '',
|
|
881
|
+
icon: null,
|
|
882
|
+
color: '#6366f1',
|
|
883
|
+
min_exp: 0,
|
|
884
|
+
max_exp: 100,
|
|
885
|
+
privileges: [],
|
|
886
|
+
badge_url: null,
|
|
887
|
+
description: '',
|
|
888
|
+
...overrides
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* 创建默认用户等级信息
|
|
894
|
+
* @param {Object} overrides - 覆盖字段
|
|
895
|
+
* @returns {Object}
|
|
896
|
+
*/
|
|
897
|
+
function createDefaultUserLevel(overrides = {}) {
|
|
898
|
+
return {
|
|
899
|
+
user_did: null,
|
|
900
|
+
level: 1,
|
|
901
|
+
level_name: '',
|
|
902
|
+
current_exp: 0,
|
|
903
|
+
total_exp: 0,
|
|
904
|
+
exp_to_next: 100,
|
|
905
|
+
progress: 0,
|
|
906
|
+
privileges: [],
|
|
907
|
+
badges: [],
|
|
908
|
+
created_at: null,
|
|
909
|
+
updated_at: null,
|
|
910
|
+
...overrides
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* 创建默认经验记录
|
|
916
|
+
* @param {Object} overrides - 覆盖字段
|
|
917
|
+
* @returns {Object}
|
|
918
|
+
*/
|
|
919
|
+
function createDefaultExpRecord(overrides = {}) {
|
|
920
|
+
return {
|
|
921
|
+
id: null,
|
|
922
|
+
user_did: null,
|
|
923
|
+
amount: 0,
|
|
924
|
+
source: ExpSource.SYSTEM,
|
|
925
|
+
source_id: null,
|
|
926
|
+
description: '',
|
|
927
|
+
balance_after: 0,
|
|
928
|
+
created_at: null,
|
|
929
|
+
...overrides
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* 创建默认权益
|
|
935
|
+
* @param {Object} overrides - 覆盖字段
|
|
936
|
+
* @returns {Object}
|
|
937
|
+
*/
|
|
938
|
+
function createDefaultPrivilege(overrides = {}) {
|
|
939
|
+
return {
|
|
940
|
+
id: null,
|
|
941
|
+
name: '',
|
|
942
|
+
type: PrivilegeType.FEATURE,
|
|
943
|
+
description: '',
|
|
944
|
+
icon: null,
|
|
945
|
+
unlock_level: 1,
|
|
946
|
+
value: null,
|
|
947
|
+
is_active: true,
|
|
948
|
+
...overrides
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* 创建默认成就
|
|
954
|
+
* @param {Object} overrides - 覆盖字段
|
|
955
|
+
* @returns {Object}
|
|
956
|
+
*/
|
|
957
|
+
function createDefaultAchievement(overrides = {}) {
|
|
958
|
+
return {
|
|
959
|
+
id: null,
|
|
960
|
+
name: '',
|
|
961
|
+
type: AchievementType.LEVEL,
|
|
962
|
+
description: '',
|
|
963
|
+
icon: null,
|
|
964
|
+
condition: {},
|
|
965
|
+
reward_exp: 0,
|
|
966
|
+
reward_badge: null,
|
|
967
|
+
is_hidden: false,
|
|
968
|
+
...overrides
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* 计算升级所需经验
|
|
974
|
+
* @param {number} level - 目标等级
|
|
975
|
+
* @param {string} formula - 公式类型 (linear, quadratic, exponential)
|
|
976
|
+
* @param {Object} params - 公式参数
|
|
977
|
+
* @returns {number}
|
|
978
|
+
*/
|
|
979
|
+
function calculateExpForLevel(level, formula = 'quadratic', params = {}) {
|
|
980
|
+
const {
|
|
981
|
+
base = 100,
|
|
982
|
+
factor = 1.5
|
|
983
|
+
} = params;
|
|
984
|
+
switch (formula) {
|
|
985
|
+
case 'linear':
|
|
986
|
+
return base * level;
|
|
987
|
+
case 'quadratic':
|
|
988
|
+
return Math.floor(base * Math.pow(level, factor));
|
|
989
|
+
case 'exponential':
|
|
990
|
+
return Math.floor(base * Math.pow(factor, level - 1));
|
|
991
|
+
default:
|
|
992
|
+
return base * level;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* 计算进度百分比
|
|
998
|
+
* @param {number} current - 当前经验
|
|
999
|
+
* @param {number} min - 当前等级最低经验
|
|
1000
|
+
* @param {number} max - 下一等级所需经验
|
|
1001
|
+
* @returns {number} 0-100 的百分比
|
|
1002
|
+
*/
|
|
1003
|
+
function calculateProgress(current, min, max) {
|
|
1004
|
+
if (max <= min) return 100;
|
|
1005
|
+
const progress = (current - min) / (max - min) * 100;
|
|
1006
|
+
return Math.min(100, Math.max(0, progress));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Level SDK - LevelProgress 组件
|
|
1011
|
+
* 等级进度条
|
|
1012
|
+
*/
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* 等级进度组件
|
|
1017
|
+
*/
|
|
1018
|
+
function LevelProgress({
|
|
1019
|
+
currentLevel,
|
|
1020
|
+
currentExp,
|
|
1021
|
+
expToNext,
|
|
1022
|
+
totalExp,
|
|
1023
|
+
levelName,
|
|
1024
|
+
nextLevelName,
|
|
1025
|
+
color = '#6366f1',
|
|
1026
|
+
showStats = true,
|
|
1027
|
+
showLabels = true,
|
|
1028
|
+
size = 'medium',
|
|
1029
|
+
// small, medium, large
|
|
1030
|
+
animated = true,
|
|
1031
|
+
className = ''
|
|
1032
|
+
}) {
|
|
1033
|
+
const progress = calculateProgress(currentExp, 0, expToNext);
|
|
1034
|
+
const sizeClasses = {
|
|
1035
|
+
small: 'level-progress-sm',
|
|
1036
|
+
medium: 'level-progress-md',
|
|
1037
|
+
large: 'level-progress-lg'
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// 格式化数字
|
|
1041
|
+
const formatNumber = num => {
|
|
1042
|
+
if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
|
|
1043
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
|
|
1044
|
+
return num?.toString() || '0';
|
|
1045
|
+
};
|
|
1046
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1047
|
+
className: `level-progress ${sizeClasses[size]} ${className}`,
|
|
1048
|
+
style: {
|
|
1049
|
+
'--level-color': color
|
|
1050
|
+
}
|
|
1051
|
+
}, showLabels && /*#__PURE__*/React.createElement("div", {
|
|
1052
|
+
className: "level-progress-header"
|
|
1053
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1054
|
+
className: "level-progress-current"
|
|
1055
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1056
|
+
className: "level-number"
|
|
1057
|
+
}, "Lv.", currentLevel), levelName && /*#__PURE__*/React.createElement("span", {
|
|
1058
|
+
className: "level-name"
|
|
1059
|
+
}, levelName)), /*#__PURE__*/React.createElement("div", {
|
|
1060
|
+
className: "level-progress-next"
|
|
1061
|
+
}, nextLevelName ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
|
|
1062
|
+
className: "level-name"
|
|
1063
|
+
}, nextLevelName), /*#__PURE__*/React.createElement("span", {
|
|
1064
|
+
className: "level-number"
|
|
1065
|
+
}, "Lv.", currentLevel + 1)) : /*#__PURE__*/React.createElement("span", {
|
|
1066
|
+
className: "level-max"
|
|
1067
|
+
}, t('maxLevel')))), /*#__PURE__*/React.createElement("div", {
|
|
1068
|
+
className: "level-progress-bar"
|
|
1069
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1070
|
+
className: `level-progress-fill ${animated ? 'animated' : ''}`,
|
|
1071
|
+
style: {
|
|
1072
|
+
width: `${progress}%`
|
|
1073
|
+
}
|
|
1074
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
1075
|
+
className: "level-progress-text"
|
|
1076
|
+
}, progress.toFixed(1), "%")), showStats && /*#__PURE__*/React.createElement("div", {
|
|
1077
|
+
className: "level-progress-stats"
|
|
1078
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1079
|
+
className: "level-stat"
|
|
1080
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1081
|
+
className: "level-stat-label"
|
|
1082
|
+
}, t('currentLevel')), /*#__PURE__*/React.createElement("span", {
|
|
1083
|
+
className: "level-stat-value"
|
|
1084
|
+
}, formatNumber(currentExp))), /*#__PURE__*/React.createElement("div", {
|
|
1085
|
+
className: "level-stat"
|
|
1086
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1087
|
+
className: "level-stat-label"
|
|
1088
|
+
}, t('expToNext')), /*#__PURE__*/React.createElement("span", {
|
|
1089
|
+
className: "level-stat-value"
|
|
1090
|
+
}, formatNumber(expToNext - currentExp))), /*#__PURE__*/React.createElement("div", {
|
|
1091
|
+
className: "level-stat"
|
|
1092
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1093
|
+
className: "level-stat-label"
|
|
1094
|
+
}, t('totalExp')), /*#__PURE__*/React.createElement("span", {
|
|
1095
|
+
className: "level-stat-value"
|
|
1096
|
+
}, formatNumber(totalExp)))));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Level SDK - LevelCard 组件
|
|
1101
|
+
* 等级信息卡片
|
|
1102
|
+
*/
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* 等级卡片组件
|
|
1107
|
+
*/
|
|
1108
|
+
function LevelCard({
|
|
1109
|
+
userLevel,
|
|
1110
|
+
levelConfig,
|
|
1111
|
+
nextLevelConfig,
|
|
1112
|
+
showPrivileges = true,
|
|
1113
|
+
showProgress = true,
|
|
1114
|
+
onViewPrivileges,
|
|
1115
|
+
onViewHistory,
|
|
1116
|
+
className = ''
|
|
1117
|
+
}) {
|
|
1118
|
+
if (!userLevel) {
|
|
1119
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1120
|
+
className: `level-card level-card-empty ${className}`
|
|
1121
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1122
|
+
className: "level-card-loading"
|
|
1123
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1124
|
+
className: "level-loading-spinner"
|
|
1125
|
+
}), /*#__PURE__*/React.createElement("span", null, t('loading'))));
|
|
1126
|
+
}
|
|
1127
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1128
|
+
className: `level-card ${className}`
|
|
1129
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1130
|
+
className: "level-card-header"
|
|
1131
|
+
}, /*#__PURE__*/React.createElement(LevelBadge, {
|
|
1132
|
+
level: userLevel.level,
|
|
1133
|
+
name: userLevel.level_name || levelConfig?.name,
|
|
1134
|
+
icon: levelConfig?.icon,
|
|
1135
|
+
color: levelConfig?.color,
|
|
1136
|
+
size: "large"
|
|
1137
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
1138
|
+
className: "level-card-title"
|
|
1139
|
+
}, /*#__PURE__*/React.createElement("h3", null, userLevel.level_name || `${t('level')} ${userLevel.level}`), /*#__PURE__*/React.createElement("p", null, levelConfig?.description))), showProgress && /*#__PURE__*/React.createElement("div", {
|
|
1140
|
+
className: "level-card-progress"
|
|
1141
|
+
}, /*#__PURE__*/React.createElement(LevelProgress, {
|
|
1142
|
+
currentLevel: userLevel.level,
|
|
1143
|
+
currentExp: userLevel.current_exp,
|
|
1144
|
+
expToNext: userLevel.exp_to_next,
|
|
1145
|
+
totalExp: userLevel.total_exp,
|
|
1146
|
+
levelName: userLevel.level_name,
|
|
1147
|
+
nextLevelName: nextLevelConfig?.name,
|
|
1148
|
+
color: levelConfig?.color,
|
|
1149
|
+
size: "large"
|
|
1150
|
+
})), showPrivileges && userLevel.privileges?.length > 0 && /*#__PURE__*/React.createElement("div", {
|
|
1151
|
+
className: "level-card-privileges"
|
|
1152
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1153
|
+
className: "level-privileges-header"
|
|
1154
|
+
}, /*#__PURE__*/React.createElement("h4", null, t('privileges')), onViewPrivileges && /*#__PURE__*/React.createElement("button", {
|
|
1155
|
+
className: "level-link-btn",
|
|
1156
|
+
onClick: onViewPrivileges
|
|
1157
|
+
}, t('more'), " \u2192")), /*#__PURE__*/React.createElement("div", {
|
|
1158
|
+
className: "level-privileges-list"
|
|
1159
|
+
}, userLevel.privileges.slice(0, 4).map((privilege, index) => /*#__PURE__*/React.createElement("div", {
|
|
1160
|
+
key: privilege.id || index,
|
|
1161
|
+
className: "level-privilege-item"
|
|
1162
|
+
}, privilege.icon && /*#__PURE__*/React.createElement("span", {
|
|
1163
|
+
className: "level-privilege-icon"
|
|
1164
|
+
}, privilege.icon), /*#__PURE__*/React.createElement("span", {
|
|
1165
|
+
className: "level-privilege-name"
|
|
1166
|
+
}, privilege.name))))), /*#__PURE__*/React.createElement("div", {
|
|
1167
|
+
className: "level-card-actions"
|
|
1168
|
+
}, onViewHistory && /*#__PURE__*/React.createElement("button", {
|
|
1169
|
+
className: "level-btn level-btn-outline",
|
|
1170
|
+
onClick: onViewHistory
|
|
1171
|
+
}, t('expHistory')), onViewPrivileges && /*#__PURE__*/React.createElement("button", {
|
|
1172
|
+
className: "level-btn level-btn-primary",
|
|
1173
|
+
onClick: onViewPrivileges
|
|
1174
|
+
}, t('privileges'))));
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Level SDK - Leaderboard 组件
|
|
1179
|
+
* 等级排行榜
|
|
1180
|
+
*/
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* 排行榜组件
|
|
1185
|
+
*/
|
|
1186
|
+
function Leaderboard({
|
|
1187
|
+
items = [],
|
|
1188
|
+
myRank = null,
|
|
1189
|
+
timeRange = 'all',
|
|
1190
|
+
onTimeRangeChange,
|
|
1191
|
+
loading = false,
|
|
1192
|
+
showTop = 100,
|
|
1193
|
+
className = ''
|
|
1194
|
+
}) {
|
|
1195
|
+
const [activeRange, setActiveRange] = useState(timeRange);
|
|
1196
|
+
const handleRangeChange = range => {
|
|
1197
|
+
setActiveRange(range);
|
|
1198
|
+
onTimeRangeChange?.(range);
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// 格式化排名
|
|
1202
|
+
const formatRank = rank => {
|
|
1203
|
+
if (rank === 1) return '🥇';
|
|
1204
|
+
if (rank === 2) return '🥈';
|
|
1205
|
+
if (rank === 3) return '🥉';
|
|
1206
|
+
return `#${rank}`;
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
// 格式化数字
|
|
1210
|
+
const formatNumber = num => {
|
|
1211
|
+
if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
|
|
1212
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
|
|
1213
|
+
return num?.toString() || '0';
|
|
1214
|
+
};
|
|
1215
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1216
|
+
className: `level-leaderboard ${className}`
|
|
1217
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1218
|
+
className: "level-leaderboard-header"
|
|
1219
|
+
}, /*#__PURE__*/React.createElement("h3", null, t('leaderboard')), /*#__PURE__*/React.createElement("div", {
|
|
1220
|
+
className: "level-time-tabs"
|
|
1221
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
1222
|
+
className: activeRange === 'all' ? 'active' : '',
|
|
1223
|
+
onClick: () => handleRangeChange('all')
|
|
1224
|
+
}, "\u5168\u90E8"), /*#__PURE__*/React.createElement("button", {
|
|
1225
|
+
className: activeRange === 'monthly' ? 'active' : '',
|
|
1226
|
+
onClick: () => handleRangeChange('monthly')
|
|
1227
|
+
}, "\u672C\u6708"), /*#__PURE__*/React.createElement("button", {
|
|
1228
|
+
className: activeRange === 'weekly' ? 'active' : '',
|
|
1229
|
+
onClick: () => handleRangeChange('weekly')
|
|
1230
|
+
}, "\u672C\u5468"))), myRank && /*#__PURE__*/React.createElement("div", {
|
|
1231
|
+
className: "level-my-rank"
|
|
1232
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1233
|
+
className: "level-my-rank-label"
|
|
1234
|
+
}, t('myRank')), /*#__PURE__*/React.createElement("span", {
|
|
1235
|
+
className: "level-my-rank-value"
|
|
1236
|
+
}, formatRank(myRank.rank)), /*#__PURE__*/React.createElement("span", {
|
|
1237
|
+
className: "level-my-rank-info"
|
|
1238
|
+
}, "Lv.", myRank.level, " \xB7 ", formatNumber(myRank.total_exp), " EXP")), loading ? /*#__PURE__*/React.createElement("div", {
|
|
1239
|
+
className: "level-leaderboard-loading"
|
|
1240
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1241
|
+
className: "level-loading-spinner"
|
|
1242
|
+
}), /*#__PURE__*/React.createElement("span", null, t('loading'))) : items.length === 0 ? /*#__PURE__*/React.createElement("div", {
|
|
1243
|
+
className: "level-leaderboard-empty"
|
|
1244
|
+
}, /*#__PURE__*/React.createElement("span", null, "\u6682\u65E0\u6570\u636E")) : /*#__PURE__*/React.createElement("div", {
|
|
1245
|
+
className: "level-leaderboard-list"
|
|
1246
|
+
}, items.slice(0, showTop).map((item, index) => /*#__PURE__*/React.createElement("div", {
|
|
1247
|
+
key: item.user_did || index,
|
|
1248
|
+
className: `level-leaderboard-item ${index < 3 ? 'top-three' : ''}`
|
|
1249
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1250
|
+
className: "level-rank"
|
|
1251
|
+
}, formatRank(index + 1)), /*#__PURE__*/React.createElement("div", {
|
|
1252
|
+
className: "level-user-info"
|
|
1253
|
+
}, item.avatar ? /*#__PURE__*/React.createElement("img", {
|
|
1254
|
+
src: item.avatar,
|
|
1255
|
+
alt: "",
|
|
1256
|
+
className: "level-user-avatar"
|
|
1257
|
+
}) : /*#__PURE__*/React.createElement("div", {
|
|
1258
|
+
className: "level-user-avatar-placeholder"
|
|
1259
|
+
}, item.name?.charAt(0) || '?'), /*#__PURE__*/React.createElement("span", {
|
|
1260
|
+
className: "level-user-name"
|
|
1261
|
+
}, item.name || 'Anonymous')), /*#__PURE__*/React.createElement(LevelBadge, {
|
|
1262
|
+
level: item.level,
|
|
1263
|
+
color: item.level_color,
|
|
1264
|
+
size: "small",
|
|
1265
|
+
showName: false
|
|
1266
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
1267
|
+
className: "level-exp"
|
|
1268
|
+
}, formatNumber(item.total_exp), " EXP")))));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Level SDK - PrivilegeList 组件
|
|
1273
|
+
* 权益列表展示
|
|
1274
|
+
*/
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* 权益列表组件
|
|
1279
|
+
*/
|
|
1280
|
+
function PrivilegeList({
|
|
1281
|
+
privileges = [],
|
|
1282
|
+
userLevel = 0,
|
|
1283
|
+
showLocked = true,
|
|
1284
|
+
showUnlockLevel = true,
|
|
1285
|
+
onPrivilegeClick,
|
|
1286
|
+
className = ''
|
|
1287
|
+
}) {
|
|
1288
|
+
// 分离已解锁和未解锁的权益
|
|
1289
|
+
const unlockedPrivileges = privileges.filter(p => userLevel >= p.unlock_level);
|
|
1290
|
+
const lockedPrivileges = privileges.filter(p => userLevel < p.unlock_level);
|
|
1291
|
+
const renderPrivilege = (privilege, isUnlocked) => /*#__PURE__*/React.createElement("div", {
|
|
1292
|
+
key: privilege.id,
|
|
1293
|
+
className: `level-privilege ${isUnlocked ? 'unlocked' : 'locked'}`,
|
|
1294
|
+
onClick: () => onPrivilegeClick?.(privilege)
|
|
1295
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1296
|
+
className: "level-privilege-icon-wrapper"
|
|
1297
|
+
}, privilege.icon ? /*#__PURE__*/React.createElement("span", {
|
|
1298
|
+
className: "level-privilege-icon"
|
|
1299
|
+
}, privilege.icon) : /*#__PURE__*/React.createElement("span", {
|
|
1300
|
+
className: "level-privilege-icon-default"
|
|
1301
|
+
}, "\u2728"), !isUnlocked && /*#__PURE__*/React.createElement("span", {
|
|
1302
|
+
className: "level-privilege-lock"
|
|
1303
|
+
}, "\uD83D\uDD12")), /*#__PURE__*/React.createElement("div", {
|
|
1304
|
+
className: "level-privilege-content"
|
|
1305
|
+
}, /*#__PURE__*/React.createElement("h4", {
|
|
1306
|
+
className: "level-privilege-name"
|
|
1307
|
+
}, privilege.name), privilege.description && /*#__PURE__*/React.createElement("p", {
|
|
1308
|
+
className: "level-privilege-desc"
|
|
1309
|
+
}, privilege.description), showUnlockLevel && !isUnlocked && /*#__PURE__*/React.createElement("span", {
|
|
1310
|
+
className: "level-privilege-unlock"
|
|
1311
|
+
}, t('unlockAt', {
|
|
1312
|
+
level: privilege.unlock_level
|
|
1313
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
1314
|
+
className: "level-privilege-status"
|
|
1315
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1316
|
+
className: `level-status-tag ${isUnlocked ? 'tag-unlocked' : 'tag-locked'}`
|
|
1317
|
+
}, isUnlocked ? t('unlocked') : t('locked'))));
|
|
1318
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1319
|
+
className: `level-privilege-list ${className}`
|
|
1320
|
+
}, unlockedPrivileges.length > 0 && /*#__PURE__*/React.createElement("div", {
|
|
1321
|
+
className: "level-privilege-section"
|
|
1322
|
+
}, /*#__PURE__*/React.createElement("h3", {
|
|
1323
|
+
className: "level-privilege-section-title"
|
|
1324
|
+
}, t('unlocked'), " (", unlockedPrivileges.length, ")"), /*#__PURE__*/React.createElement("div", {
|
|
1325
|
+
className: "level-privilege-grid"
|
|
1326
|
+
}, unlockedPrivileges.map(p => renderPrivilege(p, true)))), showLocked && lockedPrivileges.length > 0 && /*#__PURE__*/React.createElement("div", {
|
|
1327
|
+
className: "level-privilege-section"
|
|
1328
|
+
}, /*#__PURE__*/React.createElement("h3", {
|
|
1329
|
+
className: "level-privilege-section-title"
|
|
1330
|
+
}, t('locked'), " (", lockedPrivileges.length, ")"), /*#__PURE__*/React.createElement("div", {
|
|
1331
|
+
className: "level-privilege-grid"
|
|
1332
|
+
}, lockedPrivileges.map(p => renderPrivilege(p, false)))), privileges.length === 0 && /*#__PURE__*/React.createElement("div", {
|
|
1333
|
+
className: "level-privilege-empty"
|
|
1334
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1335
|
+
className: "level-empty-icon"
|
|
1336
|
+
}, "\uD83C\uDF81"), /*#__PURE__*/React.createElement("span", null, "\u6682\u65E0\u6743\u76CA")));
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
export { AchievementType, ExpSource, Leaderboard, LevelApiClient, LevelBadge, LevelCard, LevelProgress, LevelProvider, PrivilegeList, PrivilegeType, SUPPORTED_LANGUAGES, calculateExpForLevel, calculateProgress, createDefaultAchievement, createDefaultExpRecord, createDefaultLevelConfig, createDefaultPrivilege, createDefaultUserLevel, getLanguage, levelApi, messages, setLanguage, t, useExpHistory, useLeaderboard, useLevel };
|
|
1340
|
+
//# sourceMappingURL=index.esm.js.map
|