🎯 你正在阅读「半路出家玩前端」系列文章 🎯
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
- 🔌 接口测试从入门到跑路
- ☕ 一个后端的 JavaEE 续命指南
- 🛜 网络原理续命手册
- ☕ Java项目-轻聊
- ☕ Java项目-企悦抽
✨ 靠热爱去书写自己,靠勇敢去书写生活!
🌟 博主简介:

一、前言
铁汁们,本期我们将会通过一个简单的案例来了解element-plus组件
二、案例介绍-从零搭「用户管理」页
1、程序项目结构

2、程序入口main.js
js
/**
* 入口:接通 Element Plus(对应文档「步骤 1」)
*
* 三件事缺一不可:
* 1) import ElementPlus → 拿到组件库
* 2) import css → 样式,漏了会「有标签没颜值」
* 3) app.use(ElementPlus) → 全局注册,模板才能写 <el-*>
*/
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
3、样式
css
/**
* 后台壳页面基础样式(Element Plus 自带组件样式,这里只管布局留白)
*/
* {
box-sizing: border-box;
}
html,
body,
#app {
margin: 0;
height: 100%;
}
body {
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: #f0f2f5;
color: #303133;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
background: #409eff;
color: #fff;
height: 56px !important;
padding: 0 20px;
}
.page-header h1 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #fff;
}
.page-aside {
background: #fff;
border-right: 1px solid #ebeef5;
display: flex;
flex-direction: column;
overflow: auto;
}
.aside-block {
padding: 12px;
border-top: 1px solid #ebeef5;
}
.aside-block h3 {
margin: 0 0 8px;
font-size: 13px;
color: #909399;
font-weight: 500;
}
.page-main {
padding: 16px 20px 24px;
background: #f0f2f5;
}
.panel {
background: #fff;
padding: 16px 20px;
margin-bottom: 16px;
border-radius: 4px;
}
.panel-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
.pagination-wrap {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.page-footer {
text-align: center;
color: #909399;
font-size: 13px;
background: #fff;
height: 40px !important;
line-height: 40px;
border-top: 1px solid #ebeef5;
}
.hint {
font-size: 13px;
color: #909399;
margin: 0 0 12px;
}
4、根组件app.vue
html
<script setup>
/**
* ============================================================
* 用户管理壳页面 ------ 对照文档《Element-Plus核心组件实战与原理解析》
* 边学边调试:改数据 / 点按钮,在浏览器里看效果;打开控制台看日志
*
* 覆盖步骤:
* 1B Button / Container / Icon
* 2 Table + scope.row
* 3 Pagination(本地 slice,翻页真的会切数据)
* 3B Tree / Tag
* 4 Form + Input + Select + rules
* 5 Menu / Tabs
* 6 Dialog / Message / MessageBox
* ============================================================
*/
import { ref, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 步骤 1B:图标要单独安装 @element-plus/icons-vue,再按名引入
import { Edit, Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
// ---------- 步骤 5:Menu 当前项(字符串要和 el-menu-item 的 index 一致)----------
const activeMenu = ref('users')
const onMenuSelect = (index) => {
activeMenu.value = index
// 真实项目里常:router.push(...);这里只改状态方便调试
ElMessage.info(`切换菜单:${index}`)
}
// ---------- 步骤 5:Tabs 当前面板 ----------
const activeTab = ref('base')
const onTabChange = (name) => {
console.log('[Tabs] 切到', name)
}
// ---------- 步骤 3B:Tree(字段用 name,需 :props 映射)----------
const treeProps = {
label: 'name',
children: 'children',
}
const deptTree = ref([
{
id: 1,
name: '总公司',
children: [
{ id: 11, name: '研发部' },
{
id: 12,
name: '市场部',
children: [{ id: 121, name: '华东组' }],
},
],
},
])
/** 当前选中的部门名;点树节点后可用来过滤表格(演示用) */
const selectedDept = ref('')
const onTreeClick = (data) => {
selectedDept.value = data.name
console.log('[Tree] 点击节点', data)
ElMessage.success(`筛选部门:${data.name}`)
}
// ---------- 步骤 3B:Tag(可关闭要自己 splice)----------
const tags = ref(['全部', '在职', '试用'])
const removeTag = (index) => {
tags.value.splice(index, 1)
}
// ---------- 步骤 2 + 3:表格全量数据 + 分页状态 ----------
/** 全量数据(模拟后端库);真正给表格看的是下面的 pagedData */
const allUsers = ref([
{ id: 1, date: '2026-03-01', name: '张三', role: 'admin', address: '上海市普陀区', dept: '研发部' },
{ id: 2, date: '2026-03-02', name: '李四', role: 'user', address: '北京市朝阳区', dept: '市场部' },
{ id: 3, date: '2026-03-03', name: '王五', role: 'guest', address: '广州市天河区', dept: '华东组' },
{ id: 4, date: '2026-03-04', name: '赵六', role: 'user', address: '深圳市南山区', dept: '研发部' },
{ id: 5, date: '2026-03-05', name: '钱七', role: 'admin', address: '杭州市西湖区', dept: '市场部' },
{ id: 6, date: '2026-03-06', name: '孙八', role: 'user', address: '成都市高新区', dept: '研发部' },
{ id: 7, date: '2026-03-07', name: '周九', role: 'guest', address: '武汉市洪山区', dept: '华东组' },
{ id: 8, date: '2026-03-08', name: '吴十', role: 'user', address: '南京市鼓楼区', dept: '市场部' },
{ id: 9, date: '2026-03-09', name: '郑十一', role: 'user', address: '苏州市工业园', dept: '研发部' },
{ id: 10, date: '2026-03-10', name: '王十二', role: 'admin', address: '重庆市渝中区', dept: '市场部' },
{ id: 11, date: '2026-03-11', name: '冯十三', role: 'guest', address: '西安市雁塔区', dept: '华东组' },
{ id: 12, date: '2026-03-12', name: '陈十四', role: 'user', address: '长沙市岳麓区', dept: '研发部' },
])
const currentPage = ref(1)
const pageSize = ref(5)
/**
* 先按部门过滤,再算总数、再 slice 当前页
* ------ 对应文档踩坑:「分页点了表格不变」→ 必须在这里真正改展示数据
*/
const filteredUsers = computed(() => {
if (!selectedDept.value || selectedDept.value === '总公司') {
return allUsers.value
}
return allUsers.value.filter((u) => u.dept === selectedDept.value)
})
const total = computed(() => filteredUsers.value.length)
/** 当前页要渲染的行 */
const pagedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredUsers.value.slice(start, start + pageSize.value)
})
const onSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1 // 改每页条数后通常回到第 1 页
console.log('[Pagination] pageSize =', size)
}
const onCurrentChange = (page) => {
currentPage.value = page
console.log('[Pagination] currentPage =', page)
}
const clearDeptFilter = () => {
selectedDept.value = ''
currentPage.value = 1
ElMessage.info('已清除部门筛选')
}
// ---------- 步骤 1B:工具条 loading 演示 ----------
const saving = ref(false)
const onToolbarSave = () => {
saving.value = true
setTimeout(() => {
saving.value = false
ElMessage.success('模拟保存完成')
}, 800)
}
// ---------- 步骤 4:Form + Input + Select + rules ----------
const ruleForm = reactive({
name: '',
role: '', // Select 存的是 option 的 value,不是中文 label
address: '',
dept: '研发部',
})
const roleOptions = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
{ label: '访客', value: 'guest' },
]
const roleLabel = (value) => {
const hit = roleOptions.find((o) => o.value === value)
return hit ? hit.label : value
}
const rules = reactive({
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
})
/** 对应模板 ref="formRef",才能调用 validate / resetFields */
const formRef = ref()
let nextId = 100
const submitForm = () => {
formRef.value.validate((valid) => {
if (!valid) {
ElMessage.warning('请先通过表单校验')
return
}
allUsers.value.unshift({
id: nextId++,
date: new Date().toISOString().slice(0, 10),
name: ruleForm.name,
role: ruleForm.role,
address: ruleForm.address,
dept: ruleForm.dept || '研发部',
})
currentPage.value = 1
formRef.value.resetFields()
ElMessage.success('新增成功,已插入表格第一页')
})
}
const resetForm = () => {
formRef.value.resetFields()
}
// ---------- 步骤 6:Dialog 编辑 ----------
const dialogVisible = ref(false)
const editForm = reactive({ name: '', role: '', address: '', dept: '' })
/** 正在编辑的那一行对象引用(保存时直接改它,表格会响应式刷新) */
let editingRow = null
const openEdit = (row) => {
editingRow = row
editForm.name = row.name
editForm.role = row.role
editForm.address = row.address
editForm.dept = row.dept
dialogVisible.value = true
}
const saveEdit = () => {
if (editingRow) {
editingRow.name = editForm.name
editingRow.role = editForm.role
editingRow.address = editForm.address
editingRow.dept = editForm.dept
}
dialogVisible.value = false
ElMessage.success('保存成功')
}
// ---------- 步骤 6:MessageBox 删除确认 ----------
const onDelete = (row) => {
ElMessageBox.confirm(`确定删除「${row.name}」吗?`, '提示', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
const idx = allUsers.value.findIndex((u) => u.id === row.id)
if (idx !== -1) {
allUsers.value.splice(idx, 1)
}
// 删光当前页时回退一页,避免空白页
if (pagedData.value.length === 0 && currentPage.value > 1) {
currentPage.value -= 1
}
ElMessage.success('已删除')
})
.catch(() => {
// 点取消:什么都不做
})
}
</script>
<template>
<!--
步骤 1B:Container 定骨架
header / aside / main / footer ------ 后台壳子不必手写 flex CSS
-->
<el-container style="height: 100%">
<!-- ========== 顶栏 ========== -->
<el-header class="page-header">
<h1>用户管理 · Element Plus 实战调试页</h1>
<!-- 步骤 1B:Button + Icon -->
<div class="toolbar" style="margin: 0">
<el-button type="primary" :icon="Plus" size="small" @click="activeTab = 'base'">
去新增
</el-button>
<el-button
type="success"
size="small"
:icon="Refresh"
:loading="saving"
@click="onToolbarSave"
>
模拟保存
</el-button>
</div>
</el-header>
<el-container>
<!-- ========== 侧栏:Menu + Tree ========== -->
<el-aside width="220px" class="page-aside">
<!-- 步骤 5:Menu;default-active 必须和 index 字符串一致 -->
<el-menu :default-active="activeMenu" @select="onMenuSelect">
<el-menu-item index="users">用户管理</el-menu-item>
<el-menu-item index="roles">角色管理</el-menu-item>
<el-sub-menu index="sys">
<template #title>系统设置</template>
<el-menu-item index="sys-log">操作日志</el-menu-item>
<el-menu-item index="sys-cfg">参数配置</el-menu-item>
</el-sub-menu>
</el-menu>
<div class="aside-block">
<h3>部门树(步骤 3B)</h3>
<p class="hint">点击节点过滤表格;「总公司」显示全部</p>
<!--
:data 树数据
:props 字段映射(后端叫 name,不是默认 label)
node-key 唯一 id
-->
<el-tree
:data="deptTree"
:props="treeProps"
node-key="id"
default-expand-all
highlight-current
@node-click="onTreeClick"
/>
<el-button
v-if="selectedDept"
text
type="primary"
size="small"
style="margin-top: 8px"
@click="clearDeptFilter"
>
清除筛选(当前:{{ selectedDept }})
</el-button>
</div>
</el-aside>
<!-- ========== 主区 ========== -->
<el-main class="page-main">
<!-- 非「用户管理」菜单时给个占位,方便理解 Menu 状态 -->
<div v-if="activeMenu !== 'users'" class="panel">
<p class="panel-title">菜单演示区</p>
<p class="hint">
当前 activeMenu = <code>{{ activeMenu }}</code>
------ 真实项目在这里换路由或换组件。点左侧「用户管理」回到主功能。
</p>
</div>
<template v-else>
<!-- 步骤 3B:Tag -->
<div class="panel">
<p class="panel-title">状态 Tag(可关闭)</p>
<div class="tag-row">
<el-tag>默认</el-tag>
<el-tag type="success">成功</el-tag>
<el-tag type="warning">警告</el-tag>
<el-tag type="danger">危险</el-tag>
<el-tag
v-for="(tag, i) in tags"
:key="tag"
closable
type="info"
@close="removeTag(i)"
>
{{ tag }}
</el-tag>
<el-icon :size="18" color="#409eff"><Search /></el-icon>
</div>
</div>
<!-- 步骤 5:Tabs -->
<el-tabs v-model="activeTab" class="panel" @tab-change="onTabChange">
<!-- ---- 基本信息:表单 + 表格 + 分页 ---- -->
<el-tab-pane label="基本信息" name="base">
<!-- 步骤 4:Form -->
<p class="panel-title">新增用户(Form / Input / Select)</p>
<p class="hint">
空提交看红字校验;Select 写入的是 value(如 admin),表格用 roleLabel 转中文。
</p>
<el-form
ref="formRef"
:model="ruleForm"
:rules="rules"
label-width="80px"
style="max-width: 520px; margin-bottom: 20px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="ruleForm.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select
v-model="ruleForm.role"
placeholder="请选择角色"
style="width: 100%"
>
<el-option
v-for="opt in roleOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="ruleForm.address" placeholder="请输入地址" />
</el-form-item>
<el-form-item label="部门">
<el-select v-model="ruleForm.dept" style="width: 100%">
<el-option label="研发部" value="研发部" />
<el-option label="市场部" value="市场部" />
<el-option label="华东组" value="华东组" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
<!-- 步骤 2:Table;:data 必须是「当前页数组」pagedData -->
<p class="panel-title">用户表格</p>
<p class="hint">
操作列用 #default 插槽拿 scope.row;编辑走 Dialog,删除走 MessageBox。
</p>
<el-table :data="pagedData" border stripe style="width: 100%">
<el-table-column prop="date" label="日期" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column label="角色" width="110">
<template #default="scope">
<!-- scope.row = 这一行的对象 -->
<el-tag
:type="
scope.row.role === 'admin'
? 'danger'
: scope.row.role === 'user'
? 'success'
: 'info'
"
size="small"
>
{{ roleLabel(scope.row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="dept" label="部门" width="100" />
<el-table-column prop="address" label="地址" min-width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
:icon="Edit"
@click="openEdit(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
link
:icon="Delete"
@click="onDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 步骤 3:Pagination(和外层 table 同属一个根结构,不会多根报错) -->
<div class="pagination-wrap">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onSizeChange"
@current-change="onCurrentChange"
/>
</div>
</el-tab-pane>
<!-- ---- 权限 / 日志:占位,体会 Tabs 只切面板 ---- -->
<el-tab-pane label="权限" name="perm">
<p class="hint">
当前 activeTab = <code>{{ activeTab }}</code>
。可在切换时按需请求权限数据(见 onTabChange 控制台日志)。
</p>
<el-button type="primary" plain>演示按钮(步骤 1B)</el-button>
<el-button type="warning" plain>警告</el-button>
<el-button type="danger" plain>危险</el-button>
</el-tab-pane>
<el-tab-pane label="操作日志" name="log">
<p class="hint">日志面板占位。打开控制台可看 tab-change 输出。</p>
</el-tab-pane>
</el-tabs>
</template>
</el-main>
</el-container>
<el-footer class="page-footer">
对照文档步骤调试 · 改 script 数据即可看表格/分页/树联动
</el-footer>
</el-container>
<!--
步骤 6:Dialog
v-model 绑布尔;打开时把行数据拷进 editForm,保存时写回 editingRow
-->
<el-dialog v-model="dialogVisible" title="编辑用户" width="480px">
<el-form :model="editForm" label-width="80px">
<el-form-item label="姓名">
<el-input v-model="editForm.name" />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="editForm.role" style="width: 100%">
<el-option
v-for="opt in roleOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="部门">
<el-input v-model="editForm.dept" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="editForm.address" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</template>
</el-dialog>
</template>
5、效果

- 上中下布局 + 按钮/图标工具条
- 用户表格 + 分页
- 左侧部门树 / 顶部状态 Tag
- 带 Input、Select 与 rules 的录入表单
- 顶部菜单或内容区 Tabs 切换
- 编辑用 Dialog;删除用 MessageBox;结果用 Message
三、知识介绍
§0 先建立概念:它是什么、干什么用
官网
https://element-plus.org/zh-CN/component/overview
概念说明
Element Plus 是基于 Vue 3 的开源 UI 组件库(饿了么前端团队;旧版叫 Element UI,Plus 是增强版)。
把它想成 搭积木 :页面是成品模型,按钮、表单、表格、分页、对话框是现成积木块。标签普遍以 el- 开头(如 el-button、el-table)。浏览器本身不认识这些标签,必须先注册组件库 + 引入样式,才会被编译成真实 DOM。

核心 :页面由大量
el-*积木拼成;日常路径是「官网复制 → 粘贴 → 按业务改」。
适用场景
- 中后台管理系统(表格 + 分页 + 表单是刚需)
- 要统一视觉、少写 CSS 的 Vue3 项目
- 快速出可交互原型
注意事项
| 坑 | 说明 |
|---|---|
| 只装包不注册 | 页面空白或未知自定义元素 |
| 忘记引入 CSS | 结构在,样式全乱 |
| 官网示例是 TS | 选 JS 项目时要删掉类型标注;选 TS 可少改 |
| IE | Vue3 / Element Plus 不支持 IE |
组件分类速览------本期全部展开
| 分类 | 代表 | 介绍位置 |
|---|---|---|
| 基础 | Button、Container、Icon | 步骤 1B |
| 表单 | Form、Input、Select | 步骤 4 |
| 数据 | Table、Pagination、Tree、Tag | 步骤 2 / 3 / 3B |
| 导航 | Menu、Tabs | 步骤 5 |
| 反馈 | Dialog、Message、MessageBox | 步骤 6 |
步骤 1:建项目并「接通」Element Plus
这一步要做什么
- Vite 创建 Vue 项目
npm i element-plus- 在入口 全局注册 + 引入样式
- 清空默认 HelloWorld,留一个空页面壳
对应命令与代码
bash
npm create vite@latest
cd 你的项目名
npm i
npm i element-plus
npm run dev
编写顺序(入口文件,JS 版示例):
javascript
// src/main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// ① 引入组件库本体
import ElementPlus from 'element-plus'
// ② 引入样式(漏了会「有标签没颜值」)
import 'element-plus/dist/index.css'
const app = createApp(App)
// ③ 全局注册:之后任意 .vue 都可直接写 <el-xxx>
app.use(ElementPlus)
app.mount('#app')

html
<!-- src/App.vue:先留空壳 -->
<script setup>
// 后续步骤往这里加表格 / 分页 / 表单
</script>
<template>
<div class="page">
<h2>用户列表(Element Plus 实战)</h2>
<!-- 步骤 2 起往下填 -->
</div>
</template>

写完实现了什么功能
- 项目能跑
- 任意页面可直接使用
el-*标签 - 样式生效
图解:注册时发生了什么

注意 :
use(ElementPlus)把组件名挂进应用;漏掉这一步,模板里的el-table无法正确渲染。
text
T0 npm 装好包 → node_modules 有代码,但应用还不知道
T1 import ElementPlus / css → 模块进入内存
T2 app.use(ElementPlus) → 批量注册 el-button、el-table...
T3 mount → 模板编译遇到 el-* → 找到已注册组件 → 渲染真实 DOM
架构关系:
text
main.js(注册 Element Plus)
└─ App.vue(写 el-* 积木)
└─ 浏览器 DOM(最终可见 UI)
步骤 1B:基础组件 ------ Button、Container、Icon

注意:Container 定页面骨架;Button 触发操作;Icon 是 SVG 积木,需单独安装图标包。
Button(按钮)
| 层 | 内容 |
|---|---|
| 概念 | 统一样式的可点击操作入口 |
| 场景 | 提交、重置、删除、工具栏 |
| 踩坑 | type 只影响样式,不自动提交原生 form;要在 @click 里写逻辑 |
html
<template>
<!-- type:视觉语义;size:尺寸;:icon:可配图标组件 -->
<el-button>默认</el-button>
<el-button type="primary">主要</el-button>
<el-button type="success">成功</el-button>
<el-button type="warning">警告</el-button>
<el-button type="danger">危险</el-button>
<el-button type="info" plain>信息-朴素</el-button>
<el-button type="primary" size="small" :loading="saving" @click="onSave">
保存
</el-button>
</template>
<script setup>
import { ref } from 'vue'
const saving = ref(false)
const onSave = () => {
saving.value = true
setTimeout(() => (saving.value = false), 800)
}
</script>

结果:

底层一小步 :loading 绑的是布尔 ref;为 true 时组件内部换加载态 UI,栈上仍是同一个 ref,堆上布尔值翻转 → 视图更新。
| 常用属性 | 含义 |
|---|---|
type |
primary / success / warning / danger / info |
size |
large / default / small |
plain / disabled / loading |
朴素 / 禁用 / 加载中 |
Container(布局容器)
| 层 | 内容 |
|---|---|
| 概念 | 现成的上/下/左/右/中布局积木 |
| 场景 | 后台外壳:顶栏 + 侧栏 + 主内容 |
| 踩坑 | 官网整段复制即可;注意只有一套 el-container 嵌套关系,别漏闭合标签 |
html
<template>
<!-- 垂直:头 - 体 - 脚 -->
<el-container style="height: 360px; border: 1px solid #eee">
<el-header style="background: #409eff; color: #fff">顶栏 Header</el-header>
<el-container>
<el-aside width="200px" style="background: #d3dce6">侧栏 Aside</el-aside>
<el-main style="background: #e9eef3">主区 Main(表格/表单放这里)</el-main>
</el-container>
<el-footer style="background: #b3c0d1">底栏 Footer</el-footer>
</el-container>
</template>
效果

text
T0 模板声明 el-container 树(虚拟 DOM)
T1 注册过的组件渲染成带 flex 布局的真实 DOM
T2 你只需往 el-main 塞业务(table/form),壳子不用手写 CSS 骨架
| 子组件 | 作用 |
|---|---|
el-header |
顶栏 |
el-aside |
侧栏(可设 width) |
el-main |
主内容 |
el-footer |
底栏 |
Icon(图标)
| 层 | 内容 |
|---|---|
| 概念 | SVG 图标集合,当组件用 |
| 场景 | 按钮旁图标、菜单折叠态只显示图标 |
| 踩坑 | 图标包要另外安装 :npm i @element-plus/icons-vue;用前在 script 里 import 具体图标 |
bash
npm i @element-plus/icons-vue
html
<script setup>
// 按需引入具体图标组件
import { Edit, Delete, Search } from '@element-plus/icons-vue'
</script>
<template>
<el-icon :size="20"><Search /></el-icon>
<el-button type="primary" :icon="Edit">编辑</el-button>
<el-button type="danger" :icon="Delete" circle />
</template>
效果

底层 :Edit 是一个组件定义(堆上的对象);:icon="Edit" 把组件引用传给 Button,由 Button 内部再渲染成 SVG。不是图片 URL 字符串。
步骤 2:粘贴并改通「表格 Table」
这一步要做什么
从官网 Table 示例复制「带编辑/删除操作」一类代码,粘到 App.vue,改成可运行的 JS (去掉 TS 类型),绑定本地数组 tableData。
概念:表格组件
| 层 | 说明 |
|---|---|
| 概念 | 用结构化行列展示多条同类数据 |
| 场景 | 用户列表、订单列表、后台查询结果 |
| 注意 | 数据必须是数组;自定义列常用插槽拿「当前行」 |
对应代码(编写顺序:先数据 → 再模板列 → 再操作按钮)
html
<script setup>
import { ref } from 'vue'
/**
* tableData:表格的「数据源」
* 存在组件内存里(ref → 堆上的数组)
* el-table 通过 :data 读取它;数组变了,表格自动重画
*/
const tableData = ref([
{ date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
{ date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
{ date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
{ date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
])
/** 编辑:参数 index=下标,row=这一行对象(与数组里某一项是同一引用) */
const handleEdit = (index, row) => {
console.log('编辑', index, row)
}
/** 删除:演示从数组删掉一行 → 视图同步少一行 */
const handleDelete = (index, row) => {
console.log('删除', index, row)
tableData.value.splice(index, 1)
}
</script>
<template>
<div class="page">
<h2>用户列表(Element Plus 实战)</h2>
<!--
el-table:表格容器
:data 必须绑数组;prop 对应对象字段名
-->
<el-table :data="tableData" style="width: 100%" border>
<el-table-column prop="date" label="日期" width="180" />
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="address" label="地址" />
<!-- 自定义列:插槽拿到当前行 -->
<el-table-column label="操作" width="200">
<!--
#default="scope":默认插槽
scope.row = 当前行数据对象
scope.$index = 当前行下标
-->
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">
编辑
</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(scope.$index, scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
写完实现了什么功能

- 页面出现四行示例数据
- 点「删除」少一行
- 点「编辑」控制台打印该行对象
图解:数据如何从数组走到单元格

注意 :
tableData数组 →:data→ 每行渲染 → 插槽里scope.row就是该行对象。
text
T0 script 执行
栈:tableData(ref 引用)
堆:数组 [ {date,name,address}, ... ]
T1 模板渲染 el-table
组件读取 tableData(模板自动解包 ref)
按数组长度生成行
T2 普通列 prop="name"
等价于显示 row.name
T3 操作列 #default="scope"
scope = { row: 当前对象, $index: 下标, ... }
点击按钮 → handleDelete(index, row)
splice 改堆上的数组 → 依赖更新 → 表格少一行
常见报错 :Cannot read properties of undefined (reading 'row')
→ 多半是插槽写法不对,或组件库没注册成功导致表格结构异常。先确认步骤 1 的 use + CSS,再 console.log(scope)。
Table 速查
| 属性/写法 | 作用 |
|---|---|
:data |
绑定数据数组 |
prop |
列对应字段名 |
label |
列表头文字 |
#default="scope" |
自定义单元格,用 scope.row |
border / stripe |
边框 / 斑马纹 |
步骤 3:在表格下方加「分页 Pagination」
这一步要做什么
官网复制 el-pagination,放在表格下面;模板保持单根节点 (外面再包一层 div)。用本地变量模拟 total / pageSize / currentPage。
概念:分页组件
| 层 | 说明 |
|---|---|
| 概念 | 把大数据拆成多页浏览的控件 |
| 场景 | 表格查询、列表接口分页(pageNum + pageSize) |
| 注意 | 分页条本身不自动切表格数据;要在事件里改页码并(通常)重新请求/切片 |
对应代码(在步骤 2 基础上追加)
html
<script setup>
import { ref } from 'vue'
const tableData = ref([/* 同上四条... */])
// ---------- 分页状态(都在内存里,改了 UI 会跟着变)----------
const currentPage = ref(1) // 当前第几页
const pageSize = ref(10) // 每页几条
const total = ref(400) // 总条数(真实项目来自后端)
const handleSizeChange = (val) => {
// 用户改了「每页条数」
pageSize.value = val
currentPage.value = 1 // 常见做法:回到第一页
console.log('pageSize →', val)
// 真实项目:这里重新请求列表
}
const handleCurrentChange = (val) => {
// 用户点了页码 / 上一页下一页
currentPage.value = val
console.log('currentPage →', val)
// 真实项目:带新页码请求后端
}
const handleEdit = (index, row) => console.log('编辑', index, row)
const handleDelete = (index, row) => tableData.value.splice(index, 1)
</script>
<template>
<div class="page">
<h2>用户列表(Element Plus 实战)</h2>
<el-table :data="tableData" style="width: 100%" border>
<!-- 列定义同步骤 2 -->
<el-table-column prop="date" label="日期" width="180" />
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="address" label="地址" />
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--
layout:控制显示哪些零件
total / sizes / prev / pager / next / jumper
-->
<el-pagination
style="margin-top: 16px"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
写完实现了什么功能

- 表格下方出现分页条
- 改每页条数、翻页时控制台有日志,状态变量更新
图解 :分页状态时间线

注意 :分页改的是
currentPage/pageSize;表格数据要你在事件里同步更新(请求或本地 slice)。
text
T0 currentPage=1, pageSize=10, total=400(三个 ref 在堆上)
T1 用户选择每页 20 条
→ 触发 size-change
→ pageSize=20,常重置 currentPage=1
→(项目中)带新参数请求接口 → 新数组赋给 tableData
T2 用户点第 3 页
→ 触发 current-change
→ currentPage=3
→ 再请求或 slice 第 3 页数据 → 更新 tableData
→ el-table 按新数组重渲染
Pagination 速查
| 属性/事件 | 作用 |
|---|---|
total |
总条数 |
page-size / v-model:page-size |
每页条数 |
current-page / v-model:current-page |
当前页 |
page-sizes |
每页条数下拉选项 |
layout |
显示哪些控件 |
@size-change |
每页条数变化 |
@current-change |
当前页变化 |
踩坑 :模板多个根节点会报错 → 外层包一个 div。
步骤 3B:数据展示进阶 ------ Tree、Tag

注意:Tree 吃「树形数组」;Tag 吃「标签列表」。都是「数据 → 组件 → DOM」,只是数据结构不同。
Tree(树形控件)
| 层 | 内容 |
|---|---|
| 概念 | 把带 children 的层级数据渲染成可展开树 |
| 场景 | 部门树、权限树、文件目录 |
| 踩坑 | 节点字段名默认是 label / children;若后端叫 name / kids,要用 :props 映射 |
html
<template>
<!-- data:树形数组;props:字段映射;node-key:唯一键;show-checkbox:多选 -->
<el-tree
:data="deptTree"
:props="treeProps"
node-key="id"
show-checkbox
default-expand-all
@check-change="onCheck"
/>
</template>
<script setup>
import { ref } from 'vue'
const treeProps = {
label: 'name', // 显示文字用节点的 name 字段
children: 'children'
}
const deptTree = ref([
{
id: 1,
name: '总公司',
children: [
{ id: 11, name: '研发部' },
{ id: 12, name: '市场部', children: [{ id: 121, name: '华东组' }] }
]
}
])
const onCheck = (data, checked) => {
console.log('节点', data.name, '选中?', checked)
}
</script>

text
T0 deptTree = 带 children 的树(堆上嵌套对象)
T1 el-tree 递归把每个节点渲染成 DOM 行
T2 勾选触发 check-change → 你拿到节点数据做权限/筛选
| 常用 API | 含义 |
|---|---|
:data |
树数据 |
:props |
{ label, children } 字段映射 |
node-key |
节点唯一 id |
show-checkbox |
显示复选框 |
@node-click / @check-change |
点击 / 勾选 |
Tag(标签)
| 层 | 内容 |
|---|---|
| 概念 | 小块彩色文字标记,可关闭 |
| 场景 | 状态、分类、可删除的筛选项 |
| 踩坑 | 可关闭标签要在 @close 里自己从数组 splice,组件不会自动改你的数据 |
html
<template>
<el-tag>默认</el-tag>
<el-tag type="success">成功</el-tag>
<el-tag type="warning">警告</el-tag>
<el-tag type="danger">危险</el-tag>
<el-tag type="info">信息</el-tag>
<!-- 可关闭:v-for + @close -->
<el-tag
v-for="(tag, i) in tags"
:key="tag"
closable
style="margin-right: 8px"
@close="tags.splice(i, 1)"
>
{{ tag }}
</el-tag>
</template>
<script setup>
import { ref } from 'vue'
const tags = ref(['Vue', 'Element Plus', 'Axios'])
</script>

底层 :tags 是 ref([]);splice 改堆上数组 → 依赖收集 → 少渲染一个 Tag。
步骤 4:表单 Form + Input + Select 与校验
这一步要做什么
官网复制「带 rules 校验」的 Form,理解三条线,并加上 Select(下拉选角色):
:model→ 表单数据对象:rules→ 校验规则prop→ 这一项对应 model 里哪个字段
概念:表单组件族
| 组件 | 概念 | 场景 | 注意 |
|---|---|---|---|
| Form | 输入控件集合 + 可选校验 | 新增用户、登录 | ref + validate();prop 对齐字段 |
| Input | 文本输入 | 姓名、地址 | v-model 绑 model 字段 |
| Select | 从选项里选一个值 | 角色、状态下拉 | v-model 绑的是选项的 value,不是 label 文案 |
对应代码(追加在页面上方)
html
<script setup>
import { ref, reactive } from 'vue'
// ---- 表格/分页状态略(沿用步骤 2~3)----
/**
* ruleForm:表单模型(reactive 对象,字段直接点属性,不用 .value)
* Input / Select 都通过 v-model 写进同一个对象
*/
const ruleForm = reactive({
name: '',
role: '', // Select 绑定:存的是 option 的 value
address: '',
})
/** 下拉选项:label 给人看,value 进 model */
const roleOptions = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
{ label: '访客', value: 'guest' },
]
/**
* rules:校验规则字典
* key 必须和 el-form-item 的 prop、以及 ruleForm 字段名一致
*/
const rules = reactive({
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
],
role: [
{ required: true, message: '请选择角色', trigger: 'change' },
],
address: [
{ required: true, message: '请输入地址', trigger: 'blur' },
],
})
/** 通过 ref 拿到 el-form 实例 */
const formRef = ref()
const submitForm = () => {
formRef.value.validate((valid) => {
if (!valid) {
console.log('校验未通过')
return
}
tableData.value.unshift({
date: new Date().toISOString().slice(0, 10),
name: ruleForm.name,
role: ruleForm.role,
address: ruleForm.address,
})
total.value += 1
formRef.value.resetFields()
})
}
const resetForm = () => {
formRef.value.resetFields()
}
</script>
<template>
<div class="page">
<h2>用户列表(Element Plus 实战)</h2>
<!--
ref="formRef" → script 里 formRef 对应同一实例
:model="ruleForm" → 整表数据对象
:rules="rules" → 校验规则
-->
<el-form
ref="formRef"
:model="ruleForm"
:rules="rules"
label-width="80px"
style="max-width: 520px; margin-bottom: 20px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="ruleForm.name" placeholder="请输入姓名" />
</el-form-item>
<!-- Select:v-model 绑 value;option 的 label 只负责显示 -->
<el-form-item label="角色" prop="role">
<el-select v-model="ruleForm.role" placeholder="请选择角色" style="width: 100%">
<el-option
v-for="opt in roleOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="ruleForm.address" placeholder="请输入地址" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
<!-- 下面继续放 el-table + el-pagination(步骤 2~3) -->
</div>
</template>
写完实现了什么功能
- 空提交 / 未选角色 → 红字校验提示
- 填完提交 → 新行出现在表格顶部(含
role),总数 +1 - 重置 → 清空 Input 与 Select
图解 :model 与 rules 如何配合

注意 :
model存值,rules定规矩,prop把某一项对上字段;validate时逐项检查。Select 写入的是 value (如'admin'),不是「管理员」文案。
text
T0 内存中有两个对象:
ruleForm = { name:'', role:'', address:'' } (值)
rules = { name:[...], role:[...], address:[...] } (规则)
T1 用户输入「张三」、选择「管理员」
Input → ruleForm.name = '张三'
Select → ruleForm.role = 'admin'(value,不是 label)
T2 点击提交 → formRef.validate()
读 prop=name / role / address → 用对应 rules 检查
失败:该项下方红字,valid=false
成功:valid=true → 业务把对象拷进 tableData
T3 resetFields()
字段回到初始值,错误提示清除
Form / Input / Select 速查
| 属性 | 作用 |
|---|---|
ref |
获取表单实例(validate / resetFields) |
:model |
表单数据对象 |
:rules |
校验规则 |
label-width |
标签宽度 |
el-form-item + prop |
单项 + 绑定规则字段 |
el-input + v-model |
文本双向绑定 |
el-select + v-model |
选中值双向绑定 |
el-option :label / :value |
显示文案 / 写入 model 的值 |
:model 本质 :就是 v-bind:model,把脚本里的对象交给表单组件管理显示与校验。
步骤 5:导航 ------ Menu、Tabs

核心 :Menu 管「去哪一页/哪一块」;Tabs 管「同一区域内切换面板」。状态都是字符串
ref。
Menu(菜单)
| 层 | 内容 |
|---|---|
| 概念 | 侧栏或顶栏导航列表 |
| 场景 | 后台左侧菜单、顶栏水平菜单 |
| 踩坑 | default-active / v-model 的值要和 el-menu-item 的 index 字符串一致 ;真正跳路由要自己 @select 里 router.push |
html
<template>
<el-container style="height: 280px">
<el-aside width="200px">
<!--
mode="vertical" 侧栏竖排
:default-active 当前高亮项(对应 index)
-->
<el-menu
:default-active="activeMenu"
@select="onMenuSelect"
>
<el-menu-item index="users">用户管理</el-menu-item>
<el-menu-item index="roles">角色管理</el-menu-item>
<el-sub-menu index="sys">
<template #title>系统设置</template>
<el-menu-item index="sys-log">操作日志</el-menu-item>
<el-menu-item index="sys-cfg">参数配置</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<el-main>当前菜单:{{ activeMenu }}</el-main>
</el-container>
</template>
<script setup>
import { ref } from 'vue'
const activeMenu = ref('users')
const onMenuSelect = (index) => {
activeMenu.value = index
// 项目中常写:router.push({ name: index }) 或 path 映射
}
</script>

text
T0 activeMenu = 'users'(栈上 ref,堆上字符串)
T1 用户点「角色管理」→ @select('roles')
T2 activeMenu = 'roles' → 高亮切换;你再决定是否改路由 / 换主区内容
| 常用 API | 含义 |
|---|---|
mode |
vertical / horizontal |
default-active / v-model |
当前激活 index |
el-menu-item index |
菜单项唯一标识(字符串) |
el-sub-menu |
可折叠子菜单 |
@select |
选中回调,参数为 index |
Tabs(标签页)
| 层 | 内容 |
|---|---|
| 概念 | 同一区域内多面板切换 |
| 场景 | 基本信息 / 权限 / 日志 分栏;详情页多块内容 |
| 踩坑 | v-model 绑的是当前 tab 的 name ;el-tab-pane 的 name 必须与之一致 |
html
<template>
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="基本信息" name="base">
<!-- 可放步骤 4 的表单 -->
基本信息面板
</el-tab-pane>
<el-tab-pane label="权限" name="perm">权限面板</el-tab-pane>
<el-tab-pane label="操作日志" name="log">日志面板</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref } from 'vue'
const activeTab = ref('base')
const onTabChange = (name) => {
console.log('切到', name)
// 可按需懒加载该面板数据
}
</script>

底层 :activeTab 变 → 只有匹配 name 的 pane 显示;其它 pane 仍可保留在 DOM(或按配置销毁),栈/堆上仍是同一个字符串 ref。
| 常用 API | 含义 |
|---|---|
v-model |
当前激活 pane 的 name |
el-tab-pane label / name |
页签文案 / 标识 |
@tab-change |
切换回调 |
步骤 6:反馈 ------ Dialog、Message、MessageBox

注意 :Dialog 用布尔
ref控制显隐;Message / MessageBox 多为 API 调用(不必写进模板)。
Dialog(对话框)
| 层 | 内容 |
|---|---|
| 概念 | 覆盖在当前页上的模态内容区 |
| 场景 | 编辑用户、查看详情(不整页跳转) |
| 踩坑 | 用 v-model(或 :model-value + @update:model-value)绑布尔;关窗后记得清空编辑缓存,避免下次打开还是旧数据 |
html
<template>
<el-button type="primary" @click="openEdit(row)">编辑</el-button>
<el-dialog v-model="dialogVisible" title="编辑用户" width="480px">
<el-form :model="editForm" label-width="80px">
<el-form-item label="姓名">
<el-input v-model="editForm.name" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="editForm.address" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
const dialogVisible = ref(false)
const editForm = reactive({ name: '', address: '' })
let editingRow = null
const openEdit = (row) => {
editingRow = row
editForm.name = row.name
editForm.address = row.address
dialogVisible.value = true
}
const saveEdit = () => {
if (editingRow) {
editingRow.name = editForm.name
editingRow.address = editForm.address
}
dialogVisible.value = false
ElMessage.success('保存成功')
}
</script>

text
T0 dialogVisible = false → Dialog 不展示
T1 点编辑 → 拷贝行数据到 editForm → dialogVisible = true → 弹层出现
T2 保存 → 写回行对象 → dialogVisible = false → Message 提示
Message(消息提示)
| 层 | 内容 |
|---|---|
| 概念 | 顶部轻量提示,几秒自动消失 |
| 场景 | 保存成功 / 失败 / 警告 |
| 踩坑 | 从 element-plus 按需 import API ,不是模板里的 el-message 标签(也有指令写法,新手优先 API) |
javascript
import { ElMessage } from 'element-plus'
ElMessage.success('操作成功')
ElMessage.warning('请先选择一行')
ElMessage.error('请求失败')
ElMessage.info('提示信息')

MessageBox(确认框)
| 层 | 内容 |
|---|---|
| 概念 | 需要用户明确确认/取消的对话 |
| 场景 | 删除确认、危险操作二次确认 |
| 踩坑 | 返回 Promise :确定走 then,取消走 catch;别忘了在 then 里才真正删数据 |
javascript
import { ElMessageBox, ElMessage } from 'element-plus'
const onDelete = (row, index) => {
ElMessageBox.confirm(`确定删除「${row.name}」吗?`, '提示', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
tableData.value.splice(index, 1)
total.value -= 1
ElMessage.success('已删除')
})
.catch(() => {
// 点取消:什么都不做
})
}
| 对比 | Dialog | Message | MessageBox |
|---|---|---|---|
| 用法 | 模板组件 + 布尔 | API | API(Promise) |
| 内容 | 可放表单/详情 | 一行文案 | 标题 + 文案 + 按钮 |
| 典型 | 编辑弹窗 | 成功提示 | 删除确认 |
