Files
aue/docs/superpowers/plans/2026-05-14-sidebar-navigation-implementation.md
2026-06-03 13:16:30 +08:00

32 KiB
Raw Blame History

侧边栏导航系统实施计划

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: 将现有的顶部水平 header 导航重构为左侧固定侧边栏导航系统,支持展开/收起、状态记忆、响应式适配,并保持与现有白色体系设计规范的一致性。

Architecture: 采用 Vue 3 Composition API + Flexbox 布局实现经典的左侧固定侧边栏模式。通过 useSidebar composable 集中管理状态(展开/收起、移动端检测、localStorage 持久化),将侧边栏拆分为 5 个职责单一的子组件(品牌区、导航菜单、导航项、用户信息区、移动端按钮),确保代码可维护性和可测试性。

Tech Stack:

  • Vue 3 (Composition API, <script setup>)
  • CSS Variables (白色体系 design-tokens)
  • localStorage (状态持久化)
  • Ant Design Vue (图标、部分组件)

文件结构总览

新增文件 (8个)

src/
├── components/
│   └── layout/
│       ├── AppSidebar.vue              # 侧边栏主容器组件 (~200行)
│       ├── SidebarHeader.vue           # 品牌区组件 (~80行)
│       ├── SidebarNav.vue              # 导航菜单容器 (~60行)
│       ├── SidebarNavItem.vue          # 单个导航项 (~100行)
│       ├── SidebarUser.vue             # 用户信息区 (~120行)
│       └── MobileMenuButton.vue        # 移动端汉堡按钮 (~40行)
│
├── composables/
│   └── useSidebar.js                   # 侧边栏状态管理逻辑 (~80行)
│
└── utils/
    └── debounce.js                     # 防抖工具函数 (~15行)

修改文件 (3个)

src/
├── views/
│   └── Home.vue                        # 主要改造移除旧header集成侧边栏布局
│
├── design-tokens.css                   # 扩展添加侧边栏相关CSS变量
│
└── style.css                           # 微调:可能需要调整全局样式

Task 1: 创建基础设施 - 工具函数和设计令牌

Files:

  • Create: src/utils/debounce.js
  • Modify: src/design-tokens.css

目标: 建立项目所需的基础工具函数和扩展设计变量定义

  • Step 1: 创建防抖工具函数
// src/utils/debounce.js
export function debounce(fn, delay = 150) {
  let timer = null
  
  return function (...args) {
    if (timer) {
      clearTimeout(timer)
    }
    
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, delay)
  }
}
  • Step 2: 扩展设计令牌文件

src/design-tokens.css:root 选择器末尾添加以下变量:

/* ========== 侧边栏系统 ========== */
:root {
  /* ... 已有变量 ... */
  
  /* 尺寸规范 */
  --sidebar-width: 200px;
  --sidebar-collapsed-width: 56px;
  --sidebar-nav-item-height: 40px;
  --sidebar-header-height: 64px;
  
  /* 颜色规范 */
  --bg-sidebar: #ffffff;
  --bg-sidebar-hover: #f5f7fa;
  --bg-sidebar-active: #e6f4ff;
  --border-sidebar: #e5e7eb;
  
  /* 动画规范 */
  --sidebar-transition-duration: 280ms;
  --sidebar-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
  
  /* 响应式断点 */
  --breakpoint-mobile: 768px;
  --breakpoint-tablet: 1024px;
}
  • Step 3: 验证

运行开发服务器,确认无语法错误:

npm run dev

预期:正常启动,控制台无错误

  • Step 4: Commit
git add src/utils/debounce.js src/design-tokens.css
git commit -m "chore: add debounce utility and sidebar design tokens"

Task 2: 实现 useSidebar Composable

Files:

  • Create: src/composables/useSidebar.js

目标: 创建侧边栏状态管理的核心逻辑,包括展开/收起切换、移动端检测、localStorage 持久化

  • Step 1: 编写 useSidebar 基础结构
