Skip to content

UI 组件库

Vue 生态系统拥有丰富的 UI 组件库,为开发者提供了各种风格和功能的组件选择。本文介绍主流的 Vue 3 UI 组件库及其使用方法。

主流 UI 组件库对比

组件库特点适用场景包大小TypeScript
Element Plus企业级、功能丰富后台管理系统较大
Ant Design Vue企业级、设计语言完整企业应用较大
VuetifyMaterial Design现代 Web 应用较大
Naive UI轻量、现代各种应用中等
Quasar全平台、功能强大跨平台应用
Vant移动端专用移动端应用
PrimeVue主题丰富企业应用中等
Arco Design字节跳动出品企业应用中等

Element Plus

安装和配置

bash
# 安装
npm install element-plus

# 图标库(可选)
npm install @element-plus/icons-vue

完整引入

javascript
// 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')

按需引入(推荐)

bash
# 安装自动导入插件
npm install -D unplugin-vue-components unplugin-auto-import
javascript
// 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()],
    }),
  ],
})

基本使用

vue
<template>
  <div class="demo-container">
    <!-- 按钮 -->
    <div class="demo-section">
      <h3>按钮</h3>
      <el-button>默认按钮</el-button>
      <el-button type="primary">主要按钮</el-button>
      <el-button type="success">成功按钮</el-button>
      <el-button type="warning">警告按钮</el-button>
      <el-button type="danger">危险按钮</el-button>
    </div>
    
    <!-- 表单 -->
    <div class="demo-section">
      <h3>表单</h3>
      <el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
        <el-form-item label="用户名" prop="username">
          <el-input v-model="form.username" placeholder="请输入用户名"></el-input>
        </el-form-item>
        
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="form.password"
            type="password"
            placeholder="请输入密码"
            show-password
          ></el-input>
        </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="music">音乐</el-checkbox>
            <el-checkbox label="sports">运动</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>
    </div>
    
    <!-- 表格 -->
    <div class="demo-section">
      <h3>表格</h3>
      <el-table :data="tableData" style="width: 100%">
        <el-table-column prop="name" label="姓名" width="120"></el-table-column>
        <el-table-column prop="age" label="年龄" width="80"></el-table-column>
        <el-table-column prop="email" label="邮箱"></el-table-column>
        <el-table-column label="操作" width="150">
          <template #default="{ row, $index }">
            <el-button size="small" @click="editUser(row)">编辑</el-button>
            <el-button size="small" type="danger" @click="deleteUser($index)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    
    <!-- 对话框 -->
    <el-dialog v-model="dialogVisible" title="编辑用户" width="30%">
      <el-form :model="editForm" label-width="80px">
        <el-form-item label="姓名">
          <el-input v-model="editForm.name"></el-input>
        </el-form-item>
        <el-form-item label="年龄">
          <el-input-number v-model="editForm.age" :min="1" :max="120"></el-input-number>
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="editForm.email"></el-input>
        </el-form-item>
      </el-form>
      
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveUser">保存</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'

// 表单数据
const form = reactive({
  username: '',
  password: '',
  gender: '',
  hobbies: []
})

// 表单验证规则
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' }
  ],
  gender: [
    { required: true, message: '请选择性别', trigger: 'change' }
  ]
}

const formRef = ref()

// 表格数据
const tableData = ref([
  { name: '张三', age: 25, email: 'zhangsan@example.com' },
  { name: '李四', age: 30, email: 'lisi@example.com' },
  { name: '王五', age: 28, email: 'wangwu@example.com' }
])

// 对话框
const dialogVisible = ref(false)
const editForm = reactive({
  name: '',
  age: 0,
  email: ''
})
let editIndex = -1

// 表单提交
function submitForm() {
  formRef.value.validate((valid) => {
    if (valid) {
      ElMessage.success('提交成功!')
      console.log('表单数据:', form)
    } else {
      ElMessage.error('请检查表单数据')
    }
  })
}

// 重置表单
function resetForm() {
  formRef.value.resetFields()
}

// 编辑用户
function editUser(row) {
  editIndex = tableData.value.indexOf(row)
  Object.assign(editForm, row)
  dialogVisible.value = true
}

// 删除用户
function deleteUser(index) {
  ElMessageBox.confirm('确定要删除这个用户吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    tableData.value.splice(index, 1)
    ElMessage.success('删除成功')
  }).catch(() => {
    ElMessage.info('已取消删除')
  })
}

