Vue3 与 Element Plus / Ant Design Vue

全面掌握 Vue3 主流 UI 组件库的使用、定制与选型,提升企业级项目开发效率。

一、前言

在企业级 Vue3 项目开发中,从头手写每一个 UI 组件既不现实也不经济。成熟的 UI 组件库能够帮助团队快速构建美观、一致、功能完善的用户界面,将精力聚焦于业务逻辑本身。

Vue3 生态经过多年发展,已经涌现出一批优秀的 UI 组件库。本文将重点介绍国内最流行的 Element PlusAnt Design Vue,涵盖安装配置、主题定制、常用组件使用以及组件库选型策略。

二、Vue3 UI 组件库生态概览

2.1 主流组件库一览

Vue3 UI 组件库生态

国内主流

国际主流

特色/新兴

Element Plus

Ant Design Vue

Naive UI

Vuetify 3

Quasar

PrimeVue

Varlet

TDesign Vue Next

Arco Design Vue

组件库 出品方 设计风格 组件数量 特点
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 选型建议

企业级中后台

阿里系/习惯Ant Design

其他/追求简洁

现代化/创新项目

国际化/Material Design

开始选型

项目类型

团队背景

Ant Design Vue

Element Plus

Naive UI

Vuetify 3

确认选择

选型决策要点:

  1. 团队熟悉度优先:如果团队已有 Element UI 经验,Element Plus 是平滑过渡的最佳选择
  2. 设计规范要求:需要严格遵循设计规范的大型企业项目,Ant Design Vue 更合适
  3. TypeScript 重度用户:Naive UI 的 TypeScript 体验最佳
  4. 国际化产品:Vuetify 的 Material Design 在国际市场接受度更高
  5. 包体积敏感: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 学习路径

阅读组件库源码

组件设计模式

TypeScript 实践

工程化方案

性能优化技巧

配置化组件设计

插槽与渲染函数

组合式函数封装

类型推导与泛型

Props 类型定义

按需加载实现

主题系统架构

虚拟列表

懒加载与按需渲染

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 组件库的使用方法:

  1. Element Plus:适合追求简洁、快速开发的项目,配置简单,文档完善
  2. Ant Design Vue:适合企业级中后台,设计规范严格,生态丰富
  3. 按需加载:使用 unplugin-vue-components 实现自动按需引入,优化包体积
  4. 主题定制:掌握 CSS 变量、SCSS 覆盖和在线编辑器等定制方式
  5. 二次封装:基于组件库封装业务组件,提升开发效率和一致性
  6. 选型策略:根据项目类型、团队背景和技术需求综合决策

组件库是提升开发效率的利器,但不应成为技术能力的上限。在熟练使用的基础上,深入阅读源码、理解设计思想,才能真正驾驭这些工具。

十、思考题

  1. 为本文中的 ProTable 组件添加列配置功能,支持通过配置数组动态渲染表格列。

  2. 基于 Element Plus 封装一个 ProSelect 组件,支持远程搜索、分页加载和多选功能。

  3. 对比 Element Plus 和 Ant Design Vue 的表单组件设计差异,分析各自的优缺点。

  4. 尝试阅读 Element Plus 的 ElMessage 源码,理解其服务式调用的实现原理。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