ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3+Layui后台搭建实战:表格封装、权限控制与nginx部署

Vue3+Layui后台搭建实战:表格封装、权限控制与nginx部署 简介基于VUE3与Layui相结合的后台管理系统前端源码面向有一定Vue基础、需要快速搭建企业级中后台项目的开发者。资源以源码为主包含登录注册、主题切换、国际化、系统管理、商品订单管理、数据字典、大屏监控等模块并对下拉选择、图片上传、富文本、列表/表单/详情页等常用组件做了封装同时配套基于Express的模拟后端接口以及基于Echarts和DataV的图表与大屏演示可直接用于学习二次开发。包体共2002个文件以md文档、js源码、json配置为主其中md笔记1339个便于分模块阅读js与json覆盖前端逻辑及接口数据配置整体约204.39MB。已有1724人学习下载。通过源码、组件封装思路与完整功能演示可掌握Vue3后台管理系统的目录组织、通用模块抽象和前后端联调方法适合用来搭建自身项目基础框架或作为毕业设计参考。1. 从零搭 Vue3Layui 后台比套模板多解决三件事很多团队拿到后台管理系统的需求第一反应是下载一个现成的 admin 模板改改菜单和颜色就交付。模板确实快但当你需要把旧系统里的 Layui 页面平滑迁到 Vue3、或者团队里有人熟悉 Layui 却不熟悉 Element Plus 时模板的组件体系和视觉习惯反而成了阻力。自己从空项目搭一套 Vue3Layui 的组合表面上多花了半天初始化时间实际上是把「页面框架、权限拦截、表格表单封装」这三件每个后台都要做的事提前握在自己手里。这篇文章定位在前端篇覆盖从工程创建到 nginx 部署的完整链路后端接口只做约定不展开服务端实现。适合已经会用 Vue 写业务组件、但没完整搭过后台基座的前端开发也适合准备把老 Layui 项目逐步重构到 Vue3 的团队。2. 选型与环境Layui 和 Vue3 不是二选一而是两种集成路线2.1 先搞清楚 Layui 与 Vue3 的真正关系Layui 从 2.8 版本开始官方提供了 layui-vue 组件库这是基于 Vue3 TypeScript 重写的版本组件风格延续了经典 Layui 的视觉语言。与此同时经典 Layui即通过 layui.use 加载的模块化版本本身不依赖任何框架它是一个基于原生 DOM 操作的 UI 库。所以「Vue3Layui 后台」实际有两条路可走方案适用场景代价方案 A使用 layui-vue 组件库新项目页面全部用 Vue 组件编写需要接受它的组件生态比 Element Plus 小方案 B全局引入经典 Layui通过 ref 操作 DOM老系统迁移保留原页面 HTML 结构和 Layui 组件调用方式Vue 的响应式数据不直接驱动 Layui 组件状态需要手动同步我在实际项目中多数选方案 A但会在 vite.config 里保留对经典 Layui 静态资源的引入因为老项目迁移过来的页面里往往还残留 form.render、table.render 这类调用。两种方案并存不冲突关键是不要试图在 Vue 的 template 里直接写input typecheckbox lay-skinprimary然后指望 Vue 管理它的选中态Layui 会自己渲染 DOM这会导致 Vue 的 diff 和 Layui 的 DOM 操作打架。2.2 用 Vite 创建 Vue3 项目并接入 Layui常见的做法是用 Vite 创建项目然后通过 npm 安装 layui-vue。Vite 创建命令如下npm create vitelatest vue3-layui-admin -- --template vue cd vue3-layui-admin npm install npm install layui-vue注意这里没有加--ts因为后台管理系统里大量是表单和表格配置JavaScript 的灵活度更高团队里如果有后端转前端的同事纯 JS 的学习成本也低。如果团队习惯 TypeScript可以自行加上。安装完成后在main.js里注册import { createApp } from vue import App from ./App.vue import Layui from layui-vue import layui-vue/lib/index.css const app createApp(App) app.use(Layui) app.mount(#app)app.use(Layui)会全局注册所有 layui-vue 组件好处是开发时不用每个页面手动 import坏处是打包体积偏大。对后台管理系统来说首屏加载速度不是核心指标全局注册节省的维护成本更划算。如果以后要做 C 端页面再考虑按需引入。2.3 目录结构把通用能力从业务页面里拆出去创建一个后台管理系统最怕的是所有页面塞进 src/views然后公共逻辑写在每个页面里。我会在 src 下建立以下目录src/ api/ # 按模块拆分的接口定义 assets/ components/ # 通用业务组件如 SearchForm、ProTable、UploadFile layout/ # 后台框架侧边菜单、顶栏、标签页 router/ store/ # 用 Pinia 管理用户信息和权限 utils/ # request.js、auth.js、download.js views/ login/ dashboard/ system/ # 用户管理、角色管理、菜单管理layout和components是后台基座的核心。layout 负责整体框架components 里的 ProTable、SearchForm 这类组件是后续每个列表页都要用到的。不要急着写业务页面先把 layout 跑通再封装一版表格和表单组件后面开发效率会明显提升。3. 表格与表单把 Layui 的 table 和日期组件封装成可复用件3.1 layui-vue 表格组件与服务端分页的对接后台管理系统 90% 的页面是表格加搜索条件。layui-vue 的Table组件接收columns和dataSource两个核心属性但真实项目中数据永远来自服务端分页接口。封装一个 ProTable 的思路是组件自己维护页码、每页条数并把请求参数抛给父组件传入的request函数。template lay-table :columnscolumns :data-sourcedataSource :pagepage changehandleTableChange /lay-table /template script setup import { ref, watch, onMounted } from vue const props defineProps({ columns: { type: Array, required: true }, request: { type: Function, required: true }, searchParams: { type: Object, default: () ({}) } }) const dataSource ref([]) const page ref({ current: 1, limit: 10, total: 0 }) async function loadData() { const res await props.request({ pageNum: page.value.current, pageSize: page.value.limit, ...props.searchParams }) dataSource.value res.data.records page.value.total res.data.total } function handleTableChange({ current, limit }) { page.value.current current page.value.limit limit loadData() } onMounted(loadData) watch(() props.searchParams, loadData, { deep: true }) /script这段代码有几个关键点。change是 layui-vue 表格翻页时触发的事件回调参数里带current和limit不需要自己监听页码的变化再去手动调用接口。searchParams用watch监听并loadData实现搜索条件变化自动刷新列表。request函数由父组件传入这样 ProTable 不关心具体接口地址只管把pageNum和pageSize拼进去。这里要注意 layui-vue 的表格组件和经典 Layui 的table.render传参风格不一致。经典 Layui 默认传page和limit服务端接口如果之前按这个参数名写的用 layui-vue 时要么在 request 里转换要么让后端兼容两种参数名。我一般会统一在封装的 request 函数里做一次映射避免后端改动。3.2 日期范围选择与「最大日期为当前日期」的坑搜索表单里最常见的组件是日期范围。layui-vue 的DatePicker组件用typedatetime配置为日期时间选择但有一个高频需求容易被忽略不允许选择未来日期。这个需求在 vue3 后台管理系统里经常被检索因为很多业务场景如查询历史订单、登记过往记录的选择范围必须截止到今天。lay-date-picker v-modelform.dateRange typedatetime :rangetrue :maxmaxDate /lay-date-pickerconst maxDate new Date().toISOString().slice(0, 10) 23:59:59max参数接收一个日期字符串限制可选的最大日期。这里容易踩坑的地方是直接写new Date()传给max组件要求的是格式化后的字符串完整日期时间格式才能精确到当天的最后一秒否则可能出现当天选不了或者默认选中时间不对的情况。另一个坑是range模式下开始时间和结束时间的联动。layui-vue 的 DatePicker 在range为true时v-model绑定的是一个包含两个日期字符串的数组。后端接口如果要求传开始时间和结束时间两个独立参数需要在提交前拆开const params { startTime: form.dateRange[0], endTime: form.dateRange[1] }如果后端要的是时间戳则先用new Date(form.dateRange[0]).getTime()转换。这个转换逻辑不要写在每个页面里封装在 ProTable 的 searchParams 提交函数中统一处理。3.3 select 动态赋值的三种写法Layui 的经典 select 组件在动态赋值上有一个出名的问题直接修改v-model的值下拉框的显示文本不会同步更新。这是因为 Layui 组件内部维护了自己的选中状态Vue 的响应式数据变化无法直接驱动它。在 layui-vue 组件库中这个问题已经解决但在经典 Layui 遗留页面里依然存在。如果项目中还保留了经典 Layui 页面动态赋值需要手动调用 Layui 的更新方法// 经典 Layui 遗留页面的正确赋值方式 layui.form.val(searchForm, { status: 1 }) layui.form.render(select)第一行代码给指定 form 表单赋值第二行render重新渲染下拉组件。很多人在迁移时只写了第一行发现下拉框没有变化就开始怀疑 Vue3 的响应式失效其实是 Layui 的 DOM 结构和 Vue 的虚拟 DOM 互不感知导致的。对于新写的 layui-vue 页面select组件的动态赋值就简单多了选项数据通过v-model绑定的数组控制选中值通过v-model绑定响应式变量lay-select v-modelform.status lay-select-option v-foritem in statusList :keyitem.value :valueitem.value :labelitem.label/lay-select-option /lay-selectstatusList可以在接口返回后直接赋值选中值form.status也完全由 Vue 控制不存在同步问题。这就是方案 B 里「不要混写」的原因混写时最容易出现这类隐性 bug。4. 权限与动态路由菜单渲染和按钮控制的边界条件4.1 登录态存储与路由守卫的完整链路后台管理系统必须处理登录态失效的问题。常见做法是登录成功后把 token 存到 localStorage接口返回 401 时清空登录态并跳回登录页。但只做这一步不够刷新页面时路由守卫需要根据 token 和用户信息决定放行还是跳转。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) { next() } else { next(/login?redirect${to.fullPath}) } return } if (to.path /login) { next(/dashboard) return } const userInfo localStorage.getItem(userInfo) if (!userInfo) { // 异步获取用户信息后再放行 store.dispatch(fetchUserInfo).then(() { next() }).catch(() { localStorage.removeItem(token) next(/login) }) } else { next() } })这里用到redirect参数登录成功后可以在登录页读取它并跳回原目标页面。很多系统忽略这个细节用户登录态过期后重新登录总是被扔到首页体验很差。权限更新的另一面是接口 401 的统一处理这要在 axios 拦截器里完成axios.interceptors.response.use( response response, error { if (error.response.status 401) { localStorage.clear() window.location.href /login } return Promise.reject(error) } )用window.location.href而不是router.push(/login)是因为 401 场景下可能 Vue 实例还没有完全恢复或者路由实例不可用直接跳转更稳定。localStorage.clear()会清掉所有缓存数据比逐个 removeItem 更干净但如果系统里有不依赖登录态的本地配置改成只移除 token 和 userInfo 更精细。4.2 动态菜单与 addRoute 的配合方式不同角色登录后台看到的菜单应该不同。菜单数据一般由后端根据角色返回前端拿到数据后动态注册路由再渲染侧边栏。用 Vue Router 4 的addRoute方法可以在应用启动后追加路由但有一个前提路由表里必须先有一个空的路由占位组件比如 layout。export function setupDynamicRoutes(menuData) { const layoutRoute { path: /, component: () import(/layout/index.vue), children: [] } menuData.forEach(item { layoutRoute.children.push({ path: item.path, name: item.name, component: () import(/views/${item.component}.vue), meta: { title: item.title, icon: item.icon } }) }) router.addRoute(layoutRoute) }动态import的路径不能完全用变量拼接Vite 在构建时无法解析这种动态路径。需要把映射关系提前写在一个常量对象里const viewMap { system/user: () import(/views/system/user.vue), system/role: () import(/views/system/role.vue) }这个限制在打包时会出现「模块找不到」的报错很多人以为是代码问题其实是构建工具的静态分析机制导致的。如果菜单项很多可以写一个脚本扫描 views 目录自动生成 mapping 文件但一般后台的页面数量也就几十个手写一份映射表成本可控。4.3 按钮级权限控制用自定义指令菜单级权限只解决了「能看到哪些页面」的问题页面里的操作按钮新增、删除、导出还需要按角色控制。Vue3 里自定义指令是实现按钮权限最干净的方式const permission { mounted(el, binding) { const required binding.value const userPermissions store.state.user.permissions if (!userPermissions.includes(required)) { el.parentNode el.parentNode.removeChild(el) } } }button v-permissionsystem:user:add新增用户/button自定义指令的细节在于mounted阶段执行移除逻辑此时按钮刚被插入 DOMparentNode存在直接移除不会引起元素闪烁。如果按钮的父容器是v-for渲染的列表项移除元素会导致列表布局跳动这种情况下更适合用 v-if 判断然后渲染一个空注释节点。5. 上线部署nginx 配置与功能验证清单后台管理系统开发完成后构建产物是一个纯静态目录用 nginx 托管是最常见的部署方式。Vue Router 默认使用 history 模式这会导致刷新页面时 nginx 找不到对应的文件路径返回 404。这是 vue3 后台管理系统上线时最高频的问题。在 nginx 配置文件中加入以下规则location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; }try_files的含义是依次尝试$uri请求的文件路径、$uri/请求的目录路径都找不到就回退到/index.html。这样前端路由在刷新时nginx 会把请求交给 index.html由 Vue Router 自己根据 URL 解析对应页面。如果你的后台部署在子路径下比如https://example.com/admin/需要在 vite.config 里配置base: /admin/同时 nginx 的 location 也要同步调整location /admin { alias /usr/share/nginx/html; try_files $uri $uri/ /admin/index.html; }上线前除了验证功能流程我一般会额外检查三件事第一构建产物里是否有残留的 console.log用vite-plugin-remove-console插件移除第二接口地址是否写死为开发环境的localhost要通过环境变量区分第三登录态的 token 过期时间是否和后端保持一致避免前端跳转登录页了后端接口还在接受旧 token 的请求。最后在浏览器无痕窗口里完整走一遍登录、进入列表页、执行一次搜索、翻页、编辑保存、退出登录再重新登录跳回原页面。这套路径能覆盖后台管理系统 80% 的常见缺陷。本文还有配套的精品资源点击获取
返回列表