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

📅 2026/6/25 14:31:38
Vue 3 后台管理系统前端骨架小案例1.0版本
文章摘要本文详细介绍了基于 Vue 3 的后台管理系统前端项目实现涵盖以下核心内容 核心功能路由嵌套架构使用ParentLayout.vue实现/user和/order模块的嵌套路由动态菜单系统MenuTree.vue组件递归渲染路由配置支持无限级嵌套响应式布局MainLayout.vue提供侧边栏、面包屑导航和内容区域分离布局完整页面示例包含仪表盘、用户管理、订单管理、系统设置等典型后台页面️ 技术栈Vue 3 Vue RouterComposition API (script setup)TypeScript 类型支持 (env.d.ts)模块化 CSS (Scoped Styles) 项目结构router/- 路由配置支持嵌套路由、元数据驱动layout/- 布局组件主布局、菜单树、父级路由出口views/- 页面组件5个典型后台页面完整的样式设计和交互 快速开始一键运行bash npm install npm run devdev访问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.ts1.src/router/index.jsimport{createRouter,createWebHistory}fromvue-routerimportDashboardfrom../views/Dashboard.vueimportUserListfrom../views/UserList.vueimportUserDetailfrom../views/UserDetail.vueimportOrderListfrom../views/OrderList.vueimportSettingsfrom../views/Settings.vueimportParentLayoutfrom../layout/ParentLayout.vueconstroutes[{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:⚙️}}]constroutercreateRouter({history:createWebHistory(),routes})exportdefaultrouterexport{routes}2.src/layout/MainLayout.vuetemplate div classapp-container !-- 侧边栏 -- aside classsidebar div classlogo✨ 后台系统/div nav classmenu MenuTree :routesmenuRoutes / /nav /aside !-- 主区域 -- main classmain header classheader div classbreadcrumb span v-for(crumb, index) in breadcrumbs :keyindex {{ crumb }} span v-ifindex breadcrumbs.length - 1 classseparator / /span /span /div div classuser-info span classavatar/span span classname管理员/span /div /header section classcontent 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; } /style3.src/layout/MenuTree.vuetemplate ul classmenu-tree template v-forroute in routes :keyroute.path li v-ifroute.children route.children.length 0 classmenu-item div classmenu-label clicktoggle(route.path) span classicon{{ route.meta?.icon }}/span span classtitle{{ route.meta?.title }}/span span classarrow :class{ open: isOpen(route.path) }▶/span /div ul classsub-menu v-showisOpen(route.path) MenuTree :routesroute.children / /ul /li li v-else classmenu-item !-- 使用 route.path绝对路径 -- router-link :toroute.path classmenu-label active-classactive span classicon{{ route.meta?.icon }}/span span classtitle{{ 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; } /style4.src/layout/ParentLayout.vuetemplate router-view / /template5.src/views/Dashboard.vuetemplate div h2 stylemargin-bottom: 20px; 仪表盘/h2 div classcard-grid div classcard v-fori in 4 :keyi div classcard-title数据 {{ i }}/div div classcard-value{{ Math.floor(Math.random() * 1000) }}/div div classcard-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; } /style6.src/views/UserList.vuetemplate div h2 用户列表/h2 table classtable thead trthID/thth姓名/thth角色/thth状态/th/tr /thead tbody tr v-foruser in users :keyuser.id td{{ user.id }}/td td{{ user.name }}/td td{{ user.role }}/td td span classtag :classuser.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; } /style7.src/views/UserDetail.vuetemplate div h2 用户详情/h2 div stylebackground:#fff; padding:20px; border-radius:8px; margin-top:20px; box-shadow:0 2px 12px rgba(0,0,0,0.06); pstrong用户 ID/strong{{ $route.params.id || 无 }}/p pstrong姓名/strong张三/p pstrong邮箱/strongzhangsanexample.com/p pstrong角色/strong管理员/p pstrong注册时间/strong2024-01-15/p /div /div /template8.src/views/OrderList.vuetemplate div h2 订单列表/h2 table classtable thead trth订单号/thth金额/thth状态/thth下单时间/th/tr /thead tbody tr v-fororder in orders :keyorder.id td{{ order.id }}/td td¥{{ order.amount.toFixed(2) }}/td td span classtag :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; } /style9.src/views/Settings.vuetemplate div h2⚙️ 系统设置/h2 div stylebackground:#fff; padding:24px; border-radius:8px; margin-top:20px; box-shadow:0 2px 12px rgba(0,0,0,0.06); div stylemargin-bottom:16px; padding-bottom:16px; border-bottom:1px solid #ebeef5; label styledisplay:inline-block; width:100px; color:#606266;网站名称/label input typetext value后台管理系统 stylepadding:8px 12px; border:1px solid #dcdfe6; border-radius:4px; width:300px; /div div stylemargin-bottom:16px; padding-bottom:16px; border-bottom:1px solid #ebeef5; label styledisplay:inline-block; width:100px; color:#606266;登录超时/label input typenumber value30 stylepadding:8px 12px; border:1px solid #dcdfe6; border-radius:4px; width:100px; 分钟 /div div button stylepadding:8px 24px; background:#409eff; color:#fff; border:none; border-radius:4px; cursor:pointer;保存设置/button /div /div /div /template10.src/App.vuetemplate MainLayout / /template script setup import MainLayout from ./layout/MainLayout.vue /script11.src/main.jsimport{createApp}fromvueimportAppfrom./App.vueimportrouterfrom./routerconstappcreateApp(App)app.use(router)app.mount(#app)12.src/env.d.ts/// reference typesvite/client /declaremodule*.vue{importtype{DefineComponent}fromvueconstcomponent:DefineComponent{},{},anyexportdefaultcomponent} 运行npminstallnpmrun dev访问http://localhost:5173点击左侧菜单即可看到对应的页面内容。 项目总结与扩展思路项目核心要点总结路由嵌套架构清晰通过ParentLayout.vue作为父级路由出口实现了/user和/order等模块的嵌套路由结构使代码组织更加模块化。菜单动态生成MenuTree.vue组件递归渲染路由配置支持无限级嵌套菜单并实现了路由匹配时自动展开父级菜单的功能。布局组件分离MainLayout.vue作为主布局容器将侧边栏、面包屑导航和内容区域分离提高了组件的可维护性和复用性。绝对路径路由所有子路由均使用绝对路径如/user/list避免了相对路径可能带来的混淆使路由跳转更加直观。元数据驱动路由配置中的meta字段title、icon驱动了菜单显示和面包屑导航实现了配置与展示的分离。后续扩展思路1. 添加路由权限控制实现方案在路由守卫中根据用户角色动态过javascript码示例**// router/index.js 中添加 meta.roles 字段{path:/admin,component:AdminPage,meta:{title:管理员页面,roles:[admin]}}// 路由守卫中检查权限router.beforeEach((to,from,next){constuserRolesgetUserRoles()if(to.meta.roles!to.meta.roles.some(roleuserRoles.includes(role))){next(/403)// 无权限页面}else{next()}})2. 实现路由懒加载优化目的减少首屏加载时间按javascript现方式**// 修改路由配置使用动态导入constroutes[{path:/dashboard,name:Dashboard,component:()import(../views/Dashboard.vue),meta:{title:仪表盘,icon:}},// ... 其他路由同理]3. 封装面包屑导航组件当前问题面包屑逻辑直接写在 MainLayout.vuevue性差改进方案!-- Breadcrumb.vue -- template div classbreadcrumb router-link v-for(item, index) in items :keyindex :toitem.path :class{ last-item: index items.length - 1 } {{ item.title }} span v-ifindex items.length - 1 classseparator / /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 })) }) /script4. 添加页面切换动画实现效果路由切换时添加淡vue动画实现方式!-- 在 MainLayout.vue 的 content 区域 -- template section classcontent router-view v-slot{ Component } transition namefade modeout-in component :isComponent / /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; } /style5. 集成状态管理Pinia应用场景用户信息、全局配置javascript基础示例**// stores/user.jsimport{defineStore}frompiniaexportconstuseUserStoredefineStore(user,{state:()({name:管理员,avatar:,permissions:[]}),actions:{updateUserInfo(info){this.nameinfo.namethis.avatarinfo.avatar}}})总结建议本项目已搭建了一个功能完整的 Vue 3 后台管理系统前端骨架具备清晰的模块划分和良好的扩展性。建议在实际开发中按需扩展根据项目规模选择上述扩展思路避免过度设计保持一致性新增功能时遵循现有的代码风格和架构模式渐进式优化优先实现业务需求再逐步进行性能优化通过以上扩展可以进一步提升项目的可维护性、用户体验和性能表现。看到对应的页面内容。