283 lines
7.7 KiB
TypeScript
283 lines
7.7 KiB
TypeScript
'use client'
|
|
import {useState, useCallback, useRef} from 'react'
|
|
import {Input} from '@/components/ui/input'
|
|
import {Button} from '@/components/ui/button'
|
|
import {Checkbox} from '@/components/ui/checkbox'
|
|
import {merge} from '@/lib/utils'
|
|
import Image from 'next/image'
|
|
import logo from '@/assets/logo.webp'
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardContent,
|
|
CardTitle,
|
|
} from '@/components/ui/card'
|
|
import {
|
|
Form,
|
|
FormField,
|
|
} from '@/components/ui/form'
|
|
import {zodResolver} from '@hookform/resolvers/zod'
|
|
import {useForm} from 'react-hook-form'
|
|
import zod from 'zod'
|
|
import Captcha from './captcha'
|
|
import verify from '@/actions/auth/verify'
|
|
import {login} from '@/actions/auth/login'
|
|
import {useRouter} from 'next/navigation'
|
|
import {toast} from 'sonner'
|
|
import {ApiResponse} from '@/lib/api'
|
|
import {Label} from '@/components/ui/label'
|
|
|
|
export type LoginPageProps = {}
|
|
|
|
// 定义表单验证模式
|
|
const formSchema = zod.object({
|
|
username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'),
|
|
password: zod.string().min(1, '请输入验证码'),
|
|
remember: zod.boolean().default(false),
|
|
})
|
|
|
|
type FormValues = zod.infer<typeof formSchema>
|
|
|
|
export default function LoginPage(props: LoginPageProps) {
|
|
const router = useRouter()
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [countdown, setCountdown] = useState(0)
|
|
const [showCaptcha, setShowCaptcha] = useState(false)
|
|
const timerRef = useRef<NodeJS.Timeout>(undefined)
|
|
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
username: '',
|
|
password: '',
|
|
remember: false,
|
|
},
|
|
})
|
|
|
|
// 获取表单值的快捷方式
|
|
const username = form.watch('username')
|
|
|
|
// 处理短信验证码发送前的验证
|
|
const checkUsername = useCallback(() => {
|
|
if (!username || username.length !== 11) {
|
|
form.setError('username', {
|
|
type: 'manual',
|
|
message: '请输入正确的手机号码',
|
|
})
|
|
return
|
|
}
|
|
|
|
// 显示图形验证码
|
|
setShowCaptcha(true)
|
|
}, [username, form])
|
|
|
|
// 验证图形验证码并发送短信验证码
|
|
const sendCode = useCallback(async (captchaCode: string) => {
|
|
if (!captchaCode) {
|
|
toast.error('请输入图形验证码')
|
|
return false
|
|
}
|
|
|
|
// 发送验证码
|
|
let resp: ApiResponse
|
|
try {
|
|
resp = await verify({
|
|
phone: username,
|
|
captcha: captchaCode,
|
|
})
|
|
}
|
|
catch (e) {
|
|
toast.error(`请求失败:${e}`)
|
|
return false
|
|
}
|
|
|
|
// 处理验证码发送结果
|
|
let waiting = 60
|
|
if (!resp.success) {
|
|
if (resp.status != 429) {
|
|
toast.error(resp.message)
|
|
return true
|
|
}
|
|
|
|
setShowCaptcha(false)
|
|
waiting = parseInt(resp.message)
|
|
console.log(resp.message)
|
|
toast.error('发送频率过快', {
|
|
description: '请稍后再试',
|
|
})
|
|
}
|
|
else {
|
|
setShowCaptcha(false)
|
|
toast.success('验证码已发送', {
|
|
description: '请注意查收短信',
|
|
})
|
|
}
|
|
|
|
// 开始倒计时
|
|
setCountdown(waiting)
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current)
|
|
}
|
|
timerRef.current = setInterval(() => {
|
|
setCountdown((prev) => {
|
|
if (prev <= 1) {
|
|
clearInterval(timerRef.current)
|
|
return 0
|
|
}
|
|
return prev - 1
|
|
})
|
|
}, 1000)
|
|
|
|
return false
|
|
}, [username])
|
|
|
|
// 处理表单提交
|
|
const onSubmit = async (values: FormValues) => {
|
|
try {
|
|
setSubmitting(true)
|
|
|
|
// 验证表单数据
|
|
if (values.username?.length !== 11) {
|
|
form.setError('username', {
|
|
type: 'manual',
|
|
message: '请输入有效的手机号码',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (!values.password) {
|
|
form.setError('password', {
|
|
type: 'manual',
|
|
message: '请输入验证码',
|
|
})
|
|
return
|
|
}
|
|
|
|
// 调用登录函数
|
|
const result = await login({
|
|
username: values.username,
|
|
password: values.password, // 使用验证码作为密码
|
|
remember: values.remember,
|
|
})
|
|
|
|
if (result.success) {
|
|
// 登录成功
|
|
toast.success('登陆成功', {
|
|
description: '欢迎回来!',
|
|
})
|
|
|
|
// 跳转到首页或用户仪表板
|
|
router.push('/')
|
|
router.refresh() // 刷新页面状态
|
|
}
|
|
else {
|
|
// 登录失败
|
|
toast.error(result.message, {
|
|
description: '请检查您的手机号码和验证码',
|
|
})
|
|
}
|
|
}
|
|
catch (e) {
|
|
toast.error('服务器错误', {
|
|
description: '请稍后再试',
|
|
})
|
|
}
|
|
finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className={merge(
|
|
`relative`,
|
|
`h-screen w-screen xl:pr-64 bg-[url(/login/bg.webp)] bg-cover bg-left`,
|
|
`flex justify-center xl:justify-end items-center`,
|
|
)}>
|
|
<Image src={logo} alt={`logo`} height={64} className={`absolute top-8 left-8`}/>
|
|
|
|
{/* 登录表单 */}
|
|
<Card className="w-96 mx-4 shadow-lg">
|
|
<CardHeader className="text-center">
|
|
<CardTitle className="text-2xl">登录/注册</CardTitle>
|
|
</CardHeader>
|
|
|
|
<CardContent className={`px-8`}>
|
|
<Form<FormValues> className="space-y-6" onSubmit={onSubmit} form={form}>
|
|
<FormField name="username" label={`手机号码`}>
|
|
{({id, field}) => (
|
|
<Input
|
|
{...field}
|
|
id={id}
|
|
type="tel"
|
|
placeholder="请输入手机号码"
|
|
autoComplete="tel-national"
|
|
/>
|
|
)}
|
|
</FormField>
|
|
|
|
<FormField name="password" label={`验证码`}>
|
|
{({id, field}) => (
|
|
<div className="flex space-x-4">
|
|
<Input
|
|
{...field}
|
|
id={id}
|
|
className="h-12"
|
|
placeholder="请输入验证码"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="whitespace-nowrap h-12"
|
|
onClick={checkUsername}
|
|
disabled={countdown > 0}
|
|
>
|
|
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
|
</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"
|
|
variant="gradient"
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? '登录中...' : '注册 / 登录'}
|
|
</Button>
|
|
|
|
<p className="text-xs text-center text-gray-500">
|
|
登录即表示您同意<a href="#" className="text-blue-600 hover:text-blue-500">《用户协议》</a>和<a href="#" className="text-blue-600 hover:text-blue-500">《隐私政策》</a>
|
|
</p>
|
|
</div>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 图形验证码弹窗 */}
|
|
<Captcha
|
|
showCaptcha={showCaptcha}
|
|
setShowCaptcha={setShowCaptcha}
|
|
handleSendCode={sendCode}
|
|
/>
|
|
|
|
</main>
|
|
)
|
|
}
|