Files
carrotskin/src/app/auth/page.tsx
Wuying Created Local Users 5f90f48a1c feat: 完成navbar隐藏优化和侧边栏冻结功能
- 优化navbar滚动隐藏逻辑,更敏感响应
- 添加返回顶部按钮,固定在右下角
- 实现profile页面侧边栏真正冻结效果
- 修复首页滑动指示器位置
- 优化整体布局确保首屏内容完整显示
2025-12-04 20:05:13 +08:00

733 lines
31 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';
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 { 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 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;
setIsLoading(true);
setAuthError('');
try {
await register(formData.username, formData.email, formData.password, formData.verificationCode);
errorManager.showSuccess('注册成功欢迎加入CarrotSkin');
router.push('/');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '注册失败,请稍后重试';
setAuthError(errorMessage);
errorManager.showError(errorMessage);
} finally {
setIsLoading(false);
}
}
};
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>
</div>
);
}