ARTICLE DETAIL

资讯详情

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

Element UI核心组件实战:布局、表单、菜单、表格避坑指南

Element UI核心组件实战:布局、表单、菜单、表格避坑指南 1. Element UI到底是什么从一个后台项目说起先问一句做过后台管理系统的人是不是每次新项目启动最头疼的都是那套长得差不多的页面——左侧菜单、顶部栏、中间一块表格加表单这套东西如果纯手写CSS和组件光是布局、交互、状态同步就够折腾一周。Element UI解决的就是这件事它是基于Vue 2的一套桌面端组件库把后台系统里最常用的布局容器、表单、导航菜单、表格、弹窗、消息提示这些组件都封装好了你只需要拿来拼接就能搭出一个能用的后台界面。我第一次接触Element UI是接手一个二手后台项目当时的感受是组件确实全但网上教程大多停留在照着抄代码的层面真正遇到布局错乱、表格固定列遮挡、动态表单配置这类实际问题时很难找到靠谱的解决方案。这篇文章就从我的实际使用经验出发把Layout布局、Form表单、Menu菜单、Table表格这几个最核心的板块拆开讲一遍重点放在为什么这样做以及踩过哪些坑上适合刚接触Element UI的初学者也适合已经用了很久但一直没时间整理细节的开发者。1.1 它的定位与核心组成Element UI本质上是一套用Vue 2语法写的组件库官方叫法是一套为开发者、设计师和产品经理准备的基于Vue 2.0的桌面端组件库。核心组成可以分成三类第一类是基础组件比如Button按钮、Input输入框、Radio单选框这些最小的交互单元第二类是容器组件比如Container布局容器、Card卡片、Dialog对话框负责把页面搭出骨架第三类是业务组合组件比如Form表单内部联动Input、Select、DatePicker等、Table表格内部联动Column、Pagination分页这类组件的特点是用起来方便调起来麻烦。和同类组件库相比Element UI的特点是默认设计风格统一、文档详细、中文化做得好而且在国内项目中的使用基数极大。哪怕现在Vue 3流行了Element Plus也出了很久但存量项目里Element UI仍然占了很大比例很多公司的老后台系统到现在还在用。所以你如果维护老项目这套东西是绕不开的。1.2 为什么至今还有大量项目在用很多人会问Vue 3都普及了为什么不全面切Element Plus原因很现实。一个成熟项目里除了组件本身还有大量基于Element UI封装的自定义组件、业务模块和团队约定全面升级意味着从头验证一遍所有交互成本极高。而且Element UI的组件API设计得比较简洁使用习惯一旦形成团队很难说服自己去做破坏性升级。从技术角度说Element UI基于Vue 2的响应式系统和指令机制在数据驱动、表单校验、表格渲染这些场景下已经打磨得相当稳定。它不像某些轻量组件库那样需要自己拼装很多功能也不像重型框架那样有很高的学习门槛属于拿来即用、用起来不烦的类型。对大多数后台管理系统来说它的功能边界刚好覆盖了80%的日常需求。2. 布局容器搭起页面骨架的正确姿势布局是后台项目最先要确定的东西。Element UI的布局体系有两个层次底层是Row和Col栅格系统负责页面内的横向分栏顶层是Container系列布局容器负责整个页面的上下左右骨架也就是el-container、el-header、el-aside、el-main、el-footer这五个组件。实际项目中这两个层次经常结合使用先用Container把页面切成大块再用Row/Col在大块内部做细分。2.1 六个组件的搭配逻辑先看一个最基础的后台布局结构el-container el-aside width200px左侧菜单/el-aside el-container el-header顶部栏/el-header el-main内容区/el-main el-footer底部栏/el-footer /el-container /el-container这个结构的核心逻辑是el-container可以无限嵌套但要注意父子关系决定的是水平分割还是垂直分割。当el-container的子元素中包含el-aside或el-header时容器会自动按水平或垂直方向排列但如果你嵌套错了层级页面会出现内容区被挤压或高度塌陷的问题。还有一个很多人忽略的点el-container本身默认不是全屏高度它的高度取决于内容。如果希望左侧菜单和右侧内容区撑满整个视口高度需要手动给容器设置高度。我常用的做法是html, body, #app { height: 100%; margin: 0; } .el-container { height: 100%; }如果不设置这个高度el-aside里的菜单在内容少的时候只会占上半屏下半屏露出白底视觉上非常不协调。2.2 三种经典布局模板第一种是侧边栏顶栏内容区的主流结构适合大多数管理后台。左侧菜单固定宽度顶部放面包屑、用户信息、通知按钮内容区放业务页面。这里要注意el-aside的宽度可以用width属性设置但如果你用了折叠功能宽度是动态变的建议用v-model绑定isCollapse状态同时用CSS过渡让宽度变化平滑el-aside :widthisCollapse ? 64px : 200px第二种是顶栏主导航内容区结构适合导航层级较浅的系统比如只有一级菜单的运营后台。这种结构不用el-aside直接在el-header里放el-menu的modehorizontal横向菜单。第三种是多级侧边栏标签页结构适合大型系统。侧边栏菜单有多级嵌套内容区顶部加一个标签页栏记录用户打开过的页面。这种结构里el-main内部还要再嵌套一个el-container来放标签页和内容页层级一多布局心态就容易乱。2.3 嵌套布局与自适应细节嵌套布局最常见的需求是左侧菜单栏内部再分上下两块上面放logo下面放菜单el-aside width200px div classlogo系统名称/div el-menu el-menu-item菜单项/el-menu-item /el-menu /el-aside这里有一个自适应细节当el-aside内的菜单项数量超出可视区域时需要让菜单区域内部的el-menu可滚动但el-aside本身固定高度不滚动。你可以给el-menu加上height: calc(100vh - 60px)这类样式其中60px是logo区域的高度。另一个细节是响应式。虽然Element UI这套布局主要面向PC端后台但有些项目需要适配窄屏。栅格系统el-row和el-col支持:xs、:sm、:md、:lg、:xl五种断点你可以在不同断点下设置列宽比例或是否换行。我在实际项目中的经验是后台系统的窄屏适配不要做太复杂保持内容区最小宽度1000px左右过窄时横向滚动的策略比强行响应式要稳得多。3. 表单从写死到动态配置的完整思路表单在后台项目里占据的代码量通常是最多的。Element UI的el-form组件把表单校验、布局、数据绑定做了一套完整的方案但真正用得好的人不多。这一节我按基础写法 - 校验规则 - 动态配置 - 重置与默认值的顺序讲这一路走通了大部分业务表单都能顺畅搞定。3.1 基础表单的写法与数据绑定一个标准表单包含三部分el-form负责整体行为el-form-item负责单行布局和校验展示内部组件el-input、el-select、el-date-picker等负责具体输入。数据绑定用v-model但很多人会忽略一点el-form上应该绑定:modelel-form-item上应该绑定prop。prop的作用有两个一是关联校验规则二是在resetFields时能定位到对应字段。el-form :modelformData :rulesrules refformRef label-width100px el-form-item label用户名 propusername el-input v-modelformData.username placeholder请输入用户名/el-input /el-form-item el-form-item label邮箱 propemail el-input v-modelformData.email placeholder请输入邮箱/el-input /el-form-item /el-form需要注意一个细节el-form-item的prop必须和formData里的字段名一致否则校验和重置都会失效。这在动态表单里尤其容易出错后面会专门讲。3.2 表单校验规则rules的三种形态Element UI的校验规则底层基于async-validator库有三种写法。第一种是直接在模板里用required属性适合只需要判断是否必填的简单场景。第二种是在el-form上绑定:rules对象最常见const rules { username: [ { required: true, message: 用户名不能为空, trigger: blur }, { min: 3, max: 12, message: 用户名长度为3到12个字符, trigger: blur } ], email: [ { type: email, message: 请输入正确的邮箱格式, trigger: blur } ] };第三种是自定义校验器适合格式复杂或需要异步校验的场景比如判断用户名是否已存在。写法是在规则里传validator方法const validateUsername (rule, value, callback) { if (value admin) { callback(new Error(该用户名已被注册)); } else { callback(); } }; const rules { username: [{ validator: validateUsername, trigger: blur }] };这里有一个重要经验trigger的选择直接影响用户体验。blur在输入框失焦时校验change在值变化时校验。对el-input来说两种都常用但对el-select、el-date-picker这类组件blur事件在部分版本里触发不稳定建议用change。3.3 动态表单配置把表单变成数据驱动动态表单是目前很热门的一个需求通常用在审批流、工单系统这类场景里管理员配置表单结构用户按配置填写。先看最简单的动态添加/删除一行el-form-item v-for(item, index) in formData.items :keyitem.id :propitems. index .value :rules{ required: true, message: 该项不能为空, trigger: blur } el-input v-modelitem.value/el-input el-button clickremoveItem(index)删除/el-button /el-form-item这里最关键的是:prop的写法必须用items. index .value这种路径形式当数组中间删除一项后后续项的校验规则会自动跟随新的索引更新。如果你漏了prop或者写成了item.value会发现校验规则不生效或者重置时字段定位错乱。更进一步的动态表单配置是把整个表单的字段配置存在后端前端循环渲染。比如后端返回[ { key: name, label: 姓名, type: input, required: true }, { key: age, label: 年龄, type: input, required: false }, { key: department, label: 部门, type: select, options: [...], required: true } ]前端用一个configList数组结合v-for渲染渲染时需要动态生成rules对象每个字段的规则根据配置动态拼装。做动态表单时有一个容易踩的坑rules是对象字段是动态生成的如果你事先在data里声明了一个空rules对象运行时往里面增加字段Vue的响应式系统可能无法追踪新增的属性。解决办法是用this.$set(this.rules, key, ruleArray)或直接整体重新赋值this.rules newRules。3.4 清空表单、默认值与重置的坑先区分两个APIresetFields和clearValidate。resetFields会把表单重置到初始值并清除校验状态注意是初始值就是你进入页面时formData里的值不是空值。很多人在新增和编辑共用同一个弹窗时踩坑先打开新增弹窗填了一些数据关闭后再打开编辑弹窗发现表单显示的是上次填的内容。原因就是resetFields只回到弹窗初始化时的数据不会主动清空为null或。解决办法有两种一是每次打开弹窗时手动重置数据再调用resetFieldsthis.formData this.getDefaultFormData(); // 手动赋默认空值 this.$nextTick(() { this.$refs.formRef.resetFields(); });二是在组件创建时就给formData赋上默认值让resetFields能回到这个默认状态。clearValidate的作用则更单一只清除校验状态不清除数据适合用户改了字段但校验还没触发的场景。默认值还有一个场景是回显比如编辑时后端返回了age: null但你希望显示0或空字符串这通常需要在接口层做数据清洗而不是在组件里强行判断。Element UI的表单组件对null和undefined的处理不一致有的显示空白有的会显示上一次的值所以回显前统一把空值处理成是最稳的做法。4. 导航菜单后台系统的导航仪导航菜单组件el-menu是后台系统使用频率最高的导航组件。单独用很简单但放进真实项目就涉及路由绑定、菜单高亮、折叠、权限过滤、面包屑联动这一整套逻辑。这一节我重点讲侧边栏菜单的完整实现思路。4.1 侧边栏菜单与路由的绑定最简单的菜单每个el-menu-item的index就是路由路径el-menu :default-active$route.path router el-menu-item index/dashboard首页/el-menu-item el-menu-item index/user用户管理/el-menu-item /el-menu加上router属性后点击菜单会自动跳转到index对应的路由。default-active绑定$route.path页面刷新时菜单能自动高亮当前项。这里要注意default-active是初始化时的值如果你希望路由变化时菜单也动态高亮只绑定$route.path是可以的因为$route.path是响应式的菜单组件会随之更新。多级菜单用el-submenu嵌套子菜单的index要按层级拆开el-submenu index/system template #title系统管理/template el-menu-item index/system/user用户列表/el-menu-item el-menu-item index/system/role角色管理/el-menu-item /el-submenu这里有一个经验index尽量不要用路径之外的字符串因为配合router跳转时index就是路由一旦不匹配就会出现菜单高亮了但路由没跳过去或跳转过不去的问题。4.2 动态菜单生成与权限控制后台系统做权限控制时通常是根据角色动态生成菜单。前端拿到后端返回的菜单树递归渲染成el-menu。核心是写一个递归组件比如SidebarItem.vue接收一个item对象如果是叶子节点就渲染el-menu-item如果有子节点就渲染el-submenu并循环。template el-submenu v-ifitem.children item.children.length :indexitem.path template #title i :classitem.icon/i span{{ item.title }}/span /template sidebar-item v-forchild in item.children :keychild.path :itemchild / /el-submenu el-menu-item v-else :indexitem.path i :classitem.icon/i span slottitle{{ item.title }}/span /el-menu-item /template script export default { name: SidebarItem, props: [item] }; /script动态菜单有一点要注意后端返回的菜单树里path可能不是完整路径比如子菜单返回的是相对路径。如果父级路径是/system子级返回user最终路由跳转要拼成/system/user。我一般会在前端做一次路径拼接或者要求后端直接返回完整路径避免在递归渲染时反复处理路径拼接逻辑。4.3 菜单高亮、折叠与面包屑联动菜单折叠是后台的标配功能。el-menu的collapse属性控制是否折叠折叠时el-submenu的弹出层默认从左侧浮出但实际体验中偶尔会因为父容器overflow: hidden导致弹出层被裁剪。解决办法是给el-menu所在的父容器留出可溢出的空间或者使用popper-class自定义弹出层样式。面包屑联动一般这样实现在路由配置里为每个路由添加meta.title然后在面包屑组件里根据$route.matched获取层级结构const breadcrumbs this.$route.matched.filter( item item.meta item.meta.title );菜单高亮容易出现一个问题当路由是/system/user/detail这种详情页时侧边栏菜单应该高亮/system/user但default-active$route.path匹配不到精确路径。解决方法是维护一个路径映射关系或者让后端返回的菜单结构里包含一个activePath字段指定当前菜单应该高亮的菜单路径。我踩过这个坑之后通常会在路由守卫里根据$route.path计算出菜单的activeMenu值然后传给el-menu的default-active。5. 表格后台系统最重的组件实战表格基本是后台系统里使用成本最高的组件。不是说基础渲染难而是涉及固定列、合并、自适应宽度、合计行、大数据性能这些具体需求时细节问题层出不穷。Element UI的el-table功能很强大但它的API设计有些反直觉很多功能需要组合使用才能达到效果。5.1 基础表格与列配置最基础的表格写法el-table :datatableData border stripe el-table-column propname label姓名 width120/el-table-column el-table-column propage label年龄 width80/el-table-column el-table-column propaddress label地址 min-width200/el-table-column /el-table这里有两个属性要分开理解width是固定宽度min-width是最小宽度。当列内容很长时设了width的列会横向挤压其他列甚至溢出设了min-width的列则会自适应弹性分配剩余空间。关于列宽还有一个常见的需求是表格自适应宽度。很多新手会问怎么让表格宽度跟随容器变化其实el-table默认就是width: 100%前提是el-table-column里至少有一列使用min-width而不是width。如果所有列都用固定宽度容器变宽时表格不会自动撑满右侧会出现空白带。解决方法是把最后一列或内容不确定的列设为min-width这是我处理表格自适应的首选方案。5.2 固定列、固定表头与底部重叠的解法固定列用fixed属性比如想让操作列固定在右侧el-table-column label操作 fixedright width150 template #default{ row } el-button clickhandleEdit(row)编辑/el-button /template /el-table-column固定列和固定表头同时使用时有个著名的Bug当表格滚动到底部时固定列区域和表头底部会出现一条重叠线表头看起来被固定列压住了一截。这个问题在Element UI的某个版本里很常见网上搜element-ui 表格固定列和底部重叠能找到很多讨论。根因是固定列渲染时会在表格底部附加一层阴影和一个额外的滚动条占位当内容区滚动到最底部时这个占位层和表头的z-index层级冲突。我实测后推荐的解决方法有两种。第一种是给表格加一个样式补丁让固定列底部不显示阴影层.el-table__fixed-right { bottom: 0 !important; box-shadow: none !important; }第二种更彻底是升级到修复该问题的Element UI版本2.13.0之后或者在使用固定列的同时避免只固定一列而留出右侧空隙。如果你遇到固定列和表格主体对不齐的问题先检查表格是否有列宽为width且数据里的某些字段值为nullnull值会导致单元格宽度计算异常可以对数据进行null转的预处理。5.3 行合并与列合并的完整示例表格合并是另一个高频需求典型场景是同一个公司下有多条人员记录希望公司单元格跨行合并。el-table提供了span-method属性可以自定义单元格的合并行数和列数。文档里的示例比较抽象我写一个实际可用的例子// 假设数据结构是 [{ company: A公司, name: 张三 }, { company: A公司, name: 李四 }] // 先计算每个公司出现次数生成一个合并映射 const getSpanArr (data) { const spanArr []; let pos 0; data.forEach((item, index) { if (index 0) { spanArr.push(1); pos 0; } else { if (data[index].company data[index - 1].company) { spanArr[pos] 1; spanArr.push(0); } else { spanArr.push(1); pos index; } } }); return spanArr; }; const spanMethod ({ row, column, rowIndex, columnIndex }) { if (columnIndex 0) { const spanArr getSpanArr(this.tableData); const rowSpan spanArr[rowIndex]; return { rowspan: rowSpan, colspan: 1 }; } return { rowspan: 1, colspan: 1 }; };注意span-method方法会被每个单元格调用一次如果函数里每次都重新计算getSpanArr数据量大的时候开销不小。建议在数据加载时就把合并结果算好存起来spanMethod里直接查表。合并后还要考虑一个问题合并行时只有第一行显示数据其余行的数据被隐藏了底部合计行统计时如果用了row的原始值会出现重复计数所以合计行需要自己根据原始数据重新聚合不能直接用合并后的展示结果。列合并的场景相对少但原理一样columnIndex判断哪一列需要合并返回colspan即可。据我所知Element UI的colspan跨列合并对表头header-cell也支持需要在header-span-method里实现但实操中我发现表头合并对列宽要求比较苛刻很容易出现列宽错位尽量少用。5.4 合计行、自适应宽度与样式微调合计行有两种方式。普通模式是在表格数据里加一行合计数据缺点是排序时合计行也会参与排序要在排序逻辑里排除它。推荐用法是show-summary属性让表格自动计算数值列的合计el-table :datatableData show-summary :summary-methodgetSummary自定义summary-method时params包含columns和data你可以遍历data计算总和const getSummary ({ columns, data }) { return columns.map((col, index) { if (index 0) return 合计; const values data.map(item Number(item[col.property])); const sum values.reduce((acc, cur) acc (isNaN(cur) ? 0 : cur), 0); return sum; }); };这里有个坑show-summary默认对每一列都求合计如果某列不是数值列比如备注列需要显式在summary-method里返回空字符串或--。还有如果表格启用了固定列合计行的固定列部分也可能出现与主表对齐偏移的问题处理思路和固定列重叠问题一致。自动列宽是另一个常见需求。Element UI没有内置根据内容自动计算列宽的API常见的做法是遍历数据找到该列最长内容根据字符长度估算宽度。中文宽度和英文宽度不同要分开计算const calcColumnWidth (data, prop) { const maxLength Math.max( ...data.map(item (item[prop] ? String(item[prop]).length : 0)) ); // 每个中文字符按24px估算英文字符按14px估算加上padding return maxLength * 24 30; };这个方案在数据量几百条以内表现不错但如果上万条数据遍历求最大长度的成本很高建议只对首屏前几十条用估算值或者直接用min-width让表格自己分配。5.5 大数据量下的渲染性能当表格数据量达到几千条甚至上万条时el-table会出现明显的卡顿尤其是同时使用了固定列、排序和筛选时。Element UI官方给出的方案是el-table的virtual虚拟滚动但这个功能在长期维护中表现并不理想尤其和固定列组合使用容易出现白屏或滚动跳动。我个人的经验是如果数据量在1000条以内直接用el-table没问题超过1000条优先做服务端分页这也是后台项目最常用的方案——每次只加载当前页的20到50条数据。如果必须前端渲染几万条数据可以考虑两种替代方案一是使用专门为大数据场景设计的表格组件比如基于虚拟滚动的第三方表格库但要注意与项目技术栈的兼容性二是做滚动加载或展开加载把表格和分组结合起来默认只显示分组头点击展开才加载子行。再补充一个性能细节el-table在数据更新时会重新渲染所有单元格如果你在表格里放了复杂组件比如每行一个el-select或自定义组件渲染成本会急剧上升。做法是尽量减少行内复杂组件的数量操作列按钮优先使用link类型而不是button类型因为button的样式渲染成本更高。6. 常见问题与避坑实录做Element UI开发这几年我把遇到的高频问题整理成了一份速查表这里挑几个最典型的分享出来都是实际踩过的坑。6.1 高频问题与排查思路问题现象常见原因解决办法resetFields不生效表单字段的prop与数据字段不一致或el-form没绑:model检查prop与formData字段名一致确保el-form-item的prop正确表格固定列底部重叠Element UI版本Bug导致固定列表头与主体错位升级到2.13.0或用CSS覆盖固定列底部阴影菜单刷新后不高亮default-active绑定了初始化时不可变的值改为绑定$route.path这种响应式值确保在路由守卫里更新表单校验不触发trigger设置为blur但对el-select不生效对选择类组件改用change触发动态表单新增行后校验失效新增行的prop路径错误或rules不是响应式用路径格式items. index .field必要时用this.$set重设rules表格自适应宽度失败列全部使用固定width属性部分列改用min-width让表格弹性分配表格合并后合计重复计数合计行直接使用了合并后的row数据合计计算基于原始data不能用展示后的合并行el-container高度塌陷未设置html/body/#app及容器高度为100%在全局样式设置高度链6.2 我个人踩过的一些坑先说一个和表单默认值相关的真实经历。有次做订单审批系统新增和编辑共用同一个弹窗我在弹窗初始化时给formData赋了默认值但新增时后端要求所有字段为空。结果用户新增时看到表单里带着编辑时的残留数据操作员误提交了好几次。后来我改成每次打开弹窗都先执行一次this.formData this.getEmptyFormData()再在$nextTick里调resetFields这个问题才彻底解决。注意resetFields必须在$nextTick里调因为el-form-item的prop追踪需要在DOM渲染之后才生效。再说表格固定列和底部重叠的问题。当时项目用的是2.12.0版本操作列固定右侧表头固定表格滚动到底部时固定列区域和表头交叠处出现一条约1到2像素的阴影线很难看。我用过网上各种CSS hack最稳定的是给固定列容器设置bottom: 0 !important但要注意这会影响到固定列在滚动时的表现。后来升级到2.13.3问题就自然消失了所以遇到这个问题的第一反应应该是查版本而不是改样式。还有El-menu的折叠问题。折叠后如果菜单项有嵌套子菜单默认的弹出层宽度只有固定的200px左右如果子菜单标题太长内容会换行观感很差。我当时的处理是自定义弹出层内容用popper-class指定一个更宽的样式同时把子菜单项的文字用ellipsis处理。这里要留意popper-class的样式必须是全局样式不能写在scoped里因为弹出层被挂载到了body下不是组件内部。7. 关于这套组件库我的一些个人体会用了几年Element UI我对它的定位是后台系统的及格线。它不会让页面变得特别惊艳也基本不会让页面变得特别糟糕只要你按照规范使用就能得到一个中规中矩、稳定可用的后台界面。它的上限取决于你对业务的理解深度而不是组件本身的功能广度。动态表单、表格合并、固定列适配这些能力本质上是对Vue响应式和DOM渲染机制的理解组件库只是提供了一套封装好的API而已。如果你能把这一层的原理搞清楚哪怕有一天换了Element Plus、换了其他框架这些经验依然能直接迁移过去。最后再分享一个处理这类组件库问题的小技巧遇到奇怪的问题先别急着搜怎么解决先想清楚组件是在什么生命周期里渲染的、数据的响应式链路是什么样的、有没有可能是层级嵌套导致的事件冒泡或作用域问题。大部分Element UI的诡异Bug其实都是布局结构或数据流设计不合理导致的理清思路后修起来很快。这套方法论比记住任何单个API细节都重要。
返回列表