refactor(wavelet): 侧栏导航改为 openflareSidebarNav 动态配置

用 kind: item | group 的单一数组描述侧栏顺序,移除 slice(0, 4) 硬编码切分。
This commit is contained in:
ryan
2026-06-18 19:54:08 +08:00
parent 29176da35f
commit b56b4c93b6
4 changed files with 113 additions and 92 deletions
@@ -14,98 +14,103 @@ import {
SidebarMenuSubButton, SidebarMenuSubButton,
SidebarMenuSubItem, SidebarMenuSubItem,
} from '@/components/ui/sidebar'; } from '@/components/ui/sidebar';
import type {OpenFlareNavGroup} from '@/lib/navigation/openflare-nav';
import { import {
matchesNavPath, matchesNavPath,
openflareNavItems, openflareSidebarNav,
openflareWebsiteNavGroup,
isNavGroupActive, isNavGroupActive,
} from '@/lib/navigation/openflare-nav'; } from '@/lib/navigation/openflare-nav';
function SidebarNavGroupMenuItem({
group,
pathname,
onNavigate,
}: {
group: OpenFlareNavGroup;
pathname: string;
onNavigate?: () => void;
}) {
const groupActive = isNavGroupActive(pathname, group);
const [open, setOpen] = useState(groupActive);
useEffect(() => {
if (groupActive) {
setOpen(true);
}
}, [groupActive]);
return (
<Collapsible
asChild
open={open}
onOpenChange={setOpen}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={group.title} isActive={groupActive}>
<group.icon />
<span>{group.title}</span>
<ChevronRight className="ml-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{group.items.map((item) => (
<SidebarMenuSubItem key={item.title}>
<SidebarMenuSubButton
asChild
isActive={matchesNavPath(pathname, item.url, item.childUrls)}
>
<Link href={item.url} onClick={onNavigate}>
<span>{item.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
);
}
export function OpenFlareSidebarMenu({ export function OpenFlareSidebarMenu({
onNavigate, onNavigate,
}: { }: {
onNavigate?: () => void; onNavigate?: () => void;
}) { }) {
const pathname = usePathname(); 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 ( return (
<SidebarMenu className="gap-1"> <SidebarMenu className="gap-1">
{renderFlatItemsBeforeGroup.map((item) => ( {openflareSidebarNav.map((entry) => {
<SidebarMenuItem key={item.title}> if (entry.kind === 'group') {
<SidebarMenuButton return (
tooltip={item.title} <SidebarNavGroupMenuItem
isActive={matchesNavPath(pathname, item.url, item.childUrls)} key={entry.title}
asChild group={entry}
> pathname={pathname}
<Link href={item.url} onClick={onNavigate}> onNavigate={onNavigate}
<item.icon /> />
<span>{item.title}</span> );
</Link> }
</SidebarMenuButton>
</SidebarMenuItem>
))}
<Collapsible return (
asChild <SidebarMenuItem key={entry.title}>
open={websiteGroupOpen}
onOpenChange={setWebsiteGroupOpen}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton <SidebarMenuButton
tooltip={openflareWebsiteNavGroup.title} tooltip={entry.title}
isActive={websiteGroupActive} isActive={matchesNavPath(pathname, entry.url, entry.childUrls)}
asChild
> >
<openflareWebsiteNavGroup.icon /> <Link href={entry.url} onClick={onNavigate}>
<span>{openflareWebsiteNavGroup.title}</span> <entry.icon />
<ChevronRight className="ml-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> <span>{entry.title}</span>
</Link>
</SidebarMenuButton> </SidebarMenuButton>
</CollapsibleTrigger> </SidebarMenuItem>
<CollapsibleContent> );
<SidebarMenuSub> })}
{openflareWebsiteNavGroup.items.map((item) => (
<SidebarMenuSubItem key={item.title}>
<SidebarMenuSubButton
asChild
isActive={matchesNavPath(pathname, item.url, item.childUrls)}
>
<Link href={item.url} onClick={onNavigate}>
<span>{item.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
{renderFlatItemsAfterGroup.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
tooltip={item.title}
isActive={matchesNavPath(pathname, item.url, item.childUrls)}
asChild
>
<Link href={item.url} onClick={onNavigate}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu> </SidebarMenu>
); );
} }
@@ -64,7 +64,7 @@ import {
import {useUser} from "@/contexts/user-context" import {useUser} from "@/contexts/user-context"
import {usePublicConfig} from "@/hooks/use-public-config" import {usePublicConfig} from "@/hooks/use-public-config"
import {OpenFlareSidebarMenu} from "@/components/layout/openflare-sidebar-menu" import {OpenFlareSidebarMenu} from "@/components/layout/openflare-sidebar-menu"
import {openflareNavItems} from "@/lib/navigation/openflare-nav" import {openflareSidebarNav} from "@/lib/navigation/openflare-nav"
/* 导航数据 */ /* 导航数据 */
const data = { const data = {
@@ -295,7 +295,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</DropdownMenu> </DropdownMenu>
</SidebarHeader> </SidebarHeader>
<SidebarContent className="group-data-[collapsible=icon]"> <SidebarContent className="group-data-[collapsible=icon]">
{openflareNavItems.length > 0 && ( {openflareSidebarNav.length > 0 && (
<SidebarGroup className="py-0"> <SidebarGroup className="py-0">
<SidebarGroupContent className="py-1"> <SidebarGroupContent className="py-1">
<OpenFlareSidebarMenu onNavigate={handleCloseSidebar} /> <OpenFlareSidebarMenu onNavigate={handleCloseSidebar} />
@@ -31,17 +31,9 @@ export interface OpenFlareNavGroup {
items: OpenFlareNavSubItem[]; items: OpenFlareNavSubItem[];
} }
/** OpenFlare 业务控制台侧栏导航(子页面通过父级入口或页内链接访问) */ export type OpenFlareSidebarNavEntry =
export const openflareNavItems: OpenFlareNavItem[] = [ | ({kind: 'item'} & OpenFlareNavItem)
{title: '数据看板', url: '/', icon: LayoutDashboard}, | ({kind: 'group'} & OpenFlareNavGroup);
{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: 'WAF', url: '/waf', icon: Shield, childUrls: ['/waf/ip-groups']},
{title: '版本发布', url: '/config-versions', icon: GitBranch},
{title: '访问日志', url: '/access-logs', icon: ScrollText},
{title: '性能调优', url: '/performance', icon: Gauge},
];
/** 网站管理折叠组 */ /** 网站管理折叠组 */
export const openflareWebsiteNavGroup: OpenFlareNavGroup = { export const openflareWebsiteNavGroup: OpenFlareNavGroup = {
@@ -55,6 +47,27 @@ export const openflareWebsiteNavGroup: OpenFlareNavGroup = {
], ],
}; };
/**
* OpenFlare 侧栏导航顺序(单一配置源)。
* 调整菜单顺序或折叠组位置时,只需修改此数组。
*/
export const openflareSidebarNav: OpenFlareSidebarNavEntry[] = [
{kind: 'item', title: '数据看板', url: '/', icon: LayoutDashboard},
{kind: 'item', title: '节点管理', url: '/nodes', icon: Server, childUrls: ['/nodes/detail']},
{kind: 'item', title: '规则管理', url: '/proxy-routes', icon: Route, childUrls: ['/proxy-routes/detail']},
{kind: 'item', title: 'WAF', url: '/waf', icon: Shield, childUrls: ['/waf/ip-groups']},
{kind: 'group', ...openflareWebsiteNavGroup},
{kind: 'item', title: 'Pages', url: '/pages', icon: FileText, childUrls: ['/pages/detail']},
{kind: 'item', title: '版本发布', url: '/config-versions', icon: GitBranch},
{kind: 'item', title: '访问日志', url: '/access-logs', icon: ScrollText},
{kind: 'item', title: '性能调优', url: '/performance', icon: Gauge},
];
/** 扁平菜单项(供路由判断等逻辑复用) */
export const openflareNavItems: OpenFlareNavItem[] = openflareSidebarNav
.filter((entry): entry is {kind: 'item'} & OpenFlareNavItem => entry.kind === 'item')
.map(({kind: _kind, ...item}) => item);
/** 网站模块页内二级导航 */ /** 网站模块页内二级导航 */
export const openflareWebsiteSubNav = [ export const openflareWebsiteSubNav = [
{title: '网站列表', url: '/websites'}, {title: '网站列表', url: '/websites'},
@@ -92,9 +105,11 @@ export function isOpenFlareConsoleRoute(pathname: string): boolean {
return false; return false;
} }
if (isNavGroupActive(pathname, openflareWebsiteNavGroup)) { return openflareSidebarNav.some((entry) => {
return true; if (entry.kind === 'group') {
} return isNavGroupActive(pathname, entry);
}
return openflareNavItems.some((item) => matchesNavPath(pathname, item.url, item.childUrls)); return matchesNavPath(pathname, entry.url, entry.childUrls);
});
} }
+1
View File
@@ -57,6 +57,7 @@ sidebar: false
- 节点详情 24 小时网络/磁盘 IO 趋势图对齐原版 ECharts 样式,恢复系列摘要卡与平滑面积折线图。 - 节点详情 24 小时网络/磁盘 IO 趋势图对齐原版 ECharts 样式,恢复系列摘要卡与平滑面积折线图。
- 侧栏新增「网站管理」折叠组(shadcn Collapsible + SidebarMenuSub),收纳网站、证书、DNS、源站入口。 - 侧栏新增「网站管理」折叠组(shadcn Collapsible + SidebarMenuSub),收纳网站、证书、DNS、源站入口。
- 移除网站、证书、DNS 页面右上角互相跳转按钮,改由侧栏折叠组统一导航。 - 移除网站、证书、DNS 页面右上角互相跳转按钮,改由侧栏折叠组统一导航。
- 侧栏导航改为 `openflareSidebarNav` 单一配置源,折叠组位置由数组顺序决定,移除 `slice(0, 4)` 硬编码切分。
- OpenFlare 前端路由去除 `/openflare` 前缀,业务页面直接挂载于 `/`(如 `/nodes`、`/websites`);侧栏移除「首页」「我的文件」;旧路径 `/openflare/*`、`/home` 永久重定向至新路径。 - OpenFlare 前端路由去除 `/openflare` 前缀,业务页面直接挂载于 `/`(如 `/nodes`、`/websites`);侧栏移除「首页」「我的文件」;旧路径 `/openflare/*`、`/home` 永久重定向至新路径。
## [v2.3.4] - 2026-06-17 ## [v2.3.4] - 2026-06-17