feat(ui): 新增通用确认弹窗并重构英雄出售逻辑

1. 新增CommonPrompt通用确认弹窗组件,支持富文本内容、自定义按钮与弹窗动画
2. 在UIID配置中新增Window弹窗枚举与对应UI配置
3. 重构HeroBoxComp的出售逻辑,添加二次确认防止误触出售英雄
This commit is contained in:
panFD
2026-08-01 23:05:54 +08:00
parent 11ae66fe1d
commit 53fa0d7dc0
5 changed files with 659 additions and 268 deletions

View File

@@ -0,0 +1,226 @@
/**
* @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);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8c571795-0ff9-4765-ad7e-1de7f1cdcb3c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -12,6 +12,8 @@ export enum UIID {
Loading = 1,
/** 加载与延时提示界面 */
Netinstable,
/** 公共确认窗口(标题 + 内容 + 确定/取消) */
Window,
/** 角色控制 */
Role_Controller,
Victory,
@@ -34,6 +36,7 @@ export enum UIID {
export var UIConfigData: { [key: number]: UIConfig } = {
[UIID.Loading]: { layer: LayerType.UI, prefab: "loading/prefab/loading", bundle: "resources" },
[UIID.Netinstable]: { layer: LayerType.PopUp, prefab: "common/prefab/netinstable" },
[UIID.Window]: { layer: LayerType.Dialog, prefab: "common/prefab/window" },
[UIID.Role_Controller]: { layer: LayerType.UI, prefab: "gui/role_controller" },
[UIID.Victory]: { layer: LayerType.UI, prefab: "gui/element/victory" },
[UIID.IBox]: { layer: LayerType.PopUp, prefab: "gui/element/ibox" },