| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- import { useEffect, useState, useRef } from 'react';
- import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
- import { message } from 'antd';
- import { setStorage, STORAGE_KEYS, getUserId, getVisitedRoutes, setUserName } from '@/utils/storage';
- import { updateUserUIState, getQQAuthUrl, getUserInfo } from '@/utils/api';
- import { px2vmin } from '@/utils/viewport';
- import { getOssUrl } from '@/utils/oss';
- // const giftProgressBar = getOssUrl('gift-progress-bar.png'); // Unused
- // const wechatLogin = getOssUrl('wechat-login.png');
- const qqLogin = getOssUrl('qq-login.png');
- const phoneLogin = getOssUrl('phone-login.png');
- const emailLogin = getOssUrl('email-login.png');
- const continueBtn = getOssUrl('continue-btn.png');
- const continueBtnA = getOssUrl('continue-btn-a.png');
- const focusGradient =
- 'linear-gradient(90deg, #FFC900 11%, #13A6FF 32%, #806EFF 50%, #FF8EEC 66%, #FE4F51 89%)';
- interface LocationState {
- loginType?: 'phone' | 'qq' | 'email';
- areaCode?: string;
- phoneNumber?: string;
- email?: string;
- }
- export const FirstLoginPage = () => {
- const location = useLocation();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- 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, setSelectedLoginType] = 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(() => {
- 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);
- }, []);
- useEffect(() => {
- // 记录用户当前所在页面
- setStorage(STORAGE_KEYS.CURRENT_PAGE, 'first-login');
- const visited = getVisitedRoutes();
- const userId = getUserId();
- if (userId) {
- updateUserUIState(userId, {
- current_page: 'first-login',
- visited_routes: visited,
- }).catch(() => {
- // 同步失败不影响前端流程
- });
- }
- }, []);
- // 处理QQ OAuth回调
- useEffect(() => {
- const oauth = searchParams.get('oauth');
- const userId = searchParams.get('user_id');
- const usernameParam = searchParams.get('username');
- const error = searchParams.get('error');
- if (error === 'qq_auth_failed') {
- message.error('QQ登录失败,请重试');
- // 清除URL参数
- navigate('/first-login', { replace: true });
- return;
- }
- if (error === 'user_not_registered' && oauth === 'qq') {
- // 用户不存在,但这是从 first-login 来的,应该自动注册
- // 这个情况应该由后端处理,如果后端没有处理,这里可以提示用户
- message.error('QQ用户未注册,请使用其他方式登录');
- navigate('/first-login', { replace: true });
- return;
- }
- if (oauth === 'qq' && userId && usernameParam) {
- // QQ登录/注册成功,保存用户信息并完成登录流程
- const handleQQLoginSuccess = async () => {
- try {
- // 保存基本用户信息
- setStorage(STORAGE_KEYS.USER_ID, userId);
- setStorage(STORAGE_KEYS.USER_NAME, decodeURIComponent(usernameParam));
-
- // 设置登录状态
- setStorage(STORAGE_KEYS.IS_LOGGED_IN, 'true');
-
- // 获取完整用户信息并同步到 localStorage(刷新数据)
- try {
- const userInfo = await getUserInfo(userId);
- if (userInfo.success && userInfo.data) {
- // 同步用户名
- if (userInfo.data.username) {
- setUserName(userInfo.data.username);
- }
- // 同步机器人名字
- if (userInfo.data.bot_name) {
- setStorage(STORAGE_KEYS.LUMI_NAME, userInfo.data.bot_name);
- }
- // 同步用户等级
- if (typeof userInfo.data.user_level === 'number') {
- setStorage(STORAGE_KEYS.USER_LEVEL, String(userInfo.data.user_level));
- }
- // 背景图片会在 getUserInfo 中自动同步
- }
- } catch (error) {
- // 获取完整用户信息失败不影响登录流程,静默处理
- console.warn('获取完整用户信息失败:', error);
- }
-
- message.success('QQ登录成功!已获得帽子奖励');
-
- // 清除URL参数并跳转到奖励页面
- navigate('/first-login-reward', { replace: true });
- } catch (error) {
- console.error('处理QQ登录成功回调失败:', error);
- message.error('登录处理失败,请重试');
- navigate('/first-login', { replace: true });
- }
- };
- handleQQLoginSuccess();
- }
- }, [searchParams, navigate]);
- return (
- <div>
- {/* 顶部进度条图片 */}
- <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:
- '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)',
- }}
- >
- <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: px2vmin(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: px2vmin(12),
- width: px2vmin(20),
- height: px2vmin(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>
- {/* Continue 按钮 */}
- <button
- type="button"
- onClick={() => {
- // 手机号码或邮箱登录时,跳转到验证码页面
- if (selectedLoginType === 'phone' && phoneNumber) {
- navigate('/verification-code', {
- state: {
- loginType: 'phone',
- areaCode,
- phoneNumber,
- },
- });
- } else if (selectedLoginType === 'email' && email) {
- navigate('/verification-code', {
- state: {
- loginType: 'email',
- email,
- },
- });
- } else if (selectedLoginType === 'qq') {
- // QQ登录:跳转到QQ授权页面(传递来源信息)
- try {
- const qqAuthUrl = getQQAuthUrl('first-login');
- window.location.href = qqAuthUrl;
- } catch (error) {
- message.error('QQ登录失败,请重试');
- }
- }
- }}
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(436),
- width: px2vmin(279),
- transform: 'translateX(-50%)',
- border: 'none',
- padding: 0,
- background: 'none',
- }}
- >
- <img
- src={
- (selectedLoginType === 'phone' && phoneNumber) ||
- (selectedLoginType === 'email' && email) ||
- selectedLoginType === 'qq'
- ? continueBtnA
- : continueBtn
- }
- alt="continue"
- style={{
- width: '100%',
- display: 'block',
- }}
- />
- </button>
- {/* 微信登录按钮 */}
- {/* <button
- type="button"
- onClick={() => navigate('/first-login-reward')}
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(567),
- width: px2vmin(279),
- transform: 'translateX(-50%)',
- border: 'none',
- padding: 0,
- background: 'none',
- }}
- >
- <img
- src={wechatLogin}
- alt="wechat login"
- style={{
- width: '100%',
- display: 'block',
- }}
- />
- </button> */}
- {/* 其他登录方式文字 */}
- <div
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(562),
- transform: 'translateX(-50%)',
- color: '#F5F3F0',
- fontSize: px2vmin(12),
- whiteSpace: 'nowrap',
- }}
- >
- —— 其他登陆方式 ——
- </div>
- {/* 按钮行:QQ / 手机 / 邮箱 */}
- <div
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(590),
- transform: 'translateX(-50%)',
- display: 'flex',
- gap: px2vmin(32),
- alignItems: 'center',
- justifyContent: 'center',
- width: 'fit-content',
- }}
- >
- {selectedLoginType !== 'qq' && (
- <img
- src={qqLogin}
- alt="qq login"
- onClick={() => setSelectedLoginType('qq')}
- style={{
- width: px2vmin(40),
- height: px2vmin(40),
- cursor: 'pointer',
- }}
- />
- )}
- {selectedLoginType !== 'phone' && (
- <img
- src={phoneLogin}
- alt="phone login"
- onClick={() => setSelectedLoginType('phone')}
- style={{
- width: px2vmin(40),
- height: px2vmin(40),
- cursor: 'pointer',
- }}
- />
- )}
- {selectedLoginType !== 'email' && (
- <img
- src={emailLogin}
- alt="email login"
- onClick={() => setSelectedLoginType('email')}
- style={{
- width: px2vmin(40),
- height: px2vmin(40),
- cursor: 'pointer',
- }}
- />
- )}
- </div>
- </div>
- );
- };
|