feat(web): 侧边栏一级目录分组可折叠

新增 CollapsibleDirectoryGroup 组件,点击目录标题可展开/折叠子菜单,
默认展开,导航到子菜单时自动展开。侧边栏整体折叠时回落到图标模式。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-21 08:36:21 +08:00
parent c1458b1e4b
commit 8c9d177642
2 changed files with 140 additions and 35 deletions

View File

@@ -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
* ==================================================================== */

View File

@@ -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) => (
<SidebarMenuItem
key={child.id}
item={{
key: child.path || child.id,
icon: getIcon(child.icon),
label: child.title,
}}
isActive={currentPath === (child.path || child.id)}
collapsed
onClick={() => onNavigate(child.path || child.id)}
/>
))}
</>
);
}
return (
<div>
<div
className="erp-sidebar-group-toggle"
onClick={() => setExpanded((e) => !e)}
>
<span className="erp-sidebar-group-arrow">
<RightOutlined
style={{ fontSize: 10, transition: 'transform 0.2s', transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)' }}
/>
</span>
<span className="erp-sidebar-group-label">{directory.title}</span>
</div>
<div
className="erp-sidebar-group-content"
style={{
maxHeight: expanded ? '2000px' : '0px',
overflow: 'hidden',
transition: 'max-height 0.25s ease',
}}
>
<div className="erp-sidebar-menu">
{visibleChildren.map((child) => {
if (child.menu_type === 'directory') {
return (
<CollapsibleSubGroup
key={child.id}
directory={child}
collapsed={collapsed}
currentPath={currentPath}
onNavigate={onNavigate}
/>
);
}
if (isLeafType(child.menu_type)) {
return (
<SidebarMenuItem
key={child.id}
item={{
key: child.path || child.id,
icon: getIcon(child.icon),
label: child.title,
}}
isActive={currentPath === (child.path || child.id)}
collapsed={collapsed}
onClick={() => onNavigate(child.path || child.id)}
/>
);
}
return null;
})}
</div>
</div>
</div>
);
});
// 动态菜单渲染(支持多级嵌套)
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 (
<div key={menu.id}>
{!collapsed && <div className="erp-sidebar-group">{menu.title}</div>}
<div className="erp-sidebar-menu">
{visibleChildren.map((child) => {
if (child.menu_type === 'directory') {
return (
<CollapsibleSubGroup
key={child.id}
directory={child}
collapsed={collapsed}
currentPath={currentPath}
onNavigate={onNavigate}
/>
);
}
if (isLeafType(child.menu_type)) {
return (
<SidebarMenuItem
key={child.id}
item={{
key: child.path || child.id,
icon: getIcon(child.icon),
label: child.title,
}}
isActive={currentPath === (child.path || child.id)}
collapsed={collapsed}
onClick={() => onNavigate(child.path || child.id)}
/>
);
}
return null;
})}
</div>
</div>
<CollapsibleDirectoryGroup
key={menu.id}
directory={menu}
collapsed={collapsed}
currentPath={currentPath}
onNavigate={onNavigate}
/>
);
}
if (isLeafType(menu.menu_type) && menu.visible !== false) {