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