// 保存用户
function saveUser() {
  if (editIndex >= 0) {
    Object.assign(tableData.value[editIndex], editForm)
    ElMessage.success('保存成功')
    dialogVisible.value = false
  }
}
</script>

<style scoped>
.demo-container {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.demo-section {
  margin-bottom: 40px;
  padding: 20px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
}

.demo-section h3 {
  margin-top: 0;
  color: #303133;
}

.el-button {
  margin-right: 10px;
  margin-bottom: 10px;
}
</style>

主题定制

scss
// 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,
    ),
    'error': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  )
);
javascript
// vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element-variables.scss" as *;`,
      },
    },
  },
})

Ant Design Vue

安装和配置

bash
npm install ant-design-vue
javascript
// main.js
import { createApp } from 'vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import App from './App.vue'

const app = createApp(App)
app.use(Antd)
app.mount('#app')

基本使用

vue
<template>
  <div class="antd-demo">
    <!-- 布局 -->
    <a-layout style="min-height: 100vh">
      <a-layout-sider v-model:collapsed="collapsed" collapsible>
        <div class="logo" />
        <a-menu theme="dark" mode="inline" :default-selected-keys="['1']">
          <a-menu-item key="1">
            <user-outlined />
            <span>用户管理</span>
          </a-menu-item>
          <a-menu-item key="2">
            <video-camera-outlined />
            <span>内容管理</span>
          </a-menu-item>
          <a-menu-item key="3">
            <upload-outlined />
            <span>文件管理</span>
          </a-menu-item>
        </a-menu>
      </a-layout-sider>
      
      <a-layout>
        <a-layout-header style="background: #fff; padding: 0">
          <menu-unfold-outlined
            v-if="collapsed"
            class="trigger"
            @click="() => (collapsed = !collapsed)"
          />
          <menu-fold-outlined
            v-else
            class="trigger"
            @click="() => (collapsed = !collapsed)"
          />
        </a-layout-header>
        
        <a-layout-content style="margin: 16px">
          <div style="padding: 24px; background: #fff; min-height: 360px">
            <!-- 表格 -->
            <a-table
              :columns="columns"
              :data-source="dataSource"
              :pagination="pagination"
              :loading="loading"
              @change="handleTableChange"
            >
              <template #bodyCell="{ column, record }">
                <template v-if="column.key === 'action'">
                  <a-space>
                    <a-button type="primary" size="small" @click="edit(record)">
                      编辑
                    </a-button>
                    <a-popconfirm
                      title="确定要删除吗?"
                      @confirm="deleteRecord(record.key)"
                    >
                      <a-button type="primary" danger size="small">
                        删除
                      </a-button>
                    </a-popconfirm>
                  </a-space>
                </template>
              </template>
            </a-table>
          </div>
        </a-layout-content>
      </a-layout>
    </a-layout>
    
    <!-- 编辑模态框 -->
    <a-modal
      v-model:visible="modalVisible"
      title="编辑用户"
      @ok="handleOk"
      @cancel="handleCancel"
    >
      <a-form
        :model="formData"
        :rules="rules"
        ref="formRef"
        :label-col="{ span: 6 }"
        :wrapper-col="{ span: 18 }"
      >
        <a-form-item label="姓名" name="name">
          <a-input v-model:value="formData.name" />
        </a-form-item>
        
        <a-form-item label="年龄" name="age">
          <a-input-number v-model:value="formData.age" :min="1" :max="120" />
        </a-form-item>
        
        <a-form-item label="邮箱" name="email">
          <a-input v-model:value="formData.email" />
        </a-form-item>
        
        <a-form-item label="状态" name="status">
          <a-select v-model:value="formData.status">
            <a-select-option value="active">活跃</a-select-option>
            <a-select-option value="inactive">非活跃</a-select-option>
          </a-select>
        </a-form-item>
      </a-form>
    </a-modal>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import {
  UserOutlined,
  VideoCameraOutlined,
  UploadOutlined,
  MenuUnfoldOutlined,
  MenuFoldOutlined,
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'

// 侧边栏折叠状态
const collapsed = ref(false)

// 表格配置
const columns = [
  {
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: '年龄',
    dataIndex: 'age',
    key: 'age',
  },
  {
    title: '邮箱',
    dataIndex: 'email',
    key: 'email',
  },
  {
    title: '状态',
    dataIndex: 'status',
    key: 'status',
  },
  {
    title: '操作',
    key: 'action',
  },
]

// 表格数据
const dataSource = ref([])
const loading = ref(false)

// 分页配置
const pagination = reactive({
  current: 1,
  pageSize: 10,
  total: 0,
  showSizeChanger: true,
  showQuickJumper: true,
  showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条`,
})

// 模态框
const modalVisible = ref(false)
const formRef = ref()
const formData = reactive({
  key: '',
  name: '',
  age: 0,
  email: '',
  status: 'active'
})

// 表单验证规则
const rules = {
  name: [{ required: true, message: '请输入姓名' }],
  age: [{ required: true, message: '请输入年龄' }],
  email: [
    { required: true, message: '请输入邮箱' },
    { type: 'email', message: '请输入正确的邮箱格式' }
  ],
  status: [{ required: true, message: '请选择状态' }]
}

// 加载数据
function loadData() {
  loading.value = true
  
  // 模拟 API 调用
  setTimeout(() => {
    dataSource.value = [
      {
        key: '1',
        name: '张三',
        age: 25,
        email: 'zhangsan@example.com',
        status: 'active'
      },
      {
        key: '2',
        name: '李四',
        age: 30,
        email: 'lisi@example.com',
        status: 'inactive'
      },
      {
        key: '3',
        name: '王五',
        age: 28,
        email: 'wangwu@example.com',
        status: 'active'
      }
    ]
    
    pagination.total = dataSource.value.length
    loading.value = false
  }, 1000)
}

// 表格变化处理
function handleTableChange(pag, filters, sorter) {
  pagination.current = pag.current
  pagination.pageSize = pag.pageSize
  loadData()
}

// 编辑记录
function edit(record) {
  Object.assign(formData, record)
  modalVisible.value = true
}

// 删除记录
function deleteRecord(key) {
  const index = dataSource.value.findIndex(item => item.key === key)
  if (index > -1) {
    dataSource.value.splice(index, 1)
    message.success('删除成功')
  }
}

// 模态框确认
function handleOk() {
  formRef.value.validate().then(() => {
    const index = dataSource.value.findIndex(item => item.key === formData.key)
    if (index > -1) {
      Object.assign(dataSource.value[index], formData)
    }
    
    modalVisible.value = false
    message.success('保存成功')
  }).catch(() => {
    message.error('请检查表单数据')
  })
}

// 模态框取消
function handleCancel() {
  modalVisible.value = false
  formRef.value.resetFields()
}

onMounted(() => {
  loadData()
})
</script>

<style scoped>
.antd-demo {
  height: 100vh;
}

.logo {
  height: 32px;
  margin: 16px;
  background: rgba(255, 255, 255, 0.3);
}

.trigger {
  font-size: 18px;
  line-height: 64px;
  padding: 0 24px;
  cursor: pointer;
  transition: color 0.3s;
}

.trigger:hover {
  color: #1890ff;
}
</style>

Naive UI

安装和配置

bash
npm install naive-ui
javascript
// main.js
import { createApp } from 'vue'
import naive from 'naive-ui'
import App from './App.vue'

const app = createApp(App)
app.use(naive)
app.mount('#app')

基本使用

vue
<template>
  <n-config-provider :theme="theme">
    <div class="naive-demo">
      <n-space vertical size="large">
        <!-- 主题切换 -->
        <n-card title="主题设置">
          <n-space>
            <n-button @click="toggleTheme">
              {{ isDark ? '切换到亮色' : '切换到暗色' }}
            </n-button>
          </n-space>
        </n-card>
        
        <!-- 表单 -->
        <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="请输入密码"
                show-password-on="mousedown"
              />
            </n-form-item>
            
            <n-form-item label="年龄" path="age">
              <n-input-number v-model:value="model.age" :min="1" :max="120" />
            </n-form-item>
            
            <n-form-item label="性别" path="gender">
              <n-radio-group v-model:value="model.gender">
                <n-radio value="male">男</n-radio>
                <n-radio value="female">女</n-radio>
              </n-radio-group>
            </n-form-item>
            
            <n-form-item>
              <n-space>
                <n-button type="primary" @click="handleSubmit">
                  提交
                </n-button>
                <n-button @click="handleReset">
                  重置
                </n-button>
              </n-space>
            </n-form-item>
          </n-form>
        </n-card>
        
        <!-- 数据表格 -->
        <n-card title="数据表格">
          <n-data-table
            :columns="columns"
            :data="data"
            :pagination="paginationReactive"
            :loading="loading"
          />
        </n-card>
      </n-space>
    </div>
  </n-config-provider>
</template>

<script setup>
import { ref, reactive, h } from 'vue'
import {
  NButton,
  NSpace,
  NPopconfirm,
  useMessage,
  useDialog,
  darkTheme
} from 'naive-ui'

// 主题
const isDark = ref(false)
const theme = ref(null)

function toggleTheme() {
  isDark.value = !isDark.value
  theme.value = isDark.value ? darkTheme : null
}

// 消息和对话框
const message = useMessage()
const dialog = useDialog()

// 表单
const formRef = ref()
const model = reactive({
  username: '',
  password: '',
  age: null,
  gender: null
})

const rules = {
  username: {
    required: true,
    message: '请输入用户名',
    trigger: ['input', 'blur']
  },
  password: {
    required: true,
    message: '请输入密码',
    trigger: ['input', 'blur']
  },
  age: {
    required: true,
    type: 'number',
    message: '请输入年龄',
    trigger: ['input', 'blur']
  },
  gender: {
    required: true,
    message: '请选择性别',
    trigger: 'change'
  }
}

function handleSubmit() {
  formRef.value?.validate((errors) => {
    if (!errors) {
      message.success('提交成功')
      console.log('表单数据:', model)
    } else {
      message.error('请检查表单数据')
    }
  })
}

function handleReset() {
  formRef.value?.restoreValidation()
  Object.assign(model, {
    username: '',
    password: '',
    age: null,
    gender: null
  })
}

// 表格
const loading = ref(false)
const data = ref([
  {
    key: 1,
    name: '张三',
    age: 25,
    email: 'zhangsan@example.com',
    status: '活跃'
  },
  {
    key: 2,
    name: '李四',
    age: 30,
    email: 'lisi@example.com',
    status: '非活跃'
  },
  {
    key: 3,
    name: '王五',
    age: 28,
    email: 'wangwu@example.com',
    status: '活跃'
  }
])

const columns = [
  {
    title: '姓名',
    key: 'name'
  },
  {
    title: '年龄',
    key: 'age'
  },
  {
    title: '邮箱',
    key: 'email'
  },
  {
    title: '状态',
    key: 'status'
  },
  {
    title: '操作',
    key: 'actions',
    render(row) {
      return h(NSpace, null, {
        default: () => [
          h(
            NButton,
            {
              size: 'small',
              type: 'primary',
              onClick: () => editRow(row)
            },
            { default: () => '编辑' }
          ),
          h(
            NPopconfirm,
            {
              onPositiveClick: () => deleteRow(row.key)
            },
            {
              trigger: () => h(
                NButton,
                {
                  size: 'small',
                  type: 'error'
                },
                { default: () => '删除' }
              ),
              default: () => '确定要删除吗?'
            }
          )
        ]
      })
    }
  }
]

const paginationReactive = reactive({
  page: 1,
  pageSize: 5,
  showSizePicker: true,
  pageSizes: [3, 5, 7],
  onChange: (page) => {
    paginationReactive.page = page
  },
  onUpdatePageSize: (pageSize) => {
    paginationReactive.pageSize = pageSize
    paginationReactive.page = 1
  }
})

function editRow(row) {
  dialog.info({
    title: '编辑',
    content: `编辑用户: ${row.name}`,
    positiveText: '确定',
    onPositiveClick: () => {
      message.success('编辑成功')
    }
  })
}

function deleteRow(key) {
  const index = data.value.findIndex(item => item.key === key)
  if (index > -1) {
    data.value.splice(index, 1)
    message.success('删除成功')
  }
}
</script>

<style scoped>
.naive-demo {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}
</style>

Vant (移动端)

安装和配置

bash
npm install vant
javascript
// main.js
import { createApp } from 'vue'
import Vant from 'vant'
import 'vant/lib/index.css'
import App from './App.vue'

const app = createApp(App)
app.use(Vant)
app.mount('#app')

移动端应用示例

vue
<template>
  <div class="mobile-app">
    <!-- 导航栏 -->
    <van-nav-bar
      title="移动端应用"
      left-text="返回"
      right-text="按钮"
      left-arrow
      @click-left="onClickLeft"
      @click-right="onClickRight"
    />
    
    <!-- 标签页 -->
    <van-tabs v-model:active="activeTab" @change="onTabChange">
      <!-- 首页 -->
      <van-tab title="首页">
        <div class="tab-content">
          <!-- 轮播图 -->
          <van-swipe :autoplay="3000" indicator-color="white">
            <van-swipe-item v-for="(image, index) in banners" :key="index">
              <img :src="image" alt="Banner" class="banner-image">
            </van-swipe-item>
          </van-swipe>
          
          <!-- 网格导航 -->
          <van-grid :column-num="4" :gutter="10">
            <van-grid-item
              v-for="item in gridItems"
              :key="item.id"
              :icon="item.icon"
              :text="item.text"
              @click="onGridClick(item)"
            />
          </van-grid>
          
          <!-- 商品列表 -->
          <van-list
            v-model:loading="loading"
            :finished="finished"
            finished-text="没有更多了"
            @load="onLoad"
          >
            <van-card
              v-for="item in products"
              :key="item.id"
              :num="item.num"
              :price="item.price"
              :title="item.title"
              :thumb="item.thumb"
              @click="onProductClick(item)"
            >
              <template #footer>
                <van-button size="mini" type="primary">
                  加入购物车
                </van-button>
              </template>
            </van-card>
          </van-list>
        </div>
      </van-tab>
      
      <!-- 分类 -->
      <van-tab title="分类">
        <div class="tab-content">
          <van-sidebar v-model="activeSidebar">
            <van-sidebar-item
              v-for="category in categories"
              :key="category.id"
              :title="category.name"
            />
          </van-sidebar>
        </div>
      </van-tab>
      
      <!-- 购物车 -->
      <van-tab title="购物车">
        <div class="tab-content">
          <van-submit-bar
            :price="totalPrice"
            button-text="提交订单"
            @submit="onSubmit"
          >
            <van-checkbox v-model="selectAll" @change="onSelectAll">
              全选
            </van-checkbox>
          </van-submit-bar>
          
          <van-card
            v-for="item in cartItems"
            :key="item.id"
            :num="item.num"
            :price="item.price"
            :title="item.title"
            :thumb="item.thumb"
          >
            <template #num>
              <van-stepper
                v-model="item.num"
                :min="1"
                @change="onStepperChange"
              />
            </template>
            
            <template #footer>
              <van-checkbox
                v-model="item.selected"
                @change="onItemSelect"
              />
            </template>
          </van-card>
        </div>
      </van-tab>
      
      <!-- 我的 -->
      <van-tab title="我的">
        <div class="tab-content">
          <van-cell-group>
            <van-cell
              title="个人信息"
              is-link
              @click="showUserInfo"
            />
            <van-cell
              title="我的订单"
              is-link
              @click="showOrders"
            />
            <van-cell
              title="设置"
              is-link
              @click="showSettings"
            />
          </van-cell-group>
        </div>
      </van-tab>
    </van-tabs>
    
    <!-- 弹出层 -->
    <van-popup v-model:show="showPopup" position="bottom" :style="{ height: '30%' }">
      <div class="popup-content">
        <h3>弹出层内容</h3>
        <p>这是一个从底部弹出的层</p>
        <van-button type="primary" @click="showPopup = false">
          关闭
        </van-button>
      </div>
    </van-popup>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { showToast, showDialog, showNotify } from 'vant'

// 标签页
const activeTab = ref(0)

// 轮播图
const banners = ref([
  'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
  'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
  'https://fastly.jsdelivr.net/npm/@vant/assets/apple-3.jpeg'
])

// 网格导航
const gridItems = ref([
  { id: 1, icon: 'home-o', text: '首页' },
  { id: 2, icon: 'search', text: '搜索' },
  { id: 3, icon: 'friends-o', text: '朋友' },
  { id: 4, icon: 'setting-o', text: '设置' }
])

// 商品列表
const products = ref([])
const loading = ref(false)
const finished = ref(false)

// 分类
const activeSidebar = ref(0)
const categories = ref([
  { id: 1, name: '电子产品' },
  { id: 2, name: '服装鞋帽' },
  { id: 3, name: '家居用品' },
  { id: 4, name: '运动户外' }
])

// 购物车
const cartItems = ref([
  {
    id: 1,
    title: 'iPhone 14',
    price: 5999,
    num: 1,
    thumb: 'https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg',
    selected: true
  },
  {
    id: 2,
    title: 'MacBook Pro',
    price: 12999,
    num: 1,
    thumb: 'https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg',
    selected: false
  }
])

const selectAll = ref(false)

// 弹出层
const showPopup = ref(false)

// 计算属性
const totalPrice = computed(() => {
  return cartItems.value
    .filter(item => item.selected)
    .reduce((total, item) => total + item.price * item.num, 0)
})

// 方法
function onClickLeft() {
  showToast('返回')
}

function onClickRight() {
  showToast('按钮')
}

function onTabChange(index) {
  console.log('切换到标签页:', index)
}

function onGridClick(item) {
  showToast(item.text)
}

function onProductClick(item) {
  showDialog({
    title: '商品详情',
    message: `商品: ${item.title}\n价格: ¥${item.price}`
  })
}

function onLoad() {
  loading.value = true
  
  // 模拟异步加载
  setTimeout(() => {
    for (let i = 0; i < 10; i++) {
      products.value.push({
        id: products.value.length + 1,
        title: `商品 ${products.value.length + 1}`,
        price: Math.floor(Math.random() * 1000) + 100,
        thumb: 'https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg'
      })
    }
    
    loading.value = false
    
    if (products.value.length >= 50) {
      finished.value = true
    }
  }, 1000)
}

function onSelectAll(checked) {
  cartItems.value.forEach(item => {
    item.selected = checked
  })
}

function onItemSelect() {
  const selectedCount = cartItems.value.filter(item => item.selected).length
  selectAll.value = selectedCount === cartItems.value.length
}

function onStepperChange() {
  // 数量变化处理
}

function onSubmit() {
  const selectedItems = cartItems.value.filter(item => item.selected)
  if (selectedItems.length === 0) {
    showToast('请选择商品')
    return
  }
  
  showDialog({
    title: '确认订单',
    message: `总价: ¥${totalPrice.value}`,
    confirmButtonText: '确认支付',
    cancelButtonText: '取消'
  }).then(() => {
    showNotify({ type: 'success', message: '支付成功' })
  }).catch(() => {
    showToast('取消支付')
  })
}

function showUserInfo() {
  showPopup.value = true
}

function showOrders() {
  showToast('我的订单')
}

function showSettings() {
  showToast('设置')
}

onMounted(() => {
  onLoad()
})
</script>

<style scoped>
.mobile-app {
  height: 100vh;
  background-color: #f7f8fa;
}

.tab-content {
  padding: 16px;
}

.banner-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.popup-content {
  padding: 20px;
  text-align: center;
}

.popup-content h3 {
  margin-bottom: 16px;
}

.popup-content p {
  margin-bottom: 20px;
  color: #666;
}
</style>

选择建议

企业级应用

  • Element Plus: 功能最全面,生态最完善
  • Ant Design Vue: 设计语言统一,组件质量高
  • Arco Design: 字节跳动出品,现代化设计

现代 Web 应用

  • Naive UI: 轻量现代,TypeScript 友好
  • Vuetify: Material Design 风格
  • PrimeVue: 主题丰富,定制性强

移动端应用

  • Vant: 专业的移动端组件库
  • NutUI: 京东出品的移动端组件库

跨平台应用

  • Quasar: 一套代码,多端运行
  • Ionic Vue: 混合应用开发

最佳实践

1. 按需引入

javascript
// 推荐:按需引入
import { ElButton, ElInput } from 'element-plus'

// 避免:全量引入
import ElementPlus from 'element-plus'

2. 主题定制

scss
// 定制主题变量
$--color-primary: #409eff;
$--color-success: #67c23a;
$--color-warning: #e6a23c;
$--color-danger: #f56c6c;

3. 组件封装

vue
<!-- 封装业务组件 -->
<template>
  <el-table
    :data="data"
    :loading="loading"
    v-bind="$attrs"
  >
    <slot></slot>
  </el-table>
</template>

<script setup>
defineProps({
  data: Array,
  loading: Boolean
})
</script>

4. 国际化支持

javascript
// 配置国际化
import { createI18n } from 'vue-i18n'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

app.use(ElementPlus, {
  locale: zhCn
})

下一步

选择合适的 UI 组件库能够大大提升开发效率,建议根据项目需求和团队技术栈来选择!

基于 Vue.js 官方文档构建的学习宝典