using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using BaseGames.Core.Events; namespace BaseGames.UI { /// /// 可复用标签组(从 的 Tab 逻辑提取)。 /// /// 职责:管理一组 Tab(内容根 + 头部按钮 + 高亮),处理 Next/Prev/Select 切换、 /// 头部高亮、焦点委托(内容若实现 则接管,否则聚焦头部按钮), /// 可选跨重建记忆当前 Tab,并通过 C# 事件 + 可选 SO 频道广播索引。 /// /// 解耦:不引用任何具体 Tab 类型,只持有 Tab 根 。 /// 由宿主面板在 OnEnable 中调用 ,并把肩键频道接到 /。 /// public class UITabGroup : MonoBehaviour { [Serializable] public struct Tab { [Tooltip("Tab 内容根节点(激活时 SetActive(true))。")] public GameObject content; [Tooltip("Tab 头部按钮(点击跳转;高亮显示当前选中)。可空。")] public Button headerButton; [Tooltip("Tab 头部高亮节点(选中时 SetActive(true))。可空。")] public GameObject headerHighlight; } [Header("Tabs(按显示顺序)")] [SerializeField] private Tab[] _tabs; [Tooltip("默认打开的 Tab 索引。")] [SerializeField] private int _defaultIndex = 0; [Tooltip("是否记住上次停留的 Tab(跨面板重开)。")] [SerializeField] private bool _rememberLast = true; [Tooltip("跨重建记忆用的键;为空则只在本实例存活期间记忆。")] [SerializeField] private string _persistKey = ""; [Header("Event Channels(可选)")] [Tooltip("当前 Tab 变化时广播索引。可空。")] [SerializeField] private IntEventChannelSO _onTabChanged; /// 当前 Tab 变化时触发(参数为新索引)。 public event Action TabChanged; public int CurrentIndex { get; private set; } = -1; public int TabCount => _tabs?.Length ?? 0; // 跨重建记忆(静态:面板/组件被销毁重建后仍保留) private static readonly Dictionary s_persist = new(); private bool _wired; private void Awake() => EnsureWired(); private void EnsureWired() { if (_wired || _tabs == null) return; for (int i = 0; i < _tabs.Length; i++) { int captured = i; if (_tabs[i].headerButton != null) { _tabs[i].headerButton.onClick.RemoveAllListeners(); _tabs[i].headerButton.onClick.AddListener(() => Select(captured)); } if (_tabs[i].content != null) _tabs[i].content.SetActive(false); if (_tabs[i].headerHighlight != null) _tabs[i].headerHighlight.SetActive(false); } _wired = true; } /// 激活标签组并打开起始 Tab(默认或记忆)。宿主面板在 OnEnable 中调用。 public void Activate() { EnsureWired(); if (TabCount == 0) return; int start = _rememberLast && TryGetPersisted(out var p) ? p : _defaultIndex; CurrentIndex = -1; // 强制 Select 执行切换 Select(Mathf.Clamp(start, 0, TabCount - 1)); } public void Next() => Step(+1); public void Prev() => Step(-1); private void Step(int dir) { if (TabCount == 0) return; int next = CurrentIndex; for (int i = 0; i < _tabs.Length; i++) // 跳过未配置 content 的空槽 { next = (next + dir + _tabs.Length) % _tabs.Length; if (_tabs[next].content != null) break; } Select(next); } /// 切换到指定 Tab;无效或与当前相同则忽略。 public void Select(int index) { EnsureWired(); if (TabCount == 0) return; index = Mathf.Clamp(index, 0, _tabs.Length - 1); if (index == CurrentIndex) return; // 关闭旧 Tab if (CurrentIndex >= 0 && CurrentIndex < _tabs.Length) { if (_tabs[CurrentIndex].content != null) _tabs[CurrentIndex].content.SetActive(false); if (_tabs[CurrentIndex].headerHighlight != null) _tabs[CurrentIndex].headerHighlight.SetActive(false); } CurrentIndex = index; SetPersisted(index); // 打开新 Tab var tab = _tabs[index]; if (tab.content != null) tab.content.SetActive(true); if (tab.headerHighlight != null) tab.headerHighlight.SetActive(true); FocusCurrent(); _onTabChanged?.Raise(index); TabChanged?.Invoke(index); } /// 把焦点交给当前 Tab(内容 IFocusable 优先,否则其头部按钮)。供宿主 OnFocusRestored 调用。 public void FocusCurrent() { if (CurrentIndex < 0 || CurrentIndex >= TabCount) return; var tab = _tabs[CurrentIndex]; var focusable = tab.content != null ? tab.content.GetComponent() : null; if (focusable != null) focusable.OnFocusRestored(); else if (tab.headerButton != null && EventSystem.current != null) EventSystem.current.SetSelectedGameObject(tab.headerButton.gameObject); } // ── 记忆 ────────────────────────────────────────────────────────────── private bool TryGetPersisted(out int index) { if (!string.IsNullOrEmpty(_persistKey) && s_persist.TryGetValue(_persistKey, out index)) return true; if (CurrentIndex >= 0) { index = CurrentIndex; return true; } index = -1; return false; } private void SetPersisted(int index) { if (!string.IsNullOrEmpty(_persistKey)) s_persist[_persistKey] = index; } } }