子仓库提交
This commit is contained in:
190
src/components/auth/CaptchaInput.tsx
Normal file
190
src/components/auth/CaptchaInput.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { getCaptchaApiV1AuthCaptchaGet } from '@/lib/api/sdk.gen';
|
||||
import type { CaptchaResponse } from '@/lib/api/types.gen';
|
||||
|
||||
interface CaptchaInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onCaptchaChange?: (captchaData: CaptchaResponse | null) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CaptchaInput({ value, onChange, onCaptchaChange, className = '' }: CaptchaInputProps) {
|
||||
const [captchaData, setCaptchaData] = useState<CaptchaResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchCaptcha = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
onChange(''); // 清空验证码输入
|
||||
|
||||
try {
|
||||
const response = await getCaptchaApiV1AuthCaptchaGet();
|
||||
console.log('API验证码获取成功:', response);
|
||||
setCaptchaData(response.data);
|
||||
if (onCaptchaChange) {
|
||||
onCaptchaChange(response.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('验证码获取失败:', err);
|
||||
|
||||
// 如果API失败,使用备用验证码
|
||||
const fallbackCaptcha = generateFallbackCaptcha();
|
||||
console.log('生成备用验证码:', fallbackCaptcha);
|
||||
setCaptchaData(fallbackCaptcha);
|
||||
if (onCaptchaChange) {
|
||||
onCaptchaChange(fallbackCaptcha);
|
||||
}
|
||||
setError(''); // 清除错误状态,因为备用验证码已生成
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateFallbackCaptcha = (): CaptchaResponse => {
|
||||
// 备用验证码生成(使用Canvas)
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
let text = '';
|
||||
for (let i = 0; i < 4; i++) {
|
||||
text += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 120;
|
||||
canvas.height = 40;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
return {
|
||||
captcha_id: 'fallback-' + Date.now(),
|
||||
image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
|
||||
};
|
||||
}
|
||||
|
||||
// 背景
|
||||
ctx.fillStyle = '#f0f0f0';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 干扰线
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.strokeStyle = `rgba(${Math.random() * 100}, ${Math.random() * 100}, ${Math.random() * 100}, 0.3)`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
||||
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 干扰点
|
||||
for (let i = 0; i < 30; i++) {
|
||||
ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.3)`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
Math.random() * canvas.width,
|
||||
Math.random() * canvas.height,
|
||||
1,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// 验证码文字
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const x = 20 + i * 25;
|
||||
const y = 20 + (Math.random() - 0.5) * 6;
|
||||
const angle = (Math.random() - 0.5) * 0.4;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate(angle);
|
||||
|
||||
// 随机颜色
|
||||
const colors = ['#16a34a', '#2563eb', '#dc2626', '#ea580c', '#8b5cf6'];
|
||||
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
ctx.fillText(char, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
return {
|
||||
captcha_id: 'fallback-' + Date.now(),
|
||||
image: canvas.toDataURL()
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCaptcha();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchCaptcha();
|
||||
};
|
||||
|
||||
const handleCaptchaChange = (inputValue: string) => {
|
||||
onChange(inputValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex gap-2 ${className}`}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入验证码"
|
||||
value={value}
|
||||
onChange={(e) => handleCaptchaChange(e.target.value.toUpperCase())}
|
||||
maxLength={4}
|
||||
className="flex-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="flex gap-2 items-center">
|
||||
{loading ? (
|
||||
<div className="w-[120px] h-10 border border-gray-300 rounded flex items-center justify-center bg-gray-100">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="w-[120px] h-10 border border-red-300 rounded flex items-center justify-center bg-red-50">
|
||||
<span className="text-xs text-red-500">获取失败</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="h-10 w-[120px] border border-gray-300 rounded cursor-pointer hover:opacity-80 transition-opacity overflow-hidden"
|
||||
onClick={handleRefresh}
|
||||
title="点击刷新验证码"
|
||||
>
|
||||
{captchaData && (
|
||||
<img
|
||||
src={captchaData.image}
|
||||
alt="验证码"
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
// 如果图片加载失败,隐藏错误图片
|
||||
e.currentTarget.style.display = 'none';
|
||||
setError('图片加载失败');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
title="刷新验证码"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user