123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
import { NavLink, Outlet } from "react-router-dom";
|
||
import { ChevronsUpDown, LogOut, ShieldCheck } from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
import { navForRole } from "@/lib/rbac";
|
||
import { useAuth } from "@/lib/auth";
|
||
import {
|
||
Avatar,
|
||
AvatarFallback,
|
||
} from "@/components/ui/avatar";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuLabel,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
|
||
export function AppShell() {
|
||
const { role, user, logout } = useAuth();
|
||
const groups = navForRole(role);
|
||
// 展示文本标签(账号类型): 运营方/载体方/OPC/服务方 —— 后端下发 account_type_label, 不做代码映射
|
||
const roleLabel = (user as { account_type_label?: string })?.account_type_label || role || "运营方";
|
||
|
||
return (
|
||
<div className="flex min-h-screen">
|
||
{/* 侧边栏 */}
|
||
<aside className="fixed inset-y-0 left-0 z-30 flex w-60 flex-col border-r bg-card">
|
||
<div className="flex h-14 items-center gap-2 px-4">
|
||
<ShieldCheck className="h-5 w-5 text-(--mascot-accent)" />
|
||
<span className="font-semibold">云超服 · 运营管理</span>
|
||
</div>
|
||
<nav className="flex-1 overflow-y-auto px-2 py-4">
|
||
{groups.map((g, gi) => (
|
||
<div key={gi} className="mb-4">
|
||
{g.title && (
|
||
<div className="px-2 pb-1 text-xs font-medium uppercase text-muted-foreground">
|
||
{g.title}
|
||
</div>
|
||
)}
|
||
<div className="space-y-1">
|
||
{g.items.map((item) => (
|
||
<NavLink
|
||
key={item.to}
|
||
to={item.to}
|
||
className={({ isActive }) =>
|
||
cn(
|
||
"flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
|
||
isActive
|
||
? "bg-accent font-medium text-accent-foreground"
|
||
: "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground",
|
||
)
|
||
}
|
||
>
|
||
<item.icon className="h-4 w-4" />
|
||
{item.label}
|
||
</NavLink>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</nav>
|
||
<div className="border-t p-2">
|
||
<UserMenu
|
||
roleLabel={roleLabel}
|
||
username={user?.username || "账号"}
|
||
onLogout={logout}
|
||
/>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* 主内容 */}
|
||
<div className="flex-1 pl-60">
|
||
<header className="sticky top-0 z-20 flex h-14 items-center justify-between border-b bg-background/95 px-6 backdrop-blur">
|
||
<div className="text-sm text-muted-foreground">
|
||
当前身份:
|
||
<span className="ml-1 font-medium text-foreground">{roleLabel}</span>
|
||
</div>
|
||
</header>
|
||
<main className="p-6">
|
||
<Outlet />
|
||
</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function UserMenu({
|
||
roleLabel,
|
||
username,
|
||
onLogout,
|
||
}: {
|
||
roleLabel: string;
|
||
username: string;
|
||
onLogout: () => void;
|
||
}) {
|
||
return (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="w-full justify-start gap-2 px-2">
|
||
<Avatar className="h-7 w-7">
|
||
<AvatarFallback>{(username || "A").slice(0, 1).toUpperCase()}</AvatarFallback>
|
||
</Avatar>
|
||
<div className="flex flex-col items-start leading-tight">
|
||
<span className="text-sm">{username}</span>
|
||
<span className="text-xs text-muted-foreground">{roleLabel}</span>
|
||
</div>
|
||
<ChevronsUpDown className="ml-auto h-4 w-4 opacity-50" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-56">
|
||
<DropdownMenuLabel>{username}</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem onClick={onLogout}>
|
||
<LogOut className="h-4 w-4" />
|
||
退出登录
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
);
|
||
}
|