chore(history): import initial web product snapshot
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { NavLink, useNavigate } from 'react-router-dom'
|
||||
import { PanelLeftClose, PanelLeftOpen, Sparkles } from 'lucide-react'
|
||||
import { NAV_ITEMS } from '@/config/navigation'
|
||||
import { useUiStore } from '@/app/store/ui'
|
||||
import { useAuthStore } from '@/app/store/auth'
|
||||
import { usePermission } from '@/hooks/usePermission'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { cn } from '@/utils/cn'
|
||||
import { getPlan } from '@/config/pricingPlans'
|
||||
import { Progress } from '@/components/ui/feedback'
|
||||
import { Avatar } from '@/components/ui/avatar'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getBilling } from '@/services/api/org'
|
||||
|
||||
export function Sidebar() {
|
||||
const { sidebarCollapsed, toggleSidebar, setMobileNavOpen } = useUiStore()
|
||||
const { t } = useI18n()
|
||||
const { can } = usePermission()
|
||||
const tenant = useAuthStore((s) => s.tenant)
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { data: billing } = useQuery({ queryKey: ['billing'], queryFn: getBilling, staleTime: 300_000 })
|
||||
const plan = getPlan(billing?.info.planKey ?? tenant?.plan ?? 'pro')
|
||||
const msgUsage = billing?.info.usage.messages
|
||||
const usagePct = msgUsage ? Math.round((msgUsage.used / msgUsage.total) * 100) : 0
|
||||
|
||||
const items = NAV_ITEMS.filter((item) => !item.permission || can(item.permission))
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'flex h-full flex-col border-e border-border bg-card transition-[width] duration-200',
|
||||
sidebarCollapsed ? 'w-16' : 'w-60',
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div
|
||||
className="flex h-14 cursor-pointer items-center gap-2.5 border-b border-border px-4"
|
||||
onClick={() => navigate('/dashboard')}
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||
<Sparkles className="h-4.5 w-4.5 h-[18px] w-[18px]" />
|
||||
</span>
|
||||
{!sidebarCollapsed && (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold leading-tight">{t('app.name')}</p>
|
||||
<p className="truncate text-[11px] text-muted-foreground leading-tight">{t('app.slogan')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 导航 */}
|
||||
<nav className="flex-1 space-y-0.5 overflow-y-auto px-2 py-3 scrollbar-thin">
|
||||
{items.map((item) => (
|
||||
<NavLink
|
||||
key={item.key}
|
||||
to={item.path}
|
||||
onClick={() => setMobileNavOpen(false)}
|
||||
title={sidebarCollapsed ? t(item.labelKey as never) : undefined}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm transition-colors',
|
||||
isActive
|
||||
? 'bg-primary-50 font-medium text-primary-700'
|
||||
: 'text-secondary-foreground hover:bg-muted',
|
||||
sidebarCollapsed && 'justify-center px-0',
|
||||
)
|
||||
}
|
||||
>
|
||||
<item.icon className="h-[18px] w-[18px] shrink-0" />
|
||||
{!sidebarCollapsed && <span className="truncate">{t(item.labelKey as never)}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* 底部:套餐与企业信息 */}
|
||||
{!sidebarCollapsed && (
|
||||
<div className="border-t border-border p-3">
|
||||
<div className="rounded-md bg-muted/60 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">{t('sidebar.currentPlan')}</span>
|
||||
<span className="text-xs font-medium text-primary-700">{plan.name}</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="mb-1 flex justify-between text-[11px] text-muted-foreground">
|
||||
<span>{t('sidebar.usage')}(消息)</span>
|
||||
<span>{usagePct}%</span>
|
||||
</div>
|
||||
<Progress value={usagePct} />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/billing')}
|
||||
className="mt-2 text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
升级套餐 →
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-2 px-1">
|
||||
<Avatar name={user?.name ?? '?'} size="sm" online />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-medium">{tenant?.name ?? '—'}</p>
|
||||
<p className="truncate text-[11px] text-muted-foreground">
|
||||
{user?.name} · {user?.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 收起按钮 */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="hidden h-10 items-center justify-center gap-2 border-t border-border text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:flex"
|
||||
>
|
||||
{sidebarCollapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
||||
{!sidebarCollapsed && <span>{t('sidebar.collapse')}</span>}
|
||||
</button>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user