diff --git a/package.json b/package.json index bc22c36..24d96ae 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,13 @@ "@base-ui/react": "^1.3.0", "@kevisual/api": "^0.0.64", "@kevisual/context": "^0.0.8", - "@kevisual/router": "0.1.2", - "@tanstack/react-query": "^5.90.21", - "@tanstack/react-router": "^1.167.3", + "@kevisual/router": "0.1.6", + "@tanstack/react-query": "^5.91.3", + "@tanstack/react-router": "^1.168.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "convex": "^1.33.1", + "convex": "^1.34.0", "dayjs": "^1.11.20", "es-toolkit": "^1.45.1", "fuse.js": "^7.1.0", @@ -45,22 +45,22 @@ "devDependencies": { "@kevisual/ai": "0.0.28", "@kevisual/kv-login": "^0.1.18", - "@kevisual/query": "0.0.53", + "@kevisual/query": "0.0.55", "@kevisual/types": "^0.0.12", "@kevisual/vite-html-plugin": "^0.0.1", - "@tailwindcss/vite": "^4.2.1", - "@tanstack/react-router-devtools": "^1.166.9", - "@tanstack/router-plugin": "^1.166.12", + "@tailwindcss/vite": "^4.2.2", + "@tanstack/react-router-devtools": "^1.166.10", + "@tanstack/router-plugin": "^1.167.1", "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "dotenv": "^17.3.1", "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.2.1", + "tailwindcss": "^4.2.2", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vite": "v8.0.0", + "vite": "v8.0.1", "vite-plugin-pwa": "^1.2.0" } } \ No newline at end of file diff --git a/src/components/a/Sidebar.tsx b/src/components/a/Sidebar.tsx new file mode 100644 index 0000000..31a177f --- /dev/null +++ b/src/components/a/Sidebar.tsx @@ -0,0 +1,281 @@ +'use client' +import { useNavigate, useLocation } from '@tanstack/react-router' +import { useState } from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react' +import { Resizable } from 're-resizable' + +export interface NavItem { + title: string + path: string + icon?: React.ReactNode + isDeveloping?: boolean + badge?: string + hidden?: boolean + children?: NavItem[] + external?: boolean + onClick?: () => void +} + +export interface SidebarProps { + items: NavItem[] + className?: string + children?: React.ReactNode + logo?: React.ReactNode + title?: React.ReactNode + defaultCollapsed?: boolean + defaultWidth?: number + minWidth?: number + maxWidth?: number +} + +export function Sidebar({ + items, + className, + children, + logo, + title, + defaultCollapsed = false, + defaultWidth = 208, + minWidth = 120, + maxWidth = 400, +}: SidebarProps) { + const navigate = useNavigate() + const location = useLocation() + const currentPath = location.pathname + + const [collapsed, setCollapsed] = useState(defaultCollapsed) + const [sidebarWidth, setSidebarWidth] = useState(defaultWidth) + const [expandedGroups, setExpandedGroups] = useState>(new Set()) + const [developingDialog, setDevelopingDialog] = useState<{ open: boolean; title: string }>({ + open: false, + title: '', + }) + + const toggleGroup = (path: string) => { + const newExpanded = new Set(expandedGroups) + if (newExpanded.has(path)) { + newExpanded.delete(path) + } else { + newExpanded.add(path) + } + setExpandedGroups(newExpanded) + } + + const handleNavClick = (item: NavItem) => { + // 优先执行 onClick 回调 + if (item.onClick) { + item.onClick() + return + } + + if (item.isDeveloping) { + setDevelopingDialog({ open: true, title: item.title }) + } else if (item.external && item.path.startsWith('http')) { + window.open(item.path, '_blank') + } else if (item.path.startsWith('/')) { + navigate({ to: item.path }) + } else { + navigate({ href: item.path }) + } + } + + // 判断当前路径是否激活(以导航路径开头) + const isActive = (path: string) => { + if (path === '/') { + return currentPath === '/' + } + return currentPath.startsWith(path); + } + + // 渲染导航项 + const renderNavItem = (item: NavItem, isChild = false) => { + if (item.hidden) return null + + const hasChildren = item.children && item.children.length > 0 + const isExpanded = expandedGroups.has(item.path) + const active = isActive(item.path) + + return ( +
  • + {hasChildren ? ( + // 父菜单项(可展开) +
    + + {/* 子菜单 */} + {!collapsed && isExpanded && item.children && ( +
      + {item.children.map(child => renderNavItem(child, true))} +
    + )} +
    + ) : ( + // 普通菜单项 + + )} +
  • + ) + } + + return ( + <> +
    + {/* 侧边栏 */} + {!collapsed ? ( + { + setSidebarWidth(ref.offsetWidth) + }} + enable={{ + right: true, + }} + handleComponent={{ + right:
    , + }} + > + + + ) : ( + // 收起状态 + + )} + + {/* 主内容区域 */} +
    + {children} +
    +
    + + {/* 开发中弹窗 */} + setDevelopingDialog({ open, title: '' })} + > + + + {developingDialog.title} - 开发中 + + 该功能正在紧张开发中,敬请期待!暂时无法访问。 + + + + + + + + + ) +}