Vue3.2 + Element-Plus 二次封装 el-table(Pro版)

2024-02-03 03:10

本文主要是介绍Vue3.2 + Element-Plus 二次封装 el-table(Pro版),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言 📖

ProTable 组件目前已是 2.0版本🌈,在 1.0版本[1] 中大家提出的问题与功能优化,目前已经得到优化和解决。

😀 欢迎大家在使用过程中发现任何问题或更好的想法,都可以在下方评论区留言,或者我的开源项目 issues 中提出。如果你觉得还不错,请帮我点个小小的 Star 🧡

一、在线预览 👀

Link:admin.spicyboy.cn[2]

二、Git 仓库地址 (欢迎 Star⭐⭐⭐)

Gitee:gitee.com/laramie/Gee…[3]

GitHub:github.com/HalseySpicy…[4]

三、ProTable 功能 🚀🚀🚀

ProTable 组件目前使用属性透传进行重构,支持 el-table && el-table-column 所有属性、事件、方法的调用,不会有任何心智负担。

  • 表格内容自适应屏幕宽高,溢出内容表格内部滚动(flex 布局)

  • 表格搜索、重置、分页查询 Hooks 封装 (页面使用不会存在任何搜索、重置、分页查询逻辑)

  • 表格数据操作 Hooks 封装 (单条数据删除、批量删除、重置密码、状态切换等操作)

  • 表格数据多选 Hooks 封装 (支持现跨页勾选数据)

  • 表格数据导入组件、导出 Hooks 封装

  • 表格搜索区域使用 Grid 布局重构,支持自定义响应式配置

  • 表格分页组件封装(Pagination)

  • 表格数据刷新、列显隐、列排序、搜索区域显隐设置

  • 表格数据打印功能(可勾选行数据、隐藏列打印)

  • 表格配置支持多级 prop(示例 ==> prop: user.detail.name)

  • 单元格内容格式化、tag 标签显示(有字典 enum 会根据字典 enum 自动格式化)

  • 支持多级表头、表头内容自定义渲染(支持作用域插槽、tsx 语法、h 函数)

  • 支持单元格内容自定义渲染(支持作用域插槽、tsx 语法、h 函数)

  • 配合 TreeFilter、SelectFilter 组件使用更佳(项目中有使用示例)

四、ProTable 功能需求分析 📑

首先我们来看效果图(总共可以分为五个模块):

b86b1dd225264064f1b2b915d6db6a20.jpeg
image.png
  • 1、表格搜索区域

  • 2、表格数据操作按钮区域

  • 3、表格功能按钮区域

  • 4、表格主体内容展示区域

  • 5、表格分页区域

1、表格搜索区域需求分析:

可以看到搜索区域的字段都是存在于表格当中的,并且每个页面的搜索、重置方法都是一样的逻辑,只是不同的查询参数而已。我们完全可以在传表格配置项 columns 时,直接指定某个 columnsearch 配置,就能把该项变为搜索项,然后使用 el 字段可以指定搜索框的类型,最后把表格的搜索方法都封装成 Hooks 钩子函数。页面上完全就不会存在任何搜索、重置逻辑了。

1.0 版本中使用 v-if 判断太麻烦,为了更方便用户传递参数,搜索组件在 2.0 版本中通过 component :is 动态组件 && v-bind 属性透传实现,将用户传递的参数全部透传到组件上,所以大家可以直接根据 element 官方文档在 props 中传递参数了。以下代码还结合了自己逻辑上的一些处理:

<template><componentv-if="column.search?.el":is="`el-${column.search.el}`"v-bind="column.search.props"v-model="searchParam[column.search.key ?? handleProp(column.prop!)]":data="column.search?.el === 'tree-select' ? columnEnum : []":placeholder="placeholder(column)":clearable="clearable(column)"range-separator="至"start-placeholder="开始时间"end-placeholder="结束时间"><template v-if="column.search.el === 'select'"><component:is="`el-option`"v-for="(col, index) in columnEnum":key="index":label="col[fieldNames().label]":value="col[fieldNames().value]"></component></template><slot v-else></slot></component>
</template><script setup lang="ts" name="searchFormItem">
import { computed, inject, ref } from "vue";
import { handleProp } from "@/utils/util";
import { ColumnProps } from "@/components/ProTable/interface";interface SearchFormItem {column: ColumnProps; // 具体每一个搜索项的配置searchParam: { [key: string]: any }; // 搜索参数
}
const props = defineProps<SearchFormItem>();// 接受 enumMap
const enumMap = inject("enumMap", ref(new Map()));const columnEnum = computed(() => {if (!enumMap.value.get(props.column.prop)) return [];return enumMap.value.get(props.column.prop);
});// 判断 fieldNames 设置 label && value 的 key 值
const fieldNames = () => {return {label: props.column.fieldNames?.label ?? "label",value: props.column.fieldNames?.value ?? "value"};
};// 判断 placeholder
const placeholder = (column: ColumnProps) => {return column.search?.props?.placeholder ?? (column.search?.el === "input" ? "请输入" : "请选择");
};// 是否有清除按钮 (当搜索项有默认值时,清除按钮不显示)
const clearable = (column: ColumnProps) => {return column.search?.props?.clearable ?? (column.search?.defaultValue == null || column.search?.defaultValue == undefined);
};
</script>
复制代码

表格搜索组件在 2.0 版本中还支持了响应式配置,使用 Grid 方法进行整体重构 😋。

动画.gif

2、表格数据操作按钮区域需求分析:

