using System; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using TMPro; using BaseGames.Core; using BaseGames.Core.Events; using BaseGames.Core.Save; using BaseGames.Localization; namespace BaseGames.UI.Menus { /// /// 驱动主菜单存档槽选择面板(新游戏 / 继续 / 删除)。 /// public class SaveSlotController : MonoBehaviour { [SerializeField] private SaveSlotUI[] _slotUIs; [Header("Event Channels")] [SerializeField] private IntEventChannelSO _onSlotConfirmed; private CancellationTokenSource _cts; private void Awake() { for (int i = 0; i < _slotUIs.Length; i++) if (_slotUIs[i] != null) _slotUIs[i].Init(i, this); } private void OnEnable() { _cts = new CancellationTokenSource(); var ct = _cts.Token; var task = RefreshAsync(ct); task.ContinueWith(t => { if (t.IsFaulted && !(t.Exception?.InnerException is OperationCanceledException)) Debug.LogException(t.Exception?.InnerException ?? t.Exception, this); }, TaskScheduler.FromCurrentSynchronizationContext()); } private void OnDisable() { _cts?.Cancel(); _cts?.Dispose(); _cts = null; } private async Task RefreshAsync(CancellationToken ct = default) { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; var tasks = new Task[_slotUIs.Length]; for (int i = 0; i < _slotUIs.Length; i++) tasks[i] = svc.GetSlotSummaryAsync(i); var summaries = await Task.WhenAll(tasks); ct.ThrowIfCancellationRequested(); // 组件已失活时不写 UI for (int i = 0; i < _slotUIs.Length; i++) { if (_slotUIs[i] == null) continue; _slotUIs[i].Refresh(summaries[i]); } } /// 选中指定槽位(新局或继续)。由 SaveSlotUI 内部按钮调用。 public void OnSlotSelected(int slotIndex) { if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return; _ = SelectSlotAsync(slotIndex); } private async Task SelectSlotAsync(int slotIndex) { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; if (svc.HasSave(slotIndex)) await svc.LoadAsync(slotIndex); else svc.CreateSlot(slotIndex); _onSlotConfirmed?.Raise(slotIndex); } /// 删除指定槽位存档并刷新 UI。由 SaveSlotUI 内部按钮调用。 public void OnSlotDeleteRequested(int slotIndex) { if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return; _ = DeleteAndRefreshAsync(slotIndex); } private async Task DeleteAndRefreshAsync(int slotIndex) { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; await svc.DeleteSlotAsync(slotIndex); await RefreshAsync(_cts?.Token ?? CancellationToken.None); } } /// /// 单个存档槽卡片组件,负责显示存档摘要或空槽提示。 /// public class SaveSlotUI : MonoBehaviour { [SerializeField] private TMP_Text _playtimeText; [SerializeField] private TMP_Text _regionText; [SerializeField] private TMP_Text _lastSavedText; [SerializeField] private Image _formIcon; [SerializeField] private GameObject _emptyIndicator; // 空槽时显示的"新游戏"提示 [SerializeField] private GameObject _dataIndicator; // 有数据时显示的内容根 [SerializeField] private Button _selectButton; [SerializeField] private Button _deleteButton; [Header("删除确认(可选)")] [Tooltip("删除确认对话框根节点,为 null 时删除按钮直接执行。")] [SerializeField] private GameObject _deleteConfirmRoot; [Tooltip("确认删除按钮。")] [SerializeField] private Button _btnConfirmDelete; [Tooltip("取消删除按钮。")] [SerializeField] private Button _btnCancelDelete; private int _slotIndex; private SaveSlotController _controller; /// 由 SaveSlotController 在 Awake 或初始化时调用以完成按钮绑定。 public void Init(int slotIndex, SaveSlotController controller) { _slotIndex = slotIndex; _controller = controller; if (_selectButton != null) { _selectButton.onClick.RemoveAllListeners(); _selectButton.onClick.AddListener(() => _controller.OnSlotSelected(_slotIndex)); } if (_deleteButton != null) { _deleteButton.onClick.RemoveAllListeners(); // 配置了确认对话框时先弹出确认,未配置时直接删除 _deleteButton.onClick.AddListener(ShowDeleteConfirm); } if (_btnConfirmDelete != null) { _btnConfirmDelete.onClick.RemoveAllListeners(); _btnConfirmDelete.onClick.AddListener(() => { HideDeleteConfirm(); _controller.OnSlotDeleteRequested(_slotIndex); }); } if (_btnCancelDelete != null) { _btnCancelDelete.onClick.RemoveAllListeners(); _btnCancelDelete.onClick.AddListener(HideDeleteConfirm); } HideDeleteConfirm(); } /// 用摘要数据刷新显示;summary 为 null 表示空槽。 public void Refresh(SlotSummary summary) { bool hasData = summary != null; if (_emptyIndicator != null) _emptyIndicator.SetActive(!hasData); if (_dataIndicator != null) _dataIndicator.SetActive(hasData); if (_deleteButton != null) _deleteButton.gameObject.SetActive(hasData); // 刷新时重置确认对话框状态 HideDeleteConfirm(); if (!hasData) return; if (_playtimeText != null) _playtimeText.text = FormatPlaytime(summary.Playtime); if (_regionText != null) { string key = summary.SceneName ?? string.Empty; string loc = !string.IsNullOrEmpty(key) ? LocalizationManager.Get(key, LocalizationTable.UI) : null; _regionText.text = !string.IsNullOrEmpty(loc) && loc != key ? loc : key; } if (_lastSavedText != null) _lastSavedText.text = FormatDateTime(summary.LastSaved); } // ── 删除确认 ────────────────────────────────────────────────────────── /// /// 显示删除确认对话框。 /// 未配置 _deleteConfirmRoot 时直接执行删除(向下兼容旧 Prefab)。 /// private void ShowDeleteConfirm() { if (_deleteConfirmRoot == null) { // 旧 Prefab 未添加确认根节点,直接删除 _controller.OnSlotDeleteRequested(_slotIndex); return; } _deleteConfirmRoot.SetActive(true); // 手柄导航:将焦点移至"确认"按钮,防止误触选择按钮 EventSystem.current?.SetSelectedGameObject(_btnConfirmDelete?.gameObject); } private void HideDeleteConfirm() { if (_deleteConfirmRoot != null) _deleteConfirmRoot.SetActive(false); } // ── 格式化工具 ──────────────────────────────────────────────────────── private static string FormatPlaytime(float seconds) { int h = (int)(seconds / 3600); int m = (int)((seconds % 3600) / 60); int s = (int)(seconds % 60); return $"{h:D2}:{m:D2}:{s:D2}"; } private static string FormatDateTime(string iso8601) { if (string.IsNullOrEmpty(iso8601)) return string.Empty; if (DateTime.TryParse(iso8601, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind, out DateTime dt)) { return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm"); } return iso8601; } } }