【Vue3速成】09-Element Plus 核心组件实战与原理解析

🎯 你正在阅读「半路出家玩前端」系列文章 🎯

🔥 弹简特 个人主页

❄️ 个人专栏直通车:

靠热爱去书写自己,靠勇敢去书写生活!


🌟 博主简介:

一、前言

铁汁们,本期我们将会通过一个简单的案例来了解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、效果

  1. 上中下布局 + 按钮/图标工具条
  2. 用户表格 + 分页
  3. 左侧部门树 / 顶部状态 Tag
  4. 带 Input、Select 与 rules 的录入表单
  5. 顶部菜单或内容区 Tabs 切换
  6. 编辑用 Dialog;删除用 MessageBox;结果用 Message

三、知识介绍

§0 先建立概念:它是什么、干什么用

官网

https://element-plus.org/zh-CN/component/overview

概念说明

Element Plus 是基于 Vue 3 的开源 UI 组件库(饿了么前端团队;旧版叫 Element UI,Plus 是增强版)。

把它想成 搭积木 :页面是成品模型,按钮、表单、表格、分页、对话框是现成积木块。标签普遍以 el- 开头(如 el-buttonel-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

这一步要做什么

  1. Vite 创建 Vue 项目
  2. npm i element-plus
  3. 在入口 全局注册 + 引入样式
  4. 清空默认 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>

底层tagsref([])splice 改堆上数组 → 依赖收集 → 少渲染一个 Tag。


步骤 4:表单 Form + Input + Select 与校验

这一步要做什么

官网复制「带 rules 校验」的 Form,理解三条线,并加上 Select(下拉选角色):

  1. :model → 表单数据对象
  2. :rules → 校验规则
  3. 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,把脚本里的对象交给表单组件管理显示与校验。


核心 :Menu 管「去哪一页/哪一块」;Tabs 管「同一区域内切换面板」。状态都是字符串 ref

内容
概念 侧栏或顶栏导航列表
场景 后台左侧菜单、顶栏水平菜单
踩坑 default-active / v-model 的值要和 el-menu-itemindex 字符串一致 ;真正跳路由要自己 @selectrouter.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 的 nameel-tab-panename 必须与之一致
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)
内容 可放表单/详情 一行文案 标题 + 文案 + 按钮
典型 编辑弹窗 成功提示 删除确认

相关推荐
钛态10 小时前
AI 辅助:前端框架反模式:过度封装、状态滥用与副作用失控
前端·vue·react·web
by__csdn10 小时前
Vue vs React vs Angular:前端框架终极对决
前端·vue.js·react.js·前端框架·vue·react·angular
钛态10 小时前
前端框架选型决策框架:React、Vue、Svelte、Solid 的 2026 年横评
前端·vue·react·web
反正我还没长大10 小时前
Vue3响应式原理深度解析:揭秘现代前端框架的核心引擎
vue.js·前端框架·vue·proxy模式
FeelTouch Labs1 天前
Node.js 中的 spawn 和 exec:子进程执行的不同方式对比
node.js·编辑器·vue·vim
shawxlee1 天前
vue3+elementplus+sass/scss安装配置教程:使用svg图标、引入外部字体、定义变量模块、修改组件样式、一键切换主题等
vue·sass·svg·字体·element·scss·主题样式
弹简特1 天前
【Vue3速成】07-vue3异步请求之Axios-01
vue·axios
弹简特2 天前
【Vue3速成】06-vue3官方库路由机制之Pinia状态管理
vue·pinia
java1234_小锋3 天前
Vue3专题 - 响应式基础
vue·vue3·响应式