chore(history): import initial web product snapshot

This commit is contained in:
wmq
2026-07-25 00:30:00 +08:00
commit cf0eed46c3
131 changed files with 15693 additions and 0 deletions
+121
View File
@@ -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>
)
}