Vue 3 Composition API实战:从零构建企业级后台管理系统

DryKnight
DryKnight 2026-01-29T07:04:14+08:00
0 0 1

引言

随着前端技术的快速发展,Vue.js 3的发布为开发者带来了全新的开发体验。Composition API作为Vue 3的核心特性之一,彻底改变了我们编写组件的方式。本文将通过一个完整的企业级后台管理系统的构建过程,深入探讨如何运用Vue 3 Composition API进行现代化前端开发。

在传统Vue 2项目中,我们通常使用选项式API来组织代码,虽然功能完备,但在复杂组件的维护和复用方面存在一定的局限性。而Composition API通过函数的方式组织逻辑,使得代码更加灵活、可复用性和可维护性都得到了显著提升。

Vue 3 Composition API核心概念

什么是Composition API

Composition API是Vue 3中引入的一种新的组件逻辑组织方式。它允许我们使用函数来组织和重用组件逻辑,而不是传统的选项式API(如data、methods、computed等)。这种设计模式更符合现代JavaScript的开发习惯,特别适合处理复杂的业务逻辑。

主要优势

  1. 更好的逻辑复用:通过组合函数实现跨组件的逻辑共享
  2. 更清晰的代码组织:将相关的逻辑集中在一起,而非分散在不同选项中
  3. 更强的类型支持:与TypeScript配合使用时提供更好的开发体验
  4. 更好的调试体验:函数调用栈更加直观

项目初始化与环境搭建

创建Vue 3项目

# 使用Vite创建Vue 3项目
npm create vite@latest admin-system --template vue
cd admin-system
npm install

# 安装依赖
npm install element-plus pinia vue-router axios

项目结构设计

src/
├── assets/              # 静态资源
├── components/          # 公共组件
├── composables/         # 组合式函数
├── hooks/               # 自定义Hook
├── layouts/             # 布局组件
├── pages/               # 页面组件
├── plugins/             # 插件
├── router/              # 路由配置
├── services/            # API服务
├── store/               # 状态管理
├── styles/              # 样式文件
├── utils/               # 工具函数
└── App.vue              # 根组件

路由系统配置

基础路由配置

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/store/auth'

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/pages/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/',
    redirect: '/dashboard',
    component: () => import('@/layouts/MainLayout.vue'),
    meta: { requiresAuth: true },
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/pages/Dashboard.vue')
      },
      {
        path: 'users',
        name: 'Users',
        component: () => import('@/pages/Users.vue')
      },
      {
        path: 'products',
        name: 'Products',
        component: () => import('@/pages/Products.vue')
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()
  
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

export default router

状态管理设计

Pinia状态管理

// src/store/auth.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: localStorage.getItem('token') || null,
    isAuthenticated: false
  }),
  
  getters: {
    hasPermission: (state) => (permission) => {
      return state.user?.permissions?.includes(permission)
    }
  },
  
  actions: {
    async login(credentials) {
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(credentials)
        })
        
        const data = await response.json()
        
        if (data.token) {
          this.token = data.token
          this.user = data.user
          this.isAuthenticated = true
          localStorage.setItem('token', data.token)
        }
        
        return data
      } catch (error) {
        console.error('Login failed:', error)
        throw error
      }
    },
    
    logout() {
      this.token = null
      this.user = null
      this.isAuthenticated = false
      localStorage.removeItem('token')
    }
  }
})

组件化开发实践

公共组件设计

用户卡片组件

<!-- src/components/UserCard.vue -->
<template>
  <div class="user-card">
    <el-avatar 
      :src="user.avatar" 
      :size="40"
      :alt="user.name"
    />
    <div class="user-info">
      <h4>{{ user.name }}</h4>
      <p class="email">{{ user.email }}</p>
    </div>
    <div class="actions">
      <el-button 
        type="primary" 
        size="small"
        @click="handleEdit"
      >
        编辑
      </el-button>
      <el-button 
        type="danger" 
        size="small"
        @click="handleDelete"
      >
        删除
      </el-button>
    </div>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  user: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['edit', 'delete'])

const handleEdit = () => {
  emit('edit', props.user)
}

const handleDelete = () => {
  emit('delete', props.user)
}
</script>

<style scoped>
.user-card {
  display: flex;
  align-items: center;
  padding: 12px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  margin-bottom: 8px;
}

.user-info {
  flex: 1;
  margin-left: 12px;
}

.email {
  color: #909399;
  font-size: 12px;
  margin: 4px 0 0 0;
}

.actions {
  display: flex;
  gap: 8px;
}
</style>

表格组件