// src/composables/useSidebar.js
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { debounce } from '../utils/debounce'

export function useSidebar() {
  const collapsed = ref(false)
  const isMobile = ref(false)
  const mobileOpen = ref(false)

  // 从 localStorage 恢复状态
  onMounted(() => {
    const savedState = localStorage.getItem('sidebar-collapsed')
    if (savedState !== null) {
      collapsed.value = savedState === 'true'
    }
    
    checkMobile()
    window.addEventListener('resize', debouncedCheckMobile)
  })

  onUnmounted(() => {
    window.removeEventListener('resize', debouncedCheckMobile)
  })

  // 监听变化并持久化
  watch(collapsed, (val) => {
    localStorage.setItem('sidebar-collapsed', String(val))
  }, { immediate: false })

  const debouncedCheckMobile = debounce(checkMobile, 150)

  function checkMobile() {
    isMobile.value = window.innerWidth < 768
    if (isMobile.value && mobileOpen.value) {
      mobileOpen.value = false
    }
  }

  function toggleCollapse() {
    collapsed.value = !collapsed.value
  }

  function openMobile() {
    mobileOpen.value = true
  }

  function closeMobile() {
    mobileOpen.value = false
  }

  return {
    collapsed,
    isMobile,
    mobileOpen,
    toggleCollapse,
    openMobile,
    closeMobile
  }
}
  • Step 2: 在临时测试文件中验证功能

在任意 .vue 文件的 <script setup> 中临时添加:

import { useSidebar } from './composables/useSidebar'

const { collapsed, isMobile, toggleCollapse } = useSidebar()

// 打开浏览器控制台,手动调用 toggleCollapse() 测试
window.testSidebar = { collapsed, isMobile, toggleCollapse }

预期:window.testSidebar.toggleCollapsed() 可正确切换状态

  • Step 3: 清理测试代码

删除临时代码

  • Step 4: Commit
git add src/composables/useSidebar.js
git commit -m "feat: add useSidebar composable for state management"

Task 3: 实现 SidebarHeader 组件

Files:

  • Create: src/components/layout/SidebarHeader.vue

目标: 实现侧边栏顶部的品牌区域,包含 Logo、系统名称、折叠按钮

  • Step 1: 编写组件模板和脚本
<template>
  <div class="sidebar-header">
    <div class="brand">
      <span class="brand-icon">🤖</span>
      <transition name="fade">
        <span v-show="!collapsed" class="brand-text">
          制度文件管理学习<br/>AI智能体
        </span>
      </transition>
    </div>
    <button 
      class="collapse-btn" 
      @click="$emit('toggle')"
      :title="collapsed ? '展开侧边栏' : '收起侧边栏'"
    >
      {{ collapsed ? '→' : '←' }}
    </button>
  </div>
</template>

<script setup>
defineProps({
  collapsed: {
    type: Boolean,
    default: false
  }
})

defineEmits(['toggle'])
</script>

<style scoped>
.sidebar-header {
  height: var(--sidebar-header-height);
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-bottom: 1px solid var(--border-sidebar);
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
}

.brand-icon {
  font-size: 28px;
  line-height: 1;
  flex-shrink: 0;
}

.brand-text {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.3;
}

.collapse-btn {
  align-self: flex-end;
  margin-top: 8px;
  width: 24px;
  height: 24px;
  border: none;
  background: transparent;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
  font-size: 14px;
  transition: all var(--transition-fast);
}

