2026-08-24 13:07:09 +08:00
|
|
|
|
import { NavLink, Outlet } from "react-router-dom";
|
2026-08-27 19:53:45 +08:00
|
|
|
|
import { ChevronsUpDown, LogOut } from "lucide-react";
|
2026-08-24 13:07:09 +08:00
|
|
|
|
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() {
|
2026-08-26 12:42:35 +08:00
|
|
|
|
const { role, user, logout } = useAuth();
|
2026-08-24 13:07:09 +08:00
|
|
|
|
const groups = navForRole(role);
|
2026-08-26 19:03:09 +08:00
|
|
|
|
// 展示文本标签(账号类型): 运营方/载体方/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 || "运营方";
|
2026-08-24 13:07:09 +08:00
|
|
|
|
|
|
|
|
|
|
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">
|
2026-08-27 19:53:45 +08:00
|
|
|
|
<img src="/logo.png" alt="云超服" className="h-7 w-7 object-contain" />
|
2026-08-24 13:07:09 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|