19-Vue3 与 Element Plus / Ant Design Vue
Vue3 与 Element Plus / Ant Design Vue
全面掌握 Vue3 主流 UI 组件库的使用、定制与选型,提升企业级项目开发效率。
一、前言
在企业级 Vue3 项目开发中,从头手写每一个 UI 组件既不现实也不经济。成熟的 UI 组件库能够帮助团队快速构建美观、一致、功能完善的用户界面,将精力聚焦于业务逻辑本身。
Vue3 生态经过多年发展,已经涌现出一批优秀的 UI 组件库。本文将重点介绍国内最流行的 Element Plus 和 Ant Design Vue,涵盖安装配置、主题定制、常用组件使用以及组件库选型策略。
二、Vue3 UI 组件库生态概览
2.1 主流组件库一览
| 组件库 | 出品方 | 设计风格 | 组件数量 | 特点 |
|---|---|---|---|---|
| Element Plus | 饿了么前端 | 简约商务 | 70+ | Vue3 官方推荐,文档完善 |
| Ant Design Vue | 蚂蚁集团 | 企业级中后台 | 70+ | 设计规范严格,功能丰富 |
| Naive UI | TuSimple | 现代清新 | 80+ | TypeScript 友好,性能优秀 |
| Vuetify 3 | Vuetify 团队 | Material Design | 80+ | 国际化程度高 |
| TDesign Vue Next | 腾讯 | 企业级设计 | 60+ | 腾讯内部沉淀 |
| Arco Design Vue | 字节跳动 | 现代简约 | 70+ | 字节内部使用 |
三、Element Plus 详解
3.1 Element Plus 简介
Element Plus 是饿了么前端团队开源的 Vue3 组件库,是 Element UI 的 Vue3 升级版。它继承了 Element UI 的简洁设计风格,同时充分利用了 Vue3 的新特性:
- 基于 Vue3 Composition API 重构
- 支持 TypeScript,提供完整的类型定义
- 使用 CSS 变量实现主题定制
- 支持暗黑模式
- 提供国际化支持(60+ 语言)
3.2 安装与基础配置
# 安装 Element Plus
npm install element-plus
# 安装图标库
npm install @element-plus/icons-vue
完整引入(适合快速原型开发):
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
按需导入(推荐用于生产项目):
# 安装自动导入插件
npm install -D unplugin-vue-components unplugin-auto-import
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue']
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
})
使用按需导入后,组件和 API 会自动引入,无需手动 import:
<template>
<div>
<!-- 直接使用组件,无需导入 -->
<el-button type="primary" @click="handleClick">
点击我
</el-button>
<el-input v-model="inputValue" placeholder="请输入" />
<el-dialog v-model="visible" title="提示">
<p>这是一段内容</p>
</el-dialog>
</div>
</template>
<script setup>
// ref 等 API 自动导入
const inputValue = ref('')
const visible = ref(false)
const handleClick = () => {
ElMessage.success('操作成功')
visible.value = true
}
</script>
3.3 主题定制
Element Plus 提供了多种主题定制方式:
方式一:CSS 变量覆盖(推荐)
/* styles/element-variables.css */
:root {
/* 主色调 */
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
/* 圆角 */
--el-border-radius-base: 8px;
/* 字体 */
--el-font-size-base: 14px;
}
/* 暗黑模式变量 */
html.dark {
--el-color-primary: #409eff;
--el-bg-color: #141414;
--el-text-color-primary: #e0e0e0;
}
方式二:SCSS 变量覆盖
npm install -D sass
// styles/element-plus.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #409eff,
),
'success': (
'base': #67c23a,
),
),
$border-radius: (
'base': 8px,
'small': 4px,
)
);
// 引入全部样式
@use 'element-plus/theme-chalk/src/index.scss' as *;
方式三:在线主题编辑器
Element Plus 官方提供了在线主题编辑器(https://element-plus.org/zh-CN/theme-editor.html),可以可视化调整主题色、圆角、阴影等参数,一键导出配置文件。
3.4 常用组件示例
Table 表格组件
<template>
<el-table
:data="tableData"
stripe
border
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="date" label="日期" width="180" />
<el-table-column prop="name" label="姓名" width="180" />
<el-table-column prop="address" label="地址" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<script setup>
const tableData = ref([
{ date: '2024-01-01', name: '张三', address: '北京市朝阳区' },
{ date: '2024-01-02', name: '李四', address: '上海市浦东新区' },
])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(100)
const handleSelectionChange = (selection) => {
console.log('选中项:', selection)
}
const handleEdit = (row) => {
ElMessage.info(`编辑 ${row.name}`)
}
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除 ${row.name}?`, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessage.success('删除成功')
})
}
</script>
Form 表单组件
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
status-icon
>
<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="gender">
<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="爱好" prop="hobbies">
<el-checkbox-group v-model="form.hobbies">
<el-checkbox label="reading">阅读</el-checkbox>
<el-checkbox label="sports">运动</el-checkbox>
<el-checkbox label="music">音乐</el-checkbox>
</el-checkbox-group>
</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>
</template>
<script setup>
const formRef = ref()
const form = reactive({
username: '',
email: '',
gender: '',
hobbies: []
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
]
}
const submitForm = async () => {
try {
await formRef.value.validate()
ElMessage.success('表单验证通过')
// 提交数据...
} catch (error) {
ElMessage.error('请检查表单填写')
}
}
const resetForm = () => {
formRef.value.resetFields()
}
</script>
Dialog 与 Message 组件
<template>
<div>
<el-button type="primary" @click="dialogVisible = true">
打开对话框
</el-button>
<el-dialog
v-model="dialogVisible"
title="用户信息"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<p>对话框内容...</p>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirm">确认</el-button>
</span>
</template>
</el-dialog>
<el-button @click="showMessage">显示消息</el-button>
<el-button @click="showNotification">显示通知</el-button>
</div>
</template>
<script setup>
const dialogVisible = ref(false)
const confirm = () => {
ElMessage.success('操作成功')
dialogVisible.value = false
}
const showMessage = () => {
ElMessage({
message: '这是一条消息',
type: 'success',
duration: 3000,
showClose: true
})
}
const showNotification = () => {
ElNotification({
title: '通知标题',
message: '这是一条通知消息',
type: 'info',
position: 'top-right'
})
}
</script>
四、Ant Design Vue 详解
4.1 Ant Design Vue 简介
Ant Design Vue 是蚂蚁集团 Ant Design 的 Vue3 实现版本,继承了 Ant Design 的设计语言和组件规范:
- 企业级中后台产品设计风格
- 提供 Vue3 和 Vue2 两个版本
- 完善的 TypeScript 支持
- 丰富的组件和配套工具(ProComponents)
- 强大的表单和数据处理组件
4.2 安装与配置
# 安装 Ant Design Vue
npm install ant-design-vue
# 安装图标库
npm install @ant-design/icons-vue
完整引入:
// main.js
import { createApp } from 'vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import App from './App.vue'
const app = createApp(App)
app.use(Antd)
app.mount('#app')
按需导入(推荐):
npm install -D unplugin-vue-components unplugin-auto-import
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false // 不自动引入样式,手动引入
})
]
})
]
})
// main.js 中引入样式
import 'ant-design-vue/dist/reset.css'
4.3 常用组件示例
<template>
<div class="antd-demo">
<!-- 按钮 -->
<a-space>
<a-button type="primary">主按钮</a-button>
<a-button>默认按钮</a-button>
<a-button type="dashed">虚线按钮</a-button>
<a-button type="link">链接按钮</a-button>
</a-space>
<!-- 表格 -->
<a-table
:columns="columns"
:data-source="dataSource"
:row-selection="rowSelection"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="edit(record)">编辑</a-button>
<a-button type="link" danger @click="remove(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
<!-- 表单 -->
<a-form
:model="formState"
:rules="rules"
@finish="handleFinish"
>
<a-form-item label="用户名" name="username">
<a-input v-model:value="formState.username" />
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password v-model:value="formState.password" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">提交</a-button>
</a-form-item>
</a-form>
<!-- 消息提示 -->
<a-button @click="showMessage">显示消息</a-button>
</div>
</template>
<script setup>
const columns = [
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '年龄', dataIndex: 'age', key: 'age' },
{ title: '地址', dataIndex: 'address', key: 'address' },
{ title: '操作', key: 'action' }
]
const dataSource = ref([
{ key: '1', name: '张三', age: 32, address: '北京' },
{ key: '2', name: '李四', age: 28, address: '上海' },
])
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
console.log('选中:', selectedRowKeys, selectedRows)
}
}
const formState = reactive({
username: '',
password: ''
})
const rules = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }]
}
const handleTableChange = (pagination, filters, sorter) => {
console.log('表格变化:', pagination, filters, sorter)
}
const edit = (record) => {
message.info(`编辑 ${record.name}`)
}
const remove = (record) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除 ${record.name} 吗?`,
onOk() {
message.success('删除成功')
}
})
}
const handleFinish = (values) => {
message.success('提交成功')
console.log(values)
}
const showMessage = () => {
message.success('操作成功', 3) // 3秒后自动关闭
}
</script>
五、组件库选型对比
5.1 详细对比表
| 对比维度 | Element Plus | Ant Design Vue | Naive UI | Vuetify 3 |
|---|---|---|---|---|
| 设计风格 | 简约商务 | 企业级严谨 | 现代清新 | Material Design |
| 组件数量 | 70+ | 70+ | 80+ | 80+ |
| TypeScript | 优秀 | 优秀 | 极佳 | 良好 |
| 按需加载 | 支持 | 支持 | 自动按需 | 支持 |
| 主题定制 | CSS 变量/SCSS | Less 变量 | 内置主题系统 | SCSS |
| 暗黑模式 | 原生支持 | 原生支持 | 原生支持 | 原生支持 |
| 国际化 | 60+ 语言 | 多种语言 | 多种语言 | 多种语言 |
| 文档质量 | 优秀 | 优秀 | 优秀 | 良好 |
| 社区活跃度 | 极高 | 高 | 中高 | 高 |
| 企业背景 | 饿了么/美团 | 蚂蚁集团 | TuSimple | 社区驱动 |
| 适合场景 | 中后台/通用 | 企业级中后台 | 现代化应用 | 国际化产品 |
| 包体积(gzip) | ~200KB | ~250KB | ~150KB | ~300KB |
5.2 选型建议
选型决策要点:
- 团队熟悉度优先:如果团队已有 Element UI 经验,Element Plus 是平滑过渡的最佳选择
- 设计规范要求:需要严格遵循设计规范的大型企业项目,Ant Design Vue 更合适
- TypeScript 重度用户:Naive UI 的 TypeScript 体验最佳
- 国际化产品:Vuetify 的 Material Design 在国际市场接受度更高
- 包体积敏感:Naive UI 的按需加载机制最完善,总体积最小
六、二次封装组件
在实际项目中,直接使用组件库往往不能满足所有业务需求。基于组件库进行二次封装是常见做法。
6.1 封装通用表格组件
<!-- components/ProTable.vue -->
<template>
<div class="pro-table">
<!-- 搜索区域 -->
<div v-if="showSearch" class="pro-table-search">
<slot name="search" />
</div>
<!-- 工具栏 -->
<div class="pro-table-toolbar">
<slot name="toolbar" />
</div>
<!-- 表格 -->
<el-table
v-bind="$attrs"
:data="data"
v-loading="loading"
@selection-change="handleSelectionChange"
>
<slot />
</el-table>
<!-- 分页 -->
<el-pagination
v-if="showPagination"
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="pageSizes"
layout="total, sizes, prev, pager, next"
@size-change="handlePaginationChange"
@current-change="handlePaginationChange"
/>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
total: { type: Number, default: 0 },
showSearch: { type: Boolean, default: true },
showPagination: { type: Boolean, default: true },
pageSizes: { type: Array, default: () => [10, 20, 50, 100] }
})
const emit = defineEmits(['update:page', 'update:pageSize', 'pagination-change', 'selection-change'])
const page = defineModel('page', { type: Number, default: 1 })
const pageSize = defineModel('pageSize', { type: Number, default: 10 })
const handlePaginationChange = () => {
emit('pagination-change', { page: page.value, pageSize: pageSize.value })
}
const handleSelectionChange = (selection) => {
emit('selection-change', selection)
}
</script>
6.2 封装通用表单组件
<!-- components/ProForm.vue -->
<template>
<el-form
ref="formRef"
:model="model"
:rules="rules"
v-bind="formProps"
>
<el-form-item
v-for="item in schema"
:key="item.prop"
:label="item.label"
:prop="item.prop"
>
<!-- 输入框 -->
<el-input
v-if="item.type === 'input'"
v-model="model[item.prop]"
v-bind="item.attrs"
/>
<!-- 选择器 -->
<el-select
v-else-if="item.type === 'select'"
v-model="model[item.prop]"
v-bind="item.attrs"
>
<el-option
v-for="opt in item.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- 日期选择 -->
<el-date-picker
v-else-if="item.type === 'date'"
v-model="model[item.prop]"
v-bind="item.attrs"
/>
<!-- 自定义插槽 -->
<slot v-else :name="item.prop" :item="item" />
</el-form-item>
<!-- 操作按钮 -->
<el-form-item v-if="showActions">
<el-button type="primary" @click="submit">{{ submitText }}</el-button>
<el-button @click="reset">{{ resetText }}</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
const props = defineProps({
model: { type: Object, required: true },
schema: { type: Array, required: true },
rules: { type: Object, default: () => ({}) },
formProps: { type: Object, default: () => ({}) },
showActions: { type: Boolean, default: true },
submitText: { type: String, default: '提交' },
resetText: { type: String, default: '重置' }
})
const emit = defineEmits(['submit', 'reset'])
const formRef = ref()
const submit = async () => {
try {
await formRef.value.validate()
emit('submit', props.model)
} catch (error) {
console.error('表单验证失败', error)
}
}
const reset = () => {
formRef.value.resetFields()
emit('reset')
}
// 暴露方法供父组件调用
defineExpose({
validate: () => formRef.value.validate(),
resetFields: () => formRef.value.resetFields(),
clearValidate: () => formRef.value.clearValidate()
})
</script>
6.3 使用封装组件
<template>
<ProForm
v-model="formData"
:schema="formSchema"
:rules="formRules"
@submit="handleSubmit"
>
<template #customField="{ item }">
<!-- 自定义字段渲染 -->
<MyCustomComponent v-model="formData.customField" />
</template>
</ProForm>
</template>
<script setup>
const formData = reactive({
name: '',
department: '',
date: '',
customField: ''
})
const formSchema = [
{ prop: 'name', label: '姓名', type: 'input', attrs: { placeholder: '请输入姓名' } },
{
prop: 'department',
label: '部门',
type: 'select',
attrs: { placeholder: '请选择部门' },
options: [
{ label: '技术部', value: 'tech' },
{ label: '产品部', value: 'product' }
]
},
{ prop: 'date', label: '日期', type: 'date', attrs: { type: 'date' } },
{ prop: 'customField', label: '自定义字段', type: 'custom' }
]
const formRules = {
name: [{ required: true, message: '请输入姓名' }],
department: [{ required: true, message: '请选择部门' }]
}
const handleSubmit = (data) => {
console.log('提交数据:', data)
}
</script>
七、组件库源码学习建议
深入学习优秀组件库的源码,是提升 Vue3 技术能力的有效途径。
7.1 学习路径
7.2 推荐阅读重点
| 组件库 | 推荐学习重点 |
|---|---|
| Element Plus | 主题系统(CSS 变量)、表单验证集成、消息提示服务 |
| Ant Design Vue | 表格组件(虚拟滚动)、表单组件(动态渲染)、ConfigProvider |
| Naive UI | 组件类型设计、CSS-in-JS 替代方案、性能优化 |
7.3 调试技巧
# 克隆 Element Plus 源码
git clone https://github.com/element-plus/element-plus.git
cd element-plus
# 安装依赖并启动文档站点
pnpm install
pnpm docs:dev
# 在浏览器中调试组件源码
# 文档站点会加载未压缩的源码,方便断点调试
八、常见问题
Q1:Element Plus 按需导入后样式丢失怎么办?
确保 unplugin-vue-components 配置正确,或在 main.js 中手动引入样式:
import 'element-plus/dist/index.css'
// 或按需引入特定组件样式
import 'element-plus/es/components/button/style/css'
Q2:Ant Design Vue 的表单验证不生效?
检查 name 属性是否与 v-model 绑定的字段一致,且 rules 配置正确:
<a-form-item label="用户名" name="username" :rules="[{ required: true }]">
<a-input v-model:value="formState.username" />
</a-form-item>
Q3:组件库样式与项目样式冲突?
使用 CSS 前缀或命名空间隔离:
// 为 Element Plus 添加命名空间
.el-button {
// 自定义样式
}
// 或使用深度选择器
:deep(.el-table) {
// 自定义样式
}
Q4:如何同时引入 Element Plus 和 Ant Design Vue?
技术上可行但不推荐,会导致样式冲突和包体积膨胀。建议选定一个主组件库,另一个的特殊组件通过别名引入:
// vite.config.js
resolve: {
alias: {
'antd-vue': 'ant-design-vue'
}
}
九、总结
本文全面介绍了 Vue3 主流 UI 组件库的使用方法:
- Element Plus:适合追求简洁、快速开发的项目,配置简单,文档完善
- Ant Design Vue:适合企业级中后台,设计规范严格,生态丰富
- 按需加载:使用
unplugin-vue-components实现自动按需引入,优化包体积 - 主题定制:掌握 CSS 变量、SCSS 覆盖和在线编辑器等定制方式
- 二次封装:基于组件库封装业务组件,提升开发效率和一致性
- 选型策略:根据项目类型、团队背景和技术需求综合决策
组件库是提升开发效率的利器,但不应成为技术能力的上限。在熟练使用的基础上,深入阅读源码、理解设计思想,才能真正驾驭这些工具。
十、思考题
-
为本文中的
ProTable组件添加列配置功能,支持通过配置数组动态渲染表格列。 -
基于 Element Plus 封装一个
ProSelect组件,支持远程搜索、分页加载和多选功能。 -
对比 Element Plus 和 Ant Design Vue 的表单组件设计差异,分析各自的优缺点。
-
尝试阅读 Element Plus 的
ElMessage源码,理解其服务式调用的实现原理。
更多推荐


所有评论(0)