MatchMaster/assets/mini_program_benefits/script/MiniProgramBenefitsPanel.ts
2026-09-18 11:49:52 +08:00

184 lines
9.3 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 MiniProgramBenefitsReward from "./MiniProgramBenefitsReward";
const { ccclass, property } = cc._decorator;
const TASKS = ["favorite_entry", "desktop_entry"];
/** Layout and artwork live in bundle prefabs. This component only binds state, events and motion. */
@ccclass
export default class MiniProgramBenefitsPanel extends cc.Component {
@property(cc.Prefab) mainPrefab: cc.Prefab = null;
@property(cc.Prefab) favoritePrefab: cc.Prefab = null;
@property(cc.Prefab) desktopPrefab: cc.Prefab = null;
@property(cc.Prefab) rewardPrefab: cc.Prefab = null;
private host: any;
private motion: cc.Node;
private mask: cc.Node;
private pages: cc.Node;
private views: { [key: string]: cc.Node } = {};
private page = "list";
private message = "";
private busy = false;
private claiming = false;
private closing = false;
private generation = 0;
private unsubscribe: () => void;
private cancelReward: () => void;
private shownReceipts = new Set<string>();
init(host: any): void {
this.host = host;
host.bindRewardApi(new MiniProgramBenefitsReward(host.getRewardStorageKey(), host.getUid(), () => host.getState()));
const canvas = cc.find("Canvas");
this.node.setContentSize(canvas ? canvas.getContentSize() : cc.view.getVisibleSize());
this.mask = this.node.getChildByName("Mask");
this.mask.setContentSize(this.node.getContentSize());
this.motion = this.node.getChildByName("Motion"); this.pages = this.motion.getChildByName("Pages");
[this.mainPrefab, this.favoritePrefab, this.desktopPrefab].forEach((prefab, index) => {
if (!prefab) throw new Error("福利页面预制体缺失");
const key = index === 0 ? "list" : TASKS[index - 1];
const view = this.views[key] = cc.instantiate(prefab); view.parent = this.pages;
const content = view.getChildByName("Content") || view;
content.getChildByName("Close").on(cc.Node.EventType.TOUCH_END, () => {
if (this.closing || this.busy) return;
if (key === "list") this.closeWithAnimation();
else { this.page = "list"; this.message = ""; this.render(); }
});
if (key === "list") TASKS.forEach((task, i) => {
const row = view.getChildByName("Task" + i);
row.getChildByName("View").on(cc.Node.EventType.TOUCH_END, () => {
if (this.busy || this.closing) return;
this.page = task; this.message = ""; this.render();
});
row.getChildByName("Claim").on(cc.Node.EventType.TOUCH_END, () => this.claim(task));
});
});
this.unsubscribe = host.subscribe(() => { if (!this.busy) this.render(); });
this.render(); this.refresh(); this.playOpenAnimation();
}
private playOpenAnimation(): void {
this.motion.scale = 0.86;
cc.tween(this.motion).to(0.20, { scale: 1.035 }, { easing: "quadOut" })
.to(0.12, { scale: 1 }, { easing: "sineOut" }).start();
}
private closeWithAnimation(): void {
if (this.closing || this.claiming) return;
this.closing = true;
cc.Tween.stopAllByTarget(this.motion);
cc.Tween.stopAllByTarget(this.mask);
cc.tween(this.motion)
.to(0.15, { scale: 0.90 }, { easing: "sineIn" })
.call(() => {
if (cc.isValid(this.node, true) && this.host) this.host.close();
})
.start();
}
private alive(token: number): boolean { return cc.isValid(this.node, true) && this.generation === token; }
private async refresh(): Promise<void> {
const token = this.generation;
this.busy = true; this.message = "正在查询福利…"; this.render();
try {
await this.host.refresh();
if (!this.alive(token)) return;
this.message = "";
await this.showPendingReceipt();
} catch (error) {
if (this.alive(token)) this.message = error && error.message || "查询失败,请点击刷新重试";
} finally {
if (this.alive(token)) { this.busy = false; this.render(); }
}
}
private async showPendingReceipt(): Promise<void> {
const token = this.generation;
const state = this.host.getState();
const pending = TASKS.filter(task => state[task] && state[task].claimedAt && !state[task].acknowledged)
.map(task => ({ task, id: state[task].receiptId }));
const unseen = pending.filter(receipt => !this.shownReceipts.has(receipt.id));
this.page = "list";
if (unseen.length) {
await this.showRewardAnimation(unseen.length);
if (!this.alive(token)) return;
unseen.forEach(receipt => this.shownReceipts.add(receipt.id));
}
for (const receipt of pending) {
if (!this.alive(token)) return;
await this.host.acknowledge(receipt.task, receipt.id);
}
}
private render(): void {
if (this.closing || !this.pages) return;
Object.keys(this.views).forEach(key => {
const view = this.views[key]; view.active = key === this.page;
if (!view.active) return;
// Keep editor-authored local positions and sizes; only fit the complete page to the screen.
view.scale = Math.min(1.45, (this.node.width - 64) / view.width, (this.node.height - 70) / view.height);
const content = view.getChildByName("Content") || view;
content.getChildByName("Message").getComponent(cc.Label).string = this.message;
});
const state = this.host.getState();
TASKS.forEach((task, index) => {
const item = state[task] || {}, row = this.views.list.getChildByName("Task" + index);
row.getChildByName("View").active = !item.claimedAt && !item.eligibleAt;
row.getChildByName("Claim").active = !item.claimedAt && !!item.eligibleAt;
row.getChildByName("Claimed").active = !!item.claimedAt;
});
}
private showRewardAnimation(count: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this.rewardPrefab) { reject(new Error("领取展示预制体缺失")); return; }
const overlay = cc.instantiate(this.rewardPrefab); overlay.parent = this.node; overlay.zIndex = 4000;
overlay.setContentSize(this.node.getContentSize());
overlay.getChildByName("Mask").setContentSize(overlay.getContentSize());
const board = overlay.getChildByName("Board");
board.scale = Math.min(1.4, (overlay.width - 40) / board.width, (overlay.height - 40) / board.height);
const animated: cc.Node[] = [overlay]; let ready = false, finished = false;
const finish = (cancelled: boolean) => {
if (finished) return; finished = true; this.cancelReward = null;
animated.forEach(node => cc.Tween.stopAllByTarget(node));
overlay.active = false; overlay.destroy();
if (cancelled) reject(new Error("领取展示已关闭,请重新打开福利查看")); else resolve();
};
this.cancelReward = () => finish(true);
overlay.on(cc.Node.EventType.TOUCH_END, () => { if (ready) finish(false); });
const rewards = board.getChildByName("Rewards");
[0, 1, 2].forEach(index => {
const item = rewards.getChildByName("Item" + index), badge = item.getChildByName("Quantity");
badge.getChildByName("Times").active = count === 1;
badge.getChildByName("One").active = count === 1;
const number = badge.getChildByName("Count"); number.active = count !== 1;
number.getComponent(cc.Label).string = "×" + count;
const scale = item.scale; animated.push(item, badge); item.scale = 0;
cc.tween(item).delay(0.12 + index * 0.13).to(0.22, { scale: scale * 1.12 }, { easing: "quadOut" })
.to(0.12, { scale }, { easing: "sineOut" }).start();
});
cc.tween(overlay).delay(1.08).call(() => { ready = true; }).start();
});
}
private async claim(task: string): Promise<void> {
if (this.closing || this.busy || this.claiming) return;
const token = this.generation;
this.claiming = true; this.busy = true; this.message = "正在领取…"; this.render();
try {
await this.host.claim(task);
if (!this.alive(token)) return;
this.message = "";
await this.showPendingReceipt();
} catch (error) {
if (this.alive(token)) this.message = error && error.message || "领取结果待确认,请刷新状态";
} finally {
if (this.alive(token)) { this.claiming = false; this.busy = false; this.render(); }
}
}
onDestroy(): void {
this.generation++; if (this.cancelReward) this.cancelReward();
if (this.motion) cc.Tween.stopAllByTarget(this.motion);
if (this.mask) cc.Tween.stopAllByTarget(this.mask);
if (this.unsubscribe) this.unsubscribe();
if (this.host) this.host.onPanelDestroyed(this.node);
this.unsubscribe = null; this.host = null; this.views = {}; this.shownReceipts.clear();
}
}