refactor: 前端重构控制台用户状态管理

This commit is contained in:
2025-12-17 15:38:06 +08:00
parent 86086a7054
commit 0f0b5f227d
6 changed files with 142 additions and 221 deletions

View File

@@ -12,37 +12,23 @@ import {
SidebarProvider, SidebarProvider,
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { useUserMe } from "@/hooks/user/use-user-me"; import { useUserStore } from "@/store/useUserStore";
import { UserApi } from "@/lib/api"; // import { useUserMe } from "@/hooks/user/use-user-me";
import { useRouter } from "next/navigation"; // import { useRouter } from "next/navigation";
import { toast } from "sonner"; // import { toast } from "sonner";
export default function ConsoleMenuLayout({ export default function ConsoleMenuLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const router = useRouter(); // const router = useRouter();
const { user, isLoading, error } = useUserMe({ const user = useUserStore().user;
onError: (e) => {
if (e.statusCode === 401) {
toast.info('登录凭证已失效,请重新登录');
router.replace('/console/login');
}
}
});
if (!isLoading && !error && !user) {
router.replace('/console/login');
localStorage.removeItem('token');
localStorage.removeItem(UserApi.USER_ME_CACHE_KEY);
toast.error('账户状态异常,请重新登录');
}
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar user={user} isUserLoading={isLoading} /> <AppSidebar user={user} />
<SidebarInset> <SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4"> <div className="flex items-center gap-2 px-4">

View File

@@ -1,16 +0,0 @@
export type SubmitMode = 'password' | 'phone' | 'email';
export type LoginFormData = {
type: SubmitMode;
account?: string;
password?: string;
phone?: string;
email?: string;
code?: string;
}
export type SendCodeMode = 'phone' | 'email';
export type SendCodeFormData = {
type: SendCodeMode;
phone?: string;
email?: string;
}

View File

@@ -1,5 +1,4 @@
'use client'; 'use client';
// import { authApi, verificationApi } from "@/lib/api";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import Header from "@/components/Header"; import Header from "@/components/Header";
@@ -11,58 +10,18 @@ import { KeyRound, Phone, FileKey2 } from "lucide-react";
import EmailLoginMode from "./components/EmailLoginMode"; import EmailLoginMode from "./components/EmailLoginMode";
import PasswordLoginMode from "./components/PasswordLoginMode"; import PasswordLoginMode from "./components/PasswordLoginMode";
import PhoneLoginMode from "./components/PhoneLoginMode"; import PhoneLoginMode from "./components/PhoneLoginMode";
import { LoginFormData, SendCodeFormData, SubmitMode } from "./components/types"; import { useState } from "react";
import { useCallback, useState } from "react";
import LoginBG from './components/login-bg.jpg'; import LoginBG from './components/login-bg.jpg';
import Image from "next/image"; import Image from "next/image";
import { handleAPIError } from "@/lib/api/common"; import { handleAPIError } from "@/lib/api/common";
// import { ApiError } from "@/lib/api/fetcher"; import { useUserStore } from "@/store/useUserStore";
export type SubmitMode = 'password' | 'phone' | 'passkey';
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
const [loginMode, setLoginMode] = useState<SubmitMode>('password'); const [loginMode, setLoginMode] = useState<SubmitMode>('password');
// const handleSendCode = async (data: SendCodeFormData) => {
// try {
// const res = await verificationApi.send({
// type: 'login',
// targetType: data.type,
// phone: data.phone,
// email: data.email,
// })
// if (res) {
// toast.success('验证码已发送,请注意查收');
// return true;
// } else {
// throw new Error();
// }
// } catch (error) {
// toast.error((error as ApiError).message || '验证码发送失败,请稍后再试');
// return false;
// }
// }
// const handleSubmit = async (data: LoginFormData) => {
// try {
// const res = await authApi.login({
// ...data,
// });
// if (res.token) {
// toast.success('登录成功');
// localStorage.setItem('token', res.token);
// router.replace('/console');
// return true;
// } else {
// throw new Error();
// }
// } catch (error) {
// toast.error((error as ApiError).message || '登录失败,请稍后再试');
// return false;
// }
// }
return ( return (
<> <>
<Header /> <Header />
@@ -84,9 +43,10 @@ export default function Login() {
return toast.error('登陆状态异常'); return toast.error('登陆状态异常');
} }
handler(formData).then((user) => { handler(formData).then((data) => {
localStorage.setItem('user_profile', JSON.stringify(user)); useUserStore.getState().setUser(data.user);
// to main page // to main page
router.replace('/console');
}, (e) => { }, (e) => {
handleAPIError(e, ({ message }) => toast.error(message)) handleAPIError(e, ({ message }) => toast.error(message))
}) })
@@ -103,15 +63,13 @@ export default function Login() {
</span> </span>
</div> </div>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<Button variant={loginMode === 'password' ? 'default' : 'outline'} type="button" className="w-full" onClick={() => setLoginMode('password')}> {
<KeyRound /> ([['password', KeyRound], ['phone', Phone], ['passkey', FileKey2]] as const).map(([mode, Icon]) => (
</Button> <Button key={mode} variant={loginMode === mode ? 'default' : 'outline'} type="button" className="w-full" onClick={() => setLoginMode(mode)}>
<Button variant={loginMode === 'phone' ? 'default' : 'outline'} type="button" className="w-full" onClick={() => setLoginMode('phone')}> <Icon />
<Phone /> </Button>
</Button> ))
<Button variant={loginMode === 'email' ? 'default' : 'outline'} type="button" className="w-full" onClick={() => setLoginMode('email')}> }
<FileKey2 />
</Button>
</div> </div>
<div className="text-center text-sm"> <div className="text-center text-sm">

View File

@@ -28,7 +28,7 @@ import Link from "next/link"
import { User } from "@/lib/types/user" import { User } from "@/lib/types/user"
import { Role } from "@/lib/types/role" import { Role } from "@/lib/types/role"
export function AppSidebar({ user, isUserLoading, ...props }: React.ComponentProps<typeof Sidebar> & { user: User | undefined, isUserLoading: boolean }) { export function AppSidebar({ user, ...props }: React.ComponentProps<typeof Sidebar> & { user: User | null }) {
const data = { const data = {
user: { user: {
name: "shadcn", name: "shadcn",
@@ -49,76 +49,74 @@ export function AppSidebar({ user, isUserLoading, ...props }: React.ComponentPro
}[], }[],
} }
if (!isUserLoading) { data.navMain = [
data.navMain = [ {
{ title: "网站管理",
title: "网站管理", url: "/console/web",
url: "/console/web", icon: SquareTerminal,
icon: SquareTerminal, isHidden: !user?.roles.includes(Role.Admin),
isHidden: !user?.roles.includes(Role.Admin), items: [
items: [ {
{ title: "资源",
title: "资源", url: "/console/web/resource",
url: "/console/web/resource", },
}, {
{ title: "博客",
title: "博客", url: "/console/web/blog",
url: "/console/web/blog", },
}, ],
], },
}, {
{ title: "用户管理",
title: "用户管理", url: "/console/user/list",
url: "/console/user/list", icon: UsersRound,
icon: UsersRound, isHidden: !user?.roles.includes(Role.Admin),
isHidden: !user?.roles.includes(Role.Admin), },
}, {
{ title: "邮件系统",
title: "邮件系统", url: "/console/mail",
url: "/console/mail", icon: Mail,
icon: Mail, items: [
items: [ {
{ title: "收件箱",
title: "收件箱", url: "/console/mail/inbox",
url: "/console/mail/inbox", },
}, {
{ title: "已发送",
title: "已发送", url: "/console/mail/sent",
url: "/console/mail/sent", },
}, {
{ title: "发送邮件",
title: "发送邮件", url: "/console/mail/send",
url: "/console/mail/send", },
}, {
{ title: "邮件管理",
title: "邮件管理", url: "/console/mail/manage",
url: "/console/mail/manage", isHidden: !user?.roles.includes(Role.Admin),
isHidden: !user?.roles.includes(Role.Admin), },
}, ],
], },
}, {
{ title: "文件存储",
title: "文件存储", url: "/console/storage",
url: "/console/storage", icon: CloudUpload,
icon: CloudUpload, },
}, {
{ title: "虚拟云空间",
title: "虚拟云空间", url: "/console/vspace",
url: "/console/vspace", icon: Inbox,
icon: Inbox, },
}, {
{ title: "虚拟主机",
title: "虚拟主机", url: "/console/vserver",
url: "/console/vserver", icon: Server,
icon: Server, },
}, {
{ title: "前往首页",
title: "前往首页", url: "/",
url: "/", icon: Undo2,
icon: Undo2, },
}, ]
]
}
return ( return (
<Sidebar collapsible="icon" {...props}> <Sidebar collapsible="icon" {...props}>
@@ -145,7 +143,7 @@ export function AppSidebar({ user, isUserLoading, ...props }: React.ComponentPro
<NavMain items={data.navMain} /> <NavMain items={data.navMain} />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<NavUser user={user} isUserLoading={isUserLoading} /> <NavUser user={user} />
</SidebarFooter> </SidebarFooter>
<SidebarRail /> <SidebarRail />
</Sidebar> </Sidebar>

View File

@@ -26,7 +26,6 @@ import {
SidebarMenuItem, SidebarMenuItem,
useSidebar, useSidebar,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { authApi, UserApi } from "@/lib/api"
import { Skeleton } from "./ui/skeleton" import { Skeleton } from "./ui/skeleton"
import { toast } from "sonner" import { toast } from "sonner"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
@@ -35,20 +34,20 @@ import { useState } from "react"
import { User } from "@/lib/types/user" import { User } from "@/lib/types/user"
import UserProfile from "./nav-user/UserProfile" import UserProfile from "./nav-user/UserProfile"
export function NavUser({ user, isUserLoading }: { user: User | undefined, isUserLoading: boolean }) { export function NavUser({ user }: { user: User | null }) {
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const router = useRouter(); const router = useRouter();
async function logout() { async function logout() {
try { // try {
await authApi.logout(); // await authApi.logout();
localStorage.removeItem('token'); // localStorage.removeItem('token');
localStorage.removeItem(UserApi.USER_ME_CACHE_KEY) // localStorage.removeItem(UserApi.USER_ME_CACHE_KEY)
toast.success('登出成功'); // toast.success('登出成功');
router.replace('/console/login'); // router.replace('/console/login');
} catch { // } catch {
toast.error('登出失败,请稍后再试'); // toast.error('登出失败,请稍后再试');
} // }
} }
const [userProfileOpen, setUserProfileOpen] = useState(false); const [userProfileOpen, setUserProfileOpen] = useState(false);
@@ -65,25 +64,24 @@ export function NavUser({ user, isUserLoading }: { user: User | undefined, isUse
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
> >
{ {
user && <> user ?
<Avatar className="h-8 w-8 rounded-lg"> <>
<AvatarImage src={user.avatar} /> <Avatar className="h-8 w-8 rounded-lg">
<AvatarFallback className="rounded-lg">U</AvatarFallback> <AvatarImage src={user.avatar} />
</Avatar> <AvatarFallback className="rounded-lg">U</AvatarFallback>
<div className="grid flex-1 text-left text-sm leading-tight"> </Avatar>
<span className="truncate font-medium">{user.nickname}</span> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate text-xs">{user.username}</span> <span className="truncate font-medium">{user.nickname}</span>
<span className="truncate text-xs">{user.username}</span>
</div>
</> :
<div className="w-full flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="flex-1 flex flex-col gap-1">
<Skeleton className="w-full h-4" />
<Skeleton className="w-full h-4" />
</div>
</div> </div>
</>
}
{
isUserLoading && <div className="w-full flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="flex-1 flex flex-col gap-1">
<Skeleton className="w-full h-4" />
<Skeleton className="w-full h-4" />
</div>
</div>
} }
<ChevronsUpDown className="ml-auto size-4" /> <ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton> </SidebarMenuButton>
@@ -96,26 +94,24 @@ export function NavUser({ user, isUserLoading }: { user: User | undefined, isUse
> >
<DropdownMenuLabel className="p-0 font-normal"> <DropdownMenuLabel className="p-0 font-normal">
{ {
user && user ?
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg"> <Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} /> <AvatarImage src={user.avatar} />
<AvatarFallback className="rounded-lg">U</AvatarFallback> <AvatarFallback className="rounded-lg">U</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.nickname}</span> <span className="truncate font-medium">{user.nickname}</span>
<span className="truncate text-xs">{user.username}</span> <span className="truncate text-xs">{user.username}</span>
</div>
</div> :
<div className="flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="flex-1 flex flex-col gap-1">
<Skeleton className="w-full h-4" />
<Skeleton className="w-full h-4" />
</div>
</div> </div>
</div>
}
{
isUserLoading && <div className="flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="flex-1 flex flex-col gap-1">
<Skeleton className="w-full h-4" />
<Skeleton className="w-full h-4" />
</div>
</div>
} }
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />

View File

@@ -14,7 +14,6 @@ import { Label } from "@/components/ui/label"
import { FC } from "react"; import { FC } from "react";
import { DialogProps } from "@radix-ui/react-dialog"; import { DialogProps } from "@radix-ui/react-dialog";
import { toast } from "sonner"; import { toast } from "sonner";
import { UserApi } from "@/lib/api";
import { ApiError } from "next/dist/server/api-utils"; import { ApiError } from "next/dist/server/api-utils";
export default function SetPassword({ onOpenChange, ...props }: React.ComponentProps<FC<DialogProps>>) { export default function SetPassword({ onOpenChange, ...props }: React.ComponentProps<FC<DialogProps>>) {
@@ -24,13 +23,13 @@ export default function SetPassword({ onOpenChange, ...props }: React.ComponentP
return; return;
} }
try { // try {
await UserApi.updatePassword(password); // await UserApi.updatePassword(password);
toast.success('新密码设置成功'); // toast.success('新密码设置成功');
onOpenChange?.(false); // onOpenChange?.(false);
} catch (error) { // } catch (error) {
toast.error((error as ApiError).message || '新密码设置失败'); // toast.error((error as ApiError).message || '新密码设置失败');
} // }
} }
return ( return (