Vue UI 组件库
2026/3/20大约 10 分钟
Vue UI 组件库实战指南:Element、Ant Design 与 Naive UI
选择合适的 UI 组件库能极大提升开发效率。Vue 生态有多个优秀的组件库,本文将介绍最流行的几个,帮你做出正确的选择。
主流组件库对比
| 组件库 | Vue 版本 | 设计风格 | TypeScript | 按需引入 | 适用场景 |
|---|---|---|---|---|---|
| Element UI | Vue2 | 简洁大气 | 部分支持 | 支持 | 中后台系统 |
| Element Plus | Vue3 | 简洁大气 | 完全支持 | 支持 | 中后台系统 |
| Ant Design Vue | Vue2/Vue3 | 企业级 | 完全支持 | 支持 | 企业应用 |
| Naive UI | Vue3 | 清新现代 | 完全支持 | 原生支持 | 各类应用 |
| Vuetify | Vue2/Vue3 | Material | 完全支持 | 支持 | Material 风格 |
| Vant | Vue2/Vue3 | 移动端 | 完全支持 | 支持 | 移动端应用 |
Element Plus(Vue3 推荐)
安装与配置
npm install element-plus
// 完整引入
// main.js
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import App from "./App.vue";
const app = createApp(App);
app.use(ElementPlus, { locale: zhCn });
app.mount("#app");
// 按需引入(推荐)
// vite.config.js
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
});
常用组件示例
<template>
<div class="page">
<!-- 布局 -->
<el-container>
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
<!-- 表单 -->
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</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 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="gaming">游戏</el-checkbox>
<el-checkbox label="sports">运动</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="日期" prop="date">
<el-date-picker
v-model="form.date"
type="date"
placeholder="选择日期"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table :data="tableData" stripe border>
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" sortable />
<el-table-column prop="address" label="地址" show-overflow-tooltip />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-popconfirm title="确定删除吗?" @confirm="handleDelete(row)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
<!-- 弹窗 -->
<el-dialog v-model="dialogVisible" title="编辑用户" width="500px">
<el-form :model="editForm">
<el-form-item label="姓名">
<el-input v-model="editForm.name" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
<!-- 消息提示 -->
<el-button @click="showMessage">显示消息</el-button>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
const formRef = ref();
const dialogVisible = ref(false);
const form = reactive({
username: "",
password: "",
gender: "",
hobbies: [],
date: "",
});
const rules = {
username: [
{ required: true, message: "请输入用户名", trigger: "blur" },
{ min: 2, max: 20, message: "长度在 2 到 20 个字符", trigger: "blur" },
],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
};
const tableData = ref([
{ name: "张三", age: 25, address: "北京市朝阳区" },
{ name: "李四", age: 30, address: "上海市浦东新区" },
]);
const pagination = reactive({
page: 1,
pageSize: 10,
total: 100,
});
const editForm = reactive({ name: "" });
async function handleSubmit() {
try {
await formRef.value.validate();
ElMessage.success("提交成功");
} catch {
ElMessage.error("请检查表单");
}
}
function handleReset() {
formRef.value.resetFields();
}
function handleEdit(row) {
editForm.name = row.name;
dialogVisible.value = true;
}
async function handleDelete(row) {
ElMessage.success(`删除 ${row.name}`);
}
function handleSave() {
dialogVisible.value = false;
ElMessage.success("保存成功");
}
function handlePageChange(page) {
console.log("页码:", page);
}
function handleSizeChange(size) {
console.log("每页条数:", size);
}
function showMessage() {
ElMessageBox.confirm("确定执行此操作?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
ElMessage.success("操作成功");
})
.catch(() => {
ElMessage.info("已取消");
});
}
</script>
主题定制
// styles/element-variables.scss
@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: (
"primary": (
"base": #409eff,
),
"success": (
"base": #67c23a,
),
"warning": (
"base": #e6a23c,
),
"danger": (
"base": #f56c6c,
),
"info": (
"base": #909399,
),
)
);
// vite.config.js
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/element-variables.scss" as *;`;
}
}
}
Ant Design Vue
安装与配置
# Vue3
npm install ant-design-vue@4
# Vue2
npm install ant-design-vue@1
// 完整引入
import { createApp } from "vue";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/reset.css";
import App from "./App.vue";
createApp(App).use(Antd).mount("#app");
// 按需引入
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
Components({
resolvers: [AntDesignVueResolver({ importStyle: false })],
}),
],
});
常用组件示例
<template>
<div class="page">
<!-- 布局 -->
<a-layout>
<a-layout-header>Header</a-layout-header>
<a-layout>
<a-layout-sider>Sider</a-layout-sider>
<a-layout-content>Content</a-layout-content>
</a-layout>
</a-layout>
<!-- 表单 -->
<a-form
:model="formState"
:rules="rules"
ref="formRef"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<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 label="性别" name="gender">
<a-radio-group v-model:value="formState.gender">
<a-radio value="male">男</a-radio>
<a-radio value="female">女</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="日期" name="date">
<a-date-picker v-model:value="formState.date" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 4 }">
<a-button type="primary" @click="onSubmit">提交</a-button>
<a-button style="margin-left: 10px" @click="onReset">重置</a-button>
</a-form-item>
</a-form>
<!-- 表格 -->
<a-table
:columns="columns"
:data-source="dataSource"
:pagination="tablePagination"
:loading="loading"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a @click="handleEdit(record)">编辑</a>
<a-popconfirm title="确定删除吗?" @confirm="handleDelete(record)">
<a>删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 弹窗 -->
<a-modal v-model:open="modalVisible" title="编辑" @ok="handleOk">
<a-form :model="editForm">
<a-form-item label="姓名">
<a-input v-model:value="editForm.name" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { message, Modal } from "ant-design-vue";
const formRef = ref();
const modalVisible = ref(false);
const loading = ref(false);
const formState = reactive({
username: "",
password: "",
gender: "",
date: null,
});
const rules = {
username: [{ required: true, message: "请输入用户名" }],
password: [{ required: true, message: "请输入密码" }],
};
const columns = [
{ title: "姓名", dataIndex: "name", key: "name" },
{ title: "年龄", dataIndex: "age", key: "age", sorter: true },
{ title: "地址", dataIndex: "address", key: "address", ellipsis: true },
{ title: "操作", key: "action" },
];
const dataSource = ref([
{ key: "1", name: "张三", age: 25, address: "北京市" },
{ key: "2", name: "李四", age: 30, address: "上海市" },
]);
const tablePagination = reactive({
current: 1,
pageSize: 10,
total: 100,
showSizeChanger: true,
showQuickJumper: true,
});
const editForm = reactive({ name: "" });
async function onSubmit() {
try {
await formRef.value.validate();
message.success("提交成功");
} catch {
message.error("请检查表单");
}
}
function onReset() {
formRef.value.resetFields();
}
function handleTableChange(pagination, filters, sorter) {
console.log("分页:", pagination);
console.log("排序:", sorter);
}
function handleEdit(record) {
editForm.name = record.name;
modalVisible.value = true;
}
function handleDelete(record) {
message.success(`删除 ${record.name}`);
}
function handleOk() {
modalVisible.value = false;
message.success("保存成功");
}
</script>
主题定制
// vite.config.js
import { theme } from 'ant-design-vue'
// 使用 ConfigProvider
<a-config-provider
:theme="{
token: {
colorPrimary: '#1890ff',
borderRadius: 4
}
}"
>
<App />
</a-config-provider>
Naive UI(Vue3)
Naive UI 是一个比较新但非常优秀的 Vue3 组件库,全面使用 TypeScript 编写,原生支持按需引入。
安装与配置
npm install naive-ui
// 按需引入(原生支持,无需插件)
// 直接在组件中导入使用
// 如需自动导入
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
AutoImport({
imports: [
{
"naive-ui": [
"useDialog",
"useMessage",
"useNotification",
"useLoadingBar",
],
},
],
}),
Components({
resolvers: [NaiveUiResolver()],
}),
],
});
常用组件示例
<template>
<n-config-provider :theme="darkTheme" :locale="zhCN" :date-locale="dateZhCN">
<n-message-provider>
<n-dialog-provider>
<div class="page">
<!-- 布局 -->
<n-layout has-sider>
<n-layout-sider>Sider</n-layout-sider>
<n-layout>
<n-layout-header>Header</n-layout-header>
<n-layout-content>Content</n-layout-content>
</n-layout>
</n-layout>
<!-- 表单 -->
<n-form ref="formRef" :model="formValue" :rules="rules">
<n-form-item label="用户名" path="username">
<n-input v-model:value="formValue.username" />
</n-form-item>
<n-form-item label="密码" path="password">
<n-input
v-model:value="formValue.password"
type="password"
show-password-on="click"
/>
</n-form-item>
<n-form-item label="性别" path="gender">
<n-radio-group v-model:value="formValue.gender">
<n-radio value="male">男</n-radio>
<n-radio value="female">女</n-radio>
</n-radio-group>
</n-form-item>
<n-form-item label="日期" path="date">
<n-date-picker v-model:value="formValue.date" type="date" />
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSubmit">提交</n-button>
<n-button style="margin-left: 10px" @click="handleReset">
重置
</n-button>
</n-form-item>
</n-form>
<!-- 表格 -->
<n-data-table
:columns="columns"
:data="data"
:pagination="pagination"
:loading="loading"
/>
<!-- 弹窗 -->
<n-modal v-model:show="showModal" preset="dialog" title="编辑">
<n-form :model="editForm">
<n-form-item label="姓名">
<n-input v-model:value="editForm.name" />
</n-form-item>
</n-form>
<template #action>
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" @click="handleSave">保存</n-button>
</template>
</n-modal>
</div>
</n-dialog-provider>
</n-message-provider>
</n-config-provider>
</template>
<script setup>
import { ref, reactive, h } from "vue";
import { useMessage, useDialog } from "naive-ui";
import { darkTheme, zhCN, dateZhCN } from "naive-ui";
const message = useMessage();
const dialog = useDialog();
const formRef = ref();
const showModal = ref(false);
const loading = ref(false);
const formValue = reactive({
username: "",
password: "",
gender: null,
date: null,
});
const rules = {
username: { required: true, message: "请输入用户名", trigger: "blur" },
password: { required: true, message: "请输入密码", trigger: "blur" },
};
const columns = [
{ title: "姓名", key: "name" },
{ title: "年龄", key: "age", sorter: (a, b) => a.age - b.age },
{ title: "地址", key: "address", ellipsis: { tooltip: true } },
{
title: "操作",
key: "actions",
render(row) {
return h("div", [
h("a", { onClick: () => handleEdit(row) }, "编辑"),
h(
"a",
{
style: "margin-left: 10px",
onClick: () => handleDelete(row),
},
"删除"
),
]);
},
},
];
const data = ref([
{ name: "张三", age: 25, address: "北京市" },
{ name: "李四", age: 30, address: "上海市" },
]);
const pagination = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
});
const editForm = reactive({ name: "" });
async function handleSubmit() {
try {
await formRef.value.validate();
message.success("提交成功");
} catch {
message.error("请检查表单");
}
}
function handleReset() {
formRef.value.restoreValidation();
Object.assign(formValue, {
username: "",
password: "",
gender: null,
date: null,
});
}
function handleEdit(row) {
editForm.name = row.name;
showModal.value = true;
}
function handleDelete(row) {
dialog.warning({
title: "确认删除",
content: `确定删除 ${row.name} 吗?`,
positiveText: "确定",
negativeText: "取消",
onPositiveClick: () => {
message.success(`删除 ${row.name}`);
},
});
}
function handleSave() {
showModal.value = false;
message.success("保存成功");
}
</script>
Vant(移动端)
安装与配置
npm install vant
// 按需引入
import { createApp } from "vue";
import { Button, Cell, CellGroup, Form, Field } from "vant";
import "vant/lib/index.css";
const app = createApp(App);
app.use(Button);
app.use(Cell);
app.use(CellGroup);
app.use(Form);
app.use(Field);
// 自动按需引入
import Components from "unplugin-vue-components/vite";
import { VantResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
Components({
resolvers: [VantResolver()],
}),
],
});
移动端适配
// postcss.config.js
module.exports = {
plugins: {
"postcss-pxtorem": {
rootValue: 37.5,
propList: ["*"],
},
},
};
常用组件示例
<template>
<div class="page">
<!-- 导航栏 -->
<van-nav-bar
title="标题"
left-text="返回"
left-arrow
@click-left="onBack"
/>
<!-- 表单 -->
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="form.username"
name="username"
label="用户名"
placeholder="请输入用户名"
:rules="[{ required: true, message: '请输入用户名' }]"
/>
<van-field
v-model="form.password"
type="password"
name="password"
label="密码"
placeholder="请输入密码"
:rules="[{ required: true, message: '请输入密码' }]"
/>
<van-field name="gender" label="性别">
<template #input>
<van-radio-group v-model="form.gender" direction="horizontal">
<van-radio name="male">男</van-radio>
<van-radio name="female">女</van-radio>
</van-radio-group>
</template>
</van-field>
</van-cell-group>
<div style="margin: 16px">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
<!-- 列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item.id" :title="item.name" />
</van-list>
</van-pull-refresh>
<!-- 弹出层 -->
<van-popup v-model:show="showPopup" position="bottom" round>
<van-picker
:columns="columns"
@confirm="onConfirm"
@cancel="showPopup = false"
/>
</van-popup>
<!-- 操作反馈 -->
<van-button @click="showToast">Toast</van-button>
<van-button @click="showDialog">Dialog</van-button>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { showToast, showDialog } from "vant";
const form = reactive({
username: "",
password: "",
gender: "",
});
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const showPopup = ref(false);
const columns = ["选项1", "选项2", "选项3"];
function onSubmit() {
showToast("提交成功");
}
function onBack() {
history.back();
}
function onLoad() {
// 模拟加载
setTimeout(() => {
for (let i = 0; i < 10; i++) {
list.value.push({
id: list.value.length,
name: `Item ${list.value.length}`,
});
}
loading.value = false;
if (list.value.length >= 40) {
finished.value = true;
}
}, 1000);
}
function onRefresh() {
finished.value = false;
loading.value = true;
list.value = [];
onLoad();
refreshing.value = false;
}
function onConfirm(value) {
showPopup.value = false;
showToast(`选择了 ${value}`);
}
function showToast() {
showToast("这是一个提示");
}
function showDialog() {
showDialog({
title: "标题",
message: "这是一个弹窗",
});
}
</script>
组件库选择建议
根据项目类型选择
| 项目类型 | 推荐组件库 |
|---|---|
| Vue2 中后台 | Element UI |
| Vue3 中后台 | Element Plus / Naive UI |
| 企业级应用 | Ant Design Vue |
| 移动端应用 | Vant |
| Material 风格 | Vuetify |
根据团队情况选择
- 团队熟悉度:选择团队已经熟悉的组件库
- 文档质量:中文文档友好的更适合国内团队
- 社区活跃度:Issue 响应速度、更新频率
- TypeScript:如果项目使用 TS,优先选择 TS 友好的库
总结
Vue 生态有丰富的 UI 组件库可供选择:
- Element Plus:Vue3 最流行的中后台组件库
- Ant Design Vue:企业级应用的首选
- Naive UI:新兴的高质量 Vue3 组件库
- Vant:移动端开发的最佳选择
选择适合自己项目的组件库,能极大提升开发效率和产品质量。