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)}
/>
|