32 KiB
侧边栏导航系统实施计划
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: 功能验证
逐一测试以下场景:
- ✅ 页面加载后侧边栏默认展开(或从 localStorage 恢复)
- ✅ 点击折叠按钮可切换展开/收起
- ✅ 点击导航项可切换模块内容
- ✅ 刷新页面后保持上次的状态
- ✅ 用户信息正确显示
- ✅ 退出按钮弹出确认对话框
- ✅ 调整浏览器宽度至 < 768px,出现汉堡按钮
- ✅ 移动端点击汉堡按钮,侧边栏以 overlay 方式滑出
- ✅ 移动端点击遮罩层或导航项,侧边栏关闭
- 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 检查:
-
展开/收起动画帧率 ≥ 60fps(Performance 面板)
-
无明显的内存泄漏(Memory 面板时间线)
-
首屏加载时间增加 < 200ms(Network 面板)
-
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