diff --git a/Wavelet/frontend/components/layout/openflare-sidebar-menu.tsx b/Wavelet/frontend/components/layout/openflare-sidebar-menu.tsx new file mode 100644 index 00000000..36f0c4b0 --- /dev/null +++ b/Wavelet/frontend/components/layout/openflare-sidebar-menu.tsx @@ -0,0 +1,111 @@ +'use client'; + +import Link from 'next/link'; +import {usePathname} from 'next/navigation'; +import {useEffect, useState} from 'react'; +import {ChevronRight} from 'lucide-react'; + +import {Collapsible, CollapsibleContent, CollapsibleTrigger} from '@/components/ui/collapsible'; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from '@/components/ui/sidebar'; +import { + matchesNavPath, + openflareNavItems, + openflareWebsiteNavGroup, + isNavGroupActive, +} from '@/lib/navigation/openflare-nav'; + +export function OpenFlareSidebarMenu({ + onNavigate, +}: { + onNavigate?: () => void; +}) { + const pathname = usePathname(); + const websiteGroupActive = isNavGroupActive(pathname, openflareWebsiteNavGroup); + const [websiteGroupOpen, setWebsiteGroupOpen] = useState(websiteGroupActive); + + useEffect(() => { + if (websiteGroupActive) { + setWebsiteGroupOpen(true); + } + }, [websiteGroupActive]); + + const renderFlatItemsBeforeGroup = openflareNavItems.slice(0, 4); + const renderFlatItemsAfterGroup = openflareNavItems.slice(4); + + return ( + + {renderFlatItemsBeforeGroup.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + + {openflareWebsiteNavGroup.title} + + + + + + {openflareWebsiteNavGroup.items.map((item) => ( + + + + {item.title} + + + + ))} + + + + + + {renderFlatItemsAfterGroup.map((item) => ( + + + + + {item.title} + + + + ))} + + ); +} \ No newline at end of file diff --git a/Wavelet/frontend/components/layout/sidebar.tsx b/Wavelet/frontend/components/layout/sidebar.tsx index 3c6127ca..e927b3de 100644 --- a/Wavelet/frontend/components/layout/sidebar.tsx +++ b/Wavelet/frontend/components/layout/sidebar.tsx @@ -63,6 +63,7 @@ import { import {useUser} from "@/contexts/user-context" import {usePublicConfig} from "@/hooks/use-public-config" +import {OpenFlareSidebarMenu} from "@/components/layout/openflare-sidebar-menu" import {openflareNavItems} from "@/lib/navigation/openflare-nav" /* 导航数据 */ @@ -297,26 +298,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {openflareNavItems.length > 0 && ( - - {openflareNavItems.map((item) => ( - - - - - {item.title} - - - - ))} - + )} diff --git a/Wavelet/frontend/components/ui/collapsible.tsx b/Wavelet/frontend/components/ui/collapsible.tsx new file mode 100644 index 00000000..5a341ea7 --- /dev/null +++ b/Wavelet/frontend/components/ui/collapsible.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import {Collapsible as CollapsiblePrimitive} from "radix-ui" + +function Collapsible({ + ...props +}: React.ComponentProps) { + return +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } \ No newline at end of file diff --git a/Wavelet/frontend/lib/navigation/openflare-nav.ts b/Wavelet/frontend/lib/navigation/openflare-nav.ts index 32b2bac0..abe1b75b 100644 --- a/Wavelet/frontend/lib/navigation/openflare-nav.ts +++ b/Wavelet/frontend/lib/navigation/openflare-nav.ts @@ -22,24 +22,25 @@ export interface OpenFlareNavItem { childUrls?: string[]; } +export interface OpenFlareNavSubItem { + title: string; + url: string; + childUrls?: string[]; +} + +export interface OpenFlareNavGroup { + title: string; + icon: LucideIcon; + items: OpenFlareNavSubItem[]; +} + /** OpenFlare 业务控制台侧栏导航(子页面通过父级入口或页内链接访问) */ export const openflareNavItems: OpenFlareNavItem[] = [ {title: '总览', url: '/', icon: LayoutDashboard}, {title: '节点', url: '/nodes', icon: Server, childUrls: ['/nodes/detail']}, {title: '规则', url: '/proxy-routes', icon: Route, childUrls: ['/proxy-routes/detail']}, {title: 'Pages', url: '/pages', icon: FileText, childUrls: ['/pages/detail']}, - { - title: '网站', - url: '/websites', - icon: Globe, - childUrls: [ - '/websites/detail', - '/websites/certificates', - '/websites/dns-accounts', - ], - }, {title: 'WAF', url: '/waf', icon: Shield, childUrls: ['/waf/ip-groups']}, - {title: '源站', url: '/origins', icon: MapPin, childUrls: ['/origins/detail']}, {title: '发布', url: '/config-versions', icon: GitBranch}, {title: '访问日志', url: '/access-logs', icon: ScrollText}, {title: '应用日志', url: '/apply-logs', icon: ClipboardList}, @@ -47,6 +48,18 @@ export const openflareNavItems: OpenFlareNavItem[] = [ {title: '关于', url: '/about', icon: Info}, ]; +/** 网站管理折叠组 */ +export const openflareWebsiteNavGroup: OpenFlareNavGroup = { + title: '网站管理', + icon: Globe, + items: [ + {title: '网站', url: '/websites', childUrls: ['/websites/detail']}, + {title: '证书', url: '/websites/certificates'}, + {title: 'DNS', url: '/websites/dns-accounts'}, + {title: '源站', url: '/origins', childUrls: ['/origins/detail']}, + ], +}; + /** 网站模块页内二级导航 */ export const openflareWebsiteSubNav = [ {title: '网站列表', url: '/websites'}, @@ -56,16 +69,37 @@ export const openflareWebsiteSubNav = [ const nonConsoleRoutePrefixes = ['/admin', '/settings', '/files', '/home', '/login', '/register', '/docs']; +export function matchesNavPath( + pathname: string, + url: string, + childUrls?: string[], +): boolean { + if (url === '/') { + return pathname === '/'; + } + + if (pathname === url || pathname.startsWith(`${url}/`)) { + return true; + } + + return (childUrls ?? []).some( + (childUrl) => pathname === childUrl || pathname.startsWith(`${childUrl}/`), + ); +} + +export function isNavGroupActive(pathname: string, group: OpenFlareNavGroup): boolean { + return group.items.some((item) => matchesNavPath(pathname, item.url, item.childUrls)); +} + /** 判断当前路径是否属于 OpenFlare 业务控制台(用于顶栏版本入口等) */ export function isOpenFlareConsoleRoute(pathname: string): boolean { if (nonConsoleRoutePrefixes.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) { return false; } - return openflareNavItems.some((item) => { - if (item.url === '/') { - return pathname === '/'; - } - return pathname === item.url || pathname.startsWith(`${item.url}/`); - }); + if (isNavGroupActive(pathname, openflareWebsiteNavGroup)) { + return true; + } + + return openflareNavItems.some((item) => matchesNavPath(pathname, item.url, item.childUrls)); } \ No newline at end of file diff --git a/docs/changelog/index.md b/docs/changelog/index.md index e6a59ca6..e6259a07 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -54,6 +54,7 @@ sidebar: false - 补全 Wavelet 边缘节点详情数据看板:迁移运行诊断摘要、系统信息、实时资源、网络流量、24 小时请求/容量/网络/磁盘趋势、请求结构分布与健康事件时间线。 - 节点详情页改为 Tabs 布局(概览 / 数据看板 / 配置与部署),统一 Edge/Relay/Tunnel 详情壳层并支持 `tab` URL 参数深链。 - 节点详情 24 小时网络/磁盘 IO 趋势图对齐原版 ECharts 样式,恢复系列摘要卡与平滑面积折线图。 +- 侧栏新增「网站管理」折叠组(shadcn Collapsible + SidebarMenuSub),收纳网站、证书、DNS、源站入口。 - OpenFlare 前端路由去除 `/openflare` 前缀,业务页面直接挂载于 `/`(如 `/nodes`、`/websites`);侧栏移除「首页」「我的文件」;旧路径 `/openflare/*`、`/home` 永久重定向至新路径。 ## [v2.3.4] - 2026-06-17