AdmSysV2【公共组件库】@前端(For Git Submodule)
Tevin
2 days ago d633a6657fe3f83c2fc130f7d2690bb377a04960
feat(framework): 添加 CSideMenu 组件提案

- 新增侧边栏菜单组件设计文档
- 决策:antd Menu 三级嵌套、全局手风琴、移动端覆盖模式
- example 极简结构、vitest 测试放 test/unit/
- 文档从 public 迁移至 openspec/docs

Co-Authored-By: ClaudeCode
8 files added
565 ■■■■■ changed files
openspec/changes/implement-c-side-menu/.openspec.yaml 2 ●●●●● patch | view | raw | blame | history
openspec/changes/implement-c-side-menu/design.md 111 ●●●●● patch | view | raw | blame | history
openspec/changes/implement-c-side-menu/proposal.md 23 ●●●●● patch | view | raw | blame | history
openspec/changes/implement-c-side-menu/specs/side-menu/spec.md 28 ●●●●● patch | view | raw | blame | history
openspec/changes/implement-c-side-menu/tasks.md 50 ●●●●● patch | view | raw | blame | history
openspec/docs/old-refactors/side-menu/adr.md 89 ●●●●● patch | view | raw | blame | history
openspec/docs/old-refactors/side-menu/spec.md 207 ●●●●● patch | view | raw | blame | history
openspec/docs/old-refactors/side-menu/task.md 55 ●●●●● patch | view | raw | blame | history
openspec/changes/implement-c-side-menu/.openspec.yaml
New file
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-07
openspec/changes/implement-c-side-menu/design.md
New file
@@ -0,0 +1,111 @@
## Context
admin2-components 是管理后台组件库,位于 `src/framework/sideMenu/`。组件需要:
- 基于 antd@6 Menu 实现三级菜单(antd 原生支持内嵌三级)
- 实现全局手风琴展开(最多同时展开一个分支,避免滚动条过长)
- 响应式固定模式(窄屏下覆盖式抽屉 + 遮罩,仅移动端生效)
- 受控组件模式,菜单数据由外部注入
详细行为规范见 `openspec/docs/old-refactors/side-menu/` 下的 spec.md、adr.md、task.md。
## Goals / Non-Goals
**Goals:**
- 实现 P0:基础三级菜单 + 全局手风琴展开
- 实现 P1:响应式固定模式 + 遮罩
**Non-Goals:**
- P2 自定义滚动条、P3 动效细节留作后续迭代
- 不实现菜单数据请求(由宿主提供已合并的树结构)
- PC 端不启用覆盖模式(固定模式仅移动端生效)
## Decisions
### 1. 三级菜单采用 antd 原生实现
**决定**:利用 antd@6 Menu 的 `items` + `children` 嵌套结构,支持 `key > label > children > key > label > children` 三级结构。
**原因**:antd@6 已原生支持多级菜单展开,自定义第三级增加了不必要的复杂度。
### 2. 手风琴逻辑为全局互斥
**决定**:在 `onOpenChange` 中实现全局互斥——当用户展开一个新分支时,关闭之前展开的分支。
**原因**:避免多分支同时展开导致滚动条过长,影响导航效率。
**实现**:维护单一 `openKeys` 数组,展开时用新 key 替换数组。
### 3. 响应式固定模式仅移动端生效,使用样式自定义覆盖
**决定**:
- 移动端(断点 `< lg`):侧边栏以 `position: fixed` 悬浮覆盖在内容之上,配合半透明遮罩
- PC 端:侧边栏固定在内容左侧,不覆盖
**原因**:使用 Drawer 组件会增加复杂性,且 PC 端不需要覆盖效果。
**实现**:使用 antd Layout.Sider 的 `breakpoint` 检测断点,配合 CSS 媒体查询控制覆盖样式。
### 4. 组件 Props 设计
```typescript
interface CSideMenuProps {
  title: string;                          // 顶区标题
  tree: MenuTree;                         // 合并后的菜单树
  collapsed: boolean;                      // 是否收起
  curActivePaneKey?: string | number;    // 当前选中键
  panesOnShelf?: Array<{ key: string }>; // 已打开页签列表
  onClickMenuItem: (item: MenuItem) => void;
  onSetMenuCollapse: (collapsed: boolean | void) => void;
}
```
### 5. 统一组件导出入口
**决定**:创建 `src/index.ts` 作为组件库的统一导出入口。
**结构**:
```typescript
export { CSideMenu } from './framework/sideMenu/CSideMenu';
// 后续组件陆续添加
```
### 6. example 极简结构
**决定**:example 保持极简,仅作为组件演示用途。
**结构**:
```
example/
├── App.tsx          # 组件列表页
├── main.tsx         # 入口
├── index.css       # 全局样式
└── pages/
    └── side-menu/
        └── SideMenuPage.tsx  # 组件示例页
```
**原因**:极简结构减少维护成本,重点在组件本身,Playwright 测试也依赖此示例。
### 7. vitest 测试文件放在 test 文件夹
**决定**:组件的 vitest 测试文件放在 `test/unit/` 目录。
**结构**:
```
test/
├── setup.ts
└── unit/
    └── CSideMenu.test.tsx
```
## Risks / Trade-offs
| Risk | Mitigation |
|------|------------|
| 全局手风琴限制了多分支展开的灵活性 | 当前业务场景以导航效率优先,后续可扩展为可配置 |
| 移动端覆盖模式可能影响内容区交互 | 遮罩层 `z-index` 正确设置,确保点击遮罩可关闭侧栏 |
## Open Questions
无。所有决策已确认。
openspec/changes/implement-c-side-menu/proposal.md
New file
@@ -0,0 +1,23 @@
## Why
admin2-components 作为管理后台组件库,需要一个可折叠的三级侧边菜单组件,承担整站主导航职能。现有的 spec 设计文档(`openspec/docs/old-refactors/side-menu/`)已详细定义行为契约,需要转化为可实现的组件代码。
## What Changes
- 新增 `CSideMenu` 组件(`src/framework/sideMenu/`)
- 支持三级菜单嵌套(利用 antd@6 Menu 原生内嵌三级能力)
- 实现同级手风琴展开行为(同一父节点下仅一个展开分支)
- 实现响应式固定模式(窄屏下覆盖式抽屉 + 遮罩)
- 支持菜单数据由外部注入(受控组件模式)
## Capabilities
### New Capabilities
- `side-menu`:可折叠的三级侧边菜单组件,支持响应式布局、同级手风琴展开
## Impact
- 新增 `src/framework/sideMenu/` 组件目录
- 依赖 antd@6 Menu 组件
- 组件作为受控组件,宿主负责菜单数据管理与路由跳转
openspec/changes/implement-c-side-menu/specs/side-menu/spec.md
New file
@@ -0,0 +1,28 @@
## ADDED Requirements
### Requirement: 三级菜单树渲染
组件 SHALL 从注入的 `tree` 数据结构渲染三级菜单树。结构为 `key > label > children > key > label > children`。每个节点 SHALL 包含 `id`、`name`、可选 `type` 字段。当节点包含非空 `children` 时,SHALL 渲染为 SubMenu;否则为叶子项。
### Requirement: 全局手风琴展开
当用户展开一个节点时,组件 SHALL 关闭之前展开的节点。全局最多同时只有一个展开分支。
### Requirement: 响应式固定模式
当视口宽度低于 `lg` 断点时(移动端),侧边栏 SHALL 进入固定模式,以 `position: fixed` 悬浮覆盖在内容之上。当菜单展开时 SHALL 显示背景遮罩,点击遮罩 SHALL 触发收起。PC 端不启用覆盖模式。
### Requirement: 固定模式延迟导航
在固定模式下,当用户点击叶子项时,组件 SHALL 先触发收起(`onSetMenuCollapse(true)`),然后在约 300ms 延迟后调用 `onClickMenuItem`,以避免动画闪烁。
### Requirement: 菜单项点击回调
当用户点击叶子菜单项时,组件 SHALL 调用 `onClickMenuItem`,传入完整的菜单项对象,至少包含 `id`、`name`、`path`、`pageName` 字段。
### Requirement: 折叠状态回调
组件 SHALL 支持 `onSetMenuCollapse` 的两种调用模式:boolean 参数(设置精确折叠状态)和无参调用(切换)。宿主应用 SHALL 处理这两种模式。
### Requirement: 空树处理
当注入的 `tree` 为空或 undefined 时,组件 SHALL 渲染标题区域和空菜单区域,且不抛出错误。
### Requirement: 当前选中键高亮
当 `curActivePaneKey` 与菜单节点的 `id` 或 `key` 匹配时,该节点 SHALL 显示选中样式。当叶子项被选中时,其所属 SubMenu SHALL 显示子树选中样式。
### Requirement: 已打开标签指示器
当 `panesOnShelf` 中的某个条目的 `key` 与叶子节点的 `id` 匹配时,该叶子 SHALL 显示"已打开"指示器样式。
openspec/changes/implement-c-side-menu/tasks.md
New file
@@ -0,0 +1,50 @@
## 1. 项目初始化
- [ ] 1.1 创建 `src/framework/sideMenu/` 目录结构
- [ ] 1.2 创建类型定义文件 `sideMenuTypes.ts`(MenuTree、MenuItem、CSideMenuProps)
- [ ] 1.3 创建组件文件:`CSideMenu.tsx`、`cSideMenu.scss`
- [ ] 1.4 创建 `src/index.ts` 统一导出入口
## 2. example 基础结构
- [ ] 2.1 创建 `example/pages/side-menu/SideMenuPage.tsx` 组件示例页
- [ ] 2.2 在 `example/App.tsx` 中添加组件列表入口
## 3. 核心组件实现
- [ ] 3.1 实现 `CSideMenu` 组件框架,定义 props 接口
- [ ] 3.2 使用 antd Menu 的 `items` 属性实现三级菜单嵌套
- [ ] 3.3 实现 `tree` 到 antd `items` 的转换函数
- [ ] 3.4 处理 `type` 字段的图标映射(chart、setting 等,缺省为默认图标)
## 4. 手风琴行为
- [ ] 4.1 实现全局手风琴逻辑:在 `onOpenChange` 中用新 key 替换 `openKeys`
- [ ] 4.2 实现 `openKeys` 受控展开状态
## 5. 响应式固定模式
- [ ] 5.1 添加 antd Layout.Sider,设置 `breakpoint="lg"`
- [ ] 5.2 实现移动端覆盖样式(CSS 媒体查询)
- [ ] 5.3 实现遮罩层及其点击关闭逻辑
## 6. 交互与回调
- [ ] 6.1 实现叶子项点击 → `onClickMenuItem` 回调,传入完整菜单项对象
- [ ] 6.2 移动端点击叶子时:先 `onSetMenuCollapse(true)`,300ms 延迟后再调用导航回调
- [ ] 6.3 支持 `onSetMenuCollapse` 的 boolean 和无参两种调用模式
## 7. 选中与指示器
- [ ] 7.1 连接 `selectedKeys` 与 `curActivePaneKey`
- [ ] 7.2 SubMenu 下叶子被选中时应用子树选中样式
- [ ] 7.3 在 `panesOnShelf` 键匹配时显示"已打开"指示器
## 8. 空状态与类型处理
- [ ] 8.1 空树时渲染标题和空区域,不报错
- [ ] 8.2 支持 `id`/`key` 比较,包含数值类型键(string/number)的类型转换
## 9. 测试
- [ ] 9.1 创建 `test/unit/CSideMenu.test.tsx` 单元测试
openspec/docs/old-refactors/side-menu/adr.md
New file
@@ -0,0 +1,89 @@
# 侧栏菜单(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 契约调整 |
openspec/docs/old-refactors/side-menu/spec.md
New file
@@ -0,0 +1,207 @@
# 侧栏菜单(Side Menu)— 规格说明
## 1. 概述与范围
### 1.1 业务职能
- 本组件承担**整站(管理后台)的主导航**:在典型布局中**常驻于视口左侧**,是用户从任意业务页跳转到其它功能模块的**一级入口**(与顶栏、中部多标签内容区共同构成「壳」;侧栏负责「去哪」,内容区负责「看什么」)。
- 菜单项与**可打开的业务页**一一对应(具体路由、嵌入页、权限裁剪由宿主与数据层决定,见 §2、§6)。
- 窄屏下仍承担同一职能,仅**呈现形态**改为覆盖式抽屉(§3.2),不改变「主导航」定位。
### 1.2 职责(能力概要)
在壳布局中提供**可折叠的纵向导航**:展示多级菜单树,标识当前选中项与已打开页签,在窄屏下以「全宽抽屉式」呈现并支持遮罩关闭。
### 1.3 非目标
- 不在本组件内实现路由注册、权限过滤、菜单数据请求(由宿主提供树数据)。
- 不重复阐述 **antd@6** 已提供的通用能力(`Layout.Sider`、`Menu` 内联/深色主题等以官方文档为准)。
### 1.4 技术栈
实现目标:**React**,**antd@6**(自 v4.3 升级场景);SPEC 仅描述与壳子及自定义逻辑相关的约定。
---
## 2. 数据模型
### 2.1 菜单树(由宿主传入)
- 顶层为**分组**列表(一级),每项包含子列表 `items`(二级及以下)。
- 每个节点至少具备:
  - **标识**:`id`(优先)或 `key`,在整棵树中用于选中、展开状态;比较时按字符串语义相等即可。
  - **展示名**:`name`。
  - **可选类型**:`type`,用于图标映射;常见取值见 §2.3;未匹配时使用默认图标。
