using System; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using TMPro; using BaseGames.Core; using BaseGames.Core.Events; using BaseGames.Core.Save; namespace BaseGames.UI.Menus { /// /// 驱动主菜单存档槽选择面板(新游戏 / 继续 / 删除)。 /// public class SaveSlotController : MonoBehaviour { [SerializeField] private SaveSlotUI[] _slotUIs; // 存档槽 UI(数量由 Inspector 决定) [Header("Event Channels")] [SerializeField] private IntEventChannelSO _onSlotConfirmed; // 携带槽索引,供 GameManager 监听 private void OnEnable() { var task = RefreshAsync(); task.ContinueWith(t => { if (t.IsFaulted) Debug.LogException(t.Exception?.InnerException ?? t.Exception, this); }, TaskScheduler.FromCurrentSynchronizationContext()); } private async Task RefreshAsync() { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; for (int i = 0; i < _slotUIs.Length; i++) { if (_slotUIs[i] == null) continue; var summary = await svc.GetSlotSummaryAsync(i); _slotUIs[i].Refresh(summary); } } /// 选中指定槽位(新局或继续)。由 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(); } } /// /// 单个存档槽卡片组件,负责显示存档摘要或空槽提示。 /// 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; 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(() => _controller.OnSlotDeleteRequested(_slotIndex)); } } /// 用摘要数据刷新显示;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); if (!hasData) return; if (_playtimeText != null) _playtimeText.text = FormatPlaytime(summary.Playtime); if (_regionText != null) _regionText.text = summary.SceneName ?? string.Empty; if (_lastSavedText != null) _lastSavedText.text = FormatDateTime(summary.LastSaved); } 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; } } }