Vue 3 后台管理系统前端骨架小案例1.0版本

📋 文章摘要

本文详细介绍了基于 Vue 3 的后台管理系统前端项目实现,涵盖以下核心内容:

🎯 核心功能

  • 路由嵌套架构:使用 ParentLayout.vue 实现 /user/order 模块的嵌套路由
  • 动态菜单系统MenuTree.vue 组件递归渲染路由配置,支持无限级嵌套
  • 响应式布局MainLayout.vue 提供侧边栏、面包屑导航和内容区域分离布局
  • 完整页面示例:包含仪表盘、用户管理、订单管理、系统设置等典型后台页面

🛠️ 技术栈

  • Vue 3 + Vue Router
  • Composition API (<script setup>)
  • TypeScript 类型支持 (env.d.ts)
  • 模块化 CSS (Scoped Styles)

📁 项目结构

  • router/ - 路由配置(支持嵌套路由、元数据驱动)
  • layout/ - 布局组件(主布局、菜单树、父级路由出口)
  • views/ - 页面组件(5个典型后台页面)
  • 完整的样式设计和交互🚀 快速开始
  • 一键运行:bash npm install && npm run dev dev`
  • 访问 http://localhost:5173 即可体验完整功能

💡 扩展性
文章最后提供了权限控制、路由懒加载、面包屑组件封装、页面切换动画、状态管理等5个扩展思路,方便项目后续演进。


在这里插入图片描述

在这里插入图片描述

下面是完整的可运行代码,复制即可使用。


📁 完整项目结构

src/
├── router/
│   └── index.js
├── layout/
│   ├── MainLayout.vue
│   ├── MenuTree.vue
│   └── ParentLayout.vue          // 父级路由出口组件
├── views/
│   ├── Dashboard.vue
│   ├── UserList.vue
│   ├── UserDetail.vue
│   ├── OrderList.vue
│   └── Settings.vue
├── App.vue
├── main.js
└── env.d.ts

1. src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
import UserList from '../views/UserList.vue'
import UserDetail from '../views/UserDetail.vue'
import OrderList from '../views/OrderList.vue'
import Settings from '../views/Settings.vue'
import ParentLayout from '../layout/ParentLayout.vue'

const routes = [
  {
    path: '/',
    redirect: '/dashboard'
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { title: '仪表盘', icon: '📊' }
  },
  {
    path: '/user',
    component: ParentLayout,
    meta: { title: '用户管理', icon: '👤' },
    children: [
      {
        path: '/user/list',         // 绝对路径
        name: 'UserList',
        component: UserList,
        meta: { title: '用户列表', icon: '📋' }
      },
      {
        path: '/user/detail/:id?',  // 绝对路径
        name: 'UserDetail',
        component: UserDetail,
        meta: { title: '用户详情', icon: '📄' }
      }
    ]
  },
  {
    path: '/order',
    component: ParentLayout,
    meta: { title: '订单管理', icon: '📦' },
    children: [
      {
        path: '/order/list',        // 绝对路径
        name: 'OrderList',
        component: OrderList,
        meta: { title: '订单列表', icon: '📋' }
      }
    ]
  },
  {
    path: '/settings',
    name: 'Settings',
    component: Settings,
    meta: { title: '系统设置', icon: '⚙️' }
  }
]

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

export default router
export { routes }

2. src/layout/MainLayout.vue

<template>
  <div class="app-container">
    <!-- 侧边栏 -->
    <aside class="sidebar">
      <div class="logo">✨ 后台系统</div>
      <nav class="menu">
        <MenuTree :routes="menuRoutes" />
      </nav>
    </aside>

    <!-- 主区域 -->
    <main class="main">
      <header class="header">
        <div class="breadcrumb">
          <span v-for="(crumb, index) in breadcrumbs" :key="index">
            {{ crumb }}
            <span v-if="index < breadcrumbs.length - 1" class="separator"> / </span>
          </span>
        </div>
        <div class="user-info">
          <span class="avatar">👤</span>
          <span class="name">管理员</span>
        </div>
      </header>
      <section class="content">
        <router-view />
      </section>
    </main>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { routes } from '@/router'
import MenuTree from './MenuTree.vue'

const route = useRoute()

const menuRoutes = computed(() => {
  return routes.filter(r => r.path !== '/' && r.meta?.title)
})

const breadcrumbs = computed(() => {
  return route.matched.map(m => m.meta?.title).filter(Boolean)
})
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}

.app-container {
  display: flex;
  height: 100vh;
  background: #f0f2f5;
}

/* ===== 侧边栏 ===== */
.sidebar {
  width: 240px;
  background: #2d3a4b;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}

.logo {
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  font-weight: bold;
  color: #fff;
  border-bottom: 1px solid #1f2d3d;
  background: #1f2d3d;
  letter-spacing: 2px;
}

.menu {
  flex: 1;
  overflow-y: auto;
  padding: 8px 0;
}

/* ===== 主区域 ===== */
.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.header {
  height: 60px;
  background: #fff;
  border-bottom: 1px solid #e4e7ed;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  flex-shrink: 0;
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
}

.breadcrumb {
  font-size: 14px;
  color: #606266;
}
.breadcrumb .separator {
  margin: 0 4px;
  color: #c0c4cc;
}
.breadcrumb span:last-child {
  color: #409eff;
  font-weight: 500;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 8px;
}
.user-info .avatar {
  font-size: 24px;
}
.user-info .name {
  font-size: 14px;
  color: #303133;
}

.content {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
  background: #f0f2f5;
}

/* ===== 滚动条美化 ===== */
.menu::-webkit-scrollbar,
.content::-webkit-scrollbar {
  width: 4px;
}
.menu::-webkit-scrollbar-thumb {
  background: #4a5a6e;
  border-radius: 4px;
}
.content::-webkit-scrollbar-thumb {
  background: #c0c4cc;
  border-radius: 4px;
}
</style>

3. src/layout/MenuTree.vue

<template>
  <ul class="menu-tree">
    <template v-for="route in routes" :key="route.path">
      <li v-if="route.children && route.children.length > 0" class="menu-item">
        <div class="menu-label" @click="toggle(route.path)">
          <span class="icon">{{ route.meta?.icon }}</span>
          <span class="title">{{ route.meta?.title }}</span>
          <span class="arrow" :class="{ open: isOpen(route.path) }">▶</span>
        </div>
        <ul class="sub-menu" v-show="isOpen(route.path)">
          <MenuTree :routes="route.children" />
        </ul>
      </li>
      <li v-else class="menu-item">
        <!-- 使用 route.path(绝对路径) -->
        <router-link :to="route.path" class="menu-label" active-class="active">
          <span class="icon">{{ route.meta?.icon }}</span>
          <span class="title">{{ route.meta?.title }}</span>
        </router-link>
      </li>
    </template>
  </ul>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

const props = defineProps({
  routes: {
    type: Array,
    required: true
  }
})

const route = useRoute()
const openMap = ref({})

// 根据当前路由自动展开父级
function initOpenState() {
  const matched = route.matched
  matched.forEach(m => {
    if (m.children && m.children.length > 0) {
      openMap.value[m.path] = true
    }
  })
}
initOpenState()

function toggle(path) {
  openMap.value[path] = !openMap.value[path]
}

function isOpen(path) {
  return !!openMap.value[path]
}

// 路由变化时自动展开父级
watch(() => route.path, () => {
  const matched = route.matched
  matched.forEach(m => {
    if (m.children && m.children.length > 0) {
      openMap.value[m.path] = true
    }
  })
}, { immediate: true })
</script>

<style scoped>
.menu-tree {
  list-style: none;
  padding: 0;
  margin: 0;
}

.menu-item {
  font-size: 14px;
  line-height: 1.5;
}

.menu-label {
  display: flex;
  align-items: center;
  padding: 0 20px;
  height: 44px;
  color: #bfcbd9;
  text-decoration: none;
  cursor: pointer;
  transition: all 0.3s;
  position: relative;
}
.menu-label:hover {
  background: #1f2d3d;
  color: #fff;
}

.menu-label .icon {
  width: 20px;
  margin-right: 10px;
  text-align: center;
  font-size: 16px;
}
.menu-label .title {
  flex: 1;
}
.menu-label .arrow {
  font-size: 12px;
  transition: transform 0.3s;
  margin-left: 8px;
}
.menu-label .arrow.open {
  transform: rotate(90deg);
}

/* 激活的菜单项 */
.router-link-active {
  background: #1f2d3d;
  color: #fff;
  border-right: 3px solid #409eff;
}

/* 子菜单缩进 */
.sub-menu {
  list-style: none;
  padding: 0;
  margin: 0;
  background: #1f2d3d;
}
.sub-menu .menu-label {
  padding-left: 50px;
}
.sub-menu .sub-menu .menu-label {
  padding-left: 70px;
}
</style>

4. src/layout/ParentLayout.vue

<template>
  <router-view />
</template>

5. src/views/Dashboard.vue

<template>
  <div>
    <h2 style="margin-bottom: 20px;">📊 仪表盘</h2>
    <div class="card-grid">
      <div class="card" v-for="i in 4" :key="i">
        <div class="card-title">数据 {{ i }}</div>
        <div class="card-value">{{ Math.floor(Math.random() * 1000) }}</div>
        <div class="card-desc">较昨日 +{{ Math.floor(Math.random() * 10) }}%</div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
}
.card {
  background: #fff;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.card-title {
  color: #909399;
  font-size: 14px;
}
.card-value {
  font-size: 28px;
  font-weight: 600;
  margin: 10px 0;
}
.card-desc {
  color: #67c23a;
  font-size: 13px;
}
</style>

6. src/views/UserList.vue

<template>
  <div>
    <h2>👥 用户列表</h2>
    <table class="table">
      <thead>
        <tr><th>ID</th><th>姓名</th><th>角色</th><th>状态</th></tr>
      </thead>
      <tbody>
        <tr v-for="user in users" :key="user.id">
          <td>{{ user.id }}</td>
          <td>{{ user.name }}</td>
          <td>{{ user.role }}</td>
          <td>
            <span class="tag" :class="user.status === 'active' ? 'tag-success' : 'tag-danger'">
              {{ user.status === 'active' ? '启用' : '禁用' }}
            </span>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
const users = [
  { id: 1, name: '张三', role: '管理员', status: 'active' },
  { id: 2, name: '李四', role: '普通用户', status: 'active' },
  { id: 3, name: '王五', role: '访客', status: 'inactive' }
]
</script>

<style scoped>
.table {
  width: 100%;
  border-collapse: collapse;
  background: #fff;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  margin-top: 20px;
}
.table th {
  background: #f5f7fa;
  color: #303133;
  font-weight: 600;
  padding: 12px 16px;
  text-align: left;
}
.table td {
  padding: 12px 16px;
  border-bottom: 1px solid #ebeef5;
}
.table tr:hover td {
  background: #f5f7fa;
}
.tag {
  display: inline-block;
  padding: 2px 12px;
  border-radius: 4px;
  font-size: 12px;
}
.tag-success {
  background: #e1f3d8;
  color: #67c23a;
}
.tag-danger {
  background: #fde2e2;
  color: #f56c6c;
}
</style>

7. src/views/UserDetail.vue

<template>
  <div>
    <h2>📄 用户详情</h2>
    <div style="background:#fff; padding:20px; border-radius:8px; margin-top:20px; box-shadow:0 2px 12px rgba(0,0,0,0.06);">
      <p><strong>用户 ID:</strong>{{ $route.params.id || '无' }}</p>
      <p><strong>姓名:</strong>张三</p>
      <p><strong>邮箱:</strong>zhangsan@example.com</p>
      <p><strong>角色:</strong>管理员</p>
      <p><strong>注册时间:</strong>2024-01-15</p>
    </div>
  </div>
</template>

8. src/views/OrderList.vue

<template>
  <div>
    <h2>📦 订单列表</h2>
    <table class="table">
      <thead>
        <tr><th>订单号</th><th>金额</th><th>状态</th><th>下单时间</th></tr>
      </thead>
      <tbody>
        <tr v-for="order in orders" :key="order.id">
          <td>{{ order.id }}</td>
          <td>¥{{ order.amount.toFixed(2) }}</td>
          <td>
            <span class="tag" :class="{
              'tag-success': order.status === '已完成',
              'tag-warning': order.status === '配送中',
              'tag-danger': order.status === '已取消'
            }">
              {{ order.status }}
            </span>
          </td>
          <td>{{ order.time }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
const orders = [
  { id: 'ORD-001', amount: 199.00, status: '已完成', time: '2024-06-20 14:30' },
  { id: 'ORD-002', amount: 299.50, status: '配送中', time: '2024-06-22 09:15' },
  { id: 'ORD-003', amount: 59.90, status: '已取消', time: '2024-06-21 16:40' }
]
</script>

<style scoped>
.table {
  width: 100%;
  border-collapse: collapse;
  background: #fff;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  margin-top: 20px;
}
.table th {
  background: #f5f7fa;
  color: #303133;
  font-weight: 600;
  padding: 12px 16px;
  text-align: left;
}
.table td {
  padding: 12px 16px;
  border-bottom: 1px solid #ebeef5;
}
.table tr:hover td {
  background: #f5f7fa;
}
.tag {
  display: inline-block;
  padding: 2px 12px;
  border-radius: 4px;
  font-size: 12px;
}
.tag-success {
  background: #e1f3d8;
  color: #67c23a;
}
.tag-warning {
  background: #fdf6ec;
  color: #e6a23c;
}
.tag-danger {
  background: #fde2e2;
  color: #f56c6c;
}
</style>

9. src/views/Settings.vue

<template>
  <div>
    <h2>⚙️ 系统设置</h2>
    <div style="background:#fff; padding:24px; border-radius:8px; margin-top:20px; box-shadow:0 2px 12px rgba(0,0,0,0.06);">
      <div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid #ebeef5;">
        <label style="display:inline-block; width:100px; color:#606266;">网站名称</label>
        <input type="text" value="后台管理系统" style="padding:8px 12px; border:1px solid #dcdfe6; border-radius:4px; width:300px;">
      </div>
      <div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid #ebeef5;">
        <label style="display:inline-block; width:100px; color:#606266;">登录超时</label>
        <input type="number" value="30" style="padding:8px 12px; border:1px solid #dcdfe6; border-radius:4px; width:100px;"> 分钟
      </div>
      <div>
        <button style="padding:8px 24px; background:#409eff; color:#fff; border:none; border-radius:4px; cursor:pointer;">保存设置</button>
      </div>
    </div>
  </div>
</template>

10. src/App.vue

<template>
  <MainLayout />
</template>

<script setup>
import MainLayout from './layout/MainLayout.vue'
</script>

11. src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

12. src/env.d.ts

/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

🚀 运行

npm install
npm run dev

访问 http://localhost:5173,点击左侧菜单即可看到对应的页面内容。


💡 项目总结与扩展思路

项目核心要点总结

  1. 路由嵌套架构清晰:通过 ParentLayout.vue 作为父级路由出口,实现了 /user/order 等模块的嵌套路由结构,使代码组织更加模块化。

  2. 菜单动态生成MenuTree.vue 组件递归渲染路由配置,支持无限级嵌套菜单,并实现了路由匹配时自动展开父级菜单的功能。

  3. 布局组件分离MainLayout.vue 作为主布局容器,将侧边栏、面包屑导航和内容区域分离,提高了组件的可维护性和复用性。

  4. 绝对路径路由:所有子路由均使用绝对路径(如 /user/list),避免了相对路径可能带来的混淆,使路由跳转更加直观。

  5. 元数据驱动:路由配置中的 meta 字段(titleicon)驱动了菜单显示和面包屑导航,实现了配置与展示的分离。

后续扩展思路

1. 添加路由权限控制
  • 实现方案:在路由守卫中根据用户角色动态过```javascript码示例**:
// router/index.js 中添加 meta.roles 字段
{
  path: '/admin',
  component: AdminPage,
  meta: { title: '管理员页面', roles: ['admin'] }
}

// 路由守卫中检查权限
router.beforeEach((to, from, next) => {
  const userRoles = getUserRoles()
  if (to.meta.roles && !to.meta.roles.some(role => userRoles.includes(role))) {
    next('/403') // 无权限页面
  } else {
    next()
  }
})
2. 实现路由懒加载
  • 优化目的:减少首屏加载时间,按```javascript现方式**:
// 修改路由配置,使用动态导入
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/Dashboard.vue'),
    meta: { title: '仪表盘', icon: '📊' }
  },
  // ... 其他路由同理
]
3. 封装面包屑导航组件
  • 当前问题:面包屑逻辑直接写在 `MainLayout.vue```vue性差
  • 改进方案
<!-- Breadcrumb.vue -->
<template>
  <div class="breadcrumb">
    <router-link v-for="(item, index) in items" 
                 :key="index" 
                 :to="item.path"
                 :class="{ 'last-item': index === items.length - 1 }">
      {{ item.title }}
      <span v-if="index < items.length - 1" class="separator"> / </span>
    </router-link>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const items = computed(() => {
  return route.matched
    .filter(record => record.meta?.title)
    .map(record => ({
      path: record.path,
      title: record.meta.title
    }))
})
</script>
4. 添加页面切换动画
  • 实现效果:路由切换时添加淡```vue动画
  • 实现方式
<!-- 在 MainLayout.vue 的 content 区域 -->
<template>
  <section class="content">
    <router-view v-slot="{ Component }">
      <transition name="fade" mode="out-in">
        <component :is="Component" />
      </transition>
    </router-view>
  </section>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
5. 集成状态管理(Pinia)
  • 应用场景:用户信息、全局配置```javascript基础示例**:
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '管理员',
    avatar: '👤',
    permissions: []
  }),
  actions: {
    updateUserInfo(info) {
      this.name = info.name
      this.avatar = info.avatar
    }
  }
})

总结建议

本项目已搭建了一个功能完整的 Vue 3 后台管理系统前端骨架,具备清晰的模块划分和良好的扩展性。建议在实际开发中:

  1. 按需扩展:根据项目规模选择上述扩展思路,避免过度设计
  2. 保持一致性:新增功能时遵循现有的代码风格和架构模式
  3. 渐进式优化:优先实现业务需求,再逐步进行性能优化

通过以上扩展,可以进一步提升项目的可维护性、用户体验和性能表现。
看到对应的页面内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值