.collapse-btn:hover {
  background: var(--bg-hover);
  color: var(--text-primary);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 200ms ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
  • Step 2: 手动验证

Home.vue 中临时导入并渲染该组件,检查:

  • 展开时显示完整文字

  • 收起时文字隐藏

  • 点击折叠按钮触发事件

  • Step 3: Commit

git add src/components/layout/SidebarHeader.vue
git commit -m "feat: add SidebarHeader component with brand area and collapse button"

Task 4: 实现 SidebarNavItem 组件

Files:

  • Create: src/components/layout/SidebarNavItem.vue

目标: 实现单个导航项支持激活状态、悬停效果、Tooltip收起状态

  • Step 1: 编写组件完整代码
<template>
  <button
    :class="['nav-item', { active: isActive }]"
    @click="$emit('select', item.key)"
    :aria-label="item.label"
    :aria-current="isActive ? 'page' : undefined"
  >
    <span class="nav-indicator"></span>
    <span class="nav-icon" v-html="item.icon"></span>
    <transition name="fade">
      <span v-show="!collapsed" class="nav-label">{{ item.label }}</span>
    </transition>
    
    <!-- Tooltip (仅收起状态显示) -->
    <transition name="tooltip">
      <span v-if="collapsed && isHovering" class="nav-tooltip">
        {{ item.label }}
      </span>
    </transition>
  </button>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  item: {
    type: Object,
    required: true
  },
  active: {
    type: Boolean,
    default: false
  },
  collapsed: {
    type: Boolean,
    default: false
  }
})

defineEmits(['select'])

const isHovering = ref(false)
</script>

<style scoped>
.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  height: var(--sidebar-nav-item-height);
  padding: 0 16px;
  margin: 2px 8px;
  border: none;
  border-radius: var(--radius-nav-item);
  cursor: pointer;
  background: transparent;
  color: var(--text-secondary);
  transition: all var(--transition-fast);
  white-space: nowrap;
  overflow: hidden;
}

.nav-item:hover {
  background: var(--bg-sidebar-hover);
  color: var(--text-primary);
}

.nav-item.active {
  background: var(--bg-sidebar-active);
  color: var(--color-primary);
  font-weight: 600;
}

.nav-item.active .nav-indicator {
  opacity: 1;
  transform: scaleY(1);
}

.nav-indicator {
  position: absolute;
  left: -8px;
  top: 50%;
  transform: translateY(-50%) scaleY(0);
  width: 3px;
  height: 20px;
  background: var(--color-primary);
  border-radius: 2px;
  opacity: 0;
  transition: all var(--transition-fast);
}

.nav-icon {
  font-size: 20px;
  line-height: 1;
  flex-shrink: 0;
}

