如何基于el

您所在的位置:网站首页 什么情况下用函数封装比较好 如何基于el

如何基于el

2024-07-17 13:23:29| 来源: 网络整理| 查看: 265

本篇博客属于随手一写,建议主要看模板和类型定义那块的实现思路。另外代码中会有部分不规范的地方,只优化了一部分,比如在template里写了些逻辑代码,类型有些用了any等等,最近总感觉自己欠了很多技术债没还,但是周末又不想去还(/doge)。

后面会出一期合辑,分享一下从零开始搭建组件库的过程,包含从组件写法到打包到发布npm私服的全过程。中间会穿插一些重点比如组件国际化兼容、打包、如何按需引入等等等等,这次是认真的,一定要更出来!

之前听到很多人讲在用 element-ui 的 table 组件时,还是自己循环 el-table-column 去实现的!每个页面都这么写想想就头大,另外万一产品要求你项目中所有的表格都要扩展一个功能,比如说按shift,可以实现数据行多选?那岂不是当场傻眼!

我们完全可以基于el-table去封装一个组件,基于配置自动生成表格。

配置都提供哪些属性呢?element-ui 每个组件都提供了一份 types文件,每个组件支持的属性参数,我们都可以二次封装后暴露给使用者。没错就是用到了 ts 的继承和 Partial 关键字,好奇的可以直接去看类型文件那部分。

所以以此作为基础,我们完全不用担心自己二次封装会导致某些属性或者事件覆盖不到位,需要什么功能的时候,再往这个组件里加对应的属性或者 emit 相应的事件就好了。

对于这里属性覆盖不到位而且还需要通过prop一个一个把属性配置传进来的情况,感觉评论区老哥的提示,我们可以采用透传的方式,参考 掘金的这篇文章 如何实现组件属性透传?

简单的讲下实现思路: 在这里插入图片描述 上图是el-table官网提供的用法示例,首先看到这么多重复的 el-table-column 标签,我下意识的反应就是用循环替换重复的代码,对于标签上的属性不同怎么解决呢?

我们可以提供一个对象数组形如 [{ prop: 'date', label: '日期', minWidth: '180', }],在对象内配置prop、 label 、width属性。然后循环这个对象数组即可。同理 el-table-column 提供的所有可配置属性我们都可以配置在对象里。

对于el-table的配置项我们也可以在data里提供一个对象,放这些配置项。

那么问题来了,el-table和el-table-column有这么多属性呢,我正常在标签上写的时候还有个代码提示,现在转到配置项里了,我还要去扒文档多麻烦啊!这时候就要用到ts的类型继承和Partial关键字了。

上面就说过了饿了么ui给每个组件都提供了一份类型声明文件 在这里插入图片描述

我们完全可以自定义一个类型,去继承饿了么提供的类型文件,从而依靠ts提供代码提示, 但当你兴致冲冲的去用extends 继承了 ElTable,用的时候会发现报错,因为在饿了么提供的类型声明里,所有属性都是必填的,而我们在提供配置信息的时候,显然不可能把每个属性都配置出来,一般只会配置我们需要的。

此时Partial就闪亮登场了,Partial的意思就是部分的,使用方式可以去ts官方文档去看,当你使用 Partial后,就产生了一个新的类型,这个新的类型包含了所有ElTable的属性,但所有属性就变成了可选的,所以上述的问题就完美解决了。我们直接去继承 Partial就好了。还有一些骚操作可以去看下面代码中类型定义部分。

支持的功能

目前支持的功能有:

支持原生el-table的所有功能(本文中展示的代码,或许会有漏的属性或事件,因为我只加了本项目用到的那些)高度可扩展(可在el-table的基础上扩展更多自定义功能)根据传入的配置文件自动生成table并渲染数据所有的配置项支持代码和类型提示支持传入实际的业务数据类型,在配置列字段时会根据传入的ts类型做校验,校验列标识配置是否错误(基于泛型实现)会根据配置的列标识为每个单元格和表头生成插槽支持传入配置在行尾生成操作按钮支持高度自适应 、 行单选支持按shift实现任意行多选等等一些其他的小功能

