feat(web): 侧边栏一级目录分组可折叠
新增 CollapsibleDirectoryGroup 组件,点击目录标题可展开/折叠子菜单, 默认展开,导航到子菜单时自动展开。侧边栏整体折叠时回落到图标模式。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -866,6 +866,37 @@ body {
|
|||||||
color: var(--erp-text-tertiary);
|
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
|
* MainLayout — CSS classes replacing inline styles
|
||||||
* ==================================================================== */
|
* ==================================================================== */
|
||||||
|
|||||||
@@ -222,6 +222,108 @@ function isLeafType(menuType: string): boolean {
|
|||||||
return menuType === 'menu' || menuType === 'page';
|
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({
|
const DynamicMenuSection = memo(function DynamicMenuSection({
|
||||||
menus,
|
menus,
|
||||||
@@ -238,42 +340,14 @@ const DynamicMenuSection = memo(function DynamicMenuSection({
|
|||||||
<>
|
<>
|
||||||
{menus.map((menu) => {
|
{menus.map((menu) => {
|
||||||
if (menu.menu_type === 'directory') {
|
if (menu.menu_type === 'directory') {
|
||||||
const visibleChildren = menu.children?.filter((c) => c.visible !== false) || [];
|
|
||||||
return (
|
return (
|
||||||
<div key={menu.id}>
|
<CollapsibleDirectoryGroup
|
||||||
{!collapsed && <div className="erp-sidebar-group">{menu.title}</div>}
|
key={menu.id}
|
||||||
<div className="erp-sidebar-menu">
|
directory={menu}
|
||||||
{visibleChildren.map((child) => {
|
collapsed={collapsed}
|
||||||
if (child.menu_type === 'directory') {
|
currentPath={currentPath}
|
||||||
return (
|
onNavigate={onNavigate}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isLeafType(menu.menu_type) && menu.visible !== false) {
|
if (isLeafType(menu.menu_type) && menu.visible !== false) {
|
||||||
|
|||||||
Reference in New Issue
Block a user