列表组件,说简单也简单,说复杂那也确实有点麻烦的地方。
列表组件,主要不是封装 el-table,而是 el-table-column,因为前者设置好各种属性即可,而后者却需要各种思考。
还有就是封装风格,像 el-table 那样,提供一个组件,然后用slot 实现其他各种需求,还是依据需求,封装多个不同功能的组件?
目前采用的方案是 —— 封装多个不同功能的组件:
- 基础功能列表:可以锁定行列、多选、单选、隔行颜色、高亮等
- 可以使用 slot 的列表:使用 slot 设置操作按钮。
- 行内编辑的列表
- 一起编辑:都是 input 上来就可以改。
- 一次只编辑一行:先选一行才能改,可以选择保存或者取消。
大概四种吧。目的是:使用的时候可以简洁一点,参考一下开闭原则,做好功能管理。
el-table-column
这是列表的基础,原生组件需要我们手动一个一个设置,这样做非常灵活可以实现各种需求,但是用起来有点麻烦,所以我们先对他下手。
el-table-column 的 Interface
去官网看了一下 el-table-column 的属性,那叫一个多,不过我们不用都设置到 Interface 里面,只需要挑我们需要的即可,其他可以通过 $attrs 传入。
export interface IGridItem {
id: number | string,
prop: string,
label: string,
width: number,
align: EAlign,
headerAlign: EAlign
}
- id:字段ID、列ID
- prop:字段名称,相当于表单里的 colName
- label:列的标签、标题
- width:列的宽度
- align:内容对齐方式
- headerAlign:列标题对齐方式
做一个 enum 设置左中右:
/**
* 横向对齐方式,左、中、右
*/
export const enum EAlign {
left = 'left',
center = 'center',
right = 'right'
}
json
根据 Interface,我们来做一份json。
itemMeta: {
"90": {
"id": 90,
"colName": "kind",
"label": "分类",
"width": 140,
"title": "分类",
"align": "center",
"header-align": "center"
},
// 其他列
}
这个ID是非常重要的,虽然现在看不出来,其他的就是我们常用的属性了。
el-table
这个很简单,设置好属性即可,我们按照老规矩,设置一个 meta 和 props 的Interface
设置列表的 meta IGridMeta
export interface IGridMeta {
moduleId: number | string,
idName: string,
colOrder: Array<number|string>
}
- moduleId:模块ID
- idName:主键字段的名称
- colOrder:列(字段)显示的顺序
非常简单,主要记录一下这是哪个模块的meta,默认一个模块只有一个列表,以及列表的主键字段名称,对应 el-table 的 row-key 属性。
最后就是字段的显示依据,需要显示哪些字段、以及字段的先后顺序。用数组的形式表示,可以方便调整和更改。
设置列表的 props IGridProps
export interface IGridProps<T> {
gridMeta: IGridMeta,
itemMeta: { [key:string | number]: IGridItem },
selection: IGridSelection,
dataList: Array<T>,
showHeader: boolean,
height: number,
stripe: boolean,
border: boolean,
highlightCurrentRow: boolean
}
- gridMeta:列表的 meta
- itemMeta:列的 mate
- dataList:绑定的数据 Array, 对应 data
- selection:记录选择了哪些数据
- 原生属性
- highlightCurrentRow:要高亮当前行
- border:纵向边框
- stripe:斑马纹
- height:table的高度
- showHeader:是否显示表头,监听用
设计这几个原生属性,是因为想设置默认值,因为我希望表格是带纵向边框、斑马纹、高亮的,但是默认不是。
showHeader,是想监听一下是否显示表头,因为我给表头设置了拖拽事件,隐藏后事件就没了,重新显示后需要再次挂载,所以要监听一下。
选择的数据 IGridSelection
用户选择记录之后,可能去修改、批量删除或者导出等操作,那么就需要一个容器来记录用户选择了哪些记录。
export interface IGridSelection<T> {
dataId: number | string,
row: T,
dataIds: Array<number | string>,
rows: Array<T>
}
- dataId:单选,记录主键ID值
- row:单选,记录选择的row
- dataIds:多选,记录ID集合
- rows:多选,记录 row 的集合
兼容单选和多选,可以只关心选择了哪些ID,也可以获得选择行的数据。
这里前后端的思维好像有点小差别:列表里的row是否记录了全部信息?
后端的习惯是,一般这个 row 不会记录全部信息,比如博客内容、产品介绍等显然不能放进去,所以关心的是ID,通过ID获取全部信息,这样可以完整和实时。
前端嘛,瞎猜一下,可能认为row就是全部内容。当然只是瞎猜。
json 文件
一份完整的json文件:
{
"gridMeta": {
"moduleId": 142,
"idName": "ID",
"colOrder": [ 90, 101, 102, 105 ... ]
},
"gridPros": {
"height": 400,
"stripe": true,
"border": true,
"fit": true,
"highlight-current-row": true
},
"itemMeta": {
// 列的信息
}
}
在json文件里面,列表的 props 集中放置,这样便于维护json和扩展其他属性,不会分散开。好吧,这几个都设置默认值了,可以不填的。
实现基础功能的表格
基础功能都有哪些?
- 行列
- 锁定行列
- 可以多选和单选
- 记录选择的数据
其他的都作为扩展功能。
<el-table
ref="refControl"
style="width: 100%"
:height="height"
v-bind="$attrs"
:data="dataList"
:row-key="gridMeta.idName"
@selection-change="selectionChange"
@current-change="currentChange"
>
<!--显示选择框-->
<el-table-column
type="selection"
width="55"
align="center"
header-align="center"
>
</el-table-column>
<!--显示字段列表-->
<el-table-column
v-for="(id, index) in gridMeta.colOrder"
:key="'grid_list_' + index + '_' + id"
:min-width="50"
:column-key="'col_' + id"
v-bind="itemMeta[id]"
:prop="itemMeta[id].colName"
>
</el-table-column>
<!--右面的操作列-->
</el-table>
遍历集合,用 v-for 创建列表的列。是不是很简单。我们只需要准备好一份json文件,然后设置属性即可。
效果:
行内操作按钮
我个人是喜欢把操作按钮放在列表的上面,添加、修改、删除、查看、导出等放上一排,需要哪个按哪个。
但是客户说,我想修改的时候,先选择一行,然后再去上面找按钮,太麻烦。
你猜怎么着,我会把修改按钮放在行里面吗?才不,我加了一个双击行弹窗修改的功能。
好吧,不开玩笑了,虽然那时候真的是这么干的,但是现在不行了,不可能每个客户都那么好说话。
不过,我觉得,封装最重要的是,要有限度,不能啥都往内部封装,早晚爆炸,你看el-table 都没有把按钮封装在里面,而是通过 slot 实现的,所以我们也可以借花献佛。
传递 el-table-column 的 slot
el-table-column 的插槽是匿名插槽,因为只需要应该是个字段即可,但是封装之后却需要应对多个字段,那么怎么办呢?给匿名插槽起个名字,然后转换一下即可。
插槽规则:
- 字段名作为插槽名称,对应 el-table-column 的匿名插槽
- header_字段名作为插槽名称,对应 el-table-column 的 header 插槽
- option 作为插槽名称,对应操作列
外部设置插槽
<nf-grid
v-bind="gridMeta"
:dataList="dataList"
>
<!--普通字段,用字段名作为插槽的名称-->
<template #header_text>
扩展表头
</template>
<template #text="scope">
<span style="margin-left: 10px">扩展1:{{ scope.row.text }}</span>
</template>
<!--普通字段-->
<template #week="scope">
<span style="margin-left: 10px">{{ scope.row.week.replace('-w','年 第') + '周' }}</span>
</template>
<!--操作按钮-->
<template #option="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button
type="danger"
@click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</nf-grid>
这样就可以像 el-table 那样,灵活设置各种列了。
内部转换 插槽
那么内部如果做转换呢?
<el-table-column
v-if="(slotsKey.includes(itemMeta[id].colName))" // 如果插槽名称包含字段名称,说明该字段需要插槽
:column-key="'col_' + id"
v-bind="itemMeta[id]"
>
<template #header
v-if="slotsKey.includes('header_' + itemMeta[id].colName)" // 如果有header,设置header插槽
>
<slot :name="'header_' + itemMeta[id].colName"></slot>
</template>
<template #default="scope"> // 设置匿名插槽
<slot :name="itemMeta[id].colName" v-bind="scope"></slot> // 加载外部插槽内容,并且传出 scope
</template>
</el-table-column>
这样就可以做一个 slot 的中转,把外部设置的插槽转送给 el-table-column,把 scope 转送给外部。
转送组件的方法
UI库的组件一般都会提供一些方法,那么如果把方法都转送出去呢?有一个通用的方法:
export const myExpose = () => {
const refControl = ref(null)
const expose = {}
onMounted(() => {
Object.assign(expose, refControl.value)
})
return {
refControl,
expose
}
}
- refControl:接收组件传出来的信息
- expose:设定一个转出的容器
- onMounted:组件挂载后加载组件提供的方法到容器
defineExpose 不能放在外部,否则也可以放在这里。
封装组件里的使用方法给外部
首先引入 myExpose,然后设置两行即可:
const { refControl, expose } = myExpose()
defineExpose({ expose })
最后别忘了给el-table设置ref。这种方式支持各种组件,保证通用。
<el-table
ref="refControl">
...
原文链接:https://juejin.cn/post/7244810003576471613 作者:金色海洋