From 8c9d1776428f4f0c87a48b009d758370af729f9d Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 21 May 2026 08:36:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E4=B8=80=E7=BA=A7=E7=9B=AE=E5=BD=95=E5=88=86=E7=BB=84=E5=8F=AF?= =?UTF-8?q?=E6=8A=98=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 CollapsibleDirectoryGroup 组件,点击目录标题可展开/折叠子菜单, 默认展开,导航到子菜单时自动展开。侧边栏整体折叠时回落到图标模式。 Co-Authored-By: Claude Opus 4.7 --- apps/web/src/index.css | 31 ++++++ apps/web/src/layouts/MainLayout.tsx | 144 +++++++++++++++++++++------- 2 files changed, 140 insertions(+), 35 deletions(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index b2ed4e6..a430767 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -866,6 +866,37 @@ body { color: var(--erp-text-tertiary); } +/* Sidebar collapsible directory group */ +.erp-sidebar-group-toggle { + display: flex; + align-items: center; + padding: 16px 20px 6px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--erp-text-tertiary); + cursor: pointer; + user-select: none; +} + +.erp-sidebar-group-toggle:hover { + color: var(--erp-text-secondary); +} + +.erp-sidebar-group-arrow { + display: flex; + align-items: center; + margin-right: 6px; + font-size: 10px; +} + +.erp-sidebar-group-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + /* ==================================================================== * MainLayout — CSS classes replacing inline styles * ==================================================================== */ diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index bd9e0ef..fc5afe2 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -222,6 +222,108 @@ function isLeafType(menuType: string): boolean { return menuType === 'menu' || menuType === 'page'; } +// 一级目录分组(可折叠) +const CollapsibleDirectoryGroup = memo(function CollapsibleDirectoryGroup({ + directory, + collapsed, + currentPath, + onNavigate, +}: { + directory: MenuInfo; + collapsed: boolean; + currentPath: string; + onNavigate: (key: string) => void; +}) { + const [expanded, setExpanded] = useState(true); + const visibleChildren = directory.children?.filter((c) => c.visible !== false) || []; + const hasActive = visibleChildren.some((c) => { + if (c.menu_type === 'directory') { + return c.children?.some((gc) => currentPath === (gc.path || gc.id)); + } + return currentPath === (c.path || c.id); + }); + + useEffect(() => { + if (hasActive) setExpanded(true); // eslint-disable-line react-hooks/set-state-in-effect -- 导航到子菜单时自动展开分组 + }, [hasActive]); + + if (collapsed) { + return ( + <> + {visibleChildren.filter((c) => isLeafType(c.menu_type)).map((child) => ( + onNavigate(child.path || child.id)} + /> + ))} + + ); + } + + return ( +
+
setExpanded((e) => !e)} + > + + + + {directory.title} +
+
+
+ {visibleChildren.map((child) => { + if (child.menu_type === 'directory') { + return ( + + ); + } + if (isLeafType(child.menu_type)) { + return ( + onNavigate(child.path || child.id)} + /> + ); + } + return null; + })} +
+
+
+ ); +}); + // 动态菜单渲染(支持多级嵌套) const DynamicMenuSection = memo(function DynamicMenuSection({ menus, @@ -238,42 +340,14 @@ const DynamicMenuSection = memo(function DynamicMenuSection({ <> {menus.map((menu) => { if (menu.menu_type === 'directory') { - const visibleChildren = menu.children?.filter((c) => c.visible !== false) || []; return ( -
- {!collapsed &&
{menu.title}
} -
- {visibleChildren.map((child) => { - if (child.menu_type === 'directory') { - return ( - - ); - } - if (isLeafType(child.menu_type)) { - return ( - onNavigate(child.path || child.id)} - /> - ); - } - return null; - })} -
-
+ ); } if (isLeafType(menu.menu_type) && menu.visible !== false) {