353 lines
14 KiB
TypeScript
353 lines
14 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons'
|
|
import { Block, Text, Toast } from '@share/components'
|
|
import { router } from 'expo-router'
|
|
import React, { useCallback, useState } from 'react'
|
|
import { TextInput } from 'react-native'
|
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
|
|
|
import { APP_VERSION } from '@/app.constants'
|
|
import BannerSection from '@/components/BannerSection'
|
|
import { setAuthToken, signIn, signUp } from '@/lib/auth'
|
|
type AuthMode = 'login' | 'register'
|
|
|
|
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 [password, setPassword] = useState('')
|
|
const [confirmPassword, setConfirmPassword] = useState('')
|
|
const [showPassword, setShowPassword] = useState(false)
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const handleLogin = useCallback(async () => {
|
|
if (!username || !password) {
|
|
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)
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
if (result.error) {
|
|
Toast.show({ title: '请输入正确的账号或密码' })
|
|
} else {
|
|
Toast.show({ title: '登录成功!' })
|
|
router.replace('/(tabs)')
|
|
}
|
|
} catch (error: any) {
|
|
Toast.show({ title: error.message || '登录失败' })
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [username, password, signIn])
|
|
|
|
const handleRegister = useCallback(async () => {
|
|
if (!email || !username || !password) {
|
|
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)
|
|
|
|
if (result.error) {
|
|
Toast.show({ title: result.error.message || '注册失败' })
|
|
} else {
|
|
Toast.show({ title: '注册成功!' })
|
|
}
|
|
} catch (error: any) {
|
|
Toast.show({ title: error.message || '注册失败' })
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [email, username, password, confirmPassword, signUp])
|
|
|
|
const handleSubmit = useCallback(() => {
|
|
if (mode === 'login') {
|
|
handleLogin()
|
|
} else {
|
|
handleRegister()
|
|
}
|
|
}, [mode, handleLogin, handleRegister])
|
|
|
|
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="mb-[24px] flex-row gap-[12px]">
|
|
{(['login', 'register'] as const).map((tabMode) => {
|
|
const isActive = mode === tabMode
|
|
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={() => setMode(tabMode)}
|
|
>
|
|
<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]">
|
|
{mode === 'login' ? (
|
|
<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="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>
|
|
</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="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>
|
|
|
|
{mode === 'login' && (
|
|
<Block className="items-end">
|
|
<Text
|
|
className="font-700 text-[12px] text-gray-500"
|
|
onClick={() => router.push('/forgot-password')}
|
|
>
|
|
忘记密码
|
|
</Text>
|
|
</Block>
|
|
)}
|
|
|
|
{mode === 'register' && (
|
|
<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={handleSubmit}
|
|
>
|
|
{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 ? '处理中...' : mode === 'login' ? '登录' : '注册'}
|
|
</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>
|
|
)
|
|
}
|