Files
carrotskin/src/app/auth/page.tsx

774 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { EyeIcon, EyeSlashIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
import { useAuth } from '@/contexts/AuthContext';
import { errorManager } from '@/components/ErrorNotification';
import SliderCaptcha from '@/components/SliderCaptcha';
export default function AuthPage() {
const [isLoginMode, setIsLoginMode] = useState(true);
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
verificationCode: '',
rememberMe: false,
agreeToTerms: false
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [authError, setAuthError] = useState('');
const [isSendingCode, setIsSendingCode] = useState(false);
const [codeTimer, setCodeTimer] = useState(0);
const [showCaptcha, setShowCaptcha] = useState(false);
const [isCaptchaVerified, setIsCaptchaVerified] = useState(false);
const [captchaId, setCaptchaId] = useState<string | undefined>();
const { login, register } = useAuth();
const router = useRouter();
useEffect(() => {
let interval: NodeJS.Timeout;
if (codeTimer > 0) {
interval = setInterval(() => {
setCodeTimer(prev => prev - 1);
}, 1000);
}
return () => clearInterval(interval);
}, [codeTimer]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
if (authError) {
setAuthError('');
}
};
const validateLoginForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.username.trim()) {
newErrors.username = '请输入用户名或邮箱';
}
if (!formData.password) {
newErrors.password = '请输入密码';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const validateRegisterForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.username.trim()) {
newErrors.username = '用户名不能为空';
} else if (formData.username.length < 3) {
newErrors.username = '用户名至少需要3个字符';
} else if (formData.username.length > 50) {
newErrors.username = '用户名不能超过50个字符';
}
if (!formData.email.trim()) {
newErrors.email = '邮箱不能为空';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = '请输入有效的邮箱地址';
}
if (!formData.password) {
newErrors.password = '密码不能为空';
} else if (formData.password.length < 6) {
newErrors.password = '密码至少需要6个字符';
} else if (formData.password.length > 128) {
newErrors.password = '密码不能超过128个字符';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = '两次输入的密码不一致';
}
if (!formData.verificationCode) {
newErrors.verificationCode = '请输入验证码';
} else if (!/^\d{6}$/.test(formData.verificationCode)) {
newErrors.verificationCode = '验证码应为6位数字';
}
if (!formData.agreeToTerms) {
newErrors.agreeToTerms = '请同意服务条款';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const getPasswordStrength = () => {
const password = formData.password;
if (password.length === 0) return { strength: 0, label: '', color: 'bg-gray-200' };
if (password.length < 6) return { strength: 1, label: '弱', color: 'bg-red-500' };
if (password.length < 10) return { strength: 2, label: '中等', color: 'bg-yellow-500' };
if (password.length >= 15) return { strength: 4, label: '很强', color: 'bg-green-500' };
return { strength: 3, label: '强', color: 'bg-blue-500' };
};
const passwordStrength = getPasswordStrength();
const handleSendCode = async () => {
if (!formData.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
setErrors(prev => ({ ...prev, email: '请输入有效的邮箱地址' }));
return;
}
setIsSendingCode(true);
try {
const response = await fetch('/api/v1/auth/send-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.email,
type: 'register'
}),
});
const data = await response.json();
if (data.code === 200) {
setCodeTimer(60);
errorManager.showSuccess('验证码已发送到您的邮箱');
} else {
setErrors(prev => ({ ...prev, email: data.message || '发送验证码失败' }));
errorManager.showError(data.message || '发送验证码失败');
}
} catch (error) {
setErrors(prev => ({ ...prev, email: '发送验证码失败,请稍后重试' }));
errorManager.showError('发送验证码失败,请稍后重试');
} finally {
setIsSendingCode(false);
}
};
const handleCaptchaVerify = (success: boolean) => {
if (success) {
setIsCaptchaVerified(true);
setShowCaptcha(false);
// 验证码验证成功后,继续注册流程
handleRegisterAfterCaptcha();
} else {
setIsCaptchaVerified(false);
setShowCaptcha(false);
errorManager.showError('验证码验证失败,请重试');
}
};
const handleRegisterAfterCaptcha = async () => {
setIsLoading(true);
setAuthError('');
try {
await register(formData.username, formData.email, formData.password, formData.verificationCode, captchaId);
errorManager.showSuccess('注册成功欢迎加入CarrotSkin');
router.push('/');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '注册失败,请稍后重试';
setAuthError(errorMessage);
errorManager.showError(errorMessage);
// 注册失败时重置验证码状态
setIsCaptchaVerified(false);
} finally {
setIsLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isLoginMode) {
if (!validateLoginForm()) return;
setIsLoading(true);
setAuthError('');
try {
await login(formData.username, formData.password);
if (formData.rememberMe) {
localStorage.setItem('rememberMe', 'true');
} else {
localStorage.removeItem('rememberMe');
}
errorManager.showSuccess('登录成功!');
router.push('/');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '登录失败,请检查用户名和密码';
setAuthError(errorMessage);
errorManager.showError(errorMessage);
} finally {
setIsLoading(false);
}
} else {
if (!validateRegisterForm()) return;
// 如果验证码还未验证,显示滑动验证码
if (!isCaptchaVerified) {
setShowCaptcha(true);
return;
}
// 如果验证码已验证,直接进行注册
handleRegisterAfterCaptcha();
}
};
const switchMode = () => {
setIsLoginMode(!isLoginMode);
setAuthError('');
setErrors({});
setFormData({
username: '',
email: '',
password: '',
confirmPassword: '',
verificationCode: '',
rememberMe: false,
agreeToTerms: false
});
};
return (
<div className="min-h-screen flex">
{/* Left Side - Orange Section */}
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-orange-500 via-orange-400 to-amber-500 flex-col justify-center items-center text-white p-12">
<motion.div
className="max-w-md"
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
>
<motion.div
className="w-24 h-24 bg-white/20 rounded-2xl flex items-center justify-center mb-8 backdrop-blur-sm border border-white/30"
whileHover={{ scale: 1.05, rotate: 5 }}
transition={{ type: 'spring', stiffness: 300 }}
>
<span className="text-4xl font-black">CS</span>
</motion.div>
<motion.h1
className="text-5xl font-black mb-6 leading-tight"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.6 }}
>
{isLoginMode ? '欢迎回来' : '加入我们'}
</motion.h1>
<motion.p
className="text-2xl mb-12 text-white/90 leading-relaxed"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.6 }}
>
{isLoginMode ? '继续你的创作之旅' : '开始你的创作之旅'}
</motion.p>
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.6 }}
>
<div className="flex items-center space-x-4">
<div className="w-8 h-8 bg-white/25 rounded-full flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-xl font-medium"></span>
</div>
<div className="flex items-center space-x-4">
<div className="w-8 h-8 bg-white/25 rounded-full flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-xl font-medium"></span>
</div>
<div className="flex items-center space-x-4">
<div className="w-8 h-8 bg-white/25 rounded-full flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-xl font-medium"></span>
</div>
</motion.div>
<motion.div
className="mt-16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
<p className="text-lg text-white/80">
<span className="font-bold text-white text-2xl">10,000</span>
</p>
</motion.div>
</motion.div>
</div>
{/* Right Side - White Section */}
<div className="w-full lg:w-1/2 bg-white dark:bg-gray-900 flex flex-col justify-center items-center p-4 lg:p-8">
<motion.div
className="w-full max-w-md"
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
>
{/* Back to Home Button */}
<motion.div
className="mb-6"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1, duration: 0.6 }}
>
<Link
href="/"
className="inline-flex items-center text-gray-500 dark:text-gray-400 hover:text-orange-500 dark:hover:text-orange-400 transition-colors duration-200"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</Link>
</motion.div>
{/* Header */}
<motion.div
className="mb-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.6 }}
>
<h2 className="text-4xl font-black text-gray-900 dark:text-white mb-2">
{isLoginMode ? '登录账户' : '创建账户'}
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
{isLoginMode ? '登录您的CarrotSkin账户' : '加入我们,开始创作'}
</p>
</motion.div>
{authError && (
<motion.div
className="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
>
<p className="text-red-600 dark:text-red-400 text-sm">{authError}</p>
</motion.div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Username */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
{isLoginMode ? '用户名或邮箱' : '用户名'}
{!isLoginMode && ' (3-50个字符)'}
</label>
<div className="relative">
<input
type="text"
name="username"
value={formData.username}
onChange={handleInputChange}
className={`w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 border-2 rounded-xl transition-all duration-200 focus:ring-2 focus:ring-orange-500 focus:border-transparent focus:bg-white dark:focus:bg-gray-700 ${
errors.username ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'
}`}
placeholder={isLoginMode ? "请输入用户名或邮箱" : "请输入用户名"}
disabled={isLoading}
/>
{!isLoginMode && formData.username && !errors.username && (
<CheckCircleIcon className="absolute right-3 top-3.5 w-5 h-5 text-green-500" />
)}
{errors.username && (
<XCircleIcon className="absolute right-3 top-3.5 w-5 h-5 text-red-500" />
)}
</div>
{errors.username && (
<p className="mt-1 text-sm text-red-500">{errors.username}</p>
)}
</motion.div>
{/* Email - Only for registration */}
<AnimatePresence>
{!isLoginMode && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="relative">
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={`w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 border-2 rounded-xl transition-all duration-200 focus:ring-2 focus:ring-orange-500 focus:border-transparent focus:bg-white dark:focus:bg-gray-700 ${
errors.email ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'
}`}
placeholder="请输入邮箱地址"
disabled={isLoading}
/>
{formData.email && !errors.email && (
<CheckCircleIcon className="absolute right-3 top-3.5 w-5 h-5 text-green-500" />
)}
{errors.email && (
<XCircleIcon className="absolute right-3 top-3.5 w-5 h-5 text-red-500" />
)}
</div>
{errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
)}
</motion.div>
)}
</AnimatePresence>
{/* Password */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
{!isLoginMode && ' (6-128个字符)'}
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
name="password"
value={formData.password}
onChange={handleInputChange}
className={`w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 border-2 rounded-xl transition-all duration-200 focus:ring-2 focus:ring-orange-500 focus:border-transparent pr-12 focus:bg-white dark:focus:bg-gray-700 ${
errors.password ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'
}`}
placeholder="请输入密码"
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
{showPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
</button>
</div>
{!isLoginMode && formData.password && (
<div className="mt-2">
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-600 dark:text-gray-400"></span>
<span className={`font-medium ${passwordStrength.color.replace('bg-', 'text-')}`}>
{passwordStrength.label}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${passwordStrength.color}`}
style={{ width: `${passwordStrength.strength * 25}%` }}
/>
</div>
</div>
)}
{errors.password && (
<p className="mt-1 text-sm text-red-500">{errors.password}</p>
)}
</motion.div>
{/* Confirm Password - Only for registration */}
<AnimatePresence>
{!isLoginMode && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
className={`w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 border-2 rounded-xl transition-all duration-200 focus:ring-2 focus:ring-orange-500 focus:border-transparent pr-12 focus:bg-white dark:focus:bg-gray-700 ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'
}`}
placeholder="请再次输入密码"
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
{showConfirmPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
</button>
</div>
{formData.confirmPassword && formData.password === formData.confirmPassword && !errors.confirmPassword && (
<CheckCircleIcon className="mt-1 w-5 h-5 text-green-500" />
)}
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-500">{errors.confirmPassword}</p>
)}
</motion.div>
)}
</AnimatePresence>
{/* Verification Code - Only for registration */}
<AnimatePresence>
{!isLoginMode && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="flex space-x-3">
<div className="flex-1 relative">
<input
type="text"
name="verificationCode"
value={formData.verificationCode}
onChange={handleInputChange}
className={`w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 border-2 rounded-xl transition-all duration-200 focus:ring-2 focus:ring-orange-500 focus:border-transparent focus:bg-white dark:focus:bg-gray-700 ${
errors.verificationCode ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'
}`}
placeholder="请输入6位验证码"
disabled={isLoading}
maxLength={6}
/>
{errors.verificationCode && (
<p className="mt-1 text-sm text-red-500">{errors.verificationCode}</p>
)}
</div>
<button
type="button"
onClick={handleSendCode}
disabled={isSendingCode || codeTimer > 0 || !formData.email || !!errors.email}
className="px-6 py-3 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 disabled:from-gray-400 disabled:to-gray-500 text-white font-medium rounded-xl transition-all duration-200 whitespace-nowrap"
>
{codeTimer > 0 ? `${codeTimer}秒后重试` : (isSendingCode ? '发送中...' : '发送验证码')}
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Remember Me - Only for login */}
<AnimatePresence>
{isLoginMode && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="flex items-center justify-between"
>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
name="rememberMe"
checked={formData.rememberMe}
onChange={handleInputChange}
className="w-4 h-4 text-orange-500 border-2 border-gray-300 rounded focus:ring-orange-500 focus:ring-2"
disabled={isLoading}
/>
<span className="text-sm text-gray-600 dark:text-gray-400">
</span>
</label>
<Link href="/forgot-password" className="text-sm text-orange-500 hover:text-orange-600 transition-colors">
</Link>
</motion.div>
)}
</AnimatePresence>
{/* Terms Agreement - Only for registration */}
<AnimatePresence>
{!isLoginMode && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<label className="flex items-start space-x-3 cursor-pointer">
<input
type="checkbox"
name="agreeToTerms"
checked={formData.agreeToTerms}
onChange={handleInputChange}
className="mt-1 w-4 h-4 text-orange-500 border-2 border-gray-300 rounded focus:ring-orange-500 focus:ring-2"
disabled={isLoading}
/>
<span className="text-sm text-gray-600 dark:text-gray-400">
<Link href="/terms" className="text-orange-500 hover:text-orange-600 underline ml-1">
</Link>
<Link href="/privacy" className="text-orange-500 hover:text-orange-600 underline ml-1">
</Link>
</span>
</label>
{errors.agreeToTerms && (
<p className="mt-1 text-sm text-red-500">{errors.agreeToTerms}</p>
)}
</motion.div>
)}
</AnimatePresence>
{/* Submit Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<motion.button
type="submit"
disabled={isLoading}
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 disabled:from-gray-400 disabled:to-gray-500 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{isLoading ? (
<div className="flex items-center justify-center space-x-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>{isLoginMode ? '登录中...' : '注册中...'}</span>
</div>
) : (
isLoginMode ? '登录' : '创建账户'
)}
</motion.button>
</motion.div>
</form>
{/* Social Login */}
<motion.div
className="mt-8"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-gray-900 text-gray-500 dark:text-gray-400">
</span>
</div>
</div>
<div className="mt-6 grid grid-cols-2 gap-4">
<motion.button
type="button"
className="flex items-center justify-center px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 font-medium text-gray-700 dark:text-gray-300 text-sm"
disabled={isLoading}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
</svg>
GitHub
</motion.button>
<motion.button
type="button"
className="flex items-center justify-center px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 font-medium text-gray-700 dark:text-gray-300 text-sm"
disabled={isLoading}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M11.5 2.5H9.5V8H11.5V2.5Z" />
<path d="M11.5 12H9.5V17.5H11.5V12Z" />
<path d="M6 7H2V17H6V7Z" />
<path d="M18 7H14V17H18V7Z" />
</svg>
Microsoft
</motion.button>
</div>
</motion.div>
{/* Mode Switch */}
<motion.div
className="text-center mt-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 }}
>
<p className="text-sm text-gray-600 dark:text-gray-400">
{isLoginMode ? '还没有账户?' : '已有账户?'}
<button
type="button"
onClick={switchMode}
className="text-orange-500 hover:text-orange-600 font-bold ml-1 transition-colors duration-200"
>
{isLoginMode ? '立即注册' : '立即登录'}
</button>
</p>
</motion.div>
</motion.div>
</div>
{/* Slider Captcha Component */}
{showCaptcha && (
<SliderCaptcha
onVerify={handleCaptchaVerify}
onClose={() => setShowCaptcha(false)}
/>
)}
</div>
);
}