| | |
| | | |
| | | import { useState, useEffect } from 'react'; |
| | | import Image from 'next/image'; |
| | | import { motion } from 'framer-motion'; |
| | | import { motion, AnimatePresence } from 'framer-motion'; |
| | | import Link from 'next/link'; |
| | | import { useRouter } from 'next/navigation'; |
| | | import ApiService from '@/utils/api'; |
| | | |
| | | // Toast 通知组件 |
| | | const Toast = ({ message, type, onClose }: { message: string; type: 'error' | 'success'; onClose: () => void }) => ( |
| | | <motion.div |
| | | initial={{ opacity: 0, y: -20 }} |
| | | animate={{ opacity: 1, y: 0 }} |
| | | exit={{ opacity: 0, y: -20 }} |
| | | className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg ${ |
| | | type === 'error' |
| | | ? 'bg-[#FF6A88]/10 border border-[#FF6A88]/20 text-[#FF6A88]' |
| | | : 'bg-[#6ADBFF]/10 border border-[#6ADBFF]/20 text-[#6ADBFF]' |
| | | }`} |
| | | > |
| | | <div className="flex items-center space-x-2"> |
| | | <span className="text-sm">{message}</span> |
| | | <button onClick={onClose} className="ml-2 text-sm hover:opacity-80">×</button> |
| | | </div> |
| | | </motion.div> |
| | | ); |
| | | |
| | | export default function RegisterPage() { |
| | | const [username, setUsername] = useState(''); |
| | |
| | | const [password, setPassword] = useState(''); |
| | | const [confirmPassword, setConfirmPassword] = useState(''); |
| | | const [isLoading, setIsLoading] = useState(false); |
| | | const [isSuccess, setIsSuccess] = useState(false); |
| | | const [mounted, setMounted] = useState(false); |
| | | const [particles, setParticles] = useState<Array<{x: number, y: number}>>([]); |
| | | const [toast, setToast] = useState<{ message: string; type: 'error' | 'success' } | null>(null); |
| | | const [errors, setErrors] = useState<{ |
| | | username?: string; |
| | | accountName?: string; |
| | | password?: string; |
| | | confirmPassword?: string; |
| | | }>({}); |
| | | const router = useRouter(); |
| | | |
| | | // 确保组件挂载后再显示动画效果和生成粒子 |
| | | useEffect(() => { |
| | |
| | | ); |
| | | } |
| | | |
| | | const handleSubmit = (e: React.FormEvent) => { |
| | | const showToast = (message: string, type: 'error' | 'success') => { |
| | | setToast({ message, type }); |
| | | setTimeout(() => setToast(null), 3000); |
| | | }; |
| | | |
| | | const validateForm = () => { |
| | | const newErrors: typeof errors = {}; |
| | | let isValid = true; |
| | | |
| | | if (!username) { |
| | | newErrors.username = '用户昵称不能为空'; |
| | | isValid = false; |
| | | } |
| | | |
| | | if (!accountName) { |
| | | newErrors.accountName = '账号名不能为空'; |
| | | isValid = false; |
| | | } else if (!/^[a-zA-Z0-9_]{4,16}$/.test(accountName)) { |
| | | newErrors.accountName = '账号名必须是4-16位字母、数字或下划线'; |
| | | isValid = false; |
| | | } |
| | | |
| | | if (!password) { |
| | | newErrors.password = '密码不能为空'; |
| | | isValid = false; |
| | | } else if (!/^[a-zA-Z0-9_]{6,16}$/.test(password)) { |
| | | newErrors.password = '密码必须是6-16位字母、数字或下划线'; |
| | | isValid = false; |
| | | } |
| | | |
| | | if (!confirmPassword) { |
| | | newErrors.confirmPassword = '请确认密码'; |
| | | isValid = false; |
| | | } else if (password !== confirmPassword) { |
| | | newErrors.confirmPassword = '两次输入的密码不一致'; |
| | | isValid = false; |
| | | } |
| | | |
| | | setErrors(newErrors); |
| | | return isValid; |
| | | }; |
| | | |
| | | const handleSubmit = async (e: React.FormEvent) => { |
| | | e.preventDefault(); |
| | | |
| | | if (!validateForm()) { |
| | | return; |
| | | } |
| | | |
| | | setIsLoading(true); |
| | | |
| | | // 模拟注册请求 |
| | | setTimeout(() => { |
| | | setIsLoading(false); |
| | | // 这里应该添加实际注册逻辑 |
| | | }, 2000); |
| | | try { |
| | | const response = await ApiService.post('/users/register', { |
| | | nickname: username, |
| | | accountName, |
| | | password |
| | | }); |
| | | |
| | | if (response.code === 200) { |
| | | setIsSuccess(true); |
| | | showToast('注册成功!正在跳转到登录页面...', 'success'); |
| | | setTimeout(() => { |
| | | router.push('/login'); |
| | | }, 1500); |
| | | } else { |
| | | setErrors({ |
| | | accountName: response.message || '注册失败,请重试' |
| | | }); |
| | | } |
| | | } catch (err) { |
| | | setErrors({ |
| | | accountName: '网络错误,请稍后重试' |
| | | }); |
| | | } finally { |
| | | if (!isSuccess) { |
| | | setIsLoading(false); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <div className="h-screen w-full flex items-center justify-center relative overflow-hidden"> |
| | | <AnimatePresence> |
| | | {toast && ( |
| | | <Toast |
| | | message={toast.message} |
| | | type={toast.type} |
| | | onClose={() => setToast(null)} |
| | | /> |
| | | )} |
| | | </AnimatePresence> |
| | | |
| | | {/* 背景效果 */} |
| | | <div className="fixed inset-0 bg-gradient-to-br from-[#0A1033] via-[#1E2B63] to-[#131C41] z-0"></div> |
| | | |
| | |
| | | id="username" |
| | | name="username" |
| | | type="text" |
| | | required |
| | | value={username} |
| | | onChange={(e) => setUsername(e.target.value)} |
| | | className="block w-full px-4 py-2 bg-[#131C41]/80 border border-[#6ADBFF]/30 rounded-md focus:outline-none focus:ring-1 focus:ring-[#6ADBFF]/60 focus:border-[#6ADBFF]/60 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300" |
| | | placeholder="用户名" |
| | | onChange={(e) => { |
| | | setUsername(e.target.value); |
| | | setErrors(prev => ({ ...prev, username: undefined })); |
| | | }} |
| | | className={`block w-full px-4 py-2 bg-[#131C41]/80 border ${ |
| | | errors.username |
| | | ? 'border-[#FF6A88] focus:border-[#FF6A88] focus:ring-[#FF6A88]/50' |
| | | : 'border-[#6ADBFF]/30 focus:border-[#6ADBFF]/60 focus:ring-[#6ADBFF]/50' |
| | | } rounded-md focus:outline-none focus:ring-1 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300`} |
| | | placeholder="用户昵称" |
| | | /> |
| | | <div className="absolute bottom-0 left-0 w-full h-[1px] bg-gradient-to-r from-[#6ADBFF] to-transparent scale-x-0 origin-left transition-transform duration-300 ease-out group-focus-within:scale-x-100"></div> |
| | | {errors.username && ( |
| | | <p className="mt-1 text-xs text-[#FF6A88]">{errors.username}</p> |
| | | )} |
| | | </motion.div> |
| | | |
| | | <motion.div |
| | |
| | | id="accountName" |
| | | name="accountName" |
| | | type="text" |
| | | required |
| | | value={accountName} |
| | | onChange={(e) => setAccountName(e.target.value)} |
| | | className="block w-full px-4 py-2 bg-[#131C41]/80 border border-[#6ADBFF]/30 rounded-md focus:outline-none focus:ring-1 focus:ring-[#6ADBFF]/60 focus:border-[#6ADBFF]/60 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300" |
| | | placeholder="账号名" |
| | | onChange={(e) => { |
| | | setAccountName(e.target.value); |
| | | setErrors(prev => ({ ...prev, accountName: undefined })); |
| | | }} |
| | | className={`block w-full px-4 py-2 bg-[#131C41]/80 border ${ |
| | | errors.accountName |
| | | ? 'border-[#FF6A88] focus:border-[#FF6A88] focus:ring-[#FF6A88]/50' |
| | | : 'border-[#6ADBFF]/30 focus:border-[#6ADBFF]/60 focus:ring-[#6ADBFF]/50' |
| | | } rounded-md focus:outline-none focus:ring-1 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300`} |
| | | placeholder="账号名(4-16位字母、数字或下划线)" |
| | | /> |
| | | <div className="absolute bottom-0 left-0 w-full h-[1px] bg-gradient-to-r from-[#6ADBFF] to-transparent scale-x-0 origin-left transition-transform duration-300 ease-out group-focus-within:scale-x-100"></div> |
| | | {errors.accountName && ( |
| | | <p className="mt-1 text-xs text-[#FF6A88]">{errors.accountName}</p> |
| | | )} |
| | | </motion.div> |
| | | |
| | | <motion.div |
| | |
| | | id="password" |
| | | name="password" |
| | | type="password" |
| | | required |
| | | value={password} |
| | | onChange={(e) => setPassword(e.target.value)} |
| | | className="block w-full px-4 py-2 bg-[#131C41]/80 border border-[#6ADBFF]/30 rounded-md focus:outline-none focus:ring-1 focus:ring-[#6ADBFF]/60 focus:border-[#6ADBFF]/60 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300" |
| | | placeholder="密码" |
| | | onChange={(e) => { |
| | | setPassword(e.target.value); |
| | | setErrors(prev => ({ ...prev, password: undefined })); |
| | | }} |
| | | className={`block w-full px-4 py-2 bg-[#131C41]/80 border ${ |
| | | errors.password |
| | | ? 'border-[#FF6A88] focus:border-[#FF6A88] focus:ring-[#FF6A88]/50' |
| | | : 'border-[#6ADBFF]/30 focus:border-[#6ADBFF]/60 focus:ring-[#6ADBFF]/50' |
| | | } rounded-md focus:outline-none focus:ring-1 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300`} |
| | | placeholder="密码(6-16位字母、数字或下划线)" |
| | | /> |
| | | <div className="absolute bottom-0 left-0 w-full h-[1px] bg-gradient-to-r from-[#6ADBFF] to-transparent scale-x-0 origin-left transition-transform duration-300 ease-out group-focus-within:scale-x-100"></div> |
| | | {errors.password && ( |
| | | <p className="mt-1 text-xs text-[#FF6A88]">{errors.password}</p> |
| | | )} |
| | | </motion.div> |
| | | |
| | | <motion.div |
| | |
| | | id="confirmPassword" |
| | | name="confirmPassword" |
| | | type="password" |
| | | required |
| | | value={confirmPassword} |
| | | onChange={(e) => setConfirmPassword(e.target.value)} |
| | | className="block w-full px-4 py-2 bg-[#131C41]/80 border border-[#6ADBFF]/30 rounded-md focus:outline-none focus:ring-1 focus:ring-[#6ADBFF]/60 focus:border-[#6ADBFF]/60 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300" |
| | | onChange={(e) => { |
| | | setConfirmPassword(e.target.value); |
| | | setErrors(prev => ({ ...prev, confirmPassword: undefined })); |
| | | }} |
| | | className={`block w-full px-4 py-2 bg-[#131C41]/80 border ${ |
| | | errors.confirmPassword |
| | | ? 'border-[#FF6A88] focus:border-[#FF6A88] focus:ring-[#FF6A88]/50' |
| | | : 'border-[#6ADBFF]/30 focus:border-[#6ADBFF]/60 focus:ring-[#6ADBFF]/50' |
| | | } rounded-md focus:outline-none focus:ring-1 text-white text-xs placeholder-[#6ADBFF]/50 transition-all duration-300`} |
| | | placeholder="确认密码" |
| | | /> |
| | | <div className="absolute bottom-0 left-0 w-full h-[1px] bg-gradient-to-r from-[#6ADBFF] to-transparent scale-x-0 origin-left transition-transform duration-300 ease-out group-focus-within:scale-x-100"></div> |
| | | {errors.confirmPassword && ( |
| | | <p className="mt-1 text-xs text-[#FF6A88]">{errors.confirmPassword}</p> |
| | | )} |
| | | </motion.div> |
| | | |
| | | <motion.div |
| | |
| | | > |
| | | <button |
| | | type="submit" |
| | | disabled={isLoading} |
| | | className="relative w-full py-2 px-4 rounded-full overflow-hidden border border-[#6ADBFF]/40 bg-gradient-to-r from-[#131C41] to-[#1E2B63] hover:border-[#6ADBFF]/70 transition-all duration-300 group quantum-button" |
| | | disabled={isLoading || isSuccess} |
| | | className="relative w-full py-2 px-4 rounded-full overflow-hidden border border-[#6ADBFF]/40 bg-gradient-to-r from-[#131C41] to-[#1E2B63] hover:border-[#6ADBFF]/70 transition-all duration-300 group quantum-button disabled:opacity-50 disabled:cursor-not-allowed" |
| | | > |
| | | <div className="relative z-10 flex items-center justify-center quantum-pulse"> |
| | | {isLoading ? ( |
| | | {isSuccess ? ( |
| | | <> |
| | | <svg className="w-4 h-4 text-[#6ADBFF]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> |
| | | </svg> |
| | | <span className="ml-2 text-xs text-white">注册成功,正在跳转...</span> |
| | | </> |
| | | ) : isLoading ? ( |
| | | <> |
| | | <div className="w-3 h-3 border-2 border-white/80 border-t-transparent rounded-full animate-spin"></div> |
| | | <span className="ml-2 text-xs text-white">注册中...</span> |