.nav-label {
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Tooltip 样式 */
.nav-tooltip {
  position: absolute;
  left: calc(100% + 12px);
  top: 50%;
  transform: translateY(-50%) translateX(-4px);
  padding: 6px 12px;
  background: rgba(26, 26, 46, 0.92);
  color: #ffffff;
  font-size: 13px;
  border-radius: var(--radius-tooltip);
  white-space: nowrap;
  z-index: 1000;
  pointer-events: none;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.nav-tooltip::before {
  content: '';
  position: absolute;
  right: 100%;
  top: 50%;
  transform: translateY(-50%);
  border: 6px solid transparent;
  border-right-color: rgba(26, 26, 46, 0.92);
}

.nav-item:hover .nav-tooltip {
  transform: translateY(-50%) translateX(0);
}

.tooltip-enter-active,
.tooltip-leave-active {
  transition: all 200ms ease;
}

.tooltip-enter-from,
.tooltip-leave-to {
  opacity: 0;
  transform: translateY(-50%) translateX(-8px);
}

/* 文字淡入淡出 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 200ms ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

注意: 当前实现中 Tooltip 通过 CSS :hover 显示。如果需要更精确的控制(延迟显示等),可以在 @mouseenter@mouseleave 事件中设置 isHovering

  • Step 2: 验证交互状态

在临时测试页面中渲染多个实例:

  • 默认状态:灰色图标+文字

  • 悬停状态:背景色变浅

  • 激活状态:蓝色背景 + 左侧指示条

  • 收起状态:仅显示图标,悬停显示 tooltip

  • Step 3: Commit

git add src/components/layout/SidebarNavItem.vue
git commit -m "feat: add SidebarNavItem with active state and tooltip support"

Task 5: 实现 SidebarNav 组件

Files:

  • Create: src/components/layout/SidebarNav.vue

目标: 导航菜单容器,遍历渲染导航项列表

  • Step 1: 编写组件代码
<template>
  <nav class="sidebar-nav" role="navigation" aria-label="主导航菜单">
    <SidebarNavItem
      v-for="item in items"
      :key="item.key"
      :item="item"
      :active="activeKey === item.key"
      :collapsed="collapsed"
      @select="handleSelect"
    />
  </nav>
</template>

<script setup>
import SidebarNavItem from './SidebarNavItem.vue'

const props = defineProps({
  items: {
    type: Array,
    required: true
  },
  activeKey: {
    type: String,
    required: true
  },
  collapsed: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits(['select'])

function handleSelect(key) {
  emit('select', key)
}
</script>

<style scoped>
.sidebar-nav {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 8px 0;
}

/* 自定义滚动条 */
.sidebar-nav::-webkit-scrollbar {
  width: 4px;
}

.sidebar-nav::-webkit-scrollbar-track {
  background: transparent;
}

.sidebar-nav::-webkit-scrollbar-thumb {
  background: var(--border-default);
  border-radius: 2px;
}

.sidebar-nav::-webkit-scrollbar-thumb:hover {
  background: var(--text-tertiary);
}
</style>
  • Step 2: 集成测试

在临时页面中使用真实的导航项配置数据测试:

const testItems = [
  { key: 'read', label: '文件查看', icon: '📄' },
  { key: 'manage', label: '文件管理', icon: '📁' },
  { key: 'qa', label: '知识问答', icon: '💬' }
]

预期:正确渲染 3 个导航项,点击可触发 select 事件

  • Step 3: Commit
git add src/components/layout/SidebarNav.vue
git commit -m "feat: add SidebarNav container component"

Task 6: 实现 SidebarUser 组件

Files:

  • Create: src/components/layout/SidebarUser.vue

目标: 用户信息展示区,包含头像、用户名、角色、操作按钮

  • Step 1: 编写组件代码
<template>
  <div class="sidebar-user">
    <div class="user-info">
      <div class="user-avatar">
        {{ userInitial }}
      </div>
      <transition name="fade">
        <div v-show="!collapsed" class="user-details">
          <div class="user-name">{{ displayName }}</div>
          <div class="user-role">角色{{ roleLabel }}</div>
        </div>
      </transition>
    </div>
    
    <transition name="fade">
      <div v-show="!collapsed" class="user-actions">
        <button 
          class="action-btn" 
          @click="$emit('settings')"
          title="设置"
        >
          ⚙️ 设置
        </button>
        <button 
          class="action-btn" 
          @click="handleLogout"
          title="退出登录"
        >
          🚪 退出
        </button>
      </div>
    </transition>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { Modal } from 'ant-design-vue'

const props = defineProps({
  collapsed: {
    type: Boolean,
    default: false
  },
  userInfo: {
    type: Object,
    default: () => ({})
  },
  roleLabel: {
    type: String,
    default: '未知角色'
  }
})

const emit = defineEmits(['logout', 'settings'])

const displayName = computed(() => {
  return props.userInfo?.realName || props.userInfo?.username || '用户'
})

const userInitial = computed(() => {
  const name = displayName.value
  return name.charAt(0).toUpperCase()
})

function handleLogout() {
  Modal.confirm({
    title: '确认退出',
    content: '确定要退出登录吗?',
    okText: '确定',
    cancelText: '取消',
    onOk() {
      emit('logout')
    }
  })
}
</script>

<style scoped>
.sidebar-user {
  padding: 16px;
  border-top: 1px solid var(--border-sidebar);
  background: var(--bg-elevated);
}

.user-info {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}

.user-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--color-primary);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 16px;
  flex-shrink: 0;
}

.user-details {
  overflow: hidden;
  flex: 1;
}

.user-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.user-role {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: 2px;
}

.user-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

.action-btn {
  padding: 6px 12px;
  border: none;
  background: transparent;
  border-radius: 4px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text-secondary);
  transition: all var(--transition-fast);
}

