src/app/globals.css | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/app/layout.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/app/login/page.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/context/UserContext.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/utils/api.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
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; } 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> ); 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çæ¹æ³è®¾ç½®token ApiService.setToken(token); // ä½¿ç¨æ°tokenè·åç¨æ·ä¿¡æ¯ 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' ? ( <> 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'); // 妿è·åç¨æ·ä¿¡æ¯å¤±è´¥ï¼æ¸ é¤token } } 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; } 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;