182 lines
5.9 KiB
TypeScript
182 lines
5.9 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogPortal,
|
||
DialogOverlay,
|
||
} from "@/components/ui/dialog"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||
import { Shield, RefreshCw, AlertCircle } from "lucide-react"
|
||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||
|
||
interface CaptchaDialogProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
onVerify: () => void
|
||
onCancel: () => void
|
||
}
|
||
|
||
export function CaptchaDialog({
|
||
open,
|
||
onOpenChange,
|
||
onVerify,
|
||
onCancel
|
||
}: CaptchaDialogProps) {
|
||
const [num1, setNum1] = useState(0)
|
||
const [num2, setNum2] = useState(0)
|
||
const [userAnswer, setUserAnswer] = useState("")
|
||
const [error, setError] = useState("")
|
||
|
||
const generateCaptcha = () => {
|
||
const newNum1 = Math.floor(Math.random() * 10) + 1
|
||
const newNum2 = Math.floor(Math.random() * 10) + 1
|
||
setNum1(newNum1)
|
||
setNum2(newNum2)
|
||
setUserAnswer("")
|
||
setError("")
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
generateCaptcha()
|
||
}
|
||
}, [open])
|
||
|
||
const handleVerify = () => {
|
||
const correctAnswer = num1 + num2
|
||
const inputAnswer = parseInt(userAnswer)
|
||
|
||
if (isNaN(inputAnswer) || inputAnswer !== correctAnswer) {
|
||
setError("验证码错误,请重新计算")
|
||
generateCaptcha()
|
||
return
|
||
}
|
||
|
||
setError("")
|
||
onVerify()
|
||
onOpenChange(false)
|
||
}
|
||
|
||
const handleCancel = () => {
|
||
setUserAnswer("")
|
||
setError("")
|
||
onCancel()
|
||
onOpenChange(false)
|
||
}
|
||
|
||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||
if (e.key === 'Enter') {
|
||
handleVerify()
|
||
}
|
||
}
|
||
|
||
// 防止第一次打开时的遮罩层问题
|
||
const handleOpenChange = (newOpen: boolean) => {
|
||
if (!newOpen) {
|
||
setUserAnswer("")
|
||
setError("")
|
||
onCancel()
|
||
}
|
||
onOpenChange(newOpen)
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||
<DialogPortal>
|
||
<DialogOverlay className="fixed inset-0 z-[100] bg-black/80" />
|
||
<DialogPrimitive.Content
|
||
className="fixed left-[50%] top-[50%] z-[101] grid w-full max-w-md translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg focus:outline-none"
|
||
onPointerDownOutside={(e) => e.preventDefault()}
|
||
>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2 text-foreground">
|
||
<Shield className="h-5 w-5 text-blue-500 dark:text-blue-400" />
|
||
安全验证
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
为了防止恶意使用,请完成以下验证码验证后再发送登录邮件
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<Alert className="border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/50">
|
||
<AlertCircle className="h-4 w-4" />
|
||
<AlertDescription>
|
||
<strong>提示:</strong>计算下面的数学题,输入答案完成验证
|
||
</AlertDescription>
|
||
</Alert>
|
||
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-center p-6 bg-muted/50 rounded-lg border">
|
||
<div className="text-center">
|
||
<div className="text-2xl font-bold text-foreground mb-2">
|
||
{num1} + {num2} = ?
|
||
</div>
|
||
<div className="flex items-center justify-center gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={generateCaptcha}
|
||
className="text-muted-foreground hover:text-foreground"
|
||
>
|
||
<RefreshCw className="h-4 w-4 mr-1" />
|
||
换一题
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="captcha-answer" className="text-foreground">
|
||
请输入答案
|
||
</Label>
|
||
<Input
|
||
id="captcha-answer"
|
||
type="number"
|
||
placeholder="输入计算结果"
|
||
value={userAnswer}
|
||
onChange={(e) => setUserAnswer(e.target.value)}
|
||
onKeyDown={handleKeyPress}
|
||
className="text-center text-lg font-medium"
|
||
autoFocus
|
||
/>
|
||
</div>
|
||
|
||
{error && (
|
||
<Alert className="border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">
|
||
<AlertCircle className="h-4 w-4" />
|
||
<AlertDescription>{error}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter className="flex justify-between">
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleCancel}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleVerify}
|
||
disabled={!userAnswer.trim()}
|
||
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||
>
|
||
验证并发送邮件
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogPrimitive.Content>
|
||
</DialogPortal>
|
||
</Dialog>
|
||
)
|
||
}
|