表格数据操作按钮基本上每个页面都会不一样,所以我们直接使用 作用域插槽 来完成每个页面的数据操作按钮区域,作用域插槽 可以将表格多选数据信息从 ProTableHooks 多选钩子函数中传到页面上使用。

scope 数据中包含:selectedList(当前选择的数据)、selectedListIds(当前选择的数据id)、isSelected(当前是否选中的数据)

<!-- ProTable 中 tableHeader 插槽 -->
<slot name="tableHeader" :selectList="selectedList" :selectedListIds="selectedListIds" :isSelected="isSelected"></slot><!-- 页面使用 -->
<template #tableHeader="scope"><el-button type="primary" :icon="CirclePlus" @click="openDrawer('新增')">新增用户</el-button><el-button type="primary" :icon="Upload" plain @click="batchAdd">批量添加用户</el-button><el-button type="primary" :icon="Download" plain @click="downloadFile">导出用户数据</el-button><el-button type="danger" :icon="Delete" plain @click="batchDelete(scope.selectedListIds)" :disabled="!scope.isSelected">批量删除用户</el-button>
</template>
复制代码

3、表格功能按钮区域分析:

这块区域没什么特殊功能,只有四个按钮,其功能分别为:表格数据刷新(一直会携带当前查询和分页条件)、表格数据打印、表格列设置(列显隐、列排序)、表格搜索区域显隐(方便展示更多的数据信息)。可通过 toolButton 属性控制这块区域的显隐。

表格打印功能基于 PrintJs 实现,因 PrintJs 不支持多级表头打印,所以当页面存在多级表头时,只会打印最后一级表头。表格打印功能可根据显示的列和勾选的数据动态打印,默认打印当前显示的所有数据。

693f0d0ef415c117a0039e48c40f2d65.jpeg
image.png

4、表格主体内容展示区域分析:

🍉 该区域是最重要的数据展示区域,对于使用最多的功能就是表头和单元格内容可以自定义渲染,在第 1.0 版本中,自定义表头只支持传入renderHeader方法,自定义单元格内容只支持slot插槽。

💥 目前 2.0 版本中,表头支持headerRender方法(避免与 el-table-column 上的属性重名导致报错)、作用域插槽(column.prop + 'Header')两种方式自定义,单元格内容支持render方法和作用域插槽(column 上的 prop 属性)两种方式自定义。

  • 使用作用域插槽:

<!-- 使用作用域插槽自定义单元格内容 username -->
<template #username="scope">{{ scope.row.username }}
</template><!-- 使用作用域插槽自定义表头内容 username -->
<template #usernameHeader="scope"><el-button type="primary" @click="ElMessage.success('我是通过作用域插槽渲染的表头')">{{ scope.row.label }}</el-button>
</template>
复制代码
  • 使用 tsx 语法:

