Files
pixelheros/assets/script/game/common/CommonPrompt.ts
panFD 401a5daf77 refactor(common prompt,hero box): 优化弹窗绑定逻辑与英雄出售限制
1. 重构CommonPrompt的编辑器属性绑定,添加按钮节点序列化引用并优化兜底查找逻辑
2. 为英雄出售操作添加存活数量校验,场上仅1个英雄时禁止出售
3. 新增统一的存活英雄统计工具方法
4. 补充Claude工具命令配置
2026-08-02 09:42:30 +08:00

234 lines
8.6 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({ type: Label, displayName: "标题文本" })
private lab_title: Label | null = null;
/** 窗口内容(富文本,需在 prefab 中将 Label 换为 RichText 后重新拖入) */
@property({ type: RichText, displayName: "内容富文本" })
private lab_content: RichText | null = null;
/** 确定按钮文本 */
@property({ type: Label, displayName: "确定按钮文本" })
private lab_ok: Label | null = null;
/** 取消按钮文本 */
@property({ type: Label, displayName: "取消按钮文本" })
private lab_cancel: Label | null = null;
/** 确定按钮节点 */
@property({ type: Node, displayName: "确定按钮" })
private btn_ok: Node | null = null;
/** 取消按钮节点 */
@property({ type: Node, displayName: "取消按钮" })
private btn_cancel: Node | null = null;
/** 运行时参数 */
private config: WindowPromptParams = {};
/** 弹窗打开动画已播放标记(防止 onAdded 重复触发) */
private animPlayed: boolean = false;
onLoad() {
// 优先使用编辑器绑定的按钮节点;未绑定时按节点名查找兜底
if (!this.btn_ok) {
this.btn_ok = this.node.getChildByName("Node")?.getChildByName("btn_ok") ?? null;
}
if (!this.btn_cancel) {
this.btn_cancel = this.node.getChildByName("Node")?.getChildByName("btn_cancel") ?? null;
}
this.btn_ok?.on(Button.EventType.CLICK, this.onOk, this);
this.btn_cancel?.on(Button.EventType.CLICK, this.onCancel, this);
// 遮罩点击关闭btn_sky 为 btn_ok 内嵌子节点,无 Button 组件,直接监听触摸事件)
this.btn_ok?.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.btn_ok?.getChildByName("lab_ok")?.getComponent(Label) ?? null;
}
if (!this.lab_cancel) {
this.lab_cancel = this.btn_cancel?.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.btn_ok?.off(Button.EventType.CLICK, this.onOk, this);
this.btn_cancel?.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);
}
}