- **子节点**:若存在非空 `items`,则该节点为**可展开**的父节点;否则为**叶子**(可点击打开页)。
- **层级**:须支持**至少三级**(一级分组 → 二级可展开节点 → 叶子),以合并逻辑与侧栏子菜单的递归结构为准;§2.3 所述静态配置多为「分组 → 叶子」二级,运行时以合并后的 `tree` 为准。
### 2.2 宿主提供的运行态
- **当前选中页键**:与某叶子或节点的 `id`/`key` 对应,用于菜单选中高亮及「子树选中」样式。
- **已打开页签列表**:用于在叶子上显示「已打开」样式(例如某 `pane.key` 与节点 `id` 一致则标记)。
### 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`),以便宿主增加标签、拼接构建版本等查询参数等与既有开页逻辑一致。
---
## 3. 布局与响应式
### 3.1 常规(宽屏)
- 侧栏为固定**内容区宽度**的纵向区域(目标约 210px,含与滚动条占位相关的补偿时可由实现决定,但须避免内容被系统滚动条挤压错位)。
- 侧栏可处于**展开**或**收起**状态;收起宽度为 0(不占可视内容区),由宿主控制 `collapsed`。
### 3.2 断点与「固定」模式
- 当视口宽度低于约定断点(与 `lg` 量级一致)时,进入**固定(fixed)**布局模式:
  - 侧栏行为接近**覆盖在内容之上的抽屉**:展开时占满可视宽度(或产品约定的全宽表现),并与宿主协作设置 `collapsed`。
  - 宿主在断点变化时应被通知以同步折叠状态(例如 `onSetMenuCollapse(broken)` 语义)。
