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