.action-btn:hover {
  background: var(--bg-hover);
  color: var(--text-primary);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 200ms ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
  • Step 2: 验证用户信息显示

传入模拟数据:

{
  userInfo: { realName: '张三' },
  roleLabel: '系统管理员'
}

预期:显示头像(字母"张")、用户名、角色标签

  • Step 3: Commit
git add src/components/layout/SidebarUser.vue
git commit -m "feat: add SidebarUser component with avatar and actions"

Task 7: 实现 MobileMenuButton 组件

Files:

  • Create: src/components/layout/MobileMenuButton.vue

目标: 移动端的汉堡菜单按钮,仅在屏幕宽度 < 768px 时显示

  • Step 1: 编写组件代码
<template>
  <button
    class="mobile-menu-btn"
    @click="$emit('click')"
    aria-label="打开导航菜单"
    type="button"
  >
    <span class="hamburger-icon"></span>
  </button>
</template>

<script setup>
defineEmits(['click'])
</script>

<style scoped>
.mobile-menu-btn {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 1002;
  width: 40px;
  height: 40px;
  border: none;
  background: var(--bg-card);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  cursor: pointer;
  display: none; /* 默认隐藏,通过媒体查询显示 */
  align-items: center;
  justify-content: center;
  font-size: 20px;
  color: var(--text-primary);
  transition: all var(--transition-fast);
}

.mobile-menu-btn:hover {
  background: var(--bg-hover);
  transform: scale(1.05);
}

.mobile-menu-btn:active {
  transform: scale(0.95);
}

@media (max-width: 767px) {
  .mobile-menu-btn {
    display: flex;
  }
}
</style>
  • Step 2: 验证响应式行为

调整浏览器窗口宽度:

  • ≥ 768px按钮隐藏

  • < 768px按钮可见且可点击

  • Step 3: Commit

git add src/components/layout/MobileMenuButton.vue
git commit -m "feat: add MobileMenuButton for responsive navigation"

Task 8: 实现 AppSidebar 主组件

Files:

  • Create: src/components/layout/AppSidebar.vue

目标: 整合所有子组件,实现完整的侧边栏功能,包括桌面端固定模式和移动端 overlay 模式

  • Step 1: 编写主组件代码
<template>
  <!-- 移动端遮罩层 -->
  <transition name="overlay">
    <div 
      v-if="isMobile && mobileOpen" 
      class="mobile-overlay"
      @click="$emit('update:mobileOpen', false)"
      aria-hidden="true"
    ></div>
  </transition>

  <!-- 侧边栏主体 -->
  <aside
    class="sidebar"
    :class="{ 
      collapsed, 
      'mobile-open': isMobile && mobileOpen 
    }"
    role="navigation"
    :aria-label="collapsed ? '收起的导航菜单' : '导航菜单'"
  >
    <SidebarHeader 
      :collapsed="collapsed" 
      @toggle="$emit('update:collapsed', !collapsed)" 
    />
    
    <SidebarNav 
      :items="navItems" 
      :active-key="activeKey"
      :collapsed="collapsed"
      @select="handleNavigate"
    />
    
    <div class="sidebar-spacer"></div>
    
    <SidebarUser 
      :collapsed="collapsed"
      :user-info="userInfo"
      :role-label="roleLabel"
      @logout="$emit('logout')"
      @settings="$emit('settings')"
    />
  </aside>
</template>

<script setup>
import SidebarHeader from './SidebarHeader.vue'
import SidebarNav from './SidebarNav.vue'
import SidebarUser from './SidebarUser.vue'

