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 { CreatedShareItem, ShareDetailData, ShareParticipantRankSummary } from 'db://assets/scripts/types/ApiTypes'; import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; const { ccclass } = _decorator; interface PagePKDetailParams { share?: CreatedShareItem | null; shareCode?: string | null; detail?: ShareDetailData | null; } @ccclass('PagePKDetail') export class PagePKDetail extends BaseView { private static readonly RANK_ITEM_TOP_PADDING = 16; private static readonly RANK_ITEM_BOTTOM_PADDING = 16; private static readonly RANK_ITEM_SPACING = 16; private _backButton: Node | null = null; private _titleLabel: Label | null = null; private _championPanel: Node | null = null; private _rankListContent: Node | null = null; private _rankListItemTemplate: Node | null = null; private _rankItemNodes: Node[] = []; private _renderVersion: number = 0; onViewLoad(): void { this._resolveNodes(); this._bindEvents(); this._hideRankItemTemplate(); } onViewShow(): void { this._resolveNodes(); void this._loadAndRenderDetail(); } onViewHide(): void { this._renderVersion++; } onViewDestroy(): void { this._unbindEvents(); this._clearRankItems(); } private _resolveNodes(): void { if (!this._backButton || !this._backButton.isValid) { this._backButton = this.node.getChildByName('ButtonBack'); } this._titleLabel = this._titleLabel ?? this.node.getChildByName('Title')?.getChildByName('Label')?.getComponent(Label) ?? null; this._championPanel = this._championPanel ?? this.node.getChildByName('ChampionPanel'); const rankList = this.node.getChildByName('RankList'); const view = rankList?.getChildByName('view'); this._rankListContent = this._rankListContent ?? view?.getChildByName('content') ?? null; this._rankListItemTemplate = this._rankListItemTemplate ?? this._rankListContent?.getChildByName('RankListItem') ?? null; if (!this._backButton) { console.warn('[PagePKDetail] 未找到 ButtonBack 节点'); } if (!this._rankListContent) { console.warn('[PagePKDetail] 未找到 RankList/content 节点'); } if (!this._rankListItemTemplate) { console.warn('[PagePKDetail] 未找到 RankListItem 模板节点'); } } private _bindEvents(): void { if (this._backButton) { this._backButton.on(Button.EventType.CLICK, this._onBackClick, this); } } private _unbindEvents(): void { if (this._backButton?.isValid) { this._backButton.off(Button.EventType.CLICK, this._onBackClick, this); } } private _onBackClick(): void { ViewManager.instance.back(); } private async _loadAndRenderDetail(): Promise { const params = this.getParams() as PagePKDetailParams | null; const share = this._resolveShare(params); const passedDetail = params?.detail ?? null; const shareCode = passedDetail?.shareCode ?? params?.shareCode ?? share?.shareCode ?? null; const version = ++this._renderVersion; if (passedDetail) { this._renderShareDetail(passedDetail, version); return; } this._renderShareSummary(share, version); if (!shareCode) { ToastManager.instance.show('挑战数据异常,请稍后重试'); return; } const detail = await ShareManager.instance.fetchShareDetail(shareCode); if (version !== this._renderVersion || !this.isShowing) { return; } if (!detail) { ToastManager.instance.show('获取挑战详情失败,请稍后重试'); this._renderShareSummary(share, version); return; } this._renderShareDetail(detail, version); } private _resolveShare(params: PagePKDetailParams | null): CreatedShareItem | null { if (params?.share) { return params.share; } const shareCode = params?.shareCode ?? params?.detail?.shareCode; if (!shareCode) { return null; } return ShareManager.instance.createdShares.find((share) => share.shareCode === shareCode) ?? null; } private _renderShareSummary(share: CreatedShareItem | null, version: number): void { this._clearRankItems(); this._setLabel(this._titleLabel, share?.title || '挑战详情'); this._renderChampion(share ? this._getFirstParticipant(share) : null, share, version); this._layoutRankContent(0); this._scrollRankListToTop(); } private _renderShareDetail(detail: ShareDetailData, version: number): void { this._clearRankItems(); const rankings = this._normalizeRankings(detail.rankings ?? []); const champion = rankings.find((participant) => participant.rank === 1) ?? rankings[0] ?? null; const restRankings = rankings.filter((participant) => participant !== champion); this._setLabel(this._titleLabel, detail.title || '挑战详情'); this._renderChampion(champion, detail, version); this._renderRankList(restRankings, version); } private _normalizeRankings(rankings: ShareParticipantRankSummary[]): ShareParticipantRankSummary[] { return rankings .map((participant, index) => ({ ...participant, rank: participant.rank ?? index + 1, })) .sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0)); } private _getFirstParticipant(share: CreatedShareItem): ShareParticipantRankSummary | null { return share.firstPlaceUser ?? share.topParticipant ?? share.firstParticipant ?? share.champion ?? null; } private _renderChampion( champion: ShareParticipantRankSummary | null, shareInfo: CreatedShareItem | ShareDetailData | null, version: number, ): void { const panel = this._championPanel; if (!panel) { return; } this._setLabel(this._findLabelIn(panel, 'UserName'), this._getParticipantDisplayName(champion, '暂无参与')); this._setLabel(this._findLabelIn(panel, 'RightInfo'), this._formatCorrectText(champion, shareInfo)); this._setLabel(this._findLabelIn(panel, 'UsedTime'), this._formatTimeText(champion, shareInfo)); this._loadAvatar(champion?.avatarUrl ?? '', this._findAvatarSprite(panel), version); } private _renderRankList(participants: ShareParticipantRankSummary[], version: number): void { if (!this._rankListContent || !this._rankListItemTemplate) { return; } this._hideRankItemTemplate(); this._layoutRankContent(participants.length); participants.forEach((participant, index) => { const item = instantiate(this._rankListItemTemplate!); item.name = `RankListItem_${index + 1}`; item.active = true; this._rankListContent!.addChild(item); this._rankItemNodes.push(item); this._positionRankItem(item, index); this._applyRankItem(item, participant, version); }); this._scrollRankListToTop(); } private _applyRankItem(item: Node, participant: ShareParticipantRankSummary, version: number): void { const rank = participant.rank ?? 0; const rank2Badge = this._findChild(item, 'rank2badge'); const rank3Badge = this._findChild(item, 'rank3badge'); const rankNumberNode = this._findChild(item, 'RankNumber'); if (rank2Badge) { rank2Badge.active = rank === 2; } if (rank3Badge) { rank3Badge.active = rank === 3; } if (rankNumberNode) { rankNumberNode.active = rank !== 2 && rank !== 3; this._setLabel(rankNumberNode.getComponent(Label), rank > 0 ? `${rank}` : '-'); } this._setLabel(this._findLabelIn(item, 'UserName'), this._getParticipantDisplayName(participant, '微信用户')); this._setLabel(this._findLabelIn(item, 'RightInfo'), this._formatCorrectText(participant, null)); this._setLabel(this._findLabelIn(item, 'UsedTime'), this._formatTimeText(participant)); this._loadAvatar(participant.avatarUrl ?? '', this._findAvatarSprite(item), version); } private _layoutRankContent(itemCount: number): void { if (!this._rankListContent || !this._rankListItemTemplate) { return; } const contentTransform = this._rankListContent.getComponent(UITransform); const viewTransform = this._rankListContent.parent?.getComponent(UITransform) ?? null; const itemTransform = this._rankListItemTemplate.getComponent(UITransform); if (!contentTransform || !viewTransform || !itemTransform) { return; } const contentHeight = Math.max( viewTransform.height, PagePKDetail.RANK_ITEM_TOP_PADDING + PagePKDetail.RANK_ITEM_BOTTOM_PADDING + itemCount * itemTransform.height + Math.max(0, itemCount - 1) * PagePKDetail.RANK_ITEM_SPACING, ); contentTransform.setContentSize(contentTransform.width, contentHeight); this._rankListContent.setPosition( this._rankListContent.position.x, viewTransform.height / 2, this._rankListContent.position.z, ); } private _positionRankItem(item: Node, index: number): void { const itemTransform = item.getComponent(UITransform); if (!itemTransform) { return; } const y = -PagePKDetail.RANK_ITEM_TOP_PADDING - itemTransform.height / 2 - index * (itemTransform.height + PagePKDetail.RANK_ITEM_SPACING); item.setPosition(0, y, item.position.z); } private _scrollRankListToTop(): void { this.node.getChildByName('RankList')?.getComponent(ScrollView)?.scrollToTop(0); } private _formatCorrectText( participant: ShareParticipantRankSummary | null, shareInfo: CreatedShareItem | ShareDetailData | null, ): string { if (participant?.correctCount !== undefined && participant.correctCount !== null) { return `答对${participant.correctCount}道`; } if (shareInfo && (shareInfo.participantCount ?? 0) <= 0) { return '答对0道'; } if (shareInfo) { return `共${shareInfo.participantCount ?? 0}人参与`; } return '暂无成绩'; } private _formatTimeText( participant: ShareParticipantRankSummary | null, shareInfo: CreatedShareItem | ShareDetailData | null = null, ): string { if (participant?.totalTimeSpent === undefined || participant.totalTimeSpent === null) { if (shareInfo && (shareInfo.participantCount ?? 0) <= 0) { return '用时0秒'; } return '暂无用时'; } const totalSeconds = Math.max(0, Math.round(participant.totalTimeSpent)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `用时${minutes}:${seconds.toString().padStart(2, '0')}`; } private _getParticipantName(participant: ShareParticipantRankSummary | null): string { return participant?.nickname || participant?.nickName || ''; } private _getParticipantDisplayName( participant: ShareParticipantRankSummary | null, emptyParticipantFallback: string, ): string { if (!participant) { return emptyParticipantFallback; } return this._getParticipantName(participant) || '微信用户'; } private _loadAvatar(url: string, sprite: Sprite | null, version: number): void { if (!sprite) { return; } if (!url) { return; } assetManager.loadRemote(url, (err, imageAsset) => { if (err || !imageAsset || version !== this._renderVersion || !sprite.node.isValid) { if (err) { console.error('[PagePKDetail] 加载头像失败:', url, err); } return; } const texture = new Texture2D(); texture.image = imageAsset; const spriteFrame = new SpriteFrame(); spriteFrame.texture = texture; sprite.spriteFrame = spriteFrame; }); } private _findAvatarSprite(root: Node): Sprite | null { const avatarNode = this._findChild(root, 'Avatar'); return avatarNode?.getComponent(Sprite) ?? null; } private _clearRankItems(): void { for (const item of this._rankItemNodes) { if (item.isValid) { item.removeFromParent(); item.destroy(); } } this._rankItemNodes = []; this._hideRankItemTemplate(); } private _hideRankItemTemplate(): void { if (this._rankListItemTemplate?.isValid) { this._rankListItemTemplate.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; } }