| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- import { useEffect, useState, useRef } from 'react';
- import { useLocation, useNavigate } from 'react-router-dom';
- import { message } from 'antd';
- import { setStorage, STORAGE_KEYS, getUserId, getVisitedRoutes } from '@/utils/storage';
- import { updateUserUIState, sendCode } from '@/utils/api';
- import { px2vmin, px2vw, px2vh } from '@/utils/viewport';
- import { getOssUrl } from '@/utils/oss';
- // const giftProgressBar = getOssUrl('gift-progress-bar.png'); // Unused
- const sendCodeNo = getOssUrl('send-code-no.png');
- const sendCodeImage = getOssUrl('send-code.png');
- const backButton = getOssUrl('backbutn.png');
- const focusGradient =
- 'linear-gradient(90deg, #FFC900 11%, #13A6FF 32%, #806EFF 50%, #FF8EEC 66%, #FE4F51 89%)';
- interface LocationState {
- loginType?: 'phone' | 'email';
- areaCode?: string;
- phoneNumber?: string;
- email?: string;
- }
- export const VerificationCodePage = () => {
- const location = useLocation();
- const navigate = useNavigate();
- const state = location.state as LocationState | null;
- // 根据传入的数据初始化状态
- const [areaCode, setAreaCode] = useState(state?.areaCode || '+86');
- const [phoneNumber, setPhoneNumber] = useState(state?.phoneNumber || '');
- const [email, setEmail] = useState(state?.email || '');
- const [isAreaCodeFocused, setIsAreaCodeFocused] = useState(false);
- const [isPhoneFocused, setIsPhoneFocused] = useState(false);
- const [isEmailFocused, setIsEmailFocused] = useState(false);
- const [selectedLoginType] = useState<'phone' | 'qq' | 'email'>(state?.loginType || 'phone');
- const [isAreaCodeOpen, setIsAreaCodeOpen] = useState(false);
- const areaCodeContainerRef = useRef<HTMLDivElement>(null);
- const phoneRef = useRef<HTMLInputElement>(null);
- const emailRef = useRef<HTMLInputElement>(null);
- const areaCodeOptions = [
- { code: '+86', label: '中国', flag: getOssUrl('china.png') },
- { code: '+44', label: '英国', flag: getOssUrl('england.png') },
- ];
- useEffect(() => {
- // 记录用户当前所在页面
- setStorage(STORAGE_KEYS.CURRENT_PAGE, 'verification-code');
- const visited = getVisitedRoutes();
- const userId = getUserId();
- if (userId) {
- updateUserUIState(userId, {
- current_page: 'verification-code',
- visited_routes: visited,
- }).catch(() => {
- // 同步失败不影响前端流程
- });
- }
- }, []);
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (areaCodeContainerRef.current && !areaCodeContainerRef.current.contains(event.target as Node)) {
- setIsAreaCodeOpen(false);
- setIsAreaCodeFocused(false);
- }
- };
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
- const handleBack = () => {
- navigate('/first-login', {
- state: {
- loginType: selectedLoginType,
- areaCode,
- phoneNumber,
- email,
- },
- });
- };
- // 处理发送验证码按钮点击
- const handleSendCode = async () => {
- // 验证输入
- if (selectedLoginType === 'phone' && !phoneNumber) {
- message.error('请输入手机号码');
- return;
- }
- if (selectedLoginType === 'email' && !email) {
- message.error('请输入邮箱地址');
- return;
- }
- try {
- // 确定发送类型
- const sendType = selectedLoginType === 'email' ? 'email' : 'domestic_sms';
-
- // 发送验证码
- const response = await sendCode({
- area_code: selectedLoginType === 'phone' ? areaCode : undefined,
- phone: selectedLoginType === 'phone' ? phoneNumber : undefined,
- email: selectedLoginType === 'email' ? email : undefined,
- send_type: sendType,
- });
- if (response.success) {
- message.success('验证码已发送');
- // 跳转到验证码输入页面
- navigate('/verification-input', {
- state: {
- loginType: selectedLoginType,
- areaCode,
- phoneNumber,
- email,
- },
- });
- } else {
- message.error('发送验证码失败,请重试');
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : '发送验证码失败';
- message.error(errorMessage);
- }
- };
- return (
- <div>
- {/* 顶部返回按钮 */}
- <button
- type="button"
- onClick={handleBack}
- style={{
- position: 'absolute',
- left: px2vmin(16),
- top: px2vmin(56),
- width: px2vmin(48),
- height: px2vmin(48),
- padding: 0,
- border: 'none',
- background: 'none',
- zIndex: 10,
- cursor: 'pointer',
- }}
- aria-label="Back"
- >
- <img
- src={backButton}
- alt="Back"
- style={{
- width: '100%',
- height: '100%',
- display: 'block',
- pointerEvents: 'none',
- }}
- />
- </button>
- {/* 顶部进度条图片 */}
- <img
- src={getOssUrl('01-no-gift.gif')}
- alt="gift progress bar"
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(64),
- width: px2vmin(195),
- transform: 'translateX(-50%)',
- }}
- />
- {/* 中部 GIF 动画图片 */}
- <img
- src={getOssUrl('03-Cheer.gif')}
- alt="cheer animation"
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(132),
- height: px2vmin(184),
- transform: 'translateX(-50%)',
- }}
- />
- {/* 输入框 */}
- <div
- className="absolute"
- style={{
- top: px2vmin(326),
- left: '50%',
- transform: 'translateX(-50%)',
- width: px2vmin(312),
- display: 'flex',
- gap: px2vmin(6),
- zIndex: 20,
- }}
- >
- {/* 手机登录:区号 + 手机号 */}
- {selectedLoginType === 'phone' && (
- <>
- {/* 区号下拉选择 */}
- <div style={{ width: px2vmin(95), position: 'relative' }} ref={areaCodeContainerRef}>
- <div
- className="relative overflow-visible"
- style={{
- height: px2vmin(56),
- background: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
- borderTopLeftRadius: '12PX',
- borderBottomLeftRadius: '2PX',
- borderTopRightRadius: '12PX',
- borderBottomRightRadius: '2PX',
- boxShadow:
- 'rgba(255, 255, 255, 0.18) 0px 1px 0px 0px, rgba(0, 0, 0, 0.65) 0px 0px 0px 1px inset, rgba(0, 0, 0, 0.25) 0px 4px 8px inset',
- }}
- >
- <button
- type="button"
- onClick={() => {
- setIsAreaCodeOpen((prev) => !prev);
- setIsAreaCodeFocused(true);
- }}
- className="w-full h-full flex items-center justify-between bg-transparent border-none outline-none px-0"
- style={{
- paddingLeft: px2vmin(12),
- paddingRight: px2vmin(8),
- fontSize: px2vmin(16),
- color: '#FFFFFF',
- gap: px2vmin(8),
- cursor: 'pointer',
- }}
- >
- <div className="flex items-center" style={{ gap: px2vmin(8) }}>
- <img
- src={
- areaCodeOptions.find((item) => item.code === areaCode)?.flag ||
- getOssUrl('china.png')
- }
- alt="area flag"
- style={{
- width: px2vmin(16),
- height: px2vmin(24),
- objectFit: 'cover',
- }}
- />
- <span style={{ color: '#FFFFFF' }}>{areaCode}</span>
- </div>
- <img
- src={getOssUrl('arrow-icon.png')}
- alt="arrow"
- style={{ width: px2vmin(7.5), height: px2vmin(3.75), flexShrink: 0 }}
- />
- </button>
- {isAreaCodeOpen && (
- <div
- style={{
- position: 'absolute',
- top: '100%',
- left: 0,
- marginTop: px2vmin(4),
- width: px2vmin(95),
- background: '#1E1A26',
- borderRadius: '12PX',
- boxShadow: '0 8PX 20PX rgba(0, 0, 0, 0.45)',
- zIndex: 10,
- overflow: 'hidden',
- }}
- >
- {areaCodeOptions
- .filter((item) => item.code !== areaCode)
- .map((item) => (
- <button
- key={item.code}
- type="button"
- onClick={() => {
- setAreaCode(item.code);
- setIsAreaCodeOpen(false);
- setIsAreaCodeFocused(false);
- }}
- className="flex items-center justify-start w-full border-none bg-transparent text-left hover:opacity-80 transition-opacity"
- style={{
- padding: `${px2vmin(10)} ${px2vmin(12)}`,
- gap: px2vmin(10),
- cursor: 'pointer',
- }}
- >
- <img
- src={item.flag}
- alt={`${item.label} flag`}
- style={{
- width: px2vmin(16),
- height: px2vmin(24),
- objectFit: 'cover',
- }}
- />
- <span style={{ color: '#FFFFFF', fontSize: px2vmin(16) }}>{item.code}</span>
- </button>
- ))}
- </div>
- )}
- </div>
- </div>
- {/* 手机号输入框 */}
- <div style={{ flex: 1, position: 'relative' }}>
- <div
- className="relative overflow-visible"
- style={{
- height: px2vmin(56),
- background: 'rgba(0,0,0,0.1)',
- borderTopRightRadius: '12PX',
- borderBottomRightRadius: '2PX',
- borderTopLeftRadius: '12PX',
- borderBottomLeftRadius: '2PX',
- boxShadow:
- 'rgba(255, 255, 255, 0.18) 0px 1px 0px 0px, rgba(0, 0, 0, 0.65) 0px 0px 0px 1px inset, rgba(0, 0, 0, 0.25) 0px 4px 8px inset',
- }}
- >
- <input
- ref={phoneRef}
- type="text"
- inputMode="numeric"
- 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)]"
- style={{
- fontSize: px2vmin(16),
- paddingLeft: px2vmin(24),
- paddingRight: px2vmin(24),
- }}
- placeholder="请输入手机号码"
- value={phoneNumber}
- onChange={(e) => setPhoneNumber(e.target.value)}
- onFocus={() => setIsPhoneFocused(true)}
- onBlur={() => setIsPhoneFocused(false)}
- />
- <button
- className="absolute top-1/2 -translate-y-1/2 flex items-center justify-center transition-opacity touch-manipulation"
- style={{
- right: px2vmin(12),
- width: px2vmin(20),
- height: px2vmin(20),
- opacity: isPhoneFocused && phoneNumber ? 1 : 0.6,
- }}
- onClick={() => setPhoneNumber('')}
- aria-label="清除"
- >
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
- <circle cx="12" cy="12" r="11" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" fill="none" />
- <path
- d="M8 8L16 16M16 8L8 16"
- stroke="rgba(191, 157, 248, 1)"
- strokeWidth="2.5"
- strokeLinecap="round"
- />
- </svg>
- </button>
- </div>
- <div
- className="w-full transition-opacity duration-300"
- style={{
- height: '2PX',
- background: isAreaCodeFocused || isPhoneFocused
- ? focusGradient
- : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
- }}
- ></div>
- </div>
- </>
- )}
- {/* 邮箱登录:邮箱输入框 */}
- {selectedLoginType === 'email' && (
- <div style={{ width: '100%', position: 'relative' }}>
- <div
- className="relative overflow-visible"
- style={{
- height: px2vmin(56),
- background: 'rgba(0,0,0,0.1)',
- borderTopLeftRadius: '12PX',
- borderTopRightRadius: '12PX',
- borderBottomLeftRadius: '2PX',
- borderBottomRightRadius: '2PX',
- boxShadow:
- 'rgba(255, 255, 255, 0.18) 0px 1px 0px 0px, rgba(0, 0, 0, 0.65) 0px 0px 0px 1px inset, rgba(0, 0, 0, 0.25) 0px 4px 8px inset',
- }}
- >
- <input
- ref={emailRef}
- type="text"
- 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)]"
- style={{
- fontSize: px2vmin(16),
- paddingLeft: px2vw(24),
- paddingRight: px2vmin(24),
- }}
- placeholder="请输入邮箱地址"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- onFocus={() => setIsEmailFocused(true)}
- onBlur={() => setIsEmailFocused(false)}
- />
- <button
- className="absolute top-1/2 -translate-y-1/2 flex items-center justify-center transition-opacity touch-manipulation"
- style={{
- right: px2vw(12),
- width: px2vw(20),
- height: px2vh(20),
- opacity: isEmailFocused && email ? 1 : 0.6,
- }}
- onClick={() => setEmail('')}
- aria-label="清除"
- >
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
- <circle cx="12" cy="12" r="11" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" fill="none" />
- <path
- d="M8 8L16 16M16 8L8 16"
- stroke="rgba(191, 157, 248, 1)"
- strokeWidth="2.5"
- strokeLinecap="round"
- />
- </svg>
- </button>
- </div>
- <div
- className="w-full transition-opacity duration-300"
- style={{
- height: '2PX',
- background: isEmailFocused ? focusGradient : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
- }}
- ></div>
- </div>
- )}
- {/* QQ登录:文字提示 */}
- {selectedLoginType === 'qq' && (
- <div
- style={{
- width: '100%',
- textAlign: 'center',
- color: '#FFFFFF',
- fontSize: px2vmin(24),
- fontWeight: 'bold',
- }}
- >
- QQ登录
- </div>
- )}
- </div>
- {/* 发送验证码按钮 */}
- <button
- type="button"
- onClick={handleSendCode}
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(436),
- width: px2vmin(279),
- transform: 'translateX(-50%)',
- border: 'none',
- padding: 0,
- background: 'none',
- cursor: (selectedLoginType === 'phone' && phoneNumber) || (selectedLoginType === 'email' && email) ? 'pointer' : 'not-allowed',
- }}
- >
- <img
- src={
- (selectedLoginType === 'phone' && phoneNumber) ||
- (selectedLoginType === 'email' && email)
- ? sendCodeImage
- : sendCodeNo
- }
- alt="send code"
- style={{
- width: '100%',
- display: 'block',
- }}
- />
- </button>
- </div>
- );
- };
|