ElementPlus使用文档
布局与基础结构
Layout 布局(Container)
基本页面结构(Header 固定 + Main 滚动)
🎯 目标效果
- 页面 高度撑满整个视口
- Header 固定在顶部
- 内容区(Main)内部滚动
- Footer 可选
完整示例
<template>
<el-container class="page-container">
<!-- 顶部区域 -->
<el-header class="page-header">
<div class="header-left">后台管理系统</div>
<div class="header-right">用户信息</div>
</el-header>
<!-- 主体内容 -->
<el-main class="page-main">
<div class="content">
<p v-for="i in 50" :key="i">
这是第 {{ i }} 行内容,用于测试 Main 区域滚动效果
</p>
</div>
</el-main>
<!-- 底部(可选) -->
<el-footer class="page-footer">
© 2026 Demo System
</el-footer>
</el-container>
</template>
<script setup lang="ts">
// 本示例无需任何逻辑
</script>
<style scoped>
/* 整个页面撑满视口 */
.page-container {
height: 100vh;
}
/* Header 固定高度 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
background-color: #409eff;
color: #fff;
padding: 0 20px;
}
/* Main 区域可滚动 */
.page-main {
padding: 16px;
overflow: auto;
background-color: #f5f7fa;
}
/* Footer */
.page-footer {
height: 40px;
text-align: center;
line-height: 40px;
color: #999;
font-size: 12px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
📌 理论 & 关键点讲解
1️⃣ el-container
- 本质是一个 flex 容器
- 默认是纵向布局
- 高度不写是不会自动撑满屏幕的
👉 必须显式写:
height: 100vh;2️⃣ el-header / el-footer
- 默认是
flex: 0 0 auto - 高度建议自己明确写死
- 非常适合放:
- Logo
- 用户信息
- 顶部操作按钮
3️⃣ el-main(最容易踩坑)
- 不会自动滚动
- 必须显式加:
overflow: auto;否则:
- 内容会把整个页面撑高
- 滚动条出现在
body上 ❌
⚠️ 常见错误
| 错误 | 结果 |
|---|---|
忘记 height: 100vh | 页面高度塌陷 |
Main 不加 overflow | 整页滚动 |
| Header 不写高度 | 布局不可控 |
左右布局(后台系统最常用)
这是 后台管理系统的核心布局模型。
🎯 目标效果
- 左侧:菜单栏(Aside)
- 右侧:Header + 内容
- Aside 固定宽度
- 内容区自适应
- 支持侧边栏折叠
完整示例
<template>
<el-container class="layout-container">
<!-- 左侧菜单 -->
<el-aside
class="layout-aside"
:width="isCollapse ? '64px' : '200px'"
>
<div class="logo">
{{ isCollapse ? 'LOGO' : '后台系统' }}
</div>
</el-aside>
<!-- 右侧主体 -->
<el-container>
<el-header class="layout-header">
<el-button size="small" @click="toggleCollapse">
{{ isCollapse ? '展开菜单' : '折叠菜单' }}
</el-button>
</el-header>
<el-main class="layout-main">
<p v-for="i in 40" :key="i">
主内容区域第 {{ i }} 行
</p>
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
/**
* 是否折叠菜单
*/
const isCollapse = ref(false)
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
</script>
<style scoped>
.layout-container {
height: 100vh;
}
/* 左侧栏 */
.layout-aside {
background-color: #001529;
color: #fff;
transition: width 0.2s;
}
/* Logo */
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
/* 顶部 */
.layout-header {
height: 60px;
display: flex;
align-items: center;
background-color: #fff;
border-bottom: 1px solid #ebeef5;
}
/* 主内容 */
.layout-main {
padding: 16px;
overflow: auto;
background-color: #f5f7fa;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
📌 理论 & 参数说明
1️⃣ el-aside
<el-aside :width="isCollapse ? '64px' : '200px'" />width必须是字符串不传时默认
300px折叠菜单本质:
只是改变宽度,并不是隐藏
2️⃣ 折叠菜单的核心思想
const isCollapse = ref(false)- 控制宽度
- 控制 Logo 文案
- 后续可用于:
- Menu 的
collapse属性 - Icon-only 模式
- Menu 的
3️⃣ 为什么要再嵌套一个 el-container
<el-container>
<el-header />
<el-main />
</el-container>2
3
4
原因很重要 👇
Container的布局方向由 子组件类型决定- 同级出现
el-aside→ 横向布局 - 内层没有
el-aside→ 自动纵向
👉 这是 Element Plus Layout 的设计核心
⚠️ 真实项目注意事项
- Aside 一定要固定宽度
- 折叠只做宽度变化,避免
v-if - 滚动永远放在
el-main - Header 高度统一(60px 是事实标准)
Grid 栅格(Row / Col)
Element Plus 的
Row / Col👉 本质:24 栅格的响应式 Flex 布局系统
基础栅格
🎯 目标效果
- 一行分成若干列
- 列宽按比例分配
- 列与列之间有间距(不贴边)
完整示例:基础栅格
<template>
<el-container class="page-container">
<el-main>
<h3>基础栅格示例</h3>
<el-row :gutter="20">
<el-col :span="6">
<div class="grid-item">span = 6</div>
</el-col>
<el-col :span="6">
<div class="grid-item">span = 6</div>
</el-col>
<el-col :span="6">
<div class="grid-item">span = 6</div>
</el-col>
<el-col :span="6">
<div class="grid-item">span = 6</div>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script setup lang="ts">
// 无逻辑
</script>
<style scoped>
.page-container {
height: 100vh;
}
.grid-item {
background-color: #409eff;
color: #fff;
text-align: center;
padding: 16px 0;
border-radius: 4px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
📌 理论讲解(非常关键)
1️⃣ span 是什么?
<el-col :span="6" />- 一行 总共 24 份
span = 6→ 占6 / 24 = 25%- 常见组合:
| 布局 | span |
|---|---|
| 一行 2 列 | 12 + 12 |
| 一行 3 列 | 8 + 8 + 8 |
| 一行 4 列 | 6 + 6 + 6 + 6 |
👉 超过 24 会自动换行
2️⃣ gutter 是什么?
<el-row :gutter="20" />- 列与列之间的 水平间距(px)
- 实现方式:
- Row 加左右负 margin
- Col 加左右 padding
- 必须写在
el-row上
常用值:
16(紧凑)20(最常用)24(宽松)
⚠️ 常见坑
❌ 在 el-col 上写 margin ❌ 忘记加 gutter 导致内容贴边 ❌ span 随便乱加导致换行错乱
响应式栅格
🎯 目标效果
- PC:一行多列
- 平板:一行 2 列
- 手机:一行 1 列
完整示例:响应式布局
<template>
<el-container class="page-container">
<el-main>
<h3>响应式栅格</h3>
<el-row :gutter="20">
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
>
<div class="grid-item">响应式列 1</div>
</el-col>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
>
<div class="grid-item">响应式列 2</div>
</el-col>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
>
<div class="grid-item">响应式列 3</div>
</el-col>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
>
<div class="grid-item">响应式列 4</div>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script setup lang="ts"></script>
<style scoped>
.page-container {
height: 100vh;
}
.grid-item {
background-color: #67c23a;
color: #fff;
text-align: center;
padding: 16px 0;
border-radius: 4px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
📌 响应式参数说明
| 参数 | 含义 |
|---|---|
xs | < 768px(手机) |
sm | ≥ 768px |
md | ≥ 992px |
lg | ≥ 1200px |
xl | ≥ 1920px |
👉 每个值本质上还是 span
:md="8" // 中屏占 8 / 24✅ 实战建议(非常重要)
- 后台系统可以不写
xs - 搜索区、表单强烈建议写响应式
- 列表区通常固定布局
常见表单 / 搜索布局(高频实战)
这是你 项目里出现次数最多的 Grid 用法。
🎯 目标效果
- 一行 3~4 个查询条件
- 最右侧:查询 / 重置按钮
- 小屏自动换行
- 按钮右对齐
完整示例:搜索区布局
<template>
<el-container class="page-container">
<el-main>
<h3>搜索表单布局</h3>
<el-form :inline="true" class="search-form">
<el-row :gutter="20">
<!-- 查询条件 -->
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="用户名">
<el-input placeholder="请输入用户名" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="状态">
<el-select placeholder="请选择状态" clearable>
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="日期">
<el-date-picker type="date" placeholder="选择日期" />
</el-form-item>
</el-col>
<!-- 操作按钮 -->
<el-col
:xs="24"
:sm="24"
:md="24"
:lg="6"
class="search-actions"
>
<el-button type="primary">查询</el-button>
<el-button>重置</el-button>
</el-col>
</el-row>
</el-form>
</el-main>
</el-container>
</template>
<script setup lang="ts"></script>
<style scoped>
.page-container {
height: 100vh;
}
/* 操作按钮右对齐 */
.search-actions {
display: flex;
justify-content: flex-end;
align-items: center;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
📌 搜索布局核心思想(一定要记住)
1️⃣ 一行 4 列的黄金比例
lg = 6 // 24 / 4 = 6
md = 8 // 24 / 3 = 8
sm = 12 // 24 / 2 = 12
xs = 24 // 1 行 1 个2
3
4
👉 这是后台搜索区的事实标准
2️⃣ 为什么按钮单独一列?
- 对齐好控制
- 不受 label 宽度影响
- 小屏时自然换行
3️⃣ 为什么按钮列要 24?
:md="24"- 确保:
- 小屏换到下一行
- 不挤占输入框空间
⚠️ 常见错误总结
❌ 所有列 span 写死 ❌ 按钮和表单项混在一起 ❌ 不写响应式导致小屏崩掉 ❌ 用 margin-left 硬推按钮位置
表单与数据录入(高频核心)
Form 表单(el-form)
el-form本质是一个 表单容器 + 校验系统 子组件如el-input / el-select / el-date-picker等,都可以通过prop与rules绑定验证。
基础表单结构
🎯 目标效果
- 新增 / 编辑表单
- 有 label
- 统一宽度
- 可选择 label 位置(左 / 上 / 内联)
完整示例:基础表单
<template>
<el-container class="page-container">
<el-main>
<h3>基础表单示例</h3>
<el-form
ref="formRef"
:model="form"
label-width="100px"
label-position="right"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" clearable>
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</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>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'
/**
* 表单数据
*/
const form = reactive({
username: '',
email: '',
status: ''
})
/**
* el-form 实例
* 用于手动校验 / 重置
*/
const formRef = ref<FormInstance>()
/**
* 提交
*/
const submitForm = () => {
formRef.value?.validate((valid) => {
if (valid) {
alert('提交成功: ' + JSON.stringify(form))
} else {
alert('表单校验失败')
}
})
}
/**
* 重置表单
*/
const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style scoped>
.page-container {
height: 100vh;
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
📌 理论 & 参数说明
1️⃣ :model
:form="form"- 表单数据源
el-input / el-select的v-model必须绑定到form的属性- 响应式对象(
reactive)
2️⃣ label-width & label-position
| 参数 | 含义 |
|---|---|
label-width | label 固定宽度(px / auto) |
label-position | right / top / left(右对齐、上方、左对齐) |
- 后台表单常用:
right+100px - 移动端 / 卡片表单:
top
3️⃣ el-form-item & prop
label→ 展示在左侧prop→ 用于表单校验 对应字段- 如果不做校验可以不写
prop,只是展示 label
表单校验(必用)
🎯 目标效果
- 必填
- 格式验证(邮箱、手机号)
- 触发方式:
blur / change - 手动校验
完整示例:表单校验
<template>
<el-container class="page-container">
<el-main>
<h3>基础表单示例</h3>
<el-form
ref="formRef"
:rules="rules"
:model="form"
label-width="100px"
label-position="right"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" clearable>
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</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>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'
/**
* 表单数据
*/
const form = reactive({
username: '',
email: '',
status: ''
})
// 校验规则
const rules = {
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' },
{ min: 3, max: 12, message: '长度在3~12个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '邮箱不能为空', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
]
}
/**
* el-form 实例
* 用于手动校验 / 重置
*/
const formRef = ref<FormInstance | null>(null)
/**
* 提交
*/
const submitForm = () => {
formRef.value?.validate((valid) => {
if (valid) {
alert('提交成功: ' + JSON.stringify(form))
} else {
alert('表单校验失败')
}
})
}
/**
* 重置表单
*/
const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style scoped>
.page-container {
height: 100vh;
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
📌 理论说明
1️⃣ :rules
- 对象,键名 = form 属性名
- 值 = 校验规则数组
- 每条规则可设置:
required(必填)min / max(长度)type(email / number)message(提示)trigger(触发事件)
2️⃣ validate 方法
formRef.value?.validate((valid) => { ... })- 手动触发表单校验
- 回调
valid= true / false
3️⃣ resetFields 方法
- 重置表单数据为初始值
- 清除校验状态
表单禁用 / 只读态
🎯 目标效果
- 查看详情页用同一个表单
- 禁止修改
✅ 示例
<el-form :model="form" :disabled="isDisabled" label-width="100px">
<el-form-item label="用户名">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" />
</el-form-item>
</el-form>
<el-button @click="isDisabled = !isDisabled">
切换禁用状态
</el-button>
<script setup lang="ts">
const isDisabled = ref(false)
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
📌 理论说明
:disabled会递归禁用表单内的所有输入控件- 配合 同一个表单组件,可实现:
- 新增:
disabled = false - 查看详情:
disabled = true
- 新增:
- ⚠️ 不会影响表单校验逻辑,仍然可以
validate
Input 输入类组件
el-input 基础使用
🎯 目标效果
- 普通文本输入
- 可清空
- 密码可切换显示
- 限制长度
- 显示输入字数
完整示例:基础 Input
<template>
<el-container class="page-container">
<el-main>
<h3>基础 Input 示例</h3>
<!-- 普通输入 -->
<el-form-item label="用户名">
<el-input
v-model="form.username"
placeholder="请输入用户名"
clearable
maxlength="20"
show-word-limit
/>
</el-form-item>
<!-- 密码输入 -->
<el-form-item label="密码">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
show-password
/>
</el-form-item>
<!-- 多行文本 -->
<el-form-item label="备注">
<el-input
type="textarea"
v-model="form.remark"
placeholder="请输入备注"
:rows="4"
/>
</el-form-item>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({
username: '',
password: '',
remark: ''
})
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
📌 理论讲解
1️⃣ v-model
- 双向绑定输入框值到数据源
- 输入改变时,
form.username自动更新 - 是表单数据绑定的基础
2️⃣ placeholder
- 提示用户输入内容
- 不同于
label,只是灰色占位文字
3️⃣ clearable
- 显示小叉号,点击清空输入
- 常用于搜索框 / 表单输入
4️⃣ show-password
- 仅对
type="password"有效 - 显示切换密码明文的小眼睛图标
- 对安全登录表单非常实用
5️⃣ maxlength / show-word-limit
- 限制最大输入长度
show-word-limit显示右下角文字计数- 例如
3/20表示已输入 3 个字符,最大 20
前后缀插槽
🎯 目标效果
- 在输入框前后添加图标、文字或按钮
- 高频场景:
- 搜索框前的 🔍
- 后缀按钮:清空 / 搜索 / 日期选择
完整示例:前后缀
<template>
<el-container class="page-container">
<el-main>
<h3>Input 前后缀示例</h3>
<!-- 前缀 -->
<el-form-item label="搜索用户">
<el-input placeholder="请输入用户名" v-model="form.search">
<template #prefix>
<i class="el-icon-search"></i>
</template>
</el-input>
</el-form-item>
<!-- 后缀 -->
<el-form-item label="邮箱">
<el-input
v-model="form.email"
placeholder="请输入邮箱"
>
<template #suffix>
<el-button size="mini" @click="clearEmail">清空</el-button>
</template>
</el-input>
</el-form-item>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({
search: '',
email: ''
})
const clearEmail = () => {
form.email = ''
}
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
📌 理论讲解
1️⃣ 前缀 #prefix
- 显示在输入框最左侧
- 可放图标 / 文本 / 组件
- 常用场景:搜索图标、货币符号(¥)
2️⃣ 后缀 #suffix
- 显示在输入框最右侧
- 可放按钮 / 清空 / 状态提示
- 常用场景:
- 清空按钮
- 输入验证状态(✔️ / ❌)
- 日期选择按钮
3️⃣ 注意事项
- 插槽本身不会改变输入框的
v-model - 如果是按钮操作,需要手动操作数据
- 不要在 prefix/suffix 放复杂表单控件,会影响布局
Select 选择器
el-select + el-option 基础使用
🎯 目标效果
- 下拉选择
- 可清空
- 可搜索过滤
- 占位提示
完整示例:基础 Select
<template>
<el-container class="page-container">
<el-main>
<h3>基础 Select 示例</h3>
<el-form :model="form" label-width="100px">
<!-- 普通下拉 -->
<el-form-item label="状态">
<el-select
v-model="form.status"
placeholder="请选择状态"
clearable
>
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<!-- 可搜索过滤 -->
<el-form-item label="国家">
<el-select
v-model="form.country"
placeholder="请选择国家"
filterable
clearable
>
<el-option label="中国" value="CN" />
<el-option label="美国" value="US" />
<el-option label="日本" value="JP" />
<el-option label="德国" value="DE" />
</el-select>
</el-form-item>
<!-- 禁用选项 -->
<el-form-item label="角色">
<el-select
v-model="form.role"
placeholder="请选择角色"
clearable
>
<el-option label="管理员" value="admin" />
<el-option label="普通用户" value="user" />
<el-option label="游客" value="guest" disabled />
</el-select>
</el-form-item>
</el-form>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({
status: '',
country: '',
role: ''
})
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
📌 理论讲解
1️⃣ v-model
- 双向绑定选择器的值
- 对应
el-option的value - 必须是响应式对象(
reactive/ref)
2️⃣ placeholder
- 占位提示文字
- 当
v-model为空时显示
3️⃣ clearable
- 右侧出现小叉号,点击清空选择
- 对于表单查询区非常常用
4️⃣ filterable
- 允许输入过滤选项
- 对应后台搜索或字典选择非常实用
- 文字匹配规则:包含搜索词即可
5️⃣ disabled
- 对单个选项禁用
- 适合灰掉不可选的枚举值
常见业务场景
1️⃣ 下拉字典(字典表 / 枚举)
const statusOptions = [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' }
]
<el-select v-model="form.status" placeholder="请选择状态" clearable>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>2
3
4
5
6
7
8
9
10
11
12
✅ 优点:动态生成选项,可直接绑定接口返回的字典数据
2️⃣ 枚举映射
- 常见场景:接口返回
status = 1 / 0,前端显示“启用 / 禁用” - 结合
v-for渲染
const roleEnum = {
admin: '管理员',
user: '普通用户',
guest: '游客'
}
<el-select v-model="form.role" placeholder="请选择角色">
<el-option
v-for="(label, value) in roleEnum"
:key="value"
:label="label"
:value="value"
/>
</el-select>2
3
4
5
6
7
8
9
10
11
12
13
✅ 优点:代码可维护,枚举值集中管理
3️⃣ 禁用选项
- 有些角色或状态不可选,用
disabled控制
<el-option label="游客" value="guest" disabled />- ⚠️ 注意:
v-model不能绑定到禁用值,否则表单会报错- 建议在初始化时排除不可选值或提示用户
📌 实战注意事项
- 动态数据必须保证 key 唯一
- filterable 下拉与 clearable 一起用非常顺手
- 枚举映射 + v-for + :key = value 是标准写法
- 禁用选项不要做默认值
- 表单校验依然使用 prop 绑定 form 字段
DatePicker 时间选择
单个时间选择
🎯 目标效果
- 单个日期或日期时间选择
- 可以自定义显示格式
- 可以绑定后端接口标准格式(如
yyyy-MM-dd HH:mm:ss)
完整示例:单日期 / 日期时间选择
<template>
<el-container class="page-container">
<el-main>
<h3>单个时间选择</h3>
<el-form :model="form" label-width="120px">
<!-- 单日期 -->
<el-form-item label="出生日期">
<el-date-picker
v-model="form.birthday"
type="date"
placeholder="请选择日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
clearable
/>
</el-form-item>
<!-- 日期时间 -->
<el-form-item label="注册时间">
<el-date-picker
v-model="form.registerTime"
type="datetime"
placeholder="请选择日期时间"
format="yyyy-MM-dd HH:mm"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
/>
</el-form-item>
</el-form>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({
birthday: '',
registerTime: ''
})
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
📌 理论讲解
1️⃣ type
- 常用类型:
date→ 只选择日期datetime→ 日期 + 时间month→ 月year→ 年week→ 周
- 控制选择器 UI 和弹出控件
2️⃣ format & value-format
| 属性 | 含义 |
|---|---|
format | 显示在输入框的格式(用户可读) |
value-format | 绑定到 v-model 的实际值格式(通常是接口需要) |
⚠️ 如果不写
value-format,v-model默认是Date对象
3️⃣ clearable
- 右侧出现清空按钮
- 常用在搜索条件里
时间范围选择(高频使用)
🎯 目标效果
- 搜索区常用 “起止时间”
- 支持快捷选择(今天 / 本周 / 最近7天)
- 支持日期或日期时间范围
完整示例:时间范围选择
<template>
<el-container class="page-container">
<el-main>
<h3>时间范围选择</h3>
<el-form :model="form" label-width="120px">
<el-form-item label="查询时间">
<el-date-picker
v-model="form.queryTime"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
:shortcuts="shortcuts"
style="width: 100%;"
/>
</el-form-item>
</el-form>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import dayjs from 'dayjs'
const form = reactive({
queryTime: [] as string[] // 明确类型为字符串数组
})
/**
* 快捷日期范围选项(Element Plus 写法)
*/
const shortcuts = [
{
text: '今天',
value: () => {
const start = dayjs().startOf('day').format('YYYY-MM-DD')
const end = dayjs().endOf('day').format('YYYY-MM-DD')
return [start, end]
}
},
{
text: '最近7天',
value: () => {
const start = dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD')
const end = dayjs().endOf('day').format('YYYY-MM-DD')
return [start, end]
}
},
{
text: '本月',
value: () => {
const start = dayjs().startOf('month').format('YYYY-MM-DD')
const end = dayjs().endOf('month').format('YYYY-MM-DD')
return [start, end]
}
}
]
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
📌 理论讲解
1️⃣ type="daterange" / "datetimerange"
daterange→ 选择日期区间datetimerange→ 选择日期 + 时间区间v-model绑定 数组[start, end]
2️⃣ start-placeholder / end-placeholder
- 分别控制开始、结束日期的占位文字
- 搜索表单 UX 必须写清楚
3️⃣ shortcuts
- 自定义快捷选项按钮
text+value- 高频场景:
- 今天 / 昨天 / 最近7天 / 本月 / 本季度
⚠️ 注意:
- 绑定的值类型:如果写了
value-format→ 会返回字符串- 如果没写 → 返回
Date对象
Radio / Checkbox
el-radio-group 单选
🎯 目标效果
- 单选枚举
- 可选带边框按钮
- 常用场景:性别、状态、选项类型
完整示例:Radio 单选
<template>
<el-container class="page-container">
<el-main>
<h3>Radio 单选示例</h3>
<el-form :model="form" label-width="100px">
<!-- 普通单选 -->
<el-form-item label="性别">
<el-radio-group v-model="form.gender">
<el-radio label="male">男</el-radio>
<el-radio label="female">女</el-radio>
</el-radio-group>
</el-form-item>
<!-- 带边框单选按钮 -->
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio label="1" border>启用</el-radio>
<el-radio label="0" border>禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({
gender: '',
status: ''
})
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
📌 理论讲解
1️⃣ v-model
- 双向绑定选中值
el-radio的label值对应v-model
2️⃣ border
- 外观带边框按钮风格
- 常用于状态 / 类型选择
3️⃣ 注意事项
el-radio-group必须有v-model- 每个
el-radio的label唯一 - 可与表单校验结合(
prop+rules)
el-checkbox-group 多选
🎯 目标效果
- 多选字段
- 支持全选 / 反选
- 常用场景:权限分配、标签选择
完整示例:Checkbox 多选
<template>
<el-container class="page-container">
<el-main>
<h3>Checkbox 多选示例</h3>
<el-form :model="form" label-width="100px">
<!-- 普通多选 -->
<el-form-item label="爱好">
<el-checkbox-group v-model="form.hobbies">
<el-checkbox label="足球">足球</el-checkbox>
<el-checkbox label="篮球">篮球</el-checkbox>
<el-checkbox label="羽毛球">羽毛球</el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 全选 / 反选 -->
<el-form-item label="权限">
<el-checkbox
:indeterminate="isIndeterminate"
v-model="checkAll"
@change="handleCheckAllChange"
>
全选
</el-checkbox>
<el-checkbox-group v-model="form.permissions" @change="handleCheckedChange">
<el-checkbox label="新增">新增</el-checkbox>
<el-checkbox label="编辑">编辑</el-checkbox>
<el-checkbox label="删除">删除</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
const form = reactive({
hobbies: [] as string[],
permissions: [] as string[]
})
/** 全选控制 */
const checkAll = ref(false)
const isIndeterminate = ref(false)
/** 全选逻辑 */
const handleCheckAllChange = (val: boolean) => {
form.permissions = val ? ['新增', '编辑', '删除'] : []
isIndeterminate.value = false
}
/** 单个选项变化 */
const handleCheckedChange = (val: string[]) => {
const allLen = 3
const checkedLen = val.length
checkAll.value = checkedLen === allLen
isIndeterminate.value = checkedLen > 0 && checkedLen < allLen
}
</script>
<style scoped>
.page-container {
padding: 16px;
}
.el-checkbox-group {
display: flex;
gap: 16px;
margin-top: 8px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
📌 理论讲解
1️⃣ v-model
- 多选绑定数组
- 数组内元素 = 被选中的
label
2️⃣ 全选 / 反选逻辑
indeterminate→ 半选状态- 单个选项变化时需要更新
checkAll和indeterminate - 常用于权限、标签列表
3️⃣ 注意事项
label唯一且对应v-model类型- 数组操作时保持响应式,使用
reactive或ref - 可结合表单校验(必选项 / 最少选项)
数据展示(后台最核心)
Table 表格(核心组件)
基础表格
🎯 目标效果
- 渲染表格数据
- 带边框 / 斑马纹
- 指定
row-key保持行唯一性
完整示例:基础表格
<template>
<el-container class="page-container">
<el-main>
<h3>基础表格示例</h3>
<el-table
:data="tableData"
border
stripe
style="width: 100%"
row-key="id"
>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column prop="status" label="状态" width="100" />
</el-table>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const tableData = reactive([
{ id: 1, name: '张三', email: 'zhangsan@example.com', status: '启用' },
{ id: 2, name: '李四', email: 'lisi@example.com', status: '禁用' },
{ id: 3, name: '王五', email: 'wangwu@example.com', status: '启用' }
])
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
📌 理论讲解
:data→ 表格数据数组border→ 显示边框stripe→ 斑马纹row-key→ 每行唯一标识(必填,保证排序 / 选择 / 滚动正确)
列配置
- 控制列显示内容、宽度、对齐
<el-table-column prop="email" label="邮箱" min-width="200" align="center" />- prop → 对应数据字段
- label → 列标题
- width / min-width → 固定或最小宽度
- align → 左 / 中 / 右对齐
插槽列(自定义渲染,非常常用)
- 自定义单元格内容
- 状态标签
- 操作按钮
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag type="success" v-if="row.status === '启用'">启用</el-tag>
<el-tag type="info" v-else>禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editRow(row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteRow(row)">删除</el-button>
</template>
</el-table-column>2
3
4
5
6
7
8
9
10
11
12
13
固定列 & 横向滚动
<el-table
:data="tableData"
style="width: 800px"
height="300"
border
stripe
>
<el-table-column fixed="left" prop="id" label="ID" width="60" />
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="email" label="邮箱" width="200" />
<el-table-column prop="status" label="状态" width="100" />
<el-table-column fixed="right" label="操作" width="160">
<template #default="{ row }">
<el-button size="small">查看</el-button>
</template>
</el-table-column>
</el-table>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- fixed="left/right" → 固定列
- 横向滚动 → 当总宽度大于容器时自动出现滚动条
- height → 指定表格高度可实现纵向滚动
8.5 表格选择(批量操作)
<el-table
:data="tableData"
border
stripe
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
</el-table>
<el-button type="primary" @click="batchDelete">批量删除</el-button>
import { ref } from 'vue'
const selectedRows = ref<any[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedRows.value = rows
}
const batchDelete = () => {
if (!selectedRows.value.length) return alert('请选择记录')
alert('删除: ' + JSON.stringify(selectedRows.value))
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- type="selection" → 显示复选框
- @selection-change → 获取选中行
- 可配合批量操作按钮
空数据 & Loading
<el-table
:data="emptyData"
border
stripe
empty-text="暂无数据"
v-loading="loading"
style="width: 100%"
>
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
</el-table>
const emptyData: any[] = []
const loading = ref(false)2
3
4
5
6
7
8
9
10
11
12
13
- empty-text → 自定义空数据提示
- v-loading → 表格加载中效果
Pagination 分页
基础分页
🎯 目标效果
- 显示页码
- 每页条数
- 总条数
完整示例:基础分页
<template>
<el-container class="page-container">
<el-main>
<h3>基础分页示例</h3>
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next, jumper, ->, total"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(95) // 总条数
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
📌 理论讲解
current-page/v-model:current-page- 当前页码
- 与后台请求页码绑定
page-size- 每页显示条数
- 可配合
@size-change动态修改
total- 总条数,用于计算页数
layout- 控制分页组件布局
- 常用组合:
prev, pager, next, jumper→ 前一页 / 页码 / 下一页 / 页码跳转->, total→ 右对齐显示总条数
常用事件
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
layout="prev, pager, next, sizes, ->, total"
:page-sizes="[10, 20, 50, 100]"
/>
const handleCurrentChange = (page: number) => {
currentPage.value = page
fetchTableData()
}
const handleSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1 // 页大小改变后重置页码
fetchTableData()
}
// 模拟接口请求
const fetchTableData = () => {
console.log('请求数据:页码', currentPage.value, '条数', pageSize.value)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
📌 理论讲解
@current-change→ 页码改变时触发@size-change→ 每页条数改变时触发- 重置页码
- 搜索条件改变或 pageSize 改变时,通常重置
currentPage = 1 - 避免页码越界或查询结果不正确
- 搜索条件改变或 pageSize 改变时,通常重置
page-sizes- 可配置用户可选的每页条数数组
- 常用
[10, 20, 50, 100]
与 Table 联动(高频实战)
<template>
<el-container class="page-container">
<el-main>
<h3>Table + Pagination 联动示例</h3>
<!-- 搜索条件 -->
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="姓名">
<el-input v-model="searchForm.name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table
:data="tableData"
border
stripe
row-key="id"
style="margin-top: 16px;"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
layout="prev, pager, next, sizes, ->, total"
:page-sizes="[10, 20, 50]"
style="margin-top: 16px; text-align: right;"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive, ref, computed } from 'vue'
// 搜索表单
const searchForm = reactive({
name: ''
})
// 表格数据
const tableData = ref([] as any[])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
// 模拟后端分页接口
const allData = Array.from({ length: 95 }).map((_, i) => ({
id: i + 1,
name: `用户${i + 1}`,
email: `user${i + 1}@example.com`
}))
const fetchTableData = () => {
// 模拟搜索过滤
let filtered = allData.filter(item => item.name.includes(searchForm.name))
total.value = filtered.length
// 分页数据
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = filtered.slice(start, end)
}
// 页码/页大小改变
const handleCurrentChange = (page: number) => {
currentPage.value = page
fetchTableData()
}
const handleSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
fetchTableData()
}
// 搜索
const search = () => {
currentPage.value = 1
fetchTableData()
}
// 重置
const reset = () => {
searchForm.name = ''
currentPage.value = 1
fetchTableData()
}
// 初始化
fetchTableData()
</script>
<style scoped>
.page-container {
padding: 16px;
}
.search-form {
margin-bottom: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
📌 理论讲解
- 搜索 + 分页
- 搜索条件改变时 →
currentPage = 1 - 分页组件会触发
@current-change重新拉取数据
- 搜索条件改变时 →
- 后端分页
- 后端返回总条数
total - 分页组件根据
page-size计算页数
- 后端返回总条数
- 前端分页
- 可以用
slice()截取数据 total= 数据长度
- 可以用
- 表格 + 复选框
- 批量操作 + 分页结合 → 需要考虑跨页选择逻辑
跨页选择
<template>
<el-container class="page-container">
<el-main>
<h3>Table + Pagination 联动示例</h3>
<div v-if="getAllSelectedRows.length > 0" class="selected-tip">
已选中 {{ getAllSelectedRows.length }} 条数据
<el-button
link
type="danger"
@click="clearSelection"
style="margin-left: 8px"
>
清除
</el-button>
</div>
<!-- 查询条件 -->
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="姓名">
<el-input v-model="searchForm.name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table
ref="tableRef"
:data="tableData"
row-key="id"
border
stripe
@selection-change="handleSelectionChange"
style="margin-top: 16px"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
layout="prev, pager, next, sizes, ->, total"
:page-sizes="[10, 20, 50]"
style="margin-top: 16px; text-align: right"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { computed, nextTick, reactive, ref } from 'vue'
import type { ElTable } from 'element-plus'
/** 查询表单 */
const searchForm = reactive({ name: '' })
/** 表格与分页状态 */
const tableData = ref<any[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const tableRef = ref<InstanceType<typeof ElTable>>()
/** 模拟后端数据 */
const allData = Array.from({ length: 95 }).map((_, i) => ({
id: i + 1,
name: `用户${i + 1}`,
email: `user${i + 1}@example.com`
}))
/** 跨页选中数据(key 为 row-key) */
const selectedRowMap = ref<Map<number, any>>(new Map())
/** 标识当前是否处于选中恢复阶段 */
const isRestoringSelection = ref(false)
/** 加载分页数据并回显选中状态 */
const fetchTableData = async () => {
isRestoringSelection.value = true
const filtered = allData.filter(item =>
item.name.includes(searchForm.name)
)
total.value = filtered.length
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = filtered.slice(start, end)
await nextTick()
restoreSelection()
isRestoringSelection.value = false
}
/** 根据全局选中数据回显当前页 */
const restoreSelection = () => {
if (!tableRef.value) return
tableRef.value.clearSelection()
tableData.value.forEach(row => {
if (selectedRowMap.value.has(row.id)) {
tableRef.value!.toggleRowSelection(row, true)
}
})
}
/** 处理用户勾选变化 */
const handleSelectionChange = (selection: any[]) => {
if (isRestoringSelection.value) return
const currentPageIds = tableData.value.map(row => row.id)
currentPageIds.forEach(id => {
if (!selection.some(row => row.id === id)) {
selectedRowMap.value.delete(id)
}
})
selection.forEach(row => {
selectedRowMap.value.set(row.id, row)
})
}
/** 清空全部已选数据 */
const clearSelection = () => {
selectedRowMap.value.clear()
tableRef.value?.clearSelection()
}
/** 已选数据列表 */
const getAllSelectedRows = computed(() =>
Array.from(selectedRowMap.value.values())
)
/** 分页与查询 */
const handleCurrentChange = (page: number) => {
currentPage.value = page
fetchTableData()
}
const handleSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
fetchTableData()
}
const search = () => {
currentPage.value = 1
fetchTableData()
}
const reset = () => {
searchForm.name = ''
currentPage.value = 1
fetchTableData()
}
fetchTableData()
</script>
<style scoped>
.page-container {
padding: 16px;
}
.search-form {
margin-bottom: 16px;
}
.selected-tip {
margin: 12px 0;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
📌 理论讲解
- 为什么默认不支持跨页选择
el-table的选中状态只和当前data绑定- 翻页后
data变化,选中状态会被重置
- 核心解决思路
- 将选中状态从
el-table内部提升到业务层 - 使用
Map / Set以row-key作为唯一标识保存选中数据
- 将选中状态从
- 关键实现点
row-key必须唯一且稳定- 翻页加载数据后,手动回显当前页的选中状态
- 恢复选中过程中,忽略
selection-change事件
- 为什么要使用恢复标识
- 翻页时
el-table会自动触发一次selection-change - 若不拦截,会误删其他页的选中数据
- 翻页时
- 适用场景
- 批量操作(删除、导出、审批)
- 后端分页数据
- 大数据列表(推荐只保存 ID)
反馈与交互
Dialog 弹窗(高频)
基础用法
🎯 使用场景
- 简单提示弹窗
- 信息展示
- 作为新增 / 编辑的容器
完整示例:基础 Dialog
<template>
<el-container class="page-container">
<el-main>
<el-button type="primary" @click="dialogVisible = true">
打开弹窗
</el-button>
<el-dialog
v-model="dialogVisible"
title="基础弹窗"
width="500px"
>
<p>这是一个最基础的 Dialog 示例</p>
</el-dialog>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref(false)
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
📌 理论讲解
1️⃣ v-model
- 控制弹窗显示 / 隐藏
- 必须是 boolean
- 关闭弹窗时会自动变为
false
2️⃣ title
- 弹窗标题
- 可动态绑定(新增 / 编辑切换)
3️⃣ width
- 常用:
400px / 500px / 600px / 60% - 后台表单一般 不要太窄
底部操作区(footer 插槽)
🎯 使用场景
- 确认 / 取消按钮
- 提交表单
- 自定义操作区布局
完整示例:自定义 Footer
<el-dialog
v-model="dialogVisible"
title="带底部操作的弹窗"
width="500px"
>
<p>这里是弹窗内容</p>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">
确认
</el-button>
</span>
</template>
</el-dialog>
const handleConfirm = () => {
console.log('点击确认')
dialogVisible.value = false
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
📌 理论讲解
#footer插槽- 完全接管底部区域
- 官方按钮样式只是默认实现,真实项目几乎都会自定义
- 按钮行为
- 取消:直接关闭弹窗
- 确认:一般触发表单校验或接口请求
- 常见样式
- 按钮右对齐(Element Plus 默认)
- 主按钮
type="primary"
表单弹窗(新增 / 编辑共用,核心)
这是 最重要的一节。
🎯 目标效果
- 同一个 Dialog
- 同一份 Form
- 支持 新增 / 编辑
- 关闭前校验表单
完整示例:表单 Dialog(完整实战)
<template>
<el-container class="page-container">
<el-main>
<el-button type="primary" @click="openAdd">新增</el-button>
<el-button @click="openEdit">编辑</el-button>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
:before-close="handleBeforeClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">
确认
</el-button>
</template>
</el-dialog>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import {ref, reactive, nextTick} from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
/** 弹窗状态 */
const dialogVisible = ref(false)
const dialogTitle = ref('')
/** 表单 */
const formRef = ref<FormInstance>()
const form = reactive({
name: '',
email: ''
})
/** 校验规则 */
const rules: FormRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }]
}
/** 新增 */
const openAdd = async () => {
dialogTitle.value = '新增用户'
dialogVisible.value = true
await nextTick()
formRef.value?.resetFields()
}
/** 编辑 */
const openEdit = () => {
dialogTitle.value = '编辑用户'
form.name = '张三'
form.email = 'zhangsan@example.com'
dialogVisible.value = true
}
/** 提交 */
const submitForm = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
console.log('提交数据', form)
dialogVisible.value = false
} catch (err) {
// 校验失败是正常业务,不要抛错
console.warn('表单校验未通过', err)
}
}
/** 关闭前校验 */
const handleBeforeClose = (done: () => void) => {
// 这里可以弹确认框
done()
}
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
📌 理论讲解(重点)
1️⃣ 新增 / 编辑共用逻辑
- 新增
- 重置表单
- title = 新增
- 编辑
- 回填数据
- title = 编辑
⚠️ 真实项目: 不要复制两个 Dialog!一定要共用
2️⃣ 表单校验
formRef.validate()→ 校验通过才提交- 校验失败会自动高亮错误项
3️⃣ before-close(非常重要)
- 弹窗关闭前钩子
- 常用于:
- 提示“是否确认关闭”
- 阻止未保存数据丢失
const handleBeforeClose = (done) => {
// confirm 弹窗
done()
}2
3
4
4️⃣ 常见注意事项(项目经验)
✅ 关闭弹窗时是否重置表单
- 新增:一定要 reset
- 编辑:视情况
✅ 表单 ref
- 一定要
ref<FormInstance>() - TS 项目必做
✅ 不要用 v-if 包 el-dialog
- 会导致表单 ref 丢失
- 推荐用
v-model控制显示
Drawer 抽屉
基础抽屉
🎯 使用场景
- 侧滑面板
- 不希望遮挡整个页面(对比 Dialog)
- 编辑 / 设置 / 快速操作
完整示例:基础 Drawer
<template>
<el-container class="page-container">
<el-main>
<el-button type="primary" @click="drawerVisible = true">
打开抽屉
</el-button>
<el-drawer
v-model="drawerVisible"
title="基础抽屉"
direction="rtl"
size="400px"
>
<p>这是一个基础 Drawer 示例</p>
</el-drawer>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const drawerVisible = ref(false)
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
📌 理论讲解
1️⃣ v-model
- 控制 Drawer 显示 / 隐藏
- 类型:
boolean - 关闭时自动变为
false
2️⃣ direction
- 抽屉出现方向:
rtl→ 右侧(最常用)ltr→ 左侧ttb→ 顶部btt→ 底部
✅ 后台系统 90% 使用
rtl
3️⃣ size
- 抽屉宽度 / 高度
- 常用:
300px / 400px / 500px30% / 40%
详情页展示(高频实战)
🎯 目标效果
- 点击表格“查看”
- 抽屉展示详情
- 表单 只读
- 内容超出可滚动
完整示例:详情 Drawer(推荐用法)
<template>
<el-container class="page-container">
<el-main>
<el-button @click="openDetail">查看详情</el-button>
<el-drawer
v-model="drawerVisible"
title="用户详情"
direction="rtl"
size="500px"
>
<el-form
:model="detail"
label-width="100px"
class="detail-form"
>
<el-form-item label="姓名">
<el-input v-model="detail.name" disabled />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="detail.email" disabled />
</el-form-item>
<el-form-item label="简介">
<el-input
v-model="detail.desc"
type="textarea"
:rows="6"
disabled
/>
</el-form-item>
</el-form>
</el-drawer>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const drawerVisible = ref(false)
const detail = reactive({
name: '',
email: '',
desc: ''
})
const openDetail = () => {
// 模拟接口返回
detail.name = '张三'
detail.email = 'zhangsan@example.com'
detail.desc =
'这里是用户简介内容,通常会比较长。'.repeat(10)
drawerVisible.value = true
}
</script>
<style scoped>
.page-container {
padding: 16px;
}
/* 内容过长时滚动 */
.detail-form {
padding-right: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
📌 理论讲解(重点)
1️⃣ Drawer vs Dialog(选型建议)
| 场景 | 推荐 |
|---|---|
| 新增 / 编辑 | Dialog |
| 查看详情 | Drawer |
| 辅助操作 | Drawer |
| 强打断用户 | Dialog |
2️⃣ 表单只读实现方式(推荐)
✅ 最简单稳定
<el-input disabled />❌ 不推荐:
- 自己写 div + span(样式不统一)
- 条件渲染两套模板
3️⃣ 长内容滚动
- Drawer 默认内容区可滚动
- 表单内容建议:
- 使用
textarea - 合理
rows - 留右侧 padding,避免滚动条压内容
- 使用
4️⃣ 实际项目常见增强点
- 顶部放状态 Tag
- 底部固定操作按钮(查看 → 编辑)
- Drawer 内嵌 Table / Timeline
⚠️ 常见坑 & 注意事项
- 不要频繁销毁 Drawer
- 不用
v-if - 用
v-model控制
- 不用
- 表单只读 ≠ disabled 整个 form
- 单项 disabled 更灵活
- 抽屉太宽
- 会影响主页面感知
- 一般不超过 40%
Message / MessageBox
Message(轻量提示)
使用场景与定位
🎯 使用场景
- 操作成功 / 失败提示
- 接口返回统一提示
- 非阻断式反馈(不打断用户)
📌 核心定位
- Message 用于 结果反馈
- 非模态提示,不会阻断用户操作
- 不用于用户决策或表单校验
基础用法(必会)
本节只关注:如何快速、正确地使用 Message
常用类型
| 方法 | 场景 |
|---|---|
ElMessage.success | 新增 / 保存成功 |
ElMessage.warning | 参数不合法 |
ElMessage.error | 接口异常 |
ElMessage.info | 普通提示 |
示例
const showSuccess = () => {
ElMessage.success({
message: '操作成功',
showClose: true
})
}
const showWarning = () => {
ElMessage.warning({
message: '请注意输入内容',
showClose: true
})
}
const showError = () => {
ElMessage.error({
message: '操作失败',
showClose: true
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
展示行为控制(UI 层)
本节关注 Message 的展示方式,不涉及业务逻辑
纯色模式(plain)
const showPlain = () => {
ElMessage({
message: '数据已更新',
type: 'success',
plain: true
})
}2
3
4
5
6
7
自定义偏移量(offset)
const showOffset = () => {
ElMessage({
message: '操作成功',
type: 'success',
offset: 80
})
}2
3
4
5
6
7
变量提示(高频)
const showWithVariable = () => {
const userName = '张三'
const count = 3
ElMessage.success({
message: `用户 ${userName} 操作成功,共处理 ${count} 条数据`,
showClose: true
})
}2
3
4
5
6
7
8
9
消息管理策略(防滥用)
本节关注:如何避免 Message 滥用或刷屏
分组消息合并
const showGroup = () => {
ElMessage({
message: '分组消息合并提示.',
grouping: true,
type: 'success'
})
}2
3
4
5
6
7
防重复提示
const showSingle = () => {
ElMessage.closeAll()
ElMessage.info({
message: '当前只显示一条提示',
duration: 2000
})
}2
3
4
5
6
7
幂等按钮场景
const disabled = ref(false)
const handleClick = () => {
if (disabled.value) return
disabled.value = true
ElMessage.success('操作生效')
setTimeout(() => {
disabled.value = false
}, 1000)
}2
3
4
5
6
7
8
9
10
11
12
业务场景示例(接口请求)
Message 在真实项目中,通常配合接口请求使用
const mockRequest = async () => {
try {
ElMessage.info({
message: '正在提交...',
duration: 1000
})
await new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve(true) : reject(new Error())
}, 1200)
})
ElMessage.success({
message: '提交成功',
showClose: true
})
} catch {
ElMessage.error({
message: '提交失败,请重试',
showClose: true
})
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
使用原则(实战经验)
📌 实战经验:
不要在每个页面都写大量 Message
Message 应作为 统一反馈出口
推荐集中在以下位置处理:
- 请求拦截器
- 业务公共方法
- 提交成功 / 失败回调
目标:提示可控、语义统一、体验一致
MessageBox(确认框)
🎯 使用场景
- 删除确认
- 危险操作二次确认
- 防误操作
完整示例:删除确认(Promise 风格)
<template>
<el-container class="page-container">
<el-main>
<el-button type="danger" @click="handleDelete">
删除
</el-button>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ElMessageBox, ElMessage } from 'element-plus'
const handleDelete = async () => {
try {
await ElMessageBox.confirm(
'此操作将永久删除该数据,是否继续?',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 确认后执行
ElMessage.success('删除成功')
} catch {
// 取消不需要提示
}
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
📌 理论讲解
1️⃣ ElMessageBox.confirm
- 返回 Promise
- 用户点击:
- 确认 → resolve
- 取消 / 关闭 → reject
2️⃣ 常用参数
| 参数 | 说明 |
|---|---|
message | 提示内容 |
title | 标题 |
type | warning / error / info |
confirmButtonText | 确认按钮文字 |
cancelButtonText | 取消按钮文字 |
(进阶)危险操作二次确认
const handleDanger = async () => {
try {
await ElMessageBox.confirm(
'该操作不可恢复,是否确认执行?',
'高危操作',
{
type: 'error',
confirmButtonText: '我已确认',
cancelButtonText: '取消',
closeOnClickModal: false
}
)
ElMessage.success('操作已执行')
} catch {}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
📌 项目经验:
危险操作一定禁止点击遮罩关闭
tscloseOnClickModal: false1确认按钮文案要 明确责任
Message vs MessageBox(选型总结)
| 场景 | 推荐 |
|---|---|
| 操作结果反馈 | Message |
| 是否继续? | MessageBox |
| 删除 / 清空 | MessageBox |
| 成功 / 失败 | Message |
⚠️ 常见坑 & 注意事项
- MessageBox 不要滥用
- 会打断用户流程
- 取消操作不要提示“已取消”
- 会显得啰嗦
- 接口异常
- 网络错误 → Message.error
- 业务失败 → Message.warning / error
Notification 通知
基础通知
🎯 使用场景
- 系统级提示
- 后台任务完成通知
- 非当前操作触发的反馈
和 Message 的核心区别: Notification 更“全局”,存在时间更长,不打断用户
完整示例:基础 Notification
<template>
<el-container class="page-container">
<el-main>
<el-button type="primary" @click="notifySuccess">
成功通知
</el-button>
<el-button type="warning" @click="notifyWarning">
警告通知
</el-button>
<el-button type="danger" @click="notifyError">
错误通知
</el-button>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ElNotification } from 'element-plus'
const notifySuccess = () => {
ElNotification({
title: '成功',
message: '数据已成功同步',
type: 'success'
})
}
const notifyWarning = () => {
ElNotification({
title: '警告',
message: '部分数据未同步',
type: 'warning'
})
}
const notifyError = () => {
ElNotification({
title: '错误',
message: '同步失败,请稍后重试',
type: 'error'
})
}
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
📌 理论讲解
1️⃣ Notification 特点
- 出现在页面角落(默认右上)
- 不阻断用户操作
- 显示时间比 Message 长
- 适合 “你不一定立刻处理,但需要知道” 的信息
2️⃣ 常用类型
| type | 使用场景 |
|---|---|
| success | 后台任务完成 |
| warning | 异常但可继续 |
| error | 系统级错误 |
| info | 普通通知 |
常用配置参数(高频)
ElNotification({
title: '任务完成',
message: '导出任务已完成,请前往下载中心',
type: 'success',
duration: 4500,
position: 'top-right',
showClose: true
})2
3
4
5
6
7
8
📌 参数说明
| 参数 | 说明 |
|---|---|
title | 标题 |
message | 内容 |
type | 通知类型 |
duration | 自动关闭时间(ms) |
position | 出现位置 |
showClose | 是否显示关闭按钮 |
手动关闭 / 持久通知
🎯 使用场景
- 必须用户明确知晓
- 系统异常 / 权限问题
✅ 示例:不会自动关闭的通知
ElNotification({
title: '系统异常',
message: '检测到异常状态,请立即处理',
type: 'error',
duration: 0 // 不自动关闭
})2
3
4
5
6
📌 项目经验:
duration = 0→ 必须手动关闭- 只用于重要通知,不能滥用
Notification vs Message(关键区别)
| 维度 | Message | Notification |
|---|---|---|
| 出现位置 | 页面中间 | 页面角落 |
| 是否阻断 | 否 | 否 |
| 显示时间 | 短 | 长 |
| 适合场景 | 操作反馈 | 系统通知 |
✅ 简单规则:
- 点击按钮后的结果 → Message
- 后台事件 / 系统状态 → Notification
实际项目高频场景示例
1️⃣ 导出完成通知
ElNotification({
title: '导出完成',
message: '文件已生成,可前往下载',
type: 'success'
})2
3
4
5
2️⃣ 权限变更通知
ElNotification({
title: '权限变更',
message: '你的权限已发生变更,请重新登录',
type: 'warning',
duration: 0
})2
3
4
5
6
3️⃣ WebSocket / SSE 推送
- 新任务
- 新消息
- 审批结果
Notification 是这类 异步推送 的最佳展示方式
常见坑 & 使用规范
⚠️ 常见问题
- Notification 太多
- 会堆满右上角
- 用户会忽略
- 和 Message 混用
- 场景不清晰,体验混乱
✅ 推荐规范(非常实用)
- 用户主动操作结果 → Message
- 系统异步 / 被动结果 → Notification
- 高危 / 必须确认 → MessageBox
Loading
指令方式(v-loading)
🎯 使用场景
- 表格加载
- 表单提交中
- 局部区域加载(推荐)
完整示例:局部 Loading(最常用)
<template>
<el-container class="page-container">
<el-main>
<el-button type="primary" @click="loadData">
加载数据
</el-button>
<el-table
:data="tableData"
border
stripe
v-loading="loading"
style="margin-top: 16px;"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="姓名" />
</el-table>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const loading = ref(false)
const tableData = ref<any[]>([])
const loadData = () => {
loading.value = true
setTimeout(() => {
tableData.value = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
loading.value = false
}, 1500)
}
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
📌 理论讲解
1️⃣ v-loading
- Element Plus 提供的 指令
- 值为
boolean true→ 显示 Loadingfalse→ 隐藏 Loading
2️⃣ 推荐使用位置
✅ 表格 ✅ 表单容器 ✅ Card / 区块容器
❌ 整个页面随便套(会影响体验)
📌 常用修饰参数(了解即可)
<div
v-loading="loading"
element-loading-text="加载中..."
element-loading-background="rgba(255,255,255,0.8)"
>2
3
4
5
element-loading-text→ 提示文字element-loading-background→ 背景遮罩
全屏 Loading(请求期间锁屏)
🎯 使用场景
- 登录
- 系统初始化
- 高危 / 长耗时操作
- 全局接口拦截
完整示例:全屏 Loading
<template>
<el-container class="page-container">
<el-main>
<el-button type="danger" @click="doHeavyTask">
执行耗时操作
</el-button>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ElLoading } from 'element-plus'
const doHeavyTask = () => {
const loading = ElLoading.service({
lock: true,
text: '处理中,请稍候...',
background: 'rgba(0, 0, 0, 0.5)'
})
setTimeout(() => {
loading.close()
}, 2000)
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
📌 理论讲解
1️⃣ ElLoading.service
- 返回一个 Loading 实例
- 必须手动
close()
2️⃣ 常用参数
| 参数 | 说明 |
|---|---|
lock | 是否锁屏 |
text | 提示文本 |
background | 遮罩层背景 |
fullscreen | 是否全屏(默认 true) |
配合接口请求(真实项目)
let loadingInstance: any
const startLoading = () => {
loadingInstance = ElLoading.service({ lock: true })
}
const endLoading = () => {
loadingInstance?.close()
}2
3
4
5
6
7
8
9
📌 常见做法:
- 请求开始 →
startLoading - 请求结束 / 异常 →
endLoading - 推荐放在:
- axios 拦截器
- 全局请求封装
Loading 使用规范(非常重要)
✅ 推荐
- 列表 → 表格 Loading
- 表单提交 → 按钮 Loading / 局部 Loading
- 系统级操作 → 全屏 Loading
❌ 不推荐
- 每个请求都全屏 Loading
- Loading 时间 < 300ms 也强制显示(会闪)
常见坑 & 注意事项
- 忘记 close()
- 页面会被永久锁死
- 多次调用
- 需要统一管理 Loading 实例
- 全屏 Loading + Dialog
- 注意遮罩层层级问题
导航与页面结构
Menu 菜单
Menu 通常与
Layout + Router强绑定,是后台系统导航体系的核心
基础菜单
🎯 组成结构
el-menu:菜单容器el-menu-item:菜单项el-sub-menu:子菜单(多级)
✅ 基础示例(静态菜单)
<template>
<el-menu class="side-menu" default-active="1">
<el-menu-item index="1">
首页
</el-menu-item>
<el-menu-item index="2">
用户管理
</el-menu-item>
<el-sub-menu index="3">
<template #title>
系统设置
</template>
<el-menu-item index="3-1">角色管理</el-menu-item>
<el-menu-item index="3-2">权限管理</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<style scoped>
.side-menu {
width: 200px;
min-height: 100vh;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
📌 说明
index是菜单唯一标识el-sub-menu通过#title定义标题- 适合 原型 / 静态页面
常用配置(高频)
default-active(当前激活菜单)
<el-menu default-active="/dashboard">📌 说明:
- 决定 高亮哪一个菜单
- 实际项目中一般 绑定当前路由路径
router(结合 vue-router)
后台项目强烈推荐开启
<el-menu router>
<el-menu-item index="/dashboard">
仪表盘
</el-menu-item>
<el-menu-item index="/user">
用户管理
</el-menu-item>
</el-menu>2
3
4
5
6
7
8
9
📌 行为说明:
index= 路由路径- 点击菜单 → 自动
router.push(index) - 不需要手动写
@click
collapse(侧边栏折叠)
<el-menu :collapse="isCollapse">
const isCollapse = ref(false)2
📌 使用场景:
- 侧边栏收起 / 展开
- 常与 Header 折叠按钮 联动
Router 菜单完整示例(真实项目)
✅ 示例:SideMenu.vue
<template>
<el-menu
router
:default-active="route.path"
:collapse="collapsed"
class="side-menu"
>
<el-menu-item index="/dashboard">
仪表盘
</el-menu-item>
<el-sub-menu index="/system">
<template #title>
系统管理
</template>
<el-menu-item index="/system/user">
用户管理
</el-menu-item>
<el-menu-item index="/system/role">
角色管理
</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
defineProps<{
collapsed: boolean
}>()
const route = useRoute()
</script>
<style scoped>
.side-menu {
width: 200px;
height: 100vh;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
📌 关键点(面试 + 实战)
default-active使用route.pathrouter模式避免手动跳转- 菜单结构与路由结构 一一对应
动态菜单(权限 / 后端驱动)
90% 中大型后台都会用
示例数据结构
const menus = [
{
path: '/dashboard',
title: '仪表盘'
},
{
path: '/system',
title: '系统管理',
children: [
{ path: '/system/user', title: '用户管理' },
{ path: '/system/role', title: '角色管理' }
]
}
]2
3
4
5
6
7
8
9
10
11
12
13
14
✅ 动态渲染菜单
<el-menu router :default-active="route.path">
<template v-for="menu in menus" :key="menu.path">
<el-menu-item
v-if="!menu.children"
:index="menu.path"
>
{{ menu.title }}
</el-menu-item>
<el-sub-menu
v-else
:index="menu.path"
>
<template #title>
{{ menu.title }}
</template>
<el-menu-item
v-for="child in menu.children"
:key="child.path"
:index="child.path"
>
{{ child.title }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
常见问题 & 规范
⚠️ 常见坑
- index 不唯一
- 会导致高亮错乱
- default-active 写死
- 刷新后状态不对
- 菜单与路由不一致
- 跳转成功但不高亮
Tabs 标签页
Tabs 常用于 状态切换、分类筛选、模块分区展示 本质是一个「受控组件」,核心是
v-model
基础 Tabs
🎯 核心组件
el-tabs:标签页容器el-tab-pane:单个标签页
✅ 基础示例(最小可用)
<template>
<el-tabs v-model="activeTab">
<el-tab-pane label="用户管理" name="user" />
<el-tab-pane label="角色管理" name="role" />
<el-tab-pane label="系统设置" name="setting" />
</el-tabs>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const activeTab = ref('user')
</script>2
3
4
5
6
7
8
9
10
11
12
13
📌 参数说明
| 参数 | 说明 |
|---|---|
v-model | 当前激活的 tab,值 = name |
label | 页签显示文本 |
name | 页签唯一标识(必须唯一) |
⚠️ 注意点
name不写会自动生成,但不推荐- 实战中应 明确 name,方便状态控制
Tabs 内容区域
✅ 带内容的 Tabs
<el-tabs v-model="activeTab">
<el-tab-pane label="基本信息" name="base">
<div>这里是基本信息内容</div>
</el-tab-pane>
<el-tab-pane label="日志记录" name="log">
<div>这里是日志列表</div>
</el-tab-pane>
</el-tabs>2
3
4
5
6
7
8
9
📌 每个 el-tab-pane 内部就是普通 Vue 模板 可放 表单 / 表格 / 任意组件
多状态切换
用 Tabs 替代「状态下拉框」,体验更好
🎯 典型业务
- 全部 / 启用 / 禁用
- 待审核 / 已通过 / 已拒绝
- 处理中 / 已完成
✅ Tabs + 状态筛选示例
<template>
<el-tabs v-model="status" @tab-change="handleTabChange">
<el-tab-pane label="全部" name="all" />
<el-tab-pane label="启用" name="enabled" />
<el-tab-pane label="禁用" name="disabled" />
</el-tabs>
<el-table :data="tableData">
<!-- 表格内容 -->
</el-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const status = ref('all')
const tableData = ref([])
function handleTabChange() {
// 状态切换后重新请求列表
fetchList()
}
function fetchList() {
console.log('当前状态:', status.value)
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
📌 设计要点
- Tabs 本身 不存数据
- 只是作为 筛选条件的一部分
- 切换时重置分页(常见)
列表分类
一个页面多个「视角」,但共用一套逻辑
✅ 分类列表示例
<el-tabs v-model="category">
<el-tab-pane label="我的" name="mine" />
<el-tab-pane label="全部" name="all" />
</el-tabs>
<el-table :data="tableData" />
const category = ref('mine')
watch(category, () => {
fetchList()
})2
3
4
5
6
7
8
9
10
11
📌 适用于:
- 我的任务 / 全部任务
- 我创建的 / 我参与的
卡片风格 Tabs(后台常用)
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="基本信息" name="base" />
<el-tab-pane label="配置管理" name="config" />
</el-tabs>2
3
4
📌 常用类型
| type | 使用场景 |
|---|---|
line(默认) | 状态切换 |
card | 模块切换 |
border-card | 页面分区 |
Tabs + Router(了解)
不算必用,但在多模块后台中会用到
<el-tabs v-model="active" @tab-change="toRoute">
<el-tab-pane label="列表" name="/list" />
<el-tab-pane label="统计" name="/stat" />
</el-tabs>
import { useRouter } from 'vue-router'
const router = useRouter()
function toRoute(name: string) {
router.push(name)
}2
3
4
5
6
7
8
9
10
11
📌 更常见的做法:Menu 控制路由,Tabs 控制状态
常见问题 & 规范
⚠️ 常见坑
name重复 → 切换异常- Tabs 切换不刷新数据
- 切换后分页未重置
✅ 推荐实践
name使用 语义化字符串- Tabs ≠ 数据源,只是筛选条件
- 切换 Tabs:
- 重置分页
- 重新请求接口
Tooltip / Popover
Tooltip / Popover 用于补充说明、弱操作、辅助交互 原则:不打断主流程,不承载核心操作
Tooltip(文字提示 / 说明)
🎯 常见使用场景
- 表格中文本溢出
- 图标说明 / 字段含义解释
- 禁用按钮原因提示
文本溢出提示
<el-tooltip
content="这是一段很长的文本内容,鼠标悬浮时完整展示"
placement="top"
>
<div class="ellipsis">
这是一段很长的文本内容...
</div>
</el-tooltip>
.ellipsis {
width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}2
3
4
5
6
7
8
9
10
11
12
13
14
📌 说明
- Tooltip 负责 展示完整信息
- 样式控制(省略)交给 CSS
- 表格列中使用非常常见
表格列中使用 Tooltip
<el-table-column label="备注">
<template #default="{ row }">
<el-tooltip :content="row.remark">
<span class="ellipsis">
{{ row.remark }}
</span>
</el-tooltip>
</template>
</el-table-column>2
3
4
5
6
7
8
9
📌 注意点:
content可为动态值- 内容为空时建议不显示 Tooltip(业务自行控制)
图标说明(问号提示)
<el-tooltip content="该字段用于标识用户唯一身份">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>2
3
4
5
📌 使用建议:
- 说明型信息优先用 Tooltip
- 不要用 Dialog(太重)
常用参数汇总
| 参数 | 说明 |
|---|---|
content | 提示内容 |
placement | 弹出位置 |
effect | 主题(dark / light) |
disabled | 是否禁用 |
Popover(悬浮卡片 / 操作容器)
Popover = 可承载内容的 Tooltip
🎯 常见使用场景
- 更多操作
- 二级确认
- 小型表单 / 说明卡片
更多操作
<el-popover
placement="bottom"
trigger="click"
>
<template #reference>
<el-button type="primary" link>
更多
</el-button>
</template>
<div class="more-actions">
<el-button link @click="edit">编辑</el-button>
<el-button link type="danger" @click="remove">删除</el-button>
</div>
</el-popover>2
3
4
5
6
7
8
9
10
11
12
13
14
15
📌 说明
trigger="click"更适合操作类- 操作按钮一般用
link样式 - 常用于 表格操作列
二级确认(轻量替代 MessageBox)
<el-popover trigger="click" width="200">
<template #reference>
<el-button type="danger" link>
删除
</el-button>
</template>
<div style="text-align: center;">
<p>确认删除该数据?</p>
<el-button size="small" @click="cancel">取消</el-button>
<el-button
size="small"
type="danger"
@click="confirm"
>
确认
</el-button>
</div>
</el-popover>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
📌 适用场景
| 方式 | 使用建议 |
|---|---|
| Popover | 轻量操作 |
| MessageBox | 强确认、危险操作 |
信息卡片展示
<el-popover trigger="hover" width="300">
<template #reference>
<span class="user-name">张三</span>
</template>
<div>
<p>账号:zhangsan</p>
<p>角色:管理员</p>
<p>状态:启用</p>
</div>
</el-popover>2
3
4
5
6
7
8
9
10
11
📌 常见于:
- 用户名悬浮
- 列表补充信息
Tooltip vs Popover(选择指南)
| 对比项 | Tooltip | Popover |
|---|---|---|
| 内容 | 简短文本 | 任意内容 |
| 交互 | 只读 | 可操作 |
| 触发 | hover 为主 | click / hover |
| 使用频率 | 极高 | 高频 |
常见问题 & 规范
⚠️ 常见坑
- Tooltip 内容过多(可读性差)
- Popover 里放复杂表单(体验差)
- hover + 操作冲突
✅ 推荐规范
- Tooltip:
- 只做说明
- 不承载操作
- Popover:
- 轻量操作
- 内容 ≤ 3 个操作项
- 复杂确认 → MessageBox
你这次说得非常清楚,而且是对的 👍 我现在完全对齐你的格式与要求,并且明确回答你问的这个点:
<component :is="iconName" />✔ 可以用,而且在真实项目中非常常见(尤其是动态菜单 / 动态表格)
下面我按你给的 Layout 章节格式,重新完整给出 Icon 这一章,包含:
- 🎯 目标效果
- ✅ App.vue 完整示例(可直接跑)
- 📌 理论 & 关键点讲解
- ⚠️ 常见坑
- ✅ 特别强调
<component :is="iconName" />的正确姿势
Icon 图标(Element Plus)
Icon 是后台系统中使用频率极高但最容易写乱的部分 本章只讲 项目中真正常用、可维护、可扩展的用法
基础 Icon 使用(组件方式)
🎯 目标效果
- 正常显示 Element Plus Icon
- 控制大小、颜色
- 用于文本、状态、说明
完整示例(App.vue)
<template>
<div class="page">
<h2>基础 Icon 使用</h2>
<div class="row">
<el-icon><Edit /></el-icon>
<el-icon><Search /></el-icon>
<el-icon><Delete /></el-icon>
</div>
<div class="row">
<el-icon size="20" color="#409eff">
<InfoFilled />
</el-icon>
<span>提示信息</span>
</div>
</div>
</template>
<script setup lang="ts">
import {
Edit,
Search,
Delete,
InfoFilled
} from '@element-plus/icons-vue'
</script>
<style scoped>
.page {
padding: 20px;
}
.row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
📌 理论 & 关键点讲解
1️⃣ Icon 本质是什么?
- Element Plus 的 Icon 本质是 Vue 组件
- 所以可以:
- 当普通组件用
- 当动态组件用(重点在后面)
2️⃣ 为什么推荐这种写法?
<el-icon><Edit /></el-icon>- 统一大小与对齐
- 可直接控制
size / color - 项目中最稳定、最通用
Button + Icon(⭐ 项目最高频)
🎯 目标效果
- 按钮左侧 Icon
- 表格操作 Icon
- 圆形 Icon 按钮
完整示例(App.vue)
<template>
<div class="page">
<h2>Button + Icon</h2>
<el-button type="primary" :icon="Plus">
新增
</el-button>
<el-button type="danger" :icon="Delete">
删除
</el-button>
<el-button :icon="Search" circle />
</div>
</template>
<script setup lang="ts">
import { Plus, Delete, Search } from '@element-plus/icons-vue'
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
📌 理论 & 参数说明
<el-button :icon="Plus" />icon接收的是 组件本身- ❌ 不是字符串
- ❌ 不是组件名
动态 Icon(component :is)【重点】
使用 <component :is="icon" />
🎯 目标效果
- Icon 可配置
- Icon 来源于数据
- 常用于:
- 动态菜单
- 表格配置
- 权限驱动 UI
完整示例(App.vue)
<template>
<div class="page">
<h2>动态 Icon(component :is)</h2>
<div
v-for="item in actions"
:key="item.name"
class="action"
>
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 模拟后端 / 配置驱动的 Icon
*/
const actions = [
{
name: 'edit',
label: '编辑',
icon: 'Edit'
},
{
name: 'delete',
label: '删除',
icon: 'Delete'
},
{
name: 'view',
label: '查看',
icon: 'View'
}
]
</script>
<style scoped>
.page {
padding: 20px;
}
.action {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
📌 理论 & 核心解释(非常重要)
1️⃣ 为什么这样能用?
<component :is="item.icon" />item.icon是一个 Vue 组件<component>是 Vue 内置的 动态组件- 完全合法、完全推荐
2️⃣ 为什么不推荐字符串方式?
❌ 不推荐:
<component :is="'Edit'" />原因:
- 依赖全局注册
- TS 无法校验
- 不利于 Tree Shaking
- 容易运行时报错
✅ 推荐(你现在用的方式):
icon: Edit表格 / 菜单中的动态 Icon(真实项目)
🎯 目标效果
- 表格操作列 Icon
- 菜单 Icon 配置化
完整示例(App.vue)
<template>
<el-table :data="tableData" border>
<el-table-column prop="name" label="名称" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button link>
<el-icon>
<component :is="row.icon" />
</el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
const tableData = [
{ name: '数据一', icon: 'Edit' },
{ name: '数据二', icon: 'Delete' }
]
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
常见错误 & 注意事项(很关键)
❌ 错误 1:忘记 el-icon 包裹
<component :is="icon" />- 样式不统一
- 对齐混乱
✅ 正确姿势
<el-icon>
<component :is="icon" />
</el-icon>2
3
使用总结(你项目里就按这个来)
✅ 静态 Icon
<el-icon><Edit /></el-icon>✅ 按钮 Icon
<el-button :icon="Edit" />✅ 动态 Icon(强烈推荐)
<el-icon>
<component :is="icon" />
</el-icon>2
3
Upload 上传(文件 / 图片 / 预览 / 拖拽 / 表单联动)
Upload 是后台系统里坑最多、组合最多的组件之一 本章只覆盖 真实项目 90% 会用到的场景
基础文件上传(单文件)
🎯 目标效果
- 上传单个文件
- 手动控制上传
- 成功 / 失败回调
- 不依赖真实接口(便于本地跑)
完整示例(App.vue)
<template>
<div class="page">
<h2>基础文件上传</h2>
<el-upload
class="upload-demo"
:auto-upload="false"
:on-change="handleChange"
:on-remove="handleRemove"
:limit="1"
>
<el-button type="primary">选择文件</el-button>
</el-upload>
</div>
</template>
<script setup lang="ts">
import type { UploadFile } from 'element-plus'
import {ref} from "vue";
const uploadFile = ref<UploadFile | null>(null);
/**
* 文件变更时触发
*/
const handleChange = (file: UploadFile) => {
console.log('选择的文件:', file)
uploadFile.value = file
}
/**
* 删除文件时
*/
const handleRemove = (file: UploadFile) => {
console.log('删除的文件:', file)
uploadFile.value = null
}
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
📌 理论 & 关键点讲解
1️⃣ el-upload 本质
- 是一个 文件选择 + 状态管理组件
- 是否真的上传,取决于你是否配置
action
2️⃣ auto-upload="false"
- 只选择文件
- 上传动作交给你自己(常见于表单提交)
图片上传 + 预览(⭐ 极高频)
🎯 目标效果
- 只能上传图片
- 显示缩略图
- 支持预览
完整示例(App.vue)
<template>
<div class="page">
<h2>图片上传 + 预览</h2>
<el-upload
action="#"
list-type="picture-card"
:auto-upload="false"
:on-preview="handlePreview"
>
<el-icon><Plus /></el-icon>
</el-upload>
<el-dialog v-model="previewVisible" title="图片预览">
<img :src="previewUrl" class="preview-img" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import type { UploadFile } from 'element-plus'
const previewVisible = ref(false)
const previewUrl = ref('')
/**
* 点击预览
*/
const handlePreview = (file: UploadFile) => {
previewUrl.value = file.url!
previewVisible.value = true
}
</script>
<style scoped>
.page {
padding: 20px;
}
.preview-img {
width: 100%;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
📌 关键参数说明
| 参数 | 作用 |
|---|---|
list-type="picture-card" | 图片卡片模式 |
on-preview | 点击图片回调 |
file.url | 图片地址(后端返回) |
拖拽上传(Drag)
🎯 目标效果
- 支持拖拽
- 批量上传
- 大文件常用
完整示例(App.vue)
<template>
<div class="page">
<h2>拖拽上传</h2>
<el-upload
drag
action="#"
multiple
:auto-upload="false"
:on-change="handleChange"
:on-remove="handleRemove"
>
<el-icon class="upload-icon"><UploadFilled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或 <em>点击上传</em>
</div>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { UploadFilled } from '@element-plus/icons-vue'
import type { UploadFile, UploadFiles } from 'element-plus'
/**
* 存储所有已选择的文件
*/
const uploadFiles = ref<UploadFiles>([])
/**
* 文件变化时
*/
const handleChange = (_file: UploadFile, files: UploadFiles) => {
console.log('选择的文件:', _file)
console.log('文件列表:', files)
uploadFiles.value = files
}
/**
* 删除文件时
*/
const handleRemove = (_file: UploadFile, files: UploadFiles) => {
console.log('删除的文件:', _file)
console.log('文件列表:', files)
uploadFiles.value = files
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
📌 使用场景
- 附件上传
- 文档 / 压缩包
- 多文件业务
上传限制
单文件上传
场景:只允许 1 个 JPG / PNG,≤ 2MB
<template>
<el-upload
action="#"
:auto-upload="false"
:limit="1"
accept=".jpg,.jpeg,.png"
list-type="text"
:on-change="handleChange"
:on-exceed="handleExceed"
>
<el-button type="primary">选择图片</el-button>
</el-upload>
</template>
<script setup lang="ts">
import type { UploadFile, UploadFiles } from 'element-plus'
import { ElMessage } from 'element-plus'
const MAX_SIZE = 2 * 1024 * 1024
const ALLOW_TYPES = ['image/jpeg', 'image/png']
const handleChange = (file: UploadFile, fileList: UploadFiles) => {
const rawFile = file.raw
if (!rawFile) {
return
}
if (!ALLOW_TYPES.includes(rawFile.type)) {
ElMessage.error('仅支持 JPG / PNG 格式')
fileList.splice(fileList.indexOf(file), 1)
return
}
if (rawFile.size > MAX_SIZE) {
ElMessage.error('文件大小不能超过 2MB')
fileList.splice(fileList.indexOf(file), 1)
}
}
const handleExceed = () => {
ElMessage.warning('只能上传一个文件')
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
多文件上传
场景:最多 5 个文件,JPG / PNG,每个 ≤ 2MB
<template>
<el-upload
action="#"
multiple
:auto-upload="false"
:limit="5"
accept=".jpg,.jpeg,.png"
list-type="text"
:on-change="handleChange"
:on-exceed="handleExceed"
>
<el-button type="primary">选择图片</el-button>
</el-upload>
</template>
<script setup lang="ts">
import type { UploadFile, UploadFiles } from 'element-plus'
import { ElMessage } from 'element-plus'
const MAX_SIZE = 2 * 1024 * 1024
const ALLOW_TYPES = ['image/jpeg', 'image/png']
const handleChange = (file: UploadFile, fileList: UploadFiles) => {
const rawFile = file.raw
if (!rawFile) {
return
}
if (!ALLOW_TYPES.includes(rawFile.type)) {
ElMessage.error(`文件 ${rawFile.name} 类型不支持`)
fileList.splice(fileList.indexOf(file), 1)
return
}
if (rawFile.size > MAX_SIZE) {
ElMessage.error(`文件 ${rawFile.name} 超过 2MB`)
fileList.splice(fileList.indexOf(file), 1)
}
}
const handleExceed = (files: File[]) => {
ElMessage.warning(`最多只能上传 5 个文件,本次选择了 ${files.length} 个`)
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Upload + 表单联动(⭐ 真实项目)
🎯 目标效果
- Upload 作为表单字段
- 校验是否上传
- 提交时统一处理
完整示例(App.vue)
<template>
<div class="page">
<h2>Upload + Form 联动</h2>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
>
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="附件" prop="file">
<el-upload
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
>
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">
提交
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance, UploadFile } from 'element-plus'
const formRef = ref<FormInstance>()
const form = ref({
name: '',
file: null as UploadFile | null
})
const rules = {
name: [{ required: true, message: '请输入名称' }],
file: [{ required: true, message: '请上传文件' }]
}
/**
* 选择文件
*/
const handleFileChange = (file: UploadFile) => {
form.value.file = file
}
/**
* 提交表单
*/
const submit = () => {
formRef.value?.validate(valid => {
if (!valid) return
console.log('表单数据:', form.value)
})
}
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
📌 核心思想(非常重要)
- Upload 不负责业务数据
- 表单中只保存:
- file
- fileId
- fileUrl
- 提交时统一处理
上传到服务器
自动上传
选择文件 → 校验 → 自动上传
适合场景
- 表单中上传头像 / 附件
- 不需要“确认按钮”
- 用户体验最顺
<template>
<el-upload
action="/api/upload/image"
:limit="1"
accept=".jpg,.jpeg,.png"
list-type="picture-card"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
const MAX_SIZE = 2 * 1024 * 1024
const ALLOW_TYPES = ['image/jpeg', 'image/png']
const beforeUpload = (file: File) => {
if (!ALLOW_TYPES.includes(file.type)) {
ElMessage.error('仅支持 JPG / PNG 格式')
return false
}
if (file.size > MAX_SIZE) {
ElMessage.error('文件大小不能超过 2MB')
return false
}
return true
}
const handleSuccess = (response: any) => {
ElMessage.success('上传成功')
console.log('后端返回:', response)
}
const handleError = () => {
ElMessage.error('上传失败')
}
const handleExceed = () => {
ElMessage.warning('只能上传一个文件')
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
后端接口参考
package local.ateng.java.config.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/file")
@CrossOrigin
public class FileController {
@PostMapping("/upload")
public Map<String, Object> upload(@RequestParam("file") MultipartFile file) throws IOException {
String fileName = UUID.randomUUID().toString().replace("-", "")
+ "_" + file.getOriginalFilename();
Map<String, Object> result = new HashMap<>();
result.put("name", file.getOriginalFilename());
result.put("url", "/upload/" + fileName);
result.put("createTime", LocalDateTime.now());
return result;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
手动上传
先选文件 → 校验 → 点击按钮再上传
适合场景
- 表单 + 多个字段一起提交
- “保存 / 提交” 按钮
- 多文件统一上传
<template>
<el-upload
ref="uploadRef"
:auto-upload="false"
multiple
:limit="5"
list-type="text"
accept=".jpg,.jpeg,.png"
:on-change="handleChange"
:http-request="mockRequest"
:on-success="handleSuccess"
:on-error="handleError"
>
<el-button type="primary">选择文件</el-button>
</el-upload>
<el-button
type="success"
class="mt-2"
@click="submitUpload"
>
开始上传(模拟)
</el-button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type {
UploadInstance,
UploadFile,
UploadFiles,
UploadRequestOptions
} from 'element-plus'
import { ElMessage } from 'element-plus'
const uploadRef = ref<UploadInstance>()
const MAX_SIZE = 2 * 1024 * 1024
const ALLOW_TYPES = ['image/jpeg', 'image/png']
/**
* 选择阶段校验
*/
const handleChange = (file: UploadFile, fileList: UploadFiles) => {
const raw = file.raw
if (!raw) {
return
}
if (!ALLOW_TYPES.includes(raw.type)) {
ElMessage.error(`文件 ${raw.name} 类型不支持`)
fileList.splice(fileList.indexOf(file), 1)
return
}
if (raw.size > MAX_SIZE) {
ElMessage.error(`文件 ${raw.name} 超过 2MB`)
fileList.splice(fileList.indexOf(file), 1)
}
}
/**
* 模拟上传请求(不走后端)
*/
const mockRequest = (options: UploadRequestOptions) => {
const { file, onSuccess, onError } = options
setTimeout(() => {
if (file.size > 0) {
onSuccess?.({
url: URL.createObjectURL(file),
name: file.name
})
} else {
onError?.({
name: 'UploadError',
message: 'mock upload error'
} as any)
}
}, 1000)
}
/**
* 上传成功回调
*/
const handleSuccess = (_: any, file: UploadFile) => {
ElMessage.success(`文件 ${file.name} 上传成功`)
}
/**
* 上传失败回调
*/
const handleError = () => {
ElMessage.error('上传失败')
}
/**
* 手动触发上传
*/
const submitUpload = () => {
uploadRef.value?.submit()
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
常见坑 & 注意事项(必看)
⚠️ 坑 1:直接依赖 Upload 内部状态
❌ 不推荐:
uploadRef.value.fileList✅ 推荐:
form.file⚠️ 坑 2:Upload 当成自动提交组件
- 大多数后台项目:
- 都是表单提交时再上传
- 而不是选完立刻传
⚠️ 坑 3:图片预览 url 为空
- 本地文件需要
URL.createObjectURL - 后端文件需要返回 url
Image 图片
基础显示图片
el-image 最基础的用法,用于展示网络图片或静态资源图片。
📌 示例:网络图片加载
<template>
<el-container class="page-container">
<el-main>
<h3>基础显示图片示例</h3>
<el-image
src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"
style="width: 200px; height: 120px; border-radius: 4px;"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
// 无业务逻辑
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
📌 说明
src属性用于指定图片资源- 通过
style或class控制宽高和圆角 - 若不指定宽高,图片会按原始尺寸渲染
📌 支持的类型
src 支持:
- 网络地址(HTTP / HTTPS)
- 本地静态资源(需 import / new URL)
- Base64
- Blob URL
const imgBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...'
const imgBlob = URL.createObjectURL(file)2
后面章节会讲静态资源图片与预览功能。
加载失败占位 (slot="error")
当图片加载失败时,可以通过 error 插槽展示自定义兜底 UI(如提示文本、图标等)。
📌 示例
<template>
<el-container class="page-container">
<el-main>
<h3>加载失败占位示例</h3>
<!-- 使用错误地址模拟加载失败 -->
<el-image
src="https://xxx-not-exist.png"
style="width: 200px; height: 120px;"
>
<!-- 加载失败的兜底内容 -->
<template #error>
<div class="image-slot">加载失败</div>
</template>
</el-image>
</el-main>
</el-container>
</template>
<script setup lang="ts">
// 无逻辑
</script>
<style scoped>
.page-container {
padding: 16px;
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #f56c6c;
font-size: 14px;
background: #fef0f0;
border: 1px solid #fde2e2;
border-radius: 4px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
📌 说明
slot="error"用于定义图片加载失败时的占位内容- 通常用于显示: ✔ 提示文案 ✔ 占位图 ✔ 错误图标 ✔ 重试按钮
示例中的 src 使用不存在的链接模拟加载失败,便于演示。
加载中占位 (slot="placeholder")
当图片正在加载过程中,可以使用 placeholder 插槽展示 加载中占位元素(例如:骨架屏、loading 图标等),改善用户体验。
📌 示例
<template>
<el-container class="page-container">
<el-main>
<h3>加载中占位示例</h3>
<!-- 使用真实存在的图片模拟加载过程 -->
<el-image
src="https://element-plus.org/images/element-plus-logo.svg"
style="width: 200px; height: 120px;"
>
<!-- 加载中的占位内容 -->
<template #placeholder>
<div class="placeholder-slot">加载中...</div>
</template>
</el-image>
</el-main>
</el-container>
</template>
<script setup lang="ts">
// 无逻辑
</script>
<style scoped>
.page-container {
padding: 16px;
}
.placeholder-slot {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 14px;
color: #909399;
background: #f4f4f5;
border-radius: 4px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
📌 说明
slot="placeholder"会在图片加载完成之前显示- 推荐用于: ✔ loading 图标 ✔ 骨架屏 ✔ 字样提示
📌 与 slot="error" 区别
| 插槽 | 触发时机 |
|---|---|
| placeholder | 图片加载中 |
| error | 加载失败 |
fit 图片填充模式
el-image 的 fit 属性类似于 CSS 的 object-fit,用于控制图片如何在容器中显示。 常用的 5 个模式:
fillcontaincovernonescale-down
下面示例展示这些模式在固定容器下的不同效果。
📌 示例
<template>
<el-container class="page-container">
<el-main>
<h3>fit 图片填充模式示例</h3>
<div class="fit-list">
<div class="fit-item" v-for="mode in fitModes" :key="mode">
<p class="fit-title">{{ mode }}</p>
<el-image
src="https://element-plus.org/images/element-plus-logo.svg"
:fit="mode"
/>
</div>
</div>
</el-main>
</el-container>
</template>
<script setup lang="ts">
const fitModes = ['fill', 'contain', 'cover', 'none', 'scale-down'];
</script>
<style scoped>
.page-container {
padding: 16px;
}
.fit-list {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.fit-item {
width: 150px;
text-align: center;
}
.fit-title {
margin-bottom: 8px;
font-size: 14px;
color: #606266;
}
.fit-item .el-image {
width: 150px;
height: 100px;
border-radius: 4px;
background: #f5f7fa;
border: 1px solid #ebeef5;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
📌 模式说明
| fit | 效果说明 | 适用场景 |
|---|---|---|
fill | 拉伸铺满,会变形 | 不建议(除非确定宽高相同比例) |
contain | 保持比例完整展示,可能留白 | 海报、商品图片 |
cover | 裁切铺满,不留白 | 头像、banner |
none | 使用原始大小 | 精细图查看 |
scale-down | 取 none 与 contain 中较小的显示方式 | 图标、logo |
好的,继续给出 图片预览功能 示例,使用 Element Plus 官网图片资源,保持文档风格一致。
图片预览功能
通过设置 preview-src-list 属性,可以开启点击图片时的预览(Lightbox)功能。 支持:
✔ 放大缩小 ✔ 拖拽移动 ✔ Esc 退出 ✔ 点击遮罩关闭
📌 示例
<template>
<el-container class="page-container">
<el-main>
<h3>图片预览功能示例</h3>
<el-image
style="width: 200px; height: 120px; cursor: pointer;"
src="https://element-plus.org/images/element-plus-logo.svg"
:preview-src-list="previewList"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
const previewList = [
'https://element-plus.org/images/element-plus-logo.svg'
];
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
📌 说明
preview-src-list必须是数组- 当点击图片时,会自动打开预览弹层
- 预览层显示列表中的所有图片(当前图匹配第一张)
📌 多图切换提示
虽然这里示例只有 1 张图片,preview-src-list 为数组是为了支持多图浏览(下一节会写)。
多图预览
当有多张图片时,只要这些图片的 preview-src-list 属性传入同一数组,就可以实现 点击任意一张 → 进入多图切换预览 功能。
在预览 viewer 中可以:
✔ 左右切换 ✔ 缩放/拖拽 ✔ Esc 退出
📌 示例
<template>
<el-container class="page-container">
<el-main>
<h3>多图预览示例</h3>
<div class="multi-list">
<el-image
v-for="(url, index) in previewList"
:key="index"
:src="url"
:preview-src-list="previewList"
style="width: 200px; height: 120px; cursor: pointer;"
/>
</div>
</el-main>
</el-container>
</template>
<script setup lang="ts">
const previewList = [
'https://element-plus.org/images/element-plus-logo.svg',
'https://element-plus.org/images/element-plus-logo-light.svg',
'https://element-plus.org/images/element-plus-logo-small.svg'
];
</script>
<style scoped>
.page-container {
padding: 16px;
}
.multi-list {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
📌 实现关键点
- 每一个
el-image的preview-src-list要传递 同一个数组 - 数组中的顺序就是预览时的左右切换顺序
cursor: pointer;手势提示用户可预览
📌 实战场景
多图预览非常常见于:
✔ 个人中心 → 照片上传 ✔ 产品管理后台 → 商品图库 ✔ 工单系统 → 附件图片集 ✔ 社区或问答 → 图片内容展示
静态资源图片支持(Vite 中必须)
在 Vite 中,如果要显示项目中的本地静态图片(如 src/assets 下的文件),必须正确处理路径,否则无法加载。
Element Plus 的 el-image 支持以下方式:
✅ 方式一:使用 import 引入
适用于 TypeScript + Vite(推荐方式)
<template>
<el-container class="page-container">
<el-main>
<h3>静态资源图片(import)示例</h3>
<el-image
style="width: 200px; height: 120px;"
:src="logo"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import logo from '@/assets/element-logo.png';
</script>
<style scoped>
.page-container {
padding: 16px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
📌 特点
- 支持构建打包
- 路径经过 Vite 处理,不会失效
- 支持类型推导
✅ 方式二:使用 new URL()(官方推荐 Vite 方案)
适用于不想 import 的情况:
<template>
<el-container class="page-container">
<el-main>
<h3>静态资源图片(URL)示例</h3>
<el-image
style="width: 200px; height: 120px;"
:src="logoUrl"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
const logoUrl = new URL('@/assets/element-logo.png', import.meta.url).href;
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
📌 适用场景
- 图片名称动态(如根据变量拼接)
- 动态主题切换
- 不走
import的组件封装
响应式显示(结合 CSS)
el-image 本身不会强制控制图片比例,因此在实际业务中常与 CSS 配合实现各种响应式效果,例如:
✔ 缩略图 ✔ 宽度自适应 ✔ 图片网格展示 ✔ 等比例裁切(配合 fit)
下面展示常用的响应式缩略图布局。
📌 示例:等比例缩略图 + 自适应布局
<template>
<el-container class="page-container">
<el-main>
<h3>响应式显示(缩略图示例)</h3>
<div class="responsive-list">
<el-image
v-for="(url, index) in imgList"
:key="index"
:src="url"
fit="cover"
class="thumb"
/>
</div>
</el-main>
</el-container>
</template>
<script setup lang="ts">
const imgList = [
'https://element-plus.org/images/element-plus-logo.svg',
'https://element-plus.org/images/element-plus-logo-light.svg',
'https://element-plus.org/images/element-plus-logo-small.svg',
'https://element-plus.org/images/element-plus-logo.svg',
'https://element-plus.org/images/element-plus-logo-light.svg'
];
</script>
<style scoped>
.page-container {
padding: 16px;
}
/* 响应式图片容器 */
.responsive-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
/* 缩略图样式 */
.thumb {
width: 150px;
height: 100px;
border-radius: 6px;
cursor: pointer;
background: #f5f7fa;
border: 1px solid #ebeef5;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
📌 说明
fit="cover"用于裁切铺满容器(常见缩略图方案).responsive-list使用flex-wrap实现自动换行width和height控制缩略图展示尺寸- 可通过媒体查询实现更高级响应式
📌 扩展:宽度自适应容器
如果希望按容器宽度自动缩放:
.thumb {
width: 100%;
height: auto;
}2
3
4
但此时建议配合 object-fit 或 fit 控制比例:
<el-image fit="contain" />📌 实战场景
响应式显示在后台系统中非常常见:
✔ 商品图片列表 ✔ 工单附件图展示 ✔ 用户上传相册预览 ✔ CMS 缩略图展示 ✔ 内容流瀑布布局(配合 Masonry)
图片懒加载(lazy)
通过为 el-image 添加 lazy 属性,可以实现当图片进入可视区域时再加载,从而提升长列表或大量图片页面的性能。
懒加载适用于:
✔ 图片较多(如相册、商品列表) ✔ 页面较长(如 feed 流、动态列表) ✔ 大图场景节省带宽
📌 示例:懒加载长列表
<template>
<el-container class="page-container">
<el-main>
<h3>图片懒加载示例(可滚动容器)</h3>
<!-- 模拟一个小窗口 -->
<div class="scroll-box">
<div class="lazy-list">
<el-image
v-for="(url, index) in imgList"
:key="index"
:src="url"
lazy
class="lazy-item"
fit="cover"
/>
</div>
</div>
</el-main>
</el-container>
</template>
<script setup lang="ts">
// 多放点图片,更容易看到懒加载效果
const imgList = Array.from({ length: 30 }).map((_, i) => {
const imgs = [
'https://element-plus.org/images/element-plus-logo.svg',
'https://element-plus.org/images/element-plus-logo-light.svg',
'https://element-plus.org/images/element-plus-logo-small.svg'
]
return imgs[i % 3]
})
</script>
<style scoped>
.page-container {
padding: 16px;
}
/* 模拟一个“小窗口” */
.scroll-box {
width: 260px;
height: 300px;
border: 1px solid #dcdfe6;
border-radius: 6px;
overflow-y: auto;
padding: 12px;
background: #fff;
}
/* 图片列表 */
.lazy-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 单个图片 */
.lazy-item {
width: 200px;
height: 120px;
border-radius: 6px;
background: #f5f7fa;
border: 1px solid #ebeef5;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
📌 说明
- 添加
lazy后图片只有在进入视口时才会触发加载 fit="cover"确保缩略图裁切且不变形
📌 注意事项
lazy依赖浏览器的IntersectionObserver- 若浏览器不支持,需要自行 polyfill
- 懒加载默认监听窗口滚动,如果在滚动容器内使用,需要保证容器有
overflow: auto:
<div style="height: 400px; overflow: auto;">
<!-- 懒加载图片 -->
</div>2
3
📌 适用场景
- 商品相册
- 用户照片墙
- 工单附件图片
- 内容流(如朋友圈、feed)
- 大屏展示
控制预览行为
el-image 的图片预览功能支持多种行为控制,可通过以下属性调整:
initial-index:进入预览时的初始图片索引hide-on-click-modal:点击遮罩是否关闭zoom-rate:缩放速率min-scale/max-scale:缩放范围preview-teleported:预览层是否 Teleport 到body
下面示例展示最常见的行为控制。
📌 示例:设置初始预览索引
<template>
<el-container class="page-container">
<el-main>
<h3>控制预览行为示例(初始索引)</h3>
<div class="control-list">
<el-image
v-for="(url, index) in previewList"
:key="index"
:src="url"
:preview-src-list="previewList"
:initial-index="2"
class="control-item"
fit="cover"
/>
</div>
</el-main>
</el-container>
</template>
<script setup lang="ts">
const previewList = [
'https://element-plus.org/images/element-plus-logo.svg',
'https://element-plus.org/images/element-plus-logo-light.svg',
'https://element-plus.org/images/element-plus-logo-small.svg'
];
</script>
<style scoped>
.page-container {
padding: 16px;
}
.control-list {
display: flex;
gap: 16px;
}
.control-item {
width: 200px;
height: 120px;
cursor: pointer;
border-radius: 6px;
background: #f5f7fa;
border: 1px solid #ebeef5;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
📌 说明
在上述示例中:
:initial-index="2"表示无论点击哪一张图片,进入预览后默认展示数组中 第 3 张图片- 实际应用中可以动态控制预览的起点(例如查看某条评论对应的图片)
📌 其他行为控制字段(补充说明)
以下为 el-image-viewer(内置 Viewer 组件)支持的一些有用行为控制:
| 属性 | 类型 | 功能 |
|---|---|---|
initial-index | number | 初始显示的图片下标 |
hide-on-click-modal | boolean | 点击遮罩是否关闭 |
zoom-rate | number | 缩放速率 |
min-scale | number | 最小缩放比例 |
max-scale | number | 最大缩放比例 |
preview-teleported | boolean | Teleport 到 body(避免 overflow 隐藏) |
📌 实战用途示例
- 图片列表有分页 → 打开指定页对应的图片
- 聊天窗口/工单系统 → 打开指定附件图片
- 内容评论区 → 点击图片预览从当前那张开始
禁止点击预览
当图片只用于展示(例如 Logo、背景图、缩略图等),不希望用户点击后进入预览模式时,可以通过以下方式禁用预览:
方式一:不设置 preview-src-list 属性(最推荐)
📌 示例
<template>
<el-container class="page-container">
<el-main>
<h3>禁止点击预览示例</h3>
<el-image
style="width: 200px; height: 120px;"
src="https://element-plus.org/images/element-plus-logo.svg"
fit="cover"
class="no-preview-item"
/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
// 无业务逻辑
</script>
<style scoped>
.page-container {
padding: 16px;
}
.no-preview-item {
border-radius: 6px;
background: #f5f7fa;
border: 1px solid #ebeef5;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
📌 说明
- 只要不提供
preview-src-list,图片点击后不会进入预览层 - 推荐这种方式控制行为,简单且明确
方式二:给空数组
<el-image :preview-src-list="[]" src="..." />但这种方式不如不写属性直观,通常不推荐。
📌 适用场景
✔ LOGO 展示 ✔ Avatar 头像(点击进入编辑而非预览) ✔ 业务纯展示图片(如推广图、图标) ✔ UI 背景图
数据切换图片
当响应式数据中的图片地址变化时,el-image 会自动更新显示。
<template>
<el-image :src="imgUrl" style="width: 200px; height: 120px;" />
<el-button @click="switchImg">切换图片</el-button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const list = [
'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg'
];
const imgUrl = ref(list[0]);
const switchImg = () => {
imgUrl.value = imgUrl.value === list[0] ? list[1] : list[0];
};
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Tree 树形控件(权限 / 组织结构 / 菜单)
Tree 是后台系统里最容易写“能跑但不可用”的组件 下面所有示例都来自 真实项目写法,不是 API Demo。
基础 Tree(展示 + 展开)
🎯 使用场景
- 组织结构展示
- 菜单预览
- 分类浏览(只读)
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>基础 Tree</h2>
<el-tree
:data="treeData"
:props="treeProps"
default-expand-all
/>
</div>
</template>
<script setup lang="ts">
/**
* Tree 字段映射
*/
const treeProps = {
label: 'name',
children: 'children',
}
/**
* 树数据(通常来自后端)
*/
const treeData = [
{
id: 1,
name: '总部',
children: [
{ id: 11, name: '技术部' },
{ id: 12, name: '市场部' },
],
},
]
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
📌 关键点
default-expand-all👉 仅适合节点不多的情况- 不写
node-key👉 只能展示,不能操作
复选 Tree(权限分配核心)
🎯 使用场景
- 角色权限
- 菜单勾选
- 功能授权
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>权限 Tree(多选)</h2>
<el-tree
ref="treeRef"
:data="treeData"
show-checkbox
node-key="id"
default-expand-all
:props="treeProps"
/>
<el-button type="primary" @click="getChecked">
获取选中节点
</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ElTree } from 'element-plus'
const treeRef = ref<InstanceType<typeof ElTree>>()
const treeProps = {
label: 'name',
children: 'children',
}
const treeData = [
{
id: 1,
name: '系统管理',
children: [
{ id: 11, name: '用户管理' },
{ id: 12, name: '角色管理' },
],
},
]
/**
* 获取勾选节点(提交给后端)
*/
const getChecked = () => {
const checked = treeRef.value?.getCheckedKeys()
const halfChecked = treeRef.value?.getHalfCheckedKeys()
console.log('全选:', checked)
console.log('半选:', halfChecked)
}
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
📌 必背 API
| 方法 | 说明 |
|---|---|
getCheckedKeys() | 完全选中 |
getHalfCheckedKeys() | 半选(权限核心) |
node-key | 必须有 |
默认回显(编辑必用)
🎯 使用场景
- 编辑角色
- 修改权限
- 回显已选菜单
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>Tree 回显选中</h2>
<el-tree
ref="treeRef"
:data="treeData"
show-checkbox
node-key="id"
default-expand-all
:props="treeProps"
/>
<el-button @click="setChecked">
回显权限
</el-button>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import type { ElTree } from 'element-plus'
const treeRef = ref<InstanceType<typeof ElTree>>()
const treeProps = {
label: 'name',
children: 'children',
}
const treeData = [
{
id: 1,
name: '系统管理',
children: [
{ id: 11, name: '用户管理' },
{ id: 12, name: '角色管理' },
],
},
]
/**
* 设置选中(编辑回显)
*/
const setChecked = async () => {
await nextTick()
treeRef.value?.setCheckedKeys([12])
}
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
⚠️ 真实项目坑点
- 必须
nextTick - 必须先渲染 Tree
- 否则:
setCheckedKeys无效
Tree + Dialog(真实业务形态)
🎯 使用场景
- 弹窗分配权限
- 避免页面跳转
- 状态隔离
✅ 完整示例(App.vue)
<template>
<div class="page">
<el-button type="primary" @click="open">
分配权限
</el-button>
<el-dialog
v-model="visible"
title="权限配置"
width="600px"
destroy-on-close
>
<el-tree
ref="treeRef"
:data="treeData"
show-checkbox
node-key="id"
default-expand-all
:props="treeProps"
/>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submit">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import type { ElTree } from 'element-plus'
const visible = ref(false)
const treeRef = ref<InstanceType<typeof ElTree>>()
const treeProps = {
label: 'name',
children: 'children',
}
const treeData = [
{
id: 1,
name: '系统管理',
children: [
{ id: 11, name: '用户管理' },
{ id: 12, name: '角色管理' },
],
},
]
const open = async () => {
visible.value = true
await nextTick()
treeRef.value?.setCheckedKeys([11])
}
const submit = () => {
const keys = treeRef.value?.getCheckedKeys()
console.log('提交权限:', keys)
visible.value = false
}
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
📌 为什么一定要 destroy-on-close
- 避免上一次勾选残留
- 编辑 / 新增状态完全隔离
- 权限 Tree 必开
Tree 常用配置速查(项目级)
<el-tree
node-key="id"
show-checkbox
default-expand-all
highlight-current
check-strictly
/>2
3
4
5
6
7
| 配置 | 说明 |
|---|---|
highlight-current | 高亮当前节点 |
check-strictly | 父子不联动 |
show-checkbox | 多选 |
node-key | 操作必备 |
懒加载 Tree(大数据量必用)
Tree 节点一多,不懒加载 = 卡死页面
🎯 使用场景
- 组织架构(上万节点)
- 省 / 市 / 区 级联
- 菜单树(后端按层级查)
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>懒加载 Tree</h2>
<el-tree
:props="treeProps"
node-key="id"
lazy
:load="loadNode"
/>
</div>
</template>
<script setup lang="ts">
const treeProps = {
label: 'name',
children: 'children',
isLeaf: 'leaf',
}
/**
* 懒加载节点
*/
interface TreeNodeData {
id: number | string
name: string
leaf?: boolean
}
interface LazyTreeNode {
level: number
data: TreeNodeData
}
const loadNode = (
node: LazyTreeNode,
resolve: (data: TreeNodeData[]) => void,
) => {
console.log(node)
if (node.level === 0) {
resolve([{ id: 1, name: '总部', leaf: false }])
return
}
resolve([
{ id: `${node.data.id}-1`, name: '子部门A', leaf: true },
{ id: `${node.data.id}-2`, name: '子部门B', leaf: true },
])
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
📌 核心认知
lazy + load是一套leaf决定是否还能展开- 不要
default-expand-all❌(会失效)
Tree 勾选规则控制(权限最容易出 Bug)
90% 的权限 Bug 都是「勾选规则没想清楚」
父子不联动(按钮级权限)
<el-tree
show-checkbox
node-key="id"
check-strictly
/>2
3
4
5
🎯 场景
- 页面权限 + 按钮权限
- 勾选按钮 ≠ 勾选页面
禁用某些节点(只读权限)
const treeProps = {
label: 'name',
children: 'children',
disabled: (data: any) => data.disabled === true,
}2
3
4
5
{
id: 1,
name: '系统管理',
disabled: true,
}2
3
4
5
📌 后端字段直透 Tree 是最稳的做法
Tree 搜索 / 过滤(组织 & 菜单必备)
🎯 使用场景
- 快速定位用户 / 菜单
- 组织结构太深
✅ 完整示例
<template>
<el-input
v-model="keyword"
placeholder="输入关键字过滤"
/>
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
:props="treeProps"
:filter-node-method="filterNode"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { ElTree } from 'element-plus'
const treeRef = ref<InstanceType<typeof ElTree>>()
const keyword = ref('')
const treeProps = {
label: 'name',
children: 'children',
}
const treeData = [
{
id: 1,
name: '系统管理',
children: [
{ id: 11, name: '用户管理' },
{ id: 12, name: '角色管理' },
],
},
]
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.name.includes(value)
}
watch(keyword, (val) => {
treeRef.value?.filter(val)
})
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
⚠️ 项目坑点
- 只过滤显示,不会改数据
- 搜索 ≠ 勾选(要单独处理)
Tree 与 Table / 表单联动(高频实战)
🎯 使用场景
- 左 Tree,右 Table
- 点击部门 → 查询用户
<el-tree
:data="deptTree"
node-key="id"
@node-click="handleSelect"
/>2
3
4
5
const handleSelect = (node: any) => {
searchForm.deptId = node.id
loadTable()
}2
3
4
📌 Tree 永远只做「条件选择器」
Tree 状态重置(编辑 / 新增必备)
Tree 最大的坑:状态残留
正确做法(3 选 1)
✅ 方案一:destroy-on-close(你已经写了)
✅ 方案二:手动清空
treeRef.value?.setCheckedKeys([])✅ 方案三:key 强制刷新
<el-tree :key="treeKey" />treeKey.value++Cascader 级联选择器(区域 / 组织 / 表单联动)
Cascader ≠ 下拉框 它解决的是:层级关系 + 选择约束 + 数据联动
基础 Cascader(展示 + 选择)
🎯 使用场景
- 省 / 市 / 区
- 分类层级选择
- 简单组织结构
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>基础 Cascader</h2>
<el-cascader
v-model="value"
:options="options"
placeholder="请选择地区"
clearable
/>
<p>选中值:{{ value }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
/**
* 选中的路径值
* 示例:['zhejiang', 'hangzhou', 'xihu']
*/
const value = ref<string[]>([])
/**
* 级联数据
*/
const options = [
{
value: 'zhejiang',
label: '浙江省',
children: [
{
value: 'hangzhou',
label: '杭州市',
children: [
{ value: 'xihu', label: '西湖区' },
],
},
],
},
]
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
📌 参数说明
| 参数 | 说明 |
|---|---|
v-model | 完整路径数组 |
options | 树形数据 |
clearable | 高频必开 |
placeholder | UX 必备 |
只返回最后一级(表单最常用)
🎯 使用场景
- 后端只要
districtId - 表单提交
- 搜索条件
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>只返回最后一级</h2>
<el-cascader
v-model="value"
:options="options"
:props="{ emitPath: false }"
clearable
/>
<p>选中值:{{ value }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
/**
* 只返回末级
*/
const value = ref<string | null>(null)
const options = [
{
value: 'dept1',
label: '总部',
children: [
{ value: 'dept11', label: '技术部' },
{ value: 'dept12', label: '市场部' },
],
},
]
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
📌 核心配置
:props="{ emitPath: false }"👉 99% 表单都该开这个
禁止选择非叶子节点(真实业务)
🎯 使用场景
- 只能选最底层部门
- 只能选区县,不能选省市
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>仅允许选择叶子节点</h2>
<el-cascader
v-model="value"
:options="options"
:props="cascaderProps"
clearable
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref<string | null>(null)
const cascaderProps = {
emitPath: false,
checkStrictly: false, // 默认
}
const options = [
{
value: 'a',
label: '一级',
children: [
{
value: 'a-1',
label: '二级',
children: [
{ value: 'a-1-1', label: '三级' },
],
},
],
},
]
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
⚠️ 注意
- 不要开启
checkStrictly: true - 否则父节点也可选 ❌
可搜索 Cascader(数据多必用)
🎯 使用场景
- 城市数据
- 大组织树
- 字典级联
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>可搜索 Cascader</h2>
<el-cascader
v-model="value"
:options="options"
filterable
clearable
placeholder="搜索部门"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref<string[]>([])
const options = [
{
value: 'root',
label: '总部',
children: [
{ value: 'dev', label: '研发部' },
{ value: 'hr', label: '人事部' },
],
},
]
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
📌 真实体验
- 搜索的是 label
- 自动展开路径
- 极大提升 UX
动态加载(懒加载,接口必备)
🎯 使用场景
- 全国城市
- 超大组织架构
- 接口分页加载
✅ 完整示例(App.vue)
<template>
<div class="page">
<h2>懒加载 Cascader</h2>
<el-cascader
v-model="value"
:props="cascaderProps"
clearable
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref<string[]>([])
const cascaderProps = {
lazy: true,
emitPath: false,
lazyLoad(node: any, resolve: any) {
const { level } = node
setTimeout(() => {
if (level === 0) {
resolve([
{ value: 'zj', label: '浙江省', leaf: false },
])
} else {
resolve([
{ value: 'hz', label: '杭州市', leaf: true },
])
}
}, 500)
},
}
</script>
<style scoped>
.page {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
📌 接口对接要点
leaf: true必须返回resolve一定要调用- 常配合 Loading
Cascader + Form(高频组合)
🎯 使用场景
- 搜索表单
- 新增 / 编辑页
- 校验联动
✅ 完整示例(App.vue)
<template>
<el-form :model="form" label-width="100px">
<el-form-item label="所属部门" prop="deptId">
<el-cascader
v-model="form.deptId"
:options="options"
:props="{ emitPath: false }"
clearable
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({
deptId: null as string | null,
})
const options = [
{
value: '1',
label: '总部',
children: [
{ value: '11', label: '研发部' },
],
},
]
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
⚠️ 表单常见坑
emitPath不关 → 后端接收数组 ❌- 编辑页需回显单值
- 校验写在
el-form-item
Cascader 项目级配置速查
:props="{
emitPath: false,
lazy: true,
checkStrictly: false
}"2
3
4
5
| 场景 | 推荐 |
|---|---|
| 表单 | emitPath: false |
| 数据多 | lazy: true |
| 禁父选 | 默认即可 |