刷新令牌
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
@@ -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>> => {
|
export const getCaptcha = (reqId: string): Promise<Result<string>> => {
|
||||||
return http.get(prefix+'/captcha/'+reqId)
|
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 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>> => {
|
export const existUsername = (username: string): Promise<Result<boolean>> => {
|
||||||
return http.get(prefix+'/exist/'+username)
|
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>
|
||||||
<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>
|
||||||
|
|||||||
@@ -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
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({
|
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
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 { 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" });
|
||||||
|
setToken(res.data)
|
||||||
|
navigate("/");
|
||||||
} else {
|
} else {
|
||||||
toast.info(res.msg,{ position: "top-center" })
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 再次输入密码变更事件
|
// 再次输入密码变更事件
|
||||||
@@ -230,8 +250,8 @@ function Register() {
|
|||||||
|
|
||||||
// 验证码变更事件
|
// 验证码变更事件
|
||||||
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"
|
||||||
>
|
>
|
||||||
邀请码:
|
邀请码:
|
||||||
@@ -363,12 +383,12 @@ function Register() {
|
|||||||
/>
|
/>
|
||||||
<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>
|
||||||
|
|||||||
@@ -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/>
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user