Files
mp-xieyingeng/assets/prefabs/PagePKDetail.ts
2026-05-14 17:01:56 +08:00

412 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, property } = _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;
@property({ type: Label, tooltip: '参与人数文案例如66 人参与' })
participateLabel: Label | null = null;
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.participateLabel = this.participateLabel ?? this._findLabelIn(this.node, 'ParticipateLabel');
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 模板节点');
}
if (!this.participateLabel) {
console.warn('[PagePKDetail] 未找到 ParticipateLabel 节点');
}
}
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<void> {
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._renderParticipateCount(share);
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._renderParticipateCount(detail);
this._renderChampion(champion, detail, version);
this._renderRankList(restRankings, version);
}
private _renderParticipateCount(shareInfo: CreatedShareItem | ShareDetailData | null): void {
const participantCount = Math.max(0, shareInfo?.participantCount ?? 0);
this._setLabel(this.participateLabel, `${participantCount} 人参与`);
}
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<ImageAsset>(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;
}
}