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 插件是构建时干同样的事。

注意

按需导入的好处:打包体积小。全量导入 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>

要点

Java 对照

Spring Element Plus
@NotBlank { required: true }
@Email { type: 'email' }
@Size(min=6, max=20) { min: 6, max: 20 }
自定义 ConstraintValidator { validator: (rule, value, cb) => {...} }
@ValidMethodArgumentNotValidException 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>

要点

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>

要点

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 容器里常常高度塌陷或无限拉长。两种思路:

固定表头 + 内部滚动需要 max-heightheight 二选一设死。

坑 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 里会用到的组件清单

按学习路线推进顺序:

不需要一下子全学。用到什么查什么,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' })

小练习

  1. HomePage.vue 给笔记列表加一个 <el-input placeholder="搜索笔记">(暂时不接逻辑)
  2. ElMessage.info('示例') 试试消息提示能不能弹
  3. 在某个测试页放一个 <el-form> 表单,字段有 email / password,加必填和格式校验
  4. 把项目主色通过 CSS 变量改成绿色(--el-color-primary),看 ElButton 跟着变

做完这 4 步,你就能用 Element Plus 拼出 90% 的中后台页面骨架。


延伸阅读