feat: 接入关卡配置 API 并支持降级到本地配置

- 新增 LevelDataManager 单例管理关卡数据
- 新增 HttpUtil 封装 XMLHttpRequest 请求
- 新增 LevelTypes 类型定义
- PageLoading 集成 API 数据预加载(0-80% 进度)
- PageLevel 支持优先使用 API 数据,失败时降级到本地配置
- 字段映射: hint1/2/3 → clue1/2/3, imageUrl → SpriteFrame
This commit is contained in:
richarjiang
2026-03-15 16:07:00 +08:00
parent c9fbc5212a
commit c54a404c12
5 changed files with 430 additions and 18 deletions

View File

@@ -0,0 +1,75 @@
/**
* HTTP 请求工具类
* 封装 XMLHttpRequest支持 GET/POST 请求
*/
export class HttpUtil {
/**
* 发送 GET 请求
* @param url 请求 URL
* @param timeout 超时时间(毫秒),默认 10000
* @returns Promise<Response>
*/
static get<T>(url: string, timeout: number = 10000): Promise<T> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.timeout = timeout;
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response as T);
} else {
reject(new Error(`HTTP 错误: ${xhr.status}`));
}
};
xhr.onerror = () => {
reject(new Error('网络请求失败'));
};
xhr.ontimeout = () => {
reject(new Error('请求超时'));
};
xhr.send();
});
}
/**
* 发送 POST 请求
* @param url 请求 URL
* @param data 请求体数据
* @param timeout 超时时间(毫秒),默认 10000
* @returns Promise<Response>
*/
static post<T>(url: string, data: object, timeout: number = 10000): Promise<T> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.timeout = timeout;
xhr.responseType = 'json';
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response as T);
} else {
reject(new Error(`HTTP 错误: ${xhr.status}`));
}
};
xhr.onerror = () => {
reject(new Error('网络请求失败'));
};
xhr.ontimeout = () => {
reject(new Error('请求超时'));
};
xhr.send(JSON.stringify(data));
});
}
}

View File

@@ -0,0 +1,207 @@
import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc';
import { HttpUtil } from './HttpUtil';
import { ApiLevelData, ApiResponse, RuntimeLevelConfig } from '../types/LevelTypes';
/**
* 进度回调类型
* @param progress 进度值 0-1
* @param message 进度消息
*/
export type ProgressCallback = (progress: number, message: string) => void;
/**
* 关卡数据管理器
* 单例模式,负责从 API 获取关卡数据并预加载图片
*/
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;
/** 运行时关卡配置缓存 */
private _levelConfigs: RuntimeLevelConfig[] = [];
/** 是否已成功从 API 获取数据 */
private _hasApiData: boolean = false;
/** 图片缓存URL -> SpriteFrame */
private _imageCache: Map<string, SpriteFrame> = new Map();
/**
* 获取单例实例
*/
static get instance(): LevelDataManager {
if (!this._instance) {
this._instance = new LevelDataManager();
}
return this._instance;
}
/**
* 私有构造函数
*/
private constructor() {}
/**
* 初始化:从 API 获取数据并预加载图片
* @param onProgress 进度回调
* @returns 是否初始化成功
*/
async initialize(onProgress?: ProgressCallback): Promise<boolean> {
console.log('[LevelDataManager] 开始初始化');
try {
// 阶段1: 获取 API 数据 (0-20%)
onProgress?.(0, '正在获取关卡数据...');
const apiData = await this._fetchApiData();
if (!apiData || apiData.length === 0) {
console.warn('[LevelDataManager] API 返回空数据');
onProgress?.(0.2, 'API 数据为空,使用本地配置');
return false;
}
console.log(`[LevelDataManager] 获取到 ${apiData.length} 个关卡数据`);
onProgress?.(0.2, `获取到 ${apiData.length} 个关卡`);
// 阶段2: 预加载所有图片 (20-80%)
const configs = await this._preloadImages(apiData, (progress) => {
onProgress?.(0.2 + progress * 0.6, '正在加载关卡资源...');
});
this._levelConfigs = configs;
this._hasApiData = true;
console.log('[LevelDataManager] 初始化完成');
onProgress?.(0.8, '关卡资源加载完成');
return true;
} catch (error) {
console.error('[LevelDataManager] 初始化失败:', error);
onProgress?.(0.2, '获取数据失败,使用本地配置');
return false;
}
}
/**
* 获取指定关卡配置
* @param index 关卡索引
*/
getLevelConfig(index: number): RuntimeLevelConfig | null {
if (index < 0 || index >= this._levelConfigs.length) {
return null;
}
return this._levelConfigs[index];
}
/**
* 获取关卡总数
*/
getLevelCount(): number {
return this._levelConfigs.length;
}
/**
* 检查是否有 API 数据
*/
hasApiData(): boolean {
return this._hasApiData && this._levelConfigs.length > 0;
}
/**
* 从 API 获取关卡数据
*/
private async _fetchApiData(): Promise<ApiLevelData[] | null> {
try {
const response = await HttpUtil.get<ApiResponse>(this.API_URL, this.REQUEST_TIMEOUT);
if (response.code !== 0) {
console.warn(`[LevelDataManager] API 返回错误码: ${response.code}, 消息: ${response.message}`);
return null;
}
return response.data;
} catch (error) {
console.error('[LevelDataManager] API 请求失败:', error);
return null;
}
}
/**
* 预加载所有图片
* @param apiData API 返回的关卡数据
* @param onProgress 进度回调
*/
private async _preloadImages(
apiData: ApiLevelData[],
onProgress?: (progress: number) => void
): Promise<RuntimeLevelConfig[]> {
const configs: RuntimeLevelConfig[] = [];
const total = apiData.length;
for (let i = 0; i < total; i++) {
const data = apiData[i];
const spriteFrame = await this._loadImage(data.imageUrl);
configs.push({
id: data.id,
name: data.name,
spriteFrame: spriteFrame,
clue1: data.hint1,
clue2: data.hint2,
clue3: data.hint3,
answer: data.answer
});
onProgress?.((i + 1) / total);
}
return configs;
}
/**
* 加载远程图片为 SpriteFrame
* @param url 图片 URL
*/
private async _loadImage(url: string): Promise<SpriteFrame | null> {
// 检查缓存
if (this._imageCache.has(url)) {
return this._imageCache.get(url)!;
}
return new Promise((resolve) => {
assetManager.loadRemote<ImageAsset>(url, (err, imageAsset) => {
if (err) {
console.error(`[LevelDataManager] 加载图片失败: ${url}`, err);
resolve(null);
return;
}
const texture = new Texture2D();
texture.image = imageAsset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
// 缓存
this._imageCache.set(url, spriteFrame);
resolve(spriteFrame);
});
});
}
/**
* 清除缓存
*/
clearCache(): void {
this._levelConfigs = [];
this._hasApiData = false;
this._imageCache.clear();
console.log('[LevelDataManager] 缓存已清除');
}
}