feat(ui): 新增通用确认弹窗并重构英雄出售逻辑
1. 新增CommonPrompt通用确认弹窗组件,支持富文本内容、自定义按钮与弹窗动画 2. 在UIID配置中新增Window弹窗枚举与对应UI配置 3. 重构HeroBoxComp的出售逻辑,添加二次确认防止误触出售英雄
This commit is contained in:
226
assets/script/game/common/CommonPrompt.ts
Normal file
226
assets/script/game/common/CommonPrompt.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* @file CommonPrompt.ts
|
||||
* @description 自定义公共确认窗口组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 替换框架插件 CommonPrompt(extensions/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 的绑定约定:
|
||||
* - 根节点按钮事件 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);
|
||||
}
|
||||
}
|
||||
9
assets/script/game/common/CommonPrompt.ts.meta
Normal file
9
assets/script/game/common/CommonPrompt.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8c571795-0ff9-4765-ad7e-1de7f1cdcb3c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user