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

1478 lines
32 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 侧边栏导航系统实施计划
> **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: 创建防抖工具函数**
```javascript
// 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` 选择器末尾添加以下变量:
```css
/* ========== 侧边栏系统 ========== */
: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: 验证**
运行开发服务器,确认无语法错误:
```bash
npm run dev
```
预期:正常启动,控制台无错误
- [ ] **Step 4: Commit**
```bash
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 基础结构**
```javascript
// 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>` 中临时添加:
```javascript
import { useSidebar } from './composables/useSidebar'
const { collapsed, isMobile, toggleCollapse } = useSidebar()
// 打开浏览器控制台,手动调用 toggleCollapse() 测试
window.testSidebar = { collapsed, isMobile, toggleCollapse }
```
预期:`window.testSidebar.toggleCollapsed()` 可正确切换状态
- [ ] **Step 3: 清理测试代码**
删除临时代码
- [ ] **Step 4: Commit**
```bash
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: 编写组件模板和脚本**
```vue
<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**
```bash
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: 编写组件完整代码**
```vue
<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**
```bash
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: 编写组件代码**
```vue
<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: 集成测试**
在临时页面中使用真实的导航项配置数据测试:
```javascript
const testItems = [
{ key: 'read', label: '文件查看', icon: '📄' },
{ key: 'manage', label: '文件管理', icon: '📁' },
{ key: 'qa', label: '知识问答', icon: '💬' }
]
```
预期:正确渲染 3 个导航项,点击可触发 select 事件
- [ ] **Step 3: Commit**
```bash
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: 编写组件代码**
```vue
<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: 验证用户信息显示**
传入模拟数据:
```javascript
{
userInfo: { realName: '张三' },
roleLabel: '系统管理员'
}
```
预期:显示头像(字母"张")、用户名、角色标签
- [ ] **Step 3: Commit**
```bash
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: 编写组件代码**
```vue
<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**
```bash
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: 编写主组件代码**
```vue
<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**
```bash
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**
```bash
cp src/views/Home.vue src/views/Home.vue.backup
```
- [ ] **Step 2: 重写模板部分**
将现有的 `<template>` 替换为:
```vue
<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>` 中添加导入和使用:
```javascript
// 新增导入
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>` 替换为:
```css
<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: 清理备份文件**
确认一切正常后删除备份:
```bash
rm src/views/Home.vue.backup
```
- [ ] **Step 7: Commit**
```bash
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: 添加平滑滚动增强(可选)**
```css
/* 全局平滑滚动 */
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**
```bash
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**
```bash
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`