刷新令牌
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
114
src/components/ui/table.tsx
Normal 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,
|
||||
}
|
||||
@@ -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
53
src/pages/Article.tsx
Normal 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
268
src/pages/Login.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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/>
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
export default useUserStore;
|
||||
|
||||
@@ -5,4 +5,17 @@ export interface UserRegisterT {
|
||||
inviteCode?: string;
|
||||
verificationCode?: string;
|
||||
key?:string
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
refreshToken: string;
|
||||
tokenType: string;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface Login {
|
||||
username?: string;
|
||||
password?: string;
|
||||
verificationCode?: string;
|
||||
key?:string
|
||||
}
|
||||
Reference in New Issue
Block a user