Files
docs/superpowers/plans/2026-07-27-admin-grouped-navigation.md
T

16 KiB

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:

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:

"test": "vitest run"
  • Step 3: Add the Vitest configuration

Create apps/admin-web/vitest.config.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:

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:

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:

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:

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:

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:

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: '<nav :data-active="defaultActive" :data-collapsed="String(collapse)"><slot /></nav>',
  },
  'el-sub-menu': {
    props: ['index'],
    template: '<section data-kind="group" :data-index="index"><header><slot name="title" /></header><slot /></section>',
  },
  'el-menu-item': {
    props: ['index'],
    template: '<a data-kind="item" :data-index="index"><slot /></a>',
  },
}

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:

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:

<script setup lang="ts">
import { adminNavigation } from '@/navigation/adminNavigation'

defineProps<{
  activePath: string
  collapsed: boolean
}>()
</script>

<template>
  <el-menu
    class="side-nav-menu"
    :default-active="activePath"
    :collapse="collapsed"
    :default-openeds="['business', 'system']"
    router
  >
    <template v-for="item in adminNavigation" :key="item.key">
      <el-sub-menu v-if="item.children?.length" :index="item.key">
        <template #title>
          <component :is="item.icon" :size="19" :stroke-width="1.8" />
          <span>{{ item.title }}</span>
        </template>

        <el-menu-item v-for="child in item.children" :key="child.key" :index="child.path || child.key">
          <component :is="child.icon" :size="17" :stroke-width="1.8" />
          <template #title>{{ child.title }}</template>
        </el-menu-item>
      </el-sub-menu>

      <el-menu-item v-else :index="item.path || item.key">
        <component :is="item.icon" :size="19" :stroke-width="1.8" />
        <template #title>{{ item.title }}</template>
      </el-menu-item>
    </template>
  </el-menu>
</template>
  • Step 4: Run both focused tests and verify GREEN

Run:

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:

import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'

Add the computed collapse state so the mobile drawer never renders the desktop collapsed menu:

const menuCollapsed = computed(() => collapsed.value && !mobileMenuOpen.value)
  • Step 2: Replace the flat RouterLink loop

Replace the contents of <nav class="side-nav" aria-label="主导航"> with:

<AdminSidebarMenu :active-path="route.path" :collapsed="menuCollapsed" />

Keep the existing route watcher so selecting a menu item closes the mobile drawer.

  • Step 3: Remove flat navigation metadata from the router

Delete the NavigationItem import and adminNavigation array from apps/admin-web/src/router/index.ts. Do not change any route path, name, component or permission metadata.

  • Step 4: Verify all unit tests remain green

Run:

pnpm test

Expected: 4 tests PASS with no warnings.

Task 5: Adapt the existing sidebar styling

Files:

  • Modify: apps/admin-web/src/styles/index.css

  • Step 1: Replace the flat link styles

Replace .side-nav, .side-nav-item and its hover/active rules with:

.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 10px; }
.side-nav-menu {
  width: 100%;
  border-right: 0 !important;
  background: transparent !important;
  --el-menu-bg-color: transparent;
  --el-menu-text-color: #bdc7d7;
  --el-menu-active-color: var(--cc-navy);
  --el-menu-hover-bg-color: rgba(255,255,255,.08);
}
.side-nav-menu.el-menu--collapse { width: 56px; }
.side-nav-menu .el-menu-item,
.side-nav-menu .el-sub-menu__title {
  height: 44px;
  margin: 3px 0;
  border-radius: 8px;
  line-height: 44px;
}
.side-nav-menu .el-menu-item:hover,
.side-nav-menu .el-sub-menu__title:hover { color: #fff; }
.side-nav-menu .el-menu-item.is-active { background: #fff; color: var(--cc-navy); font-weight: 650; }
.side-nav-menu .el-sub-menu .el-menu-item { min-width: 0; padding-left: 48px !important; }
.side-nav-menu .el-menu-item svg,
.side-nav-menu .el-sub-menu__title svg { flex: 0 0 auto; margin-right: 12px; }
  • Step 2: Update the mobile override

Remove mobile rules that reference .side-nav-item and add:

@media (max-width: 720px) {
  .side-nav-menu,
  .side-nav-menu.el-menu--collapse { width: 100%; }
}

Do not alter the existing sidebar transform, scrim, 44px touch targets or reduced-motion behavior.

  • Step 3: Run static verification

Run:

pnpm test
pnpm typecheck
pnpm build

Expected: tests, Vue TypeScript checks and production build all PASS.

Task 6: Browser and Impeccable verification

Files:

  • Modify only if a verified defect is found: apps/admin-web/src/components/AdminSidebarMenu.vue, apps/admin-web/src/layouts/AppLayout.vue, apps/admin-web/src/styles/index.css

  • Step 1: Start the admin development server

Run:

pnpm dev

Expected: admin is available at http://localhost:5173.

  • Step 2: Verify desktop expanded and collapsed states in one browser pass

Check:

  • Workbench remains a direct entry.

  • Business management contains customers and first-leg orders.

  • System management contains users, roles and permissions.

  • Active route remains highlighted after direct URL navigation.

  • Collapsing the sidebar exposes grouped entries through Element Plus popovers.

  • Existing customer, order, user, role and permission interactions still work.

  • Step 3: Verify the 390px mobile state in the same pass

Check:

  • The menu button opens the drawer.

  • Both groups can expand and every leaf is reachable.

  • Selecting a leaf closes the drawer.

  • Clicking the right-side scrim closes the drawer.

  • The page itself has no horizontal viewport overflow.

  • Step 4: Run the Impeccable detector once

Run:

node C:\Users\BY\.codex\skills\impeccable\scripts\detect.mjs --json src/components/AdminSidebarMenu.vue src/layouts/AppLayout.vue src/styles/index.css

Expected: no unverified detector findings remain. Fix verified findings in one batch, then perform at most one confirmation browser pass.

Task 7: Update project documentation and complete verification

Files:

  • Modify: apps/admin-web/README.md

  • Modify: apps/admin-web/AGENTS.md

  • Step 1: Document tests and grouped navigation

Add:

测试:

```bash
pnpm test
```

后台菜单按“业务管理”和“系统管理”分组;当前页面使用 Pinia 模拟数据,不请求尚未确定的 Java 业务接口。

Update the quality-gate section of apps/admin-web/AGENTS.md to include pnpm test, and replace the obsolete statement that the project has no automated test script with a requirement to run unit tests for navigation or other tested behavior changes.

  • Step 2: Run final verification from admin-web

Run:

pnpm test
pnpm typecheck
pnpm build

Expected: all commands exit with code 0 and apps/admin-web/dist is refreshed.

  • Step 3: Confirm repository isolation

Run from the repository root:

Get-ChildItem -Force -File | Where-Object { $_.Name -in @('package.json','pnpm-workspace.yaml','pnpm-lock.yaml') }

Expected: no Node package or workspace files exist at the repository root.