diff --git a/AGENTS.md b/AGENTS.md index ff5b627..3316318 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ Git 历史采用 Conventional Commits,且摘要多为中文,例如 `feat: # Memory Context -# $CMEM mp-xieyingeng 2026-05-03 10:36pm GMT+8 +# $CMEM mp-xieyingeng 2026-05-10 9:35pm GMT+8 Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖️decision Format: ID TIME TYPE TITLE diff --git a/assets/prefabs/PageLevel.ts b/assets/prefabs/PageLevel.ts index 3ac20c5..c95200a 100644 --- a/assets/prefabs/PageLevel.ts +++ b/assets/prefabs/PageLevel.ts @@ -11,7 +11,7 @@ import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; import { PassModal } from 'db://assets/prefabs/PassModal'; import { WrongModal } from 'db://assets/prefabs/WrongModal'; import { TimeoutModal } from 'db://assets/prefabs/TimeoutModal'; -import { StaminaInfo, NextLevelData } from 'db://assets/scripts/types/ApiTypes'; +import { StaminaInfo, NextLevelData, SubmitShareLevel } from 'db://assets/scripts/types/ApiTypes'; import { AchievementTitleManager } from 'db://assets/scripts/utils/AchievementTitleManager'; import { applyRoundedCorner } from 'db://assets/scripts/utils/roundedMaterial.utils'; const { ccclass, property } = _decorator; @@ -286,6 +286,12 @@ export class PageLevel extends BaseView { /** 分享模式下的关卡索引(仅分享模式使用) */ private _shareLevelIndex: number = 0; + /** 分享模式下每关最终提交内容,等整场结束后一次性提交 */ + private _shareSubmissions: Map = new Map(); + + /** 是否正在提交分享挑战结果 */ + private _isSubmittingShareResult: boolean = false; + /** * 页面首次加载时调用 */ @@ -300,6 +306,8 @@ export class PageLevel extends BaseView { if (this._isShareMode) { this._shareLevelIndex = 0; + this._shareSubmissions.clear(); + this._isSubmittingShareResult = false; console.log('[PageLevel] 进入分享挑战模式'); } else { // 从 AuthManager 获取首关数据(由 PageLoading → game-data 提供) @@ -703,6 +711,7 @@ export class PageLevel extends BaseView { private tryAutoSubmitAnswer(): void { if (!this._currentConfig || this._isTransitioning) return; + if (this._isShareMode) return; const values = this.getInputValues(); const isFilled = values.length === Array.from(this._currentConfig.answer ?? '').length && values.every(value => value.length === 1); @@ -913,8 +922,15 @@ export class PageLevel extends BaseView { if (!this._isShareMode) { return; } + if (this._isSubmittingShareResult) { + return; + } + if (this._isTransitioning) { + return; + } this.playClickSound(); + this._recordCurrentShareSubmission(); void this.goToNextLevel(); } @@ -1639,6 +1655,11 @@ export class PageLevel extends BaseView { const userAnswer = this.getAnswer(); console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${this._currentConfig.answer}`); + if (this._isShareMode) { + void this._submitShareAnswerAndContinue(userAnswer); + return; + } + if (userAnswer === this._currentConfig.answer) { // 答案正确,只播放成功音效(不播放点击音效,避免重合) this.showSuccess(); @@ -1676,11 +1697,15 @@ export class PageLevel extends BaseView { this.reportLevelCompleted(levelId, timeSpent); - // 不论是否有谐音梗,都停留固定时间再弹出通关弹窗,保证节奏一致 + // 不论是否有谐音梗,都停留固定时间,保证玩家能看到答案反馈 await this.delay(PageLevel.PASS_MODAL_DELAY_MS); - if (this._isFinalShareLevel()) { - this._showShareEndPage(); + if (this._isShareMode) { + if (this._isFinalShareLevel()) { + await this._showShareEndPage(); + } else { + await this.goToNextLevel(); + } return; } @@ -1688,6 +1713,24 @@ export class PageLevel extends BaseView { this._showPassModal(); } + private async _submitShareAnswerAndContinue(userAnswer: string): Promise { + if (!this._currentConfig || this._isTransitioning || this._isSubmittingShareResult) { + return; + } + + this._isTransitioning = true; + this.stopCountdown(); + this.hidePunchline(); + this._recordCurrentShareSubmission(userAnswer); + + if (this._isFinalShareLevel()) { + await this._showShareEndPage(); + return; + } + + await this.goToNextLevel(); + } + private getValidPunchline(punchline: string | null): string | null { if (!punchline?.trim()) { return null; @@ -1730,8 +1773,43 @@ export class PageLevel extends BaseView { this._passModalCompletedLevelCount = null; this._passModalPreviousCompletedLevelCount = null; - // fire-and-forget: errors are logged inside reportLevelProgress - void ShareManager.instance.reportLevelProgress(levelId, true, timeSpent); + this._recordCurrentShareSubmission(undefined, timeSpent); + } + + private _recordCurrentShareSubmission(answer?: string, timeSpent?: number): void { + if (!this._isShareMode || !this._currentConfig?.id) { + return; + } + + const elapsedSeconds = Math.max(0, Math.round((Date.now() - this._levelStartTime) / 1000)); + const finalTimeSpent = Math.max(0, Math.round(timeSpent ?? elapsedSeconds)); + const finalAnswer = answer ?? this.getAnswer(); + + this._shareSubmissions.set(this._currentConfig.id, { + levelId: this._currentConfig.id, + answer: finalAnswer, + timeSpent: finalTimeSpent, + }); + + console.log( + `[PageLevel] 记录分享挑战提交: ${this._currentConfig.id}, answer="${finalAnswer}", timeSpent=${finalTimeSpent}`, + ); + } + + private _buildShareSubmissionPayload(): SubmitShareLevel[] { + const levelIds = ShareManager.instance.getShareLevelIds(); + const ids = levelIds.length > 0 + ? levelIds + : [...this._shareSubmissions.keys()]; + + return ids.map(levelId => { + const submission = this._shareSubmissions.get(levelId); + return submission ?? { + levelId, + answer: '', + timeSpent: 0, + }; + }); } private delay(ms: number): Promise { @@ -2021,7 +2099,7 @@ export class PageLevel extends BaseView { if (this._shareLevelIndex >= totalLevels) { console.log('[PageLevel] 分享关卡全部完成'); this.stopCountdown(); - this._showShareEndPage(); + await this._showShareEndPage(); return; } @@ -2056,9 +2134,41 @@ export class PageLevel extends BaseView { return totalLevels > 0 && this._shareLevelIndex >= totalLevels - 1; } - private _showShareEndPage(): void { + private async _showShareEndPage(): Promise { + if (this._isSubmittingShareResult) { + return; + } + console.log('[PageLevel] 分享关卡全部完成,进入 PK 结算页'); this.stopCountdown(); - ViewManager.instance.replace('PagePKEnd'); + + if (this._currentConfig?.id && !this._shareSubmissions.has(this._currentConfig.id)) { + this._recordCurrentShareSubmission(); + } + + const payload = this._buildShareSubmissionPayload(); + if (payload.length === 0) { + ToastManager.show('挑战数据异常,请重新进入'); + this._isTransitioning = false; + return; + } + + this._isSubmittingShareResult = true; + ToastManager.show('正在结算挑战...'); + + const result = await ShareManager.instance.submitShareChallenge(payload); + this._isSubmittingShareResult = false; + + if (!result) { + ToastManager.show('提交挑战结果失败,请稍后重试'); + this._isTransitioning = false; + return; + } + + ViewManager.instance.replace('PagePKEnd', { + params: { + result, + }, + }); } } diff --git a/assets/prefabs/PagePKEnd.prefab b/assets/prefabs/PagePKEnd.prefab index 369406e..a490cd4 100644 --- a/assets/prefabs/PagePKEnd.prefab +++ b/assets/prefabs/PagePKEnd.prefab @@ -40,14 +40,14 @@ "_active": true, "_components": [ { - "__id__": 154 + "__id__": 160 }, { - "__id__": 156 + "__id__": 162 } ], "_prefab": { - "__id__": 158 + "__id__": 164 }, "_lpos": { "__type__": "cc.Vec3", @@ -2258,20 +2258,20 @@ "__id__": 93 }, { - "__id__": 141 + "__id__": 147 } ], "_active": true, "_components": [ { - "__id__": 149 + "__id__": 155 }, { - "__id__": 151 + "__id__": 157 } ], "_prefab": { - "__id__": 153 + "__id__": 159 }, "_lpos": { "__type__": "cc.Vec3", @@ -2318,17 +2318,17 @@ "_active": true, "_components": [ { - "__id__": 134 + "__id__": 140 }, { - "__id__": 136 + "__id__": 142 }, { - "__id__": 138 + "__id__": 144 } ], "_prefab": { - "__id__": 140 + "__id__": 146 }, "_lpos": { "__type__": "cc.Vec3", @@ -2375,11 +2375,11 @@ "_active": true, "_components": [ { - "__id__": 131 + "__id__": 137 } ], "_prefab": { - "__id__": 133 + "__id__": 139 }, "_lpos": { "__type__": "cc.Vec3", @@ -2424,16 +2424,19 @@ }, { "__id__": 108 + }, + { + "__id__": 128 } ], "_active": true, "_components": [ { - "__id__": 128 + "__id__": 134 } ], "_prefab": { - "__id__": 130 + "__id__": 136 }, "_lpos": { "__type__": "cc.Vec3", @@ -3240,6 +3243,168 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.Node", + "_name": "AnswerLabel", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 95 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 129 + }, + { + "__id__": 131 + } + ], + "_prefab": { + "__id__": 133 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 163.106, + "y": -13.134, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.712, + "y": 0.712, + "z": 0.712 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 128 + }, + "_enabled": true, + "__prefab": { + "__id__": 130 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 170, + "height": 136 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "33SRp0SWVHl4kQ1pZ7BQhx" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 128 + }, + "_enabled": true, + "__prefab": { + "__id__": 132 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "答案", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 80, + "_fontSize": 80, + "_fontFamily": "Arial", + "_lineHeight": 100, + "_overflow": 0, + "_enableWrapText": true, + "_font": { + "__uuid__": "fb4acba6-6bc7-4eb3-be34-8f2ac9823a80", + "__expectedType__": "cc.TTFFont" + }, + "_isSystemFontUsed": false, + "_spacingX": 0, + "_isItalic": false, + "_isBold": true, + "_isUnderline": false, + "_underlineHeight": 2, + "_cacheMode": 0, + "_enableOutline": true, + "_outlineColor": { + "__type__": "cc.Color", + "r": 191, + "g": 127, + "b": 2, + "a": 255 + }, + "_outlineWidth": 5, + "_enableShadow": false, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_shadowOffset": { + "__type__": "cc.Vec2", + "x": 2, + "y": 2 + }, + "_shadowBlur": 2, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "940bZjtkVLIYX8EolxrbQ5" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3aNOwLJv5EJa3QIvuuIt/d", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -3250,7 +3415,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 129 + "__id__": 135 }, "_contentSize": { "__type__": "cc.Size", @@ -3291,7 +3456,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 132 + "__id__": 138 }, "_contentSize": { "__type__": "cc.Size", @@ -3332,7 +3497,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 135 + "__id__": 141 }, "_contentSize": { "__type__": "cc.Size", @@ -3360,7 +3525,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 137 + "__id__": 143 }, "_type": 0, "_inverted": false, @@ -3382,7 +3547,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 139 + "__id__": 145 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3443,17 +3608,17 @@ "_active": true, "_components": [ { - "__id__": 142 + "__id__": 148 }, { - "__id__": 144 + "__id__": 150 }, { - "__id__": 146 + "__id__": 152 } ], "_prefab": { - "__id__": 148 + "__id__": 154 }, "_lpos": { "__type__": "cc.Vec3", @@ -3490,11 +3655,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 141 + "__id__": 147 }, "_enabled": true, "__prefab": { - "__id__": 143 + "__id__": 149 }, "_contentSize": { "__type__": "cc.Size", @@ -3518,11 +3683,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 141 + "__id__": 147 }, "_enabled": true, "__prefab": { - "__id__": 145 + "__id__": 151 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3563,11 +3728,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 141 + "__id__": 147 }, "_enabled": true, "__prefab": { - "__id__": 147 + "__id__": 153 }, "_alignFlags": 40, "_target": null, @@ -3616,7 +3781,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 150 + "__id__": 156 }, "_contentSize": { "__type__": "cc.Size", @@ -3644,7 +3809,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 152 + "__id__": 158 }, "bounceDuration": 0.23, "brake": 0.75, @@ -3688,7 +3853,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 155 + "__id__": 161 }, "_contentSize": { "__type__": "cc.Size", @@ -3716,9 +3881,32 @@ }, "_enabled": true, "__prefab": { - "__id__": 157 + "__id__": 163 + }, + "settingButton": { + "__id__": 76 + }, + "rankLabel": { + "__id__": 47 + }, + "rightNumberLabel": { + "__id__": 53 + }, + "rankNumberLabel": { + "__id__": 59 + }, + "participateNumberLabel": { + "__id__": 65 + }, + "answerTitleLabel": { + "__id__": 89 + }, + "answerListContent": { + "__id__": 94 + }, + "answerItemTemplate": { + "__id__": 95 }, - "settingButton": null, "_id": "" }, { diff --git a/assets/prefabs/PagePKEnd.ts b/assets/prefabs/PagePKEnd.ts index 7486a06..09a8c55 100644 --- a/assets/prefabs/PagePKEnd.ts +++ b/assets/prefabs/PagePKEnd.ts @@ -1,25 +1,61 @@ -import { _decorator, Button, Node } from 'cc'; +import { _decorator, assetManager, Button, ImageAsset, instantiate, Label, Node, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform } from 'cc'; import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; +import { SubmitShareData, SubmittedShareLevelData } from 'db://assets/scripts/types/ApiTypes'; const { ccclass, property } = _decorator; @ccclass('PagePKEnd') export class PagePKEnd extends BaseView { + private static readonly ANSWER_ITEM_TOP_PADDING = 16; + private static readonly ANSWER_ITEM_BOTTOM_PADDING = 16; + private static readonly ANSWER_ITEM_SPACING = 16; + private static readonly COVER_IMAGE_WIDTH = 1299; + private static readonly COVER_IMAGE_HEIGHT = 1004; + @property({ type: Node, tooltip: '返回首页按钮' }) settingButton: Node | null = null; + @property({ type: Label, tooltip: '顶部排名文案,例如:获得了第1名' }) + rankLabel: Label | null = null; + + @property({ type: Label, tooltip: '答对题数文案,例如:答对了4题' }) + rightNumberLabel: Label | null = null; + + @property({ type: Label, tooltip: '本人排名文案,例如:您获得了第1名' }) + rankNumberLabel: Label | null = null; + + @property({ type: Label, tooltip: '参与人数文案,例如:一共66人参与了挑战' }) + participateNumberLabel: Label | null = null; + + @property({ type: Label, tooltip: '答案列表标题' }) + answerTitleLabel: Label | null = null; + + @property({ type: Node, tooltip: '答案列表 content 节点' }) + answerListContent: Node | null = null; + + @property({ type: Node, tooltip: '答案列表条目模板 AnswerItem' }) + answerItemTemplate: Node | null = null; + + private _answerItemNodes: Node[] = []; + private _answerButtonBindings: Array<{ node: Node; handler: () => void }> = []; + private _renderVersion: number = 0; + onViewLoad(): void { this._resolveNodes(); this._bindEvents(); + this._hideAnswerTemplate(); } onViewShow(): void { console.log('[PagePKEnd] onViewShow'); + this._resolveNodes(); + this._renderResult(this.getParams()?.result ?? null); } onViewDestroy(): void { this._unbindEvents(); + this._clearAnswerItems(); } private _resolveNodes(): void { @@ -27,9 +63,28 @@ export class PagePKEnd extends BaseView { this.settingButton = this.node.getChildByName('SettingButton'); } + this.rankLabel = this.rankLabel ?? this._findLabel('RankLabel'); + this.rightNumberLabel = this.rightNumberLabel ?? this._findLabel('RightNumberLabel'); + this.rankNumberLabel = this.rankNumberLabel ?? this._findLabel('RankNumberLabel'); + this.participateNumberLabel = this.participateNumberLabel ?? this._findLabel('PartipateNumberLabel'); + this.answerTitleLabel = this.answerTitleLabel ?? this._findLabel('AnswerTitle'); + + const answerList = this.node.getChildByName('AnswerList'); + const view = answerList?.getChildByName('view'); + this.answerListContent = this.answerListContent ?? view?.getChildByName('content') ?? null; + this.answerItemTemplate = this.answerItemTemplate + ?? this.answerListContent?.getChildByName('AnswerItem') + ?? null; + if (!this.settingButton) { console.warn('[PagePKEnd] 未找到 SettingButton 节点'); } + if (!this.answerListContent) { + console.warn('[PagePKEnd] 未找到 AnswerList/content 节点'); + } + if (!this.answerItemTemplate) { + console.warn('[PagePKEnd] 未找到 AnswerItem 模板节点'); + } } private _bindEvents(): void { @@ -46,10 +101,236 @@ export class PagePKEnd extends BaseView { if (this.settingButton && this.settingButton.isValid) { this.settingButton.off(Button.EventType.CLICK, this._onHomeClick, this); } + this._unbindAnswerButtons(); } private _onHomeClick(): void { ShareManager.instance.clearShareMode(); ViewManager.instance.replace('PageHome'); } + + private _renderResult(result: SubmitShareData | null): void { + this._renderVersion++; + this._clearAnswerItems(); + + if (!result) { + this._setLabel(this.rankLabel, '暂无排名'); + this._setLabel(this.rightNumberLabel, '答对了0题'); + this._setLabel(this.rankNumberLabel, '您暂未上榜'); + this._setLabel(this.participateNumberLabel, '暂无参与数据'); + this._setLabel(this.answerTitleLabel, '暂无挑战结果'); + this._hideAnswerTemplate(); + return; + } + + this._setLabel(this.rankLabel, `获得了第${result.rank}名`); + this._setLabel(this.rightNumberLabel, `答对了${result.correctCount}题`); + this._setLabel(this.rankNumberLabel, `您获得了第${result.rank}名`); + this._setLabel(this.participateNumberLabel, `一共${result.participantCount}人参与了挑战`); + this._setLabel(this.answerTitleLabel, `共用时${result.totalTimeSpent}s,揭晓答案吧`); + this._renderAnswerList(result.levels ?? []); + } + + private _renderAnswerList(levels: SubmittedShareLevelData[]): void { + if (!this.answerListContent || !this.answerItemTemplate) { + return; + } + + this._hideAnswerTemplate(); + const version = this._renderVersion; + this._layoutAnswerContent(levels.length); + + levels.forEach((level, index) => { + const item = instantiate(this.answerItemTemplate!); + item.name = `AnswerItem_${index + 1}`; + item.active = true; + this.answerListContent!.addChild(item); + this._answerItemNodes.push(item); + this._positionAnswerItem(item, index); + + this._applyAnswerState(item, level); + + const coverSprite = this._findChild(item, 'CoverImage')?.getComponent(Sprite) ?? null; + this._prepareCoverSprite(coverSprite); + this._loadCoverImage(level.image1Url, coverSprite, version); + }); + + this._scrollAnswerListToTop(); + } + + private _layoutAnswerContent(itemCount: number): void { + if (!this.answerListContent || !this.answerItemTemplate) { + return; + } + + const contentTransform = this.answerListContent.getComponent(UITransform); + const viewTransform = this.answerListContent.parent?.getComponent(UITransform) ?? null; + const itemTransform = this.answerItemTemplate.getComponent(UITransform); + if (!contentTransform || !viewTransform || !itemTransform) { + return; + } + + const itemHeight = itemTransform.height; + const contentHeight = Math.max( + viewTransform.height, + PagePKEnd.ANSWER_ITEM_TOP_PADDING + + PagePKEnd.ANSWER_ITEM_BOTTOM_PADDING + + itemCount * itemHeight + + Math.max(0, itemCount - 1) * PagePKEnd.ANSWER_ITEM_SPACING, + ); + + contentTransform.setContentSize(contentTransform.width, contentHeight); + this.answerListContent.setPosition( + this.answerListContent.position.x, + viewTransform.height / 2, + this.answerListContent.position.z, + ); + } + + private _positionAnswerItem(item: Node, index: number): void { + const itemTransform = item.getComponent(UITransform); + if (!itemTransform) { + return; + } + + const y = -PagePKEnd.ANSWER_ITEM_TOP_PADDING + - itemTransform.height / 2 + - index * (itemTransform.height + PagePKEnd.ANSWER_ITEM_SPACING); + item.setPosition(0, y, item.position.z); + } + + private _applyAnswerState(item: Node, level: SubmittedShareLevelData): void { + const answerButton = this._findChild(item, 'ButtonViewAnswer'); + const buttonLabel = answerButton?.getChildByName('Label')?.getComponent(Label) ?? null; + const answerLabelNode = this._findChild(item, 'AnswerLabel'); + const answerLabel = answerLabelNode?.getComponent(Label) ?? null; + + this._setLabel(buttonLabel, '查看答案'); + this._setLabel(answerLabel, level.answer || '-'); + + if (level.isCorrect) { + if (answerButton) { + answerButton.active = false; + } + if (answerLabelNode) { + answerLabelNode.active = true; + } + return; + } + + if (answerButton) { + answerButton.active = true; + } + if (answerLabelNode) { + answerLabelNode.active = false; + } + + const handler = () => { + if (answerButton?.isValid) { + answerButton.active = false; + } + if (answerLabelNode?.isValid) { + answerLabelNode.active = true; + } + }; + if (answerButton) { + answerButton.on(Button.EventType.CLICK, handler, this); + this._answerButtonBindings.push({ node: answerButton, handler }); + } + } + + private _scrollAnswerListToTop(): void { + const scrollView = this.node.getChildByName('AnswerList')?.getComponent(ScrollView); + scrollView?.scrollToTop(0); + } + + private _loadCoverImage(url: string, sprite: Sprite | null, version: number): void { + if (!url || !sprite) { + return; + } + + this._prepareCoverSprite(sprite); + assetManager.loadRemote(url, (err, imageAsset) => { + if (err || !imageAsset || version !== this._renderVersion || !sprite.node.isValid) { + if (err) { + console.error('[PagePKEnd] 加载答案封面失败:', url, err); + } + return; + } + + const texture = new Texture2D(); + texture.image = imageAsset; + const spriteFrame = new SpriteFrame(); + spriteFrame.texture = texture; + this._prepareCoverSprite(sprite); + sprite.spriteFrame = spriteFrame; + this._prepareCoverSprite(sprite); + }); + } + + private _prepareCoverSprite(sprite: Sprite | null): void { + if (!sprite?.node.isValid) { + return; + } + + sprite.sizeMode = Sprite.SizeMode.CUSTOM; + const transform = sprite.node.getComponent(UITransform); + if (transform) { + transform.setContentSize(PagePKEnd.COVER_IMAGE_WIDTH, PagePKEnd.COVER_IMAGE_HEIGHT); + } + sprite.node.setScale(0.242, 0.242, 0.242); + } + + private _clearAnswerItems(): void { + this._unbindAnswerButtons(); + + for (const item of this._answerItemNodes) { + if (item.isValid) { + item.removeFromParent(); + item.destroy(); + } + } + this._answerItemNodes = []; + this._hideAnswerTemplate(); + } + + private _unbindAnswerButtons(): void { + for (const binding of this._answerButtonBindings) { + if (binding.node.isValid) { + binding.node.off(Button.EventType.CLICK, binding.handler, this); + } + } + this._answerButtonBindings = []; + } + + private _hideAnswerTemplate(): void { + if (this.answerItemTemplate?.isValid) { + this.answerItemTemplate.active = false; + } + } + + private _setLabel(label: Label | null, text: string): void { + if (label) { + label.string = text; + } + } + + private _findLabel(nodeName: string): Label | null { + return this._findChild(this.node, nodeName)?.getComponent(Label) ?? null; + } + + private _findChild(root: Node, nodeName: string): Node | null { + if (root.name === nodeName) { + return root; + } + + for (const child of root.children) { + const found = this._findChild(child, nodeName); + if (found) { + return found; + } + } + + return null; + } } diff --git a/assets/scripts/config/ApiConfig.ts b/assets/scripts/config/ApiConfig.ts index 329abde..d9e24a4 100644 --- a/assets/scripts/config/ApiConfig.ts +++ b/assets/scripts/config/ApiConfig.ts @@ -19,7 +19,6 @@ export const API_ENDPOINTS = { /** 分享相关 */ SHARE_CREATE: `${API_BASE}/share`, SHARE_CREATED: `${API_BASE}/share/created`, - SHARE_PROGRESS: `${API_BASE}/share/progress`, /** 用户信息 */ USER_INFO: `${API_BASE}/user/info`, /** 用户所有已通关的关卡(成就墙 / 关卡回看) */ @@ -38,6 +37,10 @@ export function getShareJoinUrl(code: string): string { return `${API_BASE}/share/${code}/join`; } +export function getShareSubmitUrl(code: string): string { + return `${API_BASE}/share/${code}/submit`; +} + export function getGameConfigUrl(key: string): string { return `${API_BASE}/game-configs/${key}`; } diff --git a/assets/scripts/types/ApiTypes.ts b/assets/scripts/types/ApiTypes.ts index 88204e1..cab4b27 100644 --- a/assets/scripts/types/ApiTypes.ts +++ b/assets/scripts/types/ApiTypes.ts @@ -134,13 +134,34 @@ export interface JoinShareData { levels: ShareLevelData[]; } -/** 上报关卡进度响应 */ -export interface ReportProgressData { - passed: boolean; +/** 分享挑战单关提交 */ +export interface SubmitShareLevel { + levelId: string; + answer: string; + timeSpent: number; +} + +/** 分享挑战提交后的单关结果 */ +export interface SubmittedShareLevelData extends ShareLevelData { + submittedAnswer: string; + timeSpent: number; + isCorrect: boolean; timeLimit: number | null; withinTimeLimit: boolean; } +/** 分享挑战整场提交响应 */ +export interface SubmitShareData { + shareCode: string; + title: string; + rank: number; + correctCount: number; + levelCount: number; + participantCount: number; + totalTimeSpent: number; + levels: SubmittedShareLevelData[]; +} + /** 我创建的分享挑战条目 */ export interface CreatedShareItem { id: string; diff --git a/assets/scripts/utils/ShareManager.ts b/assets/scripts/utils/ShareManager.ts index 69447b1..778b8c6 100644 --- a/assets/scripts/utils/ShareManager.ts +++ b/assets/scripts/utils/ShareManager.ts @@ -1,15 +1,16 @@ 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 { API_ENDPOINTS, getShareJoinUrl, getShareSubmitUrl, API_TIMEOUT } from '../config/ApiConfig'; import { ApiEnvelope, CreateShareData, JoinShareData, - ReportProgressData, ShareLevelData, CreatedShareItem, CreatedShareListData, + SubmitShareData, + SubmitShareLevel, } from '../types/ApiTypes'; import { RuntimeLevelConfig } from '../types/LevelTypes'; @@ -50,6 +51,14 @@ export class ShareManager { return [...this._createdShares]; } + get shareCode(): string | null { + return this._shareCode; + } + + get shareTitle(): string { + return this._shareTitle; + } + async createShare(title: string, levelIds: string[]): Promise { try { const response = await HttpUtil.post>( @@ -101,6 +110,7 @@ export class ShareManager { clue3: level.hint3, answer: level.answer, completed: false, + timeLimit: null, })); // 预加载首关图片(两张并行加载) @@ -178,40 +188,34 @@ export class ShareManager { return this._shareLevels?.length ?? 0; } - /** - * 上报单关通关进度 - * @param levelId 关卡 ID - * @param passed 是否通过 - * @param timeSpent 用时(秒) - */ - async reportLevelProgress( - levelId: string, - passed: boolean, - timeSpent: number, - ): Promise { + getShareLevelIds(): string[] { + return this._shareApiLevels.map(level => level.id); + } + + async submitShareChallenge(levels: SubmitShareLevel[]): Promise { if (!this._shareCode) { - console.warn('[ShareManager] reportLevelProgress: 无分享码,跳过上报'); + console.warn('[ShareManager] submitShareChallenge: 无分享码,跳过提交'); return null; } try { - const response = await HttpUtil.post>( - API_ENDPOINTS.SHARE_PROGRESS, - { shareCode: this._shareCode, levelId, passed, timeSpent }, + const response = await HttpUtil.post>( + getShareSubmitUrl(this._shareCode), + { levels }, API_TIMEOUT.DEFAULT, ); if (!response.success || !response.data) { - console.error('[ShareManager] 上报进度失败:', response.message); + console.error('[ShareManager] 提交挑战结果失败:', response.message); return null; } console.log( - `[ShareManager] 上报成功: passed=${response.data.passed}, withinTimeLimit=${response.data.withinTimeLimit}`, + `[ShareManager] 提交挑战结果成功: rank=${response.data.rank}, correct=${response.data.correctCount}/${response.data.levelCount}`, ); return response.data; } catch (err) { - console.error('[ShareManager] 上报进度异常:', err); + console.error('[ShareManager] 提交挑战结果异常:', err); return null; } }