From bce8e3bf66c00ef6fb32f26c27c39b5ac3b4d47f Mon Sep 17 00:00:00 2001 From: hongjli <3117313295@qq.com> Date: 星期三, 16 四月 2025 08:59:01 +0800 Subject: [PATCH] 登录 --- src/app/layout.tsx | 5 + src/app/login/page.tsx | 77 ++++++++++++++++++- src/utils/api.ts | 63 +++++++++++++++ src/context/UserContext.tsx | 64 ++++++++++++++++ src/app/globals.css | 13 +++ 5 files changed, 216 insertions(+), 6 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 8ce8f4c..9ed40ec 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -806,3 +806,16 @@ .animate-shine { animation: shine 1.5s ease-in-out; } + +@keyframes error-scan { + 0% { + transform: translateX(0%); + } + 100% { + transform: translateX(50%); + } +} + +.animate-error-scan { + animation: error-scan 2s linear infinite; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8f6d69d..6c02623 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import "./globals.css"; import ClientLayoutContent from '@/components/layout/ClientLayoutContent'; import Script from 'next/script'; +import { UserProvider } from "@/context/UserContext"; const inter = Inter({ subsets: ["latin"] }); @@ -39,7 +40,9 @@ <html lang="zh-CN" className="smooth-scroll"> <ScrollToTop /> <body className={`${inter.className} overflow-x-hidden`}> - <ClientLayoutContent>{children}</ClientLayoutContent> + <UserProvider> + <ClientLayoutContent>{children}</ClientLayoutContent> + </UserProvider> </body> </html> ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 727e05e..4f32450 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -4,6 +4,17 @@ import Image from 'next/image'; import { motion } from 'framer-motion'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useUser } from '@/context/UserContext'; +import ApiService from '@/utils/api'; + +// 瀹氫箟User鎺ュ彛 +interface User { + id: number; + username: string; + email: string; + // 鍏朵粬鐢ㄦ埛灞炴��... +} export default function LoginPage() { const [email, setEmail] = useState(''); @@ -13,21 +24,54 @@ const [isLoading, setIsLoading] = useState(false); const [mounted, setMounted] = useState(false); const [loginMethod, setLoginMethod] = useState<'password' | 'sms'>('password'); + const [error, setError] = useState(''); + const router = useRouter(); + const { setUser } = useUser(); // 纭繚缁勪欢鎸傝浇鍚庡啀鏄剧ず鍔ㄧ敾鏁堟灉 useEffect(() => { setMounted(true); }, []); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); + setError(''); // 娓呴櫎涔嬪墠鐨勯敊璇俊鎭� - // 妯℃嫙鐧诲綍璇锋眰 - setTimeout(() => { + try { + const response = await ApiService.post<string>('/users/login', { + accountName: email, + password, + }); + + if (response.code === 200) { + // 鑾峰彇token + const token = response.data; + // 浣跨敤ApiService鐨勬柟娉曡缃畉oken + ApiService.setToken(token); + + // 浣跨敤鏂皌oken鑾峰彇鐢ㄦ埛淇℃伅 + try { + const userData = await ApiService.get<User>('/users/info', token); + + if (userData.code === 200) { + // 淇濆瓨鐢ㄦ埛淇℃伅鍒板叏灞�鐘舵�� + setUser(userData.data); + router.push('/'); // 鐧诲綍鎴愬姛鍚庤烦杞埌棣栭〉 + } else { + setError('鑾峰彇鐢ㄦ埛淇℃伅澶辫触'); + } + } catch (error) { + setError('鑾峰彇鐢ㄦ埛淇℃伅澶辫触'); + } + } else { + setError(response.message || '鐧诲綍澶辫触锛岃妫�鏌ヨ处鍙峰瘑鐮�'); + } + } catch (err) { + setError('缃戠粶閿欒锛岃绋嶅悗閲嶈瘯'); + } finally { setIsLoading(false); - // 杩欓噷搴旇娣诲姞瀹為檯鐧诲綍閫昏緫 - }, 2000); + } }; return ( @@ -239,6 +283,29 @@ {/* 璐﹀彿鐧诲綍琛ㄥ崟 */} <form onSubmit={handleSubmit} className="space-y-4"> + {error && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + className="relative overflow-hidden backdrop-blur-sm bg-[#FF6A88]/5 border border-[#FF6A88]/20 rounded-lg p-3" + > + <div className="absolute top-0 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-[#FF6A88]/50 to-transparent"></div> + <div className="absolute bottom-0 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-[#FF6A88]/50 to-transparent"></div> + <div className="absolute top-0 bottom-0 left-0 w-[1px] bg-gradient-to-b from-transparent via-[#FF6A88]/50 to-transparent"></div> + <div className="absolute top-0 bottom-0 right-0 w-[1px] bg-gradient-to-b from-transparent via-[#FF6A88]/50 to-transparent"></div> + + <div className="flex items-center space-x-2"> + <svg className="w-4 h-4 text-[#FF6A88]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> + </svg> + <span className="text-xs text-[#FF6A88]">{error}</span> + </div> + + {/* 鍔ㄦ�佹壂鎻忕嚎 */} + <div className="absolute top-0 -left-full w-[200%] h-full bg-gradient-to-r from-transparent via-[#FF6A88]/10 to-transparent animate-error-scan"></div> + </motion.div> + )} + <div className="space-y-3"> {loginMethod === 'password' ? ( <> diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx new file mode 100644 index 0000000..23bb739 --- /dev/null +++ b/src/context/UserContext.tsx @@ -0,0 +1,64 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect } from 'react'; +import ApiService from '@/utils/api'; + +interface User { + id: number; + username: string; + email: string; + // 鏍规嵁瀹為檯鐢ㄦ埛淇℃伅缁撴瀯娣诲姞鍏朵粬瀛楁 +} + +interface UserContextType { + user: User | null; + setUser: (user: User | null) => void; + isLoading: boolean; + logout: () => void; +} + +const UserContext = createContext<UserContextType | undefined>(undefined); + +export function UserProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState<User | null>(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const initializeUser = async () => { + const token = localStorage.getItem('token'); + if (token) { + try { + const response = await ApiService.get<User>('/users/info'); + if (response.code === 200) { + setUser(response.data); + } + } catch (error) { + console.error('Failed to fetch user info:', error); + localStorage.removeItem('token'); // 濡傛灉鑾峰彇鐢ㄦ埛淇℃伅澶辫触锛屾竻闄oken + } + } + setIsLoading(false); + }; + + initializeUser(); + }, []); + + const logout = () => { + setUser(null); + localStorage.removeItem('token'); + }; + + return ( + <UserContext.Provider value={{ user, setUser, isLoading, logout }}> + {children} + </UserContext.Provider> + ); +} + +export function useUser() { + const context = useContext(UserContext); + if (context === undefined) { + throw new Error('useUser must be used within a UserProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 0000000..b172533 --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,63 @@ +const API_BASE_URL = 'http://localhost:8080/api'; + +interface ApiResponse<T = any> { + code: number; + message: string; + data: T; +} + +class ApiService { + private static getToken(): string | null { + if (typeof window !== 'undefined') { + return localStorage.getItem('token'); + } + return null; + } + + static setToken(token: string): void { + if (typeof window !== 'undefined') { + localStorage.setItem('token', token); + } + } + + private static async fetchWithToken(url: string, options: RequestInit = {}, customToken?: string): Promise<ApiResponse> { + const token = customToken || this.getToken(); + const headers = { + 'Content-Type': 'application/json', + ...(token ? { 'token': token } : {}), + ...options.headers, + }; + + const response = await fetch(`${API_BASE_URL}${url}`, { + ...options, + headers, + }); + + const data = await response.json(); + return data; + } + + static async get<T>(url: string, customToken?: string): Promise<ApiResponse<T>> { + return this.fetchWithToken(url, { method: 'GET' }, customToken); + } + + static async post<T>(url: string, body: any, customToken?: string): Promise<ApiResponse<T>> { + return this.fetchWithToken(url, { + method: 'POST', + body: JSON.stringify(body), + }, customToken); + } + + static async put<T>(url: string, body: any, customToken?: string): Promise<ApiResponse<T>> { + return this.fetchWithToken(url, { + method: 'PUT', + body: JSON.stringify(body), + }, customToken); + } + + static async delete<T>(url: string, customToken?: string): Promise<ApiResponse<T>> { + return this.fetchWithToken(url, { method: 'DELETE' }, customToken); + } +} + +export default ApiService; \ No newline at end of file -- Gitblit v1.9.3