forked from CarrotSkin/carrotskin
- 优化navbar滚动隐藏逻辑,更敏感响应 - 添加返回顶部按钮,固定在右下角 - 实现profile页面侧边栏真正冻结效果 - 修复首页滑动指示器位置 - 优化整体布局确保首屏内容完整显示
733 lines
31 KiB
TypeScript
733 lines
31 KiB
TypeScript
'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>
|
||
);
|
||
}
|