<!-- src/components/DataTable.vue -->
<template>
  <div class="data-table">
    <el-table 
      :data="tableData" 
      border 
      style="width: 100%"
      v-loading="loading"
    >
      <el-table-column
        v-for="column in columns"
        :key="column.prop"
        :prop="column.prop"
        :label="column.label"
        :width="column.width"
        :formatter="column.formatter"
      />
      
      <el-table-column label="操作" width="200">
        <template #default="scope">
          <el-button 
            size="small" 
            @click="handleEdit(scope.row)"
          >
            编辑
          </el-button>
          <el-button 
            type="danger" 
            size="small" 
            @click="handleDelete(scope.row)"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <el-pagination
      v-if="showPagination"
      layout="total, prev, pager, next, jumper"
      :total="total"
      :current-page="currentPage"
      :page-size="pageSize"
      @current-change="handlePageChange"
    />
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  columns: {
    type: Array,
    required: true
  },
  tableData: {
    type: Array,
    default: () => []
  },
  loading: {
    type: Boolean,
    default: false
  },
  total: {
    type: Number,
    default: 0
  },
  currentPage: {
    type: Number,
    default: 1
  },
  pageSize: {
    type: Number,
    default: 10
  },
  showPagination: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits(['edit', 'delete', 'page-change'])

const handleEdit = (row) => {
  emit('edit', row)
}

const handleDelete = (row) => {
  emit('delete', row)
}

const handlePageChange = (page) => {
  emit('page-change', page)
}
</script>

<style scoped>
.data-table {
  padding: 16px;
}
</style>

组合式函数应用

数据获取组合式函数

// src/composables/useApi.js
import { ref, reactive } from 'vue'
import axios from 'axios'

export function useApi(baseUrl) {
  const loading = ref(false)
  const error = ref(null)
  const data = ref(null)
  
  const request = async (url, options = {}) => {
    try {
      loading.value = true
      error.value = null
      
      const response = await axios({
        url: `${baseUrl}${url}`,
        ...options
      })
      
      data.value = response.data
      return response.data
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }
  
  const get = async (url, params = {}) => {
    return await request(url, { method: 'GET', params })
  }
  
  const post = async (url, data = {}) => {
    return await request(url, { method: 'POST', data })
  }
  
  const put = async (url, data = {}) => {
    return await request(url, { method: 'PUT', data })
  }
  
  const del = async (url) => {
    return await request(url, { method: 'DELETE' })
  }
  
  return {
    loading,
    error,
    data,
    get,
    post,
    put,
    del
  }
}

表单处理组合式函数

// src/composables/useForm.js
import { ref, reactive } from 'vue'

export function useForm(initialData = {}) {
  const form = reactive({ ...initialData })
  const errors = ref({})
  const isSubmitting = ref(false)
  
  const validate = (rules) => {
    const newErrors = {}
    
    Object.keys(rules).forEach(field => {
      const rule = rules[field]
      const value = form[field]
      
      if (rule.required && !value) {
        newErrors[field] = `${field}是必填项`
      } else if (rule.pattern && !rule.pattern.test(value)) {
        newErrors[field] = rule.message || '格式不正确'
      }
    })
    
    errors.value = newErrors
    return Object.keys(newErrors).length === 0
  }
  
  const reset = () => {
    Object.keys(form).forEach(key => {
      form[key] = initialData[key] || ''
    })
    errors.value = {}
  }
  
  const submit = async (submitFn) => {
    if (!validate()) return false
    
    try {
      isSubmitting.value = true
      await submitFn(form)
      reset()
      return true
    } catch (err) {
      console.error('Submit failed:', err)
      return false
    } finally {
      isSubmitting.value = false
    }
  }
  
  return {
    form,
    errors,
    isSubmitting,
    validate,
    reset,
    submit
  }
}

页面组件实现

用户管理页面

<!-- src/pages/Users.vue -->
<template>
  <div class="users-page">
    <el-card class="page-header">
      <template #header>
        <div class="card-header">
          <span>用户管理</span>
          <el-button 
            type="primary" 
            @click="handleAddUser"
          >
            添加用户
          </el-button>
        </div>
      </template>
      
      <el-form 
        :model="searchForm" 
        inline 
        @submit.prevent="fetchUsers"
      >
        <el-form-item label="用户名">
          <el-input 
            v-model="searchForm.username" 
            placeholder="请输入用户名"
          />
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input 
            v-model="searchForm.email" 
            placeholder="请输入邮箱"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="fetchUsers">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    
    <el-card class="page-content">
      <DataTable
        :columns="columns"
        :table-data="users"
        :loading="loading"
        :total="total"
        :current-page="currentPage"
        :page-size="pageSize"
        @edit="handleEditUser"
        @delete="handleDeleteUser"
        @page-change="handlePageChange"
      />
    </el-card>
    
    <!-- 用户表单对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="isEditing ? '编辑用户' : '添加用户'"
      width="500px"
    >
      <el-form 
        :model="userForm" 
        :rules="rules" 
        ref="formRef"
        label-width="100px"
      >
        <el-form-item label="用户名" prop="username">
          <el-input v-model="userForm.username" />
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="userForm.email" />
        </el-form-item>
        <el-form-item label="手机号" prop="phone">
          <el-input v-model="userForm.phone" />
        </el-form-item>
        <el-form-item label="角色">
          <el-select v-model="userForm.role" placeholder="请选择角色">
            <el-option
              v-for="role in roles"
              :key="role.value"
              :label="role.label"
              :value="role.value"
            />
          </el-select>
        </el-form-item>
      </el-form>
      
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button 
            type="primary" 
            @click="submitForm"
            :loading="isSubmitting"
          >
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import DataTable from '@/components/DataTable.vue'
import { useAuthStore } from '@/store/auth'

const api = useApi('/api')
const authStore = useAuthStore()

// 数据相关
const users = ref([])
const loading = ref(false)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)

