后台布局实战:侧边栏 + 顶栏 + 内容区
1. 为什么这个布局值得单独讲
后端开发者写过无数个 @RestController,但第一次被要求"搭个后台管理系统"时通常卡在页面骨架:
- 侧边栏固定、内容区滚动,怎么做才不抖?
- 顶栏也要固定,侧边栏高度怎么算才不溢出?
- 手机端侧边栏要变成抽屉,切换逻辑写哪?
- 面包屑、Tab 切换、嵌套子页面,怎么和 Vue Router 搭配?
这些问题不是 CSS 难,是没有一个趁手的"骨架模板"。这篇就从 0 拆出来一个能直接用的三段式后台布局。
Java 对照:类似 Spring Boot 的"三层架构模板"——Controller → Service → Dao 的骨架固定,后面所有业务都在填空。后台布局也是一样,骨架固定后每个页面只管内容区。
2. 目标效果
┌─────────────────────────────────────────────────┐
│ 顶栏(固定高度,横贯到顶) │
├──────────┬──────────────────────────────────────┤
│ │ │
│ │ │
│ 侧边栏 │ 内容区(可滚动) │
│ (折叠/ │ │
│ 展开) │ │
│ │ │
│ │ │
└──────────┴──────────────────────────────────────┘
交付标准:
- 顶栏高度固定(如 56px),始终可见
- 侧边栏可折叠(展开 240px,折叠 64px),动画平滑
- 内容区独立滚动,顶栏和侧边栏不跟着动
- 小于 md 断点(768px)时,侧边栏变成抽屉
- 刷新页面能记住折叠状态
3. 骨架:grid 还是 flex?
两种都能做,但我推荐 flex,因为后台布局的各区域高度不对称,grid 模板矩阵反而复杂。
3.1 最简骨架
<!-- src/layouts/AdminLayout.vue -->
<template>
<div class="flex h-screen bg-gray-50 dark:bg-gray-900">
<!-- 侧边栏 -->
<aside class="w-60 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
侧边栏
</aside>
<!-- 右侧:顶栏 + 内容 -->
<div class="flex flex-1 flex-col min-w-0">
<header class="h-14 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
顶栏
</header>
<main class="flex-1 overflow-auto p-6">
内容区(可滚动)
</main>
</div>
</div>
</template>
关键点:
- 最外层
h-screen=height: 100vh,整屏撑满 - 外层
flex(水平分:侧边栏 | 右侧) - 右侧内部
flex-col(垂直分:顶栏 / 内容) - 内容区
flex-1 overflow-auto:自己滚动,别让整页滚动 min-w-0:关键防爆点,见 CSS 基础笔记 §14 — flex 子元素默认min-width: auto,当内容区放超宽表格时会把整个布局撑爆
把上面贴进任何 .vue 文件,骨架就立起来了。
4. 侧边栏折叠
折叠只是"宽度变化 + 过渡动画":
<!-- AdminLayout.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useStorage } from '@vueuse/core'
// 用 useStorage 持久化,刷新后记住状态(VueUse 工具库笔记里有讲)
const collapsed = useStorage('admin-sidebar-collapsed', false)
</script>
<template>
<div class="flex h-screen bg-gray-50 dark:bg-gray-900">
<aside
:class="[
'bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700',
'transition-[width] duration-200 ease-in-out',
collapsed ? 'w-16' : 'w-60'
]"
>
<!-- 侧边栏内容 -->
</aside>
<div class="flex flex-1 flex-col min-w-0">
<header class="flex h-14 items-center gap-3 px-4 bg-white dark:bg-gray-800 border-b">
<button
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
@click="collapsed = !collapsed"
>
<!-- 汉堡图标:三条横线 -->
<el-icon><Fold v-if="!collapsed" /><Expand v-else /></el-icon>
</button>
<span class="font-semibold">in4vue 后台</span>
</header>
<main class="flex-1 overflow-auto p-6">
<slot />
</main>
</div>
</div>
</template>
三处细节:
transition-[width] duration-200— 只对width属性做过渡,避免所有属性变化都动画(防抖动)useStorage来自 VueUse,自动同步 localStorage,刷新页面不会丢- 折叠时宽度用
w-16(64px),刚好放得下一个图标
5. 菜单项:展开时显示文字,折叠时只显示图标
<template>
<aside :class="[...]">
<nav class="py-4">
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
class="flex items-center gap-3 px-4 py-2.5 mx-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
active-class="bg-blue-50 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300"
>
<el-icon class="text-lg shrink-0"><component :is="item.icon" /></el-icon>
<!-- 折叠时隐藏文字 -->
<span v-show="!collapsed" class="truncate">{{ item.label }}</span>
</router-link>
</nav>
</aside>
</template>
<script setup lang="ts">
import { Document, Reading, ChatDotRound, Setting } from '@element-plus/icons-vue'
const menuItems = [
{ path: '/notes', label: '笔记管理', icon: Document },
{ path: '/categories', label: '分类', icon: Reading },
{ path: '/ai-logs', label: 'AI 调用日志', icon: ChatDotRound },
{ path: '/settings', label: '设置', icon: Setting },
]
</script>
关键点:
shrink-0给图标 — 防止折叠过渡中图标被挤变形active-class用router-link内置的激活样式,命中当前路由自动高亮- 折叠时不用
display: none,用v-show即可,文字隐藏但图标位置不变
6. 响应式:小屏变抽屉
桌面端侧边栏常驻,手机端变成从左滑入的抽屉。思路:断点切换实现方式,而不是同一套硬撑两端。
<script setup lang="ts">
import { useStorage, useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)') // md 以下
// 桌面:展开/折叠
const collapsed = useStorage('admin-sidebar-collapsed', false)
// 手机:抽屉开/关
const drawerOpen = ref(false)
</script>
<template>
<div class="flex h-screen bg-gray-50 dark:bg-gray-900">
<!-- 桌面侧边栏 -->
<aside
v-if="!isMobile"
:class="[
'bg-white dark:bg-gray-800 border-r transition-[width] duration-200',
collapsed ? 'w-16' : 'w-60'
]"
>
<SidebarMenu :collapsed="collapsed" />
</aside>
<!-- 手机抽屉:用 Element Plus 的 el-drawer -->
<el-drawer
v-else
v-model="drawerOpen"
direction="ltr"
:with-header="false"
size="240px"
>
<SidebarMenu :collapsed="false" />
</el-drawer>
<!-- 右侧 -->
<div class="flex flex-1 flex-col min-w-0">
<header class="flex h-14 items-center gap-3 px-4 bg-white dark:bg-gray-800 border-b">
<button
class="p-2 rounded hover:bg-gray-100"
@click="isMobile ? (drawerOpen = true) : (collapsed = !collapsed)"
>
<el-icon><Expand /></el-icon>
</button>
<span class="font-semibold">in4vue 后台</span>
</header>
<main class="flex-1 overflow-auto p-4 md:p-6">
<slot />
</main>
</div>
</div>
</template>
三处细节:
useMediaQuery响应式地监听屏幕尺寸,切换断点时自动重渲染- 把
SidebarMenu抽成独立组件,桌面和抽屉共用菜单定义,避免重复 - 抽屉用 Element Plus 现成的,不自己手搓——和 TailwindCSS 笔记 §8 的分工原则一致
7. 和 Vue Router 嵌套路由的配合
后台通常是"多个页面共用一个布局"。路由配置:
// src/router/index.ts
const routes = [
{
path: '/',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
{ path: '', redirect: '/notes' },
{ path: 'notes', component: () => import('@/pages/NoteList.vue') },
{ path: 'notes/:id', component: () => import('@/pages/NoteDetail.vue') },
{ path: 'categories', component: () => import('@/pages/Categories.vue') },
{ path: 'settings', component: () => import('@/pages/Settings.vue') },
],
},
{ path: '/login', component: () => import('@/pages/Login.vue') }, // 独立布局
]
布局组件里用 <router-view> 占位:
<main class="flex-1 overflow-auto p-6">
<router-view />
</main>
Java 对照:类似 Spring 的 @ControllerAdvice + 模板方法——父容器统一处理共同部分(导航、认证校验),子路由只关心自己的业务。
登录页不要布局:把 /login 放外层(不嵌在 AdminLayout 下),它会用自己的全屏居中样式——类似 @RequestMapping 有的不走 filter chain。
8. 面包屑:根据路由自动生成
<!-- AdminLayout.vue 顶栏里 -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { computed } from 'vue'
const route = useRoute()
// 路由 meta 里放 title,面包屑自动组装
const breadcrumbs = computed(() => {
return route.matched
.filter(r => r.meta?.title)
.map(r => ({ title: r.meta.title as string, path: r.path }))
})
</script>
<template>
<header class="flex h-14 items-center gap-4 px-4 bg-white border-b">
<button @click="...">...</button>
<el-breadcrumb separator="/" class="text-sm">
<el-breadcrumb-item
v-for="(item, idx) in breadcrumbs"
:key="item.path"
:to="idx < breadcrumbs.length - 1 ? item.path : undefined"
>
{{ item.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</header>
</template>
路由里加 meta.title:
{
path: 'notes',
component: () => import('@/pages/NoteList.vue'),
meta: { title: '笔记管理' },
children: [
{
path: ':id',
component: () => import('@/pages/NoteDetail.vue'),
meta: { title: '详情' },
},
],
}
访问 /notes/123 时面包屑自动变成:笔记管理 / 详情。
Java 对照:类似 Spring MVC 的 HandlerInterceptor 在请求处理前收集路径信息——route.matched 就是这条请求链上匹配到的所有路由层级。
9. 常见坑点与排查思路
9.1 内容区不能滚动
现象:页面长内容时整个窗口跟着滚,不是内容区独立滚。
原因:漏了某层的 overflow-auto,或者父级没给明确高度。
排查:从最外层往里看,每层必须满足"有明确高度 + 子元素能溢出"。
9.2 侧边栏高度溢出顶栏
现象:侧边栏盖住顶栏,或者顶栏底下露一条白边。
原因:侧边栏放在顶栏外面(并排),但自己撑满了 h-screen 而不是"屏幕高度 - 顶栏高度"。
解法:用第 3 节的"外层 flex(水平)→ 右侧 flex-col(顶栏+内容)"结构,侧边栏自然在顶栏左边。
9.3 折叠动画抖动
现象:折叠/展开时菜单文字跟着抽搐。
原因:transition-all 把所有属性都过渡了,包括 opacity、color 等。
解法:只对 width 做过渡,transition-[width] duration-200。
9.4 Element Plus 组件样式被 Tailwind 的 preflight 重置
现象:<el-button> 看起来没样式或样式怪。
原因:Tailwind 的 preflight 重置了浏览器默认样式,某些情况下覆盖 Element 的基础样式。
解法:保证 main.ts 里 Element Plus 的样式在 style.css(含 Tailwind)之后引入。
10. 完整示例清单
看完这篇,你可以在 in4vue 里这样组织:
src/
├── layouts/
│ ├── AdminLayout.vue ← 本篇产出
│ └── BlankLayout.vue ← 登录页用,什么都不包
├── components/
│ └── admin/
│ ├── SidebarMenu.vue ← 菜单抽出来,抽屉和桌面共用
│ └── Breadcrumb.vue ← 面包屑
├── pages/
│ ├── admin/
│ │ ├── NoteList.vue
│ │ ├── Categories.vue
│ │ └── Settings.vue
│ └── Login.vue
└── router/
└── index.ts ← 嵌套路由
小练习
- 按本篇搭一个
AdminLayout,接入 in4vue 现有的笔记列表页 - 加一个"用户菜单":顶栏右侧头像,点击后弹出下拉菜单(用
el-dropdown) - 在折叠状态下给菜单项加 tooltip:
el-tooltip包住菜单项,鼠标悬停时显示文字 - 加一个"深色模式切换"按钮到顶栏(切换
<html>的.dark类) - 手动把浏览器宽度从 1200px 拖到 400px,观察抽屉切换是否平滑
延伸阅读
- Vue Router - 嵌套路由
- Element Plus - Drawer 抽屉
- Element Plus - Breadcrumb 面包屑
- VueUse - useMediaQuery
- vue-element-admin(国内最流行的后台模板,源码可以翻翻)
- Tailwind UI - Application Shells(付费,但有免费预览,看设计灵感)