# 三级可折叠侧边栏菜单 — 论证与实施计划 ## Context HMS 健康管理平台的侧边栏菜单在"健康管理"分组下已有 18 个平铺菜单项。随着功能继续增长(AI 模块、专科模块等),这个问题会持续恶化。用户提出将菜单改为可折叠的 3 级目录结构。 ## 1. 论证分析 ### 1.1 当前问题 | 指标 | 现状 | 趋势 | |------|------|------| | 健康管理下菜单项 | 18 个 | 还会增加(AI、血透、OCR) | | 侧边栏可视区域 | ~600px 高度 | 固定 | | 单项高度 | ~40px | 固定 | | 满屏可显示项数 | ~15 个 | — | | 溢出 | 是,需滚动 | 严重恶化 | **结论:18 个平铺项已超出可视区域,必须滚动才能看到底部菜单。** 这违反了信息架构的基本原则——用户应在首屏看到完整的导航结构。 ### 1.2 方案对比 | 方案 | 描述 | 优点 | 缺点 | |------|------|------|------| | A. 维持现状 | 平铺 2 级菜单 | 简单、无改动 | 菜单项越多越难用,不可接受 | | B. 3 级可折叠 | 在 directory 下再嵌套 directory | 归类清晰、按需展开、**数据库已支持** | 前端需改造渲染逻辑 | | C. Tab 切换 | 顶部 Tab 分域(患者/预约/管理) | 分区明确 | 破坏侧边栏导航范式,改动大 | | D. 搜索导航 | 搜索框替代层级导航 | 适合超多菜单 | 学习成本高,不适合当前规模 | **推荐方案 B:3 级可折叠目录。** 核心理由: 1. **数据库零改动** — `menus` 表的 `parent_id` 自引用和 `build_tree()` 递归构建已支持 N 级嵌套 2. **前端改动小** — `DynamicMenuSection` 只需加递归渲染,`SidebarSubMenu` 的展开/折叠逻辑可直接复用 3. **向后兼容** — 没有子分组的 directory 仍按原有方式渲染(2 级),有子分组的自动变为 3 级 4. **可扩展** — 未来第 4 级也自然支持 ### 1.3 提出的分组结构 将"健康管理"下的 18 个菜单项按业务域归入 5 个子分组: ``` 健康管理 ├── 患者管理 (icon: TeamOutlined) │ ├── 患者列表 │ ├── 医护档案 │ └── 健康档案 ├── 预约排班 (icon: CalendarOutlined) │ ├── 预约管理 │ └── 排班管理 ├── 随访咨询 (icon: PhoneOutlined) │ ├── 随访管理 │ └── 咨询管理 ├── 健康数据 (icon: HeartOutlined) │ ├── 体征监测 │ ├── 化验报告 │ ├── 健康趋势 │ ├── 诊断记录 │ ├── 过敏管理 │ └── 血透记录 ├── 内容运营 (icon: FileTextOutlined) │ ├── 文章管理 │ ├── 分类管理 │ └── 标签管理 └── 综合管理 (icon: TrophyOutlined) ├── 积分管理 ├── 线下活动 └── 统计分析 ``` 其他顶级分组(仪表盘、系统管理、工作流、消息中心、AI 智能分析)保持不变。 ### 1.4 反对意见与回应 **反对:增加一级嵌套增加点击次数。** 回应:折叠状态下子分组只占一行(~40px),18 项从 ~720px 压缩到 ~200px。用户只需展开自己关注的子域,实际点击次数不会增加,反而因为分类清晰减少了寻找时间。 **反对:3 级菜单对用户认知负担更重。** 回应:当前 18 个平铺项的认知负担远大于 5 个分类名称。分组名称("患者管理"、"预约排班")本身是业务语义,不需要额外学习。 ## 2. 技术分析 ### 2.1 后端:零改动 - `menus` 表 `parent_id` 自引用已支持任意层级 - `build_tree()` 递归构建已生成完整嵌套 JSON - `MenuInfo.children` 前端类型已是递归结构 - **不需要修改任何 Rust 代码** ### 2.2 数据层:插入子分组记录 在 `menus` 表中为"健康管理"目录插入 6 个子 directory,然后将现有菜单项的 `parent_id` 指向对应子 directory。 ``` INSERT 新记录: 6 个 sub-directory (parent_id = 健康管理目录的 id) UPDATE 现有记录: 18 个菜单项的 parent_id → 对应子 directory 的 id ``` 可通过 SQL 迁移或直接 UPDATE 实现。 ### 2.3 前端改造:DynamicMenuSection 递归化 **当前代码**([MainLayout.tsx:199-256](apps/web/src/layouts/MainLayout.tsx#L199-L256)): - 遍历 `menus`,directory → 渲染标题 + 遍历 children - children 只渲染为 `SidebarMenuItem`(叶子节点),不再检查嵌套 **改造目标**: - directory 有 children → 检查 children 中是否有 sub-directory - 如果有 sub-directory → 用 `SidebarSubMenu` 的展开/折叠模式渲染 - 递归调用自身,支持任意深度 **复用的模式**:`SidebarSubMenu`([MainLayout.tsx:136-196](apps/web/src/layouts/MainLayout.tsx#L136-L196))的展开/折叠逻辑: - `useState(true)` 管理展开状态 - `RightOutlined` 箭头旋转动画 - 折叠状态下 Tooltip 显示子菜单列表 - `hasActive` 检测高亮 ### 2.4 关键文件 | 文件 | 改动 | |------|------| | `apps/web/src/layouts/MainLayout.tsx` | `DynamicMenuSection` 改为递归渲染 | | 数据库(SQL 迁移或直接 UPDATE) | 插入 6 个子 directory + 更新 18 项 parent_id | ## 3. 实施步骤 ### Step 1: 数据层 — 菜单重组 直接用 SQL 更新现有菜单数据(不需要新建迁移文件,因为这是数据变更不是 schema 变更): 1. 查出"健康管理"目录的 `id` 2. INSERT 6 个 sub-directory 记录(`menu_type = 'directory'`,`parent_id` 指向健康管理) 3. UPDATE 18 个菜单项的 `parent_id` 指向对应 sub-directory ### Step 2: 前端 — DynamicMenuSection 递归化 将 `DynamicMenuSection` 改为支持递归渲染: 1. 提取一个通用 `MenuNode` 组件,接收 `MenuInfo` 递归渲染 2. `menu_type === 'directory'` → 渲染分组标题 + 递归渲染 children 3. children 中如果是 directory → 用展开/折叠样式(复用 SidebarSubMenu 的箭头模式) 4. children 中如果是 menu → 渲染 `SidebarMenuItem` 5. 折叠侧边栏时:sub-directory 显示 Tooltip,首项可点击 ### Step 3: 样式微调 - sub-directory 标题增加左侧缩进(`padding-left: 24px`) - 展开动画过渡 - 活跃路径自动展开父级目录 ### Step 4: 验证 1. `pnpm build` 前端编译通过 2. 浏览器验证:侧边栏显示 3 级结构 3. 子分组可展开/折叠 4. 折叠侧边栏时 Tooltip 正确显示 5. 当前页面所在分组自动展开 6. 其他顶级分组(系统管理、工作流等)不受影响 ## 4. 工作量估计 | 步骤 | 预计时间 | |------|---------| | Step 1 数据重组 | 30 分钟 | | Step 2 前端递归化 | 1-2 小时 | | Step 3 样式微调 | 30 分钟 | | Step 4 验证 | 30 分钟 | | **总计** | **3-4 小时** | ## 5. 验证方式 - `pnpm build` 编译无错误 - 浏览器实际操作:展开/折叠/导航/折叠侧边栏 - 确认所有原有路由可正常访问 - 确认插件菜单不受影响