// 搜索表单
const searchForm = reactive({
  username: '',
  email: ''
})

// 用户表单
const dialogVisible = ref(false)
const isEditing = ref(false)
const userForm = reactive({
  username: '',
  email: '',
  phone: '',
  role: ''
})

// 表单验证规则
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
  ]
}

// 表格列定义
const columns = [
  { prop: 'username', label: '用户名', width: 120 },
  { prop: 'email', label: '邮箱', width: 200 },
  { prop: 'phone', label: '手机号', width: 150 },
  { prop: 'role', label: '角色', width: 120 },
  { prop: 'createdAt', label: '创建时间', width: 180, formatter: (row) => formatDate(row.createdAt) }
]

// 角色选项
const roles = [
  { value: 'admin', label: '管理员' },
  { value: 'user', label: '普通用户' },
  { value: 'editor', label: '编辑者' }
]

// 初始化数据
onMounted(() => {
  fetchUsers()
})

// 获取用户列表
const fetchUsers = async () => {
  try {
    loading.value = true
    const params = {
      page: currentPage.value,
      limit: pageSize.value,
      ...searchForm
    }
    
    const response = await api.get('/users', { params })
    users.value = response.data
    total.value = response.total
  } catch (error) {
    console.error('获取用户列表失败:', error)
  } finally {
    loading.value = false
  }
}

// 处理分页变化
const handlePageChange = (page) => {
  currentPage.value = page
  fetchUsers()
}

// 添加用户
const handleAddUser = () => {
  isEditing.value = false
  userForm.username = ''
  userForm.email = ''
  userForm.phone = ''
  userForm.role = ''
  dialogVisible.value = true
}

// 编辑用户
const handleEditUser = (user) => {
  isEditing.value = true
  Object.assign(userForm, user)
  dialogVisible.value = true
}

