feat: 支持登录、个人信息存储
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { _decorator, Component, ProgressBar, Label } from 'cc';
|
||||
import { ViewManager } from './scripts/core/ViewManager';
|
||||
import { LevelDataManager } from './scripts/utils/LevelDataManager';
|
||||
import { AuthManager } from './scripts/utils/AuthManager';
|
||||
import { StorageManager } from './scripts/utils/StorageManager';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 页面加载组件
|
||||
* 负责预加载资源并显示加载进度
|
||||
* 负责用户登录、预加载资源并显示加载进度
|
||||
* 登录与关卡数据加载并行执行以减少等待时间
|
||||
*/
|
||||
@ccclass('PageLoading')
|
||||
export class PageLoading extends Component {
|
||||
@@ -19,26 +22,40 @@ export class PageLoading extends Component {
|
||||
this._startPreload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始预加载
|
||||
*/
|
||||
private async _startPreload(): Promise<void> {
|
||||
// 初始化进度条
|
||||
if (this.progressBar) {
|
||||
this.progressBar.progress = 0;
|
||||
}
|
||||
|
||||
// 阶段1: 初始化 LevelDataManager (0-80%)
|
||||
const success = await LevelDataManager.instance.initialize((progress, message) => {
|
||||
this._updateProgress(progress);
|
||||
this._updateStatusLabel(message);
|
||||
});
|
||||
this._updateStatusLabel('正在加载...');
|
||||
|
||||
if (!success) {
|
||||
// 登录和关卡数据并行加载
|
||||
const [loginSuccess, levelSuccess] = await Promise.all([
|
||||
AuthManager.instance.initialize(),
|
||||
LevelDataManager.instance.initialize((progress, message) => {
|
||||
// 关卡加载占 0-80% 进度
|
||||
this._updateProgress(progress);
|
||||
this._updateStatusLabel(message);
|
||||
}),
|
||||
]);
|
||||
|
||||
if (loginSuccess) {
|
||||
console.log('[PageLoading] 用户登录成功');
|
||||
} else {
|
||||
console.warn('[PageLoading] 登录失败,继续离线模式');
|
||||
}
|
||||
|
||||
if (!levelSuccess) {
|
||||
this._updateStatusLabel('加载失败,请重新打开游戏');
|
||||
return;
|
||||
}
|
||||
|
||||
// 阶段2: 预加载 PageHome (80-100%)
|
||||
// 登录 + 关卡数据都就绪后,用服务端进度覆盖本地进度
|
||||
if (loginSuccess) {
|
||||
this._syncProgressFromServer();
|
||||
}
|
||||
|
||||
// 预加载 PageHome (80-100%)
|
||||
ViewManager.instance.preload('PageHome',
|
||||
(progress) => {
|
||||
this._updateProgress(0.8 + progress * 0.2);
|
||||
@@ -50,38 +67,52 @@ export class PageLoading extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度条
|
||||
*/
|
||||
private _updateProgress(progress: number): void {
|
||||
if (this.progressBar) {
|
||||
this.progressBar.progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态标签
|
||||
*/
|
||||
private _updateStatusLabel(message: string): void {
|
||||
if (this.statusLabel) {
|
||||
this.statusLabel.string = message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载完成回调
|
||||
*/
|
||||
private _onPreloadComplete(): void {
|
||||
// 确保进度条显示完成
|
||||
this._updateProgress(1);
|
||||
this._updateStatusLabel('加载完成');
|
||||
|
||||
// 打开 PageHome
|
||||
ViewManager.instance.open('PageHome', {
|
||||
onComplete: () => {
|
||||
// PageHome 打开成功后,销毁自身
|
||||
this.node.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用服务端通关进度覆盖本地进度
|
||||
* 将 completedLevelIds 转换为本地的 currentLevelIndex / maxUnlockedLevelIndex
|
||||
*/
|
||||
private _syncProgressFromServer(): void {
|
||||
const completedIds = AuthManager.instance.completedLevelIds;
|
||||
if (completedIds.length === 0) {
|
||||
console.log('[PageLoading] 服务端无通关记录,使用本地进度');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxCompletedIndex = LevelDataManager.instance.getMaxCompletedIndex(completedIds);
|
||||
if (maxCompletedIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localMax = StorageManager.getMaxUnlockedLevelIndex();
|
||||
|
||||
// 取服务端和本地的较大值,防止进度回退
|
||||
if (maxCompletedIndex > localMax) {
|
||||
// onLevelCompleted 会同时设置 currentLevelIndex = maxCompletedIndex + 1 和 maxUnlockedLevelIndex
|
||||
StorageManager.onLevelCompleted(maxCompletedIndex);
|
||||
console.log(`[PageLoading] 服务端进度同步:已通关到第 ${maxCompletedIndex + 1} 关`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, Sp
|
||||
import { BaseView } from 'db://assets/scripts/core/BaseView';
|
||||
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
|
||||
import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
|
||||
import { UserAssetsManager } from 'db://assets/scripts/utils/UserAssetsManager';
|
||||
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
|
||||
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
|
||||
import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes';
|
||||
@@ -58,6 +59,7 @@ export class PageLevel extends BaseView {
|
||||
@property(Label)
|
||||
clockLabel: Label | null = null;
|
||||
|
||||
/** 积分显示标签(prefab 中序列化名为 liveLabel,保持兼容) */
|
||||
@property(Label)
|
||||
liveLabel: Label | null = null;
|
||||
|
||||
@@ -96,6 +98,9 @@ export class PageLevel extends BaseView {
|
||||
/** 是否正在切换关卡(防止重复提交) */
|
||||
private _isTransitioning: boolean = false;
|
||||
|
||||
/** 是否正在解锁提示(防止双击重复消耗积分) */
|
||||
private _isUnlocking: boolean = false;
|
||||
|
||||
/** 通关弹窗实例 */
|
||||
private _passModalNode: Node | null = null;
|
||||
|
||||
@@ -107,7 +112,7 @@ export class PageLevel extends BaseView {
|
||||
// 从本地存储恢复关卡进度
|
||||
this.currentLevelIndex = StorageManager.getCurrentLevelIndex();
|
||||
console.log(`[PageLevel] 恢复关卡进度: 第 ${this.currentLevelIndex + 1} 关`);
|
||||
this.updateLiveLabel();
|
||||
this.updatePointsLabel();
|
||||
this.initIconSetting();
|
||||
this.initUnlockButtons();
|
||||
this.initSubmitButton();
|
||||
@@ -125,7 +130,7 @@ export class PageLevel extends BaseView {
|
||||
*/
|
||||
onViewShow(): void {
|
||||
console.log('[PageLevel] onViewShow');
|
||||
this.updateLiveLabel();
|
||||
this.updatePointsLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +148,12 @@ export class PageLevel extends BaseView {
|
||||
this.clearInputNodes();
|
||||
this.stopCountdown();
|
||||
this._closePassModal();
|
||||
|
||||
// 清理事件监听
|
||||
this.iconSetting?.off(Node.EventType.TOUCH_END, this.onIconSettingClick, this);
|
||||
this.unLockItem2?.off(Node.EventType.TOUCH_END);
|
||||
this.unLockItem3?.off(Node.EventType.TOUCH_END);
|
||||
this.submitButton?.off(Node.EventType.TOUCH_END, this.onSubmitAnswer, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,34 +471,39 @@ export class PageLevel extends BaseView {
|
||||
/**
|
||||
* 点击解锁线索
|
||||
*/
|
||||
private onUnlockClue(index: number): void {
|
||||
// 检查生命值是否足够
|
||||
if (!this.hasLives()) {
|
||||
console.warn('[PageLevel] 生命值不足,无法解锁线索');
|
||||
private async onUnlockClue(index: number): Promise<void> {
|
||||
// 防止双击重复消耗
|
||||
if (this._isUnlocking) return;
|
||||
|
||||
if (!this.hasPoints()) {
|
||||
ToastManager.show('积分不足,无法解锁提示!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 消耗一颗生命值
|
||||
if (!this.consumeLife()) {
|
||||
return;
|
||||
this._isUnlocking = true;
|
||||
|
||||
try {
|
||||
const levelId = this._currentConfig?.id;
|
||||
const success = await UserAssetsManager.instance.consumePoint(levelId, index);
|
||||
if (!success) {
|
||||
ToastManager.show('积分不足,无法解锁提示!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatePointsLabel();
|
||||
this.playClickSound();
|
||||
this.hideUnlockButton(index);
|
||||
this.showClue(index);
|
||||
|
||||
if (this._currentConfig) {
|
||||
const clueContent = index === 2 ? this._currentConfig.clue2 : this._currentConfig.clue3;
|
||||
this.setClue(index, clueContent);
|
||||
}
|
||||
|
||||
console.log(`[PageLevel] 解锁线索${index}`);
|
||||
} finally {
|
||||
this._isUnlocking = false;
|
||||
}
|
||||
|
||||
// 播放点击音效
|
||||
this.playClickSound();
|
||||
|
||||
// 隐藏解锁按钮
|
||||
this.hideUnlockButton(index);
|
||||
|
||||
// 显示线索
|
||||
this.showClue(index);
|
||||
|
||||
// 设置线索内容
|
||||
if (this._currentConfig) {
|
||||
const clueContent = index === 2 ? this._currentConfig.clue2 : this._currentConfig.clue3;
|
||||
this.setClue(index, clueContent);
|
||||
}
|
||||
|
||||
console.log(`[PageLevel] 解锁线索${index}`);
|
||||
}
|
||||
|
||||
// ========== 主图相关方法 ==========
|
||||
@@ -591,48 +607,17 @@ export class PageLevel extends BaseView {
|
||||
// 可以在这里添加游戏结束逻辑
|
||||
}
|
||||
|
||||
// ========== 生命值相关方法 ==========
|
||||
// ========== 积分相关方法 ==========
|
||||
|
||||
/**
|
||||
* 更新生命值显示
|
||||
*/
|
||||
private updateLiveLabel(): void {
|
||||
private updatePointsLabel(): void {
|
||||
if (this.liveLabel) {
|
||||
const lives = StorageManager.getLives();
|
||||
this.liveLabel.string = `x ${lives}`;
|
||||
console.log(`[PageLevel] 更新生命值显示: ${lives}`);
|
||||
const points = StorageManager.getPoints();
|
||||
this.liveLabel.string = `x ${points}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消耗一颗生命值(用于查看提示)
|
||||
* @returns 是否消耗成功
|
||||
*/
|
||||
private consumeLife(): boolean {
|
||||
const success = StorageManager.consumeLife();
|
||||
if (success) {
|
||||
this.updateLiveLabel();
|
||||
console.log('[PageLevel] 消耗一颗生命');
|
||||
} else {
|
||||
console.warn('[PageLevel] 生命值不足,无法消耗');
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加一颗生命值(用于通关奖励)
|
||||
*/
|
||||
private addLife(): void {
|
||||
StorageManager.addLife();
|
||||
this.updateLiveLabel();
|
||||
console.log('[PageLevel] 获得一颗生命');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有足够的生命值
|
||||
*/
|
||||
private hasLives(): boolean {
|
||||
return StorageManager.hasLives();
|
||||
private hasPoints(): boolean {
|
||||
return StorageManager.hasPoints();
|
||||
}
|
||||
|
||||
// ========== 答案提交与关卡切换 ==========
|
||||
@@ -659,7 +644,7 @@ export class PageLevel extends BaseView {
|
||||
/**
|
||||
* 显示成功提示
|
||||
*/
|
||||
private showSuccess(): void {
|
||||
private async showSuccess(): Promise<void> {
|
||||
console.log('[PageLevel] 答案正确!');
|
||||
|
||||
// 标记正在切换关卡,防止重复提交
|
||||
@@ -671,8 +656,10 @@ export class PageLevel extends BaseView {
|
||||
// 播放成功音效
|
||||
this.playSuccessSound();
|
||||
|
||||
// 通关奖励:增加一颗生命值
|
||||
this.addLife();
|
||||
// 通关奖励:通过服务端增加积分
|
||||
const levelId = this._currentConfig?.id ?? '';
|
||||
await UserAssetsManager.instance.earnPoint(levelId);
|
||||
this.updatePointsLabel();
|
||||
|
||||
// 显示通关弹窗
|
||||
this._showPassModal();
|
||||
|
||||
9
assets/scripts/config.meta
Normal file
9
assets/scripts/config.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "5ec36628-7826-482c-a679-eb20093b0edb",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
29
assets/scripts/config/ApiConfig.ts
Normal file
29
assets/scripts/config/ApiConfig.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* API 配置常量
|
||||
* 统一管理所有服务端 API 地址
|
||||
*/
|
||||
|
||||
/** 服务端 API 基础地址 */
|
||||
export const API_BASE = 'https://ilookai.cn/api/v1';
|
||||
|
||||
/** API 端点 */
|
||||
export const API_ENDPOINTS = {
|
||||
WX_LOGIN: `${API_BASE}/auth/wx-login`,
|
||||
USER_ASSETS: `${API_BASE}/user/assets`,
|
||||
USER_ASSETS_CONSUME: `${API_BASE}/user/assets/consume`,
|
||||
USER_ASSETS_EARN: `${API_BASE}/user/assets/earn`,
|
||||
USER_GAME_DATA: `${API_BASE}/user/game-data`,
|
||||
LEVELS: `${API_BASE}/wechat-game/levels`,
|
||||
} as const;
|
||||
|
||||
/** 积分操作原因 */
|
||||
export const POINT_REASONS = {
|
||||
HINT_UNLOCK: 'hint_unlock',
|
||||
LEVEL_COMPLETE: 'level_complete',
|
||||
} as const;
|
||||
|
||||
/** 请求超时时间(毫秒) */
|
||||
export const API_TIMEOUT = {
|
||||
DEFAULT: 8000,
|
||||
SHORT: 5000,
|
||||
} as const;
|
||||
9
assets/scripts/config/ApiConfig.ts.meta
Normal file
9
assets/scripts/config/ApiConfig.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "f84f3481-515f-4dd6-9664-566e459331bd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
31
assets/scripts/types/ApiTypes.ts
Normal file
31
assets/scripts/types/ApiTypes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 服务端 API 通用响应类型
|
||||
*/
|
||||
|
||||
/** 服务端标准响应封装 */
|
||||
export interface ApiEnvelope<T> {
|
||||
success: boolean;
|
||||
data: T | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
/** 登录响应数据 */
|
||||
export interface WxLoginData {
|
||||
token: string;
|
||||
user: {
|
||||
id: string;
|
||||
nickname: string | null;
|
||||
points: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 积分响应数据 */
|
||||
export interface UserAssetsData {
|
||||
points: number;
|
||||
}
|
||||
|
||||
/** 游戏数据响应(Loading 页面) */
|
||||
export interface GameData {
|
||||
user: { id: string; points: number };
|
||||
completedLevelIds: string[];
|
||||
}
|
||||
9
assets/scripts/types/ApiTypes.ts.meta
Normal file
9
assets/scripts/types/ApiTypes.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "13078ed9-43cf-4949-8a92-e702ee7de88a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
146
assets/scripts/utils/AuthManager.ts
Normal file
146
assets/scripts/utils/AuthManager.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { HttpUtil } from './HttpUtil';
|
||||
import { StorageManager } from './StorageManager';
|
||||
import { WxSDK } from './WxSDK';
|
||||
import { API_ENDPOINTS, API_TIMEOUT } from '../config/ApiConfig';
|
||||
import { ApiEnvelope, WxLoginData, GameData } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
* 单例模式,负责微信登录和 JWT token 管理
|
||||
*/
|
||||
export class AuthManager {
|
||||
private static _instance: AuthManager | null = null;
|
||||
|
||||
private _userId: string = '';
|
||||
private _isLoggedIn: boolean = false;
|
||||
/** 服务端返回的已完成关卡 ID(登录后暂存,等 LevelDataManager 就绪后同步) */
|
||||
private _completedLevelIds: string[] = [];
|
||||
/** 服务端返回的已完成关卡 ID(登录后暂存,等 LevelDataManager 就绪后同步) */
|
||||
private _completedLevelIds: string[] = [];
|
||||
|
||||
static get instance(): AuthManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new AuthManager();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
get isLoggedIn(): boolean {
|
||||
return this._isLoggedIn;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
return this._userId;
|
||||
}
|
||||
|
||||
get completedLevelIds(): string[] {
|
||||
return this._completedLevelIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化认证:尝试恢复 token 或执行微信登录
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
const savedToken = StorageManager.getToken();
|
||||
if (savedToken) {
|
||||
HttpUtil.setAuthToken(savedToken);
|
||||
try {
|
||||
const valid = await this.validateToken();
|
||||
if (valid) {
|
||||
console.log('[AuthManager] Token 恢复成功');
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
console.warn('[AuthManager] 本地 token 无效,重新登录');
|
||||
}
|
||||
}
|
||||
|
||||
return this.wxLogin();
|
||||
}
|
||||
|
||||
private async wxLogin(): Promise<boolean> {
|
||||
try {
|
||||
let code: string;
|
||||
|
||||
if (WxSDK.isWechat()) {
|
||||
code = await WxSDK.login();
|
||||
} else {
|
||||
console.warn('[AuthManager] 非微信环境,使用开发模式 mock code');
|
||||
code = 'dev_mock_code';
|
||||
}
|
||||
|
||||
const response = await HttpUtil.post<ApiEnvelope<WxLoginData>>(
|
||||
API_ENDPOINTS.WX_LOGIN,
|
||||
{ code },
|
||||
API_TIMEOUT.DEFAULT
|
||||
);
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
console.error('[AuthManager] 登录失败:', response.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { token, user } = response.data;
|
||||
HttpUtil.setAuthToken(token);
|
||||
StorageManager.setToken(token);
|
||||
|
||||
this._userId = user.id;
|
||||
this._isLoggedIn = true;
|
||||
StorageManager.setPoints(user.points);
|
||||
|
||||
// 获取通关进度
|
||||
await this.fetchCompletedLevels();
|
||||
|
||||
console.log(`[AuthManager] 登录成功,用户: ${user.id},积分: ${user.points}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[AuthManager] 登录异常:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async validateToken(): Promise<boolean> {
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<GameData>>(
|
||||
API_ENDPOINTS.USER_GAME_DATA,
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._userId = response.data.user.id;
|
||||
this._isLoggedIn = true;
|
||||
StorageManager.setPoints(response.data.user.points);
|
||||
this._completedLevelIds = response.data.completedLevelIds;
|
||||
|
||||
console.log(`[AuthManager] Token 验证成功,积分: ${response.data.user.points},已完成: ${this._completedLevelIds.length} 关`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后获取通关进度
|
||||
*/
|
||||
private async fetchCompletedLevels(): Promise<void> {
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<GameData>>(
|
||||
API_ENDPOINTS.USER_GAME_DATA,
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
this._completedLevelIds = response.data.completedLevelIds;
|
||||
// 同步最新积分
|
||||
StorageManager.setPoints(response.data.user.points);
|
||||
}
|
||||
} catch {
|
||||
console.warn('[AuthManager] 获取通关进度失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/scripts/utils/AuthManager.ts.meta
Normal file
9
assets/scripts/utils/AuthManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "69e4504f-57ab-43b0-b02d-8732cbef7c7f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,8 +1,27 @@
|
||||
/**
|
||||
* HTTP 请求工具类
|
||||
* 封装 XMLHttpRequest,支持 GET/POST 请求
|
||||
* 封装 XMLHttpRequest,支持 GET/POST 请求,支持 JWT 认证
|
||||
*/
|
||||
export class HttpUtil {
|
||||
/** 认证 token */
|
||||
private static _authToken: string | null = null;
|
||||
|
||||
/**
|
||||
* 设置认证 token
|
||||
* @param token JWT token
|
||||
*/
|
||||
static setAuthToken(token: string | null): void {
|
||||
HttpUtil._authToken = token;
|
||||
console.log(`[HttpUtil] Auth token ${token ? '已设置' : '已清除'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证 token
|
||||
*/
|
||||
static getAuthToken(): string | null {
|
||||
return HttpUtil._authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 GET 请求
|
||||
* @param url 请求 URL
|
||||
@@ -17,6 +36,11 @@ export class HttpUtil {
|
||||
xhr.timeout = timeout;
|
||||
xhr.responseType = 'json';
|
||||
|
||||
// 设置认证头
|
||||
if (HttpUtil._authToken) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${HttpUtil._authToken}`);
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.response as T);
|
||||
@@ -53,6 +77,11 @@ export class HttpUtil {
|
||||
xhr.responseType = 'json';
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
// 设置认证头
|
||||
if (HttpUtil._authToken) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${HttpUtil._authToken}`);
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.response as T);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc';
|
||||
import { HttpUtil } from './HttpUtil';
|
||||
import { ApiLevelData, ApiResponse, RuntimeLevelConfig } from '../types/LevelTypes';
|
||||
import { API_ENDPOINTS, API_TIMEOUT } from '../config/ApiConfig';
|
||||
|
||||
/**
|
||||
* 进度回调类型
|
||||
@@ -16,12 +17,6 @@ export type ProgressCallback = (progress: number, message: string) => void;
|
||||
export class LevelDataManager {
|
||||
private static _instance: LevelDataManager | null = null;
|
||||
|
||||
/** API 地址 */
|
||||
private readonly API_URL = 'https://ilookai.cn/api/v1/wechat-game/levels';
|
||||
|
||||
/** 请求超时时间(毫秒) */
|
||||
private readonly REQUEST_TIMEOUT = 8000;
|
||||
|
||||
/** API 请求重试次数 */
|
||||
private readonly API_RETRY_COUNT = 2;
|
||||
|
||||
@@ -114,6 +109,28 @@ export class LevelDataManager {
|
||||
return this._apiData.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据已完成的关卡 ID 列表,计算最高已完成关卡索引
|
||||
* @param completedLevelIds 服务端返回的已完成关卡 ID
|
||||
* @returns 最高已完成关卡的索引(0-based),无匹配返回 -1
|
||||
*/
|
||||
getMaxCompletedIndex(completedLevelIds: string[]): number {
|
||||
if (!this._hasApiData || completedLevelIds.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const completedSet = new Set(completedLevelIds);
|
||||
let maxIndex = -1;
|
||||
|
||||
for (let i = 0; i < this._apiData.length; i++) {
|
||||
if (completedSet.has(this._apiData[i].id)) {
|
||||
maxIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return maxIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有 API 数据
|
||||
*/
|
||||
@@ -220,7 +237,7 @@ export class LevelDataManager {
|
||||
try {
|
||||
onProgress?.(progress, `正在请求服务端数据 (第${attempt}次)...`);
|
||||
|
||||
const response = await HttpUtil.get<ApiResponse>(this.API_URL, this.REQUEST_TIMEOUT);
|
||||
const response = await HttpUtil.get<ApiResponse>(API_ENDPOINTS.LEVELS, API_TIMEOUT.DEFAULT);
|
||||
|
||||
if (!response.success) {
|
||||
console.warn(`[LevelDataManager] API 返回失败, 消息: ${response.message}`);
|
||||
|
||||
@@ -15,17 +15,20 @@ interface UserProgress {
|
||||
* 统一管理用户数据的本地持久化存储
|
||||
*/
|
||||
export class StorageManager {
|
||||
/** 生命值存储键 */
|
||||
private static readonly KEY_LIVES = 'game_lives';
|
||||
/** 积分存储键 */
|
||||
private static readonly KEY_POINTS = 'game_points';
|
||||
|
||||
/** 用户进度存储键 */
|
||||
private static readonly KEY_PROGRESS = 'game_progress';
|
||||
|
||||
/** 默认生命值 */
|
||||
private static readonly DEFAULT_LIVES = 10;
|
||||
/** 认证 token 存储键 */
|
||||
private static readonly KEY_TOKEN = 'auth_token';
|
||||
|
||||
/** 最小生命值 */
|
||||
private static readonly MIN_LIVES = 0;
|
||||
/** 默认积分 */
|
||||
private static readonly DEFAULT_POINTS = 10;
|
||||
|
||||
/** 最小积分 */
|
||||
private static readonly MIN_POINTS = 0;
|
||||
|
||||
/** 默认进度 */
|
||||
private static readonly DEFAULT_PROGRESS: UserProgress = {
|
||||
@@ -36,75 +39,101 @@ export class StorageManager {
|
||||
/** 进度缓存(避免重复读取 localStorage) */
|
||||
private static _progressCache: UserProgress | null = null;
|
||||
|
||||
// ==================== 生命值管理 ====================
|
||||
// ==================== 积分管理 ====================
|
||||
|
||||
/**
|
||||
* 获取当前生命值
|
||||
* @returns 当前生命值,新用户返回默认值 10
|
||||
* 获取当前积分
|
||||
* @returns 当前积分,新用户返回默认值 10
|
||||
*/
|
||||
static getLives(): number {
|
||||
const stored = sys.localStorage.getItem(StorageManager.KEY_LIVES);
|
||||
static getPoints(): number {
|
||||
const stored = sys.localStorage.getItem(StorageManager.KEY_POINTS);
|
||||
if (stored === null || stored === '') {
|
||||
// 新用户,设置默认值
|
||||
StorageManager.setLives(StorageManager.DEFAULT_LIVES);
|
||||
return StorageManager.DEFAULT_LIVES;
|
||||
StorageManager.setPoints(StorageManager.DEFAULT_POINTS);
|
||||
return StorageManager.DEFAULT_POINTS;
|
||||
}
|
||||
const lives = parseInt(stored, 10);
|
||||
const points = parseInt(stored, 10);
|
||||
// 防止异常数据
|
||||
if (isNaN(lives) || lives < 0) {
|
||||
StorageManager.setLives(StorageManager.DEFAULT_LIVES);
|
||||
return StorageManager.DEFAULT_LIVES;
|
||||
if (isNaN(points) || points < 0) {
|
||||
StorageManager.setPoints(StorageManager.DEFAULT_POINTS);
|
||||
return StorageManager.DEFAULT_POINTS;
|
||||
}
|
||||
return lives;
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置生命值
|
||||
* @param lives 生命值
|
||||
* 设置积分
|
||||
* @param points 积分
|
||||
*/
|
||||
static setLives(lives: number): void {
|
||||
const validLives = Math.max(StorageManager.MIN_LIVES, lives);
|
||||
sys.localStorage.setItem(StorageManager.KEY_LIVES, validLives.toString());
|
||||
console.log(`[StorageManager] 生命值已更新: ${validLives}`);
|
||||
static setPoints(points: number): void {
|
||||
const validPoints = Math.max(StorageManager.MIN_POINTS, points);
|
||||
sys.localStorage.setItem(StorageManager.KEY_POINTS, validPoints.toString());
|
||||
console.log(`[StorageManager] 积分已更新: ${validPoints}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 消耗一颗生命
|
||||
* @returns 是否消耗成功(生命值不足时返回 false)
|
||||
* 消耗一个积分
|
||||
* @returns 是否消耗成功(积分不足时返回 false)
|
||||
*/
|
||||
static consumeLife(): boolean {
|
||||
const currentLives = StorageManager.getLives();
|
||||
if (currentLives <= 0) {
|
||||
console.warn('[StorageManager] 生命值不足,无法消耗');
|
||||
static consumePoint(): boolean {
|
||||
const currentPoints = StorageManager.getPoints();
|
||||
if (currentPoints <= 0) {
|
||||
console.warn('[StorageManager] 积分不足,无法消耗');
|
||||
return false;
|
||||
}
|
||||
StorageManager.setLives(currentLives - 1);
|
||||
StorageManager.setPoints(currentPoints - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加一颗生命
|
||||
* 增加一个积分
|
||||
*/
|
||||
static addLife(): void {
|
||||
const currentLives = StorageManager.getLives();
|
||||
StorageManager.setLives(currentLives + 1);
|
||||
console.log(`[StorageManager] 获得一颗生命,当前生命值: ${currentLives + 1}`);
|
||||
static addPoint(): void {
|
||||
const currentPoints = StorageManager.getPoints();
|
||||
StorageManager.setPoints(currentPoints + 1);
|
||||
console.log(`[StorageManager] 获得一个积分,当前积分: ${currentPoints + 1}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有足够的生命值
|
||||
* @returns 是否有生命值
|
||||
* 检查是否有足够的积分
|
||||
* @returns 是否有积分
|
||||
*/
|
||||
static hasLives(): boolean {
|
||||
return StorageManager.getLives() > 0;
|
||||
static hasPoints(): boolean {
|
||||
return StorageManager.getPoints() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置生命值为默认值
|
||||
* 重置积分为默认值
|
||||
*/
|
||||
static resetLives(): void {
|
||||
StorageManager.setLives(StorageManager.DEFAULT_LIVES);
|
||||
console.log('[StorageManager] 生命值已重置为默认值');
|
||||
static resetPoints(): void {
|
||||
StorageManager.setPoints(StorageManager.DEFAULT_POINTS);
|
||||
console.log('[StorageManager] 积分已重置为默认值');
|
||||
}
|
||||
|
||||
// ==================== 认证 Token 管理 ====================
|
||||
|
||||
/**
|
||||
* 获取认证 token
|
||||
*/
|
||||
static getToken(): string | null {
|
||||
const token = sys.localStorage.getItem(StorageManager.KEY_TOKEN);
|
||||
return (token === null || token === '') ? null : token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置认证 token
|
||||
*/
|
||||
static setToken(token: string): void {
|
||||
sys.localStorage.setItem(StorageManager.KEY_TOKEN, token);
|
||||
console.log('[StorageManager] Token 已保存');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除认证 token
|
||||
*/
|
||||
static clearToken(): void {
|
||||
sys.localStorage.removeItem(StorageManager.KEY_TOKEN);
|
||||
console.log('[StorageManager] Token 已清除');
|
||||
}
|
||||
|
||||
// ==================== 关卡进度管理 ====================
|
||||
@@ -229,11 +258,12 @@ export class StorageManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有数据(生命值 + 进度)
|
||||
* 重置所有数据(积分 + 进度)
|
||||
*/
|
||||
static resetAll(): void {
|
||||
StorageManager.resetLives();
|
||||
StorageManager.resetPoints();
|
||||
StorageManager.resetProgress();
|
||||
StorageManager.clearToken();
|
||||
console.log('[StorageManager] 所有数据已重置');
|
||||
}
|
||||
}
|
||||
|
||||
117
assets/scripts/utils/UserAssetsManager.ts
Normal file
117
assets/scripts/utils/UserAssetsManager.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { HttpUtil } from './HttpUtil';
|
||||
import { StorageManager } from './StorageManager';
|
||||
import { AuthManager } from './AuthManager';
|
||||
import { API_ENDPOINTS, API_TIMEOUT, POINT_REASONS } from '../config/ApiConfig';
|
||||
import { ApiEnvelope, UserAssetsData } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 用户资产管理器
|
||||
* 单例模式,负责积分的服务端同步
|
||||
* 以服务端为准,本地 StorageManager 作为缓存
|
||||
*/
|
||||
export class UserAssetsManager {
|
||||
private static _instance: UserAssetsManager | null = null;
|
||||
|
||||
static get instance(): UserAssetsManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new UserAssetsManager();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 从服务端获取最新积分
|
||||
*/
|
||||
async fetchPoints(): Promise<number> {
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<UserAssetsData>>(
|
||||
API_ENDPOINTS.USER_ASSETS,
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
StorageManager.setPoints(response.data.points);
|
||||
return response.data.points;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[UserAssetsManager] 获取积分失败:', err);
|
||||
}
|
||||
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* 消耗积分(解锁提示)
|
||||
* @returns 是否消耗成功
|
||||
*/
|
||||
async consumePoint(levelId?: string, hintIndex?: number): Promise<boolean> {
|
||||
if (!StorageManager.hasPoints()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
return StorageManager.consumePoint();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.post<ApiEnvelope<UserAssetsData>>(
|
||||
API_ENDPOINTS.USER_ASSETS_CONSUME,
|
||||
{
|
||||
reason: POINT_REASONS.HINT_UNLOCK,
|
||||
levelId,
|
||||
hintIndex,
|
||||
},
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
StorageManager.setPoints(response.data.points);
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[UserAssetsManager] 消耗积分失败:', response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[UserAssetsManager] 消耗积分请求失败,降级本地处理:', err);
|
||||
return StorageManager.consumePoint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得积分(通关奖励)
|
||||
* @returns 获得后的积分数
|
||||
*/
|
||||
async earnPoint(levelId: string): Promise<number> {
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
StorageManager.addPoint();
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.post<ApiEnvelope<UserAssetsData>>(
|
||||
API_ENDPOINTS.USER_ASSETS_EARN,
|
||||
{
|
||||
reason: POINT_REASONS.LEVEL_COMPLETE,
|
||||
levelId,
|
||||
},
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
StorageManager.setPoints(response.data.points);
|
||||
return response.data.points;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[UserAssetsManager] 获得积分请求失败,降级本地处理:', err);
|
||||
}
|
||||
|
||||
StorageManager.addPoint();
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
}
|
||||
9
assets/scripts/utils/UserAssetsManager.ts.meta
Normal file
9
assets/scripts/utils/UserAssetsManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ddba71db-75ed-468d-ac99-b4632c0b2ae4",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -44,6 +44,38 @@ export class WxSDK {
|
||||
return typeof wx !== 'undefined' ? wx : null;
|
||||
}
|
||||
|
||||
// ==================== 登录相关 ====================
|
||||
|
||||
/**
|
||||
* 微信登录,获取临时 code
|
||||
* @returns Promise<string> 登录 code
|
||||
*/
|
||||
static login(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const wxApi = WxSDK.getWx();
|
||||
if (!wxApi) {
|
||||
reject(new Error('非微信环境,无法调用 wx.login'));
|
||||
return;
|
||||
}
|
||||
|
||||
wxApi.login({
|
||||
success: (res: any) => {
|
||||
if (res.code) {
|
||||
console.log('[WxSDK] wx.login 成功,获取到 code');
|
||||
resolve(res.code);
|
||||
} else {
|
||||
console.error('[WxSDK] wx.login 失败:', res.errMsg);
|
||||
reject(new Error(res.errMsg || 'wx.login 失败'));
|
||||
}
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('[WxSDK] wx.login 调用失败:', err);
|
||||
reject(new Error(err.errMsg || 'wx.login 调用失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 分享相关 ====================
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user