VerificationCodePage.tsx 14 KB

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