Appearance
企业级项目实战
在本章中,我们将使用 Vue3 构建一个企业级项目,涵盖项目初始化、架构搭建、路由配置、状态管理、API 调用等核心功能。
项目需求
我们需要构建一个企业级的用户管理系统,具有以下功能:
- 用户登录/注册
- 用户列表管理
- 用户详情查看
- 用户编辑
- 用户删除
- 数据分页
- 搜索功能
- 权限控制
技术栈
- 框架:Vue 3
- 构建工具:Vite
- 路由:Vue Router
- 状态管理:Pinia
- HTTP 客户端:Axios
- UI 库:Element Plus
- 样式:SCSS
- 代码规范:ESLint + Prettier
项目初始化
1. 创建项目
bash
npm create vite@latest enterprise-project -- --template vue
cd enterprise-project
npm install2. 安装依赖
bash
npm install vue-router@4 pinia axios element-plus sass
npm install --save-dev eslint prettier eslint-plugin-vue项目架构
enterprise-project/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 资源文件
│ │ ├── styles/ # 样式文件
│ │ └── images/ # 图片文件
│ ├── components/ # 通用组件
│ ├── views/ # 页面组件
│ │ ├── auth/ # 认证相关页面
│ │ └── user/ # 用户管理页面
│ ├── router/ # 路由配置
│ ├── stores/ # 状态管理
│ ├── api/ # API 调用
│ ├── utils/ # 工具函数
│ ├── hooks/ # 自定义 hooks
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── .eslintrc.js # ESLint 配置
├── .prettierrc.js # Prettier 配置
├── vite.config.js # Vite 配置
└── package.json # 项目配置核心功能实现
1. 路由配置
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const routes = [
{
path: '/',
redirect: '/user/list'
},
{
path: '/login',
name: 'Login',
component: () => import('../views/auth/Login.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/register',
name: 'Register',
component: () => import('../views/auth/Register.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/user',
name: 'User',
component: () => import('../views/user/Layout.vue'),
meta: {
requiresAuth: true
},
children: [
{
path: 'list',
name: 'UserList',
component: () => import('../views/user/List.vue')
},
{
path: 'create',
name: 'UserCreate',
component: () => import('../views/user/Create.vue')
},
{
path: 'edit/:id',
name: 'UserEdit',
component: () => import('../views/user/Edit.vue')
},
{
path: 'detail/:id',
name: 'UserDetail',
component: () => import('../views/user/Detail.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
const isLoggedIn = authStore.isLoggedIn
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login')
} else if (to.meta.requiresGuest && isLoggedIn) {
next('/user/list')
} else {
next()
}
})
export default router2. 状态管理
javascript
// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login as loginApi, register as registerApi, logout as logoutApi } from '../api/auth'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('token') || null)
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
const isLoggedIn = computed(() => !!token.value)
async function login(username, password) {
try {
const response = await loginApi(username, password)
token.value = response.token
user.value = response.user
localStorage.setItem('token', response.token)
localStorage.setItem('user', JSON.stringify(response.user))
return true
} catch (error) {
console.error('登录失败:', error)
return false
}
}
async function register(userData) {
try {
const response = await registerApi(userData)
return true
} catch (error) {
console.error('注册失败:', error)
return false
}
}
function logout() {
token.value = null
user.value = null
localStorage.removeItem('token')
localStorage.removeItem('user')
}
return {
token,
user,
isLoggedIn,
login,
register,
logout
}
})
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getUserList, getUserById, createUser, updateUser, deleteUser } from '../api/user'
export const useUserStore = defineStore('user', () => {
const users = ref([])
const currentUser = ref(null)
const loading = ref(false)
const error = ref(null)
const pagination = ref({
page: 1,
pageSize: 10,
total: 0
})
async function fetchUsers(params) {
loading.value = true
error.value = null
try {
const response = await getUserList({
...params,
page: pagination.value.page,
pageSize: pagination.value.pageSize
})
users.value = response.data
pagination.value.total = response.total
} catch (err) {
error.value = '获取用户列表失败'
console.error(err)
} finally {
loading.value = false
}
}
async function fetchUserById(id) {
loading.value = true
error.value = null
try {
currentUser.value = await getUserById(id)
} catch (err) {
error.value = '获取用户详情失败'
console.error(err)
} finally {
loading.value = false
}
}
async function addUser(userData) {
loading.value = true
error.value = null
try {
await createUser(userData)
await fetchUsers()
return true
} catch (err) {
error.value = '创建用户失败'
console.error(err)
return false
} finally {
loading.value = false
}
}
async function editUser(id, userData) {
loading.value = true
error.value = null
try {
await updateUser(id, userData)
await fetchUsers()
return true
} catch (err) {
error.value = '更新用户失败'
console.error(err)
return false
} finally {
loading.value = false
}
}
async function removeUser(id) {
loading.value = true
error.value = null
try {
await deleteUser(id)
await fetchUsers()
return true
} catch (err) {
error.value = '删除用户失败'
console.error(err)
return false
} finally {
loading.value = false
}
}
function setPage(page) {
pagination.value.page = page
fetchUsers()
}
function setPageSize(pageSize) {
pagination.value.pageSize = pageSize
pagination.value.page = 1
fetchUsers()
}
return {
users,
currentUser,
loading,
error,
pagination,
fetchUsers,
fetchUserById,
addUser,
editUser,
removeUser,
setPage,
setPageSize
}
})3. API 调用
javascript
// api/request.js
import axios from 'axios'
const request = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
return response.data
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.href = '/login'
break
case 403:
console.error('禁止访问')
break
case 404:
console.error('资源不存在')
break
case 500:
console.error('服务器错误')
break
default:
console.error('请求失败')
}
} else if (error.request) {
console.error('网络错误,无法连接到服务器')
} else {
console.error('请求配置错误:', error.message)
}
return Promise.reject(error)
}
)
export default request
// api/auth.js
import request from './request'
export const login = (username, password) => {
return request({
url: '/auth/login',
method: 'post',
data: { username, password }
})
}
export const register = (userData) => {
return request({
url: '/auth/register',
method: 'post',
data: userData
})
}
export const logout = () => {
return request({
url: '/auth/logout',
method: 'post'
})
}
// api/user.js
import request from './request'
export const getUserList = (params) => {
return request({
url: '/users',
method: 'get',
params
})
}
export const getUserById = (id) => {
return request({
url: `/users/${id}`,
method: 'get'
})
}
export const createUser = (userData) => {
return request({
url: '/users',
method: 'post',
data: userData
})
}
export const updateUser = (id, userData) => {
return request({
url: `/users/${id}`,
method: 'put',
data: userData
})
}
export const deleteUser = (id) => {
return request({
url: `/users/${id}`,
method: 'delete'
})
}4. 页面组件
登录页面
vue
<template>
<div class="login-container">
<div class="login-form">
<h2>登录</h2>
<el-form :model="loginForm" :rules="rules" ref="loginFormRef">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" :loading="loading">登录</el-button>
<el-button @click="$router.push('/register')">注册</el-button>
</el-form-item>
</el-form>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../../stores/auth'
const router = useRouter()
const authStore = useAuthStore()
const loginFormRef = ref(null)
const loading = ref(false)
const error = ref('')
const loginForm = reactive({
username: '',
password: ''
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
async function handleLogin() {
if (!loginFormRef.value) return
await loginFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
error.value = ''
const success = await authStore.login(loginForm.username, loginForm.password)
if (success) {
router.push('/user/list')
} else {
error.value = '登录失败,请检查用户名和密码'
}
loading.value = false
}
})
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f5f5;
}
.login-form {
width: 400px;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.login-form h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.error-message {
margin-top: 10px;
color: red;
text-align: center;
}
</style>用户列表页面
vue
<template>
<div class="user-list">
<div class="header">
<h2>用户管理</h2>
<el-button type="primary" @click="$router.push('/user/create')">新增用户</el-button>
</div>
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索用户名或邮箱"
style="width: 300px"
@keyup.enter="handleSearch"
>
<template #append>
<el-button @click="handleSearch"><el-icon><Search /></el-icon></el-button>
</template>
</el-input>
</div>
<el-table :data="userStore.users" style="width: 100%" v-loading="userStore.loading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="viewUser(scope.row.id)">查看</el-button>
<el-button size="small" type="primary" @click="editUser(scope.row.id)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteUser(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="userStore.pagination.page"
v-model:page-size="userStore.pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userStore.pagination.total"
@size-change="userStore.setPageSize"
@current-change="userStore.setPage"
></el-pagination>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '../../stores/user'
import { Search } from '@element-plus/icons-vue'
const router = useRouter()
const userStore = useUserStore()
const searchQuery = ref('')
onMounted(() => {
userStore.fetchUsers()
})
function handleSearch() {
userStore.fetchUsers({ keyword: searchQuery.value })
}
function viewUser(id) {
router.push(`/user/detail/${id}`)
}
function editUser(id) {
router.push(`/user/edit/${id}`)
}
function deleteUser(id) {
ElMessageBox.confirm('确定要删除这个用户吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const success = await userStore.removeUser(id)
if (success) {
ElMessage.success('删除成功')
} else {
ElMessage.error('删除失败')
}
})
.catch(() => {
// 取消删除
})
}
</script>
<style scoped>
.user-list {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #333;
}
.search-bar {
margin-bottom: 20px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>5. 布局组件
vue
<template>
<div class="layout">
<el-container>
<el-aside width="200px">
<div class="logo">
<h3>用户管理系统</h3>
</div>
<el-menu :default-active="activeMenu" class="el-menu-vertical-demo">
<el-menu-item index="/user/list">
<el-icon><User /></el-icon>
<span>用户列表</span>
</el-menu-item>
<el-menu-item index="/user/create">
<el-icon><Plus /></el-icon>
<span>新增用户</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div class="header-right">
<span>{{ authStore.user?.username }}</span>
<el-button @click="handleLogout">退出登录</el-button>
</div>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../../stores/auth'
import { User, Plus } from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const activeMenu = computed(() => {
return route.path
})
function handleLogout() {
authStore.logout()
router.push('/login')
}
</script>
<style scoped>
.layout {
height: 100vh;
overflow: hidden;
}
.logo {
padding: 20px;
background-color: #409eff;
color: white;
text-align: center;
}
.logo h3 {
margin: 0;
font-size: 18px;
}
.el-header {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 20px;
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.header-right {
display: flex;
align-items: center;
gap: 10px;
}
.el-main {
padding: 20px;
overflow-y: auto;
}
</style>项目构建与部署
1. 构建项目
bash
npm run build2. 部署项目
可以将构建后的 dist 目录部署到任何静态文件服务器,如 Nginx、Apache 等。
Nginx 配置示例
nginx
server {
listen 80;
server_name example.com;
location / {
root /path/to/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
}技术要点
项目架构:采用模块化、组件化的架构设计,便于维护和扩展。
状态管理:使用 Pinia 管理全局状态,包括用户认证和用户数据。
路由管理:使用 Vue Router 实现页面导航和路由守卫。
API 调用:封装 Axios 实现 API 调用,添加请求和响应拦截器。
UI 组件:使用 Element Plus 构建美观的用户界面。
表单验证:使用 Element Plus 的表单验证功能。
分页功能:实现数据分页和搜索功能。
权限控制:通过路由守卫实现基本的权限控制。
代码规范:使用 ESLint 和 Prettier 保证代码质量。
通过这个企业级项目,我们学习了如何使用 Vue3 及其生态系统构建一个完整的应用,包括项目初始化、架构设计、核心功能实现和部署等。这为我们未来开发更复杂的 Vue 应用打下了坚实的基础。
