feat: add frontend auth shell
This commit is contained in:
@@ -1,32 +1,66 @@
|
||||
import { Bot, BriefcaseBusiness, Database, Home, Library, Settings, ShieldCheck, Ticket } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Bot,
|
||||
BriefcaseBusiness,
|
||||
ClipboardList,
|
||||
Database,
|
||||
Home,
|
||||
Library,
|
||||
Menu,
|
||||
ScrollText,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Ticket,
|
||||
UserCog,
|
||||
Users,
|
||||
UsersRound
|
||||
} from 'lucide-react'
|
||||
|
||||
import { useAuth } from '@/features/auth/AuthProvider'
|
||||
import type { AuthMenu } from '@/features/auth/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const navGroups = [
|
||||
{
|
||||
label: '工作台',
|
||||
items: [{ href: '#/', label: '运营总览', icon: Home }]
|
||||
},
|
||||
{
|
||||
label: '业务处理',
|
||||
items: [
|
||||
{ href: '#/tickets', label: '工单中心', icon: Ticket },
|
||||
{ href: '#/tickets/new', label: '创建工单', icon: BriefcaseBusiness },
|
||||
{ href: '#/solutions', label: '解决方案库', icon: Library }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '平台能力',
|
||||
items: [
|
||||
{ href: '#/knowledge', label: '知识库', icon: Database },
|
||||
{ href: '#/agents', label: 'Agent 配置', icon: Bot },
|
||||
{ href: '#/system', label: '系统治理', icon: ShieldCheck },
|
||||
{ href: '#/design-system', label: '设计系统', icon: Settings }
|
||||
]
|
||||
}
|
||||
]
|
||||
const iconMap = {
|
||||
audit: ScrollText,
|
||||
bot: Bot,
|
||||
briefcase: BriefcaseBusiness,
|
||||
database: Database,
|
||||
home: Home,
|
||||
library: Library,
|
||||
menu: Menu,
|
||||
roles: UserCog,
|
||||
settings: Settings,
|
||||
shield: ShieldCheck,
|
||||
ticket: Ticket,
|
||||
user_groups: UsersRound,
|
||||
users: Users
|
||||
}
|
||||
|
||||
function getIcon(icon: string) {
|
||||
return iconMap[icon as keyof typeof iconMap] ?? ClipboardList
|
||||
}
|
||||
|
||||
function useCurrentHash() {
|
||||
const [hash, setHash] = React.useState(() => window.location.hash || '#/')
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleHashChange = () => setHash(window.location.hash || '#/')
|
||||
window.addEventListener('hashchange', handleHashChange)
|
||||
return () => window.removeEventListener('hashchange', handleHashChange)
|
||||
}, [])
|
||||
|
||||
return hash
|
||||
}
|
||||
|
||||
function visibleChildren(menu: AuthMenu) {
|
||||
return menu.children.filter((item) => item.is_visible && item.is_enabled)
|
||||
}
|
||||
|
||||
export function AppSidebar() {
|
||||
const { user } = useAuth()
|
||||
const currentHash = useCurrentHash()
|
||||
const groups = (user?.menus ?? []).filter((menu) => menu.is_visible && menu.is_enabled)
|
||||
|
||||
return (
|
||||
<aside className="fixed inset-y-0 left-0 z-30 hidden w-72 border-r border-border bg-surface lg:block">
|
||||
<div className="border-b border-border px-5 py-4">
|
||||
@@ -34,23 +68,25 @@ export function AppSidebar() {
|
||||
<p className="mt-1 text-xs text-muted-foreground">企业技术支持工作台</p>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-5 p-4">
|
||||
{navGroups.map((group) => (
|
||||
<div key={group.label} className="grid gap-1.5">
|
||||
<p className="px-2 text-xs font-medium text-muted-foreground">{group.label}</p>
|
||||
{groups.map((group) => (
|
||||
<div key={group.code} className="grid gap-1.5">
|
||||
<p className="px-2 text-xs font-medium text-muted-foreground">{group.title}</p>
|
||||
<div className="grid gap-1">
|
||||
{group.items.map((item) => {
|
||||
const Icon = item.icon
|
||||
{visibleChildren(group).map((item) => {
|
||||
const Icon = getIcon(item.icon)
|
||||
const active = currentHash === item.path
|
||||
return (
|
||||
<a
|
||||
className={cn(
|
||||
'flex h-9 items-center gap-2 rounded-md px-2.5 text-sm text-muted-foreground transition-colors',
|
||||
'hover:bg-muted hover:text-foreground'
|
||||
'hover:bg-muted hover:text-foreground',
|
||||
active && 'bg-muted text-foreground'
|
||||
)}
|
||||
href={item.href}
|
||||
key={item.href}
|
||||
href={item.path}
|
||||
key={item.code}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span>{item.label}</span>
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Bell, ChevronRight, UserRound } from 'lucide-react'
|
||||
import { Bell, ChevronRight, LogOut, UserRound } from 'lucide-react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { useAuth } from '@/features/auth/AuthProvider'
|
||||
|
||||
export function AppTopbar() {
|
||||
const { logout, user } = useAuth()
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 flex h-14 items-center justify-between border-b border-border bg-surface/95 px-6 backdrop-blur">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||
@@ -20,7 +23,11 @@ export function AppTopbar() {
|
||||
</Button>
|
||||
<Button variant="ghost" className="gap-2">
|
||||
<UserRound className="size-4" />
|
||||
<span>系统管理员</span>
|
||||
<span>{user?.display_name || user?.username || '当前用户'}</span>
|
||||
</Button>
|
||||
<Button variant="outline" className="gap-2" onClick={() => void logout()}>
|
||||
<LogOut className="size-4" />
|
||||
<span>退出</span>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user