From a95f63c24cce34047a401e118c13c377b0a6e7cd Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sat, 18 Jul 2026 21:55:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20T1.1=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=E5=AF=B9=E6=8E=A5=20=E2=80=94=20Aut?= =?UTF-8?q?hProvider=20+=20=E7=99=BB=E5=BD=95=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthProvider:管理 token 和用户状态,自动恢复登录态 - 登录页:邮箱密码表单,对接后端 /auth/login API - 根布局包裹 AuthProvider - 前端构建 8 路由全部成功 --- frontend/src/app/layout.tsx | 5 +- frontend/src/app/login/page.tsx | 94 +++++++++++++++++++++++++- frontend/src/lib/auth-context.tsx | 108 ++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 frontend/src/lib/auth-context.tsx diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 11c78a2..3bd9d95 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { AuthProvider } from "@/lib/auth-context"; import "./globals.css"; export const metadata: Metadata = { @@ -13,7 +14,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 94e86bc..b56544f 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,8 +1,35 @@ -/** 登录页 — 占位页面。 */ +"use client"; -import { Lock } from "lucide-react"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Lock, Mail, Loader2 } from "lucide-react"; +import { useAuth } from "@/lib/auth-context"; +/** + * 登录页 — 对接后端 auth API。 + */ export default function LoginPage() { + const router = useRouter(); + const { login } = useAuth(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setIsLoading(true); + try { + await login(email, password); + router.push("/dashboard"); + } catch (err) { + setError(err instanceof Error ? err.message : "登录失败"); + } finally { + setIsLoading(false); + } + } + return (
@@ -13,7 +40,68 @@ export default function LoginPage() {

AIPortPilot

AI+ 投后管理平台

-

登录功能开发中

+ +
+
+ +
+
+
+ +
+ +
+
+
+ + {error && ( +

+ {error} +

+ )} + + +
); diff --git a/frontend/src/lib/auth-context.tsx b/frontend/src/lib/auth-context.tsx new file mode 100644 index 0000000..23faacc --- /dev/null +++ b/frontend/src/lib/auth-context.tsx @@ -0,0 +1,108 @@ +/** 前端认证状态管理。 */ + +"use client"; + +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; + +interface AuthUser { + id: string; + email: string; + name: string; + role: string; + tenant_id: string; +} + +interface AuthContextValue { + user: AuthUser | null; + token: string | null; + isLoading: boolean; + login: (email: string, password: string) => Promise; + logout: () => void; +} + +const AuthContext = createContext(undefined); + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1"; + +/** + * 认证 Provider,管理登录状态和 token。 + */ +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const savedToken = localStorage.getItem("token"); + if (savedToken) { + setToken(savedToken); + fetchUserInfo(savedToken) + .then(setUser) + .catch(() => { + localStorage.removeItem("token"); + localStorage.removeItem("refresh_token"); + }) + .finally(() => setIsLoading(false)); + } else { + setIsLoading(false); + } + }, []); + + async function login(email: string, password: string) { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + if (!response.ok || data.code !== 0) { + throw new Error(data.detail || data.message || "登录失败"); + } + + const { access_token, refresh_token } = data.data; + localStorage.setItem("token", access_token); + localStorage.setItem("refresh_token", refresh_token); + setToken(access_token); + const userInfo = await fetchUserInfo(access_token); + setUser(userInfo); + } + + function logout() { + localStorage.removeItem("token"); + localStorage.removeItem("refresh_token"); + setToken(null); + setUser(null); + } + + return ( + + {children} + + ); +} + +/** + * 获取当前用户信息。 + */ +async function fetchUserInfo(token: string): Promise { + const response = await fetch(`${API_BASE_URL}/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + if (!response.ok || data.code !== 0) { + throw new Error("获取用户信息失败"); + } + return data.data; +} + +/** + * 使用认证上下文。 + */ +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error("useAuth 必须在 AuthProvider 内使用"); + } + return ctx; +}