Element Plus 常用组件:前端的 UI SDK
1. 为什么选 Element Plus
后端视角:Element Plus 就是一个 UI SDK。类比 Java 的 Lombok / Hutool——你不需要自己造轮子(按钮、表单、表格、弹窗),直接 <el-button> 拉过来就能用。
主流选择(知道就行,项目统一用 Element Plus):
| 库 | 风格 | 社区 | 备注 |
|---|---|---|---|
| Element Plus | 桌面中后台 | 国内最大 | Vue 3 默认选择,文档中文友好 |
| Ant Design Vue | 桌面中后台 | 国内大 | 字节出品,设计语言更克制 |
| Naive UI | 桌面中后台 | 中等 | 尤雨溪点赞过,全 TS |
| Vuetify | Material Design | 国外大 | 移动端更吃香 |
in4vue 的选型:Element Plus + TailwindCSS 混用。Element Plus 负责复杂交互组件(Table、Form、DatePicker、MessageBox),TailwindCSS 负责布局和自定义外观。两者井水不犯河水。
2. 按需自动导入:已经配好,理解一下
手动 import 每个组件太烦,项目里用 unplugin-vue-components + unplugin-auto-import 做了编译时扫描:模板里写了 <el-button> 就自动帮你 import。
vite.config.ts 里的关键段:
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
plugins: [
AutoImport({
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
resolvers: [ElementPlusResolver()], // ElMessage 等 API 自动 import
}),
Components({
resolvers: [ElementPlusResolver()], // <el-xxx> 组件自动 import
}),
]
效果:
<template>
<el-button @click="ok">确定</el-button> <!-- 不用 import -->
</template>
<script setup lang="ts">
function ok() {
ElMessage.success('已保存') // 不用 import
}
</script>
Java 对照:Spring Boot Starter 的 @EnableAutoConfiguration——扫到依赖就帮你注册 Bean。Vite 插件是构建时干同样的事。
注意:
<el-xxx>模板组件 →unplugin-vue-components处理ElMessage、ElMessageBox、ElNotification等纯 JS API →unplugin-auto-import处理- 图标
@element-plus/icons-vue不会自动导入,要手动 import(见第 7 节)
按需导入的好处:打包体积小。全量导入 Element Plus ≈ 1MB+,按需后只打进去用到的组件。
3. 表单 + 校验:最常用的组合拳
登录、注册、搜索、新增、编辑——所有中后台页面都离不开表单。Element Plus 的表单是全库最值得摸透的部分。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
interface LoginForm {
email: string
password: string
}
const formRef = ref<FormInstance>()
const form = reactive<LoginForm>({
email: '',
password: '',
})
// 校验规则(类比 JSR-303 的 @NotBlank / @Email / @Length)
const rules: FormRules<LoginForm> = {
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不对', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度 6-20 位', trigger: 'blur' },
],
}
async function submit() {
if (!formRef.value) return
try {
await formRef.value.validate() // 校验不通过会 reject
// 通过了才真正提交
await userApi.login(form)
ElMessage.success('登录成功')
} catch {
// validate reject 时 Element Plus 已经把错误标红,不用额外处理
}
}
</script>
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
@submit.prevent
>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="a@b.com" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">登录</el-button>
<el-button @click="formRef?.resetFields()">重置</el-button>
</el-form-item>
</el-form>
</template>
要点:
:model绑定数据对象,:rules绑定校验规则,prop告诉el-form-item它管的是哪个字段——三个属性对齐才能校验formRef.value.validate()是异步的:成功 resolve,失败 reject,await最简洁resetFields()重置到初始值 + 清错误,比手动清字段爽得多@submit.prevent阻止原生表单刷新页面(前端常见坑)
Java 对照:
| Spring | Element Plus |
|---|---|
@NotBlank |
{ required: true } |
@Email |
{ type: 'email' } |
@Size(min=6, max=20) |
{ min: 6, max: 20 } |
自定义 ConstraintValidator |
{ validator: (rule, value, cb) => {...} } |
@Valid 抛 MethodArgumentNotValidException |
validate() reject 一个错误对象 |
自定义校验
比如"密码和确认密码必须一致":
const rules: FormRules = {
confirmPassword: [
{
validator(_rule, value, callback) {
if (value !== form.password) callback(new Error('两次密码不一致'))
else callback()
},
trigger: 'blur',
},
],
}
注意回调:callback() 无参=通过,callback(new Error(...)) =失败。这是 async-validator 库的老式 API,Element Plus 沿用了。
4. 表格 + 分页:中后台主力
<script setup lang="ts">
import { onMounted, ref } from 'vue'
interface Note {
id: number
title: string
category: string
createdAt: string
}
const list = ref<Note[]>([])
const loading = ref(false)
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
async function fetchList() {
loading.value = true
try {
const res = await noteApi.list({ page: page.value, pageSize: pageSize.value })
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
onMounted(fetchList)
function onPageChange(p: number) {
page.value = p
fetchList()
}
function remove(row: Note) {
ElMessageBox.confirm(`确定删除《${row.title}》?`, '提示', {
type: 'warning',
})
.then(async () => {
await noteApi.remove(row.id)
ElMessage.success('已删除')
fetchList()
})
.catch(() => {
// 用户点了取消,catch 会被 Element Plus 触发——静默忽略
})
}
</script>
<template>
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" show-overflow-tooltip />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="createdAt" label="创建时间" width="180" />
<!-- 自定义列:用 #default 插槽拿到行数据 -->
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="$router.push(`/notes/${row.id}`)">查看</el-button>
<el-button size="small" type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
class="mt-4"
@current-change="onPageChange"
@size-change="fetchList"
/>
</template>
要点:
v-loading是 Element Plus 的指令——绑true就显示 loading 遮罩。比手写<div v-if="loading">干净show-overflow-tooltip让长文本省略号 + 悬浮展开,标题列必备fixed="right"操作列固定在右侧,横滚不消失#default="{ row }"是作用域插槽,row就是当前行数据——在上一篇 Slot 笔记里讲过这种语法ElMessageBox.confirm返回 Promise,取消也走 catch,这点是 Element Plus 的约定
Java 对照:表格分页对应 Spring 的 PageRequest.of(page, size) + Page<T> 返回 { list, total }——前后端结构高度对齐。
多选 + 批量操作
<el-table :data="list" @selection-change="onSelect">
<el-table-column type="selection" width="40" />
...
</el-table>
<script setup lang="ts">
const selected = ref<Note[]>([])
function onSelect(rows: Note[]) { selected.value = rows }
async function batchRemove() {
if (!selected.value.length) return ElMessage.warning('请选择')
await noteApi.batchRemove(selected.value.map((n) => n.id))
fetchList()
}
</script>
5. 消息提示:Message / Notification / MessageBox
三者长得像,用法分得很清:
| API | 形态 | 典型场景 |
|---|---|---|
ElMessage |
顶部一闪而过的轻提示 | 操作结果反馈(成功/失败/警告) |
ElNotification |
右上角卡片,停留几秒 | 重要通知、后台任务完成 |
ElMessageBox |
居中模态对话框,必须响应 | 确认危险操作(删除) |
// 轻提示
ElMessage.success('保存成功')
ElMessage.error('网络异常')
// 重要通知
ElNotification({
title: '导出完成',
message: '点击下载',
type: 'success',
duration: 5_000,
onClick: () => download(),
})
// 必须响应的确认
await ElMessageBox.confirm('不可恢复,确定?', '警告', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
})
// 走到这里说明用户点了"删除";点取消会抛出,走外层 catch
Java 对照:这三个就是前端版的日志等级 + 交互确认。ElMessage ≈ 成功日志,ElNotification ≈ 带附件的邮件通知,ElMessageBox ≈ "确定继续吗 [y/N]"。
6. 弹窗 Dialog 和抽屉 Drawer
典型"点按钮弹出编辑表单":
<script setup lang="ts">
const visible = ref(false)
const editing = ref<Note | null>(null)
function openCreate() {
editing.value = { id: 0, title: '', category: '', content: '' }
visible.value = true
}
function openEdit(row: Note) {
editing.value = { ...row } // 复制一份,避免直接改表格里的数据
visible.value = true
}
async function save() {
if (!editing.value) return
if (editing.value.id) await noteApi.update(editing.value)
else await noteApi.create(editing.value)
visible.value = false
fetchList()
}
</script>
<template>
<el-button @click="openCreate">新增</el-button>
<el-dialog v-model="visible" :title="editing?.id ? '编辑' : '新增'" width="600px">
<el-form v-if="editing" :model="editing" label-width="80px">
<el-form-item label="标题">
<el-input v-model="editing.title" />
</el-form-item>
<el-form-item label="分类">
<el-input v-model="editing.category" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</template>
要点:
v-model双向绑可见性——之前讲过 v-model 就是语法糖,等价于:modelValue+@update:modelValue- 复制一份再编辑:
{ ...row }避免直接修改表格数据,取消时不会污染 #footer插槽覆盖默认"取消/确定"按钮
Drawer 和 Dialog 用法几乎一样,只是从侧面滑出,用于长表单(<el-drawer>)。
7. 图标:Element Plus Icons
Element Plus 自带的图标不在自动导入范围内,需要显式 import:
<script setup lang="ts">
import { Search, Edit, Delete } from '@element-plus/icons-vue'
</script>
<template>
<el-button :icon="Search">搜索</el-button>
<el-icon><Edit /></el-icon>
<el-icon size="20" color="red"><Delete /></el-icon>
</template>
全量登记图标(后台管理常用):
// src/main.ts
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) // 全局注册
}
这样模板里直接 <el-icon><Search /></el-icon> 不用 import。
体积代价:全部 +几十 KB。项目只用十来个图标时,手动 import 更好。
8. 和 TailwindCSS 混用的原则
Element Plus 组件自带样式,TailwindCSS 的工具类也可以加到组件外层——但不要去覆盖组件内部样式。
推荐:
<!-- 外部布局用 Tailwind -->
<div class="flex items-center gap-4 p-4">
<el-input v-model="keyword" placeholder="搜索" class="w-64" />
<el-button type="primary">搜索</el-button>
</div>
class="w-64" 是 Tailwind 给组件最外层加宽度,不干预内部 DOM,没问题。
不推荐:
<!-- 用 Tailwind 改 Element Plus 内部按钮颜色 -->
<el-button class="!bg-red-500 !text-white">删除</el-button>
Element Plus 下个版本可能改 class 结构,这种 !important hack 就崩了。要定制主题,用第 9 节的 CSS 变量。
9. 主题定制:CSS 变量覆盖
Element Plus 2.x 基于 CSS 变量,改个主色不用写 SCSS 变量了:
/* src/style.css 或全局样式 */
:root {
--el-color-primary: #10b981; /* 改成绿色系 */
--el-color-primary-light-3: #34d399;
--el-border-radius-base: 8px;
}
/* 暗色模式下可以换一套 */
html.dark {
--el-bg-color: #1f2937;
--el-text-color-primary: #e5e7eb;
}
Java 对照:像改 Spring 的 application.yml 里的 port,一改全应用生效。
暗色模式切换只要在 <html> 上加 class="dark"——上一篇 Pinia 笔记里的 theme store 就是干这个的:
// themeStore.toggle() 里
document.documentElement.classList.toggle('dark', theme.value === 'dark')
10. 常见坑
坑 1:模板组件和 JS API 是两套自动导入
<el-message> 没有这个组件,它是纯 JS API ElMessage()。Element Plus 所有带"ElXxxBox / ElXxx API"字样的都不是模板组件。
坑 2:Table 高度不固定时的滚动
Table 在 Flex 容器里常常高度塌陷或无限拉长。两种思路:
- 给父容器固定高度:
<div class="h-[600px]">包住 Table - 给 Table 传
max-height:<el-table :max-height="600">
固定表头 + 内部滚动需要 max-height 或 height 二选一设死。
坑 3:Form 的 prop 要和 model 字段精确对齐
<el-form :model="form">
<el-form-item prop="user.email"> <!-- 嵌套字段要写完整路径 -->
<el-input v-model="form.user.email" />
</el-form-item>
</el-form>
prop 写错校验就沉默失效。规则名和 prop 也要对上。
坑 4:el-select 的 value 类型要和 options 的 value 类型一致
<el-select v-model="selected"> <!-- selected: number -->
<el-option :value="'1'" label="A" /> <!-- ❌ '1' 是字符串 -->
</el-select>
绑不上,查半天。后端返回 id 是字符串,前端状态字段类型要对齐。
坑 5:按需导入模式下用 MessageBox 不弹出样式
偶尔遇到:弹窗出来了但没样式。一般是 resolver 没装或 vite 缓存坏。清 node_modules/.vite 然后重启 dev server 能解。
11. 在 in4vue 里会用到的组件清单
按学习路线推进顺序:
- 第三阶段(现在):ElButton / ElIcon / ElInput / ElMessage——搭笔记列表和导航
- 第四阶段:ElMenu / ElDrawer / ElDropdown——侧边栏和分类导航
- 第五阶段:ElForm / ElTable / ElPagination / ElDialog——AI 问答历史管理、后续 CRUD
- 随时需要:ElMessageBox(确认操作)、ElTooltip(悬浮提示)、ElEmpty(空状态)
不需要一下子全学。用到什么查什么,Element Plus 文档示例质量非常高,粘过来改改就能跑。
速查表
<!-- 按钮 -->
<el-button type="primary|success|warning|danger" size="small|default|large" :icon="Search">
<!-- 表单 -->
<el-form ref="formRef" :model :rules label-width="80px">
<el-form-item label prop><el-input v-model /></el-form-item>
</el-form>
await formRef.value.validate()
<!-- 表格 -->
<el-table :data :loading>
<el-table-column prop label width fixed show-overflow-tooltip />
<el-table-column label="操作">
<template #default="{ row }">...</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination v-model:current-page v-model:page-size :total layout="..." />
<!-- 弹窗 -->
<el-dialog v-model="visible" title width>
<template #footer>...</template>
</el-dialog>
<!-- 消息 -->
ElMessage.success('...')
await ElMessageBox.confirm('...', '标题', { type: 'warning' })
小练习
- 在
HomePage.vue给笔记列表加一个<el-input placeholder="搜索笔记">(暂时不接逻辑) - 用
ElMessage.info('示例')试试消息提示能不能弹 - 在某个测试页放一个
<el-form>表单,字段有 email / password,加必填和格式校验 - 把项目主色通过 CSS 变量改成绿色(
--el-color-primary),看 ElButton 跟着变
做完这 4 步,你就能用 Element Plus 拼出 90% 的中后台页面骨架。
延伸阅读
- Element Plus 官方文档(中文)
- 组件总览
- async-validator 规则文档(Form 规则背后的库)
- Element Plus 主题
- unplugin-vue-components(按需导入原理)