RegisterView.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. using AppUI.Localization;
  2. using AppUI.Manager;
  3. using AppUI.Manager.Role;
  4. using AppUI.Util.Avatar;
  5. using JCUnityLib;
  6. using JCUnityLib.UI;
  7. using System.Collections;
  8. using System.Text.RegularExpressions;
  9. using TMPro;
  10. using UnityEngine;
  11. namespace AppUI.View
  12. {
  13. /// <summary>
  14. /// AppUI 注册向导:账号密码 → 短信/邮箱验证码并注册 → 昵称与预设头像。
  15. /// 全部输入使用 <see cref="TMP_InputField"/>;提示走 <see cref="ModuleViewMgr"/>,网络复用旧版 Controller。
  16. /// </summary>
  17. public class RegisterView : MonoBehaviour
  18. {
  19. enum RegisterPhase
  20. {
  21. AccountPassword,
  22. SmsAndRegister,
  23. Profile
  24. }
  25. RegisterPhase _phase = RegisterPhase.AccountPassword;
  26. [Header("阶段面板(可选,未绑定则仅逻辑推进)")]
  27. [SerializeField]
  28. GameObject panelAccountPassword;
  29. [SerializeField]
  30. GameObject panelSms;
  31. [SerializeField]
  32. GameObject panelProfile;
  33. [Header("阶段一:账号 / 密码(可 Inspector 绑定,或运行时按路径查找)")]
  34. [SerializeField]
  35. TMP_InputField accountInput;
  36. [SerializeField]
  37. TMP_InputField passwordInput;
  38. [Header("阶段二:短信验证码")]
  39. [SerializeField]
  40. TMP_InputField smsCodeInput;
  41. [SerializeField]
  42. TMP_Text sendSmsCountdownLabel;
  43. [Header("阶段三:昵称 / 预设头像")]
  44. [SerializeField]
  45. TMP_InputField nicknameInput;
  46. [SerializeField]
  47. UnityEngine.UI.Image avatarPreviewImage;
  48. [Tooltip("头像列表与 Sprite 数据源;Sprite 仅在此组件的数组中配置一份即可")]
  49. [SerializeField]
  50. AvatarGroup avatarGroup;
  51. [Header("拼图验证(与旧版 RegisterView 一致;未赋值则直接请求发送验证码)")]
  52. [SerializeField]
  53. GameObject prefabValidateJigsawView;
  54. [Header("行为")]
  55. [SerializeField]
  56. bool resetFlowOnEnable = true;
  57. // TODO(AppUI.RegisterView): 若注册前必须勾选用户协议,取消注释并在 OnClick_ConfirmRegister 开头调用。
  58. // [SerializeField] bool requireAgreementBeforeRegister = true;
  59. string _storedAccount = "";
  60. string _storedPassword = "";
  61. string _registrationTicket = "";
  62. int _selectedAvatarIndex;
  63. static long _lastSendSmsMs;
  64. Coroutine _sendCooldownRoutine;
  65. bool _sendCooldownActive;
  66. readonly Throttler _throttlerRegister = new Throttler(3000);
  67. readonly Throttler _throttlerProfileSave = new Throttler(2000);
  68. void OnEnable()
  69. {
  70. if (resetFlowOnEnable)
  71. ResetFlowToAccountPassword();
  72. }
  73. void Awake()
  74. {
  75. InitPasswordAlphanumericFilter();
  76. }
  77. void Start()
  78. {
  79. WireAvatarGroup();
  80. }
  81. void OnDestroy()
  82. {
  83. if (avatarGroup != null)
  84. avatarGroup.OnAvatarSelected -= OnAvatarGroupSelection;
  85. }
  86. /// <summary>
  87. /// 将 <see cref="AvatarGroup"/> 的写入目标设为 <see cref="avatarPreviewImage"/>,并订阅选中事件以同步 <see cref="_selectedAvatarIndex"/>。
  88. /// </summary>
  89. void WireAvatarGroup()
  90. {
  91. if (avatarGroup == null)
  92. return;
  93. if (avatarPreviewImage != null)
  94. avatarGroup.targetImage = avatarPreviewImage;
  95. avatarGroup.OnAvatarSelected -= OnAvatarGroupSelection;
  96. avatarGroup.OnAvatarSelected += OnAvatarGroupSelection;
  97. avatarGroup.SelectByIndex(0, notify: false);
  98. _selectedAvatarIndex = avatarGroup.SelectedIndex;
  99. }
  100. void OnAvatarGroupSelection(int index, Sprite _)
  101. {
  102. _selectedAvatarIndex = index;
  103. }
  104. void InitPasswordAlphanumericFilter()
  105. {
  106. if (passwordInput == null)
  107. return;
  108. passwordInput.onValueChanged.AddListener(text =>
  109. {
  110. var match = new Regex("[^A-Za-z0-9]").Match(text);
  111. if (match.Success)
  112. passwordInput.text = text.Replace(match.Value, "");
  113. });
  114. }
  115. /// <summary>外部(例如从登录页)切回本页时可调用,清空状态与 UI。</summary>
  116. public void ResetFlowToAccountPassword()
  117. {
  118. _phase = RegisterPhase.AccountPassword;
  119. _storedAccount = "";
  120. _storedPassword = "";
  121. _registrationTicket = "";
  122. _selectedAvatarIndex = 0;
  123. if (avatarGroup != null)
  124. avatarGroup.SelectByIndex(0, notify: false);
  125. if (smsCodeInput != null)
  126. smsCodeInput.text = "";
  127. if (nicknameInput != null)
  128. nicknameInput.text = "";
  129. ApplyPhasePanels();
  130. }
  131. void ApplyPhasePanels()
  132. {
  133. if (panelAccountPassword != null)
  134. panelAccountPassword.SetActive(_phase == RegisterPhase.AccountPassword);
  135. if (panelSms != null)
  136. panelSms.SetActive(_phase == RegisterPhase.SmsAndRegister);
  137. if (panelProfile != null)
  138. panelProfile.SetActive(_phase == RegisterPhase.Profile);
  139. }
  140. bool IsDomesticPhoneMode() => CommonConfig.AppArea == 0;
  141. void ShowTip(string message)
  142. {
  143. ModuleViewMgr.ins.Show(message);
  144. }
  145. /// <summary>阶段一「下一步」:校验账号密码后进入验证码阶段。</summary>
  146. public void OnClick_ContinueToSmsStep()
  147. {
  148. if (accountInput == null || passwordInput == null)
  149. {
  150. Debug.LogError("[AppUI.RegisterView] 未绑定账号或密码 TMP_InputField。");
  151. return;
  152. }
  153. string account = accountInput.text.Trim();
  154. if (account.Length == 0)
  155. {
  156. ShowTip(AppUILocalization.GetTextByCNKey("请输入账号"));
  157. return;
  158. }
  159. if (IsDomesticPhoneMode())
  160. {
  161. if (!ValidateHelper.IsMobilePhone(account))
  162. {
  163. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a1"));
  164. return;
  165. }
  166. }
  167. else
  168. {
  169. if (!ValidateHelper.IsEmail(account))
  170. {
  171. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a0"));
  172. return;
  173. }
  174. }
  175. string pwd = passwordInput.text.Trim();
  176. if (pwd.Length == 0)
  177. {
  178. ShowTip(AppUILocalization.GetTextByCNKey("请输入密码"));
  179. return;
  180. }
  181. if (pwd.Length < 6)
  182. {
  183. ShowTip(AppUILocalization.GetTextByCNKey("密码长度至少6位"));
  184. return;
  185. }
  186. _storedAccount = account;
  187. _storedPassword = pwd;
  188. _phase = RegisterPhase.SmsAndRegister;
  189. ApplyPhasePanels();
  190. //发送验证码
  191. OnClick_SendSmsCode();
  192. }
  193. /// <summary>发送短信或邮箱验证码(与旧版 <see cref="RegisterView.OnClick_SendCode"/> 对齐;限流 60s)。</summary>
  194. public void OnClick_SendSmsCode()
  195. {
  196. long gap = TimeUtils.GetTimestamp() - _lastSendSmsMs;
  197. const long maxMs = 60 * 1000;
  198. if (gap < maxMs)
  199. {
  200. long second = (maxMs - gap) / 1000;
  201. if (second <= 0)
  202. second = 1;
  203. ShowTip(string.Format(AppUILocalization.GetTextByKey("RelateValidateView-a2"), second));
  204. return;
  205. }
  206. if (_sendCooldownActive)
  207. {
  208. return;
  209. }
  210. if (IsDomesticPhoneMode())
  211. {
  212. if (!ValidateHelper.IsMobilePhone(_storedAccount))
  213. {
  214. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a1"));
  215. return;
  216. }
  217. }
  218. else
  219. {
  220. if (!ValidateHelper.IsEmail(_storedAccount))
  221. {
  222. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a0"));
  223. return;
  224. }
  225. }
  226. if (prefabValidateJigsawView != null)
  227. {
  228. var go = Instantiate(prefabValidateJigsawView);
  229. var jigsaw = go.GetComponent<ValidateJigsawView>();
  230. if (jigsaw != null)
  231. {
  232. jigsaw.onComplete = RequestSendValidateCode;
  233. jigsaw.SetTextLabel(AppUILocalization.GetTextByKey("ValidateJigsawView_label"));
  234. jigsaw.SetTextTip(AppUILocalization.GetTextByKey("ValidateJigsawView_tip"));
  235. jigsaw.SetTextOK(AppUILocalization.GetTextByKey("ValidateJigsawView_ok"));
  236. }
  237. else
  238. {
  239. Destroy(go);
  240. RequestSendValidateCode();
  241. }
  242. }
  243. else
  244. {
  245. RequestSendValidateCode();
  246. }
  247. }
  248. void RequestSendValidateCode()
  249. {
  250. _lastSendSmsMs = JCUnityLib.TimeUtils.GetTimestamp();
  251. if (_sendCooldownRoutine != null)
  252. StopCoroutine(_sendCooldownRoutine);
  253. _sendCooldownRoutine = StartCoroutine(SendSmsCooldownUiRoutine());
  254. if (IsDomesticPhoneMode())
  255. {
  256. StartCoroutine(PhoneValidateController.Instance.SendPhoneValidateCode(_storedAccount, res =>
  257. {
  258. if (res.code == 0)
  259. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b2"));
  260. else
  261. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b3"));
  262. }));
  263. }
  264. else
  265. {
  266. StartCoroutine(EmailValidateController.Instance.SendEmailValidateCode(_storedAccount, res =>
  267. {
  268. if (res.code == 0)
  269. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b0"));
  270. else
  271. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-b1"));
  272. }));
  273. }
  274. }
  275. IEnumerator SendSmsCooldownUiRoutine()
  276. {
  277. _sendCooldownActive = true;
  278. const float maxSec = 60f;
  279. float left = maxSec;
  280. while (left > 0f)
  281. {
  282. if (sendSmsCountdownLabel != null)
  283. sendSmsCountdownLabel.text = string.Format(AppUILocalization.GetTextByKey("appui-resend"),"("+ Mathf.CeilToInt(left) + " S)");
  284. left -= Time.deltaTime;
  285. yield return null;
  286. }
  287. _sendCooldownActive = false;
  288. if (sendSmsCountdownLabel != null)
  289. sendSmsCountdownLabel.text = sendSmsCountdownLabel.text = string.Format(AppUILocalization.GetTextByKey("appui-resend"), "");
  290. _sendCooldownRoutine = null;
  291. }
  292. /// <summary>提交验证码并调用注册接口;失败提示,成功进入资料步骤。</summary>
  293. public void OnClick_ConfirmRegister()
  294. {
  295. // TODO(AppUI.RegisterView): 需要协议时在此校验 AgreenmentOption.ins.IsAgreementChecked()。
  296. if (smsCodeInput == null)
  297. {
  298. Debug.LogError("[AppUI.RegisterView] 未绑定验证码 TMP_InputField。");
  299. return;
  300. }
  301. string codeText = smsCodeInput.text.Trim();
  302. if (codeText.Length != 6)
  303. {
  304. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a3"));
  305. return;
  306. }
  307. if (!int.TryParse(codeText, out int codeValue))
  308. {
  309. ShowTip(AppUILocalization.GetTextByKey("RelateValidateView-a3"));
  310. return;
  311. }
  312. if (!_throttlerRegister.CanPass())
  313. {
  314. ShowTip(AppUILocalization.GetTextByCNKey("操作过于频繁"));
  315. return;
  316. }
  317. string emailField = IsDomesticPhoneMode() ? "" : _storedAccount;
  318. string phoneField = IsDomesticPhoneMode() ? _storedAccount : "";
  319. StartCoroutine(LoginController.Instance.RegisterByCode(
  320. _storedAccount,
  321. _storedPassword,
  322. emailField,
  323. phoneField,
  324. codeValue,
  325. res =>
  326. {
  327. ShowTip(AppUILocalization.GetTextByCNKey(res.msg));
  328. if (res.code != 0)
  329. return;
  330. _registrationTicket = LoginController.TryGetRegistrationTicketFromData(res.data);
  331. if (string.IsNullOrEmpty(_registrationTicket))
  332. {
  333. ShowTip(AppUILocalization.GetTextByCNKey("注册成功但缺少资料票据,请重新登录后完善资料"));
  334. return;
  335. }
  336. _phase = RegisterPhase.Profile;
  337. if (nicknameInput != null)
  338. {
  339. nicknameInput.text = _storedAccount;
  340. }
  341. ApplyPhasePanels();
  342. TryRefreshAvatarPreview();
  343. var loginView = FindObjectOfType<LoginView>();
  344. loginView?.FillLoginInput(_storedAccount, _storedPassword);
  345. }));
  346. }
  347. /// <summary>预设头像列表项按钮可绑定:索引与 <see cref="AvatarGroup"/> 内 Sprite 数组一致。</summary>
  348. public void OnSelectPresetAvatar(int index)
  349. {
  350. if (avatarGroup == null)
  351. return;
  352. var max = Mathf.Max(0, avatarGroup.AvatarCount - 1);
  353. avatarGroup.SelectByIndex(Mathf.Clamp(index, 0, max), notify: true);
  354. }
  355. /// <summary>Inspector 无法直接传 int 参数时,可用多个按钮分别绑定下列方法。</summary>
  356. public void OnSelectPresetAvatar0() => OnSelectPresetAvatar(0);
  357. public void OnSelectPresetAvatar1() => OnSelectPresetAvatar(1);
  358. public void OnSelectPresetAvatar2() => OnSelectPresetAvatar(2);
  359. public void OnSelectPresetAvatar3() => OnSelectPresetAvatar(3);
  360. public void OnSelectPresetAvatar4() => OnSelectPresetAvatar(4);
  361. public void OnSelectPresetAvatar5() => OnSelectPresetAvatar(5);
  362. void TryRefreshAvatarPreview()
  363. {
  364. if (avatarPreviewImage == null || avatarGroup == null)
  365. return;
  366. var sp = avatarGroup.GetSelectedSprite();
  367. if (sp != null)
  368. avatarPreviewImage.sprite = sp;
  369. }
  370. /// <summary>返回上一阶段(供 UI 返回键绑定)。</summary>
  371. public void OnClick_Back()
  372. {
  373. switch (_phase)
  374. {
  375. case RegisterPhase.SmsAndRegister:
  376. _phase = RegisterPhase.AccountPassword;
  377. ApplyPhasePanels();
  378. break;
  379. case RegisterPhase.Profile:
  380. // TODO(AppUI.RegisterView): 注册已成功落库时是否允许退回验证码阶段需产品确认;当前仅回到短信页并保留已填昵称。
  381. _phase = RegisterPhase.SmsAndRegister;
  382. ApplyPhasePanels();
  383. break;
  384. default:
  385. // TODO(AppUI.RegisterView): 若需关闭注册页回到登录,在此调用 LoginMgr.showLoginView() 或关闭当前弹窗。
  386. break;
  387. }
  388. }
  389. /// <summary>
  390. /// TODO(AppUI.RegisterView): 接入 CommonConfig.banBindRelateAccount 为 true 时的 Register2(邮箱+手机双绑)分支时在此封装专用提交流程。
  391. /// </summary>
  392. public void OnClick_RegisterWithBoundContacts_Todo()
  393. {
  394. }
  395. /// <summary>
  396. /// TODO(AppUI.RegisterView): 从相册/拍照选取自定义头像后得到 Texture2D,再调用 LoginController.UploadAvatar。
  397. /// </summary>
  398. public void OnClick_PickCustomAvatarFromGallery_Todo()
  399. {
  400. }
  401. /// <summary>资料提交:完善用户信息;头像上传在登录拿 token 后尝试。</summary>
  402. public void OnClick_CompleteProfile()
  403. {
  404. if (nicknameInput == null)
  405. {
  406. Debug.LogError("[AppUI.RegisterView] 未绑定昵称 TMP_InputField。");
  407. return;
  408. }
  409. string nickname = nicknameInput.text.Trim();
  410. if (nickname.Length == 0)
  411. {
  412. ShowTip(AppUILocalization.GetTextByCNKey("请输入游戏昵称"));
  413. return;
  414. }
  415. if (!_throttlerProfileSave.CanPass())
  416. {
  417. ShowTip(AppUILocalization.GetTextByCNKey("操作过于频繁"));
  418. return;
  419. }
  420. const int defaultGender = 1;
  421. const string defaultBirthday = "2000-01-01";
  422. if (string.IsNullOrEmpty(_registrationTicket))
  423. {
  424. ShowTip(AppUILocalization.GetTextByCNKey("资料票据已失效,请从验证码步骤重新注册"));
  425. return;
  426. }
  427. int avatarId = avatarGroup != null
  428. ? avatarGroup.GetSelectedAvatarId()
  429. : RoleManager.PresetGridIndexToAvatarId(_selectedAvatarIndex);
  430. if (!RoleManager.IsAppUIPresetAvatarId(avatarId))
  431. {
  432. ShowTip(AppUILocalization.GetTextByCNKey("请选择头像"));
  433. return;
  434. }
  435. StartCoroutine(LoginController.Instance.CompleteUserInfoByTicket(
  436. _registrationTicket,
  437. avatarId,
  438. nickname,
  439. defaultGender,
  440. defaultBirthday,
  441. "",
  442. "",
  443. "",
  444. res =>
  445. {
  446. ShowTip(AppUILocalization.GetTextByCNKey(res.msg));
  447. if (res.code != 0)
  448. return;
  449. StartCoroutine(AfterProfileSavedRoutine());
  450. }));
  451. }
  452. IEnumerator AfterProfileSavedRoutine()
  453. {
  454. var loginOk = false;
  455. yield return LoginController.Instance.LoginNormal(_storedAccount, _storedPassword, res =>
  456. {
  457. if (res.code != 0 || !(res.data is string loginToken))
  458. return;
  459. var parts = loginToken.Split('&');
  460. if (parts.Length > 2)
  461. CommonConfig.businessServerWsURL = parts[2];
  462. PlayerPrefs.SetString(LoginMgr.LoginTokenKey, loginToken);
  463. loginOk = true;
  464. });
  465. if (!loginOk)
  466. ShowTip(AppUILocalization.GetTextByKey("home_loginAuthFail"));
  467. var mgr = FindObjectOfType<LoginMgr>();
  468. if (mgr != null)
  469. mgr.showLoginView();
  470. else
  471. {
  472. // TODO(AppUI.RegisterView): LoginMgr 不在当前场景时,改为跳转到登录场景或打开 AppUI 登录页。
  473. Debug.LogWarning("[AppUI.RegisterView] 未找到 LoginMgr,无法自动返回登录页。");
  474. }
  475. }
  476. }
  477. }