This commit is contained in:
2026-06-07 11:49:55 +08:00
parent ff0f3bde54
commit 1897658a00
98 changed files with 9903 additions and 13907 deletions

View File

@@ -1,31 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
using BaseGames.UI;
using BaseGames.Localization;
namespace BaseGames.UI.Menus
{
/// <summary>
/// 通用模态确认对话框(是/否):用于删除 / 覆盖 / 退出等确认场景。
/// 通用模态确认对话框(是/否):用于删除 / 覆盖 / 退出 / 传送等确认场景。
///
/// 设计:
/// · 自包含、场景无关——通过本地 SetActive 显隐 + 回调 API 工作,不依赖 UIManager 面板栈,
/// 因此既能用于主菜单场景(不走 UIManager也能在游戏内复用。
/// · 标题 / 正文 / 按钮文案均走本地化键LocalizationManager.Get传 null 则保留 Inspector 原文。
/// · 默认焦点置于"取消"按钮,防止手柄连按误触确认(破坏性操作安全默认)。
/// <para>双调用模式(同类不同实例各用其一,互不冲突):</para>
/// <list type="bullet">
/// <item><b>导航器 async主菜单</b><see cref="ShowAsync"/> 经 <see cref="IUINavigator"/> 压栈,
/// 栈式回退、ESC 取消、焦点恢复统一由导航器负责。</item>
/// <item><b>回调 legacy游戏内地图传送等尚未接入导航器的场景</b><see cref="Show"/> 本地 SetActive
/// 显隐 + onConfirm/onCancel 回调,不依赖导航器。</item>
/// </list>
///
/// 用法:
/// _confirmDialog.Show("CONFIRM_DELETE_TITLE", "CONFIRM_DELETE_BODY",
/// onConfirm: () => DoDelete(),
/// onCancel: () => {});
/// 标题 / 正文 / 按钮文案走本地化键(<see cref="LocalizationManager"/>);传 null 保留 Inspector 原文。
/// 默认焦点置于"取消",防手柄/键盘连按误触破坏性确认。
/// </summary>
public class ConfirmDialogController : MonoBehaviour
public class ConfirmDialogController : UIResultPanel<bool>
{
[Header("根节点(显隐用,留空则用本 GameObject")]
[SerializeField] private GameObject _root;
[Header("文本")]
[SerializeField] private TMP_Text _titleText;
[SerializeField] private TMP_Text _bodyText;
@@ -38,78 +37,89 @@ namespace BaseGames.UI.Menus
[SerializeField] private Button _btnConfirm;
[SerializeField] private Button _btnCancel;
private Action _onConfirm;
private Action _onCancel;
// 取消 / ESC / 销毁默认结果:否。
protected override bool CancelResult => false;
// legacy 回调模式状态
private Action _legacyConfirm;
private Action _legacyCancel;
private bool _legacyMode;
private void Awake()
{
_btnConfirm?.onClick.AddListener(HandleConfirm);
_btnCancel? .onClick.AddListener(HandleCancel);
SetVisible(false);
_btnConfirm?.onClick.AddListener(() => OnButton(true));
_btnCancel? .onClick.AddListener(() => OnButton(false));
// 不在此 SetActive(false):面板初始由场景/脚手架序列化为隐藏,激活完全交给导航器
// (对象初始 inactive 时 Awake 会被推迟到首次激活才执行,若在此关闭会立刻自我隐藏)。
}
/// <summary>
/// 弹出确认框。
/// </summary>
/// <param name="titleKey">标题本地化键null 保留原文。</param>
/// <param name="bodyKey">正文本地化键null 保留原文。</param>
/// <param name="onConfirm">点击确认后回调(确认框已自动关闭)。</param>
/// <param name="onCancel">点击取消后回调(可选)。</param>
/// <param name="confirmKey">确认按钮文案本地化键(可选)。</param>
/// <param name="cancelKey">取消按钮文案本地化键(可选)。</param>
/// <summary>默认焦点:取消按钮(破坏性操作安全默认)。</summary>
protected override GameObject ResolveFirstSelected()
=> _btnCancel != null ? _btnCancel.gameObject
: _btnConfirm != null ? _btnConfirm.gameObject : null;
// ── 导航器 async 路径(主菜单)────────────────────────────────────────
/// <summary>弹出确认框并等待结果true=确认 / false=取消)。由导航器压栈管理。</summary>
public Task<bool> ShowAsync(string titleKey, string bodyKey, CancellationToken ct = default,
string confirmKey = null, string cancelKey = null)
{
_legacyMode = false;
ApplyText(titleKey, bodyKey, confirmKey, cancelKey);
var nav = GetService<IUINavigator>();
if (nav == null)
{
Debug.LogError("[ConfirmDialog] 未找到 IUINavigator 服务,无法以 async 模式弹出。", this);
return Task.FromResult(false);
}
return nav.PushForResultAsync<bool>(this, ct);
}
// ── legacy 回调路径(游戏内尚未接入导航器的调用方)──────────────────
/// <summary>弹出确认框(回调式,本地显隐,不走导航器)。</summary>
public void Show(string titleKey, string bodyKey, Action onConfirm, Action onCancel = null,
string confirmKey = null, string cancelKey = null)
{
_onConfirm = onConfirm;
_onCancel = onCancel;
if (_titleText != null && titleKey != null) _titleText.text = Loc(titleKey);
if (_bodyText != null && bodyKey != null) _bodyText.text = Loc(bodyKey);
if (_confirmLabel != null && confirmKey != null) _confirmLabel.text = Loc(confirmKey);
if (_cancelLabel != null && cancelKey != null) _cancelLabel.text = Loc(cancelKey);
SetVisible(true);
// 安全默认:焦点置于取消,避免手柄/键盘连按直接确认破坏性操作
EventSystem.current?.SetSelectedGameObject(_btnCancel != null
? _btnCancel.gameObject
: _btnConfirm?.gameObject);
_legacyMode = true;
_legacyConfirm = onConfirm;
_legacyCancel = onCancel;
ApplyText(titleKey, bodyKey, confirmKey, cancelKey);
gameObject.SetActive(true); // OnEnable 经 UIPanelBase 自动聚焦取消按钮
}
/// <summary>外部强制关闭(如父面板被关闭时)。不触发任何回调。</summary>
/// <summary>外部强制关闭(仅 legacy 模式有效)。不触发任何回调。</summary>
public void Close()
{
_onConfirm = null;
_onCancel = null;
SetVisible(false);
if (!_legacyMode) return;
_legacyConfirm = null;
_legacyCancel = null;
gameObject.SetActive(false);
}
// ── 按钮回调 ──────────────────────────────────────────────────────────
private void HandleConfirm()
// ── 按钮 ──────────────────────────────────────────────────────────────
private void OnButton(bool confirmed)
{
var cb = _onConfirm;
SetVisible(false);
_onConfirm = null;
_onCancel = null;
cb?.Invoke();
}
private void HandleCancel()
{
var cb = _onCancel;
SetVisible(false);
_onConfirm = null;
_onCancel = null;
cb?.Invoke();
if (_legacyMode)
{
var cb = confirmed ? _legacyConfirm : _legacyCancel;
_legacyConfirm = null;
_legacyCancel = null;
gameObject.SetActive(false);
cb?.Invoke();
}
else
{
Complete(confirmed); // 设置结果 + 由导航器出栈
}
}
// ── 工具 ──────────────────────────────────────────────────────────────
private void SetVisible(bool visible)
private void ApplyText(string titleKey, string bodyKey, string confirmKey, string cancelKey)
{
var go = _root != null ? _root : gameObject;
go.SetActive(visible);
if (_titleText != null && titleKey != null) _titleText.text = Loc(titleKey);
if (_bodyText != null && bodyKey != null) _bodyText.text = Loc(bodyKey);
if (_confirmLabel != null && confirmKey != null) _confirmLabel.text = Loc(confirmKey);
if (_cancelLabel != null && cancelKey != null) _cancelLabel.text = Loc(cancelKey);
}
private static string Loc(string key)