<script setup lang="tsx">
const columns: ColumnProps[] = [{prop: "username",label: "用户姓名",// 使用 headerRender 自定义表头headerRender: (row) => {return (<el-buttontype="primary"onClick={() => {ElMessage.success("我是通过 tsx 语法渲染的表头");}}>{row.label}</el-button>);}},{prop: "status",label: "用户状态",// 使用 render 自定义表格内容render: (scope: { row }) => {return (<el-switchmodel-value={scope.row.status}active-text={scope.row.status ? "启用" : "禁用"}active-value={1}inactive-value={0}onClick={() => changeStatus(scope.row)}/>) );}},
];
</script>
复制代码

💢💢💢 最强大的功能:如果你想使用 el-table 的任何属性、事件,目前通过属性透传都能支持。

如果你还不了解属性透传,请阅读 vue 官方文档:cn.vuejs.org/guide/compo…[5]

  • ProTable 组件上的绑定的所有属性和事件都会通过 v-bind="$attrs" 透传到 el-table 上。

  • ProTable 组件内部暴露了 el-table DOM,可通过 proTable.value.element.方法名 调用其方法。

<template><el-tableref="tableRef"v-bind="$attrs" ></el-table>
</template><script setup lang="ts" name="ProTable">
import { ref } from "vue";
import { ElTable } from "element-plus";const tableRef = ref<InstanceType<typeof ElTable>>();defineExpose({ element: tableRef });
</script>
复制代码

5、表格分页区域分析:

分页区域也没有什么特殊的功能,该支持的都支持了🤣(页面上使用 ProTable 组件完全不存在分页逻辑)

<template><!-- 分页组件 --><el-pagination:current-page="pageable.pageNum":page-size="pageable.pageSize":page-sizes="[10, 25, 50, 100]":background="true"layout="total, sizes, prev, pager, next, jumper":total="pageable.total"@size-change="handleSizeChange"@current-change="handleCurrentChange"></el-pagination>
</template><script setup lang="ts" name="pagination">
interface Pageable {pageNum: number;pageSize: number;total: number;
}interface PaginationProps {pageable: Pageable;handleSizeChange: (size: number) => void;handleCurrentChange: (currentPage: number) => void;
}defineProps<PaginationProps>();
</script>
复制代码

五、ProTable 文档 📚

1、ProTable 属性(ProTableProps):

使用 v-bind="$atts" 通过属性透传将 ProTable 组件属性全部透传到 el-table 上,所以我们支持 el-table 的所有 Props 属性。在此基础上,还扩展了以下 Props:

属性名类型是否必传默认值属性描述
columnsColumnPropsProTable 组件会根据此字段渲染搜索表单与表格列,详情见 ColumnProps
requestApiFunction获取表格数据的请求 API
dataCallbackFunction后台返回数据的回调函数,可对后台返回数据进行处理
titleString表格标题,目前只在打印的时候用到
paginationBooleantrue是否显示分页组件
initParamObject{}表格请求的初始化参数
toolButtonBooleantrue是否显示表格功能按钮
selectIdString'id'当表格数据多选时,所指定的 id
searchColObject{ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }表格搜索项每列占比配置

2、Column 配置(ColumnProps):

使用 v-bind="column" 通过属性透传将每一项 column 属性全部透传到 el-table-column 上,所以我们支持 el-table-column 的所有 Props 属性。在此基础上,还扩展了以下 Props:

属性名类型是否必传默认值属性描述
tagBooleanfalse当前单元格值是否为标签展示
isShowBooleantrue当前列是否显示在表格内
searchSearchProps搜索项配置,详情见 SearchProps
enumObject | Function字典,可格式化单元格内容,还可以作为搜索框的下拉选项(字典可以为API请求函数,内部会自动执行)
isFilterEnumBooleantrue当前单元格值是否根据 enum 格式化(例如 enum 只作为搜索项数据,不参与内容格式化)
fieldNamesObject指定字典 label && value 的 key 值
headerRenderFunction自定义表头内容渲染(tsx 语法、h 语法)
renderFunction自定义单元格内容渲染(tsx 语法、h 语法)
_childrenColumnProps多级表头

3、搜索项 配置(SearchProps):

使用 v-bind="column.search.props“ 通过属性透传将 search.props 属性全部透传到每一项搜索组件上,所以我们支持 input、select、tree-select、date-packer、time-picker、time-select、swicth 大部分属性,并在其基础上还扩展了以下 Props:

属性名类型是否必传默认值属性描述
elString当前项搜索框的类型,支持:input、select、tree-select、cascader、date-packer、time-picker、time-select、swicth
propsObject根据 element plus 官方文档来传递,该属性所有值会透传到组件
defaultValueAny搜索项默认值
keyString当搜索项 key 不为 prop 属性时,可通过 key 指定
orderNumber搜索项排序(从大到小)
spanNumber1搜索项所占用的列数,默认为 1 列
offsetNumber搜索字段左侧偏移列数

4、ProTable 事件:

根据 ElementPlus Table 文档在 ProTable 组件上绑定事件即可,组件会通过 $attrs 透传给 el-table

el-table 事件文档链接[6]

5、ProTable 方法:

ProTable 组件暴露了 el-table 实例和一些组件内部的参数和方法:

el-table 方法文档链接[7]

方法名描述
elementel-table 实例,可以通过element.方法名来调用 el-table 的所有方法
tableData当前页面所展示的数据
searchParam所有的搜索参数,不包含分页
pageable当前表格的分页数据
getTableList获取、刷新表格数据的方法(携带所有参数)
clearSelection清空表格所选择的数据,除此方法之外还可使用 element.clearSelection()
enumMap当前表格使用的所有字典数据(Map 数据结构)

6、ProTable 插槽:

插槽名描述
默认插槽,支持直接写 el-table-column
tableHeader自定义表格头部左侧区域的插槽,一般情况该区域放操作按钮
append插入至表格最后一行之后的内容, 如果需要对表格的内容进行无限滚动操作,可能需要用到这个 slot。若表格有合计行,该 slot 会位于合计行之上。
empty当表格数据为空时自定义的内容
column.prop单元格的作用域插槽
column.prop + "Header"表头的作用域插槽

六、代码实现 & 基础使用 💪(代码较多,详情请去项目里查看)

使用一段话总结下我的想法:📚📚

🤔 前提:首先我们在封装 ProTable 组件的时候,在不影响 el-table 原有的属性、事件、方法的前提下,然后在其基础上做二次封装,否则做得再好,也不太完美。

🧐 思路:把一个表格页面所有重复的功能 (表格多选、查询、重置、刷新、分页、数据操作二次确认、文件下载、文件上传) 都封装成 Hooks 函数钩子或组件,然后在 ProTable 组件中使用这些函数钩子或组件。在页面中使用的时,只需传给 ProTable 当前表格数据的请求 API、表格配置项 columns 就行了,数据传输都使用 作用域插槽 或 tsx 语法从 ProTable 传递给父组件就能在页面上获取到了。

1、常用 Hooks 函数

  • useTable:

import { Table } from "./interface";
import { reactive, computed, onMounted, toRefs } from "vue";/*** @description table 页面操作方法封装* @param {Function} api 获取表格数据 api 方法(必传)* @param {Object} initParam 获取数据初始化参数(非必传,默认为{})* @param {Boolean} isPageable 是否有分页(非必传,默认为true)* @param {Function} dataCallBack 对后台返回的数据进行处理的方法(非必传)* */
export const useTable = (api: (params: any) => Promise<any>,initParam: object = {},isPageable: boolean = true,dataCallBack?: (data: any) => any
) => {const state = reactive<Table.TableStateProps>({// 表格数据tableData: [],// 分页数据pageable: {// 当前页数pageNum: 1,// 每页显示条数pageSize: 10,// 总条数total: 0,},// 查询参数(只包括查询)searchParam: {},// 初始化默认的查询参数searchInitParam: {},// 总参数(包含分页和查询参数)totalParam: {},});/*** @description 分页查询参数(只包括分页和表格字段排序,其他排序方式可自行配置)* */const pageParam = computed({get: () => {return {pageNum: state.pageable.pageNum,pageSize: state.pageable.pageSize,};},set: (newVal: any) => {console.log("我是分页更新之后的值", newVal);},});// 初始化的时候需要做的事情就是 设置表单查询默认值 && 获取表格数据(reset函数的作用刚好是这两个功能)onMounted(() => {reset();});/*** @description 获取表格数据* @return void* */const getTableList = async () => {try {// 先把初始化参数和分页参数放到总参数里面Object.assign(state.totalParam,initParam,isPageable ? pageParam.value : {});let { data } = await api(state.totalParam);dataCallBack && (data = dataCallBack(data));state.tableData = isPageable ? data.datalist : data;// 解构后台返回的分页数据 (如果有分页更新分页信息)const { pageNum, pageSize, total } = data;isPageable && updatePageable({ pageNum, pageSize, total });} catch (error) {console.log(error);}};/*** @description 更新查询参数* @return void* */const updatedTotalParam = () => {state.totalParam = {};// 处理查询参数,可以给查询参数加自定义前缀操作let nowSearchParam: { [key: string]: any } = {};// 防止手动清空输入框携带参数(这里可以自定义查询参数前缀)for (let key in state.searchParam) {// * 某些情况下参数为 false/0 也应该携带参数if (state.searchParam[key] ||state.searchParam[key] === false ||state.searchParam[key] === 0) {nowSearchParam[key] = state.searchParam[key];}}Object.assign(state.totalParam,nowSearchParam,isPageable ? pageParam.value : {});};/*** @description 更新分页信息* @param {Object} resPageable 后台返回的分页数据* @return void* */const updatePageable = (resPageable: Table.Pageable) => {Object.assign(state.pageable, resPageable);};/*** @description 表格数据查询* @return void* */const search = () => {state.pageable.pageNum = 1;updatedTotalParam();getTableList();};/*** @description 表格数据重置* @return void* */const reset = () => {state.pageable.pageNum = 1;state.searchParam = {};// 重置搜索表单的时,如果有默认搜索参数,则重置默认的搜索参数Object.keys(state.searchInitParam).forEach((key) => {state.searchParam[key] = state.searchInitParam[key];});updatedTotalParam();getTableList();};/*** @description 每页条数改变* @param {Number} val 当前条数* @return void* */const handleSizeChange = (val: number) => {state.pageable.pageNum = 1;state.pageable.pageSize = val;getTableList();};/*** @description 当前页改变* @param {Number} val 当前页* @return void* */const handleCurrentChange = (val: number) => {state.pageable.pageNum = val;getTableList();};return {...toRefs(state),getTableList,search,reset,handleSizeChange,handleCurrentChange,};
};
复制代码
  • useSelection:

import { ref, computed } from "vue";/*** @description 表格多选数据操作* @param {String} selectId 当表格可以多选时,所指定的 id* @param {Any} tableRef 当表格 ref* */
export const useSelection = (selectId: string = "id") => {// 是否选中数据const isSelected = ref<boolean>(false);// 选中的数据列表const selectedList = ref([]);// 当前选中的所有ids(数组),可根据项目自行配置id字段const selectedListIds = computed((): string[] => {let ids: string[] = [];selectedList.value.forEach(item => {ids.push(item[selectId]);});return ids;});// 获取行数据的 Key,用来优化 Table 的渲染;在使用跨页多选时,该属性是必填的const getRowKeys = (row: any) => {return row[selectId];};/*** @description 多选操作* @param {Array} rowArr 当前选择的所有数据* @return void*/const selectionChange = (rowArr: any) => {rowArr.length === 0 ? (isSelected.value = false) : (isSelected.value = true);selectedList.value = rowArr;};return {isSelected,selectedList,selectedListIds,selectionChange,getRowKeys};
};复制代码
  • useDownload:

import { ElNotification } from "element-plus";/*** @description 接收数据流生成blob,创建链接,下载文件* @param {Function} api 导出表格的api方法(必传)* @param {String} tempName 导出的文件名(必传)* @param {Object} params 导出的参数(默认为空对象)* @param {Boolean} isNotify 是否有导出消息提示(默认为 true)* @param {String} fileType 导出的文件格式(默认为.xlsx)* @return void* */
export const useDownload = async (api: (param: any) => Promise<any>,tempName: string,params: any = {},isNotify: boolean = true,fileType: string = ".xlsx"
) => {if (isNotify) {ElNotification({title: "温馨提示",message: "如果数据庞大会导致下载缓慢哦,请您耐心等待!",type: "info",duration: 3000});}try {const res = await api(params);const blob = new Blob([res]);// 兼容 edge 不支持 createObjectURL 方法if ("msSaveOrOpenBlob" in navigator) return window.navigator.msSaveOrOpenBlob(blob, tempName + fileType);const blobUrl = window.URL.createObjectURL(blob);const exportFile = document.createElement("a");exportFile.style.display = "none";exportFile.download = `${tempName}${fileType}`;exportFile.href = blobUrl;document.body.appendChild(exportFile);exportFile.click();// 去除下载对 url 的影响document.body.removeChild(exportFile);window.URL.revokeObjectURL(blobUrl);} catch (error) {console.log(error);}
};复制代码
  • useHandleData:

import { ElMessageBox, ElMessage } from "element-plus";
import { HandleData } from "./interface";/*** @description 操作单条数据信息(二次确认【删除、禁用、启用、重置密码】)* @param {Function} api 操作数据接口的api方法(必传)* @param {Object} params 携带的操作数据参数 {id,params}(必传)* @param {String} message 提示信息(必传)* @param {String} confirmType icon类型(不必传,默认为 warning)* @return Promise*/
export const useHandleData = <P = any, R = any>(api: (params: P) => Promise<R>,params: Parameters<typeof api>[0],message: string,confirmType: HandleData.MessageType = "warning"
) => {return new Promise((resolve, reject) => {ElMessageBox.confirm(`是否${message}?`, "温馨提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: confirmType,draggable: true}).then(async () => {const res = await api(params);if (!res) return reject(false);ElMessage({type: "success",message: `${message}成功!`});resolve(true);});});
};
复制代码

2、Protable 组件:

  • ProTable:

<template><!-- 查询表单 card --><SearchForm:search="search":reset="reset":searchParam="searchParam":columns="searchColumns":searchCol="searchCol"v-show="isShowSearch"/><!-- 表格内容 card --><div class="card table"><!-- 表格头部 操作按钮 --><div class="table-header"><div class="header-button-lf"><slot name="tableHeader" :selectedListIds="selectedListIds" :selectList="selectedList" :isSelected="isSelected"></slot></div><div class="header-button-ri" v-if="toolButton"><el-button :icon="Refresh" circle @click="getTableList"> </el-button><el-button :icon="Printer" circle v-if="columns.length" @click="handlePrint"> </el-button><el-button :icon="Operation" circle v-if="columns.length" @click="openColSetting"> </el-button><el-button :icon="Search" circle v-if="searchColumns.length" @click="isShowSearch = !isShowSearch"> </el-button></div></div><!-- 表格主体 --><el-tableref="tableRef"v-bind="$attrs":data="tableData":border="border":row-key="getRowKeys"@selection-change="selectionChange"><!-- 默认插槽 --><slot></slot><template v-for="item in tableColumns" :key="item"><!-- selection || index --><el-table-columnv-bind="item":align="item.align ?? 'center'":reserve-selection="item.type == 'selection'"v-if="item.type == 'selection' || item.type == 'index'"></el-table-column><!-- expand 支持 tsx 语法 && 作用域插槽 (tsx > slot) --><el-table-column v-bind="item" :align="item.align ?? 'center'" v-if="item.type == 'expand'" v-slot="scope"><component :is="item.render" :row="scope.row" v-if="item.render"> </component><slot :name="item.type" :row="scope.row" v-else></slot></el-table-column><!-- other 循环递归 --><TableColumn v-if="!item.type && item.prop && item.isShow" :column="item"><template v-for="slot in Object.keys($slots)" #[slot]="scope"><slot :name="slot" :row="scope.row"></slot></template></TableColumn></template><!-- 无数据 --><template #empty><div class="table-empty"><img src="@/assets/images/notData.png" alt="notData" /><div>暂无数据</div></div></template></el-table><!-- 分页组件 --><Paginationv-if="pagination":pageable="pageable":handleSizeChange="handleSizeChange":handleCurrentChange="handleCurrentChange"/></div><!-- 列设置 --><ColSetting v-if="toolButton" ref="colRef" v-model:colSetting="colSetting" />
</template><script setup lang="ts" name="ProTable">
import { ref, watch, computed, provide } from "vue";
import { useTable } from "@/hooks/useTable";
import { useSelection } from "@/hooks/useSelection";
import { BreakPoint } from "@/components/Grid/interface";
import { ColumnProps } from "@/components/ProTable/interface";
import { ElTable, TableProps } from "element-plus";
import { Refresh, Printer, Operation, Search } from "@element-plus/icons-vue";
import { filterEnum, formatValue, handleProp, handleRowAccordingToProp } from "@/utils/util";
import SearchForm from "@/components/SearchForm/index.vue";
import Pagination from "./components/Pagination.vue";
import ColSetting from "./components/ColSetting.vue";
import TableColumn from "./components/TableColumn.vue";
import printJS from "print-js";// 表格 DOM 元素
const tableRef = ref<InstanceType<typeof ElTable>>();// 是否显示搜索模块
const isShowSearch = ref<boolean>(true);interface ProTableProps extends Partial<Omit<TableProps<any>, "data">> {columns: ColumnProps[]; // 列配置项requestApi: (params: any) => Promise<any>; // 请求表格数据的api ==> 必传dataCallback?: (data: any) => any; // 返回数据的回调函数,可以对数据进行处理 ==> 非必传title?: string; // 表格标题,目前只在打印的时候用到 ==> 非必传pagination?: boolean; // 是否需要分页组件 ==> 非必传(默认为true)initParam?: any; // 初始化请求参数 ==> 非必传(默认为{})border?: boolean; // 是否带有纵向边框 ==> 非必传(默认为true)toolButton?: boolean; // 是否显示表格功能按钮 ==> 非必传(默认为true)selectId?: string; // 当表格数据多选时,所指定的 id ==> 非必传(默认为 id)searchCol?: number | Record<BreakPoint, number>; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }
}// 接受父组件参数,配置默认值
const props = withDefaults(defineProps<ProTableProps>(), {columns: () => [],pagination: true,initParam: {},border: true,toolButton: true,selectId: "id",searchCol: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 })
});// 表格多选 Hooks
const { selectionChange, getRowKeys, selectedList, selectedListIds, isSelected } = useSelection(props.selectId);// 表格操作 Hooks
const { tableData, pageable, searchParam, searchInitParam, getTableList, search, reset, handleSizeChange, handleCurrentChange } =useTable(props.requestApi, props.initParam, props.pagination, props.dataCallback);// 清空选中数据列表
const clearSelection = () => tableRef.value!.clearSelection();// 监听页面 initParam 改化,重新获取表格数据
watch(() => props.initParam,() => {getTableList();},{ deep: true }
);// 接收 columns 并设置为响应式
const tableColumns = ref<ColumnProps[]>(props.columns);// 定义 enumMap 存储 enum 值(避免异步请求无法格式化单元格内容 || 无法填充搜索下拉选择)
const enumMap = ref(new Map<string, { [key: string]: any }[]>());
provide("enumMap", enumMap);// 扁平化 columns && 处理 tableColumns 数据
const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => {columns.forEach(async col => {if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children));flatArr.push(col);// 给每一项 column 添加 isShow && isFilterEnum 属性col.isShow = col.isShow ?? true;col.isFilterEnum = col.isFilterEnum ?? true;if (!col.enum) return;// 如果当前 enum 为后台数据需要请求数据,则调用该请求接口,并存储到 enumMapif (typeof col.enum !== "function") return enumMap.value.set(col.prop!, col.enum);const { data } = await col.enum();enumMap.value.set(col.prop!, data);});return flatArr.filter(item => !item._children?.length);
};// 扁平 columns
const flatColumns = ref<ColumnProps[]>();
flatColumns.value = flatColumnsFunc(tableColumns.value as any);// 过滤需要搜索的配置项 && 处理搜索排序
const searchColumns = flatColumns.value.filter(item => item.search?.el).sort((a, b) => (b.search?.order ?? 0) - (a.search?.order ?? 0));// 设置搜索表单的默认值
searchColumns.forEach(column => {if (column.search?.defaultValue !== undefined && column.search?.defaultValue !== null) {searchInitParam.value[column.search.key ?? handleProp(column.prop!)] = column.search?.defaultValue;}
});// 列设置 ==> 过滤掉不需要设置显隐的列
const colRef = ref();
const colSetting = tableColumns.value!.filter(item => {return item.isShow && item.type !== "selection" && item.type !== "index" && item.type !== "expand" && item.prop !== "operation";
});
const openColSetting = () => {colRef.value.openColSetting();
};// 处理打印数据(把后台返回的值根据 enum 做转换)
const printData = computed(() => {let printDataList = JSON.parse(JSON.stringify(selectedList.value.length ? selectedList.value : tableData.value));let colEnumList = flatColumns.value!.filter(item => item.enum || (item.prop && item.prop.split(".").length > 1));colEnumList.forEach(colItem => {printDataList.forEach((tableItem: { [key: string]: any }) => {tableItem[handleProp(colItem.prop!)] =colItem.prop!.split(".").length > 1 && !colItem.enum? formatValue(handleRowAccordingToProp(tableItem, colItem.prop!)): filterEnum(handleRowAccordingToProp(tableItem, colItem.prop!), enumMap.value.get(colItem.prop!), colItem.fieldNames);});});return printDataList;
});// 打印表格数据(💥 多级表头数据打印时,只能扁平化成一维数组,printJs 不支持多级表头打印)
const handlePrint = () => {printJS({printable: printData.value,header: props.title && `<div style="display: flex;flex-direction: column;text-align: center"><h2>${props.title}</h2></div>`,properties: flatColumns.value!.filter(item =>item.isShow && item.type !== "selection" && item.type !== "index" && item.type !== "expand" && item.prop !== "operation").map((item: ColumnProps) => {return {field: handleProp(item.prop!),displayName: item.label};}),type: "json",gridHeaderStyle:"border: 1px solid #ebeef5;height: 45px;font-size: 14px;color: #232425;text-align: center;background-color: #fafafa;",gridStyle: "border: 1px solid #ebeef5;height: 40px;font-size: 14px;color: #494b4e;text-align: center"});
};// 暴露给父组件的参数和方法(外部需要什么,都可以从这里暴露出去)
defineExpose({ element: tableRef, tableData, searchParam, pageable, getTableList, clearSelection });
</script>复制代码
  • TableColumn:

<template><component :is="renderLoop(column)"></component>
</template><script lang="tsx" setup>
import { inject, ref, useSlots } from "vue";
import { ElTableColumn, ElTag } from "element-plus";
import { filterEnum, formatValue, handleRowAccordingToProp } from "@/utils/util";
import { ColumnProps } from "@/components/ProTable/interface";const slots = useSlots();defineProps<{ column: ColumnProps }>();const enumMap = inject("enumMap", ref(new Map()));// 渲染表格数据
const renderCellData = (item: ColumnProps, scope: { [key: string]: any }) => {return enumMap.value.get(item.prop) && item.isFilterEnum? filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop)!, item.fieldNames): formatValue(handleRowAccordingToProp(scope.row, item.prop!));
};// 获取 tag 类型
const getTagType = (item: ColumnProps, scope: { [key: string]: any }) => {return filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop), item.fieldNames, "tag") as any;
};const renderLoop = (item: ColumnProps) => {return (<>{item.isShow && (<ElTableColumn{...item}align={item.align ?? "center"}showOverflowTooltip={item.showOverflowTooltip ?? item.prop !== "operation"}>{{default: (scope: any) => {if (item._children) return item._children.map(child => renderLoop(child));if (item.render) return item.render(scope);if (slots[item.prop!]) return slots[item.prop!]!(scope);if (item.tag) return <ElTag type={getTagType(item, scope)}>{renderCellData(item, scope)}</ElTag>;return renderCellData(item, scope);},header: () => {if (item.headerRender) return item.headerRender(item);if (slots[`${item.prop}Header`]) return slots[`${item.prop}Header`]!({ row: item });return item.label;}}}</ElTableColumn>)}</>);
};
</script>复制代码

3、页面使用 ProTable 组件:

<template><div class="table-box"><ProTableref="proTable"title="用户列表":columns="columns":requestApi="getTableList":initParam="initParam":dataCallback="dataCallback"><!-- 表格 header 按钮 --><template #tableHeader="scope"><el-button type="primary" :icon="CirclePlus" @click="openDrawer('新增')" v-auth="['add']">新增用户</el-button><el-button type="primary" :icon="Upload" plain @click="batchAdd" v-auth="['batchAdd']">批量添加用户</el-button><el-button type="primary" :icon="Download" plain @click="downloadFile" v-auth="['export']">导出用户数据</el-button><el-button type="danger" :icon="Delete" plain @click="batchDelete(scope.selectedListIds)" :disabled="!scope.isSelected">批量删除用户</el-button></template><!-- Expand --><template #expand="scope">{{ scope.row }}</template><!-- usernameHeader --><template #usernameHeader="scope"><el-button type="primary" @click="ElMessage.success('我是通过作用域插槽渲染的表头')">{{ scope.row.label }}</el-button></template><!-- createTime --><template #createTime="scope"><el-button type="primary" link @click="ElMessage.success('我是通过作用域插槽渲染的内容')">{{ scope.row.createTime }}</el-button></template><!-- 表格操作 --><template #operation="scope"><el-button type="primary" link :icon="View" @click="openDrawer('查看', scope.row)">查看</el-button><el-button type="primary" link :icon="EditPen" @click="openDrawer('编辑', scope.row)">编辑</el-button><el-button type="primary" link :icon="Refresh" @click="resetPass(scope.row)">重置密码</el-button><el-button type="primary" link :icon="Delete" @click="deleteAccount(scope.row)">删除</el-button></template></ProTable><UserDrawer ref="drawerRef" /><ImportExcel ref="dialogRef" /></div>
</template><script setup lang="tsx" name="useComponent">
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { User } from "@/api/interface";
import { ColumnProps } from "@/components/ProTable/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { useDownload } from "@/hooks/useDownload";
import { useAuthButtons } from "@/hooks/useAuthButtons";
import ProTable from "@/components/ProTable/index.vue";
import ImportExcel from "@/components/ImportExcel/index.vue";
import UserDrawer from "@/views/proTable/components/UserDrawer.vue";
import { CirclePlus, Delete, EditPen, Download, Upload, View, Refresh } from "@element-plus/icons-vue";
import {getUserList,deleteUser,editUser,addUser,changeUserStatus,resetUserPassWord,exportUserInfo,BatchAddUser,getUserStatus,getUserGender
} from "@/api/modules/user";// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref();// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive({type: 1
});// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 datalist && total && pageNum && pageSize 这些字段,那么你可以在这里进行处理成这些字段
const dataCallback = (data: any) => {return {datalist: data.datalist,total: data.total,pageNum: data.pageNum,pageSize: data.pageSize};
};// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
// 默认不做操作就直接在 ProTable 组件上绑定 :requestApi="getUserList"
const getTableList = (params: any) => {let newParams = { ...params };newParams.username && (newParams.username = "custom-" + newParams.username);return getUserList(newParams);
};// 页面按钮权限(按钮权限既可以使用 hooks,也可以直接使用 v-auth 指令,指令适合直接绑定在按钮上,hooks 适合根据按钮权限显示不同的内容)
const { BUTTONS } = useAuthButtons();// 自定义渲染表头(使用tsx语法)
const headerRender = (row: ColumnProps) => {return (<el-buttontype="primary"onClick={() => {ElMessage.success("我是通过 tsx 语法渲染的表头");}}>{row.label}</el-button>);
};// 表格配置项
const columns: ColumnProps[] = [{ type: "selection", fixed: "left", width: 80 },{ type: "index", label: "#", width: 80 },{ type: "expand", label: "Expand", width: 100 },{prop: "username",label: "用户姓名",search: { el: "input" },render: scope => {return (<el-button type="primary" link onClick={() => ElMessage.success("我是通过 tsx 语法渲染的内容")}>{scope.row.username}</el-button>);}},{prop: "gender",label: "性别",enum: getUserGender,fieldNames: { label: "genderLabel", value: "genderValue" },search: { el: "select" }},// 多级 prop{ prop: "user.detail.age", label: "年龄", search: { el: "input" } },{ prop: "idCard", label: "身份证号", search: { el: "input" } },{ prop: "email", label: "邮箱", search: { el: "input" } },{ prop: "address", label: "居住地址" },{prop: "status",label: "用户状态",enum: getUserStatus,fieldNames: { label: "userLabel", value: "userStatus" },search: {el: "tree-select",props: { props: { label: "userLabel" }, nodeKey: "userStatus" }},render: (scope: { row: User.ResUserList }) => {return (<>{BUTTONS.value.status ? (<el-switchmodel-value={scope.row.status}active-text={scope.row.status ? "启用" : "禁用"}active-value={1}inactive-value={0}onClick={() => changeStatus(scope.row)}/>) : (<el-tag type={scope.row.status ? "success" : "danger"}>{scope.row.status ? "启用" : "禁用"}</el-tag>)}</>);}},{prop: "createTime",label: "创建时间",headerRender,width: 200,search: {el: "date-picker",span: 2,defaultValue: ["2022-11-12 11:35:00", "2022-12-12 11:35:00"],props: { type: "datetimerange" }}},{ prop: "operation", label: "操作", fixed: "right", width: 330 }
];// 删除用户信息
const deleteAccount = async (params: User.ResUserList) => {await useHandleData(deleteUser, { id: [params.id] }, `删除【${params.username}】用户`);proTable.value.getTableList();
};// 批量删除用户信息
const batchDelete = async (id: string[]) => {await useHandleData(deleteUser, { id }, "删除所选用户信息");proTable.value.clearSelection();proTable.value.getTableList();
};// 重置用户密码
const resetPass = async (params: User.ResUserList) => {await useHandleData(resetUserPassWord, { id: params.id }, `重置【${params.username}】用户密码`);proTable.value.getTableList();
};// 切换用户状态
const changeStatus = async (row: User.ResUserList) => {await useHandleData(changeUserStatus, { id: row.id, status: row.status == 1 ? 0 : 1 }, `切换【${row.username}】用户状态`);proTable.value.getTableList();
};// 导出用户列表
const downloadFile = async () => {useDownload(exportUserInfo, "用户列表", proTable.value.searchParam);
};// 批量添加用户
const dialogRef = ref();
const batchAdd = () => {let params = {title: "用户",tempApi: exportUserInfo,importApi: BatchAddUser,getTableList: proTable.value.getTableList};dialogRef.value.acceptParams(params);
};// 打开 drawer(新增、查看、编辑)
const drawerRef = ref();
const openDrawer = (title: string, rowData: Partial<User.ResUserList> = {}) => {let params = {title,rowData: { ...rowData },isView: title === "查看",api: title === "新增" ? addUser : title === "编辑" ? editUser : "",getTableList: proTable.value.getTableList};drawerRef.value.acceptParams(params);
};
</script>复制代码