// 删除用户
const handleDeleteUser = async (user) => {
  try {
    await ElMessageBox.confirm(
      `确定要删除用户 ${user.username} 吗?`,
      '提示',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    
    await api.del(`/users/${user.id}`)
    ElMessage.success('删除成功')
    fetchUsers()
  } catch (error) {
    console.error('删除失败:', error)
  }
}

// 提交表单
const submitForm = async () => {
  try {
    const isValid = await formRef.value.validate()
    if (!isValid) return
    
    const method = isEditing.value ? 'put' : 'post'
    const url = isEditing.value ? `/users/${userForm.id}` : '/users'
    
    await api[method](url, userForm)
    ElMessage.success(isEditing.value ? '更新成功' : '添加成功')
    dialogVisible.value = false
    fetchUsers()
  } catch (error) {
    console.error('提交失败:', error)
  }
}

// 重置搜索
const resetSearch = () => {
  searchForm.username = ''
  searchForm.email = ''
  currentPage.value = 1
  fetchUsers()
}

// 格式化时间
const formatDate = (dateString) => {
  return new Date(dateString).toLocaleDateString('zh-CN')
}
</script>

<style scoped>
.users-page {
  padding: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.page-header {
  margin-bottom: 20px;
}
</style>

仪表板页面

<!-- src/pages/Dashboard.vue -->
<template>
  <div class="dashboard">
    <el-row :gutter="20" class="stats-row">
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-content">
            <i class="el-icon-user"></i>
            <div class="stat-info">
              <p class="stat-value">{{ stats.userCount }}</p>
              <p class="stat-label">总用户数</p>
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-content">
            <i class="el-icon-goods"></i>
            <div class="stat-info">
              <p class="stat-value">{{ stats.productCount }}</p>
              <p class="stat-label">商品数量</p>
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-content">
            <i class="el-icon-document"></i>
            <div class="stat-info">
              <p class="stat-value">{{ stats.orderCount }}</p>
              <p class="stat-label">订单数量</p>
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-content">
            <i class="el-icon-money"></i>
            <div class="stat-info">
              <p class="stat-value">{{ stats.revenue }}</p>
              <p class="stat-label">总收入</p>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    
    <el-row :gutter="20" class="charts-row">
      <el-col :span="12">
        <el-card class="chart-card">
          <template #header>
            <div class="card-header">用户增长趋势</div>
          </template>
          <div ref="userChartRef" style="height: 300px;"></div>
        </el-card>
      </el-col>
      
      <el-col :span="12">
        <el-card class="chart-card">
          <template #header>
            <div class="card-header">销售分布</div>
          </template>
          <div ref="salesChartRef" style="height: 300px;"></div>
        </el-card>
      </el-col>
    </el-row>
    
    <el-row :gutter="20" class="recent-orders">
      <el-col :span="24">
        <el-card class="orders-card">
          <template #header>
            <div class="card-header">最近订单</div>
          </template>
          <el-table :data="recentOrders" border style="width: 100%">
            <el-table-column prop="orderNo" label="订单号" width="180"></el-table-column>
            <el-table-column prop="userName" label="用户名称" width="120"></el-table-column>
            <el-table-column prop="amount" label="金额" width="120">
              <template #default="{ row }">¥{{ row.amount }}</template>
            </el-table-column>
            <el-table-column prop="status" label="状态" width="100">
              <template #default="{ row }">
                <el-tag :type="getStatusTagType(row.status)">
                  {{ getStatusText(row.status) }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import { useApi } from '@/composables/useApi'

const api = useApi('/api')
const stats = ref({
  userCount: 0,
  productCount: 0,
  orderCount: 0,
  revenue: '0.00'
})

const recentOrders = ref([])
const userChartRef = ref(null)
const salesChartRef = ref(null)

// 获取仪表板数据
const fetchDashboardData = async () => {
  try {
    const [statsResponse, ordersResponse] = await Promise.all([
      api.get('/dashboard/stats'),
      api.get('/orders/recent')
    ])
    
    stats.value = statsResponse.data
    recentOrders.value = ordersResponse.data
  } catch (error) {
    console.error('获取仪表板数据失败:', error)
  }
}

// 初始化图表
const initCharts = () => {
  // 用户增长趋势图
  const userChart = echarts.init(userChartRef.value)
  userChart.setOption({
    title: {
      text: '用户增长趋势'
    },
    tooltip: {
      trigger: 'axis'
    },
    xAxis: {
      type: 'category',
      data: ['1月', '2月', '3月', '4月', '5月', '6月']
    },
    yAxis: {
      type: 'value'
    },
    series: [{
      data: [120, 200, 150, 80, 70, 110],
      type: 'line',
      smooth: true
    }]
  })
  
  // 销售分布图
  const salesChart = echarts.init(salesChartRef.value)
  salesChart.setOption({
    title: {
      text: '销售分布'
    },
    tooltip: {
      trigger: 'item'
    },
    series: [{
      type: 'pie',
      data: [
        { value: 1048, name: '电子产品' },
        { value: 735, name: '服装' },
        { value: 580, name: '家居用品' },
        { value: 484, name: '图书' }
      ]
    }]
  })
}

// 状态标签类型
const getStatusTagType = (status) => {
  switch (status) {
    case 'pending': return 'warning'
    case 'completed': return 'success'
    case 'cancelled': return 'danger'
    default: return 'info'
  }
}

// 状态文本
const getStatusText = (status) => {
  switch (status) {
    case 'pending': return '待处理'
    case 'completed': return '已完成'
    case 'cancelled': return '已取消'
    default: return status
  }
}

onMounted(() => {
  fetchDashboardData()
  initCharts()
})
</script>

<style scoped>
.dashboard {
  padding: 20px;
}

.stats-row {
  margin-bottom: 20px;
}

.stat-card {
  height: 120px;
}

.stat-content {
  display: flex;
  align-items: center;
}

.stat-info {
  margin-left: 15px;
}

.stat-value {
  font-size: 24px;
  font-weight: bold;
  margin: 0;
}

.stat-label {
  color: #909399;
  margin: 5px 0 0 0;
}

.charts-row {
  margin-bottom: 20px;
}

.chart-card {
  height: 350px;
}

.card-header {
  font-weight: bold;
}

.re
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000