孟凡的博客blog.mengfanyaoapp.com
课程资料库›Vue 3 + Element Plus 前端开发›第12次 · 布局容器与栅格系统
模块总览

第12次 · 布局容器与栅格系统

2026-09-18 · Vue 3 + Element Plus 前端开发 · 课程笔记
课堂练习资料 12-布局容器与栅格系统.zip
下载 ZIP 压缩包

第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>

预期看见什么

  1. 顶部一条白色顶栏:左边“收起”按钮和标题,右边状态标签与头像。
  2. 左侧白色菜单栏,点“收起”后从 200px 变成 64px,菜单只剩图标(本课菜单未加图标,所以会变成窄条)。
  3. 中间内容区:一张说明卡 + 8 张统计卡(默认一行 4 张)+ 两栏布局演示 + 居中演示。
  4. 拉动浏览器窗口宽度:卡片列数会变——窗口很大时 4 列,中等 3 列,小屏 2 列,手机宽度 1 列。
  5. 切换“每列最小宽度”的单选按钮,同一批卡片的分栏策略改变。

先猜一猜,读完代码再回答

为什么 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页 · 换成自己的代码

你的任务

  1. 做一个属于自己的后台骨架,要求:顶栏(左标题 + 右用户区)、可折叠侧栏、内容区、底栏,四块齐全。
  2. 内容区做三种布局演示:四等分(4 张卡)、两栏 1:3、居中块(offset)。
  3. 做一张断点对照表贴在页面上:列出当前窗口宽度与“一行几列”,边拉窗口边验证(提示:用 computed 根据 winWidth 算出当前命中的断点名)。
  4. 加一个纵向间距:给每张卡片加 margin-bottom,说明为什么 gutter 管不到纵向。
  5. 把菜单项换成你自己的:至少 6 项,并让选中项高亮(el-menu 的 default-active)。

按这张操作单完成

步骤 改哪里 运行后应该看到什么
1 骨架 容器系列 四块位置正确,侧栏可折叠。
2 三种布局 三处 el-row 四等分 / 1:3 / 居中都能看出来。
3 断点对照 winWidth + computed 表格实时显示当前命中断点。
4 纵向间距 卡片样式 gutter=0 时横向紧贴但纵向有间距。
5 菜单 el-menu 6 项可点,选中高亮。

最低完成标准:先把这三项做实

检查项 通过条件
结构正确 内外层容器分工明确,不是靠手写 float / flex 硬凑。
栅格算得对 能说清每一行的 span 之和是多少。
响应式生效 至少三个断点下能看到列数变化。

在本地写练习记录

  1. 画出你页面的结构框图,标出每层用的是哪种容器。
  2. 记录“span 超过 24”的实验现象。
  3. 抄下你的响应式属性那一行,并写出各宽度的实际列数。

最后交什么

提交 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。

不要用这些办法“修复”

  1. 不要用 position: absolute 硬定位侧栏。容器布局已经把结构写好了,硬定位会在响应式下崩掉。
  2. 不要给 el-col 手写 width: 25% 之类去覆盖栅格,两套宽度逻辑会打架。
  3. 不要为了“铺满屏幕”到处写 height: 100vh,多个 100vh 叠加会导致页面出现滚动条。用 min-height: 100vh 给最外层容器即可。

需要老师帮忙时交这些

你的容器嵌套结构(截图或代码)、el-row / el-col 的写法、窗口宽度、期望的列数与实际列数。

老师巡视时只问三句话

  1. 为什么这里要套两层容器?(能说出方向不同)
  2. 这一行的 span 加起来是多少?(能算出来)
  3. 屏幕上从宽到窄,卡片列数分别是多少?(能对着窗口说出来)

来源:本课自编 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? 手机屏幕窄,一行放一列才看得清。

一分钟现场演示指南

  1. 展示结构:点“收起”,说明侧栏与内容区是怎么联动的(el-aside 的宽度变化)。
  2. 展示响应式:拉窗口,从 4 列到 1 列走一遍,指出断点位置。
  3. 展示栅格规则:现场改一行 span 让它超过 24,让第三块掉到下一行。
  4. 收尾一句:“先分区,再分栏。结构对了,样式只是锦上添花。”

交作业前最后检查

  • src/App.vue 与 练习记录.md 已保存。
  • 四块结构齐全,侧栏能折叠。
  • 至少三个断点可见列数变化。
  • 页面没有横向滚动条。
  • Console 无红色报错。

课后再做一个小对照

把你的后台骨架改成“只有顶栏、没有侧栏”的横向导航布局(菜单放在顶栏里),内容区仍然用栅格。写下一句话:什么类型的系统适合侧栏式,什么类型适合顶栏式(提示:菜单层级深、项数多时侧栏更合适)。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档