Appearance
UI 组件库
Vue 生态系统拥有丰富的 UI 组件库,从企业级的完整解决方案到轻量级的专用组件,满足不同项目的需求。本文将介绍主流的 Vue UI 组件库及其特点。
企业级组件库
Element Plus
Element Plus 是饿了么团队开发的 Vue 3 组件库,提供了丰富的企业级组件。
安装和使用
bash
npm install element-plusjs
// 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')vue
<!-- 按需引入 -->
<template>
<el-button type="primary" @click="handleClick">
点击我
</el-button>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="180" />
<el-table-column prop="name" label="姓名" width="180" />
<el-table-column prop="address" label="地址" />
</el-table>
</template>
<script setup>
import { ElButton, ElTable, ElTableColumn } from 'element-plus'
import { ref } from 'vue'
const tableData = ref([
{
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}
])
const handleClick = () => {
console.log('按钮被点击')
}
</script>自动导入配置
js
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
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: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})Ant Design Vue
基于 Ant Design 设计语言的 Vue 实现,提供企业级的 UI 设计语言和组件。
bash
npm install ant-design-vuevue
<template>
<a-layout>
<a-layout-header>
<a-menu
theme="dark"
mode="horizontal"
:default-selected-keys="['2']"
>
<a-menu-item key="1">nav 1</a-menu-item>
<a-menu-item key="2">nav 2</a-menu-item>
<a-menu-item key="3">nav 3</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout-content style="padding: 0 50px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>Home</a-breadcrumb-item>
<a-breadcrumb-item>List</a-breadcrumb-item>
<a-breadcrumb-item>App</a-breadcrumb-item>
</a-breadcrumb>
<div style="background: #fff; padding: 24px; min-height: 280px">
<a-form
:model="formState"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="onFinish"
>
<a-form-item
label="Username"
name="username"
:rules="[{ required: true, message: 'Please input your username!' }]"
>
<a-input v-model:value="formState.username" />
</a-form-item>
<a-form-item
label="Password"
name="password"
:rules="[{ required: true, message: 'Please input your password!' }]"
>
<a-input-password v-model:value="formState.password" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">Submit</a-button>
</a-form-item>
</a-form>
</div>
</a-layout-content>
<a-layout-footer style="text-align: center">
Ant Design ©2023 Created by Ant UED
</a-layout-footer>
</a-layout>
</template>
<script setup>
import { reactive } from 'vue'
const formState = reactive({
username: '',
password: '',
})
const onFinish = (values) => {
console.log('Success:', values)
}
</script>移动端组件库
Vant
有赞团队开发的轻量、可靠的移动端 Vue 组件库。
bash
npm install vantvue
<template>
<div class="mobile-app">
<!-- 导航栏 -->
<van-nav-bar
title="标题"
left-text="返回"
right-text="按钮"
left-arrow
@click-left="onClickLeft"
@click-right="onClickRight"
/>
<!-- 轮播图 -->
<van-swipe :autoplay="3000" indicator-color="white">
<van-swipe-item v-for="image in images" :key="image">
<img :src="image" alt="轮播图" />
</van-swipe-item>
</van-swipe>
<!-- 商品列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell
v-for="item in list"
:key="item.id"
:title="item.title"
:value="item.price"
:label="item.desc"
/>
</van-list>
<!-- 底部导航 -->
<van-tabbar v-model="active">
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
<van-tabbar-item icon="search">搜索</van-tabbar-item>
<van-tabbar-item icon="friends-o">朋友</van-tabbar-item>
<van-tabbar-item icon="setting-o">设置</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup>
import { ref } from 'vue'
const active = ref(0)
const loading = ref(false)
const finished = ref(false)
const list = ref([])
const images = [
'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
]
const onLoad = () => {
setTimeout(() => {
for (let i = 0; i < 10; i++) {
list.value.push({
id: list.value.length + 1,
title: `商品 ${list.value.length + 1}`,
price: `¥${Math.floor(Math.random() * 1000)}`,
desc: '商品描述信息'
})
}
loading.value = false
if (list.value.length >= 40) {
finished.value = true
}
}, 1000)
}
const onClickLeft = () => {
console.log('点击返回')
}
const onClickRight = () => {
console.log('点击按钮')
}
</script>
<style scoped>
.mobile-app {
height: 100vh;
display: flex;
flex-direction: column;
}
.van-swipe {
height: 200px;
}
.van-swipe img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>NutUI
京东风格的移动端 Vue 组件库。
bash
npm install @nutui/nutuivue
<template>
<div class="nutui-app">
<!-- 头部 -->
<nut-navbar
title="NutUI"
left-text="返回"
@on-click-back="back"
@on-click-title="title"
/>
<!-- 宫格导航 -->
<nut-grid :column-num="4">
<nut-grid-item
v-for="item in gridData"
:key="item.text"
:text="item.text"
:icon="item.icon"
@click="gridClick(item)"
/>
</nut-grid>
<!-- 商品卡片 -->
<nut-card
:img-url="cardData.imgUrl"
:title="cardData.title"
:price="cardData.price"
:vip-price="cardData.vipPrice"
:shop-desc="cardData.shopDesc"
:delivery="cardData.delivery"
:shop-name="cardData.shopName"
/>
<!-- 底部按钮 -->
<nut-button type="primary" block @click="handleSubmit">
立即购买
</nut-button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const gridData = ref([
{ text: '首页', icon: 'home' },
{ text: '分类', icon: 'category' },
{ text: '购物车', icon: 'cart' },
{ text: '我的', icon: 'my' }
])
const cardData = ref({
imgUrl: 'https://img10.360buyimg.com/n2/s240x240_jfs/t1/210890/22/4728/163829/6163a590Eb7c6f4b5/6678b9a9dcbf9dcb.jpg',
title: '活蟹】湖塘煙雨 阳澄湖大闸蟹公4.5两 母3.5两 4对8只 鲜活生鲜螃蟹现货水产礼盒海鲜水',
price: '388',
vipPrice: '378',
shopDesc: '自营',
delivery: '厂商配送',
shopName: '阳澄湖大闸蟹自营店'
})
const back = () => {
console.log('返回')
}
const title = () => {
console.log('点击标题')
}
const gridClick = (item) => {
console.log('点击宫格:', item.text)
}
const handleSubmit = () => {
console.log('立即购买')
}
</script>轻量级组件库
Naive UI
一个 Vue 3 组件库,比较完整,主题可调,使用 TypeScript,快。
bash
npm install naive-uivue
<template>
<n-config-provider :theme="theme">
<n-layout>
<n-layout-header bordered>
<n-space justify="space-between" align="center">
<n-h2>Naive UI Demo</n-h2>
<n-switch
v-model:value="isDark"
@update:value="toggleTheme"
>
<template #checked>🌙</template>
<template #unchecked>☀️</template>
</n-switch>
</n-space>
</n-layout-header>
<n-layout-content content-style="padding: 24px;">
<n-grid :cols="2" :x-gap="12" :y-gap="8">
<n-grid-item>
<n-card title="表单示例">
<n-form
ref="formRef"
:model="model"
:rules="rules"
label-placement="left"
label-width="auto"
>
<n-form-item label="用户名" path="username">
<n-input
v-model:value="model.username"
placeholder="请输入用户名"
/>
</n-form-item>
<n-form-item label="密码" path="password">
<n-input
v-model:value="model.password"
type="password"
placeholder="请输入密码"
/>
</n-form-item>
<n-form-item>
<n-button
type="primary"
@click="handleValidateClick"
>
提交
</n-button>
</n-form-item>
</n-form>
</n-card>
</n-grid-item>
<n-grid-item>
<n-card title="数据展示">
<n-data-table
:columns="columns"
:data="data"
:pagination="pagination"
/>
</n-card>
</n-grid-item>
</n-grid>
</n-layout-content>
</n-layout>
</n-config-provider>
</template>
<script setup>
import { ref, computed } from 'vue'
import { darkTheme } from 'naive-ui'
const isDark = ref(false)
const theme = computed(() => isDark.value ? darkTheme : null)
const formRef = ref(null)
const model = ref({
username: '',
password: ''
})
const rules = {
username: {
required: true,
message: '请输入用户名',
trigger: 'blur'
},
password: {
required: true,
message: '请输入密码',
trigger: 'blur'
}
}
const columns = [
{ title: 'Name', key: 'name' },
{ title: 'Age', key: 'age' },
{ title: 'Address', key: 'address' }
]
const data = ref([
{ name: 'John Brown', age: 32, address: 'New York No. 1 Lake Park' },
{ name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park' },
{ name: 'Joe Black', age: 32, address: 'Sidney No. 1 Lake Park' }
])
const pagination = {
pageSize: 10
}
const toggleTheme = () => {
console.log('切换主题:', isDark.value ? '暗色' : '亮色')
}
const handleValidateClick = () => {
formRef.value?.validate((errors) => {
if (!errors) {
console.log('验证通过')
} else {
console.log('验证失败:', errors)
}
})
}
</script>Quasar
一个基于 Vue 的跨平台框架,支持 SPA、PWA、移动应用等。
bash
npm install quasar @quasar/extrasvue
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title>
Quasar App
</q-toolbar-title>
<div>Quasar v{{ $q.version }}</div>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen"
show-if-above
bordered
>
<q-list>
<q-item-label header>
Essential Links
</q-item-label>
<q-item
v-for="link in essentialLinks"
:key="link.title"
clickable
tag="a"
target="_blank"
:href="link.link"
>
<q-item-section avatar>
<q-icon :name="link.icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ link.title }}</q-item-label>
<q-item-label caption>{{ link.caption }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-drawer>
<q-page-container>
<q-page class="flex flex-center">
<q-card class="my-card">
<q-card-section>
<div class="text-h6">Our Changing Planet</div>
<div class="text-subtitle2">by John Doe</div>
</q-card-section>
<q-card-section>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.
</q-card-section>
<q-card-actions>
<q-btn flat>Action 1</q-btn>
<q-btn flat>Action 2</q-btn>
</q-card-actions>
</q-card>
</q-page>
</q-page-container>
</q-layout>
</template>
<script setup>
import { ref } from 'vue'
const leftDrawerOpen = ref(false)
const essentialLinks = [
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev'
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework'
}
]
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value
}
</script>专用组件库
Vue Use
Vue 组合式 API 的实用工具集合。
bash
npm install @vueuse/corevue
<template>
<div class="vueuse-demo">
<h2>VueUse 示例</h2>
<!-- 鼠标位置 -->
<div class="demo-section">
<h3>鼠标位置</h3>
<p>X: {{ x }}, Y: {{ y }}</p>
</div>
<!-- 本地存储 -->
<div class="demo-section">
<h3>本地存储</h3>
<input v-model="name" placeholder="输入你的名字" />
<p>存储的名字: {{ name }}</p>
</div>
<!-- 网络状态 -->
<div class="demo-section">
<h3>网络状态</h3>
<p>在线状态: {{ isOnline ? '在线' : '离线' }}</p>
</div>
<!-- 暗色模式 -->
<div class="demo-section">
<h3>暗色模式</h3>
<button @click="toggleDark()">
切换到{{ isDark ? '亮色' : '暗色' }}模式
</button>
</div>
<!-- 复制到剪贴板 -->
<div class="demo-section">
<h3>复制功能</h3>
<input v-model="textToCopy" placeholder="要复制的文本" />
<button @click="copy(textToCopy)">复制</button>
<p v-if="copied">已复制!</p>
</div>
<!-- 计数器 -->
<div class="demo-section">
<h3>计数器</h3>
<p>计数: {{ count }}</p>
<button @click="inc()">增加</button>
<button @click="dec()">减少</button>
<button @click="set(0)">重置</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
useMouse,
useLocalStorage,
useOnline,
useDark,
useToggle,
useClipboard,
useCounter
} from '@vueuse/core'
// 鼠标位置
const { x, y } = useMouse()
// 本地存储
const name = useLocalStorage('name', 'Default Name')
// 网络状态
const isOnline = useOnline()
// 暗色模式
const isDark = useDark()
const toggleDark = useToggle(isDark)
// 复制到剪贴板
const textToCopy = ref('Hello VueUse!')
const { copy, copied } = useClipboard()
// 计数器
const { count, inc, dec, set } = useCounter()
</script>
<style scoped>
.vueuse-demo {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.demo-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.demo-section h3 {
margin-top: 0;
color: #333;
}
input {
padding: 8px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
margin-right: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
</style>Headless UI
无样式的完全可访问的 UI 组件。
bash
npm install @headlessui/vuevue
<template>
<div class="headless-ui-demo">
<!-- 下拉菜单 -->
<Menu as="div" class="relative inline-block text-left">
<div>
<MenuButton class="menu-button">
选项
<ChevronDownIcon class="ml-2 h-5 w-5" />
</MenuButton>
</div>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems class="menu-items">
<div class="py-1">
<MenuItem v-slot="{ active }">
<a
href="#"
:class="[
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
]"
>
编辑
</a>
</MenuItem>
<MenuItem v-slot="{ active }">
<a
href="#"
:class="[
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
]"
>
复制
</a>
</MenuItem>
</div>
</MenuItems>
</transition>
</Menu>
<!-- 模态框 -->
<button @click="isOpen = true" class="modal-trigger">
打开模态框
</button>
<TransitionRoot appear :show="isOpen" as="template">
<Dialog as="div" @close="closeModal" class="relative z-10">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4 text-center">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel class="modal-panel">
<DialogTitle as="h3" class="modal-title">
确认操作
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-gray-500">
您确定要执行此操作吗?此操作无法撤销。
</p>
</div>
<div class="mt-4 space-x-2">
<button
type="button"
class="btn btn-primary"
@click="closeModal"
>
确认
</button>
<button
type="button"
class="btn btn-secondary"
@click="closeModal"
>
取消
</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
Dialog,
DialogPanel,
DialogTitle,
TransitionChild,
TransitionRoot,
Menu,
MenuButton,
MenuItem,
MenuItems,
} from '@headlessui/vue'
import { ChevronDownIcon } from '@heroicons/vue/20/solid'
const isOpen = ref(false)
function closeModal() {
isOpen.value = false
}
</script>
<style scoped>
.headless-ui-demo {
padding: 20px;
}
.menu-button {
inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500;
}
.menu-items {
position: absolute;
right: 0;
z-index: 10;
margin-top: 0.5rem;
width: 14rem;
origin: top-right;
border-radius: 0.375rem;
background-color: white;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
ring: 1px solid black;
ring-opacity: 5%;
focus: outline-none;
}
.modal-trigger {
padding: 0.5rem 1rem;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
.modal-panel {
width: 100%;
max-width: 28rem;
transform: scale(1);
border-radius: 0.5rem;
background-color: white;
padding: 1.5rem;
text-align: left;
vertical-align: middle;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
}
.modal-title {
font-size: 1.125rem;
font-weight: 500;
line-height: 1.5rem;
color: #111827;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #3b82f6;
color: white;
border: none;
}
.btn-primary:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #6b7280;
color: white;
border: none;
}
.btn-secondary:hover {
background-color: #4b5563;
}
</style>选择指南
企业级项目
推荐:Element Plus 或 Ant Design Vue
- 组件丰富,功能完整
- 设计规范统一
- 社区活跃,文档完善
- 适合后台管理系统
移动端项目
推荐:Vant 或 NutUI
- 专为移动端优化
- 组件轻量,性能好
- 支持触摸手势
- 适合 H5 应用
追求性能和定制
推荐:Naive UI 或 Headless UI
- 包体积小
- 高度可定制
- TypeScript 支持好
- 适合对性能要求高的项目
跨平台需求
推荐:Quasar
- 一套代码多端运行
- 支持 Web、移动端、桌面端
- 功能全面
- 适合需要多平台部署的项目
最佳实践
1. 按需引入
js
// 推荐:按需引入
import { ElButton, ElTable } from 'element-plus'
// 不推荐:全量引入
import ElementPlus from 'element-plus'2. 主题定制
scss
// 自定义主题变量
$--color-primary: #409EFF;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
@import '~element-plus/packages/theme-chalk/src/index';3. 组件封装
vue
<!-- BaseButton.vue -->
<template>
<el-button
:type="type"
:size="size"
:loading="loading"
:disabled="disabled"
@click="handleClick"
>
<slot />
</el-button>
</template>
<script setup>
interface Props {
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
size?: 'large' | 'default' | 'small'
loading?: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
type: 'default',
size: 'default',
loading: false,
disabled: false
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
const handleClick = (event: MouseEvent) => {
if (!props.loading && !props.disabled) {
emit('click', event)
}
}
</script>4. 全局配置
js
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn,
size: 'default',
zIndex: 3000,
})