refactor: 将控制台组件移动到控制台...
This commit is contained in:
151
apps/frontend/app/console/(with-menu)/components/app-sidebar.tsx
Normal file
151
apps/frontend/app/console/(with-menu)/components/app-sidebar.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
CloudUpload,
|
||||
Inbox,
|
||||
LucideIcon,
|
||||
Mail,
|
||||
Server,
|
||||
SquareTerminal,
|
||||
Undo2,
|
||||
UsersRound,
|
||||
} from "lucide-react"
|
||||
|
||||
import { NavMain } from "@/app/console/(with-menu)/components/nav-main"
|
||||
import { NavUser } from "@/app/console/(with-menu)/components/nav-user"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar"
|
||||
import Link from "next/link"
|
||||
import { User } from "@/lib/types/user"
|
||||
import { Role } from "@/lib/types/role"
|
||||
|
||||
export function AppSidebar({ user, ...props }: React.ComponentProps<typeof Sidebar> & { user: User | null }) {
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: null as null | {
|
||||
title: string
|
||||
url: string
|
||||
icon?: LucideIcon
|
||||
isActive?: boolean
|
||||
isHidden?: boolean
|
||||
items?: {
|
||||
title: string
|
||||
url: string
|
||||
isHidden?: boolean
|
||||
}[]
|
||||
}[],
|
||||
}
|
||||
|
||||
data.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 (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/console">
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<div className="cursor-pointer">
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<SquareTerminal className="size-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none">
|
||||
<span className="font-semibold">特恩的日志 - 控制台</span>
|
||||
<span className="">v1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client"
|
||||
|
||||
import { ChevronRight, type LucideIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
import Link from "next/link"
|
||||
import { Skeleton } from "../../../../components/ui/skeleton"
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: null | {
|
||||
title: string
|
||||
url: string
|
||||
icon?: LucideIcon
|
||||
isActive?: boolean
|
||||
isHidden?: boolean
|
||||
items?: {
|
||||
title: string
|
||||
url: string
|
||||
isHidden?: boolean
|
||||
}[]
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>菜单</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{
|
||||
!items && Array(5).fill(null).map((_, i) => (
|
||||
<Skeleton key={i} className="w-full h-7 mt-1" />
|
||||
))
|
||||
}
|
||||
|
||||
{items && items.filter(i => !i.isHidden).map((item) => (
|
||||
(item.items && item.items.length > 0)
|
||||
? (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items.filter(i => !i.isHidden).map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton tooltip={item.title} asChild>
|
||||
<Link href={item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
141
apps/frontend/app/console/(with-menu)/components/nav-user.tsx
Normal file
141
apps/frontend/app/console/(with-menu)/components/nav-user.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
ChevronsUpDown,
|
||||
KeyRound,
|
||||
LogOut,
|
||||
UserRoundCog,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/ui/avatar"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { Skeleton } from "../../../../components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { useRouter } from "next/navigation"
|
||||
import SetPassword from "../../../../components/nav-user/SetPassword"
|
||||
import { useState } from "react"
|
||||
import { User } from "@/lib/types/user"
|
||||
import { AuthAPI } from "@/lib/api/client"
|
||||
import { useUserStore } from "@/store/useUserStore"
|
||||
|
||||
export function NavUser({ user }: { user: User | null }) {
|
||||
const { isMobile } = useSidebar();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await AuthAPI.logout();
|
||||
userStore.clearUser();
|
||||
toast.success('登出成功');
|
||||
router.replace('/console/login');
|
||||
} catch {
|
||||
toast.error('登出失败,请稍后再试');
|
||||
}
|
||||
}
|
||||
|
||||
const [userProfileOpen, setUserProfileOpen] = useState(false);
|
||||
const [passwordOpen, setPasswordOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
{
|
||||
user ?
|
||||
<>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="rounded-lg">U</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<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>
|
||||
}
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
{
|
||||
user ?
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="rounded-lg">U</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.nickname}</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>
|
||||
}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setTimeout(() => setUserProfileOpen(true), 0)}>
|
||||
<UserRoundCog />
|
||||
账户信息
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTimeout(() => setPasswordOpen(true), 0)}>
|
||||
<KeyRound />
|
||||
修改密码
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout}>
|
||||
<LogOut />
|
||||
登出
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu >
|
||||
|
||||
{/* <UserProfile open={userProfileOpen} onOpenChange={setUserProfileOpen} /> */}
|
||||
<SetPassword open={passwordOpen} onOpenChange={setPasswordOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import { AppSidebar } from "@/app/console/(with-menu)/components/app-sidebar"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
|
||||
5
apps/frontend/app/console/(with-menu)/profile/page.tsx
Normal file
5
apps/frontend/app/console/(with-menu)/profile/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user