ARTICLE DETAIL

资讯详情

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

Element UI 后台管理系统实战:布局容器、表单校验与表格避坑指南

Element UI 后台管理系统实战:布局容器、表单校验与表格避坑指南 做过两三个后台管理系统之后基本都会遇到同一个问题每次从零搭页面布局要重新写表单校验要一个个堆规则表格列要反复调样式。这些重复劳动完全可以靠一个成熟组件库来收敛。我在项目里用了很久的 Element UI它对 Vue 2 生态来说依然是后台系统的默认选项之一。这篇文章不打算罗列官方文档而是从实际使用的角度把布局容器、表单、导航菜单、表格这几个基础组件怎么用、怎么避坑、怎么组合成完整页面一次讲清楚。如果你正准备开发管理后台、运营平台或者任何“以信息录入和展示为主”的系统这篇文章应该能帮你少走不少弯路。我会尽量把每一步的操作思路和原因都讲到而不是只扔一段代码。1. 项目概述Element UI 到底是什么解决什么问题1.1 组件库的价值与适用场景Element UI 是一套基于 Vue 2 的桌面端组件库核心价值是把后台页面上高频出现的界面元素封装成可直接使用的组件。比如你不需要自己写一个有校验功能的输入框也不需要从零实现一个支持排序、选择、翻页的表格直接引入 el-input、el-form、el-table 就能搞定。更关键的是它提供了一套统一的视觉规范和交互习惯。多人协作时大家写出来的页面风格不会跑偏产品经理验收时也不用反复纠结按钮位置和高亮颜色。对于企业后台这类“功能密度高、美观要求低于稳定性”的场景Element UI 是性价比很高的选择。1.2 适合谁学需要先掌握什么我的建议是至少先会 Vue 的基本语法比如 v-model、v-if、v-for、computed、props 这些概念再来用 Element UI。不必等完全精通 Vue 再动手你可以在写页面的过程中把两者一起学熟。组件库本质上是对 HTML 表单和布局的封装所以 HTML、CSS 基本功也不能太差。特别是布局容器相关的 Flex 概念栅格系统里的响应式逻辑如果对 flex 不了解你在调 el-row 和 el-col 的时候会明显感觉吃力。2. 整体设计与组件选型思路2.1 为什么把布局容器放在第一位后备管理系统的页面骨架无非就是顶部、侧栏、内容区这几块。Element UI 直接用 el-container 这一组组件把这个骨架搭好了。我在实际项目里的做法是这样el-container directionvertical classlayout-wrapper el-header height60px顶部导航/el-header el-container el-aside width220px侧边菜单/el-aside el-main主内容区/el-main /el-container /el-container这个结构看着简单但有几个细节值得注意。el-container 默认是横向排列竖向排列时要在外层设置 directionvertical。el-aside 的宽度即使用户把侧栏收起来这里也要提前考虑到通常我会用计算属性来动态切换 width 的值。el-main 自带 overflow 处理但实际开发中我经常遇到内容区高度撑不开的问题。原因是后台页面一般会设置全局高度 100%el-main 里内容多了撑开少了缩不回来。我的经验是给 el-main 单独设置overflow-y: auto同时让 html、body、#app 都保持高度链完整。2.2 栅格系统与响应式布局的实操理解栅格系统是 el-row 和 el-col 配合使用把一行按 24 份等分。你在设计表单时如果一行想放两列就各占 12放三列就各占 8。这样做的好处是无论屏幕宽度怎么变化比例关系能保持一致。el-row :gutter20 el-col :span12el-input placeholder左边/el-input/el-col el-col :span12el-input placeholder右边/el-input/el-col /el-rowgutter 表示每个 col 之间的间距。它并不是给 col 加 margin而是给每个 col 内部加左右 padding所以 col 里面如果还要继续套 el-row记得把间距考虑进去否则会出现视觉上左右不对称的情况。真正需要响应式的场景比如在窄屏下希望两列变一列可以用 :xs、:sm、:md 这些响应式属性来覆盖默认值。我常用的做法是给重要查询条件设置:xs24 :sm12 :md6手机上自动堆叠成单列平板上保持正常的左右排列。需要记住的是响应式属性是按媒体查询断点生效的不是你窗口缩小一点就立刻变化。2.3 导航菜单与服务端菜单联动导航菜单最常见的坑是刷新后高亮状态丢失。el-menu 的 active 状态是受控的也就是由default-active属性决定这个值一般不写死而是根据当前路由动态生成。el-menu :default-activeactiveMenu router background-color#304156 text-color#bfcbd9 active-text-color#409EFF /el-menucomputed: { activeMenu() { const route this.$route return route.meta route.meta.activeMenu ? route.meta.activeMenu : route.path } }为什么有时候菜单点来点去不高亮多半是因为 default-active 和菜单项 index 对不上。index 可以是路由路径这时候配合 router 模式直接跳转。如果菜单数据是从后端接口返回的记得把 index 字段绑定为真实路由而不是接口里的某个 id。菜单往往需要根据用户权限动态生成这就会涉及 v-for 嵌套递归。Element UI 的 el-submenu 可以嵌套但递归组件写起来比较绕。我建议把每个菜单项的数据结构先规范化统一成{ path, title, icon, children }然后封装一个 SidebarItem 组件递归调用自己。注意 el-submenu 的 index 必须是唯一字符串否则展开一个时其他同级菜单会跟着联动这个现象排查起来比较费时间。3. 表单组件的核心细节与实操要点3.1 基础表单的搭建和校验规则el-form 是整个表单体系的地基它通过model属性接收表单数据对象通过rules属性接收校验规则。每个 el-form-item 的prop属性必须和 model 里的字段名字一一对应校验规则才会生效。这是个看着简单但特别容易漏掉的细节漏掉 prop 后你写半天规则页面却完全不报错。一个典型例子el-form refruleForm :modelformData :rulesrules el-form-item label用户名 propusername el-input v-modelformData.username/el-input /el-form-item /el-formrules: { username: [ { required: true, message: 请输入用户名, trigger: blur }, { min: 3, max: 12, message: 长度在 3 到 12 个字符, trigger: blur } ] }trigger 的选项一般是 blur 或 change。输入框建议用 blur下拉框、日期选择器建议用 change这样校验触发的时机更自然不会一边打字一边弹红字。表单提交时要先调用 validate 方法通过之后再执行保存逻辑否则直接拿 formData 传后端很容易把一堆空数据和非法数据提交上去。3.2 动态表单配置与按需生成很多系统里表单不是写死的而是由后端返回配置信息前端根据配置动态渲染。这才是真正考验组件理解能力的地方。我的思路是这样的先定义一套字段描述协议比如[ { type: input, label: 项目名称, prop: projectName, rules: [{ required: true, message: 请输入项目名称, trigger: blur }] }, { type: select, label: 状态, prop: status, options: [{ label: 启用, value: 1 }, { label: 停用, value: 0 }] } ]然后前端用 v-for 渲染 FormItem再根据 type 用 v-if 切换具体控件。这样做的好处是后端调整表单结构时前端不需要发版。需要注意一个坑动态给 el-form-item 绑定 rules 时必须确认 prop 对应的字段在 formData 对象里是存在的。如果字段根本不存在校验会失效。初始化的 formData 里面最好把可能出现的字段全部预设好或者提交前做一次Object.assign({}, defaults, data)。动态表单最大的问题是组件复用和校验恢复。当配置变化后旧的校验错误信息不会自动消失。我在切换表单场景时会调用clearValidate()清空校验提示再重新 set 数据。直接重置整个表单也行但如果表单里含子组件resetFields 只能重置绑定了 prop 的字段其它信息还得手动处理。3.3 动态添加删除表单行“一行数据能增删”是一个非常常见的需求。比如维护多条收款账户、录入多个附件说明都需要批量编辑。我常用的写法是维护一个数组div v-for(item, index) in formData.items :keyindex el-form-item :label条目 (index 1) :propitems. index .name :rulesitemRules el-input v-modelitem.name/el-input /el-form-item el-button typedanger clickremoveItem(index)删除/el-button /div添加一行只需 push 一个空对象删除一行用 splice(index, 1)。关键点在于 prop 必须用items.0.name这种路径写法并且 formData 初始化时 items 数组的每个子对象都要包含 name 字段否则 v-model 绑定的值为 undefined校验同样会失效。删除行之后还有一个隐藏问题被删除行的校验信息会残留。我在实际操作中会在 splice 后加一个this.$refs.ruleForm.clearValidate()虽然稍微损失一点性能但在数据量不大时最省心。如果你用的是 Vue 3 搭配 Element Plus思路完全一样组件 API 也只是小调整。3.4 清空表单内容的几种方式与区别很多人分不清 resetFields 和直接赋空对象的区别。我帮团队排查过多次表单清空不彻底的问题根因大多数出在这里。resetFields 的作用是“把表单重置到初始值”初始化值指 el-form-item 被创建时对应 prop 字段的值。比如用户新增时打开弹窗表单打开前把 formData 里某些字段故意设为空字符串或默认值再调 resetFields表单会回到那个默认值而不是回到一个纯空对象。使用前要先给 form 挂 ref并且确保表单渲染完毕否则this.$refs.form.resetFields()会报 undefined 没有 resetFields。常见做法是把重置操作写在 nextTick 里或者等弹窗 mounted 后再调用。直接赋空对象是最粗暴但最可控的方式this.formData { name: , status: 1, remark: } this.$nextTick(() this.$refs.form.clearValidate())两者结合使用是我比较推荐的。先重置数据到默认结构再清掉校验状态这样可以避免很多遗留的红色提示。4. 表格数据展示的核心战场4.1 基础表格的列配置与数据绑定el-table 是后台系统里信息量最大的组件而且很难做到“写一次就完美”。基础结构是这样el-table :datatableData border stylewidth: 100% el-table-column propname label名称 min-width120/el-table-column el-table-column propstatus label状态 width100 template slot-scopescope el-tag :typescope.row.status 1 ? success : danger {{ scope.row.status 1 ? 启用 : 停用 }} /el-tag /template /el-table-column /el-tableprop 绑定的字段对应表格数据的每一行label 是表头文字width 和 min-width 的取舍我后面细讲。如果只是展示字符串用 prop 就够了。如果这一列需要格式化、显示按钮、拼接多个字段就要用插槽 template 来做自定义渲染。表格数据要尽量保证是纯数组。如果你从接口拿到的数据被包了一层对象比如{ list: [...] }记得在赋值时取出 list 或者是res.data || []。否则 el-table 拿不到可遍历的数组表格会一直空白控制台还在报“Invalid prop: type check failed”排查的时候很容易被误导。4.2 表格固定列与底部重叠问题“element-ui 表格固定列和底部重叠”这个问题我搜索过很多次也在项目里踩过。先说现象当你设置固定列 fixed同时又让表格出现横向滚动条时固定区域的底部边框和表格主体内容会出现视觉上的错位或者重叠看起来就像多了一条线或者一个像素的断层。这个问题的成因主要有两个方向。第一是表格高度计算问题。el-table 的 fixed 列是通过绝对定位实现的当表格整体容器高度变化比如浏览器窗口缩放table 内部的高度缓存没有同步更新固定列区域就会和实际滚动区域错开。对这种情况我推荐在窗口 resize 时调用 table 组件实例的doLayout()方法让表格重新计算布局。mounted() { window.addEventListener(resize, this.handleResize) }, beforeDestroy() { window.removeEventListener(resize, this.handleResize) }, methods: { handleResize() { if (this.$refs.dataTable) { this.$refs.dataTable.doLayout() } } }第二是样式干扰。某些全局样式会给td或.el-table__fixed-right设置overflow: hidden或奇怪的 border然后固定列与底部横线的层次关系就被打乱了。之前的项目里我排查到底部重叠时发现是团队自己加了一行 CSS 修复别的 bug结果把表格的 fixed 层级压住了。遇到这种问题先移除页面对 el-table 的自定义覆盖再试官方能力往往更快。如果项目对表格列数有严格的要求尽量不要同时设置大量的 fixed 列。固定列多计算量大而且多列固定时横向滚动下固定区域宽度会变得很宽反而影响体验。能把固定列控制在 1 到 2 列是一个比较稳妥的选择。4.3 行合并、合计行和自适应宽度行合并的场景一般在报表里出现比如相同分类的单元格需要跨行展示。el-table 提供了span-method方法返回一个数组数组里是[row, column, rowspan, colspan]分别代表行、列、合并行数、合并列数。我举个例子如果第一列同名的行要合并spanMethod({ row, column, rowIndex, columnIndex }) { if (columnIndex 0) { const rowspan this.mergeRow(rowIndex) if (rowspan 0) { return { rowspan: rowspan, colspan: 1 } } else { return { rowspan: 0, colspan: 0 } } } }这里关键在于提前算好每一行需要合并几行通常写一个方法遍历原始数据比较相邻行的值值相同则计数累加不同则结算。这个方法没有官方捷径逻辑必须自己写。我曾经因为数据排序不稳定出现合并计算随机错乱后来发现是接口返回的数据顺序每次不同。解决方案是合并前先按合并字段排序确保相同值相邻否则 span-method 里的索引逻辑会整个跑偏。合计行是另一个高频需求。el-table 自带show-summary属性开起来会显示一个表格底部合计行。如果你需要自定义哪些列求和、哪些列显示“合计”字样就传summary-method它返回一个数组数组里的值对应每一列应该展示的内容。需要注意合计行的宽度对齐问题如果使用了固定列合计行在小概率下也会出现和内容区不对齐的情况处理方式同样是 doLayout。自适应宽度的核心原则是能不定宽的列就不要定宽。字符串较短的列用 width内容长度不可控的列用 min-width让表格自己去分配剩余空间。El-table 默认是 100% 宽你给一个较大的 min-width 后它会在容器内自动伸缩。窗口变化时由于 el-table 基于浏览器 resize 监听重新计算大多数情况是准的。只要不混用大量的固定 width 和 min-width自适应表现就能符合预期。4.4 表格与动态数据的其他实用细节除了上面的核心问题我建议所有用 el-table 的人都要注意“数据更新后表格是否刷新”这个点。当你直接改数组里某个对象的属性比如row.status 2Vue 2 是能侦测到的但如果通过this.tableData[index] newObj这种方式换整行界面不会自动更新。这个时候要用this.$set(this.tableData, index, newObj)或者直接替换整个数组。还有一次遇到很隐蔽的问题表格数据没变但多选状态全丢了。原因是给 el-table 加了row-key之后数据重新赋值时组件会按 key 复用人如果 key 本身不稳定比如用了随机数行身份就会变来变去选中状态自然保不住。row-key 必须用数据里稳定唯一的字段比如 id。5. 实操过程与整体排查实录5.1 从零搭一个标准查询页面的完整流程一个后台常见的查询页面由三块组成上方的查询表单、下方的结果表格、底部分页器。我鼓起勇气说明一下我的流程供你参考。第一步先写骨架容器。根据设计稿判断主区域是上下结构还是左右结构通常查询页直接用 el-main 包起来内部放一个 el-form。第二步搭查询表单。查询表单不需要太复杂的校验字段按业务需求放 3 到 5 个就合适了。重点是把 label-width 设置统一比如 100px否则不同表单的对齐会乱。第三步搭表格。先定义好列顺序和宽度策略。使用接口数据时我会先把 mock 数据跑通确认列没有缺 prop再替换成真实接口。第四步接翻页逻辑。el-pagination 的 current-change 和 size-change 事件要绑定到方法翻页时重新请求接口并把返回的 total 赋给分页组件。容易忽略的是当当前页的数据被删光后需要把 currentPage 减一再重新拉列表否则会出现“当前页没有任何内容分页器还停在老页码”的尴尬局面。第五步做状态恢复。查询参数要存在组件的 data 里query、currentPage、pageSize 最好集中放在一个对象里这样重置时可以直接清空这个对象。5.2 常见问题速查表我把这几个月在项目里遇到的和网上搜索热度较高的问题整理成一张表方便你快速排查现象可能原因处理建议表单校验不触发el-form-item 的 prop 没写或字段没在 formData 初始化检查 prop 与 model 字段一一对应resetFields 清不掉内容没有初始化字段默认值打开弹窗前先设置好 formData 的默认结构表格固定列和底部重叠容器高度变化后未重新计算自定义样式覆盖调用 doLayout()排查全局 CSS表格刷新后多选丢失row-key 不稳定将 row-key 绑定为唯一 id 字段动态表单配置切换后旧校验残留校验状态未清空切换后调用 clearValidate()动态添加行却校验不生效prop 路径错误子字段未初始化使用items.0.name路径确保子对象字段存在菜单高亮刷新失效default-active 与菜单 index 不匹配通过当前路由动态计算 activeMenu表格合计行与固定列不对齐旧版本组件的布局计算问题数据变化后 doLayout不能解决再升级版本5.3 踩过的坑和性能优化建议用 Element UI 做复杂页面最大的隐患不是组件不会用而是滥用组件导致页面卡顿。表格数据量大时如果还在列插槽里放一堆嵌套组件渲染时间会明显上升。我的建议是能用纯字符串展示的字段就不要包一层按钮加一个弹窗。数据量超过几百行优先做服务端分页前端不要一次渲染几千行。表格里需要外部跳转或操作按钮时只给操作列渲染按钮其他列保持简单渲染。使用v-if和v-show时注意场景频繁切换用 v-show初始条件差异大用 v-if。另外还有一个小技巧Element UI 的按需引入。哪怕是你一个人开发的小项目也有必要把 Element UI 改成按需加载。全量引入会多出几百 KB 的 JS 体积按需加载配合 babel-plugin-component只引入你用到的组件和样式。团队项目里这一点更应该成为规范否则每次上线打包都带着一些强迫症。如果项目已经升级到 Vue 3那对应的是 Element PlusAPI 大体相似但有几处差异要注意。比如slot-scope变成了#defaultscopeel-table-column的写法大部分保留但下拉勾选的几个方法名有调整。迁移时不要只改版本号最好把全局搜索 slot-scope手动换成新插槽语法。从我自己的经验来看Element UI 这类组件库最重要的价值不是省下“写一遍组件”的时间而是让团队在交互和样式上有一个共同的约定。你只要把布局容器的层级、表单校验的时机、表格列的宽度策略这几个基础点摸透剩下再复杂的页面也都是把它们组合起来的过程。如果你自己动手搭后台时遇到某个表格固定列重叠、动态表单清空不彻底这类问题先回想一下我今天提到的排查顺序先看数据是否初始化再看 prop 是否对应最后看布局计算是否需要刷新。多数坑都在这个范围里。
返回列表