Skip to content

UI 组件库

Vue 生态系统拥有丰富的 UI 组件库,从企业级的完整解决方案到轻量级的专用组件,满足不同项目的需求。本文将介绍主流的 Vue UI 组件库及其特点。

企业级组件库

Element Plus

Element Plus 是饿了么团队开发的 Vue 3 组件库,提供了丰富的企业级组件。

安装和使用

bash
npm install element-plus
js
// 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-vue
vue
<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 vant
vue
<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/nutui
vue
<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-ui
vue
<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/extras
vue
<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/core
vue
<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/vue
vue
<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,
})

下一步

vue study guide - 专业的 Vue.js 学习平台