1. 菜单类型默认值改为 TOP_LEFT(顶部一级+左侧二级) 2. 二级菜单 component 设为空字符串避免嵌套布局 3. 前端和后端菜单数据同步更新 Co-authored-by: Cursor <cursoragent@cursor.com>
500 lines
15 KiB
Markdown
500 lines
15 KiB
Markdown
# 菜单结构调整实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 将 14 个一级菜单重组为 5 个一级菜单(市场、生产、质检、运营支撑、系统管理),新增菜单记录到 sys_menu 表并支持角色权限控制。
|
||
|
||
**Architecture:** 采用方案 A(一级菜单独立权限),每个新一级菜单设置独立 `perms`(如 `menu:market`),前端改造菜单组件支持两级菜单结构(顶部一级 + 左侧二级),后端同步改造菜单接口。
|
||
|
||
**Tech Stack:** Vue 3 + Element Plus + Pinia + FastAPI + SQLAlchemy
|
||
|
||
---
|
||
|
||
## 变更范围总览
|
||
|
||
| 层级 | 文件 | 变更类型 |
|
||
|------|------|----------|
|
||
| 前端路由 | `web-admin/src/router/modules/sqxp.ts` | 结构调整 |
|
||
| 前端菜单组件 | `web-admin/src/components/core/layouts/art-menus/*` | 支持两级结构 |
|
||
| 前端状态 | `web-admin/src/store/modules/menu.ts`, `setting.ts` | 新增一级菜单状态 |
|
||
| 后端菜单接口 | `backend/app/api/menu.py` | 新增一级菜单数据 |
|
||
| 后端权限服务 | `backend/app/services/web_auth_roles.py` | 支持一级菜单权限过滤 |
|
||
| 数据库 | `sys_menu` 表 | 新增 5 个一级菜单记录 |
|
||
|
||
---
|
||
|
||
## Task 1: 后端 - 更新菜单接口数据
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/api/menu.py:10-773`
|
||
|
||
- [ ] **Step 1: 添加新一级菜单定义**
|
||
|
||
在 `STATIC_MENU_TREE` 开头添加 5 个新一级菜单:
|
||
|
||
```python
|
||
# 新增一级菜单定义(放在 STATIC_MENU_TREE 最前面)
|
||
TOP_MENU_MARKET = {
|
||
"id": "TOP00000000000000000000000000001",
|
||
"name": "MarketTop",
|
||
"path": "/market",
|
||
"component": "/index/index",
|
||
"meta": {
|
||
"title": "市场",
|
||
"icon": "ri:shopping-cart-line",
|
||
"keepAlive": False,
|
||
"isTopMenu": True,
|
||
"menuCode": "menu:market",
|
||
},
|
||
"children": [
|
||
# 细胞存储、科研捐赠、中试服务、基础实验、市场基础信息、临床应用
|
||
],
|
||
}
|
||
|
||
TOP_MENU_PRODUCTION = {
|
||
"id": "TOP00000000000000000000000000002",
|
||
"name": "ProductionTop",
|
||
"path": "/production-group",
|
||
"component": "/index/index",
|
||
"meta": {
|
||
"title": "生产",
|
||
"icon": "ri:factory-line",
|
||
"keepAlive": False,
|
||
"isTopMenu": True,
|
||
"menuCode": "menu:production",
|
||
},
|
||
"children": [
|
||
# 生产管理、生产基础信息
|
||
],
|
||
}
|
||
|
||
TOP_MENU_QUALITY = {
|
||
"id": "TOP00000000000000000000000000003",
|
||
"name": "QualityTop",
|
||
"path": "/quality-group",
|
||
"component": "/index/index",
|
||
"meta": {
|
||
"title": "质检",
|
||
"icon": "ri:shield-check-line",
|
||
"keepAlive": False,
|
||
"isTopMenu": True,
|
||
"menuCode": "menu:quality",
|
||
},
|
||
"children": [
|
||
# 质检管理、质检基础信息
|
||
],
|
||
}
|
||
|
||
TOP_MENU_OPERATIONS = {
|
||
"id": "TOP00000000000000000000000000004",
|
||
"name": "OperationsTop",
|
||
"path": "/operations-group",
|
||
"component": "/index/index",
|
||
"meta": {
|
||
"title": "运营支撑",
|
||
"icon": "ri:settings-3-line",
|
||
"keepAlive": False,
|
||
"isTopMenu": True,
|
||
"menuCode": "menu:operations",
|
||
},
|
||
"children": [
|
||
# 系统基础信息、运营支撑
|
||
],
|
||
}
|
||
|
||
TOP_MENU_SYSTEM = {
|
||
"id": "TOP00000000000000000000000000005",
|
||
"name": "SystemTop",
|
||
"path": "/system-manage",
|
||
"component": "/index/index",
|
||
"meta": {
|
||
"title": "系统管理",
|
||
"icon": "ri:server-line",
|
||
"keepAlive": False,
|
||
"isTopMenu": True,
|
||
"menuCode": "menu:system-manage",
|
||
},
|
||
"children": [
|
||
# 系统管理、工作流管理
|
||
],
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 调整原有菜单到新父级下**
|
||
|
||
将现有 14 个一级菜单作为二级菜单,重新分配 parent_id 和 path:
|
||
|
||
| 原菜单 | 新父级 | 新 path |
|
||
|--------|--------|---------|
|
||
| 细胞存储 | TOP00000000000000000000000000001 | `/market/cell-storage` |
|
||
| 科研捐赠 | TOP00000000000000000000000000001 | `/market/research-donation` |
|
||
| 中试服务 | TOP00000000000000000000000000001 | `/market/pilot-service` |
|
||
| 基础实验 | TOP00000000000000000000000000001 | `/market/basic-experiment` |
|
||
| 市场基础信息 | TOP00000000000000000000000000001 | `/market/market-info` |
|
||
| 临床应用 | TOP00000000000000000000000000002 | `/production-group/clinical-application` |
|
||
| 生产管理 | TOP00000000000000000000000000002 | `/production-group/production` |
|
||
| 生产基础信息 | TOP00000000000000000000000000002 | `/production-group/production-base` |
|
||
| 质检管理 | TOP00000000000000000000000000003 | `/quality-group/quality-manage` |
|
||
| 质检基础信息 | TOP00000000000000000000000000003 | `/quality-group/quality-base` |
|
||
| 系统基础信息 | TOP00000000000000000000000000004 | `/operations-group/system-base` |
|
||
| 运营支撑 | TOP00000000000000000000000000004 | `/operations-group/operations` |
|
||
| 系统管理 | TOP00000000000000000000000000005 | `/system-manage/system` |
|
||
| 工作流管理 | TOP00000000000000000000000000005 | `/system-manage/workflow` |
|
||
|
||
- [ ] **Step 3: 更新 STATIC_MENU_TREE 结构**
|
||
|
||
将 STATIC_MENU_TREE 改为只包含 5 个新一级菜单,每个一级菜单的 children 包含对应的原菜单作为二级菜单。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add backend/app/api/menu.py
|
||
git commit -m "feat(menu): 重构菜单为5个一级分组菜单结构"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: 后端 - 更新权限服务支持一级菜单过滤
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/services/web_auth_roles.py`
|
||
|
||
- [ ] **Step 1: 添加一级菜单权限码常量**
|
||
|
||
```python
|
||
# 一级菜单权限码
|
||
TOP_MENU_PERMS = {"menu:market", "menu:production", "menu:quality", "menu:operations", "menu:system-manage"}
|
||
```
|
||
|
||
- [ ] **Step 2: 确保 iter_user_menu_perm_codes 返回一级菜单 perms**
|
||
|
||
当前 `iter_user_menu_perm_codes` 通过 `sys_role_menu → sys_menu` 查询用户拥有的菜单权限。一级菜单的 `perms`(如 `menu:market`)已经在 sys_menu 表中定义,因此无需修改查询逻辑。只需确保 sys_menu 表中有这 5 个一级菜单的记录(Task 3 处理)。
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add backend/app/services/web_auth_roles.py
|
||
git commit -m "feat(auth): 支持一级菜单权限码过滤"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: 数据库 - 插入一级菜单数据到 sys_menu
|
||
|
||
**Files:**
|
||
- Create: `docs/sql/menu_restructuring.sql`
|
||
|
||
- [ ] **Step 1: 编写 SQL 插入脚本**
|
||
|
||
创建 SQL 脚本,包含 5 个一级菜单的 INSERT 语句(menu_type = 'M' 表示目录):
|
||
|
||
```sql
|
||
-- 一级菜单
|
||
INSERT INTO sys_menu (id, menu_name, parent_id, menu_type, path, component, perms, is_frame, visible, status, tenant_no, order_num, create_by, create_dept_id, create_time, update_by, update_time, del_flag) VALUES
|
||
('TOP00000000000000000000000000001', '市场', '0', 'M', '/market', '/index/index', 'menu:market', 1, '1', '1', 'zkxw', 1, 'system', '', NOW(), 'system', NOW(), 0),
|
||
('TOP00000000000000000000000000002', '生产', '0', 'M', '/production-group', '/index/index', 'menu:production', 1, '1', '1', 'zkxw', 2, 'system', '', NOW(), 'system', NOW(), 0),
|
||
('TOP00000000000000000000000000003', '质检', '0', 'M', '/quality-group', '/index/index', 'menu:quality', 1, '1', '1', 'zkxw', 3, 'system', '', NOW(), 'system', NOW(), 0),
|
||
('TOP00000000000000000000000000004', '运营支撑', '0', 'M', '/operations-group', '/index/index', 'menu:operations', 1, '1', '1', 'zkxw', 4, 'system', '', NOW(), 'system', NOW(), 0),
|
||
('TOP00000000000000000000000000005', '系统管理', '0', 'M', '/system-manage', '/index/index', 'menu:system-manage', 1, '1', '1', 'zkxw', 5, 'system', '', NOW(), 'system', NOW(), 0);
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add docs/sql/menu_restructuring.sql
|
||
git commit -m "feat(db): 添加5个一级菜单到sys_menu表"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: 前端 - 改造 sqxp.ts 路由结构
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/router/modules/sqxp.ts:1-1264`
|
||
|
||
- [ ] **Step 1: 添加新一级菜单定义**
|
||
|
||
在文件开头添加 5 个新一级菜单(与后端对应):
|
||
|
||
```typescript
|
||
// 新一级菜单
|
||
const MARKET_TOP_MENU: AppRouteRecord = {
|
||
id: 'TOP00000000000000000000000000001',
|
||
name: 'MarketTop',
|
||
path: '/market',
|
||
component: '/index/index',
|
||
meta: {
|
||
title: '市场',
|
||
icon: 'ri:shopping-cart-line',
|
||
keepAlive: false,
|
||
isTopMenu: true,
|
||
roles: ['admin'],
|
||
menuCode: 'menu:market'
|
||
},
|
||
children: [
|
||
// 细胞存储、科研捐赠、中试服务、基础实验、市场基础信息、临床应用
|
||
]
|
||
}
|
||
|
||
const PRODUCTION_TOP_MENU: AppRouteRecord = {
|
||
id: 'TOP00000000000000000000000000002',
|
||
name: 'ProductionTop',
|
||
path: '/production-group',
|
||
component: '/index/index',
|
||
meta: {
|
||
title: '生产',
|
||
icon: 'ri:factory-line',
|
||
keepAlive: false,
|
||
isTopMenu: true,
|
||
roles: ['admin'],
|
||
menuCode: 'menu:production'
|
||
},
|
||
children: [
|
||
// 临床应用、生产管理、生产基础信息
|
||
]
|
||
}
|
||
|
||
const QUALITY_TOP_MENU: AppRouteRecord = {
|
||
id: 'TOP00000000000000000000000000003',
|
||
name: 'QualityTop',
|
||
path: '/quality-group',
|
||
component: '/index/index',
|
||
meta: {
|
||
title: '质检',
|
||
icon: 'ri:shield-check-line',
|
||
keepAlive: false,
|
||
isTopMenu: true,
|
||
roles: ['admin'],
|
||
menuCode: 'menu:quality'
|
||
},
|
||
children: [
|
||
// 质检管理、质检基础信息
|
||
]
|
||
}
|
||
|
||
const OPERATIONS_TOP_MENU: AppRouteRecord = {
|
||
id: 'TOP00000000000000000000000000004',
|
||
name: 'OperationsTop',
|
||
path: '/operations-group',
|
||
component: '/index/index',
|
||
meta: {
|
||
title: '运营支撑',
|
||
icon: 'ri:settings-3-line',
|
||
keepAlive: false,
|
||
isTopMenu: true,
|
||
roles: ['admin'],
|
||
menuCode: 'menu:operations'
|
||
},
|
||
children: [
|
||
// 系统基础信息、运营支撑
|
||
]
|
||
}
|
||
|
||
const SYSTEM_TOP_MENU: AppRouteRecord = {
|
||
id: 'TOP00000000000000000000000000005',
|
||
name: 'SystemTop',
|
||
path: '/system-manage',
|
||
component: '/index/index',
|
||
meta: {
|
||
title: '系统管理',
|
||
icon: 'ri:server-line',
|
||
keepAlive: false,
|
||
isTopMenu: true,
|
||
roles: ['admin'],
|
||
menuCode: 'menu:system-manage'
|
||
},
|
||
children: [
|
||
// 系统管理、工作流管理
|
||
]
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 将原有 14 个菜单作为二级菜单移动到对应一级菜单下**
|
||
|
||
将细胞存储、科研捐赠、中试服务、基础实验、市场基础信息移到 MARKET_TOP_MENU.children 下;
|
||
将临床应用移到 PRODUCTION_TOP_MENU.children 下;
|
||
以此类推。
|
||
|
||
- [ ] **Step 3: 更新 export 导出**
|
||
|
||
将 sqxpAdminRoutes 改为只包含 5 个新一级菜单。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/router/modules/sqxp.ts
|
||
git commit -m "feat(router): 重构路由为5个一级菜单分组"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: 前端 - 改造菜单组件支持两级结构
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue`
|
||
- Modify: `web-admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue`
|
||
- Modify: `web-admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue`
|
||
|
||
- [ ] **Step 1: 识别当前活跃的一级菜单**
|
||
|
||
新增逻辑:根据当前路由路径判断所属的一级菜单,提取出 `activeTopMenuId`。
|
||
|
||
```typescript
|
||
// 在组件中添加 computed
|
||
const activeTopMenuId = computed(() => {
|
||
const path = route.path
|
||
// 从路径中提取一级菜单 path,如 /market/cell-storage → /market
|
||
const topPath = '/' + path.split('/')[1]
|
||
return topPath
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: 分离一级菜单和二级菜单数据**
|
||
|
||
```typescript
|
||
// 过滤出一级菜单(isTopMenu: true)
|
||
const topMenus = computed(() => {
|
||
return props.list.filter(item => item.meta?.isTopMenu)
|
||
})
|
||
|
||
// 根据活跃的一级菜单获取二级菜单
|
||
const subMenus = computed(() => {
|
||
const active = topMenus.value.find(item => item.path === activeTopMenuId.value)
|
||
return active?.children || []
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 3: 修改模板**
|
||
|
||
水平菜单只显示一级菜单,点击后更新 activeTopMenuId,并触发侧边栏显示对应的二级菜单。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue
|
||
git add web-admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue
|
||
git commit -m "feat(menu): 改造顶部菜单支持两级结构"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: 前端 - 改造侧边栏组件
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue`
|
||
|
||
- [ ] **Step 1: 修改菜单列表计算逻辑**
|
||
|
||
当前 `menuList` computed 根据 `isTopLeftMenu` 或 `isDualMenu` 返回对应子菜单。改为:
|
||
- 当存在活跃一级菜单时,返回该一级菜单的 children
|
||
- 否则返回完整菜单列表(兼容一级菜单页面)
|
||
|
||
```typescript
|
||
const menuList = computed(() => {
|
||
const menuStore = useMenuStore()
|
||
const allMenus = menuStore.menuList
|
||
|
||
if (!isTopLeftMenu.value && !isDualMenu.value) {
|
||
return allMenus
|
||
}
|
||
|
||
// 处理 iframe 路径
|
||
if (isIframe(route.path)) {
|
||
return findIframeMenuList(route.path, allMenus)
|
||
}
|
||
|
||
// 一级菜单页面直接返回空
|
||
if (route.meta.isFirstLevel) {
|
||
return []
|
||
}
|
||
|
||
// 返回当前一级菜单的子菜单
|
||
const topPath = '/' + route.path.split('/')[1]
|
||
const currentTopMenu = allMenus.find(menu =>
|
||
menu.meta?.isTopMenu && menu.path === topPath
|
||
)
|
||
|
||
if (currentTopMenu?.children) {
|
||
return currentTopMenu.children.filter((child: AppRouteRecord) => !child.meta?.isHide)
|
||
}
|
||
|
||
return []
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue
|
||
git commit -m "feat(menu): 改造侧边栏显示二级菜单"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: 前端 - 更新菜单状态管理
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/store/modules/menu.ts`
|
||
- Modify: `web-admin/src/store/modules/setting.ts`
|
||
|
||
- [ ] **Step 1: 在 menu.ts 中添加活跃一级菜单状态**
|
||
|
||
```typescript
|
||
/** 活跃的一级菜单路径 */
|
||
const activeTopMenuPath = ref('')
|
||
|
||
/** 设置活跃一级菜单 */
|
||
const setActiveTopMenuPath = (path: string) => {
|
||
activeTopMenuPath.value = path
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/store/modules/menu.ts
|
||
git commit -m "feat(store): 添加一级菜单活跃状态管理"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: 测试验证
|
||
|
||
- [ ] **Step 1: 启动后端服务**
|
||
|
||
```bash
|
||
cd backend && ./start-local.sh
|
||
```
|
||
|
||
- [ ] **Step 2: 启动前端服务**
|
||
|
||
```bash
|
||
cd web-admin && pnpm dev
|
||
```
|
||
|
||
- [ ] **Step 3: 验证菜单显示**
|
||
|
||
1. 登录管理端
|
||
2. 检查顶部是否显示 5 个一级菜单
|
||
3. 点击一级菜单,检查左侧是否显示对应的二级菜单
|
||
4. 验证权限控制:没有 `menu:market` 权限的角色不应看到「市场」菜单
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "test: 验证菜单结构调整功能"
|
||
```
|
||
|
||
---
|
||
|
||
## 执行方式
|
||
|
||
**Two execution options:**
|
||
|
||
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
|
||
|
||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||
|
||
**Which approach?**
|