LoginVerificationCodePage.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import { useEffect, useState, useRef } from 'react';
  2. import { useLocation, useNavigate } from 'react-router-dom';
  3. import { message } from 'antd';
  4. import { setStorage, STORAGE_KEYS, getUserId, getVisitedRoutes } from '@/utils/storage';
  5. import { updateUserUIState, sendCode } from '@/utils/api';
  6. import { px2vmin, px2vw, px2vh } from '@/utils/viewport';
  7. import { getOssUrl } from '@/utils/oss';
  8. const giftProgressBar = getOssUrl('gift-progress-bar.png');
  9. const sendCodeNo = getOssUrl('send-code-no.png');
  10. const sendCodeImage = getOssUrl('send-code.png');
  11. const backButton = getOssUrl('backbutn.png');
  12. interface LocationState {
  13. loginType?: 'phone' | 'email';
  14. areaCode?: string;
  15. phoneNumber?: string;
  16. email?: string;
  17. }
  18. export const LoginVerificationCodePage = () => {
  19. const location = useLocation();
  20. const navigate = useNavigate();
  21. const state = location.state as LocationState | null;
  22. // 根据传入的数据初始化状态
  23. const [areaCode, setAreaCode] = useState(state?.areaCode || '+86');
  24. const [phoneNumber, setPhoneNumber] = useState(state?.phoneNumber || '');
  25. const [email, setEmail] = useState(state?.email || '');
  26. const [isAreaCodeFocused, setIsAreaCodeFocused] = useState(false);
  27. const [isPhoneFocused, setIsPhoneFocused] = useState(false);
  28. const [isEmailFocused, setIsEmailFocused] = useState(false);
  29. const [selectedLoginType] = useState<'phone' | 'qq' | 'email'>(
  30. state?.loginType || 'phone'
  31. );
  32. const areaCodeRef = useRef<HTMLInputElement>(null);
  33. const phoneRef = useRef<HTMLInputElement>(null);
  34. const emailRef = useRef<HTMLInputElement>(null);
  35. useEffect(() => {
  36. // 记录用户当前所在页面
  37. setStorage(STORAGE_KEYS.CURRENT_PAGE, 'login-verification-code');
  38. const visited = getVisitedRoutes();
  39. const userId = getUserId();
  40. if (userId) {
  41. updateUserUIState(userId, {
  42. current_page: 'login-verification-code',
  43. visited_routes: visited,
  44. }).catch(() => {
  45. // 同步失败不影响前端流程
  46. });
  47. }
  48. }, []);
  49. const handleBack = () => {
  50. // 返回登录页面,并带上传入的所有数据
  51. navigate('/login', {
  52. state: {
  53. loginType: selectedLoginType,
  54. areaCode,
  55. phoneNumber,
  56. email,
  57. },
  58. });
  59. };
  60. // 处理发送验证码按钮点击
  61. const handleSendCode = async () => {
  62. // 验证输入
  63. if (selectedLoginType === 'phone' && !phoneNumber) {
  64. message.error('请输入手机号码');
  65. return;
  66. }
  67. if (selectedLoginType === 'email' && !email) {
  68. message.error('请输入邮箱地址');
  69. return;
  70. }
  71. try {
  72. // 确定发送类型
  73. const sendType = selectedLoginType === 'email' ? 'email' : 'domestic_sms';
  74. // 发送验证码
  75. const response = await sendCode({
  76. phone: selectedLoginType === 'phone' ? phoneNumber : undefined,
  77. email: selectedLoginType === 'email' ? email : undefined,
  78. send_type: sendType,
  79. });
  80. if (response.success) {
  81. message.success('验证码已发送');
  82. // 跳转到验证码输入页面
  83. navigate('/login-verification-input', {
  84. state: {
  85. loginType: selectedLoginType,
  86. areaCode,
  87. phoneNumber,
  88. email,
  89. },
  90. });
  91. } else {
  92. message.error('发送验证码失败,请重试');
  93. }
  94. } catch (error) {
  95. const errorMessage = error instanceof Error ? error.message : '发送验证码失败';
  96. message.error(errorMessage);
  97. }
  98. };
  99. return (
  100. <div>
  101. {/* 顶部返回按钮 */}
  102. <button
  103. type="button"
  104. onClick={handleBack}
  105. style={{
  106. position: 'absolute',
  107. left: px2vmin(16),
  108. top: px2vmin(56),
  109. width: px2vmin(48),
  110. height: px2vmin(48),
  111. padding: 0,
  112. border: 'none',
  113. background: 'none',
  114. zIndex: 10,
  115. cursor: 'pointer',
  116. }}
  117. aria-label="Back"
  118. >
  119. <img
  120. src={backButton}
  121. alt="Back"
  122. style={{
  123. width: '100%',
  124. height: '100%',
  125. display: 'block',
  126. pointerEvents: 'none',
  127. }}
  128. />
  129. </button>
  130. {/* 顶部进度条图片 */}
  131. {/* <img
  132. src={giftProgressBar}
  133. alt="gift progress bar"
  134. style={{
  135. position: 'absolute',
  136. left: '50%',
  137. top: px2vmin(61),
  138. width: px2vmin(195),
  139. transform: 'translateX(-50%)',
  140. }}
  141. /> */}
  142. {/* 中部 GIF 动画图片 */}
  143. <img
  144. src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/01-Default.gif"
  145. alt="cheer animation"
  146. style={{
  147. position: 'absolute',
  148. left: '50%',
  149. top: px2vmin(132),
  150. height: px2vmin(184),
  151. transform: 'translateX(-50%)',
  152. }}
  153. />
  154. {/* 输入框 */}
  155. <div
  156. className="absolute"
  157. style={{
  158. top: px2vmin(326),
  159. left: '50%',
  160. transform: 'translateX(-50%)',
  161. width: px2vmin(312),
  162. display: 'flex',
  163. gap: 0,
  164. }}
  165. >
  166. {/* 手机登录:区号 + 手机号 */}
  167. {selectedLoginType === 'phone' && (
  168. <>
  169. {/* 区号输入框 */}
  170. <div style={{ width: px2vmin(80), position: 'relative' }}>
  171. <div
  172. className="relative overflow-visible"
  173. style={{
  174. height: px2vmin(56),
  175. background: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
  176. borderTopLeftRadius: '12PX',
  177. borderBottomLeftRadius: '2PX',
  178. borderTopRightRadius: 0,
  179. borderBottomRightRadius: 0,
  180. boxShadow:
  181. '0PX 1PX 0PX 0PX rgba(255, 255, 255, 0.18), inset 0PX 0PX 0PX 1PX rgba(0, 0, 0, 0.65), inset 0PX 4PX 8PX rgba(0, 0, 0, 0.55)',
  182. }}
  183. >
  184. <input
  185. ref={areaCodeRef}
  186. type="text"
  187. className="w-full h-full font-normal leading-normal tracking-[0.5px] text-[rgba(245,243,240,0.7)] bg-transparent border-none outline-none placeholder:text-[rgba(245,243,240,0.45)] text-center"
  188. style={{
  189. fontSize: px2vmin(16),
  190. paddingLeft: px2vw(8),
  191. paddingRight: px2vw(8),
  192. }}
  193. placeholder="+86"
  194. value={areaCode}
  195. onChange={(e) => {
  196. let value = e.target.value;
  197. // 确保始终以 + 开头
  198. if (value && !value.startsWith('+')) {
  199. value = '+' + value;
  200. }
  201. setAreaCode(value);
  202. }}
  203. onFocus={() => setIsAreaCodeFocused(true)}
  204. onBlur={() => setIsAreaCodeFocused(false)}
  205. />
  206. {/* 右侧白色竖线 */}
  207. <div
  208. style={{
  209. position: 'absolute',
  210. right: 0,
  211. top: px2vmin(12),
  212. bottom: px2vmin(12),
  213. width: '1PX',
  214. background: 'rgba(255, 255, 255, 1)',
  215. }}
  216. ></div>
  217. </div>
  218. <div
  219. className="w-full transition-opacity duration-300"
  220. style={{
  221. height: '2PX',
  222. background: isAreaCodeFocused || isPhoneFocused
  223. ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
  224. : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
  225. }}
  226. ></div>
  227. </div>
  228. {/* 手机号输入框 */}
  229. <div style={{ flex: 1, position: 'relative' }}>
  230. <div
  231. className="relative overflow-visible"
  232. style={{
  233. height: px2vmin(56),
  234. background: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
  235. borderTopRightRadius: '12PX',
  236. borderBottomRightRadius: '2PX',
  237. borderTopLeftRadius: 0,
  238. borderBottomLeftRadius: 0,
  239. boxShadow:
  240. '0PX 1PX 0PX 0PX rgba(255, 255, 255, 0.18), inset 0PX 0PX 0PX 1PX rgba(0, 0, 0, 0.65), inset 0PX 4PX 8PX rgba(0, 0, 0, 0.55)',
  241. }}
  242. >
  243. <input
  244. ref={phoneRef}
  245. type="text"
  246. className="w-full h-full font-normal leading-normal tracking-[0.5px] text-[rgba(245,243,240,0.7)] bg-transparent border-none outline-none placeholder:text-[rgba(245,243,240,0.45)]"
  247. style={{
  248. fontSize: px2vmin(16),
  249. paddingLeft: px2vw(24),
  250. paddingRight: px2vmin(24),
  251. }}
  252. placeholder="请输入手机号码"
  253. value={phoneNumber}
  254. onChange={(e) => setPhoneNumber(e.target.value)}
  255. onFocus={() => setIsPhoneFocused(true)}
  256. onBlur={() => setIsPhoneFocused(false)}
  257. />
  258. {phoneNumber && (
  259. <button
  260. className="absolute top-1/2 -translate-y-1/2 flex items-center justify-center transition-opacity hover:opacity-70 touch-manipulation"
  261. style={{
  262. right: px2vw(12),
  263. width: px2vw(20),
  264. height: px2vh(20),
  265. }}
  266. onClick={() => setPhoneNumber('')}
  267. aria-label="清除"
  268. >
  269. <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
  270. <circle cx="12" cy="12" r="11" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" fill="none" />
  271. <path
  272. d="M8 8L16 16M16 8L8 16"
  273. stroke="rgba(191, 157, 248, 1)"
  274. strokeWidth="2.5"
  275. strokeLinecap="round"
  276. />
  277. </svg>
  278. </button>
  279. )}
  280. </div>
  281. <div
  282. className="w-full transition-opacity duration-300"
  283. style={{
  284. height: '2PX',
  285. background: isAreaCodeFocused || isPhoneFocused
  286. ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
  287. : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
  288. }}
  289. ></div>
  290. </div>
  291. </>
  292. )}
  293. {/* 邮箱登录:邮箱输入框 */}
  294. {selectedLoginType === 'email' && (
  295. <div style={{ width: '100%', position: 'relative' }}>
  296. <div
  297. className="relative overflow-visible"
  298. style={{
  299. height: px2vmin(56),
  300. background: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
  301. borderTopLeftRadius: '12PX',
  302. borderTopRightRadius: '12PX',
  303. borderBottomLeftRadius: '2PX',
  304. borderBottomRightRadius: '2PX',
  305. boxShadow:
  306. '0PX 1PX 0PX 0PX rgba(255, 255, 255, 0.18), inset 0PX 0PX 0PX 1PX rgba(0, 0, 0, 0.65), inset 0PX 4PX 8PX rgba(0, 0, 0, 0.55)',
  307. }}
  308. >
  309. <input
  310. ref={emailRef}
  311. type="text"
  312. className="w-full h-full font-normal leading-normal tracking-[0.5px] text-[rgba(245,243,240,0.7)] bg-transparent border-none outline-none placeholder:text-[rgba(245,243,240,0.45)]"
  313. style={{
  314. fontSize: px2vmin(16),
  315. paddingLeft: px2vw(24),
  316. paddingRight: px2vmin(24),
  317. }}
  318. placeholder="请输入邮箱地址"
  319. value={email}
  320. onChange={(e) => setEmail(e.target.value)}
  321. onFocus={() => setIsEmailFocused(true)}
  322. onBlur={() => setIsEmailFocused(false)}
  323. />
  324. {email && (
  325. <button
  326. className="absolute top-1/2 -translate-y-1/2 flex items-center justify-center transition-opacity hover:opacity-70 touch-manipulation"
  327. style={{
  328. right: px2vw(12),
  329. width: px2vw(20),
  330. height: px2vh(20),
  331. }}
  332. onClick={() => setEmail('')}
  333. aria-label="清除"
  334. >
  335. <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
  336. <circle cx="12" cy="12" r="11" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" fill="none" />
  337. <path
  338. d="M8 8L16 16M16 8L8 16"
  339. stroke="rgba(191, 157, 248, 1)"
  340. strokeWidth="2.5"
  341. strokeLinecap="round"
  342. />
  343. </svg>
  344. </button>
  345. )}
  346. </div>
  347. <div
  348. className="w-full transition-opacity duration-300"
  349. style={{
  350. height: '2PX',
  351. background: isEmailFocused
  352. ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
  353. : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
  354. }}
  355. ></div>
  356. </div>
  357. )}
  358. {/* QQ登录:文字提示 */}
  359. {selectedLoginType === 'qq' && (
  360. <div
  361. style={{
  362. width: '100%',
  363. textAlign: 'center',
  364. color: '#FFFFFF',
  365. fontSize: px2vmin(24),
  366. fontWeight: 'bold',
  367. }}
  368. >
  369. QQ登录
  370. </div>
  371. )}
  372. </div>
  373. {/* 发送验证码按钮 */}
  374. <button
  375. type="button"
  376. onClick={handleSendCode}
  377. disabled={
  378. !((selectedLoginType === 'phone' && phoneNumber) || (selectedLoginType === 'email' && email))
  379. }
  380. style={{
  381. position: 'absolute',
  382. left: '50%',
  383. top: px2vmin(436),
  384. width: px2vmin(279),
  385. transform: 'translateX(-50%)',
  386. border: 'none',
  387. padding: 0,
  388. background: 'none',
  389. cursor:
  390. (selectedLoginType === 'phone' && phoneNumber) ||
  391. (selectedLoginType === 'email' && email)
  392. ? 'pointer'
  393. : 'not-allowed',
  394. }}
  395. >
  396. <img
  397. src={
  398. (selectedLoginType === 'phone' && phoneNumber) ||
  399. (selectedLoginType === 'email' && email)
  400. ? sendCodeImage
  401. : sendCodeNo
  402. }
  403. alt="send code"
  404. style={{
  405. width: '100%',
  406. display: 'block',
  407. }}
  408. />
  409. </button>
  410. </div>
  411. );
  412. };