> **本文档集顺序**:① [prod.md](./prod.md) → ② **本页 spec** → ③ [task.md](./task.md)。请先读 prod;验收见 task。 # 侧栏导航 — 规格说明 ## 1. 概述与范围 本规格描述壳层侧栏的**技术契约**:数据树、受控展开、响应式侧栏、antd `Layout.Sider` / `Menu` 的组合,以及分组内多级由**自研子菜单层**承载(现网实现可拆为独立组件,迁移时可替换实现,语义须一致)。业务目标与用户叙事以 [prod.md](./prod.md) 为准。 - **依赖**:React、antd v6 目标栈下的 `Layout.Sider`、`Menu`(dark、inline)。 - **范围**:侧栏容器、菜单渲染、遮罩与自绘纵向滚动条;不含路由与子页实现。 - **非目标**:不复述 antd 通用 API 手册;只写与默认不一致或叠加的约定。 **写法**:以行为、谓词与可验收参数为主,不锁定现网内部 class/state/函数名;实现可重命名,须保持等价语义。下列 antd/React 对外概念(如 `openKeys`、`onBreakpoint`)保留。 --- ## 2. 数据模型 ### 菜单树(宿主传入) - 顶层为数组;每项为一级分组,由 antd `Menu.SubMenu` 承载,含: - 标识:`id`(优先)或 `key`,全树用于选中与展开(与 `openKeys`、`selectedKeys` 字符串化一致)。 - 展示名:`name`(作分组标题)。 - 子列表:`items`;其内为二级及以下节点。 - 分组内节点(任意深度): - 若仍有非空 `items` → 可展开父节点,由自研子菜单层渲染,可递归; - 否则为叶子(点击打开页)。 ### 运行态 - 当前选中键:与某节点标识一致,驱动 `selectedKeys`(与窗格 key 字符串一致)。 - 已打开窗格列表:用于叶子「已打开」样式;判定为某窗格 key 与叶子 `id` 一致(若项目统一用 `id`/`key` 之一,须全树一致)。 - collapsed / title:宿主驱动折叠;顶区文案。 可选:静态配置与接口合并、`hiddenPaths`、负数 id 等由宿主与数据层约定,侧栏以输入树已最终可用为前提。 --- ## 3. 布局与响应式 ### 根容器与 `Layout.Sider`(须可观测) 外层根容器高度随壳层占满;下列语义由内部 state、宿主 `collapsed`、`Layout.Sider` 的 breakpoint 与拖拽态共同决定(实现可用任意 class,须满足谓词): | 语义 | 谓词(逻辑合取) | |------|------------------| | **窄屏固定覆盖态** | `Sider` 的 breakpoint 已打破(现网为 `lg`,与 ≥992px 媒体自绘轨大致同量级);内部记录与 `onBreakpoint(true)` 一致 | | **窄屏且抽屉展开为全宽** | 窄屏固定覆盖态且宿主 `collapsed === false` | | **窄屏且收起不占全宽** | 窄屏固定覆盖态且 `collapsed === true`(侧栏收至零宽量级,主内容区占满) | | **自绘滚动条拖拽中** | 用户正在拖拽自绘滑块(与 §5 联动);根容器进入「滚动拖拽」语义,z-index 抬升 | | **宽屏侧栏并排** | 非窄屏固定覆盖态;侧栏为常规并排,不施加「全宽抽屉压在主内容上」 | 断点联动(顺序与契约): 1. `onBreakpoint(broken)` 触发时,须调用 `onSetMenuCollapse(broken)`,并使内部「窄屏固定覆盖」与 `broken` 同步。 2. 不得假设仅断点一侧变化:宿主 `collapsed` 与断点共同决定「全宽展开」是否出现。 #### 用户能感知到什么(断点) - 大屏:左侧固定宽度条,主内容在右。 - 屏变窄:进入固定覆盖语义;不展开时主内容占满。 - 主动展开才呈现全宽压在主内容之上;收起后再占满。 #### 典型操作时序(断点) 1. 由宽变窄 → 宿主与内部态随断点同步,常先收起,主区全宽。 2. 用户展开菜单 → 全宽抽屉 + 遮罩;点遮罩收起。 3. 再拉宽 → 恢复并排布局。 #### 实现要点(与根容器对照) - `onBreakpoint` → `onSetMenuCollapse(broken)` 与内部窄屏态同一布尔。 - 全宽抽屉仅当窄屏且未 collapsed。 - 拖拽滚动条时根容器抬升 z-index,遮罩近透明但仍可参与指针路由(见 §5),避免挡滑块。 ### 顶区与可滚内容区 - 顶区固定高度量级约 50px;宽度与下方菜单区一致,并随滚动条占位宽度加宽(与菜单可滚区同步测量)。 - 菜单区内容宽度约 210px 量级 + 占位;可视高度 + 容差(现网约 10px)< 内容高度时判定「需要纵向滚动」,驱动宽屏自绘轨显示(§5)。 - window resize 时重测滚动需求与滑块比例。 --- ## 4. 交互与状态(逻辑) ### `openKeys`(受控) - 两套来源写入同一状态:① antd 一级 `Menu` 的 `onOpenChange`(仅作用于一级分组 `SubMenu`);② 自研子菜单层的展开/收起回调(现网用独立回调名,避免与 `Menu` 内部拦截冲突)。 - 同级手风琴:当检测到新展开的 key 时,在树中求同级其他 key,从本次意图集合中剔除同级其他再合并;纯收起则按传入集合更新。 - 展开后滚动检测:须在动画/布局稳定后再次检测是否需纵向滚动;一级路径与自研路径延迟可不同(现网约 0.35s~0.5s 量级),须各自完成一次 `_checkScrolling` 等价逻辑,避免滑块高度为 0 或永久不可滚。 ### 选中 - `selectedKeys` 与当前选中键字符串形式一致。 - 一级分组:子孙中有选中 → 子树选中样式(现网为分组标题区可辨强调)。 - 自研子菜单层:选中在本节点与选中在子级两种标题样式可区分(见 §5)。 ### 点击叶子 - 非窄屏固定覆盖:立即 `onClickMenuItem(item)`(载荷为完整 item)。 - 窄屏固定覆盖态:先 `onSetMenuCollapse()`(或等价收起),约 300ms 后 `onClickMenuItem(item)`。 ### 自绘滚动条(逻辑) - 滑块高度与可视/内容高度成比例;滑块位移与 `scrollTop` 双向同步;支持鼠标与触摸(按下/移动/结束)。 - 内容滚动驱动滑块时仅更新位移,不重复写 `scrollTop`(避免抖动);拖拽时按位移比例写回 `scrollTop`。 --- ## 5. 自定义样式与动效 用户感知与操作时序全貌见 [prod.md](./prod.md) §2–§3。本节给状态、时长、层级与参数目标,实现可用任意样式方案,须可验收。 ### 遮罩 | 项目 | 约定 | |------|------| | 可见(半透明) | 窄屏且侧栏展开且非拖拽滚动条的典型态;背景透明度目标约 0.2(黑底) | | 近透明 | 自绘滚动条拖拽中:背景约完全透明,仍占据全屏命中区时需配合 z-index(见下)避免误点关闭 | | 点击 | 非拖拽滚动条时点击遮罩 → `onSetMenuCollapse()`(或等价);拖拽中点击不收拢 | 层级(目标关系):窄屏下固定覆盖容器整体 z-index 高于主内容;拖拽中侧栏区域须高于近透明遮罩,以免滑块被挡。现网量级:容器拖拽态约 50、遮罩约 10、侧栏内容区约 2(实现可调整,须保持相对关系)。 ### 自绘滚动条(宽屏媒体 + 内容溢出) 出现条件(合取):视口 ≥ 约 992px(与 `Sider` 的 `lg` 断点同量级)且判定内容超高(§3 容差规则)且进入「显示轨」状态。 #### 交互反馈阶梯(指针 / 命中域) | 阶梯 | 命中谓词(语义) | 视觉与过渡目标 | |------|------------------|----------------| | **外围上下文** | 宽屏且溢出,未 hover 可滚菜单区;轨可 opacity 0→1 渐入(轨整体可带 delay,如约 0.3s delay + 0.2s 过渡量级) | 轨上滑块「细、深」,低对比 | | **控件邻近带** | 指针进入可滚菜单区(`.c-menu-scroll-show` 等价语义),尚未指向滑块主热区 | 滑块条先变为中间强调色(如灰蓝),宽度可仍为窄条 | | **主操作面** | 指针落在滑块热区(轨内滑块可拖区域) | 滑块加宽(如约 6px→12px)、更亮;background / width 过渡约 0.3s | | **激活 / 拖拽中** | mousedown / touch 拖拽滑块 | 保持加宽与高亮;background-color 等 `transition: 0s`;遮罩按上表近透明 | 不需要滚动时:轨不可见且 `pointer-events: none`(或等价),不形成右侧死区;菜单区无为轨预留的额外右内边距。 需要滚动时:内容区保留纵向滚动(`overflow-y: scroll` 语义),右侧内边距(目标约 20px)为轨留位;轨宽约 20px,贴于可滚区右侧;滑块在轨内用 transform: translateY 与 `scrollTop` 同步。 与 JS 的边界:「是否溢出」「是否宽屏」由检测与媒体查询共同决定;拖拽中由 React state 切换根容器「拖拽」语义并联动遮罩样式。 ### 一级菜单与叶子(深色主题局部) - hover / selected 与叶子右侧 Caret 箭头:未打开时常隐藏或极弱,已打开叶子须可见可辨。 - 一级分组子树选中:分组标题区与默认态可区分。 ### 自研子菜单层 - 标题区箭头由两段线模拟,展开时旋转可感知。 - 子列表 max-height:收起向 0、展开向大值过渡;收起与展开 duration 可不同(现网约 0.2s 收起 / 0.3s 展开),缓动可用 antd 常用 cubic-bezier。 - 选中在本节点:标题背景接近「当前页」强调色;仅有子级选中:标题为弱强调(颜色/字重与上一档可辨)。 ### 可访问性与降级 - `prefers-reduced-motion`:产品未强制时可注明「当前不约束」;若实现,应同步缩短或关闭展开与轨渐入。 - 键盘焦点与自绘轨:当前不强制与原生滚动条等价,但须避免「轨不挡指针」与焦点环逻辑冲突。 --- ## 6. 与宿主应用的契约 | 概念 / 常见 prop 名 | 说明 | |---------------------|------| | 菜单树 | §2 | | 顶区标题 | `title` | | `collapsed` | 宿主驱动;与 `Sider` collapsed 绑定 | | 当前选中键 | `curActivePaneKey` → `selectedKeys` | | 已打开列表 | `panesOnShelf`;叶子「已打开」判定见 §2 | | `onSetMenuCollapse` | 断点传入 boolean;遮罩、窄屏点叶子前等可为无参收起;语义见 §3–§4 | | `onClickMenuItem` | 叶子;载荷为完整 item;窄屏时序见 §4 | --- ## 7. 验收 验收标准见 [task.md](./task.md)。