### 3.3 顶区标题
- 侧栏顶部展示站点/产品标题(由宿主传入);区域宽度与下方菜单内容区对齐(含滚动条占位策略一致)。
---
## 4. 交互与状态(逻辑)
### 4.1 一级分组
- 一级分组由 antd `SubMenu` 承载常规展示;若当前选中项落在该分组子树内,该分组须有**可区分的选中子树**样式(例如高亮父级)。
### 4.2 展开态(openKeys,受控扩展)
- **内层 Menu**:在 `onOpenChange` 中实现**同级手风琴**——用户**新展开**某节点时,**同一父级下**已展开的**兄弟**节点须关闭;用户主动收起时以传入的 `openKeys` 为准。(非 antd 默认,须自实现。)
- **自定义子菜单层**(三级等):展开某一 key 时从 `openKeys` 移除其**兄弟** key 再并入当前 key;关闭时仅移除该 key。展开/关闭后须在 DOM/动画稳定后**重新检测**是否需滚动条(允许短延迟,如 ~300–500ms)。
### 4.3 点击叶子
- **宽屏**:立即调用宿主的「打开页」回调。
- **固定模式**:先通知宿主**收起**侧栏,再在**短延迟**(约 300ms,与侧栏动画匹配)后调用「打开页」回调,避免动画与导航冲突。
### 4.4 滚动与自定义滚动条(逻辑)
- 当菜单内容高度超过可视区域超过约 10px 时,视为**需要纵向滚动**,并切换「需滚动」状态(供 §5 样式与 §4.5 遮罩联动)。
- 提供**自定义滚动条轨道与滑块**(非仅依赖系统滚动条):滑块高度与可视比例成正比;拖拽滑块与菜单 `scrollTop` **双向同步**;支持鼠标拖拽与触摸拖拽;拖拽期间由 React 状态标记**滚动拖拽中**(与 §5 动效、§4.5 遮罩一致)。
- 窗口 **resize** 时须更新滚动条占位宽度及滚动需求检测。
### 4.5 遮罩(逻辑)
- 在**固定模式且侧栏展开**时显示遮罩;点击遮罩应通知宿主收起(若在滚动拖拽中则不应因遮罩点击关闭)。
- 自定义滚动条拖拽过程中遮罩与 §5.3「滚动拖拽」、§5.2 遮罩层级一致;点击关闭语义以 §4.5 与 §5 联合为准。
---
## 5. 自定义样式与动效
本节描述**自研叠加层**(非 antd 默认 token 可完整表达)的视觉与动效契约;实现可用任意 class 命名,但须复现状态、时长与层级关系。
### 5.1 顶区标题条(Logo 区)
- 固定高度(目标约 50px)、全宽、**主色实底**、白字加粗;过长文案 **ellipsis**。
- 与下方滚动区纵向衔接,滚动区高度为「剩余视高」(如 `calc(100% - 顶区高度)`)。
### 5.2 固定模式与遮罩(非滚动拖拽)
- **固定定位**:侧栏容器覆盖视口;**全宽展开**时使用独立 class 将宽度拉满。
- **遮罩层**:默认隐藏;在固定模式且侧栏展开时显示,**半透明深色**(如 rgba 黑 ~0.2),铺满视口,**低于侧栏内容**的 z-index,点击触发收起(逻辑见 §4.5)。
### 5.3 自定义滚动条(宽屏,约 ≥992px)
**出现与布局**
- **不需要滚动**时:自绘轨道**不可见**(如 `opacity: 0`)且 **`pointer-events: none`**,避免挡操作。
- **需要滚动**时:菜单内容区增加**右侧内边距**(目标约 20px)为轨道留位;轨道贴侧栏**右缘**固定列宽(目标约 20px),**深底**与侧栏深色主题协调;轨道整体**淡入**可用 `transition`,并带 **delay**(与菜单展开/重排动画衔接,目标约 **0.3s 延迟 + 0.2s 过渡**量级)。
- 实际滚动仍由内容区 `overflow-y: scroll` 承担;自绘滑块仅**视觉与拖拽**同步 `scrollTop`。
**滑块(thumb)**
- 默认较**细**(目标宽约 6px)、居中偏右、**圆角**条、色值深于轨道;`background` / `width` / `margin` 变化带过渡(目标约 **0.3s**),hover 时**加宽、变色**(目标宽约 12px、更高对比浅色)。
- **滚动区域 hover**(非必须点在滑块上):thumb 可先进入**中间色**态,再与滑块自身 hover 的**最亮**态区分层次。
**拖拽中(与 §4.4 `scrollDraging` 等状态对应)**
- 容器进入「滚动拖拽」class 时:thumb 保持**展开宽度与高亮主色**,且对 **background(及必要时相关属性)关闭 transition(0s)**,避免跟手时出现颜色/过渡迟滞。
- 同时 **遮罩**若仍处于显示态:变为**全透明**,仍占位或可点,**提高 z-index**,使拖拽过程中交互意图与固定模式下的实心遮罩区分(与 §4.5 逻辑一致:不阻挡拖拽结束后的常规点击语义由实现统一)。
### 5.4 菜单深色主题上的局部覆盖(节选)
- 一级 `SubMenu` 标题在 active/open/子树选中等态下**背景提亮**(rgba 白低不透明度阶梯)。
- 叶子项:`hover` / `selected` 背景与右侧 **Caret** 箭头显隐、颜色变化带**短过渡**;**已打开**叶子的箭头常显,未打开则隐藏,逻辑见数据模型,样式与 §4 选中态一致。
### 5.5 可访问性与降级
- 若产品无要求,可暂不实现 `prefers-reduced-motion`;迁移时建议评估:至少保证**键盘可达性**不与自定义滚动条 `pointer-events` 冲突。
- 窄于 §5.3 断点时以系统滚动或全宽抽屉为主,自绘轨道可隐藏(与 §3.2 一致)。
---
## 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) |
---
## 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 业务职能(整站主导航与壳布局中的角色) |
openspec/docs/old-refactors/side-menu/task.md
New file
@@ -0,0 +1,55 @@
# 侧栏菜单(Side Menu)— 验收标准
验收项应对照 [spec.md](./spec.md);技术实现基于 **React + antd@6**,下列不重复验证 antd 默认组件能力(如 Menu 基础受控、`Sider` 常规折叠属性等),仅验证**本文档约定与叠加逻辑**。
**样本数据**:可使用与 SPEC §2.3、§2.4 一致的静态骨架 + 合并规则构造最小样例树;文档不绑定某仓库内具体文件路径。
---
## 数据与展示
- [ ] 空树不报错,侧栏可显示标题与空菜单区域。
- [ ] 使用 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 默认的同级互斥)
- [ ] 三级结构下,在 antd `Menu` 层展开二级某分支时,其**同级**其它已展开分支被关闭(spec §4.2 手风琴)。
- [ ] 三级自定义子菜单层:展开某一 key 时**兄弟** key 从 `openKeys` 移除;关闭时仅移除当前 key(spec §4.2)。
- [ ] 展开/关闭动画结束后,滚动区域「是否需要滚动」的判定会更新(允许短延迟)。
## 滚动与遮罩(逻辑)
- [ ] 内容超出阈值(约 10px)时出现自定义滚动条;滑块与 `scrollTop` 双向同步;触摸拖拽可用(spec §4.4)。
- [ ] `resize` 后滚动条占位与是否需要滚动判断正确。
- [ ] 固定模式且侧栏展开时显示遮罩;点遮罩触发收起;**滚动拖拽过程中**不因误点遮罩关闭(spec §4.5)。
## 自定义样式与动效(须对照 spec §5)
- [ ] **无需滚动**时自绘轨道不可见且 `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 §3.2)。
- [ ] `onSetMenuCollapse`:传入 **boolean** 时宿主折叠态与之一致;**无参**调用时符合 spec §6.2 约定的切换语义。
- [ ] 固定模式下点击叶子:先触发收起,再于约定延迟后调用「打开页」回调,顺序符合 spec §4.3。
- [ ] 点击叶子时,回调参数为**完整菜单项对象**,至少包含宿主开页所需字段(如 `id`、`name`、`path`、`pageName`),与宿主标签/开页逻辑的消费方式一致(spec §2.5)。
---
## 修订记录
| 日期 | 摘要 |
|------|------|
| 2026-04-07 | 自 spec §6 迁入;标注 antd@6 与基线不验范围 |
| 2026-04-07 | 对齐静态配置字段与壳回调验收 |
| 2026-04-07 | 去除具体文件路径 |
| 2026-04-07 | 宿主契约节号改为 §6.2;新增 §5 动效验收 |