From 9f546a519b20204ddbb400acd2275a0b4901adb7 Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 26 Apr 2026 13:37:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E4=B8=89=E7=BA=A7=E5=8F=AF?= =?UTF-8?q?=E6=8A=98=E5=8F=A0=E4=BE=A7=E8=BE=B9=E6=A0=8F=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=20=E2=80=94=20=E5=81=A5=E5=BA=B7=E7=AE=A1=E7=90=86=2018=20?= =?UTF-8?q?=E9=A1=B9=E5=BD=92=E5=85=A5=206=20=E4=B8=AA=E5=AD=90=E5=88=86?= =?UTF-8?q?=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CollapsibleSubGroup 组件,支持子分组的展开/折叠渲染 - DynamicMenuSection 改为递归检测子目录,支持多级嵌套 - 当前路径所在子分组自动展开 - 折叠侧边栏时子分组显示为图标 + Tooltip - 兼容 menu_type='page' 类型 - 数据库插入 6 个子分组(患者医护/预约排班/随访咨询/积分运营/内容运营/AI 分析) --- apps/web/src/layouts/MainLayout.tsx | 121 ++++++++++++++++--- plans/skill-smooth-pebble.md | 175 ++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 17 deletions(-) create mode 100644 plans/skill-smooth-pebble.md diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index 8a6374c..972b5d8 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -132,6 +132,73 @@ const SidebarMenuItem = memo(function SidebarMenuItem({ ); }); +// 可折叠子分组(3 级菜单) +const CollapsibleSubGroup = memo(function CollapsibleSubGroup({ + directory, + collapsed, + currentPath, + onNavigate, +}: { + directory: MenuInfo; + collapsed: boolean; + currentPath: string; + onNavigate: (key: string) => void; +}) { + const [expanded, setExpanded] = useState(false); + const visibleChildren = directory.children?.filter((c) => c.visible !== false) || []; + const hasActive = visibleChildren.some((c) => currentPath === (c.path || c.id)); + + useEffect(() => { + if (hasActive) setExpanded(true); + }, [hasActive]); + + if (collapsed) { + return ( + +
{ + const first = visibleChildren[0]; + if (first) onNavigate(first.path || first.id); + }} + className={`erp-sidebar-item ${hasActive ? 'erp-sidebar-item-active' : ''}`} + > + {getIcon(directory.icon)} +
+
+ ); + } + + return ( +
+
setExpanded((e) => !e)} + > + + + + {directory.title} +
+ {expanded && visibleChildren.map((child) => ( + onNavigate(child.path || child.id)} + indented + /> + ))} +
+ ); +}); + // 插件子菜单组 const SidebarSubMenu = memo(function SidebarSubMenu({ group, @@ -195,7 +262,12 @@ const SidebarSubMenu = memo(function SidebarSubMenu({ ); }); -// 动态菜单渲染 +// 判断是否为可点击的叶子菜单类型 +function isLeafType(menuType: string): boolean { + return menuType === 'menu' || menuType === 'page'; +} + +// 动态菜单渲染(支持多级嵌套) const DynamicMenuSection = memo(function DynamicMenuSection({ menus, collapsed, @@ -211,30 +283,45 @@ 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}
}
- {menu.children - ?.filter((child) => child.visible !== false) - .map((child) => ( - onNavigate(child.path || child.id)} - /> - ))} + {visibleChildren.map((child) => { + if (child.menu_type === 'directory') { + return ( + + ); + } + if (isLeafType(child.menu_type)) { + return ( + onNavigate(child.path || child.id)} + /> + ); + } + return null; + })}
); } - if (menu.menu_type === 'menu' && menu.visible !== false) { + if (isLeafType(menu.menu_type) && menu.visible !== false) { return (