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.
shudao-vue-frontend/src/components/Sidebar.vue
navItems array./chat?mode=... from non-Chat pages for Chat-mode features.shudao-vue-frontend/src/views/Chat.vue
currentMode to the sidebar.select-mode with existing setMode().shudao-vue-frontend/src/components/Sidebar.test.js
router.push() with the correct fallback paths from non-Chat pages.select-mode when already on Chat.Files:
shudao-vue-frontend/src/components/Sidebar.test.jsModify: 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.
Files:
shudao-vue-frontend/src/components/Sidebar.vueTest: 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.
Run: npm run test -- src/components/Sidebar.test.js
Expected: PASS for all Sidebar tests.
Run: npm run build
Expected: Build exits with code 0.
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.