phone-login.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { useState, useEffect } from 'react';
  2. import { loginSendCodeApi, getAccountListByCodeApi } from '../../api/login';
  3. import { SelectAccount, type Account } from './select-account';
  4. import { Agreement } from '../agreement';
  5. import { pmBridge } from '../../lib/PostMessageBridge';
  6. import { useAtomValue } from 'jotai';
  7. import { modalStackAtom } from '../../store';
  8. export interface phoneLoginProps {
  9. switchAccountLogin: () => void;
  10. switchRegister:()=> void;
  11. }
  12. export const PhoneLogin = ({ switchAccountLogin, switchRegister }: phoneLoginProps) => {
  13. const modalState = useAtomValue(modalStackAtom);
  14. const [mobile, setMobile] = useState('');
  15. const [code, setCode] = useState('');
  16. const [countdown, setCountdown] = useState(0);
  17. const [loading, setLoading] = useState(false);
  18. const [accountList, setAccountList] = useState<Account[]>([]);
  19. const [showSelectAccount, setShowSelectAccount] = useState(false);
  20. const [showAgreement, setShowAgreement] = useState<{ title: string; url: string } | null>(null);
  21. useEffect(() => {
  22. let timer: ReturnType<typeof setInterval>;
  23. if (countdown > 0) {
  24. timer = setInterval(() => {
  25. setCountdown((prev) => prev - 1);
  26. }, 1000);
  27. }
  28. return () => {
  29. if (timer) clearInterval(timer);
  30. };
  31. }, [countdown]);
  32. const handleGetCode = async () => {
  33. if (!mobile) {
  34. alert('请输入手机号');
  35. return;
  36. }
  37. if (!/^1[3-9]\d{9}$/.test(mobile)) {
  38. alert('请输入正确的手机号');
  39. return;
  40. }
  41. setLoading(true);
  42. try {
  43. await loginSendCodeApi({ mobile });
  44. setCountdown(60);
  45. } catch (error: any) {
  46. console.error('获取验证码失败:', error);
  47. alert(error.message || '获取验证码失败');
  48. } finally {
  49. setLoading(false);
  50. }
  51. };
  52. const handleLogin = async () => {
  53. if (!mobile) {
  54. alert('请输入手机号');
  55. return;
  56. }
  57. if (!code) {
  58. alert('请输入验证码');
  59. return;
  60. }
  61. setLoading(true);
  62. try {
  63. const res = await getAccountListByCodeApi({ mobile, code });
  64. if (res.length > 0) {
  65. setAccountList(res);
  66. setShowSelectAccount(true);
  67. } else {
  68. alert('登录成功')
  69. const requestId = modalState.login.requestId;
  70. pmBridge.sendToIframe("LOGIN_REQUEST", {success:true, data: res}, requestId);
  71. }
  72. } catch (error: any) {
  73. console.error('验证失败:', error);
  74. alert(error.message || '验证失败');
  75. } finally {
  76. setLoading(false);
  77. }
  78. };
  79. const handleSelectConfirm = (account: Account) => {
  80. console.log('选择账号确认:', account);
  81. setShowSelectAccount(false);
  82. };
  83. return (
  84. <>
  85. <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm">
  86. <div className="w-full max-w-[320px] mx-5 bg-[#f5f7f8] rounded-2xl shadow-2xl overflow-hidden flex flex-col scale-100">
  87. <div className="flex flex-col items-center pt-4 pb-6 px-6">
  88. <h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">游戏登录</h1>
  89. </div>
  90. {/* Tabs */}
  91. <div className="flex border-b border-slate-200 dark:border-slate-800 px-6 gap-8">
  92. <button className="flex flex-col items-center justify-center border-b-[3px] border-primary text-primary pb-3 pt-2">
  93. <p className="text-sm font-bold leading-normal tracking-wide">手机号登录</p>
  94. </button>
  95. <button onClick={switchAccountLogin} className="flex flex-col items-center justify-center border-b-[3px] border-transparent text-slate-500 dark:text-slate-400 pb-3 pt-2 hover:text-primary transition-colors">
  96. <p className="text-sm font-bold leading-normal tracking-wide">账号登录</p>
  97. </button>
  98. </div>
  99. {/* Form Content */}
  100. <div className="p-6 space-y-4">
  101. {/* Country Code & Phone Input */}
  102. <div className="flex gap-2">
  103. <div className="flex-1">
  104. <input
  105. className="p-2 form-input input-field w-full rounded-lg border-slate-200 bg-white text-slate-900 h-12 text-sm"
  106. placeholder="请输入手机号"
  107. type="tel"
  108. value={mobile}
  109. onChange={(e) => setMobile(e.target.value)}
  110. />
  111. </div>
  112. </div>
  113. {/* Verification Code */}
  114. <div className="flex gap-2">
  115. <div className="flex-1">
  116. <input
  117. className="p-2 form-input input-field w-full rounded-lg border-slate-200 bg-white text-slate-900 h-12 text-sm"
  118. placeholder="请输入验证码"
  119. type="text"
  120. value={code}
  121. onChange={(e) => setCode(e.target.value)}
  122. />
  123. </div>
  124. <button
  125. className={`px-4 h-12 text-primary font-medium text-sm rounded-lg whitespace-nowrap transition-colors border border-primary/20 ${
  126. countdown > 0 || loading ? 'bg-slate-100 text-slate-400 border-slate-200 cursor-not-allowed' : 'bg-primary/10 hover:bg-primary/20'
  127. }`}
  128. onClick={handleGetCode}
  129. disabled={countdown > 0 || loading}
  130. >
  131. {countdown > 0 ? `${countdown}s` : loading ? '发送中...' : '获取验证码'}
  132. </button>
  133. </div>
  134. {/* Action Button */}
  135. <button
  136. className={`w-full bg-primary hover:bg-primary/90 text-white font-bold h-12 rounded-lg shadow-lg shadow-primary/30 transition-all flex items-center justify-center gap-2 ${
  137. loading ? 'opacity-70 cursor-not-allowed' : ''
  138. }`}
  139. onClick={handleLogin}
  140. disabled={loading}
  141. >
  142. {loading ? '处理中...' : '进入游戏'}
  143. </button>
  144. {/* Secondary Links */}
  145. <div className="flex justify-between items-center px-1">
  146. <a className="text-xs text-slate-500 dark:text-slate-400 hover:text-primary transition-colors cursor-pointer" onClick={switchRegister}>快速注册</a>
  147. <a className="text-xs text-slate-500 dark:text-slate-400 hover:text-primary transition-colors" href="#">遇到问题?</a>
  148. </div>
  149. </div>
  150. {/* Footer Privacy */}
  151. <div className="px-6 pb-6 text-center">
  152. <p className="text-[10px] text-slate-400 leading-relaxed">
  153. 登录即代表您已同意 <button className="text-primary hover:underline" onClick={() => setShowAgreement({ title: '用户协议', url: '/static/user.html' })}>用户协议</button> 和 <button className="text-primary hover:underline" onClick={() => setShowAgreement({ title: '隐私政策', url: '/static/ys.html' })}>隐私政策</button>
  154. </p>
  155. </div>
  156. </div>
  157. </div>
  158. {showSelectAccount && (
  159. <SelectAccount
  160. mobile={mobile}
  161. code={code}
  162. accounts={accountList}
  163. onConfirm={handleSelectConfirm}
  164. onClose={() => setShowSelectAccount(false)}
  165. />
  166. )}
  167. {showAgreement && (
  168. <Agreement
  169. title={showAgreement.title}
  170. url={showAgreement.url}
  171. onBack={() => setShowAgreement(null)}
  172. />
  173. )}
  174. </>
  175. );
  176. };