From 87308be0e679cc8c6b17aefcd8751e0b7a58188e Mon Sep 17 00:00:00 2001 From: Tevin <tingquanren@163.com> Date: Mon, 13 Apr 2026 11:06:30 +0800 Subject: [PATCH] refactor(side-menu): 移除旧的 ADR 文档并新增产品说明文档 --- /dev/null | 89 -------- openspec/docs/old-refactors/side-menu/task.md | 79 ++++--- openspec/docs/old-refactors/side-menu/spec.md | 250 +++++++++++-------------- openspec/docs/old-refactors/side-menu/prod.md | 142 ++++++++++++++ 4 files changed, 292 insertions(+), 268 deletions(-) diff --git a/openspec/docs/old-refactors/side-menu/adr.md b/openspec/docs/old-refactors/side-menu/adr.md deleted file mode 100644 index 82cd6b5..0000000 --- a/openspec/docs/old-refactors/side-menu/adr.md +++ /dev/null @@ -1,89 +0,0 @@ -# 侧栏菜单(Side Menu)— 架构 / 产品决策 - -**状态**:生效中 - -本文记录侧栏菜单相关**取舍**;行为细节以 `spec.md` 为准,**可勾选验收**以 `task.md` 为准。 - ---- - -### 同级手风琴式展开 - -- **状态**:生效 -- **上下文**:多级菜单若允许多个同级分支同时展开,信息密度高、滚动长,且与常见后台导航习惯不一致。 -- **决策**:在**同一父节点下**,同时仅保留一个展开子分支;用户显式收起时尊重其操作。 -- **后果**:需在 Ant Design `Menu` 的 `onOpenChange` 与自定义子菜单层**两处**保持一致语义,避免二级与三级行为分裂。SPEC §4.2。 - ---- - -### 断点以下采用固定全宽 + 遮罩 - -- **状态**:生效 -- **上下文**:窄屏下窄侧栏可读性与可点性差,且需让出主内容区。 -- **决策**:低于 `lg` 量级断点时侧栏改为覆盖式(fixed),展开时配合全宽类表现与遮罩,点击遮罩关闭。 -- **后果**:与宿主折叠状态强耦合;须通过回调同步。SPEC §3.2、§4.5。 - ---- - -### 固定模式下延迟导航 - -- **状态**:生效 -- **上下文**:收起动画与立即路由/开页同时进行时易出现闪烁或竞态。 -- **决策**:固定模式下先触发收起,再延迟约 300ms 调用「打开页」。 -- **后果**:实现须严格保证顺序;延迟与壳子动画时长应对齐。SPEC §4.3。 - ---- - -### 自定义滚动条而非纯系统滚动条 - -- **状态**:生效 -- **上下文**:深色主题侧栏与系统滚动条样式、宽度不一致时破坏视觉对齐;需统一轨道与滑块。 -- **决策**:在内容可滚动时使用自绘轨道/滑块,并与 `scrollTop` 同步;resize 时重算。 -- **后果**:实现复杂度高于原生滚动条;需处理拖拽与遮罩的交互冲突及多段动效。SPEC §4.4、§4.5、§5.3。 - ---- - -### 菜单数据由宿主注入 - -- **状态**:生效 -- **上下文**:权限、租户、动态路由等差异应在壳子或数据层处理。 -- **决策**:组件只消费树与运行态,不内置请求与策略。 -- **后果**:SPEC 必须写清数据字段与回调语义,否则对接易歧义。SPEC §2、§6。 - ---- - -### 静态路径骨架与接口权限数据分离 - -- **状态**:生效 -- **上下文**:常见做法是静态配置中的 `treePaths` 固定页面路径与分组,接口返回分组/条目做权限与展示控制,二者在宿主或数据层合并。 -- **决策**:侧栏组件不直接读取静态配置文件;仅展示宿主传入的**已合并** `tree`。路径与 `pageName` 的权威定义在数据层。 -- **后果**:「菜单从哪来」须在宿主或数据层单独说明;侧栏 TASK 以合并后数据验收。SPEC §2.3、§2.4。 - ---- - -### 侧栏外「按 pageName 找页」与多级树的潜在不一致(宿主债) - -- **状态**:生效(已知限制) -- **上下文**:若宿主提供「按 `pageName` 打开页」的辅助逻辑,且实现为仅在每个分组下**一层** `items` 上线性扫描,则一旦静态或合并后的树出现**嵌套二级以上的可点击页**,会找不到或行为错误。 -- **决策**:不在侧栏组件内解决;由宿主保证「按名开页」与真实树深度一致(必要时改为递归查找)。 -- **后果**:侧栏实现三级 UI 时,应提醒宿主团队校对同类辅助方法。与 SPEC §2.1 层级支持配套。 - ---- - -### `onSetMenuCollapse` 支持 boolean 与无参两种调用 - -- **状态**:生效 -- **上下文**:`Layout.Sider` 断点回调常传入 `broken`,顶栏按钮则多触发无参「切换」。 -- **决策**:宿主统一处理:`boolean` 时设为对应折叠态,无参时按约定切换或收起。 -- **后果**:侧栏须能触发上述两类调用,避免 antd@6 升级后只处理其中一种。SPEC §6.2。 - ---- - -## 修订记录 - -| 日期 | 摘要 | -|------|------| -| 2026-04-07 | 初稿 | -| 2026-04-07 | 明确验收以 task.md 为准 | -| 2026-04-07 | 补充路径骨架与 API 分离、折叠回调双形态、按 pageName 查找层级限制 | -| 2026-04-07 | 去除具体文件/类名,改为职责级描述 | -| 2026-04-07 | 节号随 SPEC §5 动效 / §6 契约调整 | diff --git a/openspec/docs/old-refactors/side-menu/prod.md b/openspec/docs/old-refactors/side-menu/prod.md new file mode 100644 index 0000000..c2a4978 --- /dev/null +++ b/openspec/docs/old-refactors/side-menu/prod.md @@ -0,0 +1,142 @@ +> **本文档集顺序**:① [prod.md](./prod.md)(产品与决策)→ ② [spec.md](./spec.md)(规格)→ ③ [task.md](./task.md)(验收)。编写与修订亦建议按此顺序。 + +# 侧栏导航(Side Menu)— 产品说明 + +## 1. 业务职能与要解决的问题 + +本组件是**管理后台壳层**的**左侧主导航**:在固定宽度侧栏内展示按业务模块分组的菜单树,并与「已打开的子页签」状态联动,使用户在多模块、多层级入口之间快速跳转;在视口变窄时仍以折叠 / 全宽覆盖等方式保持可用。 + +要解决的核心问题: + +- **树状模块结构**(含多级入口):保持当前位置可辨识(选中、父级高亮、已打开页签提示)。 +- **菜单可能超高**:仍能完整浏览(纵向滚动;在满足宽屏媒体条件时用与侧栏风格一致的自绘滚动条,避免原生条割裂视觉)。 +- **大屏与窄屏**:展开/收拢侧栏与点击打开子页的时序一致,避免动画未完成就切页。 + +**非目标**:子页内容、路由注册、权限与数据请求;本组件只消费宿主传入的树与状态。 + +主要使用者:登录后使用后台的运营/管理员等。 + +信息结构(便于与 spec 对齐): + +- **一级**:业务分组,由 antd `Menu.SubMenu` 承载(分组标题即模块名)。 +- **分组以内**:节点若仍有子列表,则由**自研子菜单层**递归展开(解决部分环境下嵌套 `SubMenu` 展示异常的问题);无子列表则为叶子,点击打开页。 + +--- + +## 2. 用户感知(人如何理解这个组件) + +### 可见状态 + +- **整体**:深色竖向导航;顶区为站点/产品标题;下方为可滚动菜单树。 +- **一级分组**:分组标题;若子树中有当前页,分组须有「子树选中」可辨样式。 +- **分组内仍含下级**:由自研子菜单层呈现,可展开/收起,箭头随展开在「V 形」与「展开」之间变化;选中落在本节点与选中落在子级时标题强调程度不同(本节点选中时更接近「当前页」色块)。 +- **叶子项**:类型图标与文案;若对应窗格已在「已打开」列表中,须有与未打开可区分的样式(如右侧小箭头仅在「已打开」时明显出现)。 +- **窄屏**:侧栏可变为盖住主内容的固定层;展开时可占满可视宽度;可配半透明遮罩,点遮罩收起(拖拽自绘滑块时遮罩语义见 spec §5)。 +- **菜单超高**:在宽屏媒体下,右侧出现细轨 + 滑块(非系统默认宽条外观);拖拽滑块时整壳层级与遮罩反馈与平时不同。 + +### 断点与「固定 / 覆盖」(补充详述) + +- **大屏**:主导航是左侧固定宽度竖条,主内容在右,菜单不「浮」在整页上。 +- **屏变窄**:侧栏进入固定覆盖语义;仅当用户展开时才以全宽抽屉压在主内容之上,收起后主内容再占满。 + +### 自定义滚动条(宽屏,与 `lg` 断点同量级 992px)(补充详述) + +- 一屏装不下时,在满足宽屏媒体且判定需要滚动后,右侧出现与深色侧栏一致的细条,而非突兀的系统条。 +- 指针移入可滚菜单区时滑块先略提亮,再移到滑块热区则变宽、变亮(两级 hover 可感知差异)。 +- 拖拽时列表跟手,滑块颜色立刻到位(关闭颜色过渡以免迟滞);若同时存在遮罩层,则近透明且不应误触关闭逻辑。 +- 不溢出时右侧无可点死区(轨不接收指针)。 + +### 表现行为 + +- 分组与自研子菜单的展开/收起有过渡(收起与展开时长可略有不同,约 0.2s~0.3s 量级);侧栏与内容区过渡与现网一致(约 0.3s 量级)。 +- 自绘轨在需要时渐入(可带 delay);拖拽时关闭颜色过渡以保证跟手。 + +### 交互反馈 + +- **点叶子**:大屏立即打开页;窄屏固定覆盖模式下先收侧栏,短延迟后再打开页。 +- **点遮罩**(非拖拽滚动条、且遮罩为可见半透明时):收起侧栏。 +- **拖拽自绘滑块**:内容与滑块联动;拖拽中遮罩与层级语义见 [spec.md](./spec.md) §5。 +- **resize**:重算是否需滚动条与滑块比例。 + +### 交互反馈阶梯(自绘滚动条,白话) + +从「离滚动条较远」到「正在拖拽」,用户应能感到至少三档:① 菜单区可滚但未对准轨/柄;② 对准轨或滑块热区,轨与滑块明显可交互;③ 按下拖拽,高对比、过渡关闭。详见 spec §5 表格化谓词,便于验收对齐。 + +--- + +## 3. 操作时序与流程 + +### 主路径(打开某叶子功能) + +1. 在树中找到目标叶子并点击。 +2. **大屏**(非固定覆盖):宿主立即收到「打开页」。 +3. **窄屏固定覆盖态**:先收起侧栏 → 约 300ms 后宿主收到「打开页」。 + +### 分支 + +- 手风琴:同一父级下新开一侧同级则收起其他同级展开(antd 一级 `SubMenu` 与自研子菜单层两条更新路径写入同一套 `openKeys` 语义须一致)。 +- 只收起不导航:点遮罩等 → 不收新页。 +- resize:可能在「要/不要自绘滚动条」「是否进入窄屏固定覆盖」间切换。 + +### 顺序约束 + +- 窄屏下须先收侧栏再打开页(延迟与动画对齐)。 +- 展开导致高度变化后,须在动画稍后再检测是否需滚动(避免算错高度);一级菜单与自研层回调后的延迟可略有不同,但均须完成一次可靠检测。 + +### 断点与覆盖(典型时序) + +1. 由宽变窄 → 宿主与内部态随 `breakpoint` 同步,常呈现收起、主区全宽。 +2. 用户展开菜单 → 固定覆盖 + 可配遮罩;点遮罩收起。 +3. 再拉宽 → 恢复左侧固定条与主内容并排。 + +### 自定义滚动条(典型时序) + +1. 列表超高 → 判定需滚动后,轨与滑块延时淡入。 +2. 移入菜单滚动区 → 滑块先中间强调色。 +3. 移到滑块热区 → 加宽、高亮。 +4. 拖拽 → 联动滚动;遮罩按 spec §5;滑块关闭颜色 transition。 +5. 松开 → 恢复 hover 与过渡。 +6. 不再溢出 → 轨隐藏且不挡指针,内边距恢复。 + +--- + +## 4. 产品决策与架构(逻辑与决策一体) + +### 为何需要自研子菜单层(分组内「仍有子列表」) + +- **上下文**:antd `Menu.SubMenu` 嵌套在部分移动端/窄视口下曾有多级展示异常。 +- **决策**:分组内凡仍有 `items` 的节点用自研子菜单层(外层列表项 + 内嵌 `Menu`);一级分组仍用 antd `SubMenu`。 +- **后果**:`openKeys` 同时服务两套展开来源;迁移 antd v6 须回归多级与触摸。 + +### 手风琴式同级展开 + +- **上下文**:纵向空间有限。 +- **决策**:新展开一项时,从展开集合中移除同级其他 key(基于树结构求同级)。 +- **后果**:非 antd 默认的「多同级全开记忆」;迁移勿擅自改成全开除非产品要求。 + +### 自绘纵向滚动条(宽屏媒体下且内容溢出) + +- **上下文**:原生条与深色侧栏不协调,且需与箭头、内边距对齐。 +- **决策**:在宽屏媒体且内容超出可视区(含容差阈值)时自绘轨与滑块,transform 移滑块并与 `scrollTop` 比例同步;不满足媒体条件时不依赖自绘轨(由布局内滚动或抽屉承担)。 +- **后果**:resize 与展开后须重算;拖拽与遮罩、z-index 协同见 spec §5。 + +### 遮罩 + +- **上下文**:窄屏覆盖需提示「壳外」并可点关;拖拽滚动条时不应误关。 +- **决策**:窄屏展开时半透明遮罩;拖拽滚动条时遮罩可仍占位但近透明,侧栏容器抬升 z-index 以免挡拖拽。 + +### 宿主侧「按页名找页」与深层树(已知边界) + +若宿主存在仅在分组下浅层扫描的辅助逻辑,而真实菜单深于两层,可能找不到叶子——须由宿主与数据层保证一致;详见 [adr.md](./adr.md) 相应条。 + +--- + +## 5. 对 spec 与 task 的指向 + +| 内容 | 落点 | +|------|------| +| 树字段、展开/选中/点击顺序 | [spec.md](./spec.md) §2–§4 | +| 根容器语义、断点、`Sider`、布尔组合 | spec §3 | +| 遮罩、自绘滚动条、子菜单动效 | spec §5 | +| 可测条目 | [task.md](./task.md) | +| 历史 ADR 式条目(可选对照) | [adr.md](./adr.md) | diff --git a/openspec/docs/old-refactors/side-menu/spec.md b/openspec/docs/old-refactors/side-menu/spec.md index 28aa707..8c0014f 100644 --- a/openspec/docs/old-refactors/side-menu/spec.md +++ b/openspec/docs/old-refactors/side-menu/spec.md @@ -1,207 +1,177 @@ -# 侧栏菜单(Side Menu)— 规格说明 +> **本文档集顺序**:① [prod.md](./prod.md) → ② **本页 spec** → ③ [task.md](./task.md)。请先读 prod;验收见 task。 + +# 侧栏导航 — 规格说明 ## 1. 概述与范围 -### 1.1 业务职能 +本规格描述壳层侧栏的**技术契约**:数据树、受控展开、响应式侧栏、antd `Layout.Sider` / `Menu` 的组合,以及分组内多级由**自研子菜单层**承载(现网实现可拆为独立组件,迁移时可替换实现,语义须一致)。业务目标与用户叙事以 [prod.md](./prod.md) 为准。 -- 本组件承担**整站(管理后台)的主导航**:在典型布局中**常驻于视口左侧**,是用户从任意业务页跳转到其它功能模块的**一级入口**(与顶栏、中部多标签内容区共同构成「壳」;侧栏负责「去哪」,内容区负责「看什么」)。 -- 菜单项与**可打开的业务页**一一对应(具体路由、嵌入页、权限裁剪由宿主与数据层决定,见 §2、§6)。 -- 窄屏下仍承担同一职能,仅**呈现形态**改为覆盖式抽屉(§3.2),不改变「主导航」定位。 +- **依赖**:React、antd v6 目标栈下的 `Layout.Sider`、`Menu`(dark、inline)。 +- **范围**:侧栏容器、菜单渲染、遮罩与自绘纵向滚动条;不含路由与子页实现。 +- **非目标**:不复述 antd 通用 API 手册;只写与默认不一致或叠加的约定。 -### 1.2 职责(能力概要) - -在壳布局中提供**可折叠的纵向导航**:展示多级菜单树,标识当前选中项与已打开页签,在窄屏下以「全宽抽屉式」呈现并支持遮罩关闭。 - -### 1.3 非目标 - -- 不在本组件内实现路由注册、权限过滤、菜单数据请求(由宿主提供树数据)。 -- 不重复阐述 **antd@6** 已提供的通用能力(`Layout.Sider`、`Menu` 内联/深色主题等以官方文档为准)。 - -### 1.4 技术栈 - -实现目标:**React**,**antd@6**(自 v4.3 升级场景);SPEC 仅描述与壳子及自定义逻辑相关的约定。 +**写法**:以行为、谓词与可验收参数为主,不锁定现网内部 class/state/函数名;实现可重命名,须保持等价语义。下列 antd/React 对外概念(如 `openKeys`、`onBreakpoint`)保留。 --- ## 2. 数据模型 -### 2.1 菜单树(由宿主传入) +### 菜单树(宿主传入) -- 顶层为**分组**列表(一级),每项包含子列表 `items`(二级及以下)。 -- 每个节点至少具备: - - **标识**:`id`(优先)或 `key`,在整棵树中用于选中、展开状态;比较时按字符串语义相等即可。 - - **展示名**:`name`。 - - **可选类型**:`type`,用于图标映射;常见取值见 §2.3;未匹配时使用默认图标。 -- **子节点**:若存在非空 `items`,则该节点为**可展开**的父节点;否则为**叶子**(可点击打开页)。 -- **层级**:须支持**至少三级**(一级分组 → 二级可展开节点 → 叶子),以合并逻辑与侧栏子菜单的递归结构为准;§2.3 所述静态配置多为「分组 → 叶子」二级,运行时以合并后的 `tree` 为准。 +- 顶层为数组;每项为一级分组,由 antd `Menu.SubMenu` 承载,含: + - 标识:`id`(优先)或 `key`,全树用于选中与展开(与 `openKeys`、`selectedKeys` 字符串化一致)。 + - 展示名:`name`(作分组标题)。 + - 子列表:`items`;其内为二级及以下节点。 +- 分组内节点(任意深度): + - 若仍有非空 `items` → 可展开父节点,由自研子菜单层渲染,可递归; + - 否则为叶子(点击打开页)。 -### 2.2 宿主提供的运行态 +### 运行态 -- **当前选中页键**:与某叶子或节点的 `id`/`key` 对应,用于菜单选中高亮及「子树选中」样式。 -- **已打开页签列表**:用于在叶子上显示「已打开」样式(例如某 `pane.key` 与节点 `id` 一致则标记)。 +- 当前选中键:与某节点标识一致,驱动 `selectedKeys`(与窗格 key 字符串一致)。 +- 已打开窗格列表:用于叶子「已打开」样式;判定为某窗格 key 与叶子 `id` 一致(若项目统一用 `id`/`key` 之一,须全树一致)。 +- collapsed / title:宿主驱动折叠;顶区文案。 -### 2.3 静态路径配置(数据结构约定) - -以下描述**静态路径骨架**的常见 JSON 形态,便于对齐字段语义(存放文件名与路径由项目自定)。 - -- 根对象可含 `projectName`(工程标识)、`treePaths`(分组数组)、`hiddenPaths`(**不在侧栏展示**的页面映射,供其它入口按 `pageName` 打开;侧栏可不渲染此项,但应知晓 `id` 可能为负数等特殊键,以免选中/标签逻辑异常)。 -- **`treePaths` 每一项(一级分组)**:`id`、`name`、`items`。 -- **`items` 内叶子(典型)**:`id`、`name`、`pageName`、`path`;可选 `type`(如 `chart`、`setting` 等)。若某节点再含非空嵌套 `items`,则为中间层,须按 §4.2 三级规则处理。 -- 合并后叶子可带业务/权限相关字段(由接口与宿主侧合并逻辑写入,如 `forbid`、`show` 在合并阶段已过滤);**侧栏只消费合并后的 `tree`**,不负责请求。 - -### 2.4 合并与权限(宿主职责,侧栏不实现) - -- 典型流程:接口返回的分组/条目与静态骨架在**宿主或数据层**合并,并按 `forbid`、`show` 等规则剔除不可见项(具体函数名与模块划分由项目自定)。 -- 合并后 `type` 缺省时常规范为 `'normal'`(与图标默认分支一致)。 -- 若沿用「静态配置 + 接口权限」形态,须在**宿主层**复现或等价实现合并;侧栏 SPEC 仍以「输入树已最终可用」为前提。 - -### 2.5 点击叶子时传递给宿主的载荷 - -- 「打开页」回调应传入**完整菜单项对象**(至少包含宿主开页所需字段,如 `id`、`name`、`path`、`pageName`),以便宿主增加标签、拼接构建版本等查询参数等与既有开页逻辑一致。 +可选:静态配置与接口合并、`hiddenPaths`、负数 id 等由宿主与数据层约定,侧栏以输入树已最终可用为前提。 --- ## 3. 布局与响应式 -### 3.1 常规(宽屏) +### 根容器与 `Layout.Sider`(须可观测) -- 侧栏为固定**内容区宽度**的纵向区域(目标约 210px,含与滚动条占位相关的补偿时可由实现决定,但须避免内容被系统滚动条挤压错位)。 -- 侧栏可处于**展开**或**收起**状态;收起宽度为 0(不占可视内容区),由宿主控制 `collapsed`。 +外层根容器高度随壳层占满;下列语义由内部 state、宿主 `collapsed`、`Layout.Sider` 的 breakpoint 与拖拽态共同决定(实现可用任意 class,须满足谓词): -### 3.2 断点与「固定」模式 +| 语义 | 谓词(逻辑合取) | +|------|------------------| +| **窄屏固定覆盖态** | `Sider` 的 breakpoint 已打破(现网为 `lg`,与 ≥992px 媒体自绘轨大致同量级);内部记录与 `onBreakpoint(true)` 一致 | +| **窄屏且抽屉展开为全宽** | 窄屏固定覆盖态且宿主 `collapsed === false` | +| **窄屏且收起不占全宽** | 窄屏固定覆盖态且 `collapsed === true`(侧栏收至零宽量级,主内容区占满) | +| **自绘滚动条拖拽中** | 用户正在拖拽自绘滑块(与 §5 联动);根容器进入「滚动拖拽」语义,z-index 抬升 | +| **宽屏侧栏并排** | 非窄屏固定覆盖态;侧栏为常规并排,不施加「全宽抽屉压在主内容上」 | -- 当视口宽度低于约定断点(与 `lg` 量级一致)时,进入**固定(fixed)**布局模式: - - 侧栏行为接近**覆盖在内容之上的抽屉**:展开时占满可视宽度(或产品约定的全宽表现),并与宿主协作设置 `collapsed`。 - - 宿主在断点变化时应被通知以同步折叠状态(例如 `onSetMenuCollapse(broken)` 语义)。 +断点联动(顺序与契约): -### 3.3 顶区标题 +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. 交互与状态(逻辑) -### 4.1 一级分组 +### `openKeys`(受控) -- 一级分组由 antd `SubMenu` 承载常规展示;若当前选中项落在该分组子树内,该分组须有**可区分的选中子树**样式(例如高亮父级)。 +- 两套来源写入同一状态:① antd 一级 `Menu` 的 `onOpenChange`(仅作用于一级分组 `SubMenu`);② 自研子菜单层的展开/收起回调(现网用独立回调名,避免与 `Menu` 内部拦截冲突)。 +- 同级手风琴:当检测到新展开的 key 时,在树中求同级其他 key,从本次意图集合中剔除同级其他再合并;纯收起则按传入集合更新。 +- 展开后滚动检测:须在动画/布局稳定后再次检测是否需纵向滚动;一级路径与自研路径延迟可不同(现网约 0.35s~0.5s 量级),须各自完成一次 `_checkScrolling` 等价逻辑,避免滑块高度为 0 或永久不可滚。 -### 4.2 展开态(openKeys,受控扩展) +### 选中 -- **内层 Menu**:在 `onOpenChange` 中实现**同级手风琴**——用户**新展开**某节点时,**同一父级下**已展开的**兄弟**节点须关闭;用户主动收起时以传入的 `openKeys` 为准。(非 antd 默认,须自实现。) -- **自定义子菜单层**(三级等):展开某一 key 时从 `openKeys` 移除其**兄弟** key 再并入当前 key;关闭时仅移除该 key。展开/关闭后须在 DOM/动画稳定后**重新检测**是否需滚动条(允许短延迟,如 ~300–500ms)。 +- `selectedKeys` 与当前选中键字符串形式一致。 +- 一级分组:子孙中有选中 → 子树选中样式(现网为分组标题区可辨强调)。 +- 自研子菜单层:选中在本节点与选中在子级两种标题样式可区分(见 §5)。 -### 4.3 点击叶子 +### 点击叶子 -- **宽屏**:立即调用宿主的「打开页」回调。 -- **固定模式**:先通知宿主**收起**侧栏,再在**短延迟**(约 300ms,与侧栏动画匹配)后调用「打开页」回调,避免动画与导航冲突。 +- 非窄屏固定覆盖:立即 `onClickMenuItem(item)`(载荷为完整 item)。 +- 窄屏固定覆盖态:先 `onSetMenuCollapse()`(或等价收起),约 300ms 后 `onClickMenuItem(item)`。 -### 4.4 滚动与自定义滚动条(逻辑) +### 自绘滚动条(逻辑) -- 当菜单内容高度超过可视区域超过约 10px 时,视为**需要纵向滚动**,并切换「需滚动」状态(供 §5 样式与 §4.5 遮罩联动)。 -- 提供**自定义滚动条轨道与滑块**(非仅依赖系统滚动条):滑块高度与可视比例成正比;拖拽滑块与菜单 `scrollTop` **双向同步**;支持鼠标拖拽与触摸拖拽;拖拽期间由 React 状态标记**滚动拖拽中**(与 §5 动效、§4.5 遮罩一致)。 -- 窗口 **resize** 时须更新滚动条占位宽度及滚动需求检测。 - -### 4.5 遮罩(逻辑) - -- 在**固定模式且侧栏展开**时显示遮罩;点击遮罩应通知宿主收起(若在滚动拖拽中则不应因遮罩点击关闭)。 -- 自定义滚动条拖拽过程中遮罩与 §5.3「滚动拖拽」、§5.2 遮罩层级一致;点击关闭语义以 §4.5 与 §5 联合为准。 +- 滑块高度与可视/内容高度成比例;滑块位移与 `scrollTop` 双向同步;支持鼠标与触摸(按下/移动/结束)。 +- 内容滚动驱动滑块时仅更新位移,不重复写 `scrollTop`(避免抖动);拖拽时按位移比例写回 `scrollTop`。 --- ## 5. 自定义样式与动效 -本节描述**自研叠加层**(非 antd 默认 token 可完整表达)的视觉与动效契约;实现可用任意 class 命名,但须复现状态、时长与层级关系。 +用户感知与操作时序全貌见 [prod.md](./prod.md) §2–§3。本节给状态、时长、层级与参数目标,实现可用任意样式方案,须可验收。 -### 5.1 顶区标题条(Logo 区) +### 遮罩 -- 固定高度(目标约 50px)、全宽、**主色实底**、白字加粗;过长文案 **ellipsis**。 -- 与下方滚动区纵向衔接,滚动区高度为「剩余视高」(如 `calc(100% - 顶区高度)`)。 +| 项目 | 约定 | +|------|------| +| 可见(半透明) | 窄屏且侧栏展开且非拖拽滚动条的典型态;背景透明度目标约 0.2(黑底) | +| 近透明 | 自绘滚动条拖拽中:背景约完全透明,仍占据全屏命中区时需配合 z-index(见下)避免误点关闭 | +| 点击 | 非拖拽滚动条时点击遮罩 → `onSetMenuCollapse()`(或等价);拖拽中点击不收拢 | -### 5.2 固定模式与遮罩(非滚动拖拽) +层级(目标关系):窄屏下固定覆盖容器整体 z-index 高于主内容;拖拽中侧栏区域须高于近透明遮罩,以免滑块被挡。现网量级:容器拖拽态约 50、遮罩约 10、侧栏内容区约 2(实现可调整,须保持相对关系)。 -- **固定定位**:侧栏容器覆盖视口;**全宽展开**时使用独立 class 将宽度拉满。 -- **遮罩层**:默认隐藏;在固定模式且侧栏展开时显示,**半透明深色**(如 rgba 黑 ~0.2),铺满视口,**低于侧栏内容**的 z-index,点击触发收起(逻辑见 §4.5)。 +### 自绘滚动条(宽屏媒体 + 内容溢出) -### 5.3 自定义滚动条(宽屏,约 ≥992px) +出现条件(合取):视口 ≥ 约 992px(与 `Sider` 的 `lg` 断点同量级)且判定内容超高(§3 容差规则)且进入「显示轨」状态。 -**出现与布局** +#### 交互反馈阶梯(指针 / 命中域) -- **不需要滚动**时:自绘轨道**不可见**(如 `opacity: 0`)且 **`pointer-events: none`**,避免挡操作。 -- **需要滚动**时:菜单内容区增加**右侧内边距**(目标约 20px)为轨道留位;轨道贴侧栏**右缘**固定列宽(目标约 20px),**深底**与侧栏深色主题协调;轨道整体**淡入**可用 `transition`,并带 **delay**(与菜单展开/重排动画衔接,目标约 **0.3s 延迟 + 0.2s 过渡**量级)。 -- 实际滚动仍由内容区 `overflow-y: scroll` 承担;自绘滑块仅**视觉与拖拽**同步 `scrollTop`。 +| 阶梯 | 命中谓词(语义) | 视觉与过渡目标 | +|------|------------------|----------------| +| **外围上下文** | 宽屏且溢出,未 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`;遮罩按上表近透明 | -**滑块(thumb)** +不需要滚动时:轨不可见且 `pointer-events: none`(或等价),不形成右侧死区;菜单区无为轨预留的额外右内边距。 -- 默认较**细**(目标宽约 6px)、居中偏右、**圆角**条、色值深于轨道;`background` / `width` / `margin` 变化带过渡(目标约 **0.3s**),hover 时**加宽、变色**(目标宽约 12px、更高对比浅色)。 -- **滚动区域 hover**(非必须点在滑块上):thumb 可先进入**中间色**态,再与滑块自身 hover 的**最亮**态区分层次。 +需要滚动时:内容区保留纵向滚动(`overflow-y: scroll` 语义),右侧内边距(目标约 20px)为轨留位;轨宽约 20px,贴于可滚区右侧;滑块在轨内用 transform: translateY 与 `scrollTop` 同步。 -**拖拽中(与 §4.4 `scrollDraging` 等状态对应)** +与 JS 的边界:「是否溢出」「是否宽屏」由检测与媒体查询共同决定;拖拽中由 React state 切换根容器「拖拽」语义并联动遮罩样式。 -- 容器进入「滚动拖拽」class 时:thumb 保持**展开宽度与高亮主色**,且对 **background(及必要时相关属性)关闭 transition(0s)**,避免跟手时出现颜色/过渡迟滞。 -- 同时 **遮罩**若仍处于显示态:变为**全透明**,仍占位或可点,**提高 z-index**,使拖拽过程中交互意图与固定模式下的实心遮罩区分(与 §4.5 逻辑一致:不阻挡拖拽结束后的常规点击语义由实现统一)。 +### 一级菜单与叶子(深色主题局部) -### 5.4 菜单深色主题上的局部覆盖(节选) +- hover / selected 与叶子右侧 Caret 箭头:未打开时常隐藏或极弱,已打开叶子须可见可辨。 +- 一级分组子树选中:分组标题区与默认态可区分。 -- 一级 `SubMenu` 标题在 active/open/子树选中等态下**背景提亮**(rgba 白低不透明度阶梯)。 -- 叶子项:`hover` / `selected` 背景与右侧 **Caret** 箭头显隐、颜色变化带**短过渡**;**已打开**叶子的箭头常显,未打开则隐藏,逻辑见数据模型,样式与 §4 选中态一致。 +### 自研子菜单层 -### 5.5 可访问性与降级 +- 标题区箭头由两段线模拟,展开时旋转可感知。 +- 子列表 max-height:收起向 0、展开向大值过渡;收起与展开 duration 可不同(现网约 0.2s 收起 / 0.3s 展开),缓动可用 antd 常用 cubic-bezier。 +- 选中在本节点:标题背景接近「当前页」强调色;仅有子级选中:标题为弱强调(颜色/字重与上一档可辨)。 -- 若产品无要求,可暂不实现 `prefers-reduced-motion`;迁移时建议评估:至少保证**键盘可达性**不与自定义滚动条 `pointer-events` 冲突。 -- 窄于 §5.3 断点时以系统滚动或全宽抽屉为主,自绘轨道可隐藏(与 §3.2 一致)。 +### 可访问性与降级 + +- `prefers-reduced-motion`:产品未强制时可注明「当前不约束」;若实现,应同步缩短或关闭展开与轨渐入。 +- 键盘焦点与自绘轨:当前不强制与原生滚动条等价,但须避免「轨不挡指针」与焦点环逻辑冲突。 --- ## 6. 与宿主应用的契约 -### 6.1 输入(概念) - -| 概念 | 说明 | -|------|------| -| 菜单树 | 见 §2.1~§2.4 | -| 标题 | 顶区展示文案(常与接口站点名同源;与静态配置里的 `projectName` 可能不同源) | -| 是否收起 | 宿主控制折叠 | -| 当前选中键 | 与节点 `id`/`key` 对齐;常见为**数值型菜单 id**(含 `hiddenPaths` 场景的负数 id) | -| 已打开页列表 | 用于叶子「已打开」样式;项上 `key` 与菜单项 `id` 一致 | - -### 6.2 输出(回调) - -| 回调语义 | 时机 | -|----------|------| -| 设置折叠 | 断点变化、用户点遮罩、叶子点击前(固定模式)等;参数可为 **boolean**(是否与断点「broken」对齐)或**无参**(切换/收起);宿主须兼容:**boolean 时设为对应折叠态,无参时按约定的切换语义处理**。 | -| 打开菜单项 | 用户点击叶子;载荷为 **§2.5** 菜单项对象;固定模式下在收起动画后再触发 | - -### 6.3 壳布局集成(建议 props 映射,命名可改) - -侧栏通常与主区域**兄弟**排列:一侧为侧栏,另一侧为 `Layout`(顶栏、多标签内容区等)。折叠状态应由宿主**单一数据源**驱动,并与顶栏等共享同一套折叠回调。 - -建议输入/输出与下列概念对齐(prop 名可重命名,语义须一致): - -| 侧栏侧(示例名) | 含义 | -|------------------|------| -| `title` | 顶区标题 | -| `tree` | 合并后的分组树 | -| `panesOnShelf` | 已打开页签列表(用于「已打开」样式) | -| `collapsed` | 是否收起 | -| `curActivePaneKey` | 当前选中页键 | -| `onClickMenuItem` | 打开/激活标签页 | -| `onSetMenuCollapse` | 折叠:支持 boolean 与无参(见 §6.2) | +| 概念 / 常见 prop 名 | 说明 | +|---------------------|------| +| 菜单树 | §2 | +| 顶区标题 | `title` | +| `collapsed` | 宿主驱动;与 `Sider` collapsed 绑定 | +| 当前选中键 | `curActivePaneKey` → `selectedKeys` | +| 已打开列表 | `panesOnShelf`;叶子「已打开」判定见 §2 | +| `onSetMenuCollapse` | 断点传入 boolean;遮罩、窄屏点叶子前等可为无参收起;语义见 §3–§4 | +| `onClickMenuItem` | 叶子;载荷为完整 item;窄屏时序见 §4 | --- ## 7. 验收 验收标准见 [task.md](./task.md)。 - ---- - -## 8. 修订记录 - -| 日期 | 摘要 | -|------|------| -| 2026-04-07 | 初稿,抽象侧栏菜单行为契约 | -| 2026-04-07 | 目标栈明确为 React + antd@6;验收迁至 task.md | -| 2026-04-07 | 对齐静态路径配置、壳集成与宿主合并/回调语义;去除具体仓库路径 | -| 2026-04-07 | 新增 §5 自定义样式与动效(滚动条/遮罩/Logo);宿主契约顺延为 §6 | -| 2026-04-07 | 新增 §1.1 业务职能(整站主导航与壳布局中的角色) | diff --git a/openspec/docs/old-refactors/side-menu/task.md b/openspec/docs/old-refactors/side-menu/task.md index 002ed7d..c9e5e96 100644 --- a/openspec/docs/old-refactors/side-menu/task.md +++ b/openspec/docs/old-refactors/side-menu/task.md @@ -1,55 +1,56 @@ -# 侧栏菜单(Side Menu)— 验收标准 +> **本文档集顺序**:① [prod.md](./prod.md) → ② [spec.md](./spec.md) → ③ **本页 task**。依据与条目应对齐 prod 与 spec。 -验收项应对照 [spec.md](./spec.md);技术实现基于 **React + antd@6**,下列不重复验证 antd 默认组件能力(如 Menu 基础受控、`Sider` 常规折叠属性等),仅验证**本文档约定与叠加逻辑**。 +# 侧栏导航 — 验收标准 -**样本数据**:可使用与 SPEC §2.3、§2.4 一致的静态骨架 + 合并规则构造最小样例树;文档不绑定某仓库内具体文件路径。 +在 **antd@6** 目标栈下可执行(手工或 E2E)。参数与语义以 [spec.md](./spec.md) §5 为准;不要求与现网 class 名一致。 ---- +## 数据与结构 -## 数据与展示 +- [ ] 顶层 `tree` 非法组合不渲染错误结构;有 `items` 时一级分组标题与 `name` 一致。 +- [ ] 叶子标识与菜单 `key`/`selectedKeys` 一致;`curActivePaneKey` 命中叶子时该叶为 **selected**。 +- [ ] 分组下任意深度有选中时,该一级分组具 **子树选中** 可辨样式。 -- [ ] 空树不报错,侧栏可显示标题与空菜单区域。 -- [ ] 使用 SPEC 约定字段(`id`、`name`、`pageName`、`path`、可选 `type`)时,分组名与叶子文案展示正确。 -- [ ] `type` 为 `chart` / `setting` 等与 spec 约定映射一致;缺省或 `normal` 时走默认图标分支。 -- [ ] `id` 与 `key` 混用时,选中与展开比较均正确(字符串语义一致);**数值型** `curActivePaneKey`(含负数 id 场景)与 Menu `selectedKeys` 类型转换正确。 -- [ ] 当前选中在深层叶子时,对应一级分组呈现「子树选中」样式(见 spec §4.1)。 -- [ ] 已打开页签对应的叶子显示「已打开」样式(`pane.key ===` 菜单项 `id`,见 spec §2.2)。 +## 响应式与壳层(根容器组合) -## 展开行为(超出 antd 默认的同级互斥) +- [ ] 跨越 `Sider` **breakpoint**(如 lg)时宿主收到 `onSetMenuCollapse(broken)`,且界面「**窄屏固定覆盖**」与 broken 一致。 +- [ ] **窄屏**且 `collapsed === false` 时呈现 **全宽抽屉** 语义;窄屏且 `collapsed === true` 时不呈现全宽展开。 +- [ ] 窄屏且展开时遮罩为 **可见半透明**(约 0.2 透明度量级,可肉眼对照 spec §5);点遮罩收起。 +- [ ] 由窄回宽后无永久遮罩挡主内容操作。 -- [ ] 三级结构下,在 antd `Menu` 层展开二级某分支时,其**同级**其它已展开分支被关闭(spec §4.2 手风琴)。 -- [ ] 三级自定义子菜单层:展开某一 key 时**兄弟** key 从 `openKeys` 移除;关闭时仅移除当前 key(spec §4.2)。 -- [ ] 展开/关闭动画结束后,滚动区域「是否需要滚动」的判定会更新(允许短延迟)。 +## 展开与手风琴 -## 滚动与遮罩(逻辑) +- [ ] 同父级下连续展开两个一级 `SubMenu`,最终仅保留新展开项(同级其他被收起)。 +- [ ] **自研子菜单**路径:展开一节点时同级另一节点从 `openKeys` 中移除。 +- [ ] 展开/收起后,在约 0.35s~0.5s 内完成高度与滚动检测;内容足够高时无「永久无法滚动」或「滑块高度为 0」。 -- [ ] 内容超出阈值(约 10px)时出现自定义滚动条;滑块与 `scrollTop` 双向同步;触摸拖拽可用(spec §4.4)。 -- [ ] `resize` 后滚动条占位与是否需要滚动判断正确。 -- [ ] 固定模式且侧栏展开时显示遮罩;点遮罩触发收起;**滚动拖拽过程中**不因误点遮罩关闭(spec §4.5)。 +## 点击与宿主回调 -## 自定义样式与动效(须对照 spec §5) +- [ ] **非窄屏固定覆盖**:点叶子立即 `onClickMenuItem(item)`。 +- [ ] **窄屏固定覆盖**:先触发收起,约 300ms 后 `onClickMenuItem(item)`。 +- [ ] **点遮罩**(非拖拽滚动条、且为可见半透明遮罩):触发收拢。 -- [ ] **无需滚动**时自绘轨道不可见且 `pointer-events` 不阻挡菜单操作;**需要滚动**时轨道在约定 **delay + duration** 后出现(spec §5.3)。 -- [ ] 出现滚动条时菜单区有约定 **padding-right**,一级 `SubMenu` 与自定义子菜单的箭头位置与 spec §5.3 一致(不与轨道重叠)。 -- [ ] 滑块默认细、hover **加宽与变色**有过渡(约 0.3s);**滚动拖拽**态下滑块保持展开态且 **background 等关闭 transition(0s)**(spec §5.3)。 -- [ ] **滚动拖拽**时遮罩呈全透明、容器 **z-index** 与 §5.3 一致,与固定模式下半透明遮罩可区分。 -- [ ] 顶区标题条高度、主色底、ellipsis 符合 spec §5.1。 -- [ ] 固定模式全宽 class 与遮罩 rgba 层级符合 spec §5.2。 +## 自绘滚动条 — 出现与同步(spec §5) -## 宿主契约与窄屏 +- [ ] 仅当视口 ≥ 约 992px 且内容超高(含容差)时出现轨与滑块;否则轨不可见且不形成右侧可点死区。 +- [ ] 拖拽时 `scrollTop` 与滑块位移比例一致;松手后滚动与滑块仍同步。 +- [ ] **拖拽中**:遮罩 **近透明**;点遮罩/遮罩区域不收拢侧栏(与 spec §5 一致)。 +- [ ] **非拖拽**且窄屏展开:**半透明遮罩**可点收拢。 -- [ ] 低于约定断点时进入固定模式;断点变化时宿主收到折叠同步(spec §3.2)。 -- [ ] `onSetMenuCollapse`:传入 **boolean** 时宿主折叠态与之一致;**无参**调用时符合 spec §6.2 约定的切换语义。 -- [ ] 固定模式下点击叶子:先触发收起,再于约定延迟后调用「打开页」回调,顺序符合 spec §4.3。 -- [ ] 点击叶子时,回调参数为**完整菜单项对象**,至少包含宿主开页所需字段(如 `id`、`name`、`path`、`pageName`),与宿主标签/开页逻辑的消费方式一致(spec §2.5)。 +## 自绘滚动条 — 交互反馈阶梯(spec §5 表格) ---- +- [ ] 外围 → 邻近:先进入可滚菜单区,滑块条较默认态明显提亮(中间色),宽度可仍为窄条。 +- [ ] 邻近 → 主操作面:指针落在滑块热区,滑块加宽(如约 6px→12px)且更亮,过渡约 0.3s 量级。 +- [ ] 拖拽(激活):滑块保持加宽高亮;`background-color` 等 `transition` 为 0,无跟手迟滞。 -## 修订记录 +## 自研子菜单 -| 日期 | 摘要 | -|------|------| -| 2026-04-07 | 自 spec §6 迁入;标注 antd@6 与基线不验范围 | -| 2026-04-07 | 对齐静态配置字段与壳回调验收 | -| 2026-04-07 | 去除具体文件路径 | -| 2026-04-07 | 宿主契约节号改为 §6.2;新增 §5 动效验收 | +- [ ] 展开时箭头旋转与 max-height 过渡可感知;收起与展开时长可不同,无闪断。 +- [ ] 选中在本节点与仅有子级选中时标题样式可区分。 + +## 已打开窗格 + +- [ ] `panesOnShelf` 中存在 `pane.key` 与叶子 `id` 一致的项时,该叶具 **已打开** 可辨样式,**Caret** 规则符合 spec §5。 + +## 回归(v6) + +- [ ] 窄视口/触摸下分组内多级仍可展开并点选叶子。 -- Gitblit v1.9.1