Vue 3 + Element Plus 后台管理界面开发:从零搭建完整管理后台

举报
yd_228838069 发表于 2026/11/03 09:16:10 2026/11/03
【摘要】 在前面的教程中,我们已经使用 FastAPI 和 Vue 3 实现了前后端分离的用户管理系统,完成了用户列表查询、新增、编辑和删除等基础功能。不过,目前的页面仍然比较简单,缺少实际后台管理系统常见的布局和交互组件。真实的后台管理系统通常包含:左侧导航菜单顶部导航栏面包屑导航用户信息区域数据表格搜索与筛选分页组件新增和编辑弹窗多页面路由管理本篇将使用 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')

这里完成了三项工作:

  1. 创建 Vue 应用。

  2. 注册 Element Plus 组件库。

  3. 注册 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>

这里实现了完整的用户管理交互:

  1. 用户列表查询。

  2. 用户名搜索。

  3. 搜索条件重置。

  4. 新增用户。

  5. 编辑用户。

  6. 删除用户。

  7. 表单验证。

  8. 分页查询。

  9. 加载状态提示。

需要注意,页面请求中的 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

可以按照以下顺序检查:

  1. 进入首页,检查顶部导航和侧边菜单。

  2. 点击用户管理,检查页面路由切换。

  3. 查看用户列表,确认 API 数据正常加载。

  4. 输入用户名,测试搜索。

  5. 点击新增用户,测试表单提交。

  6. 点击编辑,修改用户信息。

  7. 点击删除,测试二次确认和删除接口。

  8. 切换分页,检查列表数据是否正确变化。

如果前端页面正常显示,但用户列表无法加载,应优先检查 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 可能会尝试查找对应的物理文件。

十四、生产环境还需要完善哪些功能?

当前完成的是一个基础后台管理界面,正式项目还需要考虑:

  1. 登录认证: 接入 JWT 登录,并根据认证状态管理前端路由。

  2. 权限控制: 根据用户角色动态展示菜单,同时由 FastAPI 在服务端执行权限校验。

  3. Token 管理: 处理 Token 过期、续期和退出登录,避免敏感凭据泄露。

  4. 菜单管理: 支持多级菜单、动态路由和菜单权限。

  5. 操作日志: 记录重要管理操作,便于问题追踪。

  6. 全局异常处理: 统一处理网络错误、接口异常和登录失效。

  7. 表格优化: 对大数据量列表采用服务端分页和筛选。

  8. 组件复用: 将搜索栏、分页器、表单弹窗等抽取成公共组件。

  9. 生产构建: 使用 npm run build 生成静态文件,再交由 Nginx 部署。

  10. 安全配置: 保护管理接口、限制跨域来源,并避免将敏感密钥写入前端代码。

十五、总结

本篇使用 Vue 3、Element Plus、Vue Router 和 Axios,搭建了一个具有完整基础布局的后台管理界面。

与上一篇相比,本篇进一步完成了:

  • 后台整体布局。

  • 侧边栏与顶部导航。

  • 页面路由管理。

  • 用户数据表格。

  • 搜索与分页。

  • 新增、编辑和删除弹窗。

  • Axios 统一请求管理。

  • 前后端 API 联调。

至此,我们已经具备了开发一个基础前后端分离管理系统的前端能力。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。