213 lines
8.3 KiB
TypeScript
213 lines
8.3 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons'
|
||
import { Block, Text, Toast } from '@share/components'
|
||
import { router } from 'expo-router'
|
||
import React, { useCallback, useMemo, useState } from 'react'
|
||
import { TextInput } from 'react-native'
|
||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
||
|
||
import { APP_VERSION } from '@/app.config'
|
||
import BannerSection from '@/components/BannerSection'
|
||
import { phoneNumber, setAuthToken } from '@/lib/auth'
|
||
import { isValidPhone } from '@/utils'
|
||
|
||
const APP_NAME = '多米'
|
||
|
||
export default function Auth() {
|
||
const [phone, setPhone] = useState('')
|
||
const [code, setCode] = useState('')
|
||
const [loading, setLoading] = useState(false)
|
||
const [countdown, setCountdown] = useState(0)
|
||
|
||
const canSendCode = useMemo(() => {
|
||
return isValidPhone(phone)
|
||
}, [phone])
|
||
|
||
// TODO: 获取验证码接口还未实现,需要后端提供手机号验证码发送接口
|
||
const handleSendCode = useCallback(async () => {
|
||
if (!canSendCode) {
|
||
Toast.show({ title: '请输入有效的手机号' })
|
||
return
|
||
}
|
||
|
||
Toast.showLoading({ title: '正在发送验证码...' })
|
||
setLoading(true)
|
||
try {
|
||
// TODO: 调用获取验证码接口
|
||
// 当前使用 better-auth 的 phoneNumber.sendOtp,但后端可能还未实现
|
||
// 如果后端未实现,这里会报错,需要后端提供手机号验证码发送接口
|
||
const result = await phoneNumber.sendOtp({
|
||
phoneNumber: phone,
|
||
})
|
||
|
||
if (result.error) {
|
||
Toast.show({ title: result.error.message || '验证码发送失败,请检查后端接口是否已实现' })
|
||
return
|
||
}
|
||
|
||
Toast.show({ title: '验证码已发送,请查收短信' })
|
||
setCountdown(60) // 开始倒计时
|
||
|
||
// 倒计时逻辑
|
||
const timer = setInterval(() => {
|
||
setCountdown((prev) => {
|
||
if (prev <= 1) {
|
||
clearInterval(timer)
|
||
return 0
|
||
}
|
||
return prev - 1
|
||
})
|
||
}, 1000)
|
||
} catch (error: any) {
|
||
Toast.show({ title: error.message || '验证码发送失败' })
|
||
} finally {
|
||
Toast.hideLoading()
|
||
setLoading(false)
|
||
}
|
||
}, [phone, canSendCode])
|
||
|
||
const handleLogin = useCallback(async () => {
|
||
if (!phone || !code) {
|
||
Toast.show({ title: '请填写手机号和验证码' })
|
||
return
|
||
}
|
||
|
||
if (!isValidPhone(phone)) {
|
||
Toast.show({ title: '请输入有效的手机号' })
|
||
return
|
||
}
|
||
|
||
setLoading(true)
|
||
try {
|
||
// 使用 better-auth 的 phoneNumber.verify 进行验证和登录
|
||
// verify 方法会自动创建会话,如果用户不存在会自动注册
|
||
const result = await phoneNumber.verify({
|
||
phoneNumber: phone,
|
||
code,
|
||
updatePhoneNumber: true, // 如果已登录,更新手机号
|
||
})
|
||
|
||
if (result.error) {
|
||
Toast.show({ title: result.error.message || '验证码错误或已过期' })
|
||
return
|
||
}
|
||
|
||
// 获取认证 token(如果后端返回)
|
||
// better-auth 会自动处理 session,但如果有自定义 token,需要手动设置
|
||
if (result.data && 'token' in result.data && result.data.token) {
|
||
await setAuthToken(result.data.token)
|
||
}
|
||
|
||
Toast.show({ title: '登录成功!' })
|
||
router.replace('/(tabs)')
|
||
} catch (error: any) {
|
||
Toast.show({ title: error.message || '登录失败' })
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [phone, code])
|
||
|
||
return (
|
||
<Block className="relative flex-1 bg-black">
|
||
<BannerSection />
|
||
<KeyboardAwareScrollView bottomOffset={100}>
|
||
<Block className="flex-1 items-center justify-center px-[24px] py-[40px]">
|
||
<Block className="relative w-full max-w-screen-xs">
|
||
<Block className="relative mb-[32px] items-center">
|
||
<Block className="size-[80px] items-center justify-center rounded-full border-4 border-black bg-accent shadow-deep-black">
|
||
<Ionicons color="black" name="flash" size={40} />
|
||
</Block>
|
||
<Text className="font-900 mt-[16px] text-[32px] text-white">{APP_NAME}</Text>
|
||
<Block className="mt-[8px] h-[4px] w-[120px] bg-accent" />
|
||
</Block>
|
||
|
||
<Block
|
||
className="relative border-4 border-black bg-white p-[24px] shadow-deep-black"
|
||
style={{ transform: [{ skewX: '-3deg' }] }}
|
||
>
|
||
<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}秒` : '发送验证码'}
|
||
</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>
|
||
|
||
<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>
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
|
||
<Block className="mt-[24px] items-center">
|
||
<Text className="font-700 text-[12px] text-gray-400">© 2025 LOOMART. All rights reserved.</Text>
|
||
<Text className="font-700 text-[12px] text-gray-400">插件当前版本号: {APP_VERSION}</Text>
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
<Block className="h-[200px]" />
|
||
</KeyboardAwareScrollView>
|
||
</Block>
|
||
)
|
||
}
|