diff --git a/assets/PageLoading.ts b/assets/PageLoading.ts index f24e8bd..c97a6fc 100644 --- a/assets/PageLoading.ts +++ b/assets/PageLoading.ts @@ -3,6 +3,8 @@ import { ViewManager } from './scripts/core/ViewManager'; import { LevelDataManager } from './scripts/utils/LevelDataManager'; import { AuthManager } from './scripts/utils/AuthManager'; import { StorageManager } from './scripts/utils/StorageManager'; +import { ShareManager } from './scripts/utils/ShareManager'; +import { WxSDK } from './scripts/utils/WxSDK'; const { ccclass, property } = _decorator; /** @@ -55,7 +57,27 @@ export class PageLoading extends Component { 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', (progress) => { this._updateProgress(0.8 + progress * 0.2); diff --git a/assets/prefabs/PageLevel.ts b/assets/prefabs/PageLevel.ts index f6a513c..9c5f42e 100644 --- a/assets/prefabs/PageLevel.ts +++ b/assets/prefabs/PageLevel.ts @@ -7,6 +7,7 @@ import { WxSDK } from 'db://assets/scripts/utils/WxSDK'; import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager'; import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; +import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; import { PassModal } from 'db://assets/prefabs/PassModal'; const { ccclass, property } = _decorator; @@ -104,14 +105,26 @@ export class PageLevel extends BaseView { /** 通关弹窗实例 */ private _passModalNode: Node | null = null; + /** 是否处于分享挑战模式 */ + private _isShareMode: boolean = false; + /** * 页面首次加载时调用 */ onViewLoad(): void { console.log('[PageLevel] onViewLoad'); - // 从本地存储恢复关卡进度 - this.currentLevelIndex = StorageManager.getCurrentLevelIndex(); - console.log(`[PageLevel] 恢复关卡进度: 第 ${this.currentLevelIndex + 1} 关`); + + const params = this.getParams(); + 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.initIconSetting(); this.initUnlockButtons(); @@ -160,13 +173,18 @@ export class PageLevel extends BaseView { * 初始化关卡(从 API 数据加载,异步确保资源就绪) */ private async initLevel(): Promise { - // 先尝试从缓存获取 - let config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex); + let config: RuntimeLevelConfig | null = null; - if (!config) { - // 缓存中没有,异步加载 - console.log(`[PageLevel] 关卡 ${this.currentLevelIndex + 1} 资源未缓存,开始加载...`); - config = await LevelDataManager.instance.ensureLevelReady(this.currentLevelIndex); + if (this._isShareMode) { + // 分享模式:从 ShareManager 获取关卡 + config = await ShareManager.instance.ensureShareLevelReady(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) { @@ -212,7 +230,14 @@ export class PageLevel extends BaseView { 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}`); } @@ -356,7 +381,14 @@ export class PageLevel extends BaseView { private onIconSettingClick(): void { console.log('[PageLevel] IconSetting 点击,返回主页'); 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(); - // 通关奖励:通过服务端增加积分 - const levelId = this._currentConfig?.id ?? ''; - await UserAssetsManager.instance.earnPoint(levelId); - this.updatePointsLabel(); + // 通关奖励:分享模式下不增加积分 + if (!this._isShareMode) { + const levelId = this._currentConfig?.id ?? ''; + await UserAssetsManager.instance.earnPoint(levelId); + this.updatePointsLabel(); + } // 显示通关弹窗 this._showPassModal(); @@ -748,19 +782,29 @@ export class PageLevel extends BaseView { * 进入下一关 */ private async nextLevel(): Promise { - // 保存当前关卡进度 - StorageManager.onLevelCompleted(this.currentLevelIndex); + // 分享模式不保存本地进度 + if (!this._isShareMode) { + StorageManager.onLevelCompleted(this.currentLevelIndex); + } this.currentLevelIndex++; // 检查是否还有关卡 - const totalLevels = LevelDataManager.instance.getLevelCount(); + const totalLevels = this._isShareMode + ? ShareManager.instance.getShareLevelCount() + : LevelDataManager.instance.getLevelCount(); if (this.currentLevelIndex >= totalLevels) { // 所有关卡完成 console.log('[PageLevel] 恭喜通关!'); this.stopCountdown(); - ViewManager.instance.back(); + + if (this._isShareMode) { + ShareManager.instance.clearShareMode(); + ViewManager.instance.replace('PageHome'); + } else { + ViewManager.instance.back(); + } return; } diff --git a/assets/prefabs/PageWriteLevels.ts b/assets/prefabs/PageWriteLevels.ts index eea8a67..f0d0525 100644 --- a/assets/prefabs/PageWriteLevels.ts +++ b/assets/prefabs/PageWriteLevels.ts @@ -3,6 +3,7 @@ import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; +import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; const { ccclass, property } = _decorator; /** @@ -78,6 +79,9 @@ export class PageWriteLevels extends BaseView { /** 缓存 view 节点的 UITransform,避免每次 _updateContentSize 重复查找 */ private _viewTransform: UITransform | null = null; + /** 防止重复提交 */ + private _isSubmitting: boolean = false; + onViewLoad(): void { console.log('[PageWriteLevels] onViewLoad'); this._initButtons(); @@ -92,6 +96,9 @@ export class PageWriteLevels extends BaseView { if (this.previewBtn) { this.previewBtn.on(Button.EventType.CLICK, this._onPreviewClick, this); } + if (this.completeBtn) { + this.completeBtn.on(Button.EventType.CLICK, this._onCompleteClick, this); + } } private _initScrollView(): void { @@ -156,7 +163,10 @@ export class PageWriteLevels extends BaseView { onViewShow(): void { console.log('[PageWriteLevels] onViewShow'); - this._initLevelList(); + // 仅首次初始化列表,从预览页返回时保留选中状态 + if (this._itemNodes.length === 0) { + this._initLevelList(); + } } private _initLevelList(): void { @@ -276,6 +286,8 @@ export class PageWriteLevels extends BaseView { const toggle = isSelected.getComponent(Toggle); if (toggle) { toggle.isChecked = this._selectedIndices.has(index); + // 禁用 Toggle 交互,仅作为视觉指示器,选中逻辑由 item Button 统一处理 + toggle.interactable = false; } const checkmark = isSelected.getChildByName('Checkmark'); if (checkmark) { @@ -428,14 +440,22 @@ export class PageWriteLevels extends BaseView { ViewManager.instance.back(); } - private _onPreviewClick(): void { + /** + * 校验是否已选满关卡,未满则 Toast 提示 + * @returns true 表示校验通过 + */ + private _validateSelection(): boolean { if (this._selectedIndices.size < MAX_SELECTION) { const remaining = MAX_SELECTION - this._selectedIndices.size; ToastManager.instance.show(`还需选择${remaining}个关卡`); - return; + return false; } + return true; + } + + private _onPreviewClick(): void { + if (!this._validateSelection()) return; const shareTitle = this.shareTitleEditBox?.getComponent(EditBox)?.string?.trim() || ''; - console.log('[PageWriteLevels] 预览按钮点击,标题:', shareTitle, '已选关卡:', Array.from(this._selectedIndices)); ViewManager.instance.open('PagePreviewLevels', { params: { selectedIndices: Array.from(this._selectedIndices), @@ -444,6 +464,57 @@ export class PageWriteLevels extends BaseView { }); } + private async _onCompleteClick(): Promise { + 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 { console.log('[PageWriteLevels] onViewHide'); } @@ -456,6 +527,9 @@ export class PageWriteLevels extends BaseView { if (this.previewBtn) { this.previewBtn.off(Button.EventType.CLICK, this._onPreviewClick, this); } + if (this.completeBtn) { + this.completeBtn.off(Button.EventType.CLICK, this._onCompleteClick, this); + } if (this.scrollView) { this.scrollView.off(Node.EventType.TOUCH_START, this._onTouchStart, this); this.scrollView.off(Node.EventType.TOUCH_END, this._onTouchEnd, this); diff --git a/assets/scripts/config/ApiConfig.ts b/assets/scripts/config/ApiConfig.ts index 8ec42d2..6925551 100644 --- a/assets/scripts/config/ApiConfig.ts +++ b/assets/scripts/config/ApiConfig.ts @@ -14,8 +14,14 @@ export const API_ENDPOINTS = { USER_ASSETS_EARN: `${API_BASE}/user/assets/earn`, USER_GAME_DATA: `${API_BASE}/user/game-data`, LEVELS: `${API_BASE}/wechat-game/levels`, + SHARE_CREATE: `${API_BASE}/share`, } as const; +/** 构建加入分享的 URL */ +export function getShareJoinUrl(code: string): string { + return `${API_BASE}/share/${code}/join`; +} + /** 积分操作原因 */ export const POINT_REASONS = { HINT_UNLOCK: 'hint_unlock', diff --git a/assets/scripts/types/ApiTypes.ts b/assets/scripts/types/ApiTypes.ts index 31835b5..5973468 100644 --- a/assets/scripts/types/ApiTypes.ts +++ b/assets/scripts/types/ApiTypes.ts @@ -29,3 +29,29 @@ export interface GameData { user: { id: string; points: number }; 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[]; +} diff --git a/assets/scripts/utils/ShareManager.ts b/assets/scripts/utils/ShareManager.ts new file mode 100644 index 0000000..6d9e6ca --- /dev/null +++ b/assets/scripts/utils/ShareManager.ts @@ -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 = 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 { + try { + const response = await HttpUtil.post>( + 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 { + try { + const response = await HttpUtil.post>( + 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 { + 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 { + const cached = this._imageCache.get(url); + if (cached) { + return Promise.resolve(cached); + } + + return new Promise((resolve) => { + assetManager.loadRemote(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); + }); + }); + } +} diff --git a/assets/scripts/utils/ShareManager.ts.meta b/assets/scripts/utils/ShareManager.ts.meta new file mode 100644 index 0000000..e7a7257 --- /dev/null +++ b/assets/scripts/utils/ShareManager.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "6044a8d5-305c-4b68-8abd-6bde3da0505c", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/scripts/utils/WxSDK.ts b/assets/scripts/utils/WxSDK.ts index 1c6ed0b..4c87827 100644 --- a/assets/scripts/utils/WxSDK.ts +++ b/assets/scripts/utils/WxSDK.ts @@ -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; + } }