子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View 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>
);
}