using AppUI.Localization; using AppUI.Manager; using AppUI.Manager.Role; using AppUI.Util.Avatar; using JCUnityLib; using JCUnityLib.UI; using System.Collections; using System.Text.RegularExpressions; using TMPro; using UnityEngine; namespace AppUI.View { /// /// AppUI 注册向导:账号密码 → 短信/邮箱验证码并注册 → 昵称与预设头像。 /// 全部输入使用 ;提示走 ,网络复用旧版 Controller。 /// public class RegisterView : MonoBehaviour { enum RegisterPhase { AccountPassword, SmsAndRegister, Profile } RegisterPhase _phase = RegisterPhase.AccountPassword; [Header("阶段面板(可选,未绑定则仅逻辑推进)")] [SerializeField] GameObject panelAccountPassword; [SerializeField] GameObject panelSms; [SerializeField] GameObject panelProfile; [Header("阶段一:账号 / 密码(可 Inspector 绑定,或运行时按路径查找)")] [SerializeField] TMP_InputField accountInput; [SerializeField] TMP_InputField passwordInput; [Header("阶段二:短信验证码")] [SerializeField] TMP_InputField smsCodeInput; [SerializeField] TMP_Text sendSmsCountdownLabel; [Header("阶段三:昵称 / 预设头像")] [SerializeField] TMP_InputField nicknameInput; [SerializeField] UnityEngine.UI.Image avatarPreviewImage; [Tooltip("头像列表与 Sprite 数据源;Sprite 仅在此组件的数组中配置一份即可")] [SerializeField] AvatarGroup avatarGroup; [Header("拼图验证(与旧版 RegisterView 一致;未赋值则直接请求发送验证码)")] [SerializeField] GameObject prefabValidateJigsawView; [Header("行为")] [SerializeField] bool resetFlowOnEnable = true; // TODO(AppUI.RegisterView): 若注册前必须勾选用户协议,取消注释并在 OnClick_ConfirmRegister 开头调用。 // [SerializeField] bool requireAgreementBeforeRegister = true; string _storedAccount = ""; string _storedPassword = ""; string _registrationTicket = ""; int _selectedAvatarIndex; static long _lastSendSmsMs; Coroutine _sendCooldownRoutine; bool _sendCooldownActive; readonly Throttler _throttlerRegister = new Throttler(3000); readonly Throttler _throttlerProfileSave = new Throttler(2000); void OnEnable() { if (resetFlowOnEnable) ResetFlowToAccountPassword(); } void Awake() { InitPasswordAlphanumericFilter(); } void Start() { WireAvatarGroup(); } void OnDestroy() { if (avatarGroup != null) avatarGroup.OnAvatarSelected -= OnAvatarGroupSelection; } /// /// 将 的写入目标设为 ,并订阅选中事件以同步 。 /// void WireAvatarGroup() { if (avatarGroup == null) return; if (avatarPreviewImage != null) avatarGroup.targetImage = avatarPreviewImage; avatarGroup.OnAvatarSelected -= OnAvatarGroupSelection; avatarGroup.OnAvatarSelected += OnAvatarGroupSelection; avatarGroup.SelectByIndex(0, notify: false); _selectedAvatarIndex = avatarGroup.SelectedIndex; } void OnAvatarGroupSelection(int index, Sprite _) { _selectedAvatarIndex = index; } void InitPasswordAlphanumericFilter() { if (passwordInput == null) return; passwordInput.onValueChanged.AddListener(text => { var match = new Regex("[^A-Za-z0-9]").Match(text); if (match.Success) passwordInput.text = text.Replace(match.Value, ""); }); } /// 外部(例如从登录页)切回本页时可调用,清空状态与 UI。 public void ResetFlowToAccountPassword() { _phase = RegisterPhase.AccountPassword; _storedAccount = ""; _storedPassword = ""; _registrationTicket = ""; _selectedAvatarIndex = 0; if (avatarGroup != null) avatarGroup.SelectByIndex(0, notify: false); if (smsCodeInput != null) smsCodeInput.text = ""; if (nicknameInput != null) nicknameInput.text = ""; ApplyPhasePanels(); } void ApplyPhasePanels() { if (panelAccountPassword != null) panelAccountPassword.SetActive(_phase == RegisterPhase.AccountPassword); if (panelSms != null) panelSms.SetActive(_phase == RegisterPhase.SmsAndRegister); if (panelProfile != null) panelProfile.SetActive(_phase == RegisterPhase.Profile); } bool IsDomesticPhoneMode() => CommonConfig.AppArea == 0; void ShowTip(string message) { ModuleViewMgr.ins.Show(message); } /// 阶段一「下一步」:校验账号密码后进入验证码阶段。 public void OnClick_ContinueToSmsStep() { if (accountInput == null || passwordInput == null) { Debug.LogError("[AppUI.RegisterView] 未绑定账号或密码 TMP_InputField。"); return; } string account = accountInput.text.Trim(); if (account.Length == 0) { ShowTip(AppUILocalization.GetTextByCNKey("请输入账号")); return; } if (IsDomesticPhoneMode()) { if (!ValidateHelper.IsMobilePhone(account)) { ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a1")); return; } } else { if (!ValidateHelper.IsEmail(account)) { ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a0")); return; } } string pwd = passwordInput.text.Trim(); if (pwd.Length == 0) { ShowTip(AppUILocalization.GetTextByCNKey("请输入密码")); return; } if (pwd.Length < 6) { ShowTip(AppUILocalization.GetTextByCNKey("密码长度至少6位")); return; } _storedAccount = account; _storedPassword = pwd; _phase = RegisterPhase.SmsAndRegister; ApplyPhasePanels(); //发送验证码 OnClick_SendSmsCode(); } /// 发送短信或邮箱验证码(与旧版 对齐;限流 60s)。 public void OnClick_SendSmsCode() { long gap = TimeUtils.GetTimestamp() - _lastSendSmsMs; const long maxMs = 60 * 1000; if (gap < maxMs) { long second = (maxMs - gap) / 1000; if (second <= 0) second = 1; ShowTip(string.Format(AppUILocalization.GetTextByKey("RelateValidateView-a2"), second)); return; } if (_sendCooldownActive) { return; } if (IsDomesticPhoneMode()) { if (!ValidateHelper.IsMobilePhone(_storedAccount)) { ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a1")); return; } } else { if (!ValidateHelper.IsEmail(_storedAccount)) { ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a0")); return; } } if (prefabValidateJigsawView != null) { var go = Instantiate(prefabValidateJigsawView); var jigsaw = go.GetComponent(); if (jigsaw != null) { jigsaw.onComplete = RequestSendValidateCode; jigsaw.SetTextLabel(AppUILocalization.GetTextByKey("ValidateJigsawView_label")); jigsaw.SetTextTip(AppUILocalization.GetTextByKey("ValidateJigsawView_tip")); jigsaw.SetTextOK(AppUILocalization.GetTextByKey("ValidateJigsawView_ok")); } else { Destroy(go); RequestSendValidateCode(); } } else { RequestSendValidateCode(); } } void RequestSendValidateCode() { _lastSendSmsMs = JCUnityLib.TimeUtils.GetTimestamp(); if (_sendCooldownRoutine != null) StopCoroutine(_sendCooldownRoutine); _sendCooldownRoutine = StartCoroutine(SendSmsCooldownUiRoutine()); if (IsDomesticPhoneMode()) { StartCoroutine(PhoneValidateController.Instance.SendPhoneValidateCode(_storedAccount, res => { if (res.code == 0) ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b2")); else ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b3")); })); } else { StartCoroutine(EmailValidateController.Instance.SendEmailValidateCode(_storedAccount, res => { if (res.code == 0) ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b0")); else ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b1")); })); } } IEnumerator SendSmsCooldownUiRoutine() { _sendCooldownActive = true; const float maxSec = 60f; float left = maxSec; while (left > 0f) { if (sendSmsCountdownLabel != null) sendSmsCountdownLabel.text = string.Format(AppUILocalization.GetTextByKey("appui-resend"),"("+ Mathf.CeilToInt(left) + " S)"); left -= Time.deltaTime; yield return null; } _sendCooldownActive = false; if (sendSmsCountdownLabel != null) sendSmsCountdownLabel.text = sendSmsCountdownLabel.text = string.Format(AppUILocalization.GetTextByKey("appui-resend"), ""); _sendCooldownRoutine = null; } /// 提交验证码并调用注册接口;失败提示,成功进入资料步骤。 public void OnClick_ConfirmRegister() { // TODO(AppUI.RegisterView): 需要协议时在此校验 AgreenmentOption.ins.IsAgreementChecked()。 if (smsCodeInput == null) { Debug.LogError("[AppUI.RegisterView] 未绑定验证码 TMP_InputField。"); return; } string codeText = smsCodeInput.text.Trim(); if (codeText.Length != 6) { ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a3")); return; } if (!int.TryParse(codeText, out int codeValue)) { ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a3")); return; } if (!_throttlerRegister.CanPass()) { ShowTip(AppUILocalization.GetTextByCNKey("操作过于频繁")); return; } string emailField = IsDomesticPhoneMode() ? "" : _storedAccount; string phoneField = IsDomesticPhoneMode() ? _storedAccount : ""; StartCoroutine(LoginController.Instance.RegisterByCode( _storedAccount, _storedPassword, emailField, phoneField, codeValue, res => { ShowTip(AppUILocalization.GetTextByCNKey(res.msg)); if (res.code != 0) return; _registrationTicket = LoginController.TryGetRegistrationTicketFromData(res.data); if (string.IsNullOrEmpty(_registrationTicket)) { ShowTip(AppUILocalization.GetTextByCNKey("注册成功但缺少资料票据,请重新登录后完善资料")); return; } _phase = RegisterPhase.Profile; if (nicknameInput != null) { nicknameInput.text = _storedAccount; } ApplyPhasePanels(); TryRefreshAvatarPreview(); var loginView = FindObjectOfType(); loginView?.FillLoginInput(_storedAccount, _storedPassword); })); } /// 预设头像列表项按钮可绑定:索引与 内 Sprite 数组一致。 public void OnSelectPresetAvatar(int index) { if (avatarGroup == null) return; var max = Mathf.Max(0, avatarGroup.AvatarCount - 1); avatarGroup.SelectByIndex(Mathf.Clamp(index, 0, max), notify: true); } /// Inspector 无法直接传 int 参数时,可用多个按钮分别绑定下列方法。 public void OnSelectPresetAvatar0() => OnSelectPresetAvatar(0); public void OnSelectPresetAvatar1() => OnSelectPresetAvatar(1); public void OnSelectPresetAvatar2() => OnSelectPresetAvatar(2); public void OnSelectPresetAvatar3() => OnSelectPresetAvatar(3); public void OnSelectPresetAvatar4() => OnSelectPresetAvatar(4); public void OnSelectPresetAvatar5() => OnSelectPresetAvatar(5); void TryRefreshAvatarPreview() { if (avatarPreviewImage == null || avatarGroup == null) return; var sp = avatarGroup.GetSelectedSprite(); if (sp != null) avatarPreviewImage.sprite = sp; } /// 返回上一阶段(供 UI 返回键绑定)。 public void OnClick_Back() { switch (_phase) { case RegisterPhase.SmsAndRegister: _phase = RegisterPhase.AccountPassword; ApplyPhasePanels(); break; case RegisterPhase.Profile: // TODO(AppUI.RegisterView): 注册已成功落库时是否允许退回验证码阶段需产品确认;当前仅回到短信页并保留已填昵称。 _phase = RegisterPhase.SmsAndRegister; ApplyPhasePanels(); break; default: // TODO(AppUI.RegisterView): 若需关闭注册页回到登录,在此调用 LoginMgr.showLoginView() 或关闭当前弹窗。 break; } } /// /// TODO(AppUI.RegisterView): 接入 CommonConfig.banBindRelateAccount 为 true 时的 Register2(邮箱+手机双绑)分支时在此封装专用提交流程。 /// public void OnClick_RegisterWithBoundContacts_Todo() { } /// /// TODO(AppUI.RegisterView): 从相册/拍照选取自定义头像后得到 Texture2D,再调用 LoginController.UploadAvatar。 /// public void OnClick_PickCustomAvatarFromGallery_Todo() { } /// 资料提交:完善用户信息;头像上传在登录拿 token 后尝试。 public void OnClick_CompleteProfile() { if (nicknameInput == null) { Debug.LogError("[AppUI.RegisterView] 未绑定昵称 TMP_InputField。"); return; } string nickname = nicknameInput.text.Trim(); if (nickname.Length == 0) { ShowTip(AppUILocalization.GetTextByCNKey("请输入游戏昵称")); return; } if (!_throttlerProfileSave.CanPass()) { ShowTip(AppUILocalization.GetTextByCNKey("操作过于频繁")); return; } const int defaultGender = 1; const string defaultBirthday = "2000-01-01"; if (string.IsNullOrEmpty(_registrationTicket)) { ShowTip(AppUILocalization.GetTextByCNKey("资料票据已失效,请从验证码步骤重新注册")); return; } int avatarId = avatarGroup != null ? avatarGroup.GetSelectedAvatarId() : RoleManager.PresetGridIndexToAvatarId(_selectedAvatarIndex); if (!RoleManager.IsAppUIPresetAvatarId(avatarId)) { ShowTip(AppUILocalization.GetTextByCNKey("请选择头像")); return; } StartCoroutine(LoginController.Instance.CompleteUserInfoByTicket( _registrationTicket, avatarId, nickname, defaultGender, defaultBirthday, "", "", "", res => { ShowTip(AppUILocalization.GetTextByCNKey(res.msg)); if (res.code != 0) return; StartCoroutine(AfterProfileSavedRoutine()); })); } IEnumerator AfterProfileSavedRoutine() { var loginOk = false; yield return LoginController.Instance.LoginNormal(_storedAccount, _storedPassword, res => { if (res.code != 0 || !(res.data is string loginToken)) return; var parts = loginToken.Split('&'); if (parts.Length > 2) CommonConfig.businessServerWsURL = parts[2]; PlayerPrefs.SetString(LoginMgr.LoginTokenKey, loginToken); loginOk = true; }); if (!loginOk) ShowTip(AppUILocalization.GetTextByKey("home_loginAuthFail")); var mgr = FindObjectOfType(); if (mgr != null) mgr.showLoginView(); else { // TODO(AppUI.RegisterView): LoginMgr 不在当前场景时,改为跳转到登录场景或打开 AppUI 登录页。 Debug.LogWarning("[AppUI.RegisterView] 未找到 LoginMgr,无法自动返回登录页。"); } } } }