引言
随着前端技术的快速发展,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的开发习惯,特别适合处理复杂的业务逻辑。
主要优势
- 更好的逻辑复用:通过组合函数实现跨组件的逻辑共享
- 更清晰的代码组织:将相关的逻辑集中在一起,而非分散在不同选项中
- 更强的类型支持:与TypeScript配合使用时提供更好的开发体验
- 更好的调试体验:函数调用栈更加直观
项目初始化与环境搭建
创建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)