feat: 更新 PagePKData 组件,添加挑战参与者排行摘要支持

This commit is contained in:
richarjiang
2026-05-12 21:37:13 +08:00
parent b7dec68dd6
commit 1cbc25481b
4 changed files with 297 additions and 78 deletions

120
AGENTS.md
View File

@@ -40,78 +40,62 @@ Git 历史采用 Conventional Commits且摘要多为中文例如 `feat:
<claude-mem-context> <claude-mem-context>
# Memory Context # Memory Context
# [mp-xieyingeng] recent context, 2026-05-12 7:49pm GMT+8 # $CMEM mp-xieyingeng 2026-05-12 9:28pm GMT+8
Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖decision 🚨security_alert 🔐security_note Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖decision
Format: ID TIME TYPE TITLE Format: ID TIME TYPE TITLE
Fetch details: get_observations([IDs]) | Search: mem-search skill Fetch details: get_observations([IDs]) | Search: mem-search skill
Stats: 50 obs (11,836t read) | 1,674,484t work | 99% savings Stats: 47 obs (10,663t read) | 538,681t work | 98% savings
### Apr 26, 2026 ### Apr 24, 2026
S1309 移除 PageLevel.ts 进入关卡时弹出体力扣减 toast (Apr 26 at 5:16 PM) 101 8:46a 🟣 Live label display format updated to X/Y format
1343 5:17p 🔴 进度条圆角畸形问题已修复 114 6:40p 🟣 Dynamic Input Layout Initialization in PageLevel Prefab
1345 " ⚖️ 否决 Mask 方案,确认最小进度值方案最优 115 6:41p 🟣 Dynamic Punch Block Layout for PageLevel.prefab
### Apr 27, 2026 116 " 🔵 Layout Component Configuration for Input and Punch Blocks
1346 9:21a 🔴 移除进入游戏后的体力扣减 toast 提示 119 6:42p 🟣 Dynamic Input Blocks and Punch Layout System Implemented
1347 " 🔴 移除进入游戏后的体力扣减 toast 提示 121 " 🟣 PageLevel Prefab Updated with punchLayout Property
1348 " 🟣 首页添加体力值显示功能 122 " 🔄 Cleanup After Dynamic Block Refactoring
S1308 移除进入游戏关卡时弹出体力扣减的 toast 提示 (Apr 27 at 9:21 AM) 124 " ✅ TypeScript Compilation Check in Progress
S1310 移除进入游戏体力扣减 toast 并在首页添加体力显示 (Apr 27 at 9:22 AM) 126 6:43p ✅ TypeScript Compilation Check Extended
S1311 移除体力扣减 toast 并完善首页体验 (Apr 27 at 9:23 AM) 127 " 🔄 Complete Diff of PageLevel.ts Dynamic Block System
1349 9:25a ✅ PageHome.ts 添加 ToastManager 导入 128 " 🔵 PageLevel.prefab Changes Not Persisted
1350 " ✅ PK按钮点击改为提示"功能开发中" 129 6:44p 🟣 PageLevel Prefab Correctly Updated with punchLayout
S1313 Implement object-fit: cover image scaling in Cocos Creator to prevent downloaded images from being distorted (Apr 27 at 9:26 AM) 130 " ✅ TypeScript Compilation Blocked - Permission Required
1351 9:32a 🟣 Stamina animation system for level entry 133 6:45p 🔄 Extracted getPunchBlockLabel Helper Method
1352 " 🔵 Cocos Creator game project structure discovered 134 " 🔄 Template Node Hiding Logic Improved
1354 " 🟣 StarGame 按钮点击动画需求 136 6:48p ⚖️ TypeScript diagnostics disabled, using IDE/linter instead
1356 " 🔵 体力系统架构发现 138 " 🔄 PageLevel 输入方式从单框改为逐字格子
1353 9:33a 🔵 StaminaManager API confirmed for stamina operations 139 " 🔄 谐音梗展示从 Label 改为动态 Block 节点
1355 9:37a 🔵 项目结构和现有代码发现 140 " ✅ PageLevel.prefab 布局位置微调
1357 9:38a 🔵 EnterLevelData 结构分析 165 8:08p 🟣 PageLevel input layout simplified to single-character boxes with auto-distribution
1359 9:39a 🟣 Stamina animation system implemented for game entry flow 167 8:09p 🔵 PageLevel.ts input block structure and callback stubs discovered
1358 9:41a ⚖️ 体力值飞行动画实现计划制定完成 168 " 🟣 Auto-distribute characters across input boxes and auto-submit on completion implemented
1362 9:45a 🟣 Image Cover Mode Scaling in Cocos Creator 169 " 🔴 PageLevel.ts node cleanup now calls removeFromParent before destroy
S1312 Fix image distortion in PageLevel.ts by implementing object-fit: cover behavior for downloaded images (Apr 27 at 9:45 AM) 170 8:10p 🔄 PageLevel.ts full diff: single EditBox replaced with per-character block system
1360 " ✅ Cocos Creator animation imports added to PageHome.ts 171 " 🔴 distributeInputText() wrapped in try/finally to guarantee flag reset
1361 " ✅ Animation constants added to PageHome class 179 8:23p ✅ Game011_3.ttf font relocated from resources/ to dedicated fonts/ bundle directory
1363 " ✅ Start game button integrated with stamina check and animation flow 180 " 🟣 PageLoading.ts now loads fonts as a dynamic bundle after level data, before UI
1364 " 🟣 Stamina consumption animation fully implemented in PageHome 181 " 🔄 AssetManager import switched to type-only import in PageLoading.ts
S1314 Implement stamina consumption animation in Cocos Creator game - when clicking StarGame button, IconLive node flies to button with floating "-1" text, then navigates to level (Apr 27 at 9:45 AM) 182 8:31p 🟣 PageLevel input UX improvement: full-string editing support
1365 9:46a 🔴 Animation completes but navigation to PageLevel fails 183 " 🟣 PageLevel input UX: full-string editing implemented
S1315 为 PageLevel.ts 的 mainImage 和 mainImage2 节点实现 CSS cover 模式图片缩放 (Apr 27 at 9:46 AM) 186 8:34p 🔴 PageLevel: clear input text on wrong answer
1366 9:50a 🔴 Cocos Creator tween chain broken by premature node destroy 187 8:35p 🟣 PageLevel: delay pass modal to show punchline
1367 9:55a 🔴 Navigation still failing after first tween chain fix - further debugging needed 189 8:36p 🔄 PageLevel: level completion reporting made fire-and-forget
1368 " 🔴 Decoupled fly animation from float text timing using setTimeout 191 8:45p 🔴 PageLevel: punchline block cleanup improved
1369 9:56a 🔴 Cocos Creator tween chain broken by any node state change mid-animation 192 8:46p 🟣 PageLevel.ts TitleLevel Label variable reserved
1370 9:59a 🟣 CSS cover-style image scaling for PageLevel images 193 8:47p 🔵 PageLevel.ts level number tracking mechanism discovered
1371 10:01a ✅ PageLevel.ts imports extended for cover-style image scaling 195 " 🟣 PageLevel.ts TitleLevel dynamic label update implemented
1372 " 🟣 CSS cover-mode image scaling implemented for PageLevel images 196 8:53p 🔵 PageLevel punchline not updated from enterLevel API
S1316 Debug cover mode image overflow in PageLevel.ts - image exceeds container bounds despite Mask applied (Apr 27 at 10:02 AM) 198 " 🔴 LevelDataManager updateLevelDetails now includes punchline
1373 10:02a 🔴 Cover mode image overflows container bounds 199 " 🔴 PageLevel punchline flow and empty state layout fixed
1374 10:06a 🔵 GRAPHICS_RECT vs GRAPHICS_STENCIL for Mask in Cocos Creator 200 8:54p 🔴 LevelDataManager preserves existing punchline when enter returns null
1375 10:08a 🔴 Mask.Type.RECT does not exist in Cocos Creator API 203 9:06p 🟣 PageLevel.ts 新增图片描述标签绑定字段
1376 " 🔵 MaskType enum found in Cocos Creator 3.8 engine declarations 205 9:07p 🟣 PageLevel.ts 图片描述标签字段对接完成
1378 " 🔵 MaskType enum values confirmed - GRAPHICS_RECT exists 206 9:09p ✅ PageLevel.ts 回退图片描述标签字段命名
1377 10:09a 🔵 MaskType enum definition found at line 46015 208 9:11p 🟣 LevelDataManager 增加图片描述字段存储
1379 10:10a 🔴 Reverted Mask.Type to GRAPHICS_RECT 214 9:40p 🟣 拼图游戏关卡内 Punch Layout 显隐控制
S1317 Implement CSS cover-mode image scaling for PageLevel.ts mainImage nodes (Apr 27 at 10:10 AM) 216 " 🟣 PageLevel.ts punchline 显隐控制逻辑对接完成
### Apr 29, 2026
1481 6:33p 🟣 Implementing rounded corner images in PageLevel.ts
1482 " 🔵 RoundedRectMask component already exists for image corner rounding
1485 6:34p 🟣 Implemented rounded corners for PageLevel main images
1484 " 🔄 RoundedRectMask component improved with quality standards
1487 6:37p 🔵 TypeScript verification confirms no errors in new rounded corners implementation
### May 12, 2026
1542 4:39p 🟣 CommonModal prefab adds conditional dual-button support
1543 " 🔵 CommonModal prefab contains both button node variants
1544 4:40p 🟣 CommonModal prefab upgraded with dual-button support
1545 4:41p 🟣 CommonModal prefab restructured with dual-button ActionDouble container
1546 " 🔵 BaseModal provides modal animation infrastructure
1547 4:43p 🟣 CommonModal.ts upgraded with dual-button support and backward compatibility
1548 4:44p 🟣 CommonModal dual-button implementation completed
1549 " ✅ CommonModal dual-button feature ready for testing
1550 4:45p 🟣 CommonModal dual-button feature completed and validated
Access 1674k tokens of past work via get_observations([IDs]) or mem-search skill. Access 539k tokens of past work via get_observations([IDs]) or mem-search skill.
</claude-mem-context> </claude-mem-context>

View File

@@ -1612,7 +1612,7 @@
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": -264.639, "x": -336.301,
"y": 152.257, "y": 152.257,
"z": 0 "z": 0
}, },
@@ -1653,7 +1653,7 @@
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 560, "width": 400,
"height": 88.2 "height": 88.2
}, },
"_anchorPoint": { "_anchorPoint": {
@@ -1689,15 +1689,15 @@
"b": 29, "b": 29,
"a": 255 "a": 255
}, },
"_string": "地名谐音梗大挑战", "_string": "地名谐音梗",
"_horizontalAlign": 1, "_horizontalAlign": 0,
"_verticalAlign": 1, "_verticalAlign": 1,
"_actualFontSize": 70, "_actualFontSize": 70,
"_fontSize": 70, "_fontSize": 70,
"_fontFamily": "Arial", "_fontFamily": "Arial",
"_lineHeight": 70, "_lineHeight": 70,
"_overflow": 0, "_overflow": 1,
"_enableWrapText": true, "_enableWrapText": false,
"_font": { "_font": {
"__uuid__": "fb4acba6-6bc7-4eb3-be34-8f2ac9823a80", "__uuid__": "fb4acba6-6bc7-4eb3-be34-8f2ac9823a80",
"__expectedType__": "cc.TTFFont" "__expectedType__": "cc.TTFFont"
@@ -1988,7 +1988,7 @@
"a": 255 "a": 255
}, },
"_string": "共66人参与", "_string": "共66人参与",
"_horizontalAlign": 1, "_horizontalAlign": 0,
"_verticalAlign": 1, "_verticalAlign": 1,
"_actualFontSize": 50, "_actualFontSize": 50,
"_fontSize": 50, "_fontSize": 50,
@@ -2848,7 +2848,7 @@
"a": 255 "a": 255
}, },
"_string": "第 1 名", "_string": "第 1 名",
"_horizontalAlign": 1, "_horizontalAlign": 0,
"_verticalAlign": 1, "_verticalAlign": 1,
"_actualFontSize": 60, "_actualFontSize": 60,
"_fontSize": 60, "_fontSize": 60,
@@ -6764,6 +6764,12 @@
"backBtn": { "backBtn": {
"__id__": 266 "__id__": 266
}, },
"createdListContent": {
"__id__": 54
},
"createdListItemTemplate": {
"__id__": 63
},
"_id": "" "_id": ""
}, },
{ {

View File

@@ -1,26 +1,42 @@
import { _decorator, Node, Button } from 'cc'; import { _decorator, Node, Button, instantiate, Label, ScrollView, UITransform, Sprite, SpriteFrame, Texture2D, ImageAsset, assetManager } 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 { CreatedShareItem } from 'db://assets/scripts/types/ApiTypes'; import { CreatedShareItem, ShareParticipantRankSummary } from 'db://assets/scripts/types/ApiTypes';
import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
@ccclass('PagePKData') @ccclass('PagePKData')
export class PagePKData extends BaseView { export class PagePKData extends BaseView {
private static readonly CREATED_ITEM_TOP_PADDING = 20;
private static readonly CREATED_ITEM_BOTTOM_PADDING = 20;
private static readonly CREATED_ITEM_SPACING = 20;
@property({ type: Node, tooltip: '返回按钮' }) @property({ type: Node, tooltip: '返回按钮' })
backBtn: Node | null = null; backBtn: Node | null = null;
@property({ type: Node, tooltip: '我创建的挑战列表 content 节点' })
createdListContent: Node | null = null;
@property({ type: Node, tooltip: '我创建的挑战列表条目模板 FriendsPKRankListItem' })
createdListItemTemplate: Node | null = null;
private _createdShares: CreatedShareItem[] = []; private _createdShares: CreatedShareItem[] = [];
private _createdItemNodes: Node[] = [];
private _createdButtonBindings: Array<{ node: Node; handler: () => void }> = [];
private _isLoading: boolean = false; private _isLoading: boolean = false;
private _renderVersion: number = 0;
onViewLoad(): void { onViewLoad(): void {
this._resolveNodes();
if (this.backBtn) { if (this.backBtn) {
this.backBtn.on(Button.EventType.CLICK, this._onBackClick, this); this.backBtn.on(Button.EventType.CLICK, this._onBackClick, this);
} }
this._hideCreatedItemTemplate();
} }
onViewShow(): void { onViewShow(): void {
this._resolveNodes();
void this._loadCreatedShares(); void this._loadCreatedShares();
} }
@@ -46,6 +62,7 @@ export class PagePKData extends BaseView {
this._createdShares = items; this._createdShares = items;
console.log('[PagePKData] 我创建的挑战列表:', this._createdShares); console.log('[PagePKData] 我创建的挑战列表:', this._createdShares);
this._renderCreatedShares();
} finally { } finally {
this._isLoading = false; this._isLoading = false;
} }
@@ -55,5 +72,202 @@ export class PagePKData extends BaseView {
if (this.backBtn) { if (this.backBtn) {
this.backBtn.off(Button.EventType.CLICK, this._onBackClick, this); this.backBtn.off(Button.EventType.CLICK, this._onBackClick, this);
} }
this._clearCreatedItems();
}
private _resolveNodes(): void {
if (!this.backBtn || !this.backBtn.isValid) {
this.backBtn = this.node.getChildByName('BtnBack');
}
const createdList = this.node.getChildByName('FriendsPKRankList');
const view = createdList?.getChildByName('view');
this.createdListContent = this.createdListContent ?? view?.getChildByName('content') ?? null;
this.createdListItemTemplate = this.createdListItemTemplate
?? this.createdListContent?.getChildByName('FriendsPKRankListItem')
?? null;
if (!this.createdListContent) {
console.warn('[PagePKData] 未找到 FriendsPKRankList/content 节点');
}
if (!this.createdListItemTemplate) {
console.warn('[PagePKData] 未找到 FriendsPKRankListItem 模板节点');
}
}
private _renderCreatedShares(): void {
this._renderVersion++;
this._clearCreatedItems();
if (!this.createdListContent || !this.createdListItemTemplate) {
return;
}
this._layoutCreatedList(this._createdShares.length);
this._hideCreatedItemTemplate();
this._createdShares.forEach((share, index) => {
const item = instantiate(this.createdListItemTemplate!);
item.name = `FriendsPKRankListItem_${index + 1}`;
item.active = true;
this.createdListContent!.addChild(item);
this._createdItemNodes.push(item);
this._positionCreatedItem(item, index);
this._applyCreatedShare(item, share, this._renderVersion);
});
this._scrollCreatedListToTop();
}
private _applyCreatedShare(item: Node, share: CreatedShareItem, version: number): void {
this._setLabel(this._findLabelIn(item, 'Name'), share.title || '未命名挑战');
this._setLabel(this._findLabelIn(item, 'ParticipateLabel'), `${share.participantCount ?? 0}人参与`);
const firstParticipant = this._getFirstParticipant(share);
const firstName = this._getParticipantName(firstParticipant);
const rankNumber = firstParticipant?.rank ?? 1;
this._setLabel(this._findLabelIn(item, 'NameLabel'), firstName || (share.participantCount > 0 ? '微信用户' : '暂无参与'));
this._setLabel(this._findLabelIn(item, 'RankNumberLabel'), firstParticipant ? `${rankNumber}` : '暂无排名');
this._loadAvatar(firstParticipant?.avatarUrl ?? '', this._findAvatarSprite(item), version);
const shareButton = this._findChild(item, 'ShareButton');
if (shareButton) {
const handler = () => ShareManager.instance.triggerWxShare(share.title, share.shareCode);
shareButton.on(Button.EventType.CLICK, handler, this);
this._createdButtonBindings.push({ node: shareButton, handler });
}
}
private _getFirstParticipant(share: CreatedShareItem): ShareParticipantRankSummary | null {
return share.firstPlaceUser ?? share.topParticipant ?? share.firstParticipant ?? share.champion ?? null;
}
private _getParticipantName(participant: ShareParticipantRankSummary | null): string {
return participant?.nickname || participant?.nickName || '';
}
private _layoutCreatedList(itemCount: number): void {
if (!this.createdListContent || !this.createdListItemTemplate) {
return;
}
const contentTransform = this.createdListContent.getComponent(UITransform);
const viewTransform = this.createdListContent.parent?.getComponent(UITransform) ?? null;
const itemTransform = this.createdListItemTemplate.getComponent(UITransform);
if (!contentTransform || !viewTransform || !itemTransform) {
return;
}
const contentHeight = Math.max(
viewTransform.height,
PagePKData.CREATED_ITEM_TOP_PADDING
+ PagePKData.CREATED_ITEM_BOTTOM_PADDING
+ itemCount * itemTransform.height
+ Math.max(0, itemCount - 1) * PagePKData.CREATED_ITEM_SPACING,
);
contentTransform.setContentSize(contentTransform.width, contentHeight);
this.createdListContent.setPosition(
this.createdListContent.position.x,
viewTransform.height / 2,
this.createdListContent.position.z,
);
}
private _positionCreatedItem(item: Node, index: number): void {
const itemTransform = item.getComponent(UITransform);
if (!itemTransform) {
return;
}
const y = -PagePKData.CREATED_ITEM_TOP_PADDING
- itemTransform.height / 2
- index * (itemTransform.height + PagePKData.CREATED_ITEM_SPACING);
item.setPosition(0, y, item.position.z);
}
private _scrollCreatedListToTop(): void {
const scrollView = this.node.getChildByName('FriendsPKRankList')?.getComponent(ScrollView);
scrollView?.scrollToTop(0);
}
private _findAvatarSprite(item: Node): Sprite | null {
const headNode = this._findChild(item, 'Head');
return headNode?.children[0]?.getComponent(Sprite) ?? headNode?.getComponent(Sprite) ?? null;
}
private _loadAvatar(url: string, sprite: Sprite | null, version: number): void {
if (!url || !sprite) {
return;
}
assetManager.loadRemote<ImageAsset>(url, (err, imageAsset) => {
if (err || !imageAsset || version !== this._renderVersion || !sprite.node.isValid) {
if (err) {
console.error('[PagePKData] 加载第一名头像失败:', url, err);
}
return;
}
const texture = new Texture2D();
texture.image = imageAsset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
sprite.spriteFrame = spriteFrame;
});
}
private _clearCreatedItems(): void {
this._unbindCreatedButtons();
for (const item of this._createdItemNodes) {
if (item.isValid) {
item.removeFromParent();
item.destroy();
}
}
this._createdItemNodes = [];
this._hideCreatedItemTemplate();
}
private _unbindCreatedButtons(): void {
for (const binding of this._createdButtonBindings) {
if (binding.node.isValid) {
binding.node.off(Button.EventType.CLICK, binding.handler, this);
}
}
this._createdButtonBindings = [];
}
private _hideCreatedItemTemplate(): void {
if (this.createdListItemTemplate?.isValid) {
this.createdListItemTemplate.active = false;
}
}
private _setLabel(label: Label | null, text: string): void {
if (label) {
label.string = text;
}
}
private _findLabelIn(root: Node, nodeName: string): Label | null {
return this._findChild(root, 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

@@ -162,6 +162,17 @@ export interface SubmitShareData {
levels: SubmittedShareLevelData[]; levels: SubmittedShareLevelData[];
} }
/** 分享挑战参与者排行摘要 */
export interface ShareParticipantRankSummary {
userId?: string | null;
nickname?: string | null;
nickName?: string | null;
avatarUrl?: string | null;
rank?: number | null;
correctCount?: number | null;
totalTimeSpent?: number | null;
}
/** 我创建的分享挑战条目 */ /** 我创建的分享挑战条目 */
export interface CreatedShareItem { export interface CreatedShareItem {
id: string; id: string;
@@ -171,6 +182,10 @@ export interface CreatedShareItem {
participantCount: number; participantCount: number;
userRank: number | null; userRank: number | null;
createdAt: string; createdAt: string;
firstPlaceUser?: ShareParticipantRankSummary | null;
topParticipant?: ShareParticipantRankSummary | null;
firstParticipant?: ShareParticipantRankSummary | null;
champion?: ShareParticipantRankSummary | null;
} }
/** 我创建的分享挑战列表响应 */ /** 我创建的分享挑战列表响应 */