From 660cae2fbb5dcdfc23df8d13b738ff445fa80d03 Mon Sep 17 00:00:00 2001
From: Tevin <tingquanren@163.com>
Date: Mon, 13 Apr 2026 12:20:13 +0800
Subject: [PATCH] docs: 归档 implement-c-side-menu 提案并提升规格到主目录
---
openspec/changes/archive/2026-04-13-implement-c-side-menu/specs/side-menu/spec.md | 28 ++++
openspec/changes/archive/2026-04-13-implement-c-side-menu/.openspec.yaml | 2
openspec/changes/archive/2026-04-13-implement-c-side-menu/tasks.md | 50 ++++++++
openspec/specs/side-menu/spec.md | 91 +++++++++++++++
openspec/changes/archive/2026-04-13-implement-c-side-menu/proposal.md | 23 +++
openspec/changes/archive/2026-04-13-implement-c-side-menu/design.md | 111 ++++++++++++++++++
6 files changed, 305 insertions(+), 0 deletions(-)
diff --git a/openspec/changes/archive/2026-04-13-implement-c-side-menu/.openspec.yaml b/openspec/changes/archive/2026-04-13-implement-c-side-menu/.openspec.yaml
new file mode 100644
index 0000000..2fe001e
--- /dev/null
+++ b/openspec/changes/archive/2026-04-13-implement-c-side-menu/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-07
diff --git a/openspec/changes/archive/2026-04-13-implement-c-side-menu/design.md b/openspec/changes/archive/2026-04-13-implement-c-side-menu/design.md
new file mode 100644
index 0000000..0746b03
--- /dev/null
+++ b/openspec/changes/archive/2026-04-13-implement-c-side-menu/design.md
@@ -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
+
+无。所有决策已确认。
diff --git a/openspec/changes/archive/2026-04-13-implement-c-side-menu/proposal.md b/openspec/changes/archive/2026-04-13-implement-c-side-menu/proposal.md
new file mode 100644
index 0000000..fc44049
--- /dev/null
+++ b/openspec/changes/archive/2026-04-13-implement-c-side-menu/proposal.md
@@ -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 组件
+- 组件作为受控组件,宿主负责菜单数据管理与路由跳转
diff --git a/openspec/changes/archive/2026-04-13-implement-c-side-menu/specs/side-menu/spec.md b/openspec/changes/archive/2026-04-13-implement-c-side-menu/specs/side-menu/spec.md
new file mode 100644
index 0000000..92f37df
--- /dev/null
+++ b/openspec/changes/archive/2026-04-13-implement-c-side-menu/specs/side-menu/spec.md
@@ -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 显示"已打开"指示器样式。
diff --git a/openspec/changes/archive/2026-04-13-implement-c-side-menu/tasks.md b/openspec/changes/archive/2026-04-13-implement-c-side-menu/tasks.md
new file mode 100644
index 0000000..a78ca1b
--- /dev/null
+++ b/openspec/changes/archive/2026-04-13-implement-c-side-menu/tasks.md
@@ -0,0 +1,50 @@
+## 1. 项目初始化
+
+- [x] 1.1 创建 `src/framework/sideMenu/` 目录结构
+- [x] 1.2 创建类型定义文件 `sideMenuTypes.ts`(MenuTree、MenuItem、CSideMenuProps)
+- [x] 1.3 创建组件文件:`CSideMenu.tsx`、`cSideMenu.scss`
+- [x] 1.4 创建 `src/index.ts` 统一导出入口
+
+## 2. example 基础结构
+
+- [x] 2.1 创建 `example/pages/side-menu/SideMenuPage.tsx` 组件示例页
+- [x] 2.2 在 `example/App.tsx` 中添加组件列表入口
+
+## 3. 核心组件实现
+
+- [x] 3.1 实现 `CSideMenu` 组件框架,定义 props 接口
+- [x] 3.2 使用 antd Menu 的 `items` 属性实现三级菜单嵌套
+- [x] 3.3 实现 `tree` 到 antd `items` 的转换函数
+- [x] 3.4 处理 `type` 字段的图标映射(chart、setting 等,缺省为默认图标)
+
+## 4. 手风琴行为
+
+- [x] 4.1 实现全局手风琴逻辑:在 `onOpenChange` 中用新 key 替换 `openKeys`
+- [x] 4.2 实现 `openKeys` 受控展开状态
+
+## 5. 响应式固定模式
+
+- [x] 5.1 添加 antd Layout.Sider,设置 `breakpoint="lg"`
+- [x] 5.2 实现移动端覆盖样式(CSS 媒体查询)
+- [x] 5.3 实现遮罩层及其点击关闭逻辑
+
+## 6. 交互与回调
+
+- [x] 6.1 实现叶子项点击 → `onClickMenuItem` 回调,传入完整菜单项对象
+- [x] 6.2 移动端点击叶子时:先 `onSetMenuCollapse(true)`,300ms 延迟后再调用导航回调
+- [x] 6.3 支持 `onSetMenuCollapse` 的 boolean 和无参两种调用模式
+
+## 7. 选中与指示器
+
+- [x] 7.1 连接 `selectedKeys` 与 `curActivePaneKey`
+- [x] 7.2 SubMenu 下叶子被选中时应用子树选中样式
+- [x] 7.3 在 `panesOnShelf` 键匹配时显示"已打开"指示器
+
+## 8. 空状态与类型处理
+
+- [x] 8.1 空树时渲染标题和空区域,不报错
+- [x] 8.2 支持 `id`/`key` 比较,包含数值类型键(string/number)的类型转换
+
+## 9. 测试
+
+- [x] 9.1 创建 `test/unit/CSideMenu.test.tsx` 单元测试
diff --git a/openspec/specs/side-menu/spec.md b/openspec/specs/side-menu/spec.md
new file mode 100644
index 0000000..d60069f
--- /dev/null
+++ b/openspec/specs/side-menu/spec.md
@@ -0,0 +1,91 @@
+# CSideMenu 组件规格说明
+
+## 1. 概述与范围
+
+本规格描述 `CSideMenu` 组件的技术契约:数据树、三级菜单、全局手风琴、响应式侧栏、antd `Layout.Sider` / `Menu` 的组合。
+
+- **依赖**:React、antd v6、`@ant-design/icons`
+- **范围**:侧栏容器、菜单渲染、遮罩
+- **非目标**:路由与子页实现、自定义滚动条
+
+---
+
+## 2. 数据模型
+
+### MenuTree(宿主传入)
+
+```typescript
+interface MenuItem {
+ key: string | number; // 唯一标识
+ label: string; // 显示文本
+ children?: MenuItem[]; // 子菜单
+ path?: string; // 页面路径
+ pageName?: string; // 页面名称
+ type?: 'chart' | 'setting' | 'folder' | 'file'; // 图标类型
+}
+
+interface MenuTree extends MenuItem {}
+```
+
+### 组件 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;
+}
+```
+
+---
+
+## 3. 组件行为
+
+### 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 显示"已打开"指示器样式。
+
+---
+
+## 4. 图标映射
+
+| type 值 | 图标 |
+|---------|------|
+| chart | PieChartOutlined |
+| setting | SettingOutlined |
+| folder | FolderOutlined |
+| file (默认) | FileOutlined |
+
+---
+
+## 5. 响应式断点
+
+- **PC 端**:`breakpoint="lg"` (992px) 以上,使用 antd Layout.Sider
+- **移动端**:992px 及以下,使用固定定位覆盖层 + 遮罩
--
Gitblit v1.9.1