Vue 3 + Element Plus 后台管理界面开发:从零搭建完整管理后台
在前面的教程中,我们已经使用 FastAPI 和 Vue 3 实现了前后端分离的用户管理系统,完成了用户列表查询、新增、编辑和删除等基础功能。
不过,目前的页面仍然比较简单,缺少实际后台管理系统常见的布局和交互组件。
真实的后台管理系统通常包含:
-
左侧导航菜单
-
顶部导航栏
-
面包屑导航
-
用户信息区域
-
数据表格
-
搜索与筛选
-
分页组件
-
新增和编辑弹窗
-
多页面路由管理
本篇将使用 Vue 3、Element Plus 和 Vue Router,从零搭建一个完整的后台管理界面,并继续沿用上一篇的 FastAPI 用户管理 API。
一、技术栈介绍
本篇使用以下技术:
| 技术 | 主要用途 |
|---|---|
| Vue 3 | 前端框架 |
| Vite | 项目构建工具 |
| Element Plus | UI 组件库 |
| Vue Router | 前端路由管理 |
| Axios | HTTP 请求 |
| JavaScript | 业务逻辑开发 |
Element Plus 是 Vue 3 生态中常用的组件库,提供表格、表单、菜单、分页、弹窗、消息提示等常用组件。
二、创建 Vue 3 项目
如果已经完成上一篇的前端项目,可以直接在原项目中继续开发。
如果是新项目,执行:
npm create vite@latest admin-frontend -- --template vue
cd admin-frontend
npm install
安装 Element Plus、Vue Router 和 Axios:
npm install element-plus vue-router axios
项目基础目录如下:
admin-frontend/
├── public/
├── src/
│ ├── api/
│ │ └── request.js
│ ├── components/
│ ├── layouts/
│ │ └── AdminLayout.vue
│ ├── router/
│ │ └── index.js
│ ├── views/
│ │ ├── Dashboard.vue
│ │ ├── UserList.vue
│ │ └── NotFound.vue
│ ├── App.vue
│ └── main.js
├── index.html
├── package.json
└── vite.config.js
目录说明:
-
api/:统一管理后端 API 请求。 -
components/:存放可复用组件。 -
layouts/:存放后台整体布局。 -
router/:管理页面路由。 -
views/:存放各个业务页面。 -
main.js:前端应用入口。
三、引入 Element Plus
编辑 src/main.js:
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')
这里完成了三项工作:
-
创建 Vue 应用。
-
注册 Element Plus 组件库。
-
注册 Vue Router 路由管理器。
之后就可以在 Vue 页面中使用 el-table、el-button、el-dialog 等组件。
四、配置 Axios 请求
实际项目中,不建议在每个页面重复编写 HTTP 请求代码。
可以创建统一请求文件,对 API 地址、超时时间和异常进行集中管理。
创建 src/api/request.js:
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
request.interceptors.request.use(
config => {
// 后续可以在这里添加 JWT Token
return config
},
error => Promise.reject(error)
)
// 响应拦截器
request.interceptors.response.use(
response => response.data,
error => {
const message =
error.response?.data?.detail ||
error.message ||
'网络请求失败'
ElMessage.error(message)
return Promise.reject(error)
}
)
export default request
这样,其他页面只需要导入 request,就可以调用后端 API。
例如:
const result = await request.get('/users')
Axios 会自动将请求发送到配置的 API 地址。
五、配置 Vite 代理
编辑 vite.config.js:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
host: '127.0.0.1',
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
},
},
})
开发环境下:
Vue 3: http://127.0.0.1:5173
FastAPI:http://127.0.0.1:8000
前端请求 /api/users 时,Vite 会将请求转发到 FastAPI。
注意:Vite 代理仅用于开发环境,正式部署时需要配置 Nginx 等反向代理。
六、创建后台管理布局
后台管理系统通常采用左侧菜单、顶部导航和右侧内容区域的布局。
创建 src/layouts/AdminLayout.vue:
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const collapsed = ref(false)
const menus = [
{
title: '首页概览',
path: '/dashboard',
icon: 'Odometer',
},
{
title: '用户管理',
path: '/users',
icon: 'User',
},
]
function navigate(path) {
router.push(path)
}
</script>
<template>
<el-container class="admin-layout">
<el-aside
:width="collapsed ? '64px' : '220px'"
class="sidebar"
>
<div class="logo">
{{ collapsed ? '后台' : '管理系统' }}
</div>
<el-menu
:default-active="route.path"
:collapse="collapsed"
background-color="#1f2937"
text-color="#cbd5e1"
active-text-color="#ffffff"
@select="navigate"
>
<el-menu-item
v-for="item in menus"
:key="item.path"
:index="item.path"
>
<el-icon>
<component :is="$icons[item.icon]" />
</el-icon>
<template #title>{{ item.title }}</template>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="topbar">
<div class="topbar-left">
<el-button text @click="collapsed = !collapsed">
<el-icon>
<Fold v-if="!collapsed" />
<Expand v-else />
</el-icon>
</el-button>
<el-breadcrumb separator="/">
<el-breadcrumb-item>后台管理</el-breadcrumb-item>
<el-breadcrumb-item>
{{ route.meta.title || '页面' }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-dropdown>
<span class="user-info">
管理员
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人资料</el-dropdown-item>
<el-dropdown-item divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<style scoped>
.admin-layout {
min-height: 100vh;
}
.sidebar {
background: #1f2937;
transition: width 0.2s;
overflow: hidden;
}
.logo {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 20px;
font-weight: bold;
}
.sidebar :deep(.el-menu) {
border-right: none;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ebeef5;
background: white;
}
.topbar-left {
display: flex;
align-items: center;
gap: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.main-content {
background: #f5f7fa;
padding: 24px;
}
</style>
这里实现了:
-
左侧菜单。
-
菜单折叠。
-
顶部导航。
-
面包屑。
-
管理员下拉菜单。
-
右侧路由内容区域。
注意:上面菜单中的图标还需要正确注册,下一节会进行配置。
七、注册 Element Plus 图标
安装图标依赖:
npm install @element-plus/icons-vue
编辑 src/main.js,增加图标注册:
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
同时,将布局文件中:
<component :is="$icons[item.icon]" />
替换为更简单的方式:
<el-icon>
<Odometer v-if="item.icon === 'Odometer'" />
<User v-else-if="item.icon === 'User'" />
</el-icon>
这样就可以正常显示菜单图标。
八、配置 Vue Router
创建 src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router'
import AdminLayout from '../layouts/AdminLayout.vue'
import Dashboard from '../views/Dashboard.vue'
import UserList from '../views/UserList.vue'
import NotFound from '../views/NotFound.vue'
const routes = [
{
path: '/',
component: AdminLayout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: Dashboard,
meta: {
title: '首页概览',
},
},
{
path: 'users',
component: UserList,
meta: {
title: '用户管理',
},
},
],
},
{
path: '/:pathMatch(.*)*',
component: NotFound,
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
Vue Router 负责管理页面地址。
例如:
| 路径 | 页面 |
|---|---|
/dashboard |
首页概览 |
/users |
用户管理 |
| 其他不存在的路径 | 404 页面 |
九、创建首页概览
创建 src/views/Dashboard.vue:
<script setup>
import { ref } from 'vue'
const statistics = ref([
{
title: '用户总数',
value: 1280,
icon: 'User',
},
{
title: '今日新增',
value: 36,
icon: 'Plus',
},
{
title: '活跃用户',
value: 896,
icon: 'TrendCharts',
},
{
title: '待处理任务',
value: 12,
icon: 'Tickets',
},
])
</script>
<template>
<div>
<h2>首页概览</h2>
<el-row :gutter="20">
<el-col
v-for="item in statistics"
:key="item.title"
:xs="24"
:sm="12"
:md="6"
>
<el-card shadow="hover" class="stat-card">
<div class="stat-title">{{ item.title }}</div>
<div class="stat-value">{{ item.value }}</div>
</el-card>
</el-col>
</el-row>
<el-card class="welcome-card">
<h3>欢迎使用管理系统</h3>
<p>这里可以放置系统公告、操作提示和近期活动。</p>
</el-card>
</div>
</template>
<style scoped>
.stat-card {
margin-bottom: 20px;
}
.stat-title {
color: #909399;
font-size: 14px;
}
.stat-value {
margin-top: 14px;
font-size: 28px;
font-weight: bold;
}
.welcome-card {
margin-top: 10px;
}
</style>
这里使用 el-row 和 el-col 实现响应式布局,使用 el-card 显示统计信息。
示例中的统计数字是静态演示数据。实际系统应通过 FastAPI 统计接口获取。
十、开发用户管理页面
这是本篇的核心部分。
创建 src/views/UserList.vue:
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../api/request'
const users = ref([])
const loading = ref(false)
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const keyword = ref('')
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = reactive({
id: null,
username: '',
email: '',
is_active: true,
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '用户名长度为 2 到 50 个字符', trigger: 'blur' },
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
],
}
// 获取用户列表
async function loadUsers() {
loading.value = true
try {
const result = await request.get('/users', {
params: {
page: page.value,
page_size: pageSize.value,
keyword: keyword.value || undefined,
},
})
users.value = result.items
total.value = result.total
} finally {
loading.value = false
}
}
// 搜索
function searchUsers() {
page.value = 1
loadUsers()
}
// 重置搜索
function resetSearch() {
keyword.value = ''
page.value = 1
loadUsers()
}
// 新增用户
function openCreate() {
isEdit.value = false
Object.assign(form, {
id: null,
username: '',
email: '',
is_active: true,
})
dialogVisible.value = true
}
// 编辑用户
function openEdit(row) {
isEdit.value = true
Object.assign(form, {
id: row.id,
username: row.username,
email: row.email,
is_active: row.is_active,
})
dialogVisible.value = true
}
// 保存用户
async function saveUser() {
await formRef.value.validate()
const data = {
username: form.username,
email: form.email,
is_active: form.is_active,
}
if (isEdit.value) {
await request.put(`/users/${form.id}`, data)
ElMessage.success('修改成功')
} else {
await request.post('/users', data)
ElMessage.success('新增成功')
}
dialogVisible.value = false
loadUsers()
}
// 删除用户
async function deleteUser(row) {
try {
await ElMessageBox.confirm(
`确定删除用户 ${row.username} 吗?`,
'删除确认',
{
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
}
)
await request.delete(`/users/${row.id}`)
ElMessage.success('删除成功')
loadUsers()
} catch (error) {
if (error !== 'cancel' && error !== 'close') {
ElMessage.error('删除失败')
}
}
}
// 分页
function handlePageChange(value) {
page.value = value
loadUsers()
}
function handleSizeChange(value) {
pageSize.value = value
page.value = 1
loadUsers()
}
onMounted(loadUsers)
</script>
<template>
<div class="user-page">
<div class="page-header">
<h2>用户管理</h2>
<el-button type="primary" @click="openCreate">
新增用户
</el-button>
</div>
<el-card shadow="never">
<div class="search-bar">
<el-input
v-model="keyword"
placeholder="请输入用户名"
clearable
@keyup.enter="searchUsers"
/>
<el-button type="primary" @click="searchUsers">
搜索
</el-button>
<el-button @click="resetSearch">
重置
</el-button>
</div>
<el-table
v-loading="loading"
:data="users"
border
stripe
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column
prop="username"
label="用户名"
min-width="150"
/>
<el-table-column
prop="email"
label="邮箱"
min-width="220"
/>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'danger'">
{{ row.is_active ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
link
@click="openEdit(row)"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="deleteUser(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</el-card>
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑用户' : '新增用户'"
width="500px"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="90px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="form.is_active" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
取消
</el-button>
<el-button type="primary" @click="saveUser">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.user-page {
width: 100%;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
}
.search-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-bar .el-input {
max-width: 280px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
@media (max-width: 640px) {
.search-bar {
flex-wrap: wrap;
}
.pagination {
justify-content: flex-start;
overflow-x: auto;
}
}
</style>
这里实现了完整的用户管理交互:
-
用户列表查询。
-
用户名搜索。
-
搜索条件重置。
-
新增用户。
-
编辑用户。
-
删除用户。
-
表单验证。
-
分页查询。
-
加载状态提示。
需要注意,页面请求中的 keyword 参数要求后端支持相应的搜索功能。如果上一篇的 FastAPI 接口尚未实现该参数,应先在后端增加搜索逻辑。
十一、创建 404 页面
创建 src/views/NotFound.vue:
<template>
<div class="not-found">
<h1>404</h1>
<p>抱歉,您访问的页面不存在。</p>
<router-link to="/dashboard">
返回首页
</router-link>
</div>
</template>
<style scoped>
.not-found {
text-align: center;
padding: 100px 20px;
}
.not-found h1 {
font-size: 80px;
color: #409eff;
}
.not-found p {
color: #909399;
}
</style>
这样,当用户访问不存在的路由时,就会显示统一的错误页面。
十二、运行项目
确保 FastAPI 后端已经启动。
在前端项目目录执行:
npm run dev
浏览器打开:
http://127.0.0.1:5173
可以按照以下顺序检查:
-
进入首页,检查顶部导航和侧边菜单。
-
点击用户管理,检查页面路由切换。
-
查看用户列表,确认 API 数据正常加载。
-
输入用户名,测试搜索。
-
点击新增用户,测试表单提交。
-
点击编辑,修改用户信息。
-
点击删除,测试二次确认和删除接口。
-
切换分页,检查列表数据是否正确变化。
如果前端页面正常显示,但用户列表无法加载,应优先检查 FastAPI 是否启动、Vite 代理地址是否正确,以及后端是否返回了预期的数据结构。
十三、常见问题
1. 页面提示 Network Error
常见原因:
-
FastAPI 服务没有启动。
-
后端端口配置错误。
-
Vite 代理目标地址错误。
-
后端请求超时。
可以先直接访问后端接口,确认 API 是否正常响应。
2. 页面显示 404
检查:
-
FastAPI 路由是否存在。
-
Axios 的
baseURL是否配置正确。 -
请求路径是否重复添加了
/api。 -
Vite 代理是否正确转发。
例如,baseURL 已经是 /api 时,应请求:
request.get('/users')
而不是:
request.get('/api/users')
3. 分页组件没有正确刷新
检查前端发送的分页参数是否与后端一致。
本篇使用:
page
page_size
后端也必须使用相同参数名称。
4. 表格没有数据
检查接口返回格式。
本篇要求后端返回:
{
"items": [],
"total": 0,
"page": 1,
"page_size": 10
}
如果后端返回的是数组,前端就不能直接使用 result.items。
5. 页面刷新后提示 404
使用 createWebHistory() 时,生产环境需要配置服务器将前端路由回退到 index.html。
例如 Nginx:
location / {
try_files $uri $uri/ /index.html;
}
否则直接刷新 /users 等前端路由时,Nginx 可能会尝试查找对应的物理文件。
十四、生产环境还需要完善哪些功能?
当前完成的是一个基础后台管理界面,正式项目还需要考虑:
-
登录认证: 接入 JWT 登录,并根据认证状态管理前端路由。
-
权限控制: 根据用户角色动态展示菜单,同时由 FastAPI 在服务端执行权限校验。
-
Token 管理: 处理 Token 过期、续期和退出登录,避免敏感凭据泄露。
-
菜单管理: 支持多级菜单、动态路由和菜单权限。
-
操作日志: 记录重要管理操作,便于问题追踪。
-
全局异常处理: 统一处理网络错误、接口异常和登录失效。
-
表格优化: 对大数据量列表采用服务端分页和筛选。
-
组件复用: 将搜索栏、分页器、表单弹窗等抽取成公共组件。
-
生产构建: 使用
npm run build生成静态文件,再交由 Nginx 部署。 -
安全配置: 保护管理接口、限制跨域来源,并避免将敏感密钥写入前端代码。
十五、总结
本篇使用 Vue 3、Element Plus、Vue Router 和 Axios,搭建了一个具有完整基础布局的后台管理界面。
与上一篇相比,本篇进一步完成了:
-
后台整体布局。
-
侧边栏与顶部导航。
-
页面路由管理。
-
用户数据表格。
-
搜索与分页。
-
新增、编辑和删除弹窗。
-
Axios 统一请求管理。
-
前后端 API 联调。
至此,我们已经具备了开发一个基础前后端分离管理系统的前端能力。
- 点赞
- 收藏
- 关注作者
评论(0)