🎯 目标效果
- 页面 高度撑满整个视口
- 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>📌 理论 & 关键点讲解
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>📌 理论 & 参数说明
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>原因很重要 👇
Container的布局方向由 子组件类型决定- 同级出现
el-aside→ 横向布局 - 内层没有
el-aside→ 自动纵向
👉 这是 Element Plus Layout 的设计核心
- Aside 一定要固定宽度
- 折叠只做宽度变化,避免
v-if - 滚动永远放在
el-main - Header 高度统一(60px 是事实标准)
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>📌 理论讲解(非常关键)
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>📌 响应式参数说明
| 参数 | 含义 |
|---|---|
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>📌 搜索布局核心思想(一定要记住)
1️⃣ 一行 4 列的黄金比例
lg = 6 // 24 / 4 = 6
md = 8 // 24 / 3 = 8
sm = 12 // 24 / 2 = 12
xs = 24 // 1 行 1 个👉 这是后台搜索区的事实标准
2️⃣ 为什么按钮单独一列?
- 对齐好控制
- 不受 label 宽度影响
- 小屏时自然换行
3️⃣ 为什么按钮列要 24?
:md="24"- 确保:
- 小屏换到下一行
- 不挤占输入框空间
❌ 所有列 span 写死
❌ 按钮和表单项混在一起
❌ 不写响应式导致小屏崩掉
❌ 用 margin-left 硬推按钮位置
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>📌 理论 & 参数说明
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>📌 理论说明
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>📌 理论说明
:disabled会递归禁用表单内的所有输入控件- 配合 同一个表单组件,可实现:
- 新增:
disabled = false - 查看详情:
disabled = true
- 新增:
⚠️ 不会影响表单校验逻辑,仍然可以validate
🎯 目标效果
- 普通文本输入
- 可清空
- 密码可切换显示
- 限制长度
- 显示输入字数
完整示例:基础 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>📌 理论讲解
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>📌 理论讲解
1️⃣ 前缀 #prefix
- 显示在输入框最左侧
- 可放图标 / 文本 / 组件
- 常用场景:搜索图标、货币符号(¥)
2️⃣ 后缀 #suffix
- 显示在输入框最右侧
- 可放按钮 / 清空 / 状态提示
- 常用场景:
- 清空按钮
- 输入验证状态(✔️ / ❌)
- 日期选择按钮
3️⃣ 注意事项
- 插槽本身不会改变输入框的
v-model - 如果是按钮操作,需要手动操作数据
- 不要在 prefix/suffix 放复杂表单控件,会影响布局
🎯 目标效果
- 下拉选择
- 可清空
- 可搜索过滤
- 占位提示
完整示例:基础 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>📌 理论讲解
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️⃣ 枚举映射
- 常见场景:接口返回
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>✅ 优点:代码可维护,枚举值集中管理
3️⃣ 禁用选项
- 有些角色或状态不可选,用
disabled控制
<el-option label="游客" value="guest" disabled />⚠️ 注意:v-model不能绑定到禁用值,否则表单会报错- 建议在初始化时排除不可选值或提示用户
📌 实战注意事项
- 动态数据必须保证 key 唯一
- filterable 下拉与 clearable 一起用非常顺手
- 枚举映射 + v-for + :key = value 是标准写法
- 禁用选项不要做默认值
- 表单校验依然使用 prop 绑定 form 字段
🎯 目标效果
- 单个日期或日期时间选择
- 可以自定义显示格式
- 可以绑定后端接口标准格式(如
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>📌 理论讲解
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>📌 理论讲解
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 单选
<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>📌 理论讲解
1️⃣ v-model
- 双向绑定选中值
el-radio的label值对应v-model
2️⃣ border
- 外观带边框按钮风格
- 常用于状态 / 类型选择
3️⃣ 注意事项
el-radio-group必须有v-model- 每个
el-radio的label唯一 - 可与表单校验结合(
prop+rules)
🎯 目标效果
- 多选字段
- 支持全选 / 反选
- 常用场景:权限分配、标签选择
完整示例: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>📌 理论讲解
1️⃣ v-model
- 多选绑定数组
- 数组内元素 = 被选中的
label
2️⃣ 全选 / 反选逻辑
indeterminate→ 半选状态- 单个选项变化时需要更新
checkAll和indeterminate - 常用于权限、标签列表
3️⃣ 注意事项
label唯一且对应v-model类型- 数组操作时保持响应式,使用
reactive或ref - 可结合表单校验(必选项 / 最少选项)
🎯 目标效果
- 渲染表格数据
- 带边框 / 斑马纹
- 指定
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>📌 理论讲解
: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><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>- 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))
}- type="selection" → 显示复选框
- @selection-change → 获取选中行
- 可配合批量操作按钮
<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)- empty-text → 自定义空数据提示
- v-loading → 表格加载中效果
🎯 目标效果
- 显示页码
- 每页条数
- 总条数
完整示例:基础分页
<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>📌 理论讲解
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)
}📌 理论讲解
@current-change→ 页码改变时触发@size-change→ 每页条数改变时触发- 重置页码
- 搜索条件改变或 pageSize 改变时,通常重置
currentPage = 1 - 避免页码越界或查询结果不正确
- 搜索条件改变或 pageSize 改变时,通常重置
page-sizes- 可配置用户可选的每页条数数组
- 常用
[10, 20, 50, 100]
<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>📌 理论讲解
- 搜索 + 分页
- 搜索条件改变时 →
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>📌 理论讲解
- 为什么默认不支持跨页选择
el-table的选中状态只和当前data绑定- 翻页后
data变化,选中状态会被重置
- 核心解决思路
- 将选中状态从
el-table内部提升到业务层 - 使用
Map / Set以row-key作为唯一标识保存选中数据
- 将选中状态从
- 关键实现点
row-key必须唯一且稳定- 翻页加载数据后,手动回显当前页的选中状态
- 恢复选中过程中,忽略
selection-change事件
- 为什么要使用恢复标识
- 翻页时
el-table会自动触发一次selection-change - 若不拦截,会误删其他页的选中数据
- 翻页时
- 适用场景
- 批量操作(删除、导出、审批)
- 后端分页数据
- 大数据列表(推荐只保存 ID)
🎯 使用场景
- 简单提示弹窗
- 信息展示
- 作为新增 / 编辑的容器
完整示例:基础 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>📌 理论讲解
1️⃣ v-model
- 控制弹窗显示 / 隐藏
- 必须是 boolean
- 关闭弹窗时会自动变为
false
2️⃣ title
- 弹窗标题
- 可动态绑定(新增 / 编辑切换)
3️⃣ width
- 常用:
400px / 500px / 600px / 60% - 后台表单一般 不要太窄
🎯 使用场景
- 确认 / 取消按钮
- 提交表单
- 自定义操作区布局
完整示例:自定义 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
}📌 理论讲解
#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>📌 理论讲解(重点)
1️⃣ 新增 / 编辑共用逻辑
- 新增
- 重置表单
- title = 新增
- 编辑
- 回填数据
- title = 编辑
⚠️ 真实项目: 不要复制两个 Dialog!一定要共用
2️⃣ 表单校验
formRef.validate()→ 校验通过才提交- 校验失败会自动高亮错误项
3️⃣ before-close(非常重要)
- 弹窗关闭前钩子
- 常用于:
- 提示“是否确认关闭”
- 阻止未保存数据丢失
const handleBeforeClose = (done) => {
// confirm 弹窗
done()
}4️⃣ 常见注意事项(项目经验)
✅ 关闭弹窗时是否重置表单
- 新增:一定要 reset
- 编辑:视情况
✅ 表单 ref
- 一定要
ref<FormInstance>() - TS 项目必做
✅ 不要用 v-if 包 el-dialog
- 会导致表单 ref 丢失
- 推荐用
v-model控制显示
🎯 使用场景
- 侧滑面板
- 不希望遮挡整个页面(对比 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>📌 理论讲解
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>📌 理论讲解(重点)
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 用于 结果反馈
- 非模态提示,不会阻断用户操作
- 不用于用户决策或表单校验
本节只关注:如何快速、正确地使用 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
})
}本节关注 Message 的展示方式,不涉及业务逻辑
纯色模式(plain)
const showPlain = () => {
ElMessage({
message: '数据已更新',
type: 'success',
plain: true
})
}自定义偏移量(offset)
const showOffset = () => {
ElMessage({
message: '操作成功',
type: 'success',
offset: 80
})
}变量提示(高频)
const showWithVariable = () => {
const userName = '张三'
const count = 3
ElMessage.success({
message: `用户 ${userName} 操作成功,共处理 ${count} 条数据`,
showClose: true
})
}本节关注:如何避免 Message 滥用或刷屏
分组消息合并
const showGroup = () => {
ElMessage({
message: '分组消息合并提示.',
grouping: true,
type: 'success'
})
}防重复提示
const showSingle = () => {
ElMessage.closeAll()
ElMessage.info({
message: '当前只显示一条提示',
duration: 2000
})
}幂等按钮场景
const disabled = ref(false)
const handleClick = () => {
if (disabled.value) return
disabled.value = true
ElMessage.success('操作生效')
setTimeout(() => {
disabled.value = false
}, 1000)
}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
})
}
}📌 实战经验:
-
不要在每个页面都写大量 Message
-
Message 应作为 统一反馈出口
-
推荐集中在以下位置处理:
- 请求拦截器
- 业务公共方法
- 提交成功 / 失败回调
目标:提示可控、语义统一、体验一致
🎯 使用场景
- 删除确认
- 危险操作二次确认
- 防误操作
完整示例:删除确认(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>📌 理论讲解
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 {}
}📌 项目经验:
-
危险操作一定禁止点击遮罩关闭
closeOnClickModal: false -
确认按钮文案要 明确责任
| 场景 | 推荐 |
|---|---|
| 操作结果反馈 | Message |
| 是否继续? | MessageBox |
| 删除 / 清空 | MessageBox |
| 成功 / 失败 | Message |
- MessageBox 不要滥用
- 会打断用户流程
- 取消操作不要提示“已取消”
- 会显得啰嗦
- 接口异常
- 网络错误 → Message.error
- 业务失败 → Message.warning / error
🎯 使用场景
- 系统级提示
- 后台任务完成通知
- 非当前操作触发的反馈
和 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>📌 理论讲解
1️⃣ Notification 特点
- 出现在页面角落(默认右上)
- 不阻断用户操作
- 显示时间比 Message 长
- 适合 “你不一定立刻处理,但需要知道” 的信息
2️⃣ 常用类型
| type | 使用场景 |
|---|---|
| success | 后台任务完成 |
| warning | 异常但可继续 |
| error | 系统级错误 |
| info | 普通通知 |
ElNotification({
title: '任务完成',
message: '导出任务已完成,请前往下载中心',
type: 'success',
duration: 4500,
position: 'top-right',
showClose: true
})📌 参数说明
| 参数 | 说明 |
|---|---|
title |
标题 |
message |
内容 |
type |
通知类型 |
duration |
自动关闭时间(ms) |
position |
出现位置 |
showClose |
是否显示关闭按钮 |
🎯 使用场景
- 必须用户明确知晓
- 系统异常 / 权限问题
✅ 示例:不会自动关闭的通知
ElNotification({
title: '系统异常',
message: '检测到异常状态,请立即处理',
type: 'error',
duration: 0 // 不自动关闭
})📌 项目经验:
duration = 0→ 必须手动关闭- 只用于重要通知,不能滥用
| 维度 | Message | Notification |
|---|---|---|
| 出现位置 | 页面中间 | 页面角落 |
| 是否阻断 | 否 | 否 |
| 显示时间 | 短 | 长 |
| 适合场景 | 操作反馈 | 系统通知 |
✅ 简单规则:
- 点击按钮后的结果 → Message
- 后台事件 / 系统状态 → Notification
1️⃣ 导出完成通知
ElNotification({
title: '导出完成',
message: '文件已生成,可前往下载',
type: 'success'
})2️⃣ 权限变更通知
ElNotification({
title: '权限变更',
message: '你的权限已发生变更,请重新登录',
type: 'warning',
duration: 0
})3️⃣ WebSocket / SSE 推送
- 新任务
- 新消息
- 审批结果
Notification 是这类 异步推送 的最佳展示方式
- Notification 太多
- 会堆满右上角
- 用户会忽略
- 和 Message 混用
- 场景不清晰,体验混乱
✅ 推荐规范(非常实用)
- 用户主动操作结果 → Message
- 系统异步 / 被动结果 → Notification
- 高危 / 必须确认 → MessageBox
🎯 使用场景
- 表格加载
- 表单提交中
- 局部区域加载(推荐)
完整示例:局部 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>📌 理论讲解
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)"
>element-loading-text→ 提示文字element-loading-background→ 背景遮罩
🎯 使用场景
- 登录
- 系统初始化
- 高危 / 长耗时操作
- 全局接口拦截
完整示例:全屏 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>📌 理论讲解
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()
}📌 常见做法:
- 请求开始 →
startLoading - 请求结束 / 异常 →
endLoading - 推荐放在:
- axios 拦截器
- 全局请求封装
✅ 推荐
- 列表 → 表格 Loading
- 表单提交 → 按钮 Loading / 局部 Loading
- 系统级操作 → 全屏 Loading
❌ 不推荐
- 每个请求都全屏 Loading
- Loading 时间 < 300ms 也强制显示(会闪)
- 忘记 close()
- 页面会被永久锁死
- 多次调用
- 需要统一管理 Loading 实例
- 全屏 Loading + Dialog
- 注意遮罩层层级问题
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>📌 说明
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>📌 行为说明:
index= 路由路径- 点击菜单 → 自动
router.push(index) - 不需要手动写
@click
collapse(侧边栏折叠)
<el-menu :collapse="isCollapse">
const isCollapse = ref(false)📌 使用场景:
- 侧边栏收起 / 展开
- 常与 Header 折叠按钮 联动
✅ 示例: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>📌 关键点(面试 + 实战)
default-active使用route.pathrouter模式避免手动跳转- 菜单结构与路由结构 一一对应
90% 中大型后台都会用
示例数据结构
const menus = [
{
path: '/dashboard',
title: '仪表盘'
},
{
path: '/system',
title: '系统管理',
children: [
{ path: '/system/user', title: '用户管理' },
{ path: '/system/role', title: '角色管理' }
]
}
]✅ 动态渲染菜单
<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>- index 不唯一
- 会导致高亮错乱
- default-active 写死
- 刷新后状态不对
- 菜单与路由不一致
- 跳转成功但不高亮
Tabs 常用于 状态切换、分类筛选、模块分区展示 本质是一个「受控组件」,核心是
v-model
🎯 核心组件
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>📌 参数说明
| 参数 | 说明 |
|---|---|
v-model |
当前激活的 tab,值 = name |
label |
页签显示文本 |
name |
页签唯一标识(必须唯一) |
name不写会自动生成,但不推荐- 实战中应 明确 name,方便状态控制
✅ 带内容的 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>📌 每个 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>📌 设计要点
- 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()
})📌 适用于:
- 我的任务 / 全部任务
- 我创建的 / 我参与的
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="基本信息" name="base" />
<el-tab-pane label="配置管理" name="config" />
</el-tabs>📌 常用类型
| type | 使用场景 |
|---|---|
line(默认) |
状态切换 |
card |
模块切换 |
border-card |
页面分区 |
不算必用,但在多模块后台中会用到
<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)
}📌 更常见的做法:Menu 控制路由,Tabs 控制状态
name重复 → 切换异常- Tabs 切换不刷新数据
- 切换后分页未重置
✅ 推荐实践
name使用 语义化字符串- Tabs ≠ 数据源,只是筛选条件
- 切换 Tabs:
- 重置分页
- 重新请求接口
Tooltip / Popover 用于补充说明、弱操作、辅助交互 原则:不打断主流程,不承载核心操作
🎯 常见使用场景
- 表格中文本溢出
- 图标说明 / 字段含义解释
- 禁用按钮原因提示
<el-tooltip
content="这是一段很长的文本内容,鼠标悬浮时完整展示"
placement="top"
>
<div class="ellipsis">
这是一段很长的文本内容...
</div>
</el-tooltip>
.ellipsis {
width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}📌 说明
- Tooltip 负责 展示完整信息
- 样式控制(省略)交给 CSS
- 表格列中使用非常常见
<el-table-column label="备注">
<template #default="{ row }">
<el-tooltip :content="row.remark">
<span class="ellipsis">
{{ row.remark }}
</span>
</el-tooltip>
</template>
</el-table-column>📌 注意点:
content可为动态值- 内容为空时建议不显示 Tooltip(业务自行控制)
<el-tooltip content="该字段用于标识用户唯一身份">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>📌 使用建议:
- 说明型信息优先用 Tooltip
- 不要用 Dialog(太重)
| 参数 | 说明 |
|---|---|
content |
提示内容 |
placement |
弹出位置 |
effect |
主题(dark / light) |
disabled |
是否禁用 |
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>📌 说明
trigger="click"更适合操作类- 操作按钮一般用
link样式 - 常用于 表格操作列
<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>📌 适用场景
| 方式 | 使用建议 |
|---|---|
| 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>📌 常见于:
- 用户名悬浮
- 列表补充信息
| 对比项 | 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
- 控制大小、颜色
- 用于文本、状态、说明
完整示例(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>📌 理论 & 关键点讲解
1️⃣ Icon 本质是什么?
- Element Plus 的 Icon 本质是 Vue 组件
- 所以可以:
- 当普通组件用
- 当动态组件用(重点在后面)
2️⃣ 为什么推荐这种写法?
<el-icon><Edit /></el-icon>- 统一大小与对齐
- 可直接控制
size / color - 项目中最稳定、最通用
🎯 目标效果
- 按钮左侧 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>📌 理论 & 参数说明
<el-button :icon="Plus" />icon接收的是 组件本身- ❌ 不是字符串
- ❌ 不是组件名
使用 <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>📌 理论 & 核心解释(非常重要)
1️⃣ 为什么这样能用?
<component :is="item.icon" />item.icon是一个 Vue 组件<component>是 Vue 内置的 动态组件- 完全合法、完全推荐
2️⃣ 为什么不推荐字符串方式?
❌ 不推荐:
<component :is="'Edit'" />原因:
- 依赖全局注册
- TS 无法校验
- 不利于 Tree Shaking
- 容易运行时报错
✅ 推荐(你现在用的方式):
icon: Edit🎯 目标效果
- 表格操作列 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>❌ 错误 1:忘记 el-icon 包裹
<component :is="icon" />- 样式不统一
- 对齐混乱
✅ 正确姿势
<el-icon>
<component :is="icon" />
</el-icon>✅ 静态 Icon
<el-icon><Edit /></el-icon>✅ 按钮 Icon
<el-button :icon="Edit" />✅ 动态 Icon(强烈推荐)
<el-icon>
<component :is="icon" />
</el-icon>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>📌 理论 & 关键点讲解
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>📌 关键参数说明
| 参数 | 作用 |
|---|---|
list-type="picture-card" |
图片卡片模式 |
on-preview |
点击图片回调 |
file.url |
图片地址(后端返回) |
🎯 目标效果
- 支持拖拽
- 批量上传
- 大文件常用
完整示例(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>📌 使用场景
- 附件上传
- 文档 / 压缩包
- 多文件业务
单文件上传
场景:只允许 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>多文件上传
场景:最多 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>🎯 目标效果
- 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>📌 核心思想(非常重要)
- 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>后端接口参考
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;
}
}手动上传
先选文件 → 校验 → 点击按钮再上传
适合场景
- 表单 + 多个字段一起提交
- “保存 / 提交” 按钮
- 多文件统一上传
<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>❌ 不推荐:
uploadRef.value.fileList✅ 推荐:
form.file- 大多数后台项目:
- 都是表单提交时再上传
- 而不是选完立刻传
- 本地文件需要
URL.createObjectURL - 后端文件需要返回 url
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>📌 说明
src属性用于指定图片资源- 通过
style或class控制宽高和圆角 - 若不指定宽高,图片会按原始尺寸渲染
📌 支持的类型
src 支持:
- 网络地址(HTTP / HTTPS)
- 本地静态资源(需 import / new URL)
- Base64
- Blob URL
const imgBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...'
const imgBlob = URL.createObjectURL(file)后面章节会讲静态资源图片与预览功能。
当图片加载失败时,可以通过 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>📌 说明
slot="error"用于定义图片加载失败时的占位内容- 通常用于显示: ✔ 提示文案 ✔ 占位图 ✔ 错误图标 ✔ 重试按钮
示例中的 src 使用不存在的链接模拟加载失败,便于演示。
当图片正在加载过程中,可以使用 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>📌 说明
slot="placeholder"会在图片加载完成之前显示- 推荐用于: ✔ loading 图标 ✔ 骨架屏 ✔ 字样提示
📌 与 slot="error" 区别
| 插槽 | 触发时机 |
|---|---|
| placeholder | 图片加载中 |
| error | 加载失败 |
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>📌 模式说明
| 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>📌 说明
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>📌 实现关键点
- 每一个
el-image的preview-src-list要传递 同一个数组 - 数组中的顺序就是预览时的左右切换顺序
cursor: pointer;手势提示用户可预览
📌 实战场景
多图预览非常常见于:
✔ 个人中心 → 照片上传 ✔ 产品管理后台 → 商品图库 ✔ 工单系统 → 附件图片集 ✔ 社区或问答 → 图片内容展示
在 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>📌 特点
- 支持构建打包
- 路径经过 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>📌 适用场景
- 图片名称动态(如根据变量拼接)
- 动态主题切换
- 不走
import的组件封装
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>📌 说明
fit="cover"用于裁切铺满容器(常见缩略图方案).responsive-list使用flex-wrap实现自动换行width和height控制缩略图展示尺寸- 可通过媒体查询实现更高级响应式
📌 扩展:宽度自适应容器
如果希望按容器宽度自动缩放:
.thumb {
width: 100%;
height: auto;
}但此时建议配合 object-fit 或 fit 控制比例:
<el-image fit="contain" />📌 实战场景
响应式显示在后台系统中非常常见:
✔ 商品图片列表 ✔ 工单附件图展示 ✔ 用户上传相册预览 ✔ CMS 缩略图展示 ✔ 内容流瀑布布局(配合 Masonry)
通过为 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>📌 说明
- 添加
lazy后图片只有在进入视口时才会触发加载 fit="cover"确保缩略图裁切且不变形
📌 注意事项
lazy依赖浏览器的IntersectionObserver- 若浏览器不支持,需要自行 polyfill
- 懒加载默认监听窗口滚动,如果在滚动容器内使用,需要保证容器有
overflow: auto:
<div style="height: 400px; overflow: auto;">
<!-- 懒加载图片 -->
</div>📌 适用场景
- 商品相册
- 用户照片墙
- 工单附件图片
- 内容流(如朋友圈、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>📌 说明
在上述示例中:
: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>📌 说明
- 只要不提供
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>Tree 是后台系统里最容易写“能跑但不可用”的组件 下面所有示例都来自 真实项目写法,不是 API Demo。
🎯 使用场景
- 组织结构展示
- 菜单预览
- 分类浏览(只读)
✅ 完整示例(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>📌 关键点
default-expand-all👉 仅适合节点不多的情况- 不写
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 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>📌 必背 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>- 必须
nextTick - 必须先渲染 Tree
- 否则:
setCheckedKeys无效
🎯 使用场景
- 弹窗分配权限
- 避免页面跳转
- 状态隔离
✅ 完整示例(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>📌 为什么一定要 destroy-on-close
- 避免上一次勾选残留
- 编辑 / 新增状态完全隔离
- 权限 Tree 必开
<el-tree
node-key="id"
show-checkbox
default-expand-all
highlight-current
check-strictly
/>| 配置 | 说明 |
|---|---|
highlight-current |
高亮当前节点 |
check-strictly |
父子不联动 |
show-checkbox |
多选 |
node-key |
操作必备 |
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>📌 核心认知
lazy + load是一套leaf决定是否还能展开- 不要
default-expand-all❌(会失效)
90% 的权限 Bug 都是「勾选规则没想清楚」
父子不联动(按钮级权限)
<el-tree
show-checkbox
node-key="id"
check-strictly
/>🎯 场景
- 页面权限 + 按钮权限
- 勾选按钮 ≠ 勾选页面
禁用某些节点(只读权限)
const treeProps = {
label: 'name',
children: 'children',
disabled: (data: any) => data.disabled === true,
}{
id: 1,
name: '系统管理',
disabled: true,
}📌 后端字段直透 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>- 只过滤显示,不会改数据
- 搜索 ≠ 勾选(要单独处理)
🎯 使用场景
- 左 Tree,右 Table
- 点击部门 → 查询用户
<el-tree
:data="deptTree"
node-key="id"
@node-click="handleSelect"
/>const handleSelect = (node: any) => {
searchForm.deptId = node.id
loadTable()
}📌 Tree 永远只做「条件选择器」
Tree 最大的坑:状态残留
正确做法(3 选 1)
✅ 方案一:destroy-on-close(你已经写了)
✅ 方案二:手动清空
treeRef.value?.setCheckedKeys([])✅ 方案三:key 强制刷新
<el-tree :key="treeKey" />treeKey.value++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>📌 参数说明
| 参数 | 说明 |
|---|---|
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>📌 核心配置
: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>- 不要开启
checkStrictly: true - 否则父节点也可选 ❌
🎯 使用场景
- 城市数据
- 大组织树
- 字典级联
✅ 完整示例(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>📌 真实体验
- 搜索的是 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>📌 接口对接要点
leaf: true必须返回resolve一定要调用- 常配合 Loading
🎯 使用场景
- 搜索表单
- 新增 / 编辑页
- 校验联动
✅ 完整示例(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>emitPath不关 → 后端接收数组 ❌- 编辑页需回显单值
- 校验写在
el-form-item
:props="{
emitPath: false,
lazy: true,
checkStrictly: false
}"| 场景 | 推荐 |
|---|---|
| 表单 | emitPath: false |
| 数据多 | lazy: true |
| 禁父选 | 默认即可 |