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