0


“El-Table二次封装“这样做【高级前端必备技能之一】

🔥 前言

这篇文章给大家分享一个高级自定义列表组件从0到1的开发过程,这个列表组件的主要功能有,列表拖拽排序,右侧操作按钮统一使用Tooltip展示,操作表头增加自定列表icon,点击icon可以对列表展示数据进行是否显示、排序等操作,契合业务需求,增加表格美观以及复用性。

🔥关与自定义表格

随着系统业务复杂度的提高,列表需要展示的数据变得复杂,常见的El-table逐渐不能满足我们系统的日常使用,更为关键的是El-table 在使用的过程种比较复杂,需要书写大量的< ,并且不能满足我们的系统UI,所以我们决定对El-table进行高度的自定义二次封装,使得团队同学在使用的时候更加便捷、容易,同时也满足了我们系统统一页面风格的需求,下面就给大家介绍我们实现的详细过程。

🔥效果图展示:

在这里插入图片描述✨表格拖转排序
在这里插入图片描述
✨右侧操作按钮显示UI
在这里插入图片描述
✨自定义表头字段弹框
在这里插入图片描述

🔥基本使用

index.vue

<template><!-- 列表 --><div style="height: calc(100% - 155px)"><h-table
            ref="selectionTableRef"
            v-loading="loading":border="true":columns="state.table.columns":custom-list="customList":is-custom-list="true":table="state.table"
            operatorTheme="useless"
            stripe
            @saveCustomList="saveCustomList"</h-table></div></template><script setup>const state =reactive({table:{total:0,pageNo:1,pageSize:10,pageSizes:[10,20,50,100],// 接口返回数据data:[],// 表头数据columns:[],// 多选firstColumn:{type:'selection',fixed:'left'},// 操作列样式operatorConfig:{fixed:'right',// 固定列表右边(left则固定在左边)width:100,label:'操作',},operator:[{text:'查看',fun:(val)=>look([val]),show:[{key:'status',val:['new']}],},{text:'编辑',style:{color:'#f56c6c'},fun:(val)=>edit([val]),show:[{key:'status',val:['new']}],},],},})// 列配置const customList =reactive({//列表数据allColumns:[{label:'姓名',prop:'name',key:'name',width:150,fixed:'left',},{label:'性别',prop:'sex',key:'sex',fixed:'left',minWidth:280,},{label:'年龄',prop:'age',key:'age',minWidth:100,},{label:'时间',prop:'date',key:'date',minWidth:160,}],//自定义表头左侧数据allData:[{title:'全部',children:[{key:'name',title:'姓名'},{key:'sex',title:'性别'},{key:'age',title:'年龄'},{key:'date',title:'时间'},],},],//自定义表头右侧可拖拽数据defaultCheckData:[{key:'name',title:['姓名']},{key:'sex',title:['性别']},{key:'age',title:['年龄']},{key:'date',title:['时间']},],})</script>

🔥 碎碎念

看了上面的组件使用是不是觉得,使用起来非常简单,而且在模板层面可以减少很多HTML内容的书写

🔥内部实现

<template><div class="h-table"><el-table ref="TTable":data="state.tableData":scrollbar-always-on="scrollbarAlwaysOn":size="size":highlight-current-row="highlightCurrentRow":border="border || table.border || isTableBorder"
            @cell-dblclick="cellDblclick"
            @row-click="rowClick":cell-class-name="cellClassNameFuc":tooltip-options="tooltipOptions" v-bind="{...$attrs,class:{cursor: isCopy,highlightCurrentRow: highlightCurrentRow,radioStyle: table.firstColumn && table.firstColumn.type ==='radio',outerBorder:!border &&!(table.border || isTableBorder),},style:''}" style="height:100%;"><!-- 行拖拽列 --><template v-if="table.lockColumn"><el-table-column type="lock":width="table.lockColumn.width || 55":fixed="table.lockColumn.fixed"><template #header><slot :name="table.lockColumn.slotName + '_header'"><el-icon @click.stop="lockChange($event)"><Lock v-show="isLock":style="{ 'color': table.lockColumn.lockDefaultColor || '#006ef0' }"class="pointCursor"/><Unlock v-show="!isLock":style="{ 'color': table.lockColumn.lockActiveColor || '#3ccda0' }"class="pointCursor"/></el-icon></slot></template><template #default="scope"><!-- 自定义插槽 --><slot :name="table.lockColumn.slotName":scope="scope"><el-icon><Rank
                                    :style="{ 'color': isLock ? table.lockColumn.rankDefaultColor || '#8d9399' : table.lockColumn.rankActiveColor || '#006ef0' }":class="{ 'pointCursor': isLock ? false : true }"/></el-icon></slot></template></el-table-column></template><!-- 复选框/单选框/序列号 --><template v-if="table.firstColumn"><!-- 复选框 --><el-table-column v-if="table.firstColumn.type === 'selection'":type="table.firstColumn.type":width="table.firstColumn.width || 55":reserve-selection="table.firstColumn.isPaging || false":label="table.firstColumn.label":align="table.firstColumn.align || 'center'":fixed="table.firstColumn.fixed":selectable="table.firstColumn.selectable"/><!-- 单选框 --><el-table-column v-if="table.firstColumn.type === 'radio'":type="table.firstColumn.type":width="table.firstColumn.width || 55":label="table.firstColumn.label":fixed="table.firstColumn.fixed":align="table.firstColumn.align || 'center'"><template #default="scope"><el-radio v-model="radioVal":value="scope.$index + 1"
                            @click.stop="radioChange($event, scope.row, scope.$index + 1)"></el-radio></template></el-table-column><!-- 序列号 --><el-table-column v-if="table.firstColumn.type === 'index'":type="table.firstColumn.type":width="table.firstColumn.width || 55":label="table.firstColumn.label || '序号'":fixed="table.firstColumn.fixed":align="table.firstColumn.align || 'left'"><template #default="scope">{{isShowPagination?(table.pageNo -1)* table.pageSize + scope.$index +1: scope.$index +1}}</template></el-table-column></template><!-- 主体内容 --><template v-for="(item, index) in renderColumns"><template v-if="!item.children"><!-- 常规列 --><el-table-column v-if="item.permission === falseitem.permission : true":key="index + 'i'":type="item.type":label="item.label":prop="item.prop":width="item.width":min-width="item.minWidth || 90":sortable="item.sortable":align="item.align || 'left'":fixed="item.fixed":show-overflow-tooltip="item.noShowTip === false ? item.noShowTip : true"><template #header v-if="item.slotHeader"><slot :name="item.prop + '_header'">{{ item.label }}</slot></template><template #default="scope"><!-- formatter渲染 --><template v-if="item.formatter">{{ item.formatter({[item.prop]: scope.row[item.prop],item: scope.row,index: scope.$index })}}</template><!-- render渲染 --><template v-if="item.render"><render-col :column="item":row="scope.row":render="item.render":index="scope.$index"/></template><!-- 自定义插槽 --><template v-if="item.slotName"><slot :name="item.slotName":scope="scope":index="scope.$index"></slot></template></el-table-column></template></template><slot></slot><!-- 操作按钮 --><template><el-table-column v-if="table.operator || table.operatorConfig?.onlySetting":fixed="table.operatorConfig?.fixed":width="56":min-width="56":align="table.operatorConfig?.align || 'left'"class-name="operator operator_useless"><template #header><div class="operator-menu":class="{ 'operator-menu-disable': !customList.allData.length }"
                            v-if="isCustomList"><el-icon class="icons" @click="openCustomList"><Setting /></el-icon></div></template><template #default="scope"><el-popover placement="left" effect="customized" popper-class="mtable_operator_useless_popover":offset="0"
                            trigger="hover" v-if="operatorList(scope).length"><template #reference><!--@click="scope.row.popVisible = !scope.row.popVisible"--><div class="useless-popover-icon"><el-icon><Operation /></el-icon></div></template><template #default><div class="operator_useless_btn" v-for="(item, index) in operatorList(scope)":key="index"><template v-if="!item.slot"><m-button @click="clickOperationBtn(item, scope.row, scope.$index)":type="item.type ? item.type : 'primary'"link:style="filStyle(item)":icon="item.icon ? item.icon : ''":disabled="item.disabled":size="item.size ? item.size : 'default'":title="item.title":class="{ defbtn: !item.disabled && !item.style }"><!-- render渲染 --><template v-if="item.render"><render-col :column="item":row="scope.row":render="item.render":index="scope.$index"/></template><span v-if="!item.render">{{ item.text }}</span></m-button></template><!-- 插槽 --><template v-else><slot :name="item.slot":scope="{ row: scope.row }":index="scope.$index"></slot></template></div></template></el-popover></template></el-table-column></template></el-table><!-- 自定义列表弹窗 --><m-custom-list ref="customlisttable":allData="customList.allData":append-to-body="appendToBody"
            @save="saveCustomList"/></div></template><script setup>import{ ElMessage }from'element-plus'import{ get }from'lodash-es'import{ Setting, CaretBottom, Operation, MoreFilled, Unlock, Rank, Lock }from'@element-plus/icons-vue'import RowDrag from'sortablejs'defineOptions({name:'HTable',})const props =defineProps({// table所需数据table:{type: Object,default:()=>{},required:true,},// 表头数据columns:{type: Array,default:()=>[],// required: true},// 表格标题title:{type: String,},// 是否复制单元格isCopy:{type: Boolean,default:false,},// 是否开启点击整行选中单选框rowClickRadio:{type: Boolean,default:true,},// 是否开启编辑保存按钮isShowFooterBtn:{type: Boolean,default:false,},// 是否高亮选中行highlightCurrentRow:{type: Boolean,default:false,},// 是否开启合计行隐藏复选框/单选框/序列isTableColumnHidden:{type: Boolean,default:false,},border:{type: Boolean,default:false,},// 尺寸风格size:{type: String,default:'default',},// tooltip风格配置tooltipOptions:{type: Object,default:()=>{return{effect:'light',offset:0,}},},// 是否需要自定义列表操作isCustomList:{type: Boolean,default:false,},// 自定义列表配置customList:{type: Object,default:()=>{return{allData:[],}},},// 默认选中的数据defRadioObj:{type: Object,default:()=>{},},// 按钮权限数组btnPremList:{type: Array,default:()=>[],},// 是否长显滚动条scrollbarAlwaysOn:{type: Boolean,default:true,},// 自定义列表弹窗是否放到body下appendToBody:{type: Boolean,default:true,},// 操作栏样式主题 default-默认、uselessoperatorTheme:{type: String,default:'default',},})// 初始化数据let state =reactive({tableData: props.table?.data ||[],columnSet:[],})// 单选框const radioVal =ref(null)// 判断单选选中及取消选中const forbidden =ref(true)// 获取refconst TTable =ref(null)// 抛出事件const emits =defineEmits(['save','size-change','page-change','handleEvent','radioChange','saveCustomList','lockChange','dropRow',])// 获取所有插槽const slots =useSlots()watch(()=>[props.table?.data, props.defRadioObj],(val)=>{
        state.tableData = val[0]
        radioVal.value =null// 重置选中下标if(val[0]?.length && val[1]&& Object.keys(val[1]).length){const obj =deepClone(val[1])const _key = obj.key
            const _value = obj.value
            if(_value ===undefined|| _value ===null){
                radioVal.value =null}else{
                val[0].forEach((it, idx)=>{if(it[_key]=== _value){
                        radioVal.value = idx +1}})}}else{
            radioVal.value =null}},{immediate:true,deep:true},)// 处理操作按钮,判断权限且整合是否展示更多const operatorList =computed(()=>{returnfunction(scope){// console.log('scope: ', scope);const _op = props.table.operator
        // 过滤掉没有不展示的数据const _nop =[]
        _op.forEach((_opit, _opidx)=>{if(checkIsShow(scope, _opit)){
                _nop.push({..._opit,popVisible:false})}})let moreList =[]if(props.operatorTheme ==='default'){// 处理到“更多”if(_nop.length <=2)return _nop
            let newArray ={more:true,children:[..._nop.slice(2)],}
            moreList =[_nop[0], _nop[1], newArray]}else{
            moreList = _nop
        }return moreList
    }})// 点击操作按钮的回调constclickOperationBtn=(item, scoprow, scopindex)=>{// console.log(item, scoprow, scopindex)// scoprow.popVisible = falsereturn item.fun && item.fun(scoprow, scopindex, state.tableData)}// 更多按钮展开/收起操作-选中效果const isOpenMorebtn =ref(false)const openMoreIndex =ref(null)constchangeMorebtn=(e, index)=>{
    isOpenMorebtn.value = e
    openMoreIndex.value = index
}// 处理按钮颜色const filStyle =computed(()=>{returnfunction(item){const _color = props.operatorTheme ==='default'?'color: #006ef0':'color: #fff'return!item.disabled ? item.style ? item.style : _color :''}})// 判断如果有表头合并就自动开启单元格缩放const isTableBorder =computed(()=>{return props.columns.some((item)=> item.children)})// 处理回显数据const fileValue =computed(()=>{returnfunction(row, prop, unit){const _data =get(row, prop)return _data || _data ===0?`${ _data }${ unit ||''}`:'/'}})// 合并行隐藏复选框/单选框constcellClassNameFuc=({ row })=>{if(!props.isTableColumnHidden){returnfalse}if(
        state.tableData.length -(state.tableData.length - props.table.pageSize <0?1: state.tableData.length - props.table.pageSize)<=
        row.rowIndex
    ){return'table_column_hidden'}}// forbidden取值(选择单选或取消单选)constisForbidden=()=>{
    forbidden.value =falsesetTimeout(()=>{
        forbidden.value =true},0)}// 单选抛出事件radioChangeconstradioClick=(row, index)=>{
    forbidden.value =!!forbidden.value
    if(radioVal.value){if(radioVal.value === index){
            radioVal.value =nullisForbidden()// 取消勾选就把回传数据清除emits('radioChange',null, radioVal.value)}else{isForbidden()
            radioVal.value = index
            emits('radioChange', row, radioVal.value)}}else{isForbidden()
        radioVal.value = index
        emits('radioChange', row, radioVal.value)}}// 判断是否使用漏了某个插槽constisShow=(name)=>{return Object.keys(slots).includes(name)}// 整行编辑返回数据constsave=()=>{emits('save', state.tableData)return state.tableData
}constonMouseOver=(event, item)=>{const{ offsetWidth, offsetLeft }= event.target
    const pOffsetWidth = event.fromElement.offsetWidth
    const width = item.minWidth ? item.minWidth : pOffsetWidth
    // console.log(event)
    width < offsetWidth + offsetLeft *2?(item.showSelfTip =true):(item.showSelfTip =false)}/**
 * 公共方法
 */// 清空排序条件constclearSort=()=>{return TTable.value.clearSort()}// 取消某一项选中项consttoggleRowSelection=(row, selected =false)=>{return TTable.value.toggleRowSelection(row, selected)}// 清空复选框constclearSelection=()=>{return TTable.value.clearSelection()}const customlisttable =ref(null)// 打开自定义列表constopenCustomList=()=>{if(props.customList.allData.length){
        customlisttable.value.open(props.customList.defaultCheckData)}}// 提交自定义列表的保存的数据constsaveCustomList=(val)=>{emits('saveCustomList', val)}// 重新布局表格constdoLayout=()=>{
    TTable.value.doLayout()}// 解锁或者锁定行拖拽const isLock =ref(props.table?.lockColumn?.isLock ||true)constlockChange=(val)=>{
    isLock.value =!isLock.value
    if(isLock.value){destroyDrop()}else{rowDrop()}emits('lockChange', isLock.value)}const tbodyObj =ref(null)const tbody =ref(null)// 拖拽传参constrowDrop=()=>{if(isLock.value)return
    tbody.value = document.querySelector(".el-table__body-wrapper tbody")if(tbody.value){
        tbodyObj.value = RowDrag.create(tbody.value,{animation:300,onEnd:({ newIndex, oldIndex })=>{emits('dropRow',{oldIndex: oldIndex,newIndex: newIndex,data: TTable.value,})},})}}// 销毁RowDragconstdestroyDrop=()=>{if(tbodyObj.value instanceofRowDrag){
        tbodyObj.value.destroy()}}onMounted(()=>{rowDrop()})// 暴露方法出去defineExpose({ clearSelection, toggleRowSelection, clearSort, doLayout })</script>

