From c637d6eaecf6f0f28e66038230fd5a4c46dc3d0f Mon Sep 17 00:00:00 2001 From: Kevin987 <2920370144@qq.com> Date: Wed, 4 Feb 2026 16:47:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=B7=E6=96=B0=E4=BB=A4=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Layout.tsx | 2 +- src/api/user.ts | 48 +++++-- src/components/Content.tsx | 10 +- src/components/Header.tsx | 21 +-- src/components/ui/table.tsx | 114 +++++++++++++++ src/lib/http.ts | 92 +++++++++++-- src/pages/Article.tsx | 53 +++++++ src/pages/Login.tsx | 268 ++++++++++++++++++++++++++++++++++++ src/pages/Register.tsx | 72 ++++++---- src/routes.tsx | 10 ++ src/store/useUserStore.ts | 46 +++++-- src/types/user.ts | 13 ++ 12 files changed, 674 insertions(+), 75 deletions(-) create mode 100644 src/components/ui/table.tsx create mode 100644 src/pages/Article.tsx create mode 100644 src/pages/Login.tsx diff --git a/src/Layout.tsx b/src/Layout.tsx index 671a2ba..af272cb 100644 --- a/src/Layout.tsx +++ b/src/Layout.tsx @@ -4,7 +4,7 @@ import { Toaster } from "@/components/ui/sonner" function Layout() { return ( <> -
+
diff --git a/src/api/user.ts b/src/api/user.ts index 5034452..7c0f97c 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,18 +1,36 @@ -import http from '@/lib/http' -import type { Result } from '@/types/common' -import type { UserRegisterT } from '@/types/user' -import CryptoJS from 'crypto-js'; +import http from "@/lib/http"; +import type { Result } from "@/types/common"; +import type { Token, UserRegisterT, Login } from "@/types/user"; +import CryptoJS from "crypto-js"; -const prefix = '/user' -export const getCaptcha = (reqId:string): Promise>=>{ - return http.get(prefix+'/captcha/'+reqId) -} +const prefix = "/user"; +// 获取验证码 +export const getCaptcha = (reqId: string): Promise> => { + return http.get(prefix + "/captcha/" + reqId); +}; -export const register = (user: UserRegisterT):Promise>=>{ - user.password = CryptoJS.MD5(user.password as string).toString() - return http.post(prefix+'/register',user) -} +// 注册 +export const register = (user: UserRegisterT): Promise> => { + user.password = CryptoJS.MD5(user.password as string).toString(); + return http.post(prefix + "/register", user); +}; -export const existUsername = (username:string):Promise>=>{ - return http.get(prefix+'/exist/'+username) -} \ No newline at end of file +// 登录 +export const login = (user: Login): Promise> => { + user.password = CryptoJS.MD5(user.password as string).toString(); + return http.post(prefix + "/login", user); +}; + +// 用户是否存在 +export const existUsername = (username: string): Promise> => { + return http.get(prefix + "/exist/" + username); +}; + +// 刷新令牌 +export const refreshToken = (token: string): Promise> => { + return http.post(prefix + "/refreshToken", token, { + headers: { + "Content-Type": "text/plain", + }, + }); +}; diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 88dfec5..da06d0d 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -81,11 +81,11 @@ function Img() {
- - - - - + + + + +
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 975b278..24c09ba 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,13 +1,13 @@ import avatar from "@/assets/image.png"; -import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; +import useUserStore from "@/store/useUserStore"; + import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; @@ -21,7 +21,7 @@ import { LogOutIcon, } from "lucide-react" function Header() { - const [loginFlag, useLoginFlag] = useState(false); + const isLogin = useUserStore((state) => state.isLogin); return ( <>
@@ -29,14 +29,12 @@ function Header() {

X-Blog

- - - + {isLogin?:}
@@ -54,6 +52,11 @@ function Unlogin() { } function Logined() { + + const clearToken = useUserStore((state) => state.clearToken); + function logoutEvent(){ + clearToken() + } return ( <>
@@ -81,7 +84,7 @@ function Logined() { 更换账号 - 退出 + logoutEvent()} variant="destructive">退出 diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..5513a5c --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/lib/http.ts b/src/lib/http.ts index efdc2cb..4d8ebb4 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -1,17 +1,89 @@ -import axios from 'axios'; +import axios from "axios"; +import useUserStore from "@/store/useUserStore"; +import { refreshToken } from "@/api/user"; -const request = axios.create({ - baseURL: '/api', // 指向你的 Java SpringBoot 后端地址 - timeout: 10000, +const service = axios.create({ + baseURL: "/api", // 指向你的 Java SpringBoot 后端地址 + timeout: 60000, }); -// 响应拦截器 -request.interceptors.response.use( - (response) => response.data, +service.interceptors.request.use( + (config) => { + const { token } = useUserStore.getState(); + if (token) { + config.headers.Authorization = `${token.tokenType} ${token.accessToken}`; + } + return config; + }, (error) => { - console.error('网络请求出错:', error); + // 对请求错误做些什么 return Promise.reject(error); - } + }, ); -export default request; \ No newline at end of file +// 记录是否正在刷新中 +let isRefreshing = false; +// 存储因为等待刷新而挂起的请求 +let requestsQueue: Array<(token: string) => void> = []; + +// 响应拦截器 +service.interceptors.response.use( + (response) => response.data, + async (error) => { + const { config, response } = error; + + // 如果是 401 错误且不是正在刷新的请求本身 + if (response && response.status === 401 && !config._retry) { + if (isRefreshing) { + // 如果正在刷新,将请求暂存,返回一个未解析的 Promise + return new Promise((resolve) => { + requestsQueue.push((token) => { + config.headers["Authorization"] = `Bearer ${token}`; + resolve(service(config)); + }); + }); + } + + // 标记正在刷新,开启重试锁 + config._retry = true; + isRefreshing = true; + + try { + const { token } = useUserStore.getState(); + + // 发送刷新请求 + const res = await refreshToken(token!.refreshToken); + + if (res.code == 0) { + const access_token = res.data.accessToken; + useUserStore.setState((state) => ({ + ...state, + token: res.data, + })); + // 执行队列中的请求 + requestsQueue.forEach((callback) => callback(access_token)); + requestsQueue = []; + // 重发当前请求 + config.headers["Authorization"] = `Bearer ${access_token}`; + return service(config); + } + } catch (refreshError) { + // 刷新也失败了(如 refresh_token 过期),只能重新登录 + console.error("Token 刷新失败:", refreshError); + useUserStore.setState((state) => ({ + ...state, + token: undefined, + isLogin: false, + })); + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + } + + console.error("网络请求出错:", error); + return Promise.reject(error); + }, +); + +export default service; diff --git a/src/pages/Article.tsx b/src/pages/Article.tsx new file mode 100644 index 0000000..f2b3db0 --- /dev/null +++ b/src/pages/Article.tsx @@ -0,0 +1,53 @@ +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +export default function Article() { + return ( + <> +
+ +
+
+ + A list of your recent invoices. + + + 标题 + 状态 + 发布日期 + 浏览量 + 收藏量 + 点赞量 + 点踩量 + 操作 + + + + + INV001 + Paid + Credit Card + $250.00 + Paid + Credit Card + $250.00 + + + + + + + + +
+
+ + ); +} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..a84cfc9 --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,268 @@ +import { Input } from "@/components/ui/input"; +import { useState, useRef, useEffect } from "react"; +import { getCaptcha, login } from "@/api/user"; +import axios from "axios"; +import useUserStore from "@/store/useUserStore"; +import { toast } from "sonner"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import type { Login } from "@/types/user"; +import { useNavigate } from "react-router-dom"; + +interface LoginErrMsg { + usernameBlank?: string; + passwdBlank?: string; + verifyBlank?: string; + agreeBlank?: string; +} + + + +const user: Login = {}; +let agreeFlag:boolean = false +export default function Login() { + const [loginErrMsg, setLoginErrMsg] = useState({ + usernameBlank: "", + passwdBlank: "", + verifyBlank: "", + agreeBlank: "", + }); + + const navigate = useNavigate(); + const [captcha, setCaptcha] = useState(null); + const captchaId = useUserStore((state) => state.key); + const setToken = useUserStore((state) => state.setToken); + + // 登录检查 + function loginValidator(loginErrMsg: LoginErrMsg) { + let valid = true; + let vErrmsg: string = "请输入登录信息"; + Object.entries(loginErrMsg).forEach(([, v]) => { + if (v) { + valid = false; + vErrmsg = v; + } + }); + if(vErrmsg) valid =false + if ( + user.username && + user.password && + user.verificationCode && + agreeFlag + ) { + valid = true; + user.key = captchaId + } + return [valid, vErrmsg]; + } + + // 登录点击 + const loginClick = async ()=> { + const [valid, vErrmsg] = loginValidator(loginErrMsg); + if (!valid) { + toast.warning(vErrmsg, { position: "top-center" }); + return + } + const res = await login(user) + if(res.code==0){ + setToken(res.data) + navigate('/') + toast.info(res.msg,{position: 'top-center'}) + }else { + toast.warning(res.msg,{position: 'top-center'}) + } + } + // 用户协议选择 + function agreeChenge(checkd: boolean) { + agreeFlag = checkd + if (checkd) { + setLoginErrMsg({ ...loginErrMsg, agreeBlank: "" }); + } else { + setLoginErrMsg({ ...loginErrMsg, agreeBlank: "请确认用户及隐私协议" }); + } + } + + // 初始化验证码 + useEffect(() => { + const fetchCaptcha = async () => { + const res = await getCaptcha(captchaId); + if (res.code == 0) { + setCaptcha(res.data); + } + }; + fetchCaptcha(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // 用户名变更事件 + function usernameChange(username: string) { + user.username = username; + if (!username) { + setLoginErrMsg({ ...loginErrMsg, usernameBlank: "用户名不能为空" }); + } else { + setLoginErrMsg({ ...loginErrMsg, usernameBlank: "" }); + } + } + + // 密码变更事件 + function passwdChange(passwd: string) { + user.password = passwd; + if (!passwd) { + setLoginErrMsg({ ...loginErrMsg, passwdBlank: "密码不能为空" }); + } else { + setLoginErrMsg({ ...loginErrMsg, passwdBlank: "" }); + } + } + + // 验证码变更事件 + function verifyChange(verifyCode: string) { + user.verificationCode = verifyCode; + if (!verifyCode) { + setLoginErrMsg({ ...loginErrMsg, verifyBlank: "验证码不能为空" }); + } else { + setLoginErrMsg({ ...loginErrMsg, verifyBlank: "" }); + } + } + + // 刷新验证码 + const timerRef = useRef | null>(null); + const refreshCaptcha = () => { + // 1. 先清除上一个还没执行的定时器(真正的防抖核心) + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + // 2. 开启新定时器,并存入 ref + timerRef.current = setTimeout(async () => { + try { + const res = await getCaptcha(captchaId); + if (res.code === 0) { + setCaptcha(res.data); + } + } catch (err) { + if (axios.isAxiosError(err) && err.response && err.response.data) { + const errorMsg = err.response.data.msg || "请求过于频繁"; + toast.warning(errorMsg, { position: "top-center" }); + } else { + toast.error("网络连接异常"); + } + } finally { + timerRef.current = null; // 执行完清空引用 + } + }, 500); + }; + + return ( + <> +
+
+ 用户登录 +
+
+
+
+
+ usernameChange(e.target.value)} + id="username" + placeholder="请输入用户名" + className="mb-1 flex-9" + /> + +
+ {loginErrMsg.usernameBlank ? ( +
+ {loginErrMsg.usernameBlank} +
+ ) : ( + "" + )} +
+
+
+
+
+ passwdChange(e.target.value)} + id="passwd" + placeholder="请输入密码" + className="mb-1 flex-9" + type="password" + /> + +
+ {loginErrMsg.passwdBlank ? ( +
+ {loginErrMsg.passwdBlank} +
+ ) : ( + "" + )} +
+
+
+
+
+ refreshCaptcha()} + className="mt-2 w-16 h-6 absolute left-48 hover:cursor-pointer" + src={captcha == null ? "#" : captcha} + alt="点击刷新" + /> + verifyChange(e.target.value)} + id="verifyCode" + placeholder="请输入验证码" + className="mb-1 flex-9" + /> + +
+ {loginErrMsg.verifyBlank ? ( +
+ {loginErrMsg.verifyBlank} +
+ ) : ( + "" + )} +
+
+ agreeChenge(check)} + /> + + 同意 + + 《用户及隐私协议》 + + +
+
+
+ +
+
+
+
+ + ); +} diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index b444ec7..301268c 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -5,7 +5,10 @@ import { useState, useEffect, useRef } from "react"; import { existUsername, getCaptcha, register } from "@/api/user"; import useUserStore from "@/store/useUserStore"; import type { UserRegisterT } from "@/types/user"; -import { toast } from "sonner" +import { toast } from "sonner"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; + interface UserRegisterValidT { username: boolean; passwd: boolean; @@ -56,6 +59,7 @@ const sepasswdError: SepasswdErrorT = { }; function Register() { + const navigate = useNavigate(); const [userRegister, setUserRegister] = useState({}); const [usernameErrorMsg, setUsernameErrorMsg] = useState(""); const [passwdErrorMsg, setPasswdErrorMsg] = useState(""); @@ -64,6 +68,7 @@ function Register() { const [inviteCodeErrorMsg, setInviteCodeErrorMsg] = useState(""); const [captcha, setCaptcha] = useState(null); const captchaId = useUserStore((state) => state.key); + const setToken = useUserStore((state) => state.setToken); const [userRegisterValid, setUserRegisterValid] = useState({ username: false, @@ -76,21 +81,28 @@ function Register() { // 注册事件 const registerEvent = async () => { let valid = true; + let falCont = 0 Object.entries(userRegisterValid).forEach(([, value]) => { if (!value) { valid = false; + falCont = falCont+1; } }); - if(!userRegisterValid.agree){ - toast.warning('请同意用户及隐私协议',{ position: "top-center" }) + if (!userRegisterValid.agree) { + toast.warning("请同意用户及隐私协议", { position: "top-center" }); + } + if(falCont){ + toast.warning('请完善注册信息',{position: 'top-center'}) } if (valid) { - userRegister.key = captchaId - const res = await register(userRegister) - if(res.code!=0){ - toast.error('注册成功',{ position: "top-center" }) - }else{ - toast.info(res.msg,{ position: "top-center" }) + userRegister.key = captchaId; + const res = await register(userRegister); + if (res.code == 0) { + toast.error("注册成功", { position: "top-center" }); + setToken(res.data) + navigate("/"); + } else { + toast.info(res.msg, { position: "top-center" }); } } }; @@ -111,6 +123,13 @@ function Register() { if (res.code === 0) { setCaptcha(res.data); } + } catch (err) { + if (axios.isAxiosError(err) && err.response && err.response.data) { + const errorMsg = err.response.data.msg || "请求过于频繁"; + toast.warning(errorMsg, { position: "top-center" }); + } else { + toast.error("网络连接异常"); + } } finally { timerRef.current = null; // 执行完清空引用 } @@ -126,6 +145,7 @@ function Register() { } }; fetchCaptcha(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function inviteCodeChangeEvent(inviteCode: string) { inviteCodeValidator(inviteCode); @@ -139,7 +159,7 @@ function Register() { } else { setInviteCodeErrorMsg(""); setUserRegisterValid({ ...userRegisterValid, inviteCode: true }); - setUserRegister({...userRegister, inviteCode}) + setUserRegister({ ...userRegister, inviteCode }); } } @@ -157,7 +177,7 @@ function Register() { } else { setEmailErrorMsg(""); setUserRegisterValid({ ...userRegisterValid, email: true }); - setUserRegister({...userRegister, email}) + setUserRegister({ ...userRegister, email }); } } // 再次输入密码变更事件 @@ -225,13 +245,13 @@ function Register() { } finally { agreeTimerRef.current = null; } - },1000); + }, 1000); }; // 验证码变更事件 - const verificationCodeEvent = (code: string)=>{ - setUserRegister({...userRegister, verificationCode:code}) - } + const verificationCodeEvent = (code: string) => { + setUserRegister({ ...userRegister, verificationCode: code }); + }; return (
@@ -298,7 +318,7 @@ function Register() { htmlFor="passwd1" className="w-20 font-light text-sm flex-1" > - 再次输入密码: + 确认密码:
{sepasswdErrorMsg ? ( @@ -312,13 +332,13 @@ function Register() {
emailChangeEvent(e.target.value)} />