| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- 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, loginWithPhone, loginWithEmail, getQQAuthUrl } from '@/utils/api';
- import { px2vmin, px2vw, px2vh } from '@/utils/viewport';
- import { getOssUrl } from '@/utils/oss';
- 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 backButton = getOssUrl('backbutn.png');
- interface LocationState {
- loginType?: 'phone' | 'qq' | 'email';
- areaCode?: string;
- phoneNumber?: string;
- email?: string;
- }
- export const LoginPage = () => {
- 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, setSelectedLoginType] = useState<'phone' | 'qq' | 'email'>(
- state?.loginType || 'phone'
- );
- const [isChecking, setIsChecking] = useState(false);
- const areaCodeRef = useRef<HTMLInputElement>(null);
- const phoneRef = useRef<HTMLInputElement>(null);
- const emailRef = useRef<HTMLInputElement>(null);
- useEffect(() => {
- // 记录用户当前所在页面
- setStorage(STORAGE_KEYS.CURRENT_PAGE, 'login');
- const visited = getVisitedRoutes();
- const userId = getUserId();
- if (userId) {
- updateUserUIState(userId, {
- current_page: 'login',
- visited_routes: visited,
- }).catch(() => {
- // 同步失败不影响前端流程
- });
- }
- }, []);
- const handleBack = () => {
- navigate('/welcome');
- };
- // 处理 continue 按钮点击
- const handleContinue = async () => {
- // 手机号登录
- if (selectedLoginType === 'phone' && phoneNumber) {
- if (isChecking) return;
- setIsChecking(true);
- try {
- // 检查用户是否存在
- await loginWithPhone(phoneNumber);
- // 用户存在,跳转到验证码页面
- navigate('/login-verification-code', {
- state: {
- loginType: 'phone',
- areaCode,
- phoneNumber,
- email, // 保留邮箱数据,以便返回时恢复
- },
- });
- } catch (error) {
- // 用户不存在,提示并返回 welcome 页面
- const errorMessage = error instanceof Error ? error.message : '登录失败';
- if (errorMessage.includes('404') || errorMessage.includes('用户不存在')) {
- message.error('用户未注册,请先注册');
- navigate('/welcome');
- } else {
- message.error(errorMessage);
- }
- } finally {
- setIsChecking(false);
- }
- }
- // 邮箱登录
- else if (selectedLoginType === 'email' && email) {
- if (isChecking) return;
- setIsChecking(true);
- try {
- // 检查用户是否存在
- await loginWithEmail(email);
- // 用户存在,跳转到验证码页面
- navigate('/login-verification-code', {
- state: {
- loginType: 'email',
- areaCode, // 保留区号数据,以便返回时恢复
- phoneNumber, // 保留手机号数据,以便返回时恢复
- email,
- },
- });
- } catch (error) {
- // 用户不存在,提示并返回 welcome 页面
- const errorMessage = error instanceof Error ? error.message : '登录失败';
- if (errorMessage.includes('404') || errorMessage.includes('用户不存在')) {
- message.error('用户未注册,请先注册');
- navigate('/welcome');
- } else {
- message.error(errorMessage);
- }
- } finally {
- setIsChecking(false);
- }
- }
- // QQ登录
- else if (selectedLoginType === 'qq') {
- // 跳转到QQ授权页面
- const qqAuthUrl = getQQAuthUrl();
- window.location.href = qqAuthUrl;
- }
- };
- 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>
- {/* 欢迎文案 */}
- <div
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(111),
- width: px2vmin(270),
- transform: 'translateX(-50%)',
- fontSize: px2vmin(26),
- textAlign: 'center',
- lineHeight: 1.4,
- whiteSpace: 'normal',
- background: 'linear-gradient(97.3deg, rgba(245, 243, 240, 0.8) 2.21%, rgba(245, 243, 240, 0.3) 150.89%)',
- WebkitBackgroundClip: 'text',
- WebkitTextFillColor: 'transparent',
- backgroundClip: 'text',
- }}
- >
- Welcome back! It’s time to chat with{' '}
- <span
- style={{
- background: 'none',
- WebkitBackgroundClip: 'initial',
- WebkitTextFillColor: '#BF9DF8',
- color: '#BF9DF8',
- }}
- >
- Lumi
- </span>{' '}
- again
- </div>
- {/* 中部 GIF 动画图片 */}
- <img
- src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/01-Default.gif"
- alt="cheer animation"
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(182),
- height: px2vmin(184),
- transform: 'translateX(-50%)',
- }}
- />
- {/* 输入框 */}
- <div
- className="absolute"
- style={{
- top: px2vmin(376),
- left: '50%',
- transform: 'translateX(-50%)',
- width: px2vmin(312),
- display: 'flex',
- gap: 0,
- }}
- >
- {/* 手机登录:区号 + 手机号 */}
- {selectedLoginType === 'phone' && (
- <>
- {/* 区号输入框 */}
- <div style={{ width: px2vmin(80), position: 'relative' }}>
- <div
- className="relative overflow-visible"
- style={{
- height: px2vmin(56),
- background: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
- borderTopLeftRadius: '12PX',
- borderBottomLeftRadius: '2PX',
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
- 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)',
- }}
- >
- <input
- ref={areaCodeRef}
- 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)] text-center"
- style={{
- fontSize: px2vmin(16),
- paddingLeft: px2vw(8),
- paddingRight: px2vw(8),
- }}
- placeholder="+86"
- value={areaCode}
- onChange={(e) => {
- let value = e.target.value;
- // 确保始终以 + 开头
- if (value && !value.startsWith('+')) {
- value = '+' + value;
- }
- setAreaCode(value);
- }}
- onFocus={() => setIsAreaCodeFocused(true)}
- onBlur={() => setIsAreaCodeFocused(false)}
- />
- {/* 右侧白色竖线 */}
- <div
- style={{
- position: 'absolute',
- right: 0,
- top: px2vmin(12),
- bottom: px2vmin(12),
- width: '1PX',
- background: 'rgba(255, 255, 255, 1)',
- }}
- ></div>
- </div>
- <div
- className="w-full transition-opacity duration-300"
- style={{
- height: '2PX',
- background: isAreaCodeFocused || isPhoneFocused
- ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
- : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
- }}
- ></div>
- </div>
- {/* 手机号输入框 */}
- <div style={{ flex: 1, position: 'relative' }}>
- <div
- className="relative overflow-visible"
- style={{
- height: px2vmin(56),
- background: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
- borderTopRightRadius: '12PX',
- borderBottomRightRadius: '2PX',
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0,
- 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)',
- }}
- >
- <input
- ref={phoneRef}
- 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={phoneNumber}
- onChange={(e) => setPhoneNumber(e.target.value)}
- onFocus={() => setIsPhoneFocused(true)}
- onBlur={() => setIsPhoneFocused(false)}
- />
- {phoneNumber && (
- <button
- className="absolute top-1/2 -translate-y-1/2 flex items-center justify-center transition-opacity hover:opacity-70 touch-manipulation"
- style={{
- right: px2vw(12),
- width: px2vw(20),
- height: px2vh(20),
- }}
- 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
- ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
- : '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: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
- borderTopLeftRadius: '12PX',
- borderTopRightRadius: '12PX',
- borderBottomLeftRadius: '2PX',
- 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)',
- }}
- >
- <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)}
- />
- {email && (
- <button
- className="absolute top-1/2 -translate-y-1/2 flex items-center justify-center transition-opacity hover:opacity-70 touch-manipulation"
- style={{
- right: px2vw(12),
- width: px2vw(20),
- height: px2vh(20),
- }}
- 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
- ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
- : '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={handleContinue}
- disabled={isChecking}
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(486),
- 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>
- {/* 其他登录方式文字 */}
- <div
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(612),
- transform: 'translateX(-50%)',
- color: '#F5F3F0',
- fontSize: px2vmin(12),
- whiteSpace: 'nowrap',
- }}
- >
- —— 其他登陆方式 ——
- </div>
- {/* 按钮行:QQ / 手机 / 邮箱 */}
- <div
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(640),
- 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>
- );
- };
|