七、贡献者 👨‍👦‍👦

  • HalseySpicy[8]

  • denganjia[9]

关于本文

来自:SpicyBoy

https://juejin.cn/post/7166068828202336263

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

这篇关于Vue3.2 + Element-Plus 二次封装 el-table(Pro版)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/672814

相关文章

Pandas透视表(Pivot Table)的具体使用

《Pandas透视表(PivotTable)的具体使用》透视表用于在数据分析和处理过程中进行数据重塑和汇总,本文就来介绍一下Pandas透视表(PivotTable)的具体使用,感兴趣的可以了解一下... 目录前言什么是透视表?使用步骤1. 引入必要的库2. 读取数据3. 创建透视表4. 查看透视表总结前言

售价599元起! 华为路由器X1/Pro发布 配置与区别一览

《售价599元起!华为路由器X1/Pro发布配置与区别一览》华为路由器X1/Pro发布,有朋友留言问华为路由X1和X1Pro怎么选择,关于这个问题,本期图文将对这二款路由器做了期参数对比,大家看... 华为路由 X1 系列已经正式发布并开启预售,将在 4 月 25 日 10:08 正式开售,两款产品分别为华

鸿蒙中Axios数据请求的封装和配置方法

《鸿蒙中Axios数据请求的封装和配置方法》:本文主要介绍鸿蒙中Axios数据请求的封装和配置方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.配置权限 应用级权限和系统级权限2.配置网络请求的代码3.下载在Entry中 下载AxIOS4.封装Htt

