feat: 完善分享模式

This commit is contained in:
richarjiang
2026-05-10 21:38:10 +08:00
parent b68e32ddce
commit c53eac6b24
7 changed files with 677 additions and 70 deletions

View File

@@ -40,7 +40,7 @@ Git 历史采用 Conventional Commits且摘要多为中文例如 `feat:
<claude-mem-context> <claude-mem-context>
# Memory Context # 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 Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖decision
Format: ID TIME TYPE TITLE Format: ID TIME TYPE TITLE

View File

@@ -11,7 +11,7 @@ import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
import { PassModal } from 'db://assets/prefabs/PassModal'; import { PassModal } from 'db://assets/prefabs/PassModal';
import { WrongModal } from 'db://assets/prefabs/WrongModal'; import { WrongModal } from 'db://assets/prefabs/WrongModal';
import { TimeoutModal } from 'db://assets/prefabs/TimeoutModal'; 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 { AchievementTitleManager } from 'db://assets/scripts/utils/AchievementTitleManager';
import { applyRoundedCorner } from 'db://assets/scripts/utils/roundedMaterial.utils'; import { applyRoundedCorner } from 'db://assets/scripts/utils/roundedMaterial.utils';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
@@ -286,6 +286,12 @@ export class PageLevel extends BaseView {
/** 分享模式下的关卡索引(仅分享模式使用) */ /** 分享模式下的关卡索引(仅分享模式使用) */
private _shareLevelIndex: number = 0; private _shareLevelIndex: number = 0;
/** 分享模式下每关最终提交内容,等整场结束后一次性提交 */
private _shareSubmissions: Map<string, SubmitShareLevel> = new Map();
/** 是否正在提交分享挑战结果 */
private _isSubmittingShareResult: boolean = false;
/** /**
* 页面首次加载时调用 * 页面首次加载时调用
*/ */
@@ -300,6 +306,8 @@ export class PageLevel extends BaseView {
if (this._isShareMode) { if (this._isShareMode) {
this._shareLevelIndex = 0; this._shareLevelIndex = 0;
this._shareSubmissions.clear();
this._isSubmittingShareResult = false;
console.log('[PageLevel] 进入分享挑战模式'); console.log('[PageLevel] 进入分享挑战模式');
} else { } else {
// 从 AuthManager 获取首关数据(由 PageLoading → game-data 提供) // 从 AuthManager 获取首关数据(由 PageLoading → game-data 提供)
@@ -703,6 +711,7 @@ export class PageLevel extends BaseView {
private tryAutoSubmitAnswer(): void { private tryAutoSubmitAnswer(): void {
if (!this._currentConfig || this._isTransitioning) return; if (!this._currentConfig || this._isTransitioning) return;
if (this._isShareMode) return;
const values = this.getInputValues(); const values = this.getInputValues();
const isFilled = values.length === Array.from(this._currentConfig.answer ?? '').length && values.every(value => value.length === 1); 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) { if (!this._isShareMode) {
return; return;
} }
if (this._isSubmittingShareResult) {
return;
}
if (this._isTransitioning) {
return;
}
this.playClickSound(); this.playClickSound();
this._recordCurrentShareSubmission();
void this.goToNextLevel(); void this.goToNextLevel();
} }
@@ -1639,6 +1655,11 @@ export class PageLevel extends BaseView {
const userAnswer = this.getAnswer(); const userAnswer = this.getAnswer();
console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${this._currentConfig.answer}`); console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${this._currentConfig.answer}`);
if (this._isShareMode) {
void this._submitShareAnswerAndContinue(userAnswer);
return;
}
if (userAnswer === this._currentConfig.answer) { if (userAnswer === this._currentConfig.answer) {
// 答案正确,只播放成功音效(不播放点击音效,避免重合) // 答案正确,只播放成功音效(不播放点击音效,避免重合)
this.showSuccess(); this.showSuccess();
@@ -1676,11 +1697,15 @@ export class PageLevel extends BaseView {
this.reportLevelCompleted(levelId, timeSpent); this.reportLevelCompleted(levelId, timeSpent);
// 不论是否有谐音梗,都停留固定时间再弹出通关弹窗,保证节奏一致 // 不论是否有谐音梗,都停留固定时间,保证玩家能看到答案反馈
await this.delay(PageLevel.PASS_MODAL_DELAY_MS); await this.delay(PageLevel.PASS_MODAL_DELAY_MS);
if (this._isShareMode) {
if (this._isFinalShareLevel()) { if (this._isFinalShareLevel()) {
this._showShareEndPage(); await this._showShareEndPage();
} else {
await this.goToNextLevel();
}
return; return;
} }
@@ -1688,6 +1713,24 @@ export class PageLevel extends BaseView {
this._showPassModal(); this._showPassModal();
} }
private async _submitShareAnswerAndContinue(userAnswer: string): Promise<void> {
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 { private getValidPunchline(punchline: string | null): string | null {
if (!punchline?.trim()) { if (!punchline?.trim()) {
return null; return null;
@@ -1730,8 +1773,43 @@ export class PageLevel extends BaseView {
this._passModalCompletedLevelCount = null; this._passModalCompletedLevelCount = null;
this._passModalPreviousCompletedLevelCount = null; this._passModalPreviousCompletedLevelCount = null;
// fire-and-forget: errors are logged inside reportLevelProgress this._recordCurrentShareSubmission(undefined, timeSpent);
void ShareManager.instance.reportLevelProgress(levelId, true, 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<void> { private delay(ms: number): Promise<void> {
@@ -2021,7 +2099,7 @@ export class PageLevel extends BaseView {
if (this._shareLevelIndex >= totalLevels) { if (this._shareLevelIndex >= totalLevels) {
console.log('[PageLevel] 分享关卡全部完成'); console.log('[PageLevel] 分享关卡全部完成');
this.stopCountdown(); this.stopCountdown();
this._showShareEndPage(); await this._showShareEndPage();
return; return;
} }
@@ -2056,9 +2134,41 @@ export class PageLevel extends BaseView {
return totalLevels > 0 && this._shareLevelIndex >= totalLevels - 1; return totalLevels > 0 && this._shareLevelIndex >= totalLevels - 1;
} }
private _showShareEndPage(): void { private async _showShareEndPage(): Promise<void> {
if (this._isSubmittingShareResult) {
return;
}
console.log('[PageLevel] 分享关卡全部完成,进入 PK 结算页'); console.log('[PageLevel] 分享关卡全部完成,进入 PK 结算页');
this.stopCountdown(); 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,
},
});
} }
} }

View File

@@ -40,14 +40,14 @@
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 154 "__id__": 160
}, },
{ {
"__id__": 156 "__id__": 162
} }
], ],
"_prefab": { "_prefab": {
"__id__": 158 "__id__": 164
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -2258,20 +2258,20 @@
"__id__": 93 "__id__": 93
}, },
{ {
"__id__": 141 "__id__": 147
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 149 "__id__": 155
}, },
{ {
"__id__": 151 "__id__": 157
} }
], ],
"_prefab": { "_prefab": {
"__id__": 153 "__id__": 159
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -2318,17 +2318,17 @@
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 134 "__id__": 140
}, },
{ {
"__id__": 136 "__id__": 142
}, },
{ {
"__id__": 138 "__id__": 144
} }
], ],
"_prefab": { "_prefab": {
"__id__": 140 "__id__": 146
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -2375,11 +2375,11 @@
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 131 "__id__": 137
} }
], ],
"_prefab": { "_prefab": {
"__id__": 133 "__id__": 139
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -2424,16 +2424,19 @@
}, },
{ {
"__id__": 108 "__id__": 108
},
{
"__id__": 128
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 128 "__id__": 134
} }
], ],
"_prefab": { "_prefab": {
"__id__": 130 "__id__": 136
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -3240,6 +3243,168 @@
"targetOverrides": null, "targetOverrides": null,
"nestedPrefabInstanceRoots": 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", "__type__": "cc.UITransform",
"_name": "", "_name": "",
@@ -3250,7 +3415,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 129 "__id__": 135
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
@@ -3291,7 +3456,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 132 "__id__": 138
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
@@ -3332,7 +3497,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 135 "__id__": 141
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
@@ -3360,7 +3525,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 137 "__id__": 143
}, },
"_type": 0, "_type": 0,
"_inverted": false, "_inverted": false,
@@ -3382,7 +3547,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 139 "__id__": 145
}, },
"_customMaterial": null, "_customMaterial": null,
"_srcBlendFactor": 2, "_srcBlendFactor": 2,
@@ -3443,17 +3608,17 @@
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 142 "__id__": 148
}, },
{ {
"__id__": 144 "__id__": 150
}, },
{ {
"__id__": 146 "__id__": 152
} }
], ],
"_prefab": { "_prefab": {
"__id__": 148 "__id__": 154
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -3490,11 +3655,11 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 141 "__id__": 147
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 143 "__id__": 149
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
@@ -3518,11 +3683,11 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 141 "__id__": 147
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 145 "__id__": 151
}, },
"_customMaterial": null, "_customMaterial": null,
"_srcBlendFactor": 2, "_srcBlendFactor": 2,
@@ -3563,11 +3728,11 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 141 "__id__": 147
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 147 "__id__": 153
}, },
"_alignFlags": 40, "_alignFlags": 40,
"_target": null, "_target": null,
@@ -3616,7 +3781,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 150 "__id__": 156
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
@@ -3644,7 +3809,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 152 "__id__": 158
}, },
"bounceDuration": 0.23, "bounceDuration": 0.23,
"brake": 0.75, "brake": 0.75,
@@ -3688,7 +3853,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 155 "__id__": 161
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
@@ -3716,9 +3881,32 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__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": "" "_id": ""
}, },
{ {

View File

@@ -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 { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
import { SubmitShareData, SubmittedShareLevelData } from 'db://assets/scripts/types/ApiTypes';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
@ccclass('PagePKEnd') @ccclass('PagePKEnd')
export class PagePKEnd extends BaseView { 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: '返回首页按钮' }) @property({ type: Node, tooltip: '返回首页按钮' })
settingButton: Node | null = null; 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 { onViewLoad(): void {
this._resolveNodes(); this._resolveNodes();
this._bindEvents(); this._bindEvents();
this._hideAnswerTemplate();
} }
onViewShow(): void { onViewShow(): void {
console.log('[PagePKEnd] onViewShow'); console.log('[PagePKEnd] onViewShow');
this._resolveNodes();
this._renderResult(this.getParams()?.result ?? null);
} }
onViewDestroy(): void { onViewDestroy(): void {
this._unbindEvents(); this._unbindEvents();
this._clearAnswerItems();
} }
private _resolveNodes(): void { private _resolveNodes(): void {
@@ -27,9 +63,28 @@ export class PagePKEnd extends BaseView {
this.settingButton = this.node.getChildByName('SettingButton'); 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) { if (!this.settingButton) {
console.warn('[PagePKEnd] 未找到 SettingButton 节点'); console.warn('[PagePKEnd] 未找到 SettingButton 节点');
} }
if (!this.answerListContent) {
console.warn('[PagePKEnd] 未找到 AnswerList/content 节点');
}
if (!this.answerItemTemplate) {
console.warn('[PagePKEnd] 未找到 AnswerItem 模板节点');
}
} }
private _bindEvents(): void { private _bindEvents(): void {
@@ -46,10 +101,236 @@ export class PagePKEnd extends BaseView {
if (this.settingButton && this.settingButton.isValid) { if (this.settingButton && this.settingButton.isValid) {
this.settingButton.off(Button.EventType.CLICK, this._onHomeClick, this); this.settingButton.off(Button.EventType.CLICK, this._onHomeClick, this);
} }
this._unbindAnswerButtons();
} }
private _onHomeClick(): void { private _onHomeClick(): void {
ShareManager.instance.clearShareMode(); ShareManager.instance.clearShareMode();
ViewManager.instance.replace('PageHome'); 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<ImageAsset>(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;
}
} }

View File

@@ -19,7 +19,6 @@ export const API_ENDPOINTS = {
/** 分享相关 */ /** 分享相关 */
SHARE_CREATE: `${API_BASE}/share`, SHARE_CREATE: `${API_BASE}/share`,
SHARE_CREATED: `${API_BASE}/share/created`, SHARE_CREATED: `${API_BASE}/share/created`,
SHARE_PROGRESS: `${API_BASE}/share/progress`,
/** 用户信息 */ /** 用户信息 */
USER_INFO: `${API_BASE}/user/info`, USER_INFO: `${API_BASE}/user/info`,
/** 用户所有已通关的关卡(成就墙 / 关卡回看) */ /** 用户所有已通关的关卡(成就墙 / 关卡回看) */
@@ -38,6 +37,10 @@ export function getShareJoinUrl(code: string): string {
return `${API_BASE}/share/${code}/join`; return `${API_BASE}/share/${code}/join`;
} }
export function getShareSubmitUrl(code: string): string {
return `${API_BASE}/share/${code}/submit`;
}
export function getGameConfigUrl(key: string): string { export function getGameConfigUrl(key: string): string {
return `${API_BASE}/game-configs/${key}`; return `${API_BASE}/game-configs/${key}`;
} }

View File

@@ -134,13 +134,34 @@ export interface JoinShareData {
levels: ShareLevelData[]; levels: ShareLevelData[];
} }
/** 上报关卡进度响应 */ /** 分享挑战单关提交 */
export interface ReportProgressData { export interface SubmitShareLevel {
passed: boolean; levelId: string;
answer: string;
timeSpent: number;
}
/** 分享挑战提交后的单关结果 */
export interface SubmittedShareLevelData extends ShareLevelData {
submittedAnswer: string;
timeSpent: number;
isCorrect: boolean;
timeLimit: number | null; timeLimit: number | null;
withinTimeLimit: boolean; withinTimeLimit: boolean;
} }
/** 分享挑战整场提交响应 */
export interface SubmitShareData {
shareCode: string;
title: string;
rank: number;
correctCount: number;
levelCount: number;
participantCount: number;
totalTimeSpent: number;
levels: SubmittedShareLevelData[];
}
/** 我创建的分享挑战条目 */ /** 我创建的分享挑战条目 */
export interface CreatedShareItem { export interface CreatedShareItem {
id: string; id: string;

View File

@@ -1,15 +1,16 @@
import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc'; import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc';
import { HttpUtil } from './HttpUtil'; import { HttpUtil } from './HttpUtil';
import { WxSDK } from './WxSDK'; import { WxSDK } from './WxSDK';
import { API_ENDPOINTS, getShareJoinUrl, API_TIMEOUT } from '../config/ApiConfig'; import { API_ENDPOINTS, getShareJoinUrl, getShareSubmitUrl, API_TIMEOUT } from '../config/ApiConfig';
import { import {
ApiEnvelope, ApiEnvelope,
CreateShareData, CreateShareData,
JoinShareData, JoinShareData,
ReportProgressData,
ShareLevelData, ShareLevelData,
CreatedShareItem, CreatedShareItem,
CreatedShareListData, CreatedShareListData,
SubmitShareData,
SubmitShareLevel,
} from '../types/ApiTypes'; } from '../types/ApiTypes';
import { RuntimeLevelConfig } from '../types/LevelTypes'; import { RuntimeLevelConfig } from '../types/LevelTypes';
@@ -50,6 +51,14 @@ export class ShareManager {
return [...this._createdShares]; return [...this._createdShares];
} }
get shareCode(): string | null {
return this._shareCode;
}
get shareTitle(): string {
return this._shareTitle;
}
async createShare(title: string, levelIds: string[]): Promise<string | null> { async createShare(title: string, levelIds: string[]): Promise<string | null> {
try { try {
const response = await HttpUtil.post<ApiEnvelope<CreateShareData>>( const response = await HttpUtil.post<ApiEnvelope<CreateShareData>>(
@@ -101,6 +110,7 @@ export class ShareManager {
clue3: level.hint3, clue3: level.hint3,
answer: level.answer, answer: level.answer,
completed: false, completed: false,
timeLimit: null,
})); }));
// 预加载首关图片(两张并行加载) // 预加载首关图片(两张并行加载)
@@ -178,40 +188,34 @@ export class ShareManager {
return this._shareLevels?.length ?? 0; return this._shareLevels?.length ?? 0;
} }
/** getShareLevelIds(): string[] {
* 上报单关通关进度 return this._shareApiLevels.map(level => level.id);
* @param levelId 关卡 ID }
* @param passed 是否通过
* @param timeSpent 用时(秒) async submitShareChallenge(levels: SubmitShareLevel[]): Promise<SubmitShareData | null> {
*/
async reportLevelProgress(
levelId: string,
passed: boolean,
timeSpent: number,
): Promise<ReportProgressData | null> {
if (!this._shareCode) { if (!this._shareCode) {
console.warn('[ShareManager] reportLevelProgress: 无分享码,跳过上报'); console.warn('[ShareManager] submitShareChallenge: 无分享码,跳过提交');
return null; return null;
} }
try { try {
const response = await HttpUtil.post<ApiEnvelope<ReportProgressData>>( const response = await HttpUtil.post<ApiEnvelope<SubmitShareData>>(
API_ENDPOINTS.SHARE_PROGRESS, getShareSubmitUrl(this._shareCode),
{ shareCode: this._shareCode, levelId, passed, timeSpent }, { levels },
API_TIMEOUT.DEFAULT, API_TIMEOUT.DEFAULT,
); );
if (!response.success || !response.data) { if (!response.success || !response.data) {
console.error('[ShareManager] 上报进度失败:', response.message); console.error('[ShareManager] 提交挑战结果失败:', response.message);
return null; return null;
} }
console.log( 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; return response.data;
} catch (err) { } catch (err) {
console.error('[ShareManager] 上报进度异常:', err); console.error('[ShareManager] 提交挑战结果异常:', err);
return null; return null;
} }
} }