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>
# 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

View File

@@ -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<string, SubmitShareLevel> = 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<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 {
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<void> {
@@ -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<void> {
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,
},
});
}
}

View File

@@ -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": ""
},
{

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 { 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<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_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}`;
}

View File

@@ -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;

View File

@@ -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<string | null> {
try {
const response = await HttpUtil.post<ApiEnvelope<CreateShareData>>(
@@ -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<ReportProgressData | null> {
getShareLevelIds(): string[] {
return this._shareApiLevels.map(level => level.id);
}
async submitShareChallenge(levels: SubmitShareLevel[]): Promise<SubmitShareData | null> {
if (!this._shareCode) {
console.warn('[ShareManager] reportLevelProgress: 无分享码,跳过上报');
console.warn('[ShareManager] submitShareChallenge: 无分享码,跳过提交');
return null;
}
try {
const response = await HttpUtil.post<ApiEnvelope<ReportProgressData>>(
API_ENDPOINTS.SHARE_PROGRESS,
{ shareCode: this._shareCode, levelId, passed, timeSpent },
const response = await HttpUtil.post<ApiEnvelope<SubmitShareData>>(
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;
}
}