2026-04-16-desktop-sidebar-navigation.md 4.9 KB

Desktop Sidebar 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: Add AI写作、安全培训、考试工坊 buttons to the shared desktop sidebar and make them behave like the existing Chat input-area mode buttons.

Architecture: Extend the existing shared Sidebar.vue component with a data-driven navItems array. Chat.vue passes currentMode and handles select-mode with its existing setMode() function; other pages route to /chat?mode=... so Chat initializes the same mode on mount.

Tech Stack: Vue 3, Vue Router 4, Vite, Vitest, @vue/test-utils, Less.


File Structure

  • Modify: shudao-vue-frontend/src/components/Sidebar.vue
    • Owns the desktop left-side navigation rail.
    • Will render five navigation buttons from a navItems array.
    • Will emit Chat mode selections when already on Chat.
    • Will route to /chat?mode=... from non-Chat pages for Chat-mode features.
  • Modify: shudao-vue-frontend/src/views/Chat.vue
    • Passes currentMode to the sidebar.
    • Handles sidebar select-mode with existing setMode().
  • Create: shudao-vue-frontend/src/components/Sidebar.test.js
    • Verifies the sidebar renders the required labels.
    • Verifies route clicks call router.push() with the correct fallback paths from non-Chat pages.
    • Verifies Chat-mode clicks emit select-mode when already on Chat.
    • Verifies the current route gets the active state.

Task 1: Sidebar Navigation Test

Files:

  • Create: shudao-vue-frontend/src/components/Sidebar.test.js
  • Modify: none

  • [ ] Step 1: Write the failing test

    import { mount } from '@vue/test-utils'
    import { beforeEach, describe, expect, it, vi } from 'vitest'
    import { nextTick, reactive } from 'vue'
    import Sidebar from './Sidebar.vue'
    
    const push = vi.fn()
    const routeState = reactive({
    name: 'Chat',
    path: '/chat'
    })
    
    vi.mock('vue-router', () => ({
    useRouter: () => ({ push }),
    useRoute: () => routeState
    }))
    
    describe('Sidebar', () => {
    beforeEach(() => {
    push.mockClear()
    routeState.name = 'Chat'
    routeState.path = '/chat'
    })
    
    it('renders the desktop feature navigation buttons', () => {
    const wrapper = mount(Sidebar)
    
    expect(wrapper.text()).toContain('AI问答')
    expect(wrapper.text()).toContain('隐患提示')
    expect(wrapper.text()).toContain('AI写作')
    expect(wrapper.text()).toContain('安全培训')
    expect(wrapper.text()).toContain('考试工坊')
    })
    
    it.each([
    ['AI问答', '/chat'],
    ['隐患提示', '/hazard-detection'],
    ['AI写作', '/ai-writing'],
    ['安全培训', '/safety-hazard'],
    ['考试工坊', '/exam-workshop']
    ])('routes %s to %s', async (label, path) => {
    const wrapper = mount(Sidebar)
    
    await wrapper.find(`[data-testid="sidebar-nav-${path.slice(1)}"]`).trigger('click')
    
    expect(wrapper.text()).toContain(label)
    expect(push).toHaveBeenCalledWith(path)
    })
    
    it('marks the current route as active', async () => {
    routeState.name = 'AIWriting'
    routeState.path = '/ai-writing'
    const wrapper = mount(Sidebar)
    await nextTick()
    
    const activeItem = wrapper.find('.nav-item.active')
    
    expect(activeItem.exists()).toBe(true)
    expect(activeItem.text()).toContain('AI写作')
    })
    })
    
  • [ ] Step 2: Run test to verify it fails

Run: npm run test -- src/components/Sidebar.test.js

Expected: FAIL because the current sidebar only renders AI问答 and 隐患提示, and does not expose data-testid attributes for all five route buttons.

Task 2: Data-Driven Sidebar Implementation

Files:

  • Modify: shudao-vue-frontend/src/components/Sidebar.vue
  • Test: shudao-vue-frontend/src/components/Sidebar.test.js

  • [ ] Step 1: Implement the minimal sidebar change

Replace the hardcoded menu items with a navItems array:

const navItems = [
  { routeName: 'Chat', path: '/chat', label: 'AI问答', icon: chatIcon },
  { routeName: 'HazardDetection', path: '/hazard-detection', label: '隐患提示', icon: hazardIcon },
  { routeName: 'AIWriting', path: '/ai-writing', label: 'AI写作', icon: aiWritingIcon },
  { routeName: 'SafetyHazard', path: '/safety-hazard', label: '安全培训', icon: safetyIcon },
  { routeName: 'ExamWorkshop', path: '/exam-workshop', label: '考试工坊', icon: examIcon }
]

Render with v-for, add data-testid, and push item.path on click.

  • Step 2: Run focused test to verify it passes

Run: npm run test -- src/components/Sidebar.test.js

Expected: PASS for all Sidebar tests.

  • Step 3: Run frontend build

Run: npm run build

Expected: Build exits with code 0.

  • Step 4: Review diff

Run: git diff -- shudao-vue-frontend/src/components/Sidebar.vue shudao-vue-frontend/src/components/Sidebar.test.js

Expected: Diff only contains the shared sidebar update and the focused test.