Files
web/src/app/(auth)/login/login-card.tsx

215 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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})