From 25d10113c4d27ccdea506e352b0bdd194560b0dc Mon Sep 17 00:00:00 2001 From: hongjli <3117313295@qq.com> Date: 星期三, 16 四月 2025 09:15:31 +0800 Subject: [PATCH] 注册 --- src/app/register/page.tsx | 206 ++++++++++++++++++++++++++++++++++++++++++++------- 1 files changed, 177 insertions(+), 29 deletions(-) diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 55b505e..ead9ed1 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -2,8 +2,29 @@ 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(''); @@ -11,8 +32,17 @@ 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(() => { @@ -34,19 +64,98 @@ ); } - 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> @@ -289,13 +398,21 @@ 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 @@ -308,13 +425,21 @@ 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 @@ -327,13 +452,21 @@ 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 @@ -346,13 +479,21 @@ 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 @@ -362,11 +503,18 @@ > <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> -- Gitblit v1.9.3