1478 lines
32 KiB
Markdown
1478 lines
32 KiB
Markdown
# 侧边栏导航系统实施计划
|
||
|
||
> **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 检查:
|
||
- 展开/收起动画帧率 ≥ 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**
|
||
|
||
```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`
|