🔥Style 样式

<style lang="scss" scoped>
$table-border-color: #f0f3f5;.h-table{height: 100%;z-index: 0;:deep(.el-table::before){background: none;}:deep(.el-table__body-wrapper){background: #f7fbfe;.el-table__body{margin: 0;// 操作按钮部分
            .operator{.cell{height: 100%;display: flex;align-items: center;padding: 0 !important;}&_btn{height: 100%;display: flex;align-items: center;padding: 0 10px;.el-button{
                        // width: 82px;height: 16px;margin: 0;padding: 0 10px;border-right: 1px solid #dadee6;&:first-child{padding-left: 0;}&:last-child{border: none;padding-right: 0;&:hover{border: none;}}&:hover{border-right: 1px solid #dadee6;}}.oper_mor_btn{width: 40px;height: 100%;padding-left: 0;& > span{width: 100%;height: 100%;}.morebtn{width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;&-title{width: 100%;height: 100%;display: inline-flex;justify-content: center;align-items: center;& > i{color: #8d9399;transform:rotate(90deg);margin-left: -2px;}&:focus-visible{outline: none;}}}}.oper_mor_btn_active{

                        // background: #61a3f2;.morebtn{&-title{& > i{
                                    // color: #fff;color: #61a3f2;}}}}}}// useless模式时的操作
            .operator_useless{.cell{.useless-popover-icon{width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;cursor: pointer;& > .el-icon{font-size: 16px;}&:hover{& > .el-icon{color: #61a3f2;}}}}}}}:deep(.mpagination){margin-top: 12px;background-color: transparent;}:deep(.el-popper){max-width: 600px;}// 某行隐藏复选框/单选框
    :deep(.el-table){.el-popper{font-size: 14px;}.el-table__row{.table_column_hidden{.cell{.el-radio__input,
                    .el-checkbox__input{display: none;}& > span{display: none;}}}}}.el-table th,
    .el-table td{padding: 8px 0;}.el-table--border th:first-child .cell,
    .el-table--border td:first-child .cell{padding-left: 5px;}.el-table--scrollable-y .el-table__fixed-right{right: 8px !important;}.header_wrap{display: flex;align-items: center;.toolbar_top{flex: 0 70%;display: flex;align-items: center;justify-content: flex-end;.toolbar{display: flex;justify-content: flex-end;width: 100%;}.el-button--small{height: 32px;}.el-button--success{background-color: #355db4;border: 1px solid #355db4;}}.header_title{display: flex;align-items: center;flex: 0 30%;font-size: 16px;font-weight: bold;line-height: 35px;margin-left: 10px;}}.marginBttom{margin-bottom: -8px;}// 表格外边框
    .outerBorder{border: 1px solid $table-border-color;}// 单选样式
    .radioStyle{:deep(.el-table__header){.el-table__cell:first-child{border-right: 0;.cell{display: none;border-right: 0 !important;}}}:deep(.el-radio){&:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{box-shadow: none;}}:deep(tbody){.el-table__row{cursor: pointer;}}}// 复制功能样式
    .cursor{:deep(tbody){.el-table__row{cursor: pointer;}}}// 表格样式调整
    :deep(.el-table){.el-table__header{margin: 0;.el-table__cell{height: 50px;font-size: 16px;background: #e5f0fd;border-right: 1px solid #ecf4fe;.cell{height: 26px;line-height: 26px;color: #666;font-weight: normal;}&:last-child{border-right: 0;}}.el-table-column--selection{.cell{border-right: 0 !important;}}}.el-table__body-wrapper{.el-scrollbar__view{height: 100%;}.el-table__body{.el-table__row{.el-table__cell{height: 40px;padding: 0;border-bottom: 1px solid #e7edf9;border-right: 0 !important;// 取消展开图标的旋转
                        .el-table__expand-icon{.el-icon{display: none;}transform:rotate(0deg);&::before{content:'';display: block;width: 16px;height: 16px;margin-top: -1px;background:url('../../../images/open.png') no-repeat center top;background-size: 100% 100%;}}// 展开节点
                        .el-table__expand-icon--expanded{&::before{content:'';width: 16px;height: 16px;margin-top: -1px;background:url('../../../images/up.png') no-repeat center top;background-size: 100% 100%;}}}}}&.el-table--default .cell{padding: 0 16px;}.el-table__row--striped{.el-table__cell{background: #f5f9fe;}}}}:deep(.el-table.el-table--border){.el-table__header{.el-table__cell{.cell{border-right: 1px solid #bcd0f2;color: #282d32;font-weight: 500;}}.is-group{.el-table__cell{.cell{border-right: 0;}}}}}// 操作头部
    .operator{.cell{padding: 0 !important;// 操作样式
            .operator-title{display: flex;
                // justify-content: center;align-items: center;}.operator-menu{width: 20px;height: 20px;position: absolute;right: 16px;top: 15px;z-index: 2;cursor: pointer;.icons{color: #505363;font-size: 20px;&:hover{color: #409eff;}}}.operator-menu-disable{cursor: default;.icons{&:hover{color: #505363;}}}}}.operator_useless{.cell{.operator-menu{right: 18px;}}}// 页面缓存时,表格内操作栏每行高度撑满
    :deep(.el-table__fixed-right){height: 100% !important;}// 选中行样式
    .highlightCurrentRow{tbody{:deep(.el-table__row){cursor: pointer;}.current-row td{cursor: pointer;color: #fff;background-color: #355db4 !important;}}}.el-table--scrollable-y .el-table__body-wrapper{overflow-x: auto;}.handle_wrap{position: sticky;z-index: 10;right: 0;bottom: -8px;margin: 0 -8px -8px;padding: 12px 16px;background-color: #fff;border-top: 1px solid #ebeef5;text-align: right;.el-btn{margin-left: 8px;}}.pointCursor{cursor: pointer;}}</style>
<style lang="scss">
.morebtn-popper{.el-dropdown-menu{padding: 4px !important;.el-dropdown-menu__item{height: 32px;&:hover{background: #eaf3fc;}.el-button{width: 100%;height: 100%;}}}}.mtable_operator_useless_popover{width: inherit !important;display: flex;margin-bottom: -4px;padding: 11px 4px !important;min-width: inherit !important;&.is-customized{background:rgba(99, 108, 128, 0.9)!important;.el-popper__arrow::before{background:rgba(99, 108, 128, 0.9)!important;}}.operator_useless_btn{height: 16px;display: flex;align-items: center;margin: 0 12px 0;position: relative;.el-button{font-size: 14px;padding: 0;}.el-button.defbtn:hover{color: #52abff !important;}&:not(:first-child)::before{content:'';position: absolute;width: 1px;height: 12px;background: #949ba9;left: -12px;top: 50%;transform:translateY(-50%);}}}
</style>
标签: 前端 vue.js elementui

本文转载自: https://blog.csdn.net/weixin_43742274/article/details/140102014
版权归原作者 骨灰级尤雨溪迷弟~ 所有, 如有侵权,请联系我们删除。

““El-Table二次封装“这样做【高级前端必备技能之一】”的评论:

还没有评论