const props = defineProps({
  collapsed: {
    type: Boolean,
    default: false
  },
  navItems: {
    type: Array,
    required: true
  },
  activeKey: {
    type: String,
    required: true
  },
  userInfo: {
    type: Object,
    default: () => ({})
  },
  roleLabel: {
    type: String,
    default: ''
  },
  isMobile: {
    type: Boolean,
    default: false
  },
  mobileOpen: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits([
  'update:collapsed',
  'update:mobileOpen',
  'navigate',
  'logout',
  'settings'
])

function handleNavigate(key) {
  emit('navigate', key)
  
  // 移动端选择后自动关闭侧边栏
  if (props.isMobile) {
    emit('update:mobileOpen', false)
  }
}
</script>

<style scoped>
.sidebar {
  width: var(--sidebar-width);
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border-sidebar);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: width var(--sidebar-transition-duration) var(--sidebar-transition-easing);
  will-change: width;
  flex-shrink: 0;
}

.sidebar.collapsed {
  width: var(--sidebar-collapsed-width);
}

.sidebar-spacer {
  flex: 1;
  min-height: 0;
}

/* 移动端 Overlay 模式 */
.mobile-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
}

@media (max-width: 767px) {
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    z-index: 1001;
    transform: translateX(-100%);
    box-shadow: var(--shadow-lg);
  }
  
  .sidebar.mobile-open {
    transform: translateX(0);
  }
}

.overlay-enter-active,
.overlay-leave-active {
  transition: opacity 300ms ease;
}

.overlay-enter-from,
.overlay-leave-to {
  opacity: 0;
}
</style>
  • Step 2: 集成测试

在临时页面中组装所有子组件,测试:

  • 展开/收起动画流畅

  • 导航点击事件正确传递

  • 用户信息正确显示

  • 移动端模式下 overlay 行为正常

  • Step 3: Commit

git add src/components/layout/AppSidebar.vue
git commit -m "feat: add AppSidebar main container with responsive support"

Task 9: 改造 Home.vue - 集成侧边栏布局

Files:

  • Modify: src/views/Home.vue

目标: 移除现有的 <a-layout-header>,替换为新的侧边栏布局系统

  • Step 1: 备份当前 Home.vue
cp src/views/Home.vue src/views/Home.vue.backup
  • Step 2: 重写模板部分

将现有的 <template> 替换为:

<template>
  <div class="app-layout">
    <!-- 移动端菜单按钮 -->
    <MobileMenuButton 
      v-if="isMobile"
      @click="openMobile"
    />
    
    <!-- 侧边栏 -->
    <AppSidebar
      v-model:collapsed="collapsed"
      v-model:mobile-open="mobileOpen"
      :nav-items="visibleModules"
      :active-key="currentModule"
      :user-info="userInfo"
      :role-label="currentRoleLabel"
      :is-mobile="isMobile"
      @navigate="handleModuleChange"
      @logout="handleLogout"
      @settings="handleSettings"
    />
    
    <!-- 主内容区域 -->
    <main 
      class="main-content" 
      :class="{ 'sidebar-collapsed': collapsed && !isMobile }"
    >
      <ReadModule 
        v-if="currentModule === 'read'" 
        ref="readModuleRef" 
      />
      <ManageModule 
        v-else-if="currentModule === 'manage'" 
      />
      <QAModule 
        v-else-if="currentModule === 'qa'" 
        ref="qaModuleRef"
      />
      <ExamModule 
        v-else-if="currentModule === 'exam'" 
      />
      <MindModule 
        v-else-if="currentModule === 'mind'" 
      />
      <DashboardModule 
        v-else-if="currentModule === 'dashboard'" 
      />
      <PermissionModule 
        v-else-if="currentModule === 'permission'" 
        @permissions-updated="handlePermissionsUpdated"
        @refresh-modules="handleRefreshModules"
      />
      <div 
        v-else 
        class="empty-state"
      >
        <a-empty description="暂无权限访问任何功能,请联系管理员分配权限" />
      </div>
    </main>
  </div>
</template>

关键变更说明:

  • 删除 <a-layout><a-layout-header>

  • 新增 <div class="app-layout"> 作为根容器

  • 引入 <MobileMenuButton><AppSidebar>

  • 主内容区从 <section> 改为 <main> 并添加动态类名

  • Step 3: 更新脚本部分

