# web-admin 侧栏非叶子目录 + 右侧叶子组 Tab 设计 **日期**:2026-10-04 **状态**:待审阅(实现前须产品确认本文) **交互 Demo**:`docs/demos/sidebar-group-leaf-tabs-demo.html` **范围**:`web-admin` 全站业务菜单导航。默认壳层为**单列树形侧栏**(`MenuTypeEnum.LEFT`),只展示非叶子;无独立一级竖向 Tab。不含小程序、不含后端菜单 API 变更。 ## 背景与目标 当前侧栏同时展示目录和叶子,内容区一次只挂一个页面(默认 `showWorkTab: false`)。用户希望: - 左侧树**只展示非叶子**; - 点击**最后一层非叶子**后,右侧用 **Tab 展示该层全部可见叶子**; - 换另一个最后一层非叶子时,**整组 Tab 替换**; - **默认只加载第一个(或 URL 已指向的)叶子**,点哪个 Tab 再加载哪个界面。 报销管理是典型三层(报销管理 → 基础配置 → 费用项目等)。CRM 为三层:CRM管理 → 产品/报价单/合同/客户分组 → 组内叶子;点击分组后右侧 Tab 只含该组可见叶子。 ## 已确认决策 | 项 | 决策 | |----|------| | 覆盖范围 | 整站业务菜单,不是只做报销 | | Tab 形态 | **整组固定 Tab**:不能单个关闭;换组整组替换 | | 刷新 | 以 URL 为准,停在当前叶子或隐藏页 | | 加载 | 默认只挂载当前路由页;点过的同组叶子 `keep-alive`;换组清空缓存 | | 一级下全是叶子 | 一级当作最后一层非叶子;Tab 只含有权限且未隐藏的叶子 | | 隐藏页(`isHide`) | 不进 Tab 条;内容区切到该页;**父叶子 Tab 保持选中** | | 未保存拦截 | 不做;与现有点侧栏换页一致 | | 实现路径 | **方案 1**:布局层组 Tab 条 + 现有单 `router-view` + `keep-alive`。不打开、不改造 WorkTab 打开记录 | | 单叶子组 | 不显示组 Tab 条,直接展示该叶子页 | | 中间目录 | 只展开/收起,不改右侧组 | ## 信息架构 ``` 侧栏(非叶子) 内容区 ┌─────────────────┐ ┌──────────────────────────────────┐ │ 报销管理 │ 展开 │ Tab: 费用项目 | 工作流(备用金) | … │ │ 基础配置 ● │ ──点击──► │ (仅当前/已访问叶子真正挂载) │ │ 备用金 │ │ 页面:费用项目管理 │ │ 报销 │ └──────────────────────────────────┘ │ CRM管理 │ 展开 │ │ │ 产品管理 ● │ ──点击──► (无 Tab,直接产品管理页) │ │ 报价单管理 ● │ ──点击──► Tab: 报价单管理 | 报价单审批 │ │ 合同管理 ● │ ──点击──► Tab: 合同管理 | 合同审批 | 调价函管理 │ │ 客户管理 ● │ ──点击──► (无 Tab,直接客户管理页) │ └─────────────────┘ ``` ### 可见菜单 参与侧栏和组 Tab 计算的节点必须同时满足: - 当前用户有权限; - `meta.isHide` 不为 true; - 非登录/403/404/门户首页等无业务组页面。 ### 非叶子 / 最后一层非叶子 在**过滤后的树**上: - **非叶子**:至少有一个可见子节点。 - **最后一层非叶子**:可见子节点里**没有目录**(子节点都是叶子),或为「混合节点」(见下)。 - **中间非叶子**:可见子节点里**至少还有一个非叶子**。点击只展开树,不 `push` 叶子、不换组。 ### 混合节点 可见子节点里**既有目录又有叶子**时:该节点标题点击打开**其直接叶子**组成的 Tab 组;展开箭头只负责展开/收起子目录。子目录仍留在侧栏,可再点进各自的组。否则这些叶子无法从侧栏进入。 ### 当前组 由当前路由向上找最近的「会打开 Tab 的节点」(最后一层非叶子或混合节点)。该节点的**直接可见叶子**= Tab 全集,顺序与菜单 `order` 一致。 隐藏页不进入全集。隐藏页通过 `meta.activePath`(没有则用 path 最长前缀匹配组内叶子)归属到某一个叶子,该叶子高亮。 首页、登录、错误页、无匹配组:不渲染组 Tab。 ## 交互 1. 点击最后一层非叶子 / 混合节点:若当前路由已属于该组(含其隐藏页),不打断 URL;否则 `router.push` 该组**第一个可见叶子**。 2. 点击组内 Tab:`router.push` 该叶子 path。已是当前叶子则不重复导航。 3. 换组:Tab 条换成新组;上一组 `keep-alive` 清空。 4. 进入隐藏页:Tab 条不变;父叶子高亮;内容为隐藏页。返回/提交成功仍由页面自己 `push` 回列表。 5. 从隐藏页点另一个叶子 Tab:直接切走,无确认框。 6. 浏览器刷新 / 书签:按 URL 恢复页面;侧栏展开到对应组并选中;画出整组 Tab;**只挂载 URL 这一页**(不必先经过组内第一个叶子)。 7. 直达无权限 URL:现有 403/跳转;不生成假 Tab。 ## 加载与缓存 当前 `ArtPageContent` 仅在 `meta.keepAlive === true` 时缓存,且 `showWorkTab === false` 时 `keepAliveMax` 为 1。组 Tab 需要覆盖这一策略: | 场景 | 行为 | |------|------| | 首次进入组 | 只创建当前路由组件 | | 同组点过的可见叶子 | 纳入 `keep-alive` `include`(**不要求**页面 `meta.keepAlive`) | | 换组 | 清空本组 include,卸载上一组叶子 | | 隐藏页 | 默认不纳入组 include;若该页自身 `keepAlive: true`(如用户详情)则仍按其 meta | | 上限 | `keepAliveMax` ≥ 当前组可见叶子数(本组同时缓存的叶子不超过组大小) | 组 Tab 的缓存与 WorkTab 的 `opened` 列表无关。`showWorkTab` 保持默认 false。 ## 视觉 - 组 Tab 在内容区顶部:顶栏/面包屑之下、业务页之上。 - Fresh Mint:对齐案例仓储「库存信息表」`.subnav`——白底描边圆角轨道(10px)+ 内嵌 8px 圆角项;选中为 `--primary-bg` 填充与 `--primary-dark` 字色;无关闭图标。不得使用案例 `#167c4a`。 - 侧栏宽度、一级竖 Tab、树区规则仍遵循 `docs/web-admin-ui-spec-fresh-mint.md` §3;本能力只改树节点过滤与内容区 Tab。 - 规范文档与 OpenSpec `ui-conventions` 实现时同步补「侧栏非叶子 + 组 Tab」,避免与「默认不展示 WorkTab」条文冲突:组 Tab **不是** WorkTab。 ## 架构(方案 1) ``` 菜单树 + 当前 route │ ▼ resolveLeafGroup(route, menu) → { groupId, groupNode, leaves[], activeLeafId } │ ├─► Sidebar:只渲染非叶子;高亮 groupNode └─► GroupLeafTabs:渲染 leaves[];点击 push │ ▼ ArtPageContent KeepAlive include = 当前组已访问叶子组件名 RouterView 仍只挂当前匹配页 ``` ### 建议落点(实现时可微调路径,职责不变) | 单元 | 职责 | |------|------| | `utils/router/leaf-group.ts`(名称可调整) | 纯函数:判非叶子、解析当前组、隐藏页归属、第一片叶子 path | | 侧栏(`art-sidebar-menu` / 双列子树 / PORTAL 子导航) | 过滤叶子;最后一层/混合节点点击走「进组」而不是进单个叶子 | | `GroupLeafTabs` 组件 | 固定 Tab 条,无关闭 | | `ArtPageContent` | 挂 Tab 条;按组维护 keep-alive include / max | | 隐藏路由 `meta.activePath` | 补齐常用表单/详情,避免高亮丢失 | | UI 规范 + OpenSpec | 记录交互,明确组 Tab ≠ WorkTab | 不改业务列表内部 CRUD;不改后端 `sys_menu` 结构(除非实现时发现个别 `activePath` 必须写在路由 meta,前端即可)。 ## 错误与边界 - 组内可见叶子数为 0:不画 Tab,走现有空菜单/无权限。 - 门户首页、全屏页(`isFullPage`):无组 Tab。 - 混合节点:Tab 仅含**该节点直接叶子**,不含孙节点。 - 角标:若叶子在侧栏被拿掉,原叶子角标改挂到其所属最后一层非叶子(或 Tab 项上)。实现时与现有 `menu-badge` 对齐,避免角标消失。 ## 验收 1. 报销 → 基础配置:三个 Tab,只加载第一个;点第二个才出页面;再点备用金,上一组卸载,换成申请/审批/发放。 2. 刷新停在当前叶子或隐藏表单 URL;Tab 条仍是该组;父叶子高亮。 3. CRM:点一级只出现有权限且未隐藏的叶子;进报价表单时「报价单管理」仍高亮;返回列表不丢组。 4. 无权限叶子不出现;直达无权限 URL 仍 403。 5. 同组来回切换已访问叶子不重复打首屏列表请求(网络面板可核对);换组后旧页请求不再因缓存而显示。 ## 非目标 - 不恢复可关闭、可积压的 WorkTab 历史页签。 - 不做全局表单脏检查。 - 不把隐藏页(表单/详情)做成弹窗。 - 不强制给 CRM/系统管理补二级目录(一级可当最后一层)。 - 不改小程序导航。