第12次 · 布局容器与栅格系统
第12次 · 布局容器与栅格系统
第1/7页 · 先知道今天要做什么
页面好不好用,第一眼看的不是配色,而是布局:有没有顶栏、有没有侧边栏、内容区怎么分栏、手机上会不会挤成一团。
Element Plus 提供了两套布局工具,各管一件事:
- 容器(Container)系列:管“页面的大结构”——上中下、左中右。
el-container会自动判断方向:里面直接放el-header或el-footer就竖着排,放el-aside就横着排; - 栅格(Row / Col)系列:管“一行里的分栏”——24 栅格制,可以把一行切成 24 份,任取几份。
把这两件组合起来,就能搭出几乎所有后台界面的骨架。今天的目标是:不看教程,能自己画出一个后台布局的结构图,并用代码实现它。
今天的场景
做一个后台管理页的骨架:顶部标题栏、左侧可折叠菜单、右上角用户区、底部版权栏,中间内容区用栅格展示一组“响应式卡片”(窗口变窄时卡片数量自动减少)。
下课时拿出这三样结果
| 做到什么 | 在哪里看 | 完成的证据 |
|---|---|---|
| 会用容器系列搭大结构 | 整个页面 | 顶栏、侧栏、内容区、底栏位置正确,侧栏能折叠。 |
| 会用 24 栅格分栏 | 内容区卡片 | 把窗口从宽拉到窄,卡片从 4 列变 3 列、2 列、1 列。 |
| 说清容器与栅格的分工 | 讲解与自查 | 能一句话说明“什么场合用容器、什么场合用栅格”。 |
你只需要带着这些基础来
第 11 课的内容:会引组件、会用 ElConfigProvider。
本课时 45 分钟怎么上
| 时间 | 老师带着做 | 学生留下什么 |
|---|---|---|
| 0—5 分钟 | 打开一个真实后台系统,让学生指出它的布局分成了几块。 | 建立“布局 = 分区”的印象。 |
| 5—18 分钟 | 讲容器系列:方向自动判断规则、el-aside 宽度、常用属性。 |
能说出为什么 el-container 有时横排有时竖排。 |
| 18—32 分钟 | 讲 24 栅格:el-row / el-col、gutter、offset、响应式断点。 |
会写响应式分栏。 |
| 32—42 分钟 | 粘贴本课 App.vue,拉动窗口宽度观察列数变化。 |
响应式效果可见。 |
| 42—45 分钟 | 抽查:问“一行三栏各占多少 span”。 | 全员提交。 |
从哪里开始
复制第 11 课的工程,改名 lesson-vue-12。不需要新增依赖。
今天先掌握到这里
第 13 课开始逐个讲基础组件(按钮、图标、文字排版)。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第2/7页 · 把新词说清楚
新词与本课含义
| 名字 | 用熟悉的话理解 | 本课里长什么样 |
|---|---|---|
| 容器 | 装页面的“大盒子”,决定上下左右的大结构。 | el-container |
| 顶栏 / 底栏 / 侧栏 / 主区 | 页面的四个功能块。 | el-header / el-footer / el-aside / el-main |
| 栅格 | 把一行切成 24 份,组件占几份就说 span 几。 | el-row + el-col |
gutter |
列与列之间的间隙(只支持横向)。 | :gutter="16" |
offset |
左边空出几份,用来把内容往右推。 | :offset="4" |
| 断点 | 屏幕宽度的分界点,用来做响应式。 | xs <576、sm ≥576、md ≥768、lg ≥992、xl ≥1200、xxl ≥1920 |
容器系列:方向是怎么定的
el-container 会自动判断排列方向,规则很简单:
| 里面的直接子元素 | 排列方向 |
|---|---|
有 el-header 或 el-footer |
竖向(上下堆) |
只有 el-aside 和 el-main(没有 header/footer) |
横向(左右排) |
如果自动判断的结果不是你想要的,可以手写 direction="vertical" 或 "horizontal" 强制指定。
<!-- 典型后台骨架:整体竖向,中间那一横条里再左右分 -->
<el-container>
<el-header>顶栏</el-header>
<el-container>
<el-aside width="200px">侧栏</el-aside>
<el-main>内容区</el-main>
</el-container>
<el-footer>底栏</el-footer>
</el-container>
这就是后台系统最常见的那套结构,记住它。
24 栅格:怎么算
一行固定 24 份。常见分法:
| 想要的效果 | 写法 |
|---|---|
| 两等分 | 两个 :span="12" |
| 三等分 | 三个 :span="8" |
| 四等分 | 四个 :span="6" |
| 左窄右宽(1:3) | :span="6" + :span="18" |
| 居中一块 | :span="8" + :offset="8" |
规则:同一行里所有 span 加起来等于 24,正好占满一行;超过 24 会自动换行。
响应式:断点属性
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<!-- 手机 1 列 → 平板 2 列 → 小屏 3 列 → 大屏 4 列 -->
</el-col>
读法:屏幕越大,越靠后的属性生效(从 xs 往上匹配,取最大匹配项)。上面这行的效果:
| 屏幕宽度 | 实际 span | 一行几列 |
|---|---|---|
| < 576(手机) | 24 | 1 列 |
| ≥ 576(平板) | 12 | 2 列 |
| ≥ 768(小屏) | 8 | 3 列 |
| ≥ 992(大屏) | 6 | 4 列 |
这是“响应式”最省事的实现方式——不需要写任何媒体查询。
只看这几行:一个响应式卡片列表
<el-row :gutter="16">
<el-col v-for="i in 8" :key="i" :xs="24" :sm="12" :md="8" :lg="6">
<el-card>{{ i }}</el-card>
</el-col>
</el-row>
先口头预测:如果我把 :gutter="16" 去掉,卡片之间会怎样?
紧贴在一起,没有间隙。 注意 gutter 只在横向生效(列间距),行与行之间的纵向空隙要靠 el-row 的 style="margin-bottom" 或 el-col 内部的 padding 来处理。这是新手最常踩的一个坑。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第3/7页 · 先看一个小例子
完整代码:App.vue(后台骨架 + 响应式栅格)
<script setup>
import { ref, computed } from 'vue'
import {
ElConfigProvider, ElContainer, ElHeader, ElAside, ElMain, ElFooter,
ElRow, ElCol, ElCard, ElButton, ElTag, ElAvatar, ElDivider,
ElMenu, ElMenuItem, ElRadioGroup, ElRadioButton, ElAlert, ElSpace, ElIcon
} from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
const collapsed = ref(false)
const activeMenu = ref('1')
const density = ref('lg') // 控制栅格密度,方便对比
// 不同密度下的响应式配置
const colProps = computed(() => {
if (density.value === 'xl') return { xs: 24, sm: 12, md: 8, lg: 6, xl: 4 }
if (density.value === 'lg') return { xs: 24, sm: 12, md: 8, lg: 6 }
return { xs: 24, sm: 24, md: 12, lg: 12 }
})
const cards = [
{ title: '今日访问量', value: '1,280', type: 'success' },
{ title: '新增学员', value: '36', type: 'primary' },
{ title: '待批作业', value: '12', type: 'warning' },
{ title: '异常请求', value: '3', type: '' },
{ title: '在线人数', value: '208', type: 'success' },
{ title: '课程总数', value: '46', type: 'primary' },
{ title: '已完成课时', value: '12', type: 'info' },
{ title: '平均完成率', value: '76%', type: 'warning' }
]
const winWidth = ref(window.innerWidth)
window.addEventListener('resize', () => { winWidth.value = window.innerWidth })
</script>
<template>
<el-config-provider :locale="zhCn">
<el-container class="layout">
<el-header class="hd">
<div class="hd-left">
<el-button text @click="collapsed = !collapsed">{{ collapsed ? '展开' : '收起' }}</el-button>
<b>Vue 3 教学后台</b>
</div>
<div class="hd-right">
<el-tag size="small" type="success">在线</el-tag>
<el-avatar :size="28">管</el-avatar>
</div>
</el-header>
<el-container>
<el-aside :width="collapsed ? '64px' : '200px'" class="aside">
<el-menu :default-active="activeMenu" :collapse="collapsed" @select="k => activeMenu = k">
<el-menu-item index="1">概览</el-menu-item>
<el-menu-item index="2">学员管理</el-menu-item>
<el-menu-item index="3">课程管理</el-menu-item>
<el-menu-item index="4">数据统计</el-menu-item>
<el-menu-item index="5">系统设置</el-menu-item>
</el-menu>
</el-aside>
<el-main class="main">
<el-card class="mb">
<template #header>栅格响应式演示(拉动浏览器窗口宽度观察)</template>
<div class="bar">
<span class="label">每列最小宽度:</span>
<el-radio-group v-model="density">
<el-radio-button value="xl">更密(xl 提前生效)</el-radio-button>
<el-radio-button value="lg">默认</el-radio-button>
<el-radio-button value="md">更宽(较晚换行)</el-radio-button>
</el-radio-group>
<span class="label">当前窗口宽度:{{ winWidth }}px</span>
</div>
<el-alert
title="当前 span 配置:见下方代码。窗口 <576 时一行 1 列,≥992 时一行 4 列(默认配置)。"
type="info" :closable="false" show-icon style="margin-top: 10px"
/>
</el-card>
<el-row :gutter="16">
<el-col v-for="c in cards" :key="c.title" v-bind="colProps">
<el-card class="stat" shadow="hover">
<p class="stat-title">{{ c.title }}</p>
<p class="stat-value">{{ c.value }}</p>
<el-tag v-if="c.type" :type="c.type" size="small">状态</el-tag>
</el-card>
</el-col>
</el-row>
<el-card class="mb" style="margin-top: 16px">
<template #header>两栏布局(6 + 18)</template>
<el-row :gutter="16">
<el-col :xs="24" :md="6">
<div class="box">左侧窄栏 span=6</div>
</el-col>
<el-col :xs="24" :md="18">
<div class="box">右侧宽栏 span=18</div>
</el-col>
</el-row>
<el-divider />
<p class="label">居中一块(8 + offset 8)</p>
<el-row :gutter="16">
<el-col :span="8" :offset="8">
<div class="box center">span=8,offset=8</div>
</el-col>
</el-row>
</el-card>
</el-main>
</el-container>
<el-footer class="ft">© 2026 Vue 3 教学后台 · 布局容器与栅格演示</el-footer>
</el-container>
</el-config-provider>
</template>
<style>
html, body, #app {
margin: 0; padding: 0; max-width: none; width: auto; height: auto;
display: block; place-items: initial; text-align: left; background: #f5f7fa;
}
</style>
<style scoped>
.layout { min-height: 100vh; }
.hd {
display: flex; justify-content: space-between; align-items: center;
background: #fff; border-bottom: 1px solid #e4e7ed; padding: 0 16px;
}
.hd-left, .hd-right { display: flex; align-items: center; gap: 12px; }
.aside { background: #fff; border-right: 1px solid #e4e7ed; transition: width 0.2s; }
.main { padding: 16px; }
.ft { display: flex; align-items: center; justify-content: center; color: #909399; font-size: 12px; background: #fff; border-top: 1px solid #e4e7ed; }
.mb { margin-bottom: 16px; }
.bar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.label { color: #606266; font-size: 13px; }
.stat { text-align: center; }
.stat-title { color: #909399; font-size: 13px; margin: 0 0 6px; }
.stat-value { color: #303133; font-size: 22px; font-weight: 600; margin: 0 0 6px; }
.box {
background: #ecf5ff; border: 1px dashed #a0cfff; color: #409eff;
padding: 18px; text-align: center; border-radius: 4px;
}
.box.center { background: #f0f9eb; border-color: #b3e19d; color: #67c23a; }
:deep(.el-col) { margin-bottom: 0; }
</style>
预期看见什么
- 顶部一条白色顶栏:左边“收起”按钮和标题,右边状态标签与头像。
- 左侧白色菜单栏,点“收起”后从 200px 变成 64px,菜单只剩图标(本课菜单未加图标,所以会变成窄条)。
- 中间内容区:一张说明卡 + 8 张统计卡(默认一行 4 张)+ 两栏布局演示 + 居中演示。
- 拉动浏览器窗口宽度:卡片列数会变——窗口很大时 4 列,中等 3 列,小屏 2 列,手机宽度 1 列。
- 切换“每列最小宽度”的单选按钮,同一批卡片的分栏策略改变。
先猜一猜,读完代码再回答
为什么 el-container 里面又套了一个 el-container?
<el-container>
<el-header>...</el-header>
<el-container>
<el-aside>...</el-aside>
<el-main>...</el-main>
</el-container>
<el-footer>...</el-footer>
</el-container>
因为外层要做“上中下”的竖排,内层要做“左中右”的横排。 一个 el-container 只能有一种方向(还带自动判断的规则)。如果只用一层,把 header、aside、main、footer 全放进去,它会因为检测到 header 而整体竖排,侧栏就会跑到顶栏下面去。套一层,就能把不同方向的结构分开——这是容器布局的标准套路。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第4/7页 · 一步一步做出来
第1步:先只做上下两块
<el-container>
<el-header style="background:#fff">顶栏</el-header>
<el-main style="background:#f5f7fa">内容区</el-main>
</el-container>
预期:上下排列。验证方向自动判断:检测到 el-header 所以竖排。
第2步:加侧栏,观察方向变化
<el-container>
<el-aside width="200px">侧栏</el-aside>
<el-main>内容区</el-main>
</el-container>
预期:变成左右排列(因为没有 header/footer)。
第3步:把两种结构套起来
按第 3 页的骨架写完整结构。预期:顶栏在顶部,侧栏在左,内容区在右,底栏在底部。
第4步:让侧栏可折叠
const collapsed = ref(false)
<el-aside :width="collapsed ? '64px' : '200px'">
<el-menu :collapse="collapsed">...</el-menu>
</el-aside>
预期:点按钮侧栏变窄,菜单变成只显示图标的样子(没配图标时会显示成窄条)。
第5步:玩 24 栅格
<el-row :gutter="16">
<el-col :span="12"><div class="box">12</div></el-col>
<el-col :span="12"><div class="box">12</div></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8"><div class="box">8</div></el-col>
<el-col :span="8"><div class="box">8</div></el-col>
<el-col :span="8"><div class="box">8</div></el-col>
</el-row>
预期:第一行两块平分,第二行三块平分。
做一次超 24 的实验:三个 :span="10"(共 30 > 24)。预期:前两块占一行,第三块被挤到下一行。这验证了“同一行最多 24 份”的规则。
第6步:加响应式
把卡片列的 :span="6" 改成 :xs="24" :sm="12" :md="8" :lg="6",然后拉窗口。预期:列数随宽度变化。这一步是本课的核心,务必拉出所有档位看一遍。
第7步:整段替换为本课完整版
把第 3 页的完整 App.vue 覆盖 src/App.vue,把折叠与响应式都试一遍。
本课文件速查
| 位置 | 本次作用 |
|---|---|
外层 el-container |
上中下竖排结构。 |
内层 el-container |
左中右横排结构。 |
el-aside :width |
侧栏宽度,用 collapsed 控制折叠。 |
el-row :gutter="16" |
列间距 16px(只影响横向)。 |
v-bind="colProps" |
把 xs/sm/md/lg 一组响应式属性批量绑上去。 |
window.innerWidth |
显示当前窗口宽度,方便对照断点。 |
完成修改后检查
| 操作或输入 | 预期观察 |
|---|---|
| 点“收起” | 侧栏变窄,内容区变宽。 |
| 拉宽窗口到 1400px | 统计卡一行 4 列。 |
| 缩到约 700px | 一行 3 列。 |
| 缩到约 500px | 一行 2 列。 |
| 缩到手机宽度 | 一行 1 列。 |
| 把一行写成三个 span=10 | 第三个换行。 |
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第5/7页 · 换成自己的代码
你的任务
- 做一个属于自己的后台骨架,要求:顶栏(左标题 + 右用户区)、可折叠侧栏、内容区、底栏,四块齐全。
- 内容区做三种布局演示:四等分(4 张卡)、两栏 1:3、居中块(
offset)。 - 做一张断点对照表贴在页面上:列出当前窗口宽度与“一行几列”,边拉窗口边验证(提示:用
computed根据winWidth算出当前命中的断点名)。 - 加一个纵向间距:给每张卡片加
margin-bottom,说明为什么gutter管不到纵向。 - 把菜单项换成你自己的:至少 6 项,并让选中项高亮(
el-menu的default-active)。
按这张操作单完成
| 步骤 | 改哪里 | 运行后应该看到什么 |
|---|---|---|
| 1 骨架 | 容器系列 | 四块位置正确,侧栏可折叠。 |
| 2 三种布局 | 三处 el-row |
四等分 / 1:3 / 居中都能看出来。 |
| 3 断点对照 | winWidth + computed |
表格实时显示当前命中断点。 |
| 4 纵向间距 | 卡片样式 | gutter=0 时横向紧贴但纵向有间距。 |
| 5 菜单 | el-menu |
6 项可点,选中高亮。 |
最低完成标准:先把这三项做实
| 检查项 | 通过条件 |
|---|---|
| 结构正确 | 内外层容器分工明确,不是靠手写 float / flex 硬凑。 |
| 栅格算得对 | 能说清每一行的 span 之和是多少。 |
| 响应式生效 | 至少三个断点下能看到列数变化。 |
在本地写练习记录
- 画出你页面的结构框图,标出每层用的是哪种容器。
- 记录“span 超过 24”的实验现象。
- 抄下你的响应式属性那一行,并写出各宽度的实际列数。
最后交什么
提交 src/App.vue 与 练习记录.md。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第6/7页 · 出错时从哪里查
把现象和检查动作对上
| 看到什么现象 / 报错 | 先检查哪里 | 修好后怎样确认 |
|---|---|---|
| 侧栏跑到顶栏下面去了 | 把 header 和 aside 放进了同一层 el-container。 |
按“套一层”的写法调整后正常。 |
el-row 里的列紧贴无间隙 |
忘了写 :gutter,或写成了字符串 gutter="16"。 |
改成 :gutter="16" 后出现间隙。 |
| 行与行之间贴在一起 | 以为 gutter 管纵向。 |
给卡片加 margin-bottom 后分开。 |
| 响应式属性没生效 | 属性写成了 sm="12"(没加冒号),传的是字符串。 |
改成 :sm="12" 后生效。 |
| 同一行的列被挤到下一行 | 同行 span 之和超过 24。 | 调整 span 后回到一行。 |
| 页面出现左右滚动条 | 容器内某处固定宽度超出,或 el-row 的负 margin 与外层 padding 冲突。 |
给外层加 padding 或去掉多余固定宽度。 |
| 侧栏折叠后内容错位 | el-menu 没同步 :collapse。 |
加上后正常。 |
排错口诀
方向看不清,看它有没有 header/footer; 想横又想竖,中间套一层容器; 栅格凑 24,超了就换行; gutter 只横不纵,纵向自己加 margin。
不要用这些办法“修复”
- 不要用
position: absolute硬定位侧栏。容器布局已经把结构写好了,硬定位会在响应式下崩掉。 - 不要给
el-col手写width: 25%之类去覆盖栅格,两套宽度逻辑会打架。 - 不要为了“铺满屏幕”到处写
height: 100vh,多个100vh叠加会导致页面出现滚动条。用min-height: 100vh给最外层容器即可。
需要老师帮忙时交这些
你的容器嵌套结构(截图或代码)、el-row / el-col 的写法、窗口宽度、期望的列数与实际列数。
老师巡视时只问三句话
- 为什么这里要套两层容器?(能说出方向不同)
- 这一行的 span 加起来是多少?(能算出来)
- 屏幕上从宽到窄,卡片列数分别是多少?(能对着窗口说出来)
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第7/7页 · 用自己的话讲一遍
回到第 3 页:你刚才猜对了吗?
为什么里面还要再套一个 el-container?
参考解释:el-container 的方向是自动判断的:有 header / footer 就竖排,只有 aside / main 就横排。外层需要竖排(上中下),内层需要横排(左中右),方向不同就必须分层。一个容器只负责一个方向。
回答本课的两个问题
| 问题 | 参考解释 |
|---|---|
| 容器和栅格分别解决什么问题? | 容器解决“页面分成几大块、怎么摆”,栅格解决“某一块内部怎么分栏”。前者是房间隔断,后者是柜子分格。 |
为什么 gutter 只影响横向? |
因为栅格本来就是按“一行 24 份”横向切分的,它的间隙自然是列与列之间的横向空隙;行与行之间属于上下方向,需要自己控制。 |
五个必须能答上来的问题
| 考考自己 | 可以这样通俗回答 |
|---|---|
el-container 什么时候会横向排列? |
直接子元素里没有 el-header 和 el-footer 时。 |
| 一行 24 份,想三等分怎么写? | 三个 :span="8"。 |
| 想把一块内容右移 4 份怎么办? | 用 :offset="4"。 |
| 响应式断点从大到小还是从小到大匹配? | 属性写的是“最小宽度”,屏幕越大越靠后的属性生效。 |
为什么 xs 一般写成 24? |
手机屏幕窄,一行放一列才看得清。 |
一分钟现场演示指南
- 展示结构:点“收起”,说明侧栏与内容区是怎么联动的(
el-aside的宽度变化)。 - 展示响应式:拉窗口,从 4 列到 1 列走一遍,指出断点位置。
- 展示栅格规则:现场改一行 span 让它超过 24,让第三块掉到下一行。
- 收尾一句:“先分区,再分栏。结构对了,样式只是锦上添花。”
交作业前最后检查
src/App.vue与练习记录.md已保存。- 四块结构齐全,侧栏能折叠。
- 至少三个断点可见列数变化。
- 页面没有横向滚动条。
- Console 无红色报错。
课后再做一个小对照
把你的后台骨架改成“只有顶栏、没有侧栏”的横向导航布局(菜单放在顶栏里),内容区仍然用栅格。写下一句话:什么类型的系统适合侧栏式,什么类型适合顶栏式(提示:菜单层级深、项数多时侧栏更合适)。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档