实现权限级菜单、localStorageSWR缓存

This commit is contained in:
2025-06-19 09:03:52 +08:00
parent af0e9c6522
commit bd862e54fa
5 changed files with 115 additions and 80 deletions

View File

@@ -25,29 +25,52 @@ export default function ConsoleMenuLayout({
const router = useRouter(); const router = useRouter();
const getInitialData = () => {
if (!window || !window.localStorage) return null;
const cache = localStorage.getItem(USER_ME_CACHE_KEY);
if (!cache) return;
try {
const user = JSON.parse(cache);
if (!user || !user.userId) throw new Error();
return user;
} catch (error) {
localStorage.removeItem(USER_ME_CACHE_KEY);
}
return undefined;
}
const USER_ME_CACHE_KEY = 'user-me-cache';
const { data: user, isLoading, error } = useSWR( const { data: user, isLoading, error } = useSWR(
'/api/user/me', '/api/user/me',
() => UserApi.me(), async () => {
const data = await UserApi.me();
localStorage.setItem(USER_ME_CACHE_KEY, JSON.stringify(data));
return data;
},
{ {
onError: (error) => { onError: (error) => {
if (error.statusCode === 401) { if (error.statusCode === 401) {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem(USER_ME_CACHE_KEY);
toast.info('登录凭证已失效,请重新登录'); toast.info('登录凭证已失效,请重新登录');
router.replace('/console/login'); router.replace('/console/login');
} }
} },
fallbackData: getInitialData(),
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
} }
); );
if (!isLoading && !error && !user) { if (!isLoading && !error && !user) {
router.replace('/console/login'); router.replace('/console/login');
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem(USER_ME_CACHE_KEY);
toast.error('账户状态异常,请重新登录'); toast.error('账户状态异常,请重新登录');
} }
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar user={user} isUserLoading={isLoading} /> <AppSidebar user={user} isUserLoading={isLoading} />

View File

@@ -25,81 +25,85 @@ import {
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import Link from "next/link" import Link from "next/link"
import { User } from "@/lib/types/user" import { User } from "@/lib/types/user"
// This is sample data. import { Role } from "@/lib/types/role"
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "网站管理",
url: "/console/web",
icon: SquareTerminal,
items: [
{
title: "资源",
url: "/console/web/resource",
},
{
title: "博客",
url: "/console/web/blog",
},
],
},
{
title: "用户管理",
url: "/console/user/list",
icon: UsersRound,
},
{
title: "邮件系统",
url: "/console/mail",
icon: Mail,
items: [
{
title: "收件箱",
url: "/console/mail/inbox",
},
{
title: "已发送",
url: "/console/mail/sent",
},
{
title: "发送邮件",
url: "/console/mail/send",
},
{
title: "邮件管理",
url: "/console/mail/manage"
}
],
},
{
title: "文件存储",
url: "/console/storage",
icon: CloudUpload,
},
{
title: "虚拟云空间",
url: "/console/vspace",
icon: Inbox,
},
{
title: "虚拟主机",
url: "/console/vserver",
icon: Server,
},
{
title: "前往首页",
url: "/",
icon: Undo2,
},
],
}
export function AppSidebar({ user, isUserLoading, ...props }: React.ComponentProps<typeof Sidebar> & { user: User | undefined, isUserLoading: boolean }) { export function AppSidebar({ user, isUserLoading, ...props }: React.ComponentProps<typeof Sidebar> & { user: User | undefined, isUserLoading: boolean }) {
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "网站管理",
url: "/console/web",
icon: SquareTerminal,
isHidden: !user?.roles.includes(Role.Admin),
items: [
{
title: "资源",
url: "/console/web/resource",
},
{
title: "博客",
url: "/console/web/blog",
},
],
},
{
title: "用户管理",
url: "/console/user/list",
icon: UsersRound,
isHidden: !user?.roles.includes(Role.Admin),
},
{
title: "邮件系统",
url: "/console/mail",
icon: Mail,
items: [
{
title: "收件箱",
url: "/console/mail/inbox",
},
{
title: "已发送",
url: "/console/mail/sent",
},
{
title: "发送邮件",
url: "/console/mail/send",
},
{
title: "邮件管理",
url: "/console/mail/manage",
isHidden: !user?.roles.includes(Role.Admin),
},
],
},
{
title: "文件存储",
url: "/console/storage",
icon: CloudUpload,
},
{
title: "虚拟云空间",
url: "/console/vspace",
icon: Inbox,
},
{
title: "虚拟主机",
url: "/console/vserver",
icon: Server,
},
{
title: "前往首页",
url: "/",
icon: Undo2,
},
],
}
return ( return (
<Sidebar collapsible="icon" {...props}> <Sidebar collapsible="icon" {...props}>
<SidebarHeader> <SidebarHeader>

View File

@@ -29,9 +29,11 @@ export function NavMain({
url: string url: string
icon?: LucideIcon icon?: LucideIcon
isActive?: boolean isActive?: boolean
isHidden?: boolean
items?: { items?: {
title: string title: string
url: string url: string
isHidden?: boolean
}[] }[]
}[] }[]
}) { }) {
@@ -39,7 +41,7 @@ export function NavMain({
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel></SidebarGroupLabel> <SidebarGroupLabel></SidebarGroupLabel>
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( {items.filter(i => !i.isHidden).map((item) => (
(item.items && item.items.length > 0) (item.items && item.items.length > 0)
? ( ? (
<Collapsible <Collapsible
@@ -58,7 +60,7 @@ export function NavMain({
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<SidebarMenuSub> <SidebarMenuSub>
{item.items?.map((subItem) => ( {item.items.filter(i => !i.isHidden).map((subItem) => (
<SidebarMenuSubItem key={subItem.title}> <SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild> <SidebarMenuSubButton asChild>
<Link href={subItem.url}> <Link href={subItem.url}>

View File

@@ -0,0 +1,3 @@
export enum Role {
Admin = 'admin',
}

View File

@@ -1,3 +1,5 @@
import { Role } from "./role";
export interface User { export interface User {
userId: string; userId: string;
username: string; username: string;
@@ -8,4 +10,5 @@ export interface User {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
roles: Role[];
} }