184 lines
9.3 KiB
TypeScript
184 lines
9.3 KiB
TypeScript
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();
|
||
}
|
||
}
|