mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
refactor(wavelet): 侧栏导航改为 openflareSidebarNav 动态配置
用 kind: item | group 的单一数组描述侧栏顺序,移除 slice(0, 4) 硬编码切分。
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user