/** * @file CommonPrompt.ts * @description 自定义公共确认窗口组件(UI 视图层) * * 职责: * 1. 替换框架插件 CommonPrompt(extensions/oops-plugin-framework/.../CommonPrompt.ts), * 脚本类名、onTouchEnd 处理器与 4 个属性名保持同名,window.prefab 无需改动即可自动重链。 * 2. 内容区由 cc.Label 升级为 cc.RichText,支持 // 等富文本标签。 * 3. 扩展标题样式、窗体缩放动画、遮罩点击关闭等自定义能力。 * * 使用(oops.gui 模块): * oops.gui.open(UIID.Window, { * title: "标题", * content: "支持 富文本", * okWord: "确定", cancelWord: "取消", needCancel: true, * okFunc: () => {}, cancelFunc: () => {}, * titleColor: "#FF0000", // 可选:标题颜色 * maskClose: true, // 可选:点击空白遮罩关闭(等同取消) * closeFunc: () => {} // 可选:任意方式关闭后的统一回调 * }); */ import { _decorator, Button, Color, Component, EventTouch, Label, Node, NodeEventType, RichText, Tween, tween, Vec3 } from "cc"; import { oops } from "db://oops-framework/core/Oops"; const { ccclass, property } = _decorator; /** 确认窗口参数 */ export interface WindowPromptParams { /** 标题(普通文本) */ title?: string; /** 内容(RichText 富文本,支持 //// 等标签) */ content?: string; /** 确定按钮文字 */ okWord?: string; /** 确定回调 */ okFunc?: () => void; /** 取消按钮文字 */ cancelWord?: string; /** 取消回调 */ cancelFunc?: () => void; /** 是否显示取消按钮 */ needCancel?: boolean; /** 标题颜色(如 "#FF0000",不传用默认色) */ titleColor?: string; /** 点击窗体外空白遮罩是否关闭(等同取消,默认 false) */ maskClose?: boolean; /** 任意方式关闭后的统一回调(在 okFunc/cancelFunc 之后触发) */ closeFunc?: () => void; } /** * CommonPrompt —— 自定义公共确认窗口 * * 与 window.prefab 的绑定约定: * - 根节点按钮事件 handler:onTouchEnd(btn_ok / btn_cancel 已绑定) * - 属性名:lab_title / lab_content / lab_ok / lab_cancel * - lab_content 需将 prefab 中的 cc.Label 替换为 cc.RichText 后拖入 */ @ccclass("CommonPrompt") export class CommonPrompt extends Component { /** 窗口标题 */ @property(Label) private lab_title: Label | null = null; /** 窗口内容(富文本,需在 prefab 中将 Label 换为 RichText 后重新拖入) */ @property(RichText) private lab_content: RichText | null = null; /** 确定按钮文本 */ @property(Label) private lab_ok: Label | null = null; /** 取消按钮文本 */ @property(Label) private lab_cancel: Label | null = null; /** 运行时参数 */ private config: WindowPromptParams = {}; /** 弹窗打开动画已播放标记(防止 onAdded 重复触发) */ private animPlayed: boolean = false; /** 代码动态绑定的确定按钮(prefab clickEvents 失效时的兜底) */ private btnOk: Node | null = null; /** 代码动态绑定的取消按钮 */ private btnCancel: Node | null = null; onLoad() { // 兜底:prefab 中 clickEvents 引用的旧插件脚本被移除后,代码动态绑定保证按钮可用 this.btnOk = this.node.getChildByName("Node")?.getChildByName("btn_ok") ?? null; this.btnCancel = this.node.getChildByName("Node")?.getChildByName("btn_cancel") ?? null; this.btnOk?.on(Button.EventType.CLICK, this.onOk, this); this.btnCancel?.on(Button.EventType.CLICK, this.onCancel, this); // 遮罩点击关闭(btn_sky 为 btn_ok 内嵌子节点,无 Button 组件,直接监听触摸事件) this.btnOk?.getChildByName("btn_sky")?.on(NodeEventType.TOUCH_END, this.onMaskClick, this); // 序列化属性槽引用的是旧插件 LanguageLabel 组件实例,脚本替换后已失效; // 这里按节点名查找 Label/RichText 作为兜底,保证文字一定写入 if (!this.lab_title) { this.lab_title = this.node.getChildByName("lab_title")?.getComponent(Label) ?? null; } if (!this.lab_ok) { this.lab_ok = this.btnOk?.getChildByName("lab_ok")?.getComponent(Label) ?? null; } if (!this.lab_cancel) { this.lab_cancel = this.btnCancel?.getChildByName("lab_cancel")?.getComponent(Label) ?? null; } if (!this.lab_content) { this.lab_content = this.node.getChildByName("lab_content")?.getComponent(RichText) ?? null; } // 禁用旧插件残留的 LanguageLabel 组件:其 update 会用空 dataID 每帧覆盖 Label 文本 this.disableLegacyLanguageLabels(this.lab_title?.node); this.disableLegacyLanguageLabels(this.lab_ok?.node); this.disableLegacyLanguageLabels(this.lab_cancel?.node); this.disableLegacyLanguageLabels(this.lab_content?.node); } /** 禁用节点上残留的 LanguageLabel(旧插件组件,类名带 LanguageLabel 后缀) */ private disableLegacyLanguageLabels(node: Node | null | undefined) { if (!node || !node.isValid) return; for (const comp of node.components) { if (comp.constructor.name === "LanguageLabel") { comp.enabled = false; } } } onDestroy() { this.btnOk?.off(Button.EventType.CLICK, this.onOk, this); this.btnCancel?.off(Button.EventType.CLICK, this.onCancel, this); Tween.stopAllByTarget(this.node); this.config = {}; } /** * oops.gui 添加到舞台回调(框架约定入口) * @param params 见 WindowPromptParams */ onAdded(params: WindowPromptParams = {}) { this.config = params || {}; this.setTitle(); this.setContent(); this.setBtnOkLabel(); this.setBtnCancelLabel(); this.node.active = true; this.playOpenAnim(); } /** 按钮点击处理器(window.prefab 中 btn_ok / btn_cancel 的 clickEvents 绑定此方法) */ private onTouchEnd(event: EventTouch) { switch (event.target.name) { case "btn_ok": this.onOk(); break; case "btn_cancel": this.onCancel(); break; default: break; } } /** 遮罩(btn_sky)点击:仅当 maskClose 开启时按取消处理 */ private onMaskClick() { if (this.config.maskClose) { this.onCancel(); } } private setTitle() { if (!this.lab_title || !this.lab_title.isValid) return; this.lab_title.string = this.config.title ?? ""; if (this.config.titleColor) { this.lab_title.color = new Color().fromHEX(this.config.titleColor); } } private setContent() { if (!this.lab_content || !this.lab_content.isValid) return; this.lab_content.string = this.config.content ?? ""; } private setBtnOkLabel() { if (!this.lab_ok || !this.lab_ok.isValid) return; this.lab_ok.string = this.config.okWord ?? "确定"; } private setBtnCancelLabel() { if (!this.lab_cancel || !this.lab_cancel.isValid) return; this.lab_cancel.string = this.config.cancelWord ?? "取消"; // 取消按钮挂在 lab_cancel 的父节点上 const cancelBtn = this.lab_cancel.node?.parent; if (cancelBtn && cancelBtn.isValid) { cancelBtn.active = this.config.needCancel || false; } } /** 打开动画:缩放 0.8 → 1 回弹 */ private playOpenAnim() { if (this.animPlayed) return; this.animPlayed = true; Tween.stopAllByTarget(this.node); this.node.setScale(0.8, 0.8, 1); tween(this.node) .to(0.15, { scale: new Vec3(1, 1, 1) }, { easing: "backOut" }) .start(); } private onOk() { oops.audio.playEffect("music/button"); this.config.okFunc?.(); this.close(); } private onCancel() { oops.audio.playEffect("music/button"); this.config.cancelFunc?.(); this.close(); } private close() { this.config.closeFunc?.(); oops.gui.removeByNode(this.node); } }