本文中的代码使用了vue-class-component + ts的写法,没用过的同学看起来可能不太习惯,但主要是模板那块儿和类型定义那块的实现思路。

简单用法

看个效果就好了 在这里插入图片描述

import { Component, Vue } from 'vue-property-decorator'; import { OsTableColumn, OsTableOption, RowOperation } from 'osui/types/os-table'; import { OperationOption } from 'osui/types/os-table-operation'; import { OsQueryItemOption } from 'osui/types/os-table-query'; @Component({}) export default class Table extends Vue { // 仅为实例,所以没有为{ name: string; age: number; gender: 1 | 0; height: number }专门创建一个类型 public tableOption: OsTableOption loading: false, data: [ { name: '张三', age: 18, gender: 1, height: 175, }, { name: '李四', age: 28, gender: 1, height: 175, }, ], fit: true, }; /** * 默认的表格列配置 * label传入的是国际化的key,没用过i18n的就把tableExample.age理解成实际传入的是字符串 “年龄” 就好了 */ public columnOptions: Array prop: 'name', label: 'tableExample.name', minWidth: '120px', showOverflowTooltip: true, }, { prop: 'age', label: 'tableExample.age', minWidth: '120px', }, { prop: 'gender', label: 'tableExample.gender', minWidth: '120px', }, { prop: 'height', label: 'tableExample.height', minWidth: '120px', }, ]; /** * table行的操作配置 */ public rowOperationOptions: RowOperation fixed: 'right', width: '280px', operations: [ { operationType: 'delete', type: 'text', label: 'tableExample.delete', icon: 'el-icon-delete', permissionCode: 'delete', handleClick: (item): void => { console.log(item); }, }, ], }; }

下面这个例子包含了单元格和表头自定义,行多选、列随意拖拉、合计列等等功能 在这里插入图片描述

模板文件 {{ emptyFormatter(scope.row[item.prop]) }} {{ t(item.label) }} {{ item.label ? t(item.label) : '' }} 类型文件 import { ElButton } from 'element-ui/types/button'; import { LoadingServiceOptions } from 'element-ui/types/loading'; import { ElTableColumn, TableColumnFixedType } from 'element-ui/types/table-column'; import { ElTable, SortOrder } from 'element-ui/types/table'; export interface SummaryPropsOptions { /** * 要进行合计的属性 */ prop: keyof T; /** * 结果保留几位小数 */ fixPlace?: number; /** * 单位 */ unit?: string; } export type OsTableColumn = Partial & { /** * 要绑定的data内字段名 */ prop: keyof T; label: string; /** * 数据为空时,默认展示的内容 */ emptyValue?: string; /** * 该列是否隐藏 */ hide?: boolean; }; /** * 操作配置 */ export interface Operation extends Partial { /** * 操作类型,用于标识按钮的唯一性 */ operationType: 'add' | 'edit' | 'delete' | 'detail' | string; /** * 按钮显示的文本 * 请保证按钮文本和图标至少有一项 */ label?: string; /** * 按钮的图标 */ icon?: string; /** * 是否动态隐藏该操作按钮,不传则默认显示按钮 * 返回true则隐藏 */ dynamicHidden?: (rowData: T) => boolean; handleClick: (roleData: T, operation: Operation) => void; /** * 权限标识 */ permissionCode?: string; } /** * 行操作 */ export interface RowOperation extends Partial { /** * 同column的fixed,不传默认为 'right' */ fixed?: boolean | TableColumnFixedType; /** * 同column的label,不传默认为 '操作' */ label?: string; /** * 操作配置 */ operations: Array; } /** * 表格配置 */ export interface OsTableOption extends Partial { data: Array; /** * 加载数据时滚动 */ loading?: boolean; loadingOptions?: LoadingServiceOptions; /** * 开启表格单选 */ radioSelection?: boolean; /** * 表格距底部距离,为其他元素预留位置 */ bottomOffset?: number; size?: 'medium' | 'small' | 'mini'; className?: string; /** * 是否关闭自适应高度 */ closeAdaptiveHeight?: boolean; sumPropsOptions?: Array; /** * 默认排序 * 覆盖el-table默认的defaultSort类型定义 */ defaultSort?: { prop: keyof T; order: SortOrder }; } ts文件 import { Component, Prop, Watch } from 'vue-property-decorator'; import { ElTableColumn, TableColumn } from 'element-ui/types/table-column'; import { DefaultSortOptions, ElTable } from 'element-ui/types/table'; import { debounce } from 'lodash-es'; import { mixins } from 'vue-class-component'; import { I18nMixin } from 'src/mixins/local'; import { OsTableColumn, OsTableOption, RowOperation } from 'osui/types/os-table'; import { permissionOperation } from 'src/directives/permission-operation'; @Component({ name: 'os-table', directives: { permissionOperation }, }) export default class OsTable extends mixins(I18nMixin) { @Prop({ required: true, type: Object, }) public tableOption!: OsTableOption; @Prop({ required: true, type: Array, }) public tableColumnOption!: Array; @Prop({ type: Object, required: false, }) public rowOperationOption!: RowOperation; public radioSelected: object | null = null; private tableKey = true; private maxHeight = 200; private clientHeight = 0; private selectedRows: Array = []; /** * 实现shift多选相关的参数 */ private shiftMultipleOpts = { /** * 当前shift按键是否被按下 */ keyDown: false, /** * 记录选中的起始行索引 */ startIndex: -1, allowEmit: true, }; public get border(): boolean { return this.tableOption.border ?? true; } public get emptyText(): string | undefined { return this.tableOption.emptyText ? this.t(this.tableOption.emptyText as string).toString() : undefined; } public emptyFormatter(value: any): any { if (typeof value === 'number') { return value; } if (!value || value.length === 0) { return '--'; } return value; } public mounted(): void { this.initKeyDownListener(); if (this.tableOption.closeAdaptiveHeight) { return; } this.getAutoMaxHeight(); window.onresize = debounce((): void => { this.clientHeight = document.documentElement.clientHeight; }, 100); } public destroyed(): void { window.onresize = null; } public handleSelectionChange(values: Array): void { if (this.shiftMultipleOpts.allowEmit) { this.$emit('selection-change', values); } } public handleSortChange(sortOptions: DefaultSortOptions): void { this.$emit('sort-change', sortOptions); } public clearSelection(): void { (this.$refs.osTable as ElTable).clearSelection(); } public doLayout(): void { (this.$refs.osTable as ElTable).doLayout(); } public reload(): void { this.tableKey = !this.tableKey; } public get tableRef(): ElTable { return this.$refs.osTable as ElTable; } /** * 设置单选情况下的的行选中状态 */ public toggleRadioSelection(rowData: object): void { this.radioSelected = rowData; } public clearRadioSelection(): void { this.radioSelected = null; } public openLoading(): void { this.setLoading(true); } public closeLoading(): void { this.setLoading(false); } public handleCellClick(row: Object, column: ElTableColumn, cell: Object): void { this.$emit('cell-click', row, column, cell); } /** * 设置loading属性 * @param value 状态 */ private setLoading(value: boolean): void { this.tableOption.loading = value; } @Watch('radioSelected') private handleRadioSelection(value: object): void { this.$emit('radio-selection-change', value); } private get getLoadingText(): string { return this.tableOption.loadingOptions?.text ?? 'OSUI.table.loadingText'; } private get getLoadingSpinner(): string { return this.tableOption.loadingOptions?.spinner ?? 'el-icon-loading'; } private get getLoadingBackground(): string { return this.tableOption.loadingOptions?.background ?? ''; } private renderSlot(item: OsTableColumn): boolean { return item.type !== 'selection' && item.type !== 'index' && !item.formatter; } private getAutoMaxHeight(): void { const el = (this.$refs.osTable as ElTable).$el; let bottomOffset = 20; if (this.tableOption.bottomOffset !== 0) { bottomOffset = this.tableOption.bottomOffset || 78; } this.$nextTick(() => { // 实测,第一次渲染页面时,获取到的视口位置的高度会有31px的误差(大了31px),不知道为什么,下一次执行周期,会恢复正常 const topHeight = el.getBoundingClientRect().top; this.maxHeight = window.innerHeight - topHeight - bottomOffset; }); } @Watch('clientHeight') private computedMaxHeight(): void { this.getAutoMaxHeight(); } /** * 计算指定列的合计 * @param param * @returns */ private getSummaries(param: any): Array { const { columns, data }: { columns: Array; data: Array } = param; if (data.length === 0) { return []; } const sums: Array = []; const needSumProps: Array = this.tableOption.sumPropsOptions?.map((x) => x.prop) || []; columns.forEach((column, index) => { if (index === 0) { sums[index] = this.t('OSUI.table.summaryText') as string; return; } if (!needSumProps.includes(column.property as any)) { sums[index] = ''; return; } const values = data.map((item) => Number(item[column.property])); if (!values.every((value: any) => isNaN(value))) { const sumValue = values.reduce((prev, curr) => { const value = Number(curr); if (!isNaN(value)) { return prev + curr; } else { return prev; } }, 0); let sumRes = ''; const { fixPlace, unit } = this.tableOption.sumPropsOptions!.find((x) => x.prop === column.property)!; sumRes = fixPlace ? sumValue?.toFixed(fixPlace) : sumValue.toString(); sumRes = unit ? `${sumRes} ${unit}` : sumRes; sums[index] = sumRes; } else { sums[index] = 'N/A'; } }); return sums; } /** * 监听键盘按下、弹起事件 */ private initKeyDownListener(): void { window.addEventListener('keydown', (e) => { if (e.shiftKey) { this.shiftMultipleOpts.keyDown = true; } }); window.addEventListener('keyup', () => { this.shiftMultipleOpts.keyDown = false; }); } private shiftMultiple(selection: Array, row: object): void { // table当前数据 const data = (this.$refs.osTable as ElTable).data; // 最后选中行的索引 const endIdx = (this.$refs.osTable as ElTable).data.findIndex((x) => x === row); if (this.shiftMultipleOpts.keyDown && selection.includes(data[this.shiftMultipleOpts.startIndex])) { this.shiftMultipleOpts.allowEmit = false; // 用户可能反向选取,所以要取绝对值 const sum = Math.abs(this.shiftMultipleOpts.startIndex - endIdx) + 1; // 获取起点和终点较小的值 const min = Math.min(this.shiftMultipleOpts.startIndex, endIdx); let i = 0; this.$nextTick(() => { while (i this.shiftMultipleOpts.allowEmit = true; } if (!this.selectedRows.includes(data[index])) { (this.$refs.osTable as ElTable).toggleRowSelection(data[index], true); } i++; } }); } else { // 记录最后一次仅点击的行索引 this.shiftMultipleOpts.startIndex = endIdx; } } } scss文件 ::v-deep .default-header-style { background-color: #fafafa; color: rgba($color: #000000, $alpha: 0.85); } // 解决表格出现横向滚动条时,暂无数据不再居中的问题 ::v-deep .el-table__empty-block { width: 100% !important; }


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


    图片新闻

    实验室药品柜的特性有哪些
    实验室药品柜是实验室家具的重要组成部分之一,主要
    小学科学实验中有哪些教学
    计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
    实验室各种仪器原理动图讲
    1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
    高中化学常见仪器及实验装
    1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
    微生物操作主要设备和器具
    今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
    浅谈通风柜使用基本常识
     众所周知,通风柜功能中最主要的就是排气功能。在

    专题文章

      CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