<script setup> 中添加导入和使用:

// 新增导入
import { useSidebar } from '../composables/useSidebar'
import AppSidebar from '../components/layout/AppSidebar.vue'
import MobileMenuButton from '../components/layout/MobileMenuButton.vue'

// 使用 composable
const {
  collapsed,
  isMobile,
  mobileOpen,
  toggleCollapse,
  openMobile,
  closeMobile
} = useSidebar()

// 新增方法
function handleSettings() {
  message.info('设置功能开发中...')
}

function handleLogout() {
  // 调用原有的登出逻辑
  console.log('用户请求登出')
  // 可以在这里调用原有的 logout 方法
}

保留不变的部分:

  • 所有模块的导入语句

  • visibleModules 计算属性

  • handleModuleChange 函数

  • 权限检查相关逻辑

  • 用户信息获取逻辑

  • Step 4: 更新样式部分

<style scoped> 替换为:

<style scoped>
.app-layout {
  display: flex;
  height: 100vh;
  overflow: hidden;
}

.main-content {
  flex: 1;
  margin-left: var(--sidebar-width);
  overflow: auto;
  transition: margin-left var(--sidebar-transition-duration) var(--sidebar-transition-easing);
  background: var(--bg-page);
  will-change: margin-left;
}

.main-content.sidebar-collapsed {
  margin-left: var(--sidebar-collapsed-width);
}

.empty-state {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

/* 移动端适配 */
@media (max-width: 767px) {
  .main-content {
    margin-left: 0 !important;
  }
}
</style>

关键变更:

  • 删除 .top-nav-bar, .nav-tab, .nav-icon, .nav-label 等旧样式

  • 新增 .app-layout, .main-content 布局样式

  • 添加响应式断点处理

  • Step 5: 功能验证

逐一测试以下场景:

  1. 页面加载后侧边栏默认展开(或从 localStorage 恢复)
  2. 点击折叠按钮可切换展开/收起
  3. 点击导航项可切换模块内容
  4. 刷新页面后保持上次的状态
  5. 用户信息正确显示
  6. 退出按钮弹出确认对话框
  7. 调整浏览器宽度至 < 768px出现汉堡按钮
  8. 移动端点击汉堡按钮,侧边栏以 overlay 方式滑出
  9. 移动端点击遮罩层或导航项,侧边栏关闭
  • Step 6: 清理备份文件

确认一切正常后删除备份:

rm src/views/Home.vue.backup
  • Step 7: Commit
git add src/views/Home.vue
git commit -m "refactor: replace header navigation with sidebar layout system"

Task 10: 全局样式微调与优化

Files:

  • Modify: src/style.css

目标: 确保全局样式与新侧边栏布局兼容,移除可能的冲突样式

  • Step 1: 检查并移除冲突的全局样式

搜索并评估以下可能影响侧边栏的全局规则:

  • body, html 的 margin/padding 设置
  • 全局的 overflow 规则
  • 可能影响 fixed 定位元素的样式

如果发现冲突,在 style.css 中添加必要的覆盖或修正。

  • Step 2: 添加平滑滚动增强(可选)
/* 全局平滑滚动 */
html {
  scroll-behavior: smooth;
}

/* 优化滚动条样式Webkit */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--border-default);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--text-tertiary);
}
  • Step 3: 验证视觉效果

在不同页面间切换,确保:

  • 无意外的布局偏移

  • 滚动条样式统一

  • 过渡动画流畅自然

  • Step 4: Commit

git add src/style.css
git commit -m "style: global styles adjustment for sidebar compatibility"

Task 11: 最终测试与验收

Files: 无新增/修改(仅测试)

目标: 全面验证所有功能符合设计规范,修复边界情况 bug

  • Step 1: 功能完整性测试清单

使用以下清单逐项验证:

