108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import { Alert, Button, Input, Skeleton } from 'antd'
|
|
import http from '../api/http'
|
|
|
|
interface CaptchaChoice {
|
|
id: string
|
|
label: string
|
|
}
|
|
|
|
interface CaptchaChallenge {
|
|
captchaId: string
|
|
type: 'TextImage' | 'MathImage' | 'Selection' | 'OneClick'
|
|
prompt?: string
|
|
choices?: CaptchaChoice[]
|
|
imageData?: string
|
|
}
|
|
|
|
interface LoginCaptchaProps {
|
|
refreshKey: number
|
|
onChange: (captchaId: string, code: string) => void
|
|
}
|
|
|
|
export default function LoginCaptcha({ refreshKey, onChange }: LoginCaptchaProps) {
|
|
const [captcha, setCaptcha] = useState<CaptchaChallenge | null>(null)
|
|
const [code, setCode] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [loadError, setLoadError] = useState(false)
|
|
|
|
const refresh = useCallback(async () => {
|
|
setLoading(true)
|
|
setLoadError(false)
|
|
setCode('')
|
|
onChange('', '')
|
|
try {
|
|
const { data } = await http.post<CaptchaChallenge>('/auth/login/captcha', {}, { skipAuthSession: true })
|
|
setCaptcha(data)
|
|
} catch {
|
|
setCaptcha(null)
|
|
setLoadError(true)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [onChange])
|
|
|
|
useEffect(() => { void refresh() }, [refresh, refreshKey])
|
|
useEffect(() => { onChange(captcha?.captchaId ?? '', code) }, [captcha, code, onChange])
|
|
|
|
const isImageCaptcha = captcha?.type === 'TextImage' || captcha?.type === 'MathImage'
|
|
|
|
return (
|
|
<section className="captcha" aria-label="验证码">
|
|
<div className="captcha__heading">
|
|
<span>验证码</span>
|
|
<button className="captcha__refresh" type="button" disabled={loading} onClick={() => void refresh()}>
|
|
换一张
|
|
</button>
|
|
</div>
|
|
{loadError ? (
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
message="验证码暂时无法加载"
|
|
action={<Button size="small" onClick={() => void refresh()}>重新加载</Button>}
|
|
/>
|
|
) : loading && !captcha ? <Skeleton.Input active block style={{ height: 50 }} /> : (
|
|
<div className="captcha__body" aria-busy={loading}>
|
|
{isImageCaptcha && (
|
|
<>
|
|
{captcha?.imageData && (
|
|
<img className="captcha__image" src={captcha.imageData} alt="验证码图片,点击可更换" onClick={() => void refresh()} />
|
|
)}
|
|
<Input
|
|
value={code}
|
|
maxLength={8}
|
|
autoComplete="off"
|
|
placeholder="输入图中字符"
|
|
size="large"
|
|
onChange={(event) => setCode(event.target.value)}
|
|
/>
|
|
</>
|
|
)}
|
|
{captcha && !isImageCaptcha && (
|
|
<div className="captcha__question">
|
|
<p>{captcha.prompt}</p>
|
|
{captcha.imageData && (
|
|
<img className="captcha__image" src={captcha.imageData} alt="验证码图片,点击可更换" onClick={() => void refresh()} />
|
|
)}
|
|
{captcha.type === 'Selection' ? (
|
|
<div className="captcha__choices" role="group" aria-label="验证码选项">
|
|
{(captcha.choices ?? []).map((choice) => (
|
|
<Button key={choice.id} type={code === choice.id ? 'primary' : 'default'} onClick={() => setCode(choice.id)}>
|
|
{choice.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Button type={code === 'CONFIRM' ? 'primary' : 'default'} onClick={() => setCode('CONFIRM')}>
|
|
我确认是本人操作
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|