# 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 `