412 lines
15 KiB
TypeScript
412 lines
15 KiB
TypeScript
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;
|
||
}
|
||
}
|