桌面端(≥ 768px:

  • 侧边栏默认展开,显示完整导航项(图标+文字)
  • 点击折叠按钮,侧边栏平滑收起至 56px
  • 收起状态下,鼠标悬停导航项显示 tooltip
  • 点击导航项,主内容区切换到对应模块
  • 激活的导航项有蓝色背景和左侧指示条
  • 用户信息区显示头像、姓名、角色
  • 点击"退出"按钮,弹出确认对话框
  • 刷新页面后侧边栏状态保持localStorage
  • 按 Ctrl+B 可快捷切换(如果实现了)

移动端(< 768px:

  • 侧边栏默认隐藏
  • 左上角显示汉堡菜单按钮(☰)
  • 点击汉堡按钮,侧边栏从左侧滑入
  • 显示半透明黑色遮罩层
  • 点击遮罩层,侧边栏滑出并关闭
  • 点击导航项,切换模块并自动关闭侧边栏
  • 侧边栏内可正常滚动(如果导航项多)

响应式断点:

  • 768px 附近切换无明显闪烁

  • 平板尺寸下表现合理(可根据设计决定是否默认收起)

  • Step 2: 性能测试

使用浏览器 DevTools 检查:

  • 展开/收起动画帧率 ≥ 60fpsPerformance 面板)

  • 无明显的内存泄漏Memory 面板时间线)

  • 首屏加载时间增加 < 200msNetwork 面板)

  • Step 3: 兼容性测试

在以下浏览器中快速验证:

  • Chrome/Edge (最新版)

  • Firefox (最新版)

  • Safari (如果有 Mac/iOS 设备)

  • Step 4: 可访问性基础检查

  • 键盘 Tab 键可在导航项间切换

  • Enter/Space 键可激活导航项

  • 屏幕阅读器可识别当前页面aria-current

  • 颜色对比度满足 WCAG AA 标准

  • Step 5: 边界情况处理

测试以下边缘场景:

  • 快速连续点击折叠按钮(不会卡顿或错乱)

  • 在移动端旋转设备方向(横竖屏切换)

  • localStorage 被禁用或满载时的降级处理

  • 极小屏幕320px下的显示效果

  • 用户没有任何权限时的空状态提示

  • Step 6: 修复发现的问题

如果在上述测试中发现问题,立即修复并记录在案。

  • Step 7: 最终 Commit
git add -A
git commit -m "test: final verification and bug fixes for sidebar navigation"

自我审查清单

Spec 覆盖度检查

设计规范章节 对应任务 状态
§3 整体布局架构 Task 9
§4.2 品牌区设计 Task 3
§4.3 导航菜单设计 Task 4, 5
§4.4 用户信息区设计 Task 6
§5.1 展开/收起机制 Task 2, 8
§5.2 移动端适配 Task 7, 8, 9
§6 性能优化 Task 10, 11
§7 文件结构 所有任务
§8 样式变量 Task 1

占位符扫描

  • 无 TBD 或 TODO
  • 无"实现细节待定"
  • 无"类似其他组件"

类型一致性检查

  • Props 名称在各组件间一致 (collapsed, navItems, activeKey)
  • Event 名称遵循 Vue 3 规范 (update:collapsed, navigate, logout)
  • CSS 变量引用正确(全部来自 design-tokens.css

代码完整性检查

每个 Step 都包含:

  • 实际可执行的代码(非伪代码)
  • 明确的文件路径
  • 验证方法(手动测试或命令输出预期)
  • Git commit 命令

执行建议

预计总工时: ~8 小时(按 2-5 分钟/步计算)

推荐执行顺序: Task 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11

并行可能性:

  • Task 3-7 可由不同开发者并行实现(组件间依赖弱)
  • Task 1-2 必须最先完成(基础设施)

风险缓解:

  • 每个 Task 结束都有 commit 点,可随时回滚
  • Task 9 前有备份机制Home.vue.backup
  • Task 11 提供全面的回归测试清单

计划版本: v1.0
创建日期: 2026-05-14
基于设计规范: docs/superpowers/specs/2026-05-14-sidebar-navigation-design.md