基于ElementPlus的table组件封装

embedded/2024/10/22 11:28:06/
前言

我们在使用UI库编写页面的时候,特别是账务系统,需要用到表格的情况会比较多,如果我们每次都是复制一遍UI库中的demo然后进行调整,这样造成的结果是多次引入 Table 组件,而且从前端开发规范来讲,不符合组件化的初衷。因此我们将 Table 组件进行二次封装,无疑是最好的选择。二次封装 Table 组件就是为了增强组件的可复用性、可维护性和功能性。

二次封装的优势
  1. 统一风格和功能
    • 样式一致性:项目中可能有多个地方使用表格,二次封装可以确保所有表格在样式和功能上保持一致。
  2. 简化使用
    • 简化 API:通过封装,可以提供更简单、更直观的 API,减少开发者在使用时需要考虑的细节。例如,可以通过 props 传递配置选项,而不必每次都重复编写配置。
    • 封装复杂逻辑:将复杂的逻辑(例如数据处理、列渲染)封装到一个组件中,调用方只需使用这个封装的组件即可,降低了使用难度。
  3. 增强功能
    • 插槽:通过插槽,可以灵活地扩展表格的功能,例如自定义列内容、操作按钮等,提升组件的灵活性。
    • 事件处理:可以统一处理表格中的各种事件,如行点击、行选中等,提供更一致的事件响应方式。
  4. 提高可维护性
    • 集中管理:将表格的所有相关逻辑集中到一个地方,使得后续的维护和修改更加简单。如果需要更改某个功能,只需在封装的组件中进行修改,而不必在每个使用表格的地方重复修改。
    • 易于调试:封装后的组件可以更容易进行单元测试和调试,确保表格的各项功能在不同场景下都能正常工作。
  5. 提高开发效率
    • 复用性:封装后的组件可以在不同的项目中复用,节省开发时间。通过封装常用的表格功能,可以减少重复劳动。
    • 快速迭代:通过提供易于使用的组件,团队成员可以快速上手并使用,而不必深入了解底层实现。

我使用的技术栈是Vue3+Ts+Element Plus,所以此次table组件的二次封装也是基于Element Plus的,其中Element Plus版本选的是2.8.0

前提条件

默认大家已经能够正确地创建Vue3+Ts前端工程,Element Plus的安装可以参考https://element-plus.org/zh-CN/guide/installation.html,还需要大家在提前注册Element Plus所有的图标。代码如下:

// main.tsimport * as ElementPlusIconsVue from '@element-plus/icons-vue';// 注册elementPlus所有的图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)
}
封装代码源码
// c-table/components/table-column-item/index.vue<template><el-table-columnv-if="!column.children && column.type !== 'selection' && column.type !== 'expand'":prop="column.prop":label="column.label":width="column.width":show-overflow-tooltip="column.showOverflowTooltip":fixed="column.fixed":min-width="column.minWidth":align="column.align":type="column.type":resizable="column.resizable":sortable="column.sortable":selectable="column.selectable":column-key="column.columnKey":filters="column.filters":filter-method="column.filterMethod":sort-method="column.sortMethod":sort-by="column.sortBy":sort-orders="column.sortOrders":header-align="column.headerAlign":class-name="column.className":label-class-name="column.labelClassName":reserve-selection="column.reserveSelection":filter-placement="column.filterPlacement":filter-class-name="column.filterClassName":filter-multiple="column.filterMultiple":filter-value="column.filterValue"><template #default="{ row, $index }"><template v-if="column.formatter"><template v-if="isStringFormatter(row, $index)">{{column.formatter(row, column, row[column.prop], $index)}}</template><template v-else><component:is="column.formatter(row,column,row[column.prop],$index)"/></template></template><template v-if="column.index && !column.formatter">{{ column.index($index) }}</template><component:is="column.slots.default":row="row":index="$index"v-if="column.slots && column.slots.default"/></template><template #header="scope" v-if="column.slots && column.slots.header"><component:is="column.slots.header":row="scope.row":index="scope.$index"/></template></el-table-column><!-- 多级表头 --><el-table-columnv-if="column.children && column.type !== 'expand'":prop="column.prop":label="column.label":align="column.align":type="column.type":resizable="column.resizable"><template v-for="child in column.children" :key="child.prop"><table-column-item :column="child" /></template></el-table-column><!-- 多选 --><el-table-columnv-if="column.type == 'selection'"type="selection":width="column.width":selectable="column.selectable"/><!-- 展开行 --><el-table-column v-if="column.type == 'expand'" type="expand"><template #default="{ row, $index }"><component:is="column.slots.default":row="row":index="$index"/></template></el-table-column>
</template><script lang="ts">
import { PropType } from "vue";
import { ColumnItem } from "../../../../type";
import { ElTable, ElTableColumn } from "element-plus";export default {name: "TableColumnItem",props: {column: {type: Object as PropType<ColumnItem>,default: {},},},components: {ElTableColumn,},setup(props, context) {const isStringFormatter = (row: any, index: number): boolean => {const formattedValue = props.column.formatter(row,props.column,row[props.column.prop],index);return typeof formattedValue === "string";};return {isStringFormatter,};},
};
</script>
// c-table/index.vue<template><el-table v-bind="$attrs" :data="tableData" ref="elTableRef"><TableColumnItemv-for="column in columns":key="column.prop":column="column"></TableColumnItem><!-- 自定义空数据时的内容 --><template v-slot:empty><slot name="empty"></slot></template></el-table>
</template><script lang="ts">
import {computed,ref,reactive,onMounted,onBeforeUnmount,watch,toRefs,PropType,
} from "vue";
import { TableData, ColumnItem } from "../../type";
import TableColumnItem from "./components/table-column-item/index.vue";
import { ElButton, ElIcon, ElTable } from "element-plus";export default {name: "CTable",props: {tableData: {type: Array as PropType<TableData[]>,default: [],},columns: {type: Array as PropType<ColumnItem[]>,default: [],},},components: {TableColumnItem,},setup(props, ctx) {const elTableRef = ref<InstanceType<typeof ElTable>>();return {elTableRef,};},
};
</script>
// type.tsimport { VNode } from 'vue';
import type { TableColumnCtx } from 'element-plus'export interface TableData {name?: string,date?: string,address?: string,result?: number,amount?: number,state?: string,city?: string,zip?: string,hasChildren?: boolean,propsData?: TableData[]
}export interface ColumnItem {prop?: string,label: string,width?: number | string,minWidth?: number,align?: string,type?: string,sortable?: boolean | string,resizable?: boolean,columnKey?: string,selectable?: () => boolean,index?: (index: number) => number,filterMethod?: (value: any, row: any, column: any) => void,formatter?: (row: any, column: any, cellValue: any, index: number) => VNode | string,sortMethod?: (a: TableData, b: TableData) => number,slots?: {default?: (a: SlotsItem) => VNode;header?: (a: SlotsItem) => VNode;};showOverflowTooltip?: boolean,fixed?: boolean | 'left' | 'right',children?: ColumnItem[],filters?: FilterItem[],sortBy?: (row: any, index: number) => string | string | string[],sortOrders?: ('ascending' | 'descending' | null)[],headerAlign?: 'left' | 'center' | 'right',filterPlacement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end',className?: string,labelClassName?: string,filterClassName?: string,reserveSelection?: boolean,filterMultiple?: boolean,filterValue?: string[],
}export interface SlotsItem {index: number,row: TableData,
}export interface FilterItem {text: string,value: string,
}export interface SummaryMethodProps<T = TableData> {columns: TableColumnCtx<T>[]data: T[]
}export interface SpanMethodProps {row: TableDatacolumn: TableColumnCtx<TableData>rowIndex: numbercolumnIndex: number
}
使用
<template><div class="table-list"><!-- lazy:load="load":tree-props="{ children: 'children', hasChildren: 'hasChildren' }" --><c-Table:span-method="arraySpanMethod"show-summary:summary-method="getSummaries":default-expand-all="false":row-class-name="tableRowClassName"ref="tableRef":data="tableData":columns="columns"borderstripestyle="width: 100%"height="500"row-key="name"highlight-current-row@current-change="handleCurrentChange"table-layout="auto"><template v-slot:empty><div style="text-align: center; padding: 20px; color: #999"><p>没有数据可显示</p><el-button type="primary">添加数据</el-button></div></template></c-Table><div style="margin-top: 20px"><el-button @click="setCurrent(tableData[1])">选择第二行</el-button><el-button @click="setCurrent()">清除选中行</el-button><el-button@click="toggleSelection([tableData[1], tableData[2]], false)">勾选目标行</el-button><el-button @click="toggleSelection()">清除勾选数据</el-button></div></div>
</template><script lang="ts">
import { ref, defineComponent, reactive, toRefs, h, VNode } from "vue";
import CTable from "./components/c-table/index.vue";
import {TableData,SlotsItem,ColumnItem,SummaryMethodProps,SpanMethodProps,
} from "./type";
import { ElButton, ElIcon, ElInput } from "element-plus";
import type { TableColumnCtx } from "element-plus";
import { Check, Close } from "@element-plus/icons-vue";export default defineComponent({name: "tableList",props: {},components: {CTable,},setup() {const search = ref("");const searchValueChange = (val: any) => {console.log("searchValueChange", val);};const handleInputChange = (val: any) => {console.log("handleInputChange", val);};const filterHandler = (value: string,row: TableData,column: TableColumnCtx<TableData>) => {const property = column.property as keyof TableData;return row[property] === value;};const subTableRef = ref();const tableInfo = reactive({tableData: [{date: "2016-05-03",name: "Alice",address: "No. 189, Grove St, Los Angeles1",state: "California",city: "Los Angeles",zip: "CA 90036",result: 0,amount: 12,// hasChildren: true,// propsData: [// 	{// 		name: "Jerry",// 		state: "California",// 		city: "San Francisco",// 		address: "3650 21st St, San Francisco",// 		zip: "CA 94114",// 	},// ],},{date: "2016-05-02",name: "Tom",address: "No. 189, Grove St, Los Angeles2",state: "California",city: "Los Angeles",zip: "CA 90036",result: 1,amount: 19,},{date: "2016-05-01",name: "Bob",address: "No. 189, Grove St, Los Angeles12",state: "California",city: "Los Angeles",zip: "CA 90038",result: 0,amount: 120,},] as TableData[],subColumn: [{prop: "name",label: "name",width: 120,},{prop: "state",label: "state",width: 120,},{prop: "city",label: "city",width: 120,},{prop: "address",label: "address",width: 120,},{prop: "zip",label: "zip",},] as ColumnItem[],columns: [{label: "索引",width: 60,fixed: true,type: "index",formatter: (row: any,column: any,cellValue: any,index: number): string => {return String(index * 2);},},{type: "selection",width: 100,selectable: (val: TableData) => {return true;},reserveSelection: true,},{type: "expand",slots: {default: ({ index, row }: SlotsItem): VNode => {return h(CTable, {ref: subTableRef,data: row.propsData,columns: tableInfo.subColumn,border: true,stripe: true,style: { width: "100%" },height: "auto","row-key": "name","highlight-current-row": true,"table-layout": "auto",});},},},{prop: "date",label: "时间",width: 140,sortable: true,columnKey: "date",filterMethod: filterHandler,filterValue: ["2016-05-01", "2016-05-02"],filters: [{ text: "2016-05-01", value: "2016-05-01" },{ text: "2016-05-02", value: "2016-05-02" },{ text: "2016-05-03", value: "2016-05-03" },{ text: "2016-05-04", value: "2016-05-04" },],},{prop: "name",label: "姓名",width: 240,formatter: (row: any,column: any,cellValue: any,index: number): VNode | string => {if (!cellValue) return h("span", "");return h("span",{ style: { color: "red" } },cellValue);},sortable: true,sortMethod: (a, b) => {return a.name.localeCompare(b.name);},sortOrders: ["ascending", "descending"],align: "center",className: "custom-column",labelClassName: "custom-labelClassName",},{prop: "address",label: "地址",width: 120,// renderHeader: (data) => {//     console.log('-----', data);//     return '1'// } -> 建议通过插槽的方式实现},{label: "第一级表头",align: "center",resizable: true,children: [{label: "第二级表头",align: "center",resizable: true,children: [{prop: "state",label: "州",width: 320,resizable: true,},{prop: "city",label: "城市",width: 400,resizable: true,},],},{prop: "zip",label: "Zip",resizable: true,width: 280,},],},{prop: "amount",label: "数量",width: 120,},{prop: "result",label: "校验结果",align: "center",width: 120,slots: {default: ({ row }: SlotsItem): VNode => {return h(ElIcon,{id: "result-i",size: 14,color: row.result == 1 ? "red" : "green",},{default: () =>row.result == 0 ? h(Check) : h(Close),});},},},{label: "操作",fixed: "right",minWidth: 180,slots: {default: (val: SlotsItem): VNode => {return h(ElButton,{type: "primary",onClick: (event) => {event.stopPropagation(); // 阻止事件冒泡checkF(val.row);},},() => "查看");},header: (val: SlotsItem): VNode => {return h(ElInput, {size: "small",placeholder: "请输入",modelValue: search.value,"onUpdate:modelValue": (value) => {search.value = value; // 更新 search},onChange: (value) => {searchValueChange(value); // 处理 change 事件,失去焦点才会触发},onInput: (value) => {handleInputChange(value); // 处理 input 事件},disabled: false,});},},},] as ColumnItem[],});const tableRef = ref();const currentRow = ref();const load = (row: TableData,treeNode: unknown,resolve: (data: TableData[]) => void) => {setTimeout(() => {resolve([{date: "2016-05-01",name: "wangxiaohu",address: "No. 189, Grove St, Los Angeles",},{date: "2016-05-01",name: "wangxiaohu",address: "No. 189, Grove St, Los Angeles",},]);}, 1000);};const getSummaries = (param: SummaryMethodProps) => {const { columns, data } = param;const sums: (string | VNode)[] = [];columns.forEach((column, index) => {if (index === 0) {sums[index] = h("div",{ style: { textDecoration: "underline" } },["Total Cost"]);return;}const values = data.map((item: any) =>Number(item[column.property]));if (!values.every((value) => Number.isNaN(value))) {sums[index] = `$ ${values.reduce((prev, curr) => {const value = Number(curr);if (!Number.isNaN(value)) {return prev + curr;} else {return prev;}}, 0)}`;} else {sums[index] = "N/A";}});return sums;};const tableRowClassName = ({rowIndex,}: {rowIndex: number;}): string => {if (rowIndex === 1) {return "warning-row";} else if (rowIndex === 3) {return "success-row";}return "";};const checkF = (val: TableData) => {console.log("checkF", val, tableRef.value);};const handleCurrentChange = (val: TableData | undefined) => {currentRow.value = val;};const setCurrent = (row?: TableData) => {tableRef.value.elTableRef!.setCurrentRow(row);};const toggleSelection = (rows?: TableData[],ignoreSelectable?: boolean) => {if (rows) {rows.forEach((row) => {tableRef.value.elTableRef!.toggleRowSelection(row,undefined,ignoreSelectable);});} else {tableRef.value.elTableRef!.clearSelection();}};const arraySpanMethod = ({row,column,rowIndex,columnIndex,}: SpanMethodProps) => {if (rowIndex % 2 === 0) {if (columnIndex === 0) {return [1, 2];} else if (columnIndex === 1) {return [0, 0];}}};return {...toRefs(tableInfo),tableRowClassName,checkF,tableRef,handleCurrentChange,currentRow,setCurrent,toggleSelection,getSummaries,arraySpanMethod,load,};},
});
</script><style lang="scss" scoped>
.table-list {height: 100%;width: 100%;padding: 12px;::v-deep .el-table {.warning-row .el-table__cell {background-color: #fdf6ec;}.success-row .el-table__cell {background-color: #f0f9eb;}.el-button:focus {outline: none;border: none;}}
}
</style>

[!CAUTION]

Table 暴露出来的函数,需要通过tableRef.value.elTableRef来访问

总结

此次封装是针对Table组件的二次封装,并没有将分页、筛选等功能放在一起,后续会继续更新,完善组件封装的完整性。


http://www.ppmy.cn/embedded/129546.html

相关文章

双线性变换法

给定模拟信号&#xff0c;要用数字滤波器对它进行滤波。按照双线性变换法&#xff0c;步骤如下&#xff08;假设为高通滤波器&#xff09;&#xff1a; &#xff08;1&#xff09;选择一个采样频率&#xff0c;对进行采样&#xff0c;得到时域离散信号。 &#xff08;2&#…

YOLOv11来了 | 自定义目标检测

概述 YOLO11 在 2024 年 9 月 27 日的 YOLO Vision 2024 活动中宣布&#xff1a;https://www.youtube.com/watch?vrfI5vOo3-_A。 YOLO11 是 Ultralytics YOLO 系列的最新版本&#xff0c;结合了尖端的准确性、速度和效率&#xff0c;用于目标检测、分割、分类、定向边界框和…

userspace 和 kernelspace

Kernel Space&#xff08;内核空间&#xff09; 定义与功能 内核空间是操作系统核心代码运行的地方&#xff0c;主要包括&#xff1a; 内存管理&#xff1a;管理和分配内存资源&#xff0c;包括物理内存和虚拟内存的映射。 进程管理&#xff1a;管理进程的创建、调度和终止&am…

SQL注入原理、类型、危害与防御

SQL注入的原理概念 SQL注入是一种常见的网络攻击技术&#xff0c;攻击者通过在Web应用程序的输入字段中注入恶意构造的SQL代码&#xff0c;以欺骗后端数据库执行非预期的SQL命令。这种攻击可以导致数据泄露、权限提升、数据篡改甚至系统瘫痪。SQL注入可以分为多种类型&#xf…

Arduino配置ESP32环境

Arduino配置ESP32环境 引言一、IDE下载教程操作取巧方法 二、社区安装包三、官方手动安装 引言 最近入手了一款ESP32-C3的开发板&#xff0c;想继续沿用现有Arduino IDE&#xff0c;网上看了很多方法&#xff0c;大致分了三类&#xff1a;IDE下载、社区安装包、github手动配置…

Task Registration Process

Task Registration Process 活动报名流程&#xff1a; [蓝色隐士] 工具-【集合石】 【创建】 填写活动 标题、类型、报名截止日期、报名截止时间、详情 【确定】 可【。。。】和【分享】微信

AnaTraf | 网络流量分析仪:网络故障排除的利器

http://www.anatraf.com 网络流量分析仪作为一种强有力的工具&#xff0c;能够帮助IT运维人员快速识别和解决网络故障&#xff0c;从而优化网络性能。 什么是网络流量分析仪&#xff1f; 网络流量分析仪是一种监测和分析网络流量的工具&#xff0c;能够实时捕捉数据包并提供…

django5入门【02】创建新的django程序

注意&#xff1a; ⭐前提&#xff1a;已经安装了python以及django所依赖的包1、通过django-admin管理工具在命令行创建Django应用程序&#xff0c;创建命令如下&#xff1a; django-admin startproject ProjectName❓ 疑问&#xff1a;除了使用命令行创建django程序外&#x…