fix: 手机号验证码发送接口还未实现
This commit is contained in:
parent
e89b3c0bd8
commit
18dc038c52
556
app/auth.tsx
556
app/auth.tsx
|
|
@ -7,425 +7,104 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
|||
|
||||
import { APP_VERSION } from '@/app.config'
|
||||
import BannerSection from '@/components/BannerSection'
|
||||
import { emailOtp as emailOtpService, setAuthToken, signIn, signUp } from '@/lib/auth'
|
||||
import { isValidEmail } from '@/utils'
|
||||
type AuthMode = 'login' | 'register' | 'registerEmail'
|
||||
import { phoneNumber, setAuthToken } from '@/lib/auth'
|
||||
import { isValidPhone } from '@/utils'
|
||||
|
||||
const APP_NAME = '多米'
|
||||
|
||||
// 判断是否为邮箱格式的辅助函数
|
||||
const isEmail = (input: string) => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
|
||||
}
|
||||
|
||||
export default function Auth() {
|
||||
const [mode, setMode] = useState<AuthMode>('login')
|
||||
|
||||
const [username, setUsername] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [emailOtp, setEmailOtp] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [phone, setPhone] = useState('')
|
||||
const [code, setCode] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [countdown, setCountdown] = useState(0)
|
||||
|
||||
const canSend = useMemo(() => {
|
||||
return isValidEmail(email)
|
||||
}, [email])
|
||||
const canSendCode = useMemo(() => {
|
||||
return isValidPhone(phone)
|
||||
}, [phone])
|
||||
|
||||
const handleEmiallOtp = () => {
|
||||
if (!canSend) {
|
||||
Toast.show({ title: '请输入有效的邮箱地址' })
|
||||
// TODO: 获取验证码接口还未实现,需要后端提供手机号验证码发送接口
|
||||
const handleSendCode = useCallback(async () => {
|
||||
if (!canSendCode) {
|
||||
Toast.show({ title: '请输入有效的手机号' })
|
||||
return
|
||||
}
|
||||
|
||||
Toast.showLoading({ title: '正在发送验证码...' })
|
||||
setLoading(true)
|
||||
emailOtpService
|
||||
.sendVerificationOtp({ email, type: 'email-verification' })
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
Toast.show({ title: res.error.message || '验证码发送失败' })
|
||||
} else {
|
||||
Toast.show({ title: '验证码已发送,请查收邮箱' })
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
Toast.hideLoading()
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleEmailVerify = async () => {
|
||||
if (!canSend) {
|
||||
Toast.show({ title: '请输入有效的邮箱地址' })
|
||||
return
|
||||
}
|
||||
if (!emailOtp) {
|
||||
Toast.show({ title: '请输入邮箱验证码' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Toast.showLoading({ title: '正在验证' })
|
||||
const emailVerifyRes = await emailOtpService.verifyEmail({ email, otp: emailOtp })
|
||||
Toast.hideLoading()
|
||||
console.log('emailVerifyRes------------', emailVerifyRes)
|
||||
if (emailVerifyRes?.error) {
|
||||
Toast.show({ title: '邮箱验证码验证失败' })
|
||||
// TODO: 调用获取验证码接口
|
||||
// 当前使用 better-auth 的 phoneNumber.sendOtp,但后端可能还未实现
|
||||
// 如果后端未实现,这里会报错,需要后端提供手机号验证码发送接口
|
||||
const result = await phoneNumber.sendOtp({
|
||||
phoneNumber: phone,
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
Toast.show({ title: result.error.message || '验证码发送失败,请检查后端接口是否已实现' })
|
||||
return
|
||||
}
|
||||
setMode('register')
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
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 (!username || !password) {
|
||||
Toast.show({ title: '请填写账号和密码' })
|
||||
if (!phone || !code) {
|
||||
Toast.show({ title: '请填写手机号和验证码' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValidPhone(phone)) {
|
||||
Toast.show({ title: '请输入有效的手机号' })
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
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)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
// 使用 better-auth 的 phoneNumber.verify 进行验证和登录
|
||||
// verify 方法会自动创建会话,如果用户不存在会自动注册
|
||||
const result = await phoneNumber.verify({
|
||||
phoneNumber: phone,
|
||||
code,
|
||||
updatePhoneNumber: true, // 如果已登录,更新手机号
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
Toast.show({ title: '请输入正确的账号或密码' })
|
||||
} else {
|
||||
Toast.show({ title: '登录成功!' })
|
||||
router.replace('/(tabs)')
|
||||
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)
|
||||
}
|
||||
}, [username, password, signIn])
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!email || !username || !password || !emailOtp) {
|
||||
Toast.show({ title: '请填写所有必填项' })
|
||||
return
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
Toast.show({ title: '两次密码输入不一致' })
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
Toast.show({ title: '密码长度至少6位' })
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await signUp.email({
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
name: username,
|
||||
})
|
||||
|
||||
// console.log('result------------', result)
|
||||
|
||||
const messageMap = {
|
||||
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: '该邮箱已被注册,请使用其他邮箱',
|
||||
USERNAME_ALREADY_EXISTS_USE_ANOTHER_USERNAME: '该用户名已被注册,请使用其他用户名',
|
||||
}
|
||||
const friendlyMessage = messageMap[result?.error?.code]
|
||||
|
||||
if (result.error) {
|
||||
Toast.show({ title: friendlyMessage || result?.error?.message || '注册失败' })
|
||||
} else {
|
||||
Toast.show({ title: '注册成功!' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
Toast.show({ title: error.message || '注册失败' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const renderLogin = () => {
|
||||
if (mode !== 'login') {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Block className="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="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="items-end">
|
||||
<Text className="font-700 text-[12px] text-gray-500" onClick={() => router.push('/forgotPassword')}>
|
||||
忘记密码
|
||||
</Text>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
const renderEmailSend = () => {
|
||||
if (mode !== 'registerEmail') {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Block className="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="mail-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
placeholder="请输入邮箱"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={email}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
<Block
|
||||
onClick={handleEmiallOtp}
|
||||
className={`border-2 border-black px-[6px] py-[4px] ${canSend ? 'bg-black' : 'bg-gray-200'}`}
|
||||
>
|
||||
<Text className={`text-[10px] font-[900] ${canSend ? 'text-accent' : 'text-gray-500'}`}>发送验证码</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="mail-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
placeholder="输入邮件中的验证码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={emailOtp}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setEmailOtp}
|
||||
/>
|
||||
</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={handleEmailVerify}
|
||||
>
|
||||
{loading ? (
|
||||
<Ionicons color="black" name="hourglass-outline" size={20} />
|
||||
) : (
|
||||
<Ionicons color="black" name="flash" size={20} />
|
||||
)}
|
||||
<Text className="font-900 text-[16px] text-black">下一步</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
const renderRegister = () => {
|
||||
if (mode !== 'register') {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Block className="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="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>
|
||||
<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={!showConfirmPassword}
|
||||
placeholder="请再次输入密码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={confirmPassword}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
onChangeText={setConfirmPassword}
|
||||
/>
|
||||
<Block onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="p-1">
|
||||
<Ionicons color="black" name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
|
||||
</Block>
|
||||
</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={handleRegister}
|
||||
>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
}, [phone, code])
|
||||
|
||||
return (
|
||||
<Block className="relative flex-1 bg-black">
|
||||
|
|
@ -441,42 +120,81 @@ export default function Auth() {
|
|||
<Block className="mt-[8px] h-[4px] w-[120px] bg-accent" />
|
||||
</Block>
|
||||
|
||||
<Block className="mb-[24px] flex-row gap-[12px]">
|
||||
{(['login', 'register'] as const).map((tabMode) => {
|
||||
const isActive = tabMode === mode || (mode === 'registerEmail' && tabMode === 'register')
|
||||
return (
|
||||
<Block
|
||||
key={tabMode}
|
||||
className={`flex-1 items-center justify-center border-[3px] border-black py-[12px] shadow-hard-black ${isActive ? 'bg-accent' : 'bg-white'}`}
|
||||
style={{ transform: [{ skewX: '-6deg' }] }}
|
||||
onClick={() => {
|
||||
if (tabMode === 'register') {
|
||||
setMode('registerEmail')
|
||||
} else {
|
||||
setMode('login')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`font-900 text-[14px] ${isActive ? 'text-black' : 'text-gray-500'}`}
|
||||
style={{ transform: [{ skewX: '6deg' }] }}
|
||||
>
|
||||
{tabMode === 'login' ? '登录' : '注册'}
|
||||
</Text>
|
||||
</Block>
|
||||
)
|
||||
})}
|
||||
</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]">
|
||||
{renderLogin()}
|
||||
{renderRegister()}
|
||||
{renderEmailSend()}
|
||||
<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>
|
||||
|
|
|
|||
19
lib/auth.ts
19
lib/auth.ts
|
|
@ -114,7 +114,22 @@ export const authClient = createAuthClient({
|
|||
],
|
||||
})
|
||||
|
||||
export const { signIn, signUp, signOut, useSession, $Infer, admin, forgetPassword, resetPassword, emailOtp } =
|
||||
authClient
|
||||
export const {
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
useSession,
|
||||
$Infer,
|
||||
admin,
|
||||
forgetPassword,
|
||||
resetPassword,
|
||||
emailOtp,
|
||||
phoneNumber,
|
||||
} = authClient
|
||||
|
||||
// TODO: 手机号验证码发送接口还未实现
|
||||
// 需要后端提供手机号验证码发送接口,类似于 emailOtp.sendVerificationOtp
|
||||
// 预期接口:phoneOtp.sendVerificationOtp({ phone, type: 'phone-verification' })
|
||||
// 当前在 app/auth.tsx 中的 handleSendCode 函数中使用了临时模拟代码,需要替换为真实 API 调用
|
||||
|
||||
export const subscription: ISubscription = Reflect.get(authClient, 'subscription')
|
||||
|
|
|
|||
|
|
@ -38,3 +38,9 @@ export const isValidEmail = (email: string) => {
|
|||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
// 手机号格式验证函数(支持中国大陆手机号)
|
||||
export const isValidPhone = (phone: string) => {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue