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(); 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 { 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 { 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 { return new Promise((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 { 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(); } }