Files
admin-portal/src/components/layout/AppShell.tsx
T

131 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NavLink, Outlet } from "react-router-dom";
import { ChevronsUpDown, LogOut } 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/服务方。
// 优先按 role 映射(取自身份/端口角色, 可靠), 兜底用后端下发的 account_type_label。
const roleToLabel: Record<string, string> = {
operator: "运营方",
carrier: "载体方",
opc_member: "OPC",
service: "服务方",
};
const atypeLabel = (user as { account_type_label?: string })?.account_type_label;
const roleLabel = roleToLabel[role ?? ""] || atypeLabel || 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">
<img src="/logo.png" alt="云超服" className="h-7 w-7 object-contain" />
<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>
);
}