Files
pixelheros/assets/script/game/common/CommonPrompt.ts
panFD 53fa0d7dc0 feat(ui): 新增通用确认弹窗并重构英雄出售逻辑
1. 新增CommonPrompt通用确认弹窗组件,支持富文本内容、自定义按钮与弹窗动画
2. 在UIID配置中新增Window弹窗枚举与对应UI配置
3. 重构HeroBoxComp的出售逻辑,添加二次确认防止误触出售英雄
2026-08-01 23:05:54 +08:00

227 lines
8.4 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.
/**
* @file CommonPrompt.ts
* @description 自定义公共确认窗口组件UI 视图层)
*
* 职责:
* 1. 替换框架插件 CommonPromptextensions/oops-plugin-framework/.../CommonPrompt.ts
* 脚本类名、onTouchEnd 处理器与 4 个属性名保持同名window.prefab 无需改动即可自动重链。
* 2. 内容区由 cc.Label 升级为 cc.RichText支持 <color>/<size>/<b> 等富文本标签。
* 3. 扩展标题样式、窗体缩放动画、遮罩点击关闭等自定义能力。
*
* 使用oops.gui 模块):
* oops.gui.open(UIID.Window, {
* title: "标题",
* content: "支持 <color=#ff0000>富文本</color>",
* 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 富文本,支持 <color>/<size>/<b>/<i>/<outline> 等标签) */
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 的绑定约定:
* - 根节点按钮事件 handleronTouchEndbtn_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);
}
}