刷新令牌

This commit is contained in:
2026-02-04 16:47:44 +08:00
parent 544de32d80
commit c637d6eaec
12 changed files with 674 additions and 75 deletions

View File

@@ -4,7 +4,7 @@ import { Toaster } from "@/components/ui/sonner"
function Layout() {
return (
<>
<div className={"bg-gray-100 w-full"}>
<div className={"bg-gray-100 w-full min-h-screen"}>
<Header />
<Outlet />
<Toaster />

View File

@@ -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<Result<string>>=>{
return http.get(prefix+'/captcha/'+reqId)
}
const prefix = "/user";
// 获取验证码
export const getCaptcha = (reqId: string): Promise<Result<string>> => {
return http.get(prefix + "/captcha/" + reqId);
};
export const register = (user: UserRegisterT):Promise<Result<string>>=>{
user.password = CryptoJS.MD5(user.password as string).toString()
return http.post(prefix+'/register',user)
}
// 注册
export const register = (user: UserRegisterT): Promise<Result<Token>> => {
user.password = CryptoJS.MD5(user.password as string).toString();
return http.post(prefix + "/register", user);
};
export const existUsername = (username:string):Promise<Result<boolean>>=>{
return http.get(prefix+'/exist/'+username)
}
// 登录
export const login = (user: Login): Promise<Result<Token>> => {
user.password = CryptoJS.MD5(user.password as string).toString();
return http.post(prefix + "/login", user);
};
// 用户是否存在
export const existUsername = (username: string): Promise<Result<boolean>> => {
return http.get(prefix + "/exist/" + username);
};
// 刷新令牌
export const refreshToken = (token: string): Promise<Result<Token>> => {
return http.post(prefix + "/refreshToken", token, {
headers: {
"Content-Type": "text/plain",
},
});
};

View File

@@ -81,11 +81,11 @@ function Img() {
</div>
<div className="mt-1.5"></div>
<div className="flex gap-2">
<img className="size-50 object-cover" src={img} alt="" />
<img className="size-50 object-cover" src={img} alt="" />
<img className="size-50 object-cover" src={img} alt="" />
<img className="size-50 object-cover" src={img} alt="" />
<img className="size-50 object-cover" src={img} alt="" />
<img className="size-50 object-cover rounded-md" src={img} alt="" />
<img className="size-50 object-cover rounded-md" src={img} alt="" />
<img className="size-50 object-cover rounded-md" src={img} alt="" />
<img className="size-50 object-cover rounded-md" src={img} alt="" />
<img className="size-50 object-cover rounded-md" src={img} alt="" />
</div>
<div className="mt-2"></div>
</div>

View File

@@ -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 (
<>
<div className="bg-white w-full h-15 flex pr-10 shadow-md sticky top-0 z-50">
@@ -29,14 +29,12 @@ function Header() {
<h1 className="text-2xl font-bold ml-5 mt-3">X-Blog</h1>
</div>
<div className="flex-1 flex flex-row-reverse items-center">
<Unlogin></Unlogin>
<Logined></Logined>
{isLogin?<Logined></Logined>:<Unlogin></Unlogin>}
<ul className="flex space-x-8 ml-10">
<li className="hover:text-gray-600 cursor-pointer"><Link to={'/'}></Link></li>
<li className="hover:text-gray-600 cursor-pointer"></li>
<li className="hover:text-gray-600 cursor-pointer"></li>
<li className="hover:text-gray-600 cursor-pointer"><Link to={'/user/article'}></Link></li>
<li className="hover:text-gray-600 cursor-pointer"></li>
<li className="hover:text-gray-600 cursor-pointer"></li>
</ul>
</div>
</div>
@@ -54,6 +52,11 @@ function Unlogin() {
}
function Logined() {
const clearToken = useUserStore((state) => state.clearToken);
function logoutEvent(){
clearToken()
}
return (
<>
<div className="flex justify-between">
@@ -81,7 +84,7 @@ function Logined() {
<DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem><IterationCwIcon/></DropdownMenuItem>
<DropdownMenuItem variant="destructive"><LogOutIcon/>退</DropdownMenuItem>
<DropdownMenuItem onClick={()=>logoutEvent()} variant="destructive"><LogOutIcon/>退</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

114
src/components/ui/table.tsx Normal file
View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -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;
// 记录是否正在刷新中
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;

53
src/pages/Article.tsx Normal file
View File

@@ -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 (
<>
<div className="w-200 m-auto mt-4 mb-0 flex flex-row-reverse">
<Button className="bg-blue-900 hover:cursor-pointer"></Button>
</div>
<div className="w-200 m-auto mt-1 mb-0 p-2 bg-white rounded-md shadow-md min-h-40">
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell>$250.00</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell>$250.00</TableCell>
<TableCell>
<Button className="size-5 text-xs hover:cursor-pointer bg-blue-800"></Button>
<Button className="ml-1 size-5 text-xs hover:cursor-pointer bg-yellow-800"></Button>
<Button className="ml-1 size-5 text-xs hover:cursor-pointer bg-red-800"></Button>
<Button className="ml-1 size-5 text-xs hover:cursor-pointer bg-green-800"></Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</>
);
}

268
src/pages/Login.tsx Normal file
View File

@@ -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<LoginErrMsg>({
usernameBlank: "",
passwdBlank: "",
verifyBlank: "",
agreeBlank: "",
});
const navigate = useNavigate();
const [captcha, setCaptcha] = useState<string | null>(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<ReturnType<typeof setTimeout> | 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 (
<>
<div className="pb-20">
<div className="italic mt-20 ml-auto mr-auto w-60 h-20 text-center text-4xl">
<span></span>
</div>
<div className="bg-white w-80 ml-auto mr-auto rounded-lg shadow-md pt-10">
<div className="flex flex-col w-3/5 mr-auto ml-auto">
<div className="mb-1">
<div className="flex flex-row-reverse">
<Input
onChange={(e) => usernameChange(e.target.value)}
id="username"
placeholder="请输入用户名"
className="mb-1 flex-9"
/>
<label
htmlFor="username"
className="mt-1.5 font-light text-sm flex-4"
>
:
</label>
</div>
{loginErrMsg.usernameBlank ? (
<div className="text-red-400 text-xs pl-14">
{loginErrMsg.usernameBlank}
</div>
) : (
""
)}
</div>
</div>
<div className="flex flex-col w-3/5 mr-auto ml-auto">
<div className="mb-1">
<div className="flex flex-row-reverse">
<Input
onChange={(e) => passwdChange(e.target.value)}
id="passwd"
placeholder="请输入密码"
className="mb-1 flex-9"
type="password"
/>
<label
htmlFor="passwd"
className="mt-1.5 font-light text-sm flex-4"
>
:
</label>
</div>
{loginErrMsg.passwdBlank ? (
<div className="text-red-400 text-xs pl-14">
{loginErrMsg.passwdBlank}
</div>
) : (
""
)}
</div>
</div>
<div className="flex flex-col w-3/5 mr-auto ml-auto">
<div className="mb-1">
<div className="flex flex-row-reverse relative">
<img
onClick={() => refreshCaptcha()}
className="mt-2 w-16 h-6 absolute left-48 hover:cursor-pointer"
src={captcha == null ? "#" : captcha}
alt="点击刷新"
/>
<Input
onChange={(e) => verifyChange(e.target.value)}
id="verifyCode"
placeholder="请输入验证码"
className="mb-1 flex-9"
/>
<label
htmlFor="verifyCode"
className="mt-1.5 font-light text-sm flex-4"
>
:
</label>
</div>
{loginErrMsg.verifyBlank ? (
<div className="text-red-400 text-xs pl-14">
{loginErrMsg.verifyBlank}
</div>
) : (
""
)}
</div>
<div className="flex align-middle mb-3">
<Checkbox
onCheckedChange={(check: boolean) => agreeChenge(check)}
/>
<span className="ml-1 text-xs">
<span className="hover:text-gray-500 hover:underline hover:cursor-pointer">
</span>
</span>
</div>
<hr />
<div>
<Button
onClick={() => loginClick()}
className="w-full mt-4 mb-4 hover:cursor-pointer"
>
</Button>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -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<UserRegisterT>({});
const [usernameErrorMsg, setUsernameErrorMsg] = useState<string>("");
const [passwdErrorMsg, setPasswdErrorMsg] = useState<string>("");
@@ -64,6 +68,7 @@ function Register() {
const [inviteCodeErrorMsg, setInviteCodeErrorMsg] = useState<string>("");
const [captcha, setCaptcha] = useState<string | null>(null);
const captchaId = useUserStore((state) => state.key);
const setToken = useUserStore((state) => state.setToken);
const [userRegisterValid, setUserRegisterValid] =
useState<UserRegisterValidT>({
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 (
<div className="pb-20">
@@ -298,7 +318,7 @@ function Register() {
htmlFor="passwd1"
className="w-20 font-light text-sm flex-1"
>
:
:
</label>
</div>
{sepasswdErrorMsg ? (
@@ -312,13 +332,13 @@ function Register() {
<div className="mb-1">
<div className="flex flex-row-reverse">
<Input
id="passwd1"
id="email"
placeholder="请输入邮箱"
className="mb-1 flex-3"
onChange={(e) => emailChangeEvent(e.target.value)}
/>
<label
htmlFor="passwd1"
htmlFor="email"
className=" mt-1.5 w-20 font-light text-sm flex-1"
>
:
@@ -333,13 +353,13 @@ function Register() {
<div className="mb-1">
<div className="flex flex-row-reverse">
<Input
id="passwd1"
id="inviteCode"
placeholder="请输入邀请码"
className="mb-1 flex-3"
onChange={(e) => inviteCodeChangeEvent(e.target.value)}
/>
<label
htmlFor="passwd1"
htmlFor="inviteCode"
className=" mt-1.5 w-20 font-light text-sm flex-1"
>
:
@@ -362,13 +382,13 @@ function Register() {
alt="点击刷新"
/>
<Input
onChange={(e)=>verificationCodeEvent(e.target.value)}
id="passwd1"
onChange={(e) => verificationCodeEvent(e.target.value)}
id="verifyCode"
placeholder="请输入验证码"
className="mb-1 flex-3"
/>
<label
htmlFor="passwd1"
htmlFor="verifyCode"
className=" mt-1.5 w-20 font-light text-sm flex-1"
>
:
@@ -382,8 +402,8 @@ function Register() {
setUserRegisterValid({ ...userRegisterValid, agree: check })
}
/>
<span className="ml-1 text-xs hover:text-gray-500 hover:underline hover:cursor-pointer">
<span className="ml-1 text-xs">
<span className="hover:text-gray-500 hover:underline hover:cursor-pointer"></span>
</span>
</div>
<div className="mt-2"></div>
@@ -391,7 +411,7 @@ function Register() {
<div>
<Button
onClick={() => registerEvent()}
className="w-full mt-4 mb-4"
className="w-full mt-4 mb-4 hover:cursor-pointer"
>
</Button>

View File

@@ -2,6 +2,8 @@ import { createBrowserRouter,Navigate } from "react-router-dom";
import Layout from "@/Layout.tsx";
import Main from "./pages/Main";
import Register from "./pages/Register";
import Login from "./pages/Login";
import Article from "./pages/Article";
const router = createBrowserRouter([
{
path: "/",
@@ -19,6 +21,14 @@ const router = createBrowserRouter([
path:'/user/register',
element: <Register/>,
},
{
path:'/user/login',
element: <Login/>,
},
{
path:'/user/article',
element: <Article/>
}
],
},
]);

View File

@@ -1,12 +1,40 @@
import { uuid } from './../lib/keyGen';
import { create } from 'zustand';
import type { Token } from "@/types/user";
import { uuid } from "./../lib/keyGen";
import { create } from "zustand";
import { persist, createJSONStorage } from 'zustand/middleware';
interface UserState {
key: string
}
interface UserState {
key: string
isLogin:boolean
token?: Token
setToken: (token:Token)=>void
clearToken:()=>void
}
const useUserStore = create<UserState>(() => ({
key: uuid().replaceAll('-','')
}));
const useUserStore = create<UserState>()(
persist(
(set) => ({
key: uuid().replaceAll("-", ""),
token: undefined,
isLogin:false,
setToken: (token) => set((state)=>({
...state,
token,
isLogin:true,
})),
clearToken: () => set((state)=>({
...state,
token:undefined,
isLogin:false,
})),
}),
{
name: 'x-blog-auth-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ token: state.token,isLogin:state.isLogin }),
}
)
);
export default useUserStore;

View File

@@ -6,3 +6,16 @@ export interface UserRegisterT {
verificationCode?: string;
key?:string
}
export interface Token {
refreshToken: string;
tokenType: string;
accessToken: string;
}
export interface Login {
username?: string;
password?: string;
verificationCode?: string;
key?:string
}