Files
users/app/components/Sidebar.tsx

224 lines
7.2 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.

"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
LayoutDashboard,
Database,
Tags,
Brain,
Package,
Monitor,
ChevronDown,
ChevronRight,
FileText,
Users,
Zap,
Target,
BarChart3,
Shield,
Bell,
Server,
MessageSquare,
Sparkles,
GitBranch,
Calendar,
Bot,
Search,
FileOutput,
Webhook,
Activity,
ScrollText,
Globe,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { useState } from "react"
// 五大核心模块导航结构(不可删除)
const NAV_ITEMS = [
// 第一部分:数据概览
{
href: "/",
label: "数据概览",
icon: LayoutDashboard,
description: "AI对话 · 数据仪表板"
},
// 第二部分:数据接入
{
href: "/data-ingestion",
label: "数据接入",
icon: Database,
children: [
{ href: "/data-ingestion/sources", label: "数据源管理", icon: Database },
{ href: "/data-ingestion/ai-engine", label: "AI标签引擎", icon: Brain },
{ href: "/data-ingestion/cleaning", label: "清洗规则", icon: Zap },
{ href: "/data-ingestion/tasks", label: "任务调度", icon: Calendar },
{ href: "/data-ingestion/lineage", label: "数据血缘", icon: GitBranch },
],
},
// 第三部分:标签画像
{
href: "/tag-portrait",
label: "标签画像",
icon: Tags,
children: [
{ href: "/tag-portrait/tags", label: "标签管理", icon: Tags },
{ href: "/tag-portrait/portrait", label: "用户画像", icon: Users },
{ href: "/tag-portrait/crowd", label: "流量池", icon: Target },
],
},
// 第四部分AI Agent对接飞书/企微等外部平台)
{
href: "/ai-agent",
label: "AI Agent",
icon: Bot,
children: [
{ href: "/ai-agent/channels", label: "渠道配置", icon: Webhook },
{ href: "/ai-agent/smart-tag", label: "AI打标", icon: Sparkles },
{ href: "/ai-agent/data-cleaning", label: "AI清洗", icon: Zap },
{ href: "/ai-agent/report", label: "智能报告", icon: FileText },
],
},
// 第五部分:数据市场
{
href: "/data-market",
label: "数据市场",
icon: Package,
children: [
{ href: "/data-market/packages", label: "流量包", icon: Package },
{ href: "/data-market/api", label: "API服务", icon: Server },
{ href: "/data-market/open-api", label: "开放接口", icon: Globe },
],
},
] as const
// 底部工具菜单(系统监控等)
const BOTTOM_NAV_ITEMS = [
{
href: "/monitoring/health",
label: "系统监控",
icon: Monitor,
},
] as const
type NavItem = (typeof NAV_ITEMS)[number]
function NavItemComponent({ item, level = 0 }: { item: NavItem; level?: number }) {
const pathname = usePathname()
const [isOpen, setIsOpen] = useState(false)
const hasChildren = "children" in item && item.children && item.children.length > 0
const isExactActive = pathname === item.href
const isActive = isExactActive || (item.href !== "/" && pathname.startsWith(item.href))
// Auto expand if child is active
const childActive =
hasChildren && item.children?.some((child) => pathname === child.href || pathname.startsWith(child.href))
return (
<li>
{hasChildren ? (
<div>
{/* 父级菜单:左侧可点击跳转,右侧按钮展开子菜单 */}
<div className={cn(
"flex items-center rounded-xl transition-all duration-200",
isExactActive
? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-md"
: isActive || childActive
? "bg-blue-50 text-blue-600"
: "text-gray-600 hover:bg-gray-100",
)}>
<Link
href={item.href}
className="flex-1 flex items-center gap-3 px-4 py-3 text-sm font-medium"
>
<item.icon className="h-5 w-5" />
<span>{item.label}</span>
</Link>
<button
onClick={(e) => {
e.preventDefault()
setIsOpen(!isOpen)
}}
className={cn(
"px-3 py-3 rounded-r-xl transition-colors",
isExactActive
? "hover:bg-white/10"
: "hover:bg-gray-200"
)}
aria-label={isOpen ? "收起子菜单" : "展开子菜单"}
>
{isOpen || childActive ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</button>
</div>
{(isOpen || childActive) && (
<ul className="mt-1 ml-4 space-y-1 border-l-2 border-gray-100 pl-4">
{item.children?.map((child) => {
const childIsActive = pathname === child.href
return (
<li key={child.href}>
<Link
href={child.href}
className={cn(
"flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-all duration-200",
childIsActive
? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-md"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900",
)}
>
<child.icon className="h-4 w-4" />
<span>{child.label}</span>
</Link>
</li>
)
})}
</ul>
)}
</div>
) : (
<Link
href={item.href}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-all duration-200",
isActive
? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-md"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
)}
>
<item.icon className="h-5 w-5" />
<span>{item.label}</span>
</Link>
)}
</li>
)
}
export default function Sidebar() {
return (
<aside className="hidden md:flex flex-col w-64 shrink-0 border-r bg-white/80 backdrop-blur-md h-screen sticky top-0">
<div className="p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-1"></h2>
<p className="text-sm text-gray-600"></p>
</div>
<nav className="flex-1 px-4 pb-4 overflow-y-auto">
<ul className="space-y-1">
{NAV_ITEMS.map((item) => (
<NavItemComponent key={item.href} item={item} />
))}
</ul>
</nav>
<div className="p-4 border-t border-gray-100">
<div className="flex items-center gap-3 px-2">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-sm font-bold">
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate"></p>
<p className="text-xs text-gray-500 truncate">admin@archer.com</p>
</div>
</div>
</div>
</aside>
)
}