feat: 支持关卡配置分享

This commit is contained in:
richarjiang
2026-04-06 17:32:32 +08:00
parent c7f52ab032
commit b489ab40f5
8 changed files with 392 additions and 24 deletions

View File

@@ -3,6 +3,8 @@ import { ViewManager } from './scripts/core/ViewManager';
import { LevelDataManager } from './scripts/utils/LevelDataManager'; import { LevelDataManager } from './scripts/utils/LevelDataManager';
import { AuthManager } from './scripts/utils/AuthManager'; import { AuthManager } from './scripts/utils/AuthManager';
import { StorageManager } from './scripts/utils/StorageManager'; import { StorageManager } from './scripts/utils/StorageManager';
import { ShareManager } from './scripts/utils/ShareManager';
import { WxSDK } from './scripts/utils/WxSDK';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
/** /**
@@ -55,7 +57,27 @@ export class PageLoading extends Component {
this._syncProgressFromServer(); this._syncProgressFromServer();
} }
// 预加载 PageHome (80-100%) // 检测分享码:从微信启动参数中获取
const shareCode = WxSDK.getShareCodeFromLaunch();
if (shareCode && loginSuccess) {
this._updateStatusLabel('正在加载挑战关卡...');
const joinSuccess = await ShareManager.instance.joinShare(shareCode);
if (joinSuccess) {
this._updateProgress(1);
this._updateStatusLabel('加载完成');
// 跳过首页,直接进入分享挑战关卡
ViewManager.instance.open('PageLevel', {
params: { shareMode: true },
onComplete: () => {
this.node.destroy();
},
});
return;
}
console.warn('[PageLoading] 加入分享失败,进入正常模式');
}
// 正常流程:预加载 PageHome (80-100%)
ViewManager.instance.preload('PageHome', ViewManager.instance.preload('PageHome',
(progress) => { (progress) => {
this._updateProgress(0.8 + progress * 0.2); this._updateProgress(0.8 + progress * 0.2);

View File

@@ -7,6 +7,7 @@ import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager'; import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes'; import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
import { PassModal } from 'db://assets/prefabs/PassModal'; import { PassModal } from 'db://assets/prefabs/PassModal';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
@@ -104,14 +105,26 @@ export class PageLevel extends BaseView {
/** 通关弹窗实例 */ /** 通关弹窗实例 */
private _passModalNode: Node | null = null; private _passModalNode: Node | null = null;
/** 是否处于分享挑战模式 */
private _isShareMode: boolean = false;
/** /**
* 页面首次加载时调用 * 页面首次加载时调用
*/ */
onViewLoad(): void { onViewLoad(): void {
console.log('[PageLevel] onViewLoad'); console.log('[PageLevel] onViewLoad');
// 从本地存储恢复关卡进度
this.currentLevelIndex = StorageManager.getCurrentLevelIndex(); const params = this.getParams();
console.log(`[PageLevel] 恢复关卡进度: 第 ${this.currentLevelIndex + 1}`); this._isShareMode = params?.shareMode === true;
if (this._isShareMode) {
this.currentLevelIndex = 0;
console.log('[PageLevel] 进入分享挑战模式');
} else {
// 从本地存储恢复关卡进度
this.currentLevelIndex = StorageManager.getCurrentLevelIndex();
console.log(`[PageLevel] 恢复关卡进度: 第 ${this.currentLevelIndex + 1}`);
}
this.updatePointsLabel(); this.updatePointsLabel();
this.initIconSetting(); this.initIconSetting();
this.initUnlockButtons(); this.initUnlockButtons();
@@ -160,13 +173,18 @@ export class PageLevel extends BaseView {
* 初始化关卡(从 API 数据加载,异步确保资源就绪) * 初始化关卡(从 API 数据加载,异步确保资源就绪)
*/ */
private async initLevel(): Promise<void> { private async initLevel(): Promise<void> {
// 先尝试从缓存获取 let config: RuntimeLevelConfig | null = null;
let config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex);
if (!config) { if (this._isShareMode) {
// 缓存中没有,异步加载 // 分享模式:从 ShareManager 获取关卡
console.log(`[PageLevel] 关卡 ${this.currentLevelIndex + 1} 资源未缓存,开始加载...`); config = await ShareManager.instance.ensureShareLevelReady(this.currentLevelIndex);
config = await LevelDataManager.instance.ensureLevelReady(this.currentLevelIndex); } else {
// 正常模式:先尝试缓存,再异步加载
config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex);
if (!config) {
console.log(`[PageLevel] 关卡 ${this.currentLevelIndex + 1} 资源未缓存,开始加载...`);
config = await LevelDataManager.instance.ensureLevelReady(this.currentLevelIndex);
}
} }
if (!config) { if (!config) {
@@ -212,7 +230,14 @@ export class PageLevel extends BaseView {
this.updateClockLabel(); this.updateClockLabel();
// 预加载下一关图片(静默加载,不阻塞) // 预加载下一关图片(静默加载,不阻塞)
LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex); if (this._isShareMode) {
const nextIndex = this.currentLevelIndex + 1;
if (nextIndex < ShareManager.instance.getShareLevelCount()) {
ShareManager.instance.ensureShareLevelReady(nextIndex).catch(() => {});
}
} else {
LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex);
}
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer.length}`); console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer.length}`);
} }
@@ -356,7 +381,14 @@ export class PageLevel extends BaseView {
private onIconSettingClick(): void { private onIconSettingClick(): void {
console.log('[PageLevel] IconSetting 点击,返回主页'); console.log('[PageLevel] IconSetting 点击,返回主页');
this.playClickSound(); this.playClickSound();
ViewManager.instance.back();
// 分享模式下栈中没有 PageHome需要清除分享状态并直接打开首页
if (this._isShareMode) {
ShareManager.instance.clearShareMode();
ViewManager.instance.replace('PageHome');
} else {
ViewManager.instance.back();
}
} }
// ========== 线索相关方法 ========== // ========== 线索相关方法 ==========
@@ -656,10 +688,12 @@ export class PageLevel extends BaseView {
// 播放成功音效 // 播放成功音效
this.playSuccessSound(); this.playSuccessSound();
// 通关奖励:通过服务端增加积分 // 通关奖励:分享模式下不增加积分
const levelId = this._currentConfig?.id ?? ''; if (!this._isShareMode) {
await UserAssetsManager.instance.earnPoint(levelId); const levelId = this._currentConfig?.id ?? '';
this.updatePointsLabel(); await UserAssetsManager.instance.earnPoint(levelId);
this.updatePointsLabel();
}
// 显示通关弹窗 // 显示通关弹窗
this._showPassModal(); this._showPassModal();
@@ -748,19 +782,29 @@ export class PageLevel extends BaseView {
* 进入下一关 * 进入下一关
*/ */
private async nextLevel(): Promise<void> { private async nextLevel(): Promise<void> {
// 保存当前关卡进度 // 分享模式不保存本地进度
StorageManager.onLevelCompleted(this.currentLevelIndex); if (!this._isShareMode) {
StorageManager.onLevelCompleted(this.currentLevelIndex);
}
this.currentLevelIndex++; this.currentLevelIndex++;
// 检查是否还有关卡 // 检查是否还有关卡
const totalLevels = LevelDataManager.instance.getLevelCount(); const totalLevels = this._isShareMode
? ShareManager.instance.getShareLevelCount()
: LevelDataManager.instance.getLevelCount();
if (this.currentLevelIndex >= totalLevels) { if (this.currentLevelIndex >= totalLevels) {
// 所有关卡完成 // 所有关卡完成
console.log('[PageLevel] 恭喜通关!'); console.log('[PageLevel] 恭喜通关!');
this.stopCountdown(); this.stopCountdown();
ViewManager.instance.back();
if (this._isShareMode) {
ShareManager.instance.clearShareMode();
ViewManager.instance.replace('PageHome');
} else {
ViewManager.instance.back();
}
return; return;
} }

View File

@@ -3,6 +3,7 @@ import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager'; import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
/** /**
@@ -78,6 +79,9 @@ export class PageWriteLevels extends BaseView {
/** 缓存 view 节点的 UITransform避免每次 _updateContentSize 重复查找 */ /** 缓存 view 节点的 UITransform避免每次 _updateContentSize 重复查找 */
private _viewTransform: UITransform | null = null; private _viewTransform: UITransform | null = null;
/** 防止重复提交 */
private _isSubmitting: boolean = false;
onViewLoad(): void { onViewLoad(): void {
console.log('[PageWriteLevels] onViewLoad'); console.log('[PageWriteLevels] onViewLoad');
this._initButtons(); this._initButtons();
@@ -92,6 +96,9 @@ export class PageWriteLevels extends BaseView {
if (this.previewBtn) { if (this.previewBtn) {
this.previewBtn.on(Button.EventType.CLICK, this._onPreviewClick, this); this.previewBtn.on(Button.EventType.CLICK, this._onPreviewClick, this);
} }
if (this.completeBtn) {
this.completeBtn.on(Button.EventType.CLICK, this._onCompleteClick, this);
}
} }
private _initScrollView(): void { private _initScrollView(): void {
@@ -156,7 +163,10 @@ export class PageWriteLevels extends BaseView {
onViewShow(): void { onViewShow(): void {
console.log('[PageWriteLevels] onViewShow'); console.log('[PageWriteLevels] onViewShow');
this._initLevelList(); // 仅首次初始化列表,从预览页返回时保留选中状态
if (this._itemNodes.length === 0) {
this._initLevelList();
}
} }
private _initLevelList(): void { private _initLevelList(): void {
@@ -276,6 +286,8 @@ export class PageWriteLevels extends BaseView {
const toggle = isSelected.getComponent(Toggle); const toggle = isSelected.getComponent(Toggle);
if (toggle) { if (toggle) {
toggle.isChecked = this._selectedIndices.has(index); toggle.isChecked = this._selectedIndices.has(index);
// 禁用 Toggle 交互,仅作为视觉指示器,选中逻辑由 item Button 统一处理
toggle.interactable = false;
} }
const checkmark = isSelected.getChildByName('Checkmark'); const checkmark = isSelected.getChildByName('Checkmark');
if (checkmark) { if (checkmark) {
@@ -428,14 +440,22 @@ export class PageWriteLevels extends BaseView {
ViewManager.instance.back(); ViewManager.instance.back();
} }
private _onPreviewClick(): void { /**
* 校验是否已选满关卡,未满则 Toast 提示
* @returns true 表示校验通过
*/
private _validateSelection(): boolean {
if (this._selectedIndices.size < MAX_SELECTION) { if (this._selectedIndices.size < MAX_SELECTION) {
const remaining = MAX_SELECTION - this._selectedIndices.size; const remaining = MAX_SELECTION - this._selectedIndices.size;
ToastManager.instance.show(`还需选择${remaining}个关卡`); ToastManager.instance.show(`还需选择${remaining}个关卡`);
return; return false;
} }
return true;
}
private _onPreviewClick(): void {
if (!this._validateSelection()) return;
const shareTitle = this.shareTitleEditBox?.getComponent(EditBox)?.string?.trim() || ''; const shareTitle = this.shareTitleEditBox?.getComponent(EditBox)?.string?.trim() || '';
console.log('[PageWriteLevels] 预览按钮点击,标题:', shareTitle, '已选关卡:', Array.from(this._selectedIndices));
ViewManager.instance.open('PagePreviewLevels', { ViewManager.instance.open('PagePreviewLevels', {
params: { params: {
selectedIndices: Array.from(this._selectedIndices), selectedIndices: Array.from(this._selectedIndices),
@@ -444,6 +464,57 @@ export class PageWriteLevels extends BaseView {
}); });
} }
private async _onCompleteClick(): Promise<void> {
if (!this._validateSelection()) return;
const shareTitle = this.shareTitleEditBox?.getComponent(EditBox)?.string?.trim() || '';
if (!shareTitle) {
ToastManager.instance.show('请输入分享标题');
return;
}
if (this._isSubmitting) return;
this._isSubmitting = true;
try {
const levelIds = this._getSelectedLevelIds();
if (levelIds.length !== MAX_SELECTION) {
ToastManager.instance.show('获取关卡数据失败,请重试');
return;
}
const shareCode = await ShareManager.instance.createShare(shareTitle, levelIds);
if (!shareCode) {
ToastManager.instance.show('创建分享失败,请重试');
return;
}
console.log('[PageWriteLevels] 创建分享成功, code:', shareCode);
ShareManager.instance.triggerWxShare(shareTitle, shareCode);
ToastManager.instance.show('分享创建成功!');
} catch (err) {
console.error('[PageWriteLevels] 完成按钮异常:', err);
ToastManager.instance.show('操作失败,请重试');
} finally {
this._isSubmitting = false;
}
}
/**
* 将选中的关卡索引转换为关卡 ID 数组
*/
private _getSelectedLevelIds(): string[] {
const ids: string[] = [];
const sortedIndices = Array.from(this._selectedIndices).sort((a, b) => a - b);
for (const index of sortedIndices) {
const config = LevelDataManager.instance.getLevelConfig(index);
if (config) {
ids.push(config.id);
}
}
return ids;
}
onViewHide(): void { onViewHide(): void {
console.log('[PageWriteLevels] onViewHide'); console.log('[PageWriteLevels] onViewHide');
} }
@@ -456,6 +527,9 @@ export class PageWriteLevels extends BaseView {
if (this.previewBtn) { if (this.previewBtn) {
this.previewBtn.off(Button.EventType.CLICK, this._onPreviewClick, this); this.previewBtn.off(Button.EventType.CLICK, this._onPreviewClick, this);
} }
if (this.completeBtn) {
this.completeBtn.off(Button.EventType.CLICK, this._onCompleteClick, this);
}
if (this.scrollView) { if (this.scrollView) {
this.scrollView.off(Node.EventType.TOUCH_START, this._onTouchStart, this); this.scrollView.off(Node.EventType.TOUCH_START, this._onTouchStart, this);
this.scrollView.off(Node.EventType.TOUCH_END, this._onTouchEnd, this); this.scrollView.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);

View File

@@ -14,8 +14,14 @@ export const API_ENDPOINTS = {
USER_ASSETS_EARN: `${API_BASE}/user/assets/earn`, USER_ASSETS_EARN: `${API_BASE}/user/assets/earn`,
USER_GAME_DATA: `${API_BASE}/user/game-data`, USER_GAME_DATA: `${API_BASE}/user/game-data`,
LEVELS: `${API_BASE}/wechat-game/levels`, LEVELS: `${API_BASE}/wechat-game/levels`,
SHARE_CREATE: `${API_BASE}/share`,
} as const; } as const;
/** 构建加入分享的 URL */
export function getShareJoinUrl(code: string): string {
return `${API_BASE}/share/${code}/join`;
}
/** 积分操作原因 */ /** 积分操作原因 */
export const POINT_REASONS = { export const POINT_REASONS = {
HINT_UNLOCK: 'hint_unlock', HINT_UNLOCK: 'hint_unlock',

View File

@@ -29,3 +29,29 @@ export interface GameData {
user: { id: string; points: number }; user: { id: string; points: number };
completedLevelIds: string[]; completedLevelIds: string[];
} }
/** 创建分享响应 */
export interface CreateShareData {
shareCode: string;
title: string;
levelCount: number;
}
/** 分享关卡数据 */
export interface ShareLevelData {
id: string;
level: number;
imageUrl: string;
answer: string;
hint1: string;
hint2: string;
hint3: string;
sortOrder: number;
}
/** 加入分享响应 */
export interface JoinShareData {
shareCode: string;
title: string;
levels: ShareLevelData[];
}

View File

@@ -0,0 +1,167 @@
import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc';
import { HttpUtil } from './HttpUtil';
import { WxSDK } from './WxSDK';
import { API_ENDPOINTS, getShareJoinUrl, API_TIMEOUT } from '../config/ApiConfig';
import { ApiEnvelope, CreateShareData, JoinShareData, ShareLevelData } from '../types/ApiTypes';
import { RuntimeLevelConfig } from '../types/LevelTypes';
/**
* 分享管理器
* 负责创建分享、加入分享、缓存分享关卡数据
*/
export class ShareManager {
private static _instance: ShareManager | null = null;
/** 分享模式的关卡数据null 表示正常模式) */
private _shareLevels: RuntimeLevelConfig[] | null = null;
/** API 返回的原始关卡数据(保留 imageUrl 用于懒加载) */
private _shareApiLevels: ShareLevelData[] = [];
private _shareTitle: string = '';
private _shareCode: string | null = null;
/** 图片缓存URL -> SpriteFrame */
private _imageCache: Map<string, SpriteFrame> = new Map();
static get instance(): ShareManager {
if (!this._instance) {
this._instance = new ShareManager();
}
return this._instance;
}
private constructor() {}
get isShareMode(): boolean {
return this._shareLevels !== null && this._shareLevels.length > 0;
}
async createShare(title: string, levelIds: string[]): Promise<string | null> {
try {
const response = await HttpUtil.post<ApiEnvelope<CreateShareData>>(
API_ENDPOINTS.SHARE_CREATE,
{ title, levelIds },
API_TIMEOUT.DEFAULT,
);
if (!response.success || !response.data) {
console.error('[ShareManager] 创建分享失败:', response.message);
return null;
}
return response.data.shareCode;
} catch (err) {
console.error('[ShareManager] 创建分享异常:', err);
return null;
}
}
async joinShare(code: string): Promise<boolean> {
try {
const response = await HttpUtil.post<ApiEnvelope<JoinShareData>>(
getShareJoinUrl(code),
{},
API_TIMEOUT.DEFAULT,
);
if (!response.success || !response.data) {
console.error('[ShareManager] 加入分享失败:', response.message);
return false;
}
const { shareCode, title, levels } = response.data;
this._shareCode = shareCode;
this._shareTitle = title;
this._shareApiLevels = levels;
const runtimeLevels: RuntimeLevelConfig[] = levels.map((level) => ({
id: level.id,
name: `${level.level}`,
spriteFrame: null,
clue1: level.hint1,
clue2: level.hint2,
clue3: level.hint3,
answer: level.answer,
}));
// 预加载首关图片
if (levels.length > 0) {
const sf = await this._loadImage(levels[0].imageUrl);
if (sf) {
runtimeLevels[0].spriteFrame = sf;
}
}
this._shareLevels = runtimeLevels;
console.log(`[ShareManager] 加入分享成功: ${title}, ${levels.length}`);
return true;
} catch (err) {
console.error('[ShareManager] 加入分享异常:', err);
return false;
}
}
async ensureShareLevelReady(index: number): Promise<RuntimeLevelConfig | null> {
if (!this._shareLevels || index < 0 || index >= this._shareLevels.length) {
return null;
}
const config = this._shareLevels[index];
if (config.spriteFrame) {
return config;
}
const apiLevel = this._shareApiLevels[index];
if (apiLevel?.imageUrl) {
const sf = await this._loadImage(apiLevel.imageUrl);
if (sf) {
config.spriteFrame = sf;
}
}
return config;
}
getShareLevelCount(): number {
return this._shareLevels?.length ?? 0;
}
triggerWxShare(title: string, shareCode: string): void {
WxSDK.shareAppMessage({
title: title || '来挑战我出的谐音梗吧!',
query: `shareCode=${shareCode}`,
});
}
clearShareMode(): void {
this._shareLevels = null;
this._shareApiLevels = [];
this._shareTitle = '';
this._shareCode = null;
this._imageCache.clear();
}
private _loadImage(url: string): Promise<SpriteFrame | null> {
const cached = this._imageCache.get(url);
if (cached) {
return Promise.resolve(cached);
}
return new Promise((resolve) => {
assetManager.loadRemote<ImageAsset>(url, (err, imageAsset) => {
if (err) {
console.error('[ShareManager] 加载图片失败:', 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);
});
});
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "6044a8d5-305c-4b68-8abd-6bde3da0505c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -216,4 +216,24 @@ export class WxSDK {
} }
}); });
} }
/**
* 从启动参数中获取分享码
* @returns 分享码,不存在则返回 null
*/
static getShareCodeFromLaunch(): string | null {
const wxApi = WxSDK.getWx();
if (!wxApi) return null;
try {
const options = wxApi.getLaunchOptionsSync();
if (options?.query?.shareCode) {
console.log('[WxSDK] 检测到分享码:', options.query.shareCode);
return options.query.shareCode;
}
} catch (err) {
console.warn('[WxSDK] 获取启动参数失败:', err);
}
return null;
}
} }