将Mybatis升级为Mybatis-Plus的详细过程

《将Mybatis升级为Mybatis-Plus的详细过程》本文详细介绍了在若依管理系统(v3.8.8)中将MyBatis升级为MyBatis-Plus的过程,旨在提升开发效率,通过本文,开发者可实现... 目录说明流程增加依赖修改配置文件注释掉MyBATisConfig里面的Bean代码生成使用IDEA生

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

SpringBoot中封装Cors自动配置方式

《SpringBoot中封装Cors自动配置方式》:本文主要介绍SpringBoot中封装Cors自动配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot封装Cors自动配置背景实现步骤1. 创建 GlobalCorsProperties

Spring Boot结成MyBatis-Plus最全配置指南

《SpringBoot结成MyBatis-Plus最全配置指南》本文主要介绍了SpringBoot结成MyBatis-Plus最全配置指南,包括依赖引入、配置数据源、Mapper扫描、基本CRUD操... 目录前言详细操作一.创建项目并引入相关依赖二.配置数据源信息三.编写相关代码查zsRArly询数据库数

GORM中Model和Table的区别及使用

《GORM中Model和Table的区别及使用》Model和Table是两种与数据库表交互的核心方法,但它们的用途和行为存在著差异,本文主要介绍了GORM中Model和Table的区别及使用,具有一... 目录1. Model 的作用与特点1.1 核心用途1.2 行为特点1.3 示例China编程代码2. Tab

Java导入、导出excel用法步骤保姆级教程(附封装好的工具类)

《Java导入、导出excel用法步骤保姆级教程(附封装好的工具类)》:本文主要介绍Java导入、导出excel的相关资料,讲解了使用Java和ApachePOI库将数据导出为Excel文件,包括... 目录前言一、引入Apache POI依赖二、用法&步骤2.1 创建Excel的元素2.3 样式和字体2.

mybatis-plus分页无效问题解决

《mybatis-plus分页无效问题解决》本文主要介绍了mybatis-plus分页无效问题解决,原因是配置分页插件的版本问题,旧版本和新版本的MyBatis-Plus需要不同的分页配置,感兴趣的可... 昨天在做一www.chinasem.cn个新项目使用myBATis-plus分页一直失败,后来经过多方