# Admin Grouped Navigation Implementation Plan
> **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:** Replace the flat admin sidebar with maintainable two-level navigation while preserving every existing static page and interaction.
**Architecture:** Keep Vue Router responsible only for routes and move admin menu metadata into a local navigation module. Render the configuration through a focused Element Plus sidebar component, while all business pages continue to use the current Pinia mock store and make no Java API calls.
**Tech Stack:** Vue 3, TypeScript, Vue Router, Pinia, Element Plus, Lucide Vue, Vitest, Vue Test Utils, jsdom, pnpm
---
The repository is not a Git worktree. Commit steps are intentionally omitted; each green test/build checkpoint is the recovery boundary.
## File map
- Create `apps/admin-web/vitest.config.ts`: admin-only unit-test configuration.
- Create `apps/admin-web/src/navigation/adminNavigation.ts`: admin-local two-level menu types and data.
- Create `apps/admin-web/src/navigation/adminNavigation.test.ts`: navigation structure and uniqueness contract.
- Create `apps/admin-web/src/components/AdminSidebarMenu.vue`: Element Plus menu renderer.
- Create `apps/admin-web/src/components/AdminSidebarMenu.test.ts`: grouped labels, paths and collapsed-state behavior.
- Modify `apps/admin-web/src/layouts/AppLayout.vue`: delegate menu rendering to `AdminSidebarMenu`.
- Modify `apps/admin-web/src/router/index.ts`: stop exporting flat menu metadata.
- Modify `apps/admin-web/src/styles/index.css`: style Element Plus menu within the existing navy sidebar.
- Modify `apps/admin-web/package.json` and `apps/admin-web/pnpm-lock.yaml`: add admin-only test dependencies and script.
- Modify `apps/admin-web/README.md`: document grouped menu and test command.
- Modify `apps/admin-web/AGENTS.md`: add the new admin-local test quality gate.
### Task 1: Install the admin-only test harness
**Files:**
- Modify: `apps/admin-web/package.json`
- Modify: `apps/admin-web/pnpm-lock.yaml`
- Create: `apps/admin-web/vitest.config.ts`
- [ ] **Step 1: Install test dependencies inside admin-web**
Run:
```powershell
cd C:\Users\BY\Desktop\code\chongchong-global\apps\admin-web
pnpm add -D vitest @vue/test-utils jsdom
```
Expected: only `apps/admin-web/package.json`, `apps/admin-web/pnpm-lock.yaml` and `apps/admin-web/node_modules` change.
- [ ] **Step 2: Add the test script**
Add to `scripts` in `apps/admin-web/package.json`:
```json
"test": "vitest run"
```
- [ ] **Step 3: Add the Vitest configuration**
Create `apps/admin-web/vitest.config.ts`:
```ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
environment: 'jsdom',
include: ['src/**/*.test.ts'],
clearMocks: true,
},
})
```
- [ ] **Step 4: Verify the empty harness**
Run:
```powershell
pnpm test -- --passWithNoTests
```
Expected: PASS with no test files and no dependency-resolution errors.
### Task 2: Define the grouped navigation with TDD
**Files:**
- Create: `apps/admin-web/src/navigation/adminNavigation.test.ts`
- Create: `apps/admin-web/src/navigation/adminNavigation.ts`
- [ ] **Step 1: Write the failing navigation contract**
Create `apps/admin-web/src/navigation/adminNavigation.test.ts`:
```ts
import { describe, expect, it } from 'vitest'
import { adminNavigation, getNavigationLeaves } from './adminNavigation'
describe('adminNavigation', () => {
it('keeps only the dashboard as a direct first-level page', () => {
expect(adminNavigation.map((item) => item.title)).toEqual([
'工作台',
'业务管理',
'系统管理',
])
expect(adminNavigation[0].path).toBe('/')
expect(adminNavigation[1].children?.map((item) => item.title)).toEqual([
'客户管理',
'头程订单',
])
expect(adminNavigation[2].children?.map((item) => item.title)).toEqual([
'内部用户',
'角色管理',
'菜单权限',
])
})
it('assigns every page a unique path and permission code', () => {
const leaves = getNavigationLeaves(adminNavigation)
const paths = leaves.map((item) => item.path)
const permissions = leaves.map((item) => item.permission)
expect(paths).toEqual([
'/',
'/customers',
'/orders/first-leg',
'/system/users',
'/system/roles',
'/system/permissions',
])
expect(new Set(paths).size).toBe(paths.length)
expect(new Set(permissions).size).toBe(permissions.length)
expect(leaves.every((item) => Boolean(item.icon))).toBe(true)
})
})
```
- [ ] **Step 2: Run the test and verify RED**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts
```
Expected: FAIL because `src/navigation/adminNavigation.ts` does not exist.
- [ ] **Step 3: Implement the minimal local navigation model**
Create `apps/admin-web/src/navigation/adminNavigation.ts`:
```ts
import type { Component } from 'vue'
import {
BriefcaseBusiness,
Building2,
ClipboardList,
KeyRound,
LayoutDashboard,
Settings,
ShieldCheck,
Users,
} from '@lucide/vue'
export interface AdminNavigationNode {
key: string
title: string
icon: Component
path?: string
permission?: string
children?: AdminNavigationNode[]
}
export interface AdminNavigationLeaf extends AdminNavigationNode {
path: string
permission: string
children?: never
}
export const adminNavigation: AdminNavigationNode[] = [
{
key: 'dashboard',
title: '工作台',
icon: LayoutDashboard,
path: '/',
permission: 'dashboard:view',
},
{
key: 'business',
title: '业务管理',
icon: BriefcaseBusiness,
children: [
{ key: 'customers', title: '客户管理', icon: Building2, path: '/customers', permission: 'customer:list' },
{ key: 'first-leg-orders', title: '头程订单', icon: ClipboardList, path: '/orders/first-leg', permission: 'first-leg:list' },
],
},
{
key: 'system',
title: '系统管理',
icon: Settings,
children: [
{ key: 'internal-users', title: '内部用户', icon: Users, path: '/system/users', permission: 'system:user:list' },
{ key: 'roles', title: '角色管理', icon: ShieldCheck, path: '/system/roles', permission: 'system:role:list' },
{ key: 'permissions', title: '菜单权限', icon: KeyRound, path: '/system/permissions', permission: 'system:permission:list' },
],
},
]
export function getNavigationLeaves(nodes: AdminNavigationNode[]): AdminNavigationLeaf[] {
return nodes.flatMap((node) => {
if (node.children?.length) return getNavigationLeaves(node.children)
if (!node.path || !node.permission) return []
return [node as AdminNavigationLeaf]
})
}
```
- [ ] **Step 4: Run the test and verify GREEN**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts
```
Expected: 2 tests PASS.
### Task 3: Render the two-level menu with TDD
**Files:**
- Create: `apps/admin-web/src/components/AdminSidebarMenu.test.ts`
- Create: `apps/admin-web/src/components/AdminSidebarMenu.vue`
- [ ] **Step 1: Write the failing sidebar component test**
Create `apps/admin-web/src/components/AdminSidebarMenu.test.ts`:
```ts
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import AdminSidebarMenu from './AdminSidebarMenu.vue'
const stubs = {
'el-menu': {
props: ['defaultActive', 'collapse'],
template: '',
},
'el-sub-menu': {
props: ['index'],
template: '',
},
'el-menu-item': {
props: ['index'],
template: '',
},
}
describe('AdminSidebarMenu', () => {
it('renders grouped page entries and forwards the active route', () => {
const wrapper = mount(AdminSidebarMenu, {
props: { activePath: '/system/users', collapsed: false },
global: { stubs },
})
expect(wrapper.get('nav').attributes('data-active')).toBe('/system/users')
expect(wrapper.findAll('[data-kind="group"] header').map((node) => node.text())).toEqual([
'业务管理',
'系统管理',
])
expect(wrapper.findAll('[data-kind="item"]').map((node) => node.attributes('data-index'))).toEqual([
'/',
'/customers',
'/orders/first-leg',
'/system/users',
'/system/roles',
'/system/permissions',
])
})
it('forwards desktop collapse state to Element Plus', () => {
const wrapper = mount(AdminSidebarMenu, {
props: { activePath: '/', collapsed: true },
global: { stubs },
})
expect(wrapper.get('nav').attributes('data-collapsed')).toBe('true')
})
})
```
- [ ] **Step 2: Run the component test and verify RED**
Run:
```powershell
pnpm test -- src/components/AdminSidebarMenu.test.ts
```
Expected: FAIL because `AdminSidebarMenu.vue` does not exist.
- [ ] **Step 3: Implement the minimal menu component**
Create `apps/admin-web/src/components/AdminSidebarMenu.vue`:
```vue
```
- [ ] **Step 4: Run both focused tests and verify GREEN**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts src/components/AdminSidebarMenu.test.ts
```
Expected: 4 tests PASS.
### Task 4: Integrate the component into AppLayout
**Files:**
- Modify: `apps/admin-web/src/layouts/AppLayout.vue`
- Modify: `apps/admin-web/src/router/index.ts`
- [ ] **Step 1: Simplify AppLayout imports and state**
Remove the six page icon imports, `adminNavigation`, and `iconMap`. Import the focused component:
```ts
import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'
```
Add the computed collapse state so the mobile drawer never renders the desktop collapsed menu:
```ts
const menuCollapsed = computed(() => collapsed.value && !mobileMenuOpen.value)
```
- [ ] **Step 2: Replace the flat RouterLink loop**
Replace the contents of `