刷新令牌

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() { function Layout() {
return ( return (
<> <>
<div className={"bg-gray-100 w-full"}> <div className={"bg-gray-100 w-full min-h-screen"}>
<Header /> <Header />
<Outlet /> <Outlet />
<Toaster /> <Toaster />

View File

@@ -1,18 +1,36 @@
import http from '@/lib/http' import http from "@/lib/http";
import type { Result } from '@/types/common' import type { Result } from "@/types/common";
import type { UserRegisterT } from '@/types/user' import type { Token, UserRegisterT, Login } from "@/types/user";
import CryptoJS from 'crypto-js'; import CryptoJS from "crypto-js";
const prefix = '/user' const prefix = "/user";
export const getCaptcha = (reqId:string): Promise<Result<string>>=>{ // 获取验证码
return http.get(prefix+'/captcha/'+reqId) 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() export const register = (user: UserRegisterT): Promise<Result<Token>> => {
return http.post(prefix+'/register',user) 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>
<div className="mt-1.5"></div> <div className="mt-1.5"></div>
<div className="flex gap-2"> <div className="flex gap-2">
<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" src={img} alt="" /> <img className="size-50 object-cover rounded-md" 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" src={img} alt="" /> <img className="size-50 object-cover rounded-md" src={img} alt="" />
<img className="size-50 object-cover" src={img} alt="" /> <img className="size-50 object-cover rounded-md" src={img} alt="" />
</div> </div>
<div className="mt-2"></div> <div className="mt-2"></div>
</div> </div>

View File

@@ -1,13 +1,13 @@
import avatar from "@/assets/image.png"; import avatar from "@/assets/image.png";
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import useUserStore from "@/store/useUserStore";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
@@ -21,7 +21,7 @@ import {
LogOutIcon, LogOutIcon,
} from "lucide-react" } from "lucide-react"
function Header() { function Header() {
const [loginFlag, useLoginFlag] = useState(false); const isLogin = useUserStore((state) => state.isLogin);
return ( return (
<> <>
<div className="bg-white w-full h-15 flex pr-10 shadow-md sticky top-0 z-50"> <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> <h1 className="text-2xl font-bold ml-5 mt-3">X-Blog</h1>
</div> </div>
<div className="flex-1 flex flex-row-reverse items-center"> <div className="flex-1 flex flex-row-reverse items-center">
<Unlogin></Unlogin> {isLogin?<Logined></Logined>:<Unlogin></Unlogin>}
<Logined></Logined>
<ul className="flex space-x-8 ml-10"> <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"><Link to={'/'}></Link></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> <li className="hover:text-gray-600 cursor-pointer"></li>
<li className="hover:text-gray-600 cursor-pointer"></li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -54,6 +52,11 @@ function Unlogin() {
} }
function Logined() { function Logined() {
const clearToken = useUserStore((state) => state.clearToken);
function logoutEvent(){
clearToken()
}
return ( return (
<> <>
<div className="flex justify-between"> <div className="flex justify-between">
@@ -81,7 +84,7 @@ function Logined() {
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem><IterationCwIcon/></DropdownMenuItem> <DropdownMenuItem><IterationCwIcon/></DropdownMenuItem>
<DropdownMenuItem variant="destructive"><LogOutIcon/>退</DropdownMenuItem> <DropdownMenuItem onClick={()=>logoutEvent()} variant="destructive"><LogOutIcon/>退</DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </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({ const service = axios.create({
baseURL: '/api', // 指向你的 Java SpringBoot 后端地址 baseURL: "/api", // 指向你的 Java SpringBoot 后端地址
timeout: 10000, timeout: 60000,
}); });
// 响应拦截器 service.interceptors.request.use(
request.interceptors.response.use( (config) => {
(response) => response.data, const { token } = useUserStore.getState();
(error) => { if (token) {
console.error('网络请求出错:', error); config.headers.Authorization = `${token.tokenType} ${token.accessToken}`;
return Promise.reject(error);
} }
return config;
},
(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 { existUsername, getCaptcha, register } from "@/api/user";
import useUserStore from "@/store/useUserStore"; import useUserStore from "@/store/useUserStore";
import type { UserRegisterT } from "@/types/user"; 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 { interface UserRegisterValidT {
username: boolean; username: boolean;
passwd: boolean; passwd: boolean;
@@ -56,6 +59,7 @@ const sepasswdError: SepasswdErrorT = {
}; };
function Register() { function Register() {
const navigate = useNavigate();
const [userRegister, setUserRegister] = useState<UserRegisterT>({}); const [userRegister, setUserRegister] = useState<UserRegisterT>({});
const [usernameErrorMsg, setUsernameErrorMsg] = useState<string>(""); const [usernameErrorMsg, setUsernameErrorMsg] = useState<string>("");
const [passwdErrorMsg, setPasswdErrorMsg] = useState<string>(""); const [passwdErrorMsg, setPasswdErrorMsg] = useState<string>("");
@@ -64,6 +68,7 @@ function Register() {
const [inviteCodeErrorMsg, setInviteCodeErrorMsg] = useState<string>(""); const [inviteCodeErrorMsg, setInviteCodeErrorMsg] = useState<string>("");
const [captcha, setCaptcha] = useState<string | null>(null); const [captcha, setCaptcha] = useState<string | null>(null);
const captchaId = useUserStore((state) => state.key); const captchaId = useUserStore((state) => state.key);
const setToken = useUserStore((state) => state.setToken);
const [userRegisterValid, setUserRegisterValid] = const [userRegisterValid, setUserRegisterValid] =
useState<UserRegisterValidT>({ useState<UserRegisterValidT>({
username: false, username: false,
@@ -76,21 +81,28 @@ function Register() {
// 注册事件 // 注册事件
const registerEvent = async () => { const registerEvent = async () => {
let valid = true; let valid = true;
let falCont = 0
Object.entries(userRegisterValid).forEach(([, value]) => { Object.entries(userRegisterValid).forEach(([, value]) => {
if (!value) { if (!value) {
valid = false; valid = false;
falCont = falCont+1;
} }
}); });
if(!userRegisterValid.agree){ if (!userRegisterValid.agree) {
toast.warning('请同意用户及隐私协议',{ position: "top-center" }) toast.warning("请同意用户及隐私协议", { position: "top-center" });
}
if(falCont){
toast.warning('请完善注册信息',{position: 'top-center'})
} }
if (valid) { if (valid) {
userRegister.key = captchaId userRegister.key = captchaId;
const res = await register(userRegister) const res = await register(userRegister);
if(res.code!=0){ if (res.code == 0) {
toast.error('注册成功',{ position: "top-center" }) toast.error("注册成功", { position: "top-center" });
}else{ setToken(res.data)
toast.info(res.msg,{ position: "top-center" }) navigate("/");
} else {
toast.info(res.msg, { position: "top-center" });
} }
} }
}; };
@@ -111,6 +123,13 @@ function Register() {
if (res.code === 0) { if (res.code === 0) {
setCaptcha(res.data); 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 { } finally {
timerRef.current = null; // 执行完清空引用 timerRef.current = null; // 执行完清空引用
} }
@@ -126,6 +145,7 @@ function Register() {
} }
}; };
fetchCaptcha(); fetchCaptcha();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
function inviteCodeChangeEvent(inviteCode: string) { function inviteCodeChangeEvent(inviteCode: string) {
inviteCodeValidator(inviteCode); inviteCodeValidator(inviteCode);
@@ -139,7 +159,7 @@ function Register() {
} else { } else {
setInviteCodeErrorMsg(""); setInviteCodeErrorMsg("");
setUserRegisterValid({ ...userRegisterValid, inviteCode: true }); setUserRegisterValid({ ...userRegisterValid, inviteCode: true });
setUserRegister({...userRegister, inviteCode}) setUserRegister({ ...userRegister, inviteCode });
} }
} }
@@ -157,7 +177,7 @@ function Register() {
} else { } else {
setEmailErrorMsg(""); setEmailErrorMsg("");
setUserRegisterValid({ ...userRegisterValid, email: true }); setUserRegisterValid({ ...userRegisterValid, email: true });
setUserRegister({...userRegister, email}) setUserRegister({ ...userRegister, email });
} }
} }
// 再次输入密码变更事件 // 再次输入密码变更事件
@@ -225,13 +245,13 @@ function Register() {
} finally { } finally {
agreeTimerRef.current = null; agreeTimerRef.current = null;
} }
},1000); }, 1000);
}; };
// 验证码变更事件 // 验证码变更事件
const verificationCodeEvent = (code: string)=>{ const verificationCodeEvent = (code: string) => {
setUserRegister({...userRegister, verificationCode:code}) setUserRegister({ ...userRegister, verificationCode: code });
} };
return ( return (
<div className="pb-20"> <div className="pb-20">
@@ -298,7 +318,7 @@ function Register() {
htmlFor="passwd1" htmlFor="passwd1"
className="w-20 font-light text-sm flex-1" className="w-20 font-light text-sm flex-1"
> >
: :
</label> </label>
</div> </div>
{sepasswdErrorMsg ? ( {sepasswdErrorMsg ? (
@@ -312,13 +332,13 @@ function Register() {
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<Input <Input
id="passwd1" id="email"
placeholder="请输入邮箱" placeholder="请输入邮箱"
className="mb-1 flex-3" className="mb-1 flex-3"
onChange={(e) => emailChangeEvent(e.target.value)} onChange={(e) => emailChangeEvent(e.target.value)}
/> />
<label <label
htmlFor="passwd1" htmlFor="email"
className=" mt-1.5 w-20 font-light text-sm flex-1" className=" mt-1.5 w-20 font-light text-sm flex-1"
> >
: :
@@ -333,13 +353,13 @@ function Register() {
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<Input <Input
id="passwd1" id="inviteCode"
placeholder="请输入邀请码" placeholder="请输入邀请码"
className="mb-1 flex-3" className="mb-1 flex-3"
onChange={(e) => inviteCodeChangeEvent(e.target.value)} onChange={(e) => inviteCodeChangeEvent(e.target.value)}
/> />
<label <label
htmlFor="passwd1" htmlFor="inviteCode"
className=" mt-1.5 w-20 font-light text-sm flex-1" className=" mt-1.5 w-20 font-light text-sm flex-1"
> >
: :
@@ -362,13 +382,13 @@ function Register() {
alt="点击刷新" alt="点击刷新"
/> />
<Input <Input
onChange={(e)=>verificationCodeEvent(e.target.value)} onChange={(e) => verificationCodeEvent(e.target.value)}
id="passwd1" id="verifyCode"
placeholder="请输入验证码" placeholder="请输入验证码"
className="mb-1 flex-3" className="mb-1 flex-3"
/> />
<label <label
htmlFor="passwd1" htmlFor="verifyCode"
className=" mt-1.5 w-20 font-light text-sm flex-1" className=" mt-1.5 w-20 font-light text-sm flex-1"
> >
: :
@@ -382,8 +402,8 @@ function Register() {
setUserRegisterValid({ ...userRegisterValid, agree: check }) 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> </span>
</div> </div>
<div className="mt-2"></div> <div className="mt-2"></div>
@@ -391,7 +411,7 @@ function Register() {
<div> <div>
<Button <Button
onClick={() => registerEvent()} onClick={() => registerEvent()}
className="w-full mt-4 mb-4" className="w-full mt-4 mb-4 hover:cursor-pointer"
> >
</Button> </Button>

View File

@@ -2,6 +2,8 @@ import { createBrowserRouter,Navigate } from "react-router-dom";
import Layout from "@/Layout.tsx"; import Layout from "@/Layout.tsx";
import Main from "./pages/Main"; import Main from "./pages/Main";
import Register from "./pages/Register"; import Register from "./pages/Register";
import Login from "./pages/Login";
import Article from "./pages/Article";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: "/",
@@ -19,6 +21,14 @@ const router = createBrowserRouter([
path:'/user/register', path:'/user/register',
element: <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 type { Token } from "@/types/user";
import { create } from 'zustand'; import { uuid } from "./../lib/keyGen";
import { create } from "zustand";
import { persist, createJSONStorage } from 'zustand/middleware';
interface UserState { interface UserState {
key: string key: string
} isLogin:boolean
token?: Token
setToken: (token:Token)=>void
clearToken:()=>void
}
const useUserStore = create<UserState>(() => ({ const useUserStore = create<UserState>()(
key: uuid().replaceAll('-','') 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; export default useUserStore;

View File

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