手机号登录和邮箱登录
This commit is contained in:
parent
8f88302624
commit
747cc61393
432
app/auth.tsx
432
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<LoginType>('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<void>((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 = () => (
|
||||
<Block className="flex-row items-center gap-[8px]">
|
||||
<Block
|
||||
onClick={() => setAgreed(!agreed)}
|
||||
className={`size-[20px] items-center justify-center border-[2px] border-black ${agreed ? 'bg-black' : 'bg-white'}`}
|
||||
>
|
||||
{agreed && <Ionicons color="#FFE500" name="checkmark" size={14} />}
|
||||
</Block>
|
||||
<Block className="flex-row flex-wrap items-center">
|
||||
<Text className="font-700 text-[12px] text-black">已阅读并同意</Text>
|
||||
<Text
|
||||
className="font-700 text-[12px] text-black underline"
|
||||
onClick={() => openUrl('https://mixvideo.bowong.cc/terms', '服务条款')}
|
||||
>
|
||||
服务条款
|
||||
</Text>
|
||||
<Text className="font-700 text-[12px] text-black">和</Text>
|
||||
<Text
|
||||
className="font-700 text-[12px] text-black underline"
|
||||
onClick={() => openUrl('https://mixvideo.bowong.cc/privacy', '隐私协议')}
|
||||
>
|
||||
隐私协议
|
||||
</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
)
|
||||
|
||||
// 登录按钮组件
|
||||
const renderLoginButton = (buttonText: string = '登录') => (
|
||||
<Block
|
||||
className={`font-900 mt-[8px] flex-row items-center justify-center gap-[8px] border-[3px] border-black py-[14px] shadow-hard-black ${loading ? 'bg-gray-300' : 'bg-accent'}`}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
{loading ? (
|
||||
<Ionicons color="black" name="hourglass-outline" size={20} />
|
||||
) : (
|
||||
<Ionicons color="black" name="flash" size={20} />
|
||||
)}
|
||||
<Text className="font-900 text-[16px] text-black">{loading ? '处理中...' : buttonText}</Text>
|
||||
</Block>
|
||||
)
|
||||
|
||||
// 登录方式切换组件
|
||||
const renderLoginTypeSwitch = () => (
|
||||
<Block className="mt-[8px] flex-row items-center justify-center gap-[20px]">
|
||||
{(['phone', 'email'] as const).map((type) => {
|
||||
const isActive = type === loginType
|
||||
return (
|
||||
<Block
|
||||
key={type}
|
||||
style={{ transform: [{ skewX: '-6deg' }] }}
|
||||
onClick={() => setLoginType(type)}
|
||||
>
|
||||
<Ionicons
|
||||
color={isActive ? 'black' : '#9CA3AF'}
|
||||
name={type === 'phone' ? 'phone-portrait' : 'mail'}
|
||||
size={20}
|
||||
style={{ transform: [{ skewX: '6deg' }] }}
|
||||
/>
|
||||
</Block>
|
||||
)
|
||||
})}
|
||||
</Block>
|
||||
)
|
||||
|
||||
return (
|
||||
<Block className="relative flex-1 bg-black">
|
||||
<Block className="relative flex-1 bg-black h-[100dvh]">
|
||||
<BannerSection />
|
||||
<KeyboardAwareScrollView bottomOffset={100}>
|
||||
<Block className="flex-1 items-center justify-center px-[24px] py-[40px]">
|
||||
|
|
@ -178,100 +319,133 @@ export default function Auth() {
|
|||
>
|
||||
<Block style={{ transform: [{ skewX: '3deg' }] }}>
|
||||
<Block className="mt-[4px] gap-[16px]">
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] text-black">手机号</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons color="black" name="phone-portrait-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
keyboardType="phone-pad"
|
||||
placeholder="请输入手机号"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={phone}
|
||||
maxLength={11}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setPhone}
|
||||
/>
|
||||
<Block
|
||||
onClick={countdown > 0 ? undefined : handleSendCode}
|
||||
className={`border-2 border-black px-[6px] py-[4px] ${canSendCode && countdown === 0 ? 'bg-black' : 'bg-gray-200'}`}
|
||||
>
|
||||
<Text className={`text-[10px] font-[900] ${canSendCode && countdown === 0 ? 'text-accent' : 'text-gray-500'}`}>
|
||||
{countdown > 0 ? `${countdown}秒` : '获取验证码'}
|
||||
{loginType === 'phone' ? (
|
||||
<>
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] text-black">手机号</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons color="black" name="phone-portrait-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
keyboardType="phone-pad"
|
||||
placeholder="请输入手机号"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={phone}
|
||||
maxLength={11}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setPhone}
|
||||
/>
|
||||
<Block
|
||||
onClick={countdown > 0 ? undefined : handleSendCode}
|
||||
className={`border-2 border-black px-[6px] py-[4px] ${canSendCode && countdown === 0 ? 'bg-black' : 'bg-gray-200'}`}
|
||||
>
|
||||
<Text className={`text-[10px] font-[900] ${canSendCode && countdown === 0 ? 'text-accent' : 'text-gray-500'}`}>
|
||||
{countdown > 0 ? `${countdown}秒` : '获取验证码'}
|
||||
</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] text-black">验证码</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons color="black" name="key-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
keyboardType="number-pad"
|
||||
placeholder="请输入验证码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={code}
|
||||
maxLength={6}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setCode}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
{renderAgreementCheckbox()}
|
||||
|
||||
{renderLoginButton('登录/注册')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] text-black">账号</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons color="black" name="person-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
placeholder="用户名/邮箱"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={username}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setUsername}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] text-black">密码</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons color="black" name="lock-closed-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
secureTextEntry={!showPassword}
|
||||
placeholder="请输入密码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={password}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setPassword}
|
||||
/>
|
||||
<Block onClick={() => setShowPassword(!showPassword)} className="p-1">
|
||||
<Ionicons color="black" name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
<Block className="flex-row items-center justify-between">
|
||||
<Block className="flex-1">
|
||||
{renderAgreementCheckbox()}
|
||||
</Block>
|
||||
<Text className="font-700 text-[12px] text-gray-500" onClick={() => router.push('/forgotPassword')}>
|
||||
忘记密码
|
||||
</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
{renderLoginButton('登录')}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] text-black">验证码</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons color="black" name="key-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
keyboardType="number-pad"
|
||||
placeholder="请输入验证码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={code}
|
||||
maxLength={6}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setCode}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
<Block className="mt-[8px] flex-row items-center gap-[8px]">
|
||||
<Block
|
||||
onClick={() => setAgreed(!agreed)}
|
||||
className={`size-[20px] items-center justify-center border-[2px] border-black ${agreed ? 'bg-black' : 'bg-white'}`}
|
||||
>
|
||||
{agreed && <Ionicons color="#FFE500" name="checkmark" size={14} />}
|
||||
</Block>
|
||||
<Block className="flex-row flex-wrap items-center">
|
||||
<Text className="font-700 text-[12px] text-black">已阅读并同意</Text>
|
||||
<Text
|
||||
className="font-700 text-[12px] text-black underline"
|
||||
onClick={() => openUrl('https://mixvideo.bowong.cc/terms', '服务条款')}
|
||||
>
|
||||
服务条款
|
||||
</Text>
|
||||
<Text className="font-700 text-[12px] text-black">和</Text>
|
||||
<Text
|
||||
className="font-700 text-[12px] text-black underline"
|
||||
onClick={() => openUrl('https://mixvideo.bowong.cc/privacy', '隐私协议')}
|
||||
>
|
||||
隐私协议
|
||||
</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
<Block
|
||||
className={`font-900 mt-[8px] flex-row items-center justify-center gap-[8px] border-[3px] border-black py-[14px] shadow-hard-black ${loading ? 'bg-gray-300' : 'bg-accent'}`}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
{loading ? (
|
||||
<Ionicons color="black" name="hourglass-outline" size={20} />
|
||||
) : (
|
||||
<Ionicons color="black" name="flash" size={20} />
|
||||
)}
|
||||
<Text className="font-900 text-[16px] text-black">{loading ? '验证中...' : '登录/注册'}</Text>
|
||||
</Block>
|
||||
{renderLoginTypeSwitch()}
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ export const {
|
|||
signUp,
|
||||
signOut,
|
||||
useSession,
|
||||
getSession,
|
||||
$Infer,
|
||||
admin,
|
||||
forgetPassword,
|
||||
|
|
|
|||
Loading…
Reference in New Issue