From 747cc613936aa0a66f42200231cc626c491b8318 Mon Sep 17 00:00:00 2001 From: gww Date: Tue, 27 Jan 2026 11:40:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=92=8C=E9=82=AE=E7=AE=B1=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth.tsx | 432 ++++++++++++++++++++++++++++++++++++--------------- lib/auth.ts | 1 + 2 files changed, 304 insertions(+), 129 deletions(-) diff --git a/app/auth.tsx b/app/auth.tsx index 8b3015d..719b61e 100644 --- a/app/auth.tsx +++ b/app/auth.tsx @@ -7,15 +7,26 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-controller' import { APP_VERSION } from '@/app.config' import BannerSection from '@/components/BannerSection' -import { phoneNumber } from '@/lib/auth' -import { isValidPhone } from '@/utils' +import { getSession, phoneNumber, setAuthToken, signIn } from '@/lib/auth' +import { isValidEmail, isValidPhone } from '@/utils' import { openUrl } from '@/utils/webview-helper' const APP_NAME = '多米' +type LoginType = 'phone' | 'email' + +// 判断是否为邮箱格式的辅助函数 +const isEmail = (input: string) => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) +} + export default function Auth() { + const [loginType, setLoginType] = useState('phone') const [phone, setPhone] = useState('') const [code, setCode] = useState('') + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) const [loading, setLoading] = useState(false) const [countdown, setCountdown] = useState(0) const [agreed, setAgreed] = useState(false) @@ -33,7 +44,7 @@ export default function Auth() { } } }, []) - + // 获取验证码 const handleSendCode = useCallback(async () => { if (!isValidPhone(phone)) { @@ -93,8 +104,8 @@ export default function Auth() { } }, [phone, countdown]) - // 验证码登录/注册 - const handleLogin = useCallback(async () => { + // 手机号验证码登录/注册(无需密码) + const handlePhoneLogin = useCallback(async () => { if (!phone || !code) { Toast.show({ title: '请填写手机号和验证码' }) return @@ -118,48 +129,178 @@ export default function Auth() { setLoading(true) Toast.showLoading({ title: '正在验证...' }) try { - const result = await phoneNumber.verify({ - phoneNumber: phone, - code, - updatePhoneNumber: true, // 如果已登录,更新手机号 + // 验证验证码,如果后端配置了 signUpOnVerification,新用户会自动注册并创建 session + // disableSession: false 表示验证成功后创建 session(自动登录) + await new Promise((resolve, reject) => { + phoneNumber.verify( + { phoneNumber: phone, code: code, disableSession: false }, + { + onSuccess: async (ctx: any) => { + const authToken = ctx.response.headers.get('set-auth-token') + if (authToken) { + setAuthToken(authToken) + } + await getSession() + Toast.show({ title: '登录成功!' }) + router.replace('/(tabs)') + }, + onError: (ctx: any) => { + reject(new Error(ctx.error.message)) + }, + }, + ) }) - - if (result.error) { - const errorStatus = result.error.status - const errorMessage = result.error.message || '验证失败' - - if (errorStatus === 403) { - Toast.show({ title: '验证码尝试次数过多,请重新获取验证码' }) - setCode('') // 清空验证码 - setCountdown(0) // 重置倒计时,允许重新发送 - if (countdownTimerRef.current) { - clearInterval(countdownTimerRef.current) - countdownTimerRef.current = null - } - } else if (errorStatus === 401) { - Toast.show({ title: '手机号未验证,请先验证手机号' }) - } else { - Toast.show({ title: errorMessage }) - } - return - } - Toast.show({ title: '登录成功!' }) - - // 延迟跳转,确保 toast 显示 - setTimeout(() => { - router.replace('/(tabs)') - }, 500) + } catch (error: any) { - console.error('登录失败:', error) - Toast.show({ title: error.message || '登录失败,请稍后重试' }) + console.error('登录/注册失败:', error) + Toast.show({ title: error.message || '操作失败,请稍后重试' }) } finally { Toast.hideLoading() setLoading(false) } }, [phone, code, agreed]) + // 邮箱登录 + const handleEmailLogin = useCallback(async () => { + if (!username || !password) { + Toast.show({ title: '请填写账号和密码' }) + return + } + + if (!agreed) { + Toast.show({ title: '请先阅读并同意服务条款和隐私协议' }) + return + } + + setLoading(true) + Toast.showLoading({ title: '正在登录...' }) + try { + let result + + if (isEmail(username)) { + // 如果用户名是邮箱格式,使用邮箱登录 + result = await signIn.email( + { + email: username, + password, + }, + { + onSuccess: async (ctx) => { + const authToken = ctx.response.headers.get('set-auth-token') + if (authToken) { + setAuthToken(authToken) + } + }, + onError: (ctx) => { + console.error(`[LOGIN] email login error`, ctx) + }, + }, + ) + } else { + // 如果用户名不是邮箱格式,使用用户名登录 + result = await signIn.username( + { + username, + password, + }, + { + onSuccess: async (ctx) => { + const authToken = ctx.response.headers.get('set-auth-token') + if (authToken) { + setAuthToken(authToken) + } + }, + onError: (ctx) => { + console.error(`[LOGIN] username login error`, ctx) + }, + }, + ) + } + + if (result.error) { + Toast.show({ title: '请输入正确的账号或密码' }) + } else { + Toast.show({ title: '登录成功!' }) + router.replace('/(tabs)') + } + } catch (error: any) { + Toast.show({ title: error.message || '登录失败' }) + } finally { + Toast.hideLoading() + setLoading(false) + } + }, [username, password, agreed]) + + const handleLogin = loginType === 'phone' ? handlePhoneLogin : handleEmailLogin + + // 服务条款同意组件 + const renderAgreementCheckbox = () => ( + + setAgreed(!agreed)} + className={`size-[20px] items-center justify-center border-[2px] border-black ${agreed ? 'bg-black' : 'bg-white'}`} + > + {agreed && } + + + 已阅读并同意 + openUrl('https://mixvideo.bowong.cc/terms', '服务条款')} + > + 服务条款 + + + openUrl('https://mixvideo.bowong.cc/privacy', '隐私协议')} + > + 隐私协议 + + + + ) + + // 登录按钮组件 + const renderLoginButton = (buttonText: string = '登录') => ( + + {loading ? ( + + ) : ( + + )} + {loading ? '处理中...' : buttonText} + + ) + + // 登录方式切换组件 + const renderLoginTypeSwitch = () => ( + + {(['phone', 'email'] as const).map((type) => { + const isActive = type === loginType + return ( + setLoginType(type)} + > + + + ) + })} + + ) + return ( - + @@ -178,100 +319,133 @@ export default function Auth() { > - - 手机号 - - - - 0 ? undefined : handleSendCode} - className={`border-2 border-black px-[6px] py-[4px] ${canSendCode && countdown === 0 ? 'bg-black' : 'bg-gray-200'}`} - > - - {countdown > 0 ? `${countdown}秒` : '获取验证码'} + {loginType === 'phone' ? ( + <> + + 手机号 + + + + 0 ? undefined : handleSendCode} + className={`border-2 border-black px-[6px] py-[4px] ${canSendCode && countdown === 0 ? 'bg-black' : 'bg-gray-200'}`} + > + + {countdown > 0 ? `${countdown}秒` : '获取验证码'} + + + + + + + 验证码 + + + + + + + {renderAgreementCheckbox()} + + {renderLoginButton('登录/注册')} + + ) : ( + <> + + 账号 + + + + + + + 密码 + + + + setShowPassword(!showPassword)} className="p-1"> + + + + + + + {renderAgreementCheckbox()} + + router.push('/forgotPassword')}> + 忘记密码 - - + + {renderLoginButton('登录')} + + )} - - 验证码 - - - - - - - - setAgreed(!agreed)} - className={`size-[20px] items-center justify-center border-[2px] border-black ${agreed ? 'bg-black' : 'bg-white'}`} - > - {agreed && } - - - 已阅读并同意 - openUrl('https://mixvideo.bowong.cc/terms', '服务条款')} - > - 服务条款 - - - openUrl('https://mixvideo.bowong.cc/privacy', '隐私协议')} - > - 隐私协议 - - - - - - {loading ? ( - - ) : ( - - )} - {loading ? '验证中...' : '登录/注册'} - + {renderLoginTypeSwitch()} diff --git a/lib/auth.ts b/lib/auth.ts index 8bcde29..fa0c76f 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -119,6 +119,7 @@ export const { signUp, signOut, useSession, + getSession, $Infer, admin, forgetPassword,