215 lines
7.1 KiB
TypeScript
215 lines
7.1 KiB
TypeScript
'use client'
|
||
import {Input} from '@/components/ui/input'
|
||
import {Button} from '@/components/ui/button'
|
||
import {Checkbox} from '@/components/ui/checkbox'
|
||
import {Card, CardContent} from '@/components/ui/card'
|
||
import {Form, FormField} from '@/components/ui/form'
|
||
import {Label} from '@/components/ui/label'
|
||
import {Tabs, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
||
import {EyeClosedIcon, EyeIcon} from 'lucide-react'
|
||
import {useState, ReactNode, useEffect, Suspense} from 'react'
|
||
import zod from 'zod'
|
||
import {useForm, useFormContext, useWatch} from 'react-hook-form'
|
||
import {zodResolver} from '@hookform/resolvers/zod'
|
||
import {toast} from 'sonner'
|
||
import {useRouter} from 'next/navigation'
|
||
import {login, LoginMode} from '@/actions/auth'
|
||
import {useProfileStore} from '@/components/stores/profile'
|
||
import dynamic from 'next/dynamic'
|
||
|
||
const smsSchema = zod.object({
|
||
username: zod.string().length(11, '请输入正确的手机号码'),
|
||
password: zod.string().length(6, '请输入正确的验证码'),
|
||
remember: zod.boolean(),
|
||
})
|
||
|
||
const pwdSchema = zod.object({
|
||
username: zod.string(),
|
||
password: zod.string().min(6, '请输入至少6位密码'),
|
||
remember: zod.boolean(),
|
||
})
|
||
|
||
export type LoginSchema = zod.infer<typeof smsSchema | typeof pwdSchema>
|
||
|
||
export default function LoginCard() {
|
||
const router = useRouter()
|
||
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||
const [mode, setMode] = useState<LoginMode>('phone_code')
|
||
const [submitting, setSubmitting] = useState(false)
|
||
|
||
const updateLoginMode = (mode: LoginMode) => {
|
||
sessionStorage.setItem('login_mode', mode)
|
||
}
|
||
|
||
useEffect(() => {
|
||
const mode = sessionStorage.getItem('login_mode')
|
||
if (mode) {
|
||
setMode(mode as LoginMode)
|
||
}
|
||
}, [])
|
||
|
||
const form = useForm<LoginSchema>({
|
||
resolver: zodResolver(mode === 'phone_code' ? smsSchema : pwdSchema),
|
||
defaultValues: {
|
||
username: '',
|
||
password: '',
|
||
remember: false,
|
||
},
|
||
})
|
||
const handler = form.handleSubmit(async (data) => {
|
||
setSubmitting(true)
|
||
try {
|
||
const result = await login({...data, mode})
|
||
if (!result.success) {
|
||
throw new Error(result.message || '请检查账号和密码/验证码是否正确')
|
||
}
|
||
|
||
const params = new URLSearchParams(window.location.search)
|
||
|
||
// 登录成功
|
||
await refreshProfile()
|
||
updateLoginMode(mode)
|
||
router.push(params.get('redirect') || '/')
|
||
toast.success('登录成功', {
|
||
description: '欢迎回来!',
|
||
})
|
||
}
|
||
catch (e) {
|
||
toast.error('登录失败', {
|
||
description: (e as Error).message,
|
||
})
|
||
}
|
||
finally {
|
||
setSubmitting(false)
|
||
}
|
||
})
|
||
|
||
const [showPwd, setShowPwd] = useState(false)
|
||
|
||
return (
|
||
<Card className="w-96 mx-4 shadow-lg relative z-20 py-8">
|
||
<CardContent className="px-8">
|
||
{/* 登录方式切换 */}
|
||
<Tabs
|
||
value={mode}
|
||
onValueChange={(val) => {
|
||
setMode(val as typeof mode)
|
||
form.reset({username: form.getValues('username'), password: '', remember: false})
|
||
}}
|
||
className="mb-6">
|
||
<TabsList className="w-full p-0 bg-white">
|
||
<Tab value="password">密码登录</Tab>
|
||
<Tab value="phone_code">验证码登录</Tab>
|
||
</TabsList>
|
||
</Tabs>
|
||
<Form<LoginSchema> className="space-y-6" form={form} handler={handler}>
|
||
<FormField name="username" label={mode === 'phone_code' ? '手机号' : '用户名'}>
|
||
{({id, field}) => (
|
||
<Input
|
||
{...field}
|
||
id={id}
|
||
type="tel"
|
||
placeholder={mode === 'phone_code' ? '请输入手机号' : '请输入用户名/手机号/邮箱'}
|
||
autoComplete="tel-national"
|
||
/>
|
||
)}
|
||
</FormField>
|
||
<FormField name="password" label={mode === 'phone_code' ? '验证码' : '密码'}>
|
||
{({id, field}) =>
|
||
mode === 'phone_code' ? (
|
||
<div className="flex space-x-4">
|
||
<Input
|
||
{...field}
|
||
id={id}
|
||
className="h-10"
|
||
placeholder="请输入验证码"
|
||
autoComplete="one-time-code"
|
||
/>
|
||
<SendMsgByUsername/>
|
||
</div>
|
||
) : (
|
||
<div className="relative">
|
||
<Input
|
||
{...field}
|
||
id={id}
|
||
type={showPwd ? 'text' : 'password'}
|
||
className="h-10 pr-10"
|
||
placeholder="至少6位密码,需包含字母和数字"
|
||
autoComplete="current-password"
|
||
minLength={6}
|
||
/>
|
||
<button
|
||
type="button"
|
||
tabIndex={-1}
|
||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||
onClick={() => setShowPwd(v => !v)}
|
||
aria-label={showPwd ? '隐藏密码' : '显示密码'}
|
||
>
|
||
{showPwd ? (
|
||
<EyeIcon size={20}/>
|
||
) : (
|
||
<EyeClosedIcon size={20}/>
|
||
)}
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|
||
</FormField>
|
||
<FormField name="remember">
|
||
{({id, field}) => (
|
||
<div className="flex flex-row items-start space-x-2 space-y-0">
|
||
<Checkbox
|
||
id={id}
|
||
checked={field.value}
|
||
onCheckedChange={field.onChange}
|
||
/>
|
||
<div className="space-y-1 leading-none">
|
||
<Label>保持登录</Label>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</FormField>
|
||
<div className="flex flex-col gap-3">
|
||
<Button
|
||
className="w-full h-12 text-lg"
|
||
type="submit"
|
||
theme="gradient"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? '登录中...' : (mode === 'phone_code' ? '首次登录即注册' : '立即登录')}
|
||
</Button>
|
||
<p className="text-xs text-center text-gray-500">
|
||
登录即表示您同意
|
||
<a href="/userAgreement" className="text-blue-600 hover:text-blue-500">《用户协议》</a>
|
||
和
|
||
<a href="/privacyPolicy" className="text-blue-600 hover:text-blue-500">《隐私政策》</a>
|
||
</p>
|
||
</div>
|
||
</Form>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
function Tab(props: {
|
||
value: string
|
||
children: ReactNode
|
||
}) {
|
||
return (
|
||
<TabsTrigger
|
||
className="h-12 text-base data-[state=active]:text-primary data-[state=active]:bg-primary-muted"
|
||
value={props.value}
|
||
>
|
||
{props.children}
|
||
</TabsTrigger>
|
||
)
|
||
}
|
||
|
||
function SendMsgByUsername() {
|
||
const {control} = useFormContext<LoginSchema>()
|
||
const phone = useWatch({control, name: 'username'})
|
||
return <SendMsg phone={phone}/>
|
||
}
|
||
|
||
const SendMsg = dynamic(() => import('@/components/send-msg'), {ssr: false})
|