feat: 支持登录、个人信息存储

This commit is contained in:
richarjiang
2026-04-05 13:37:58 +08:00
parent e438f6fce4
commit b732e4d8f8
23 changed files with 3572 additions and 144 deletions

View File

@@ -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}`);
}
}
}

View File

@@ -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();

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "5ec36628-7826-482c-a679-eb20093b0edb",
"files": [],
"subMetas": {},
"userData": {}
}

View 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;

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "f84f3481-515f-4dd6-9664-566e459331bd",
"files": [],
"subMetas": {},
"userData": {}
}

View 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[];
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "13078ed9-43cf-4949-8a92-e702ee7de88a",
"files": [],
"subMetas": {},
"userData": {}
}

View 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] 获取通关进度失败');
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "69e4504f-57ab-43b0-b02d-8732cbef7c7f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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);

View File

@@ -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}`);

View File

@@ -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] 所有数据已重置');
}
}

View 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();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ddba71db-75ed-468d-ac99-b4632c0b2ae4",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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 调用失败'));
}
});
});
}
// ==================== 分享相关 ====================
/**