点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
ProTable 组件目前已是
2.0版本
,在 1.0版本[1] 中大家提出的问题与功能优化,目前已经得到优化和解决。
欢迎大家在使用过程中发现任何问题或更好的想法,都可以在下方评论区留言,或者我的开源项目 issues 中提出。如果你觉得还不错,请帮我点个小小的 Star
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 组件使用更佳(项目中有使用示例)
1、表格搜索区域
2、表格数据操作按钮区域
3、表格功能按钮区域
4、表格主体内容展示区域
5、表格分页区域
可以看到搜索区域的字段都是存在于表格当中的,并且每个页面的搜索、重置方法都是一样的逻辑,只是不同的查询参数而已。我们完全可以在传表格配置项 columns 时,直接指定某个 column 的 search 配置,就能把该项变为搜索项,然后使用 el 字段可以指定搜索框的类型,最后把表格的搜索方法都封装成 Hooks 钩子函数。页面上完全就不会存在任何搜索、重置逻辑了。
在 1.0 版本中使用 v-if 判断太麻烦,为了更方便用户传递参数,搜索组件在 2.0 版本中通过 component :is 动态组件 && v-bind 属性透传实现,将用户传递的参数全部透传到组件上,所以大家可以直接根据 element 官方文档在 props 中传递参数了。以下代码还结合了自己逻辑上的一些处理:
复制代码
动画.gif表格搜索组件在 2.0 版本中还支持了响应式配置,使用 Grid 方法进行整体重构 。
表格数据操作按钮基本上每个页面都会不一样,所以我们直接使用 作用域插槽 来完成每个页面的数据操作按钮区域,作用域插槽 可以将表格多选数据信息从 ProTable 的 Hooks 多选钩子函数中传到页面上使用。
scope 数据中包含:selectedList(当前选择的数据)、selectedListIds(当前选择的数据id)、isSelected(当前是否选中的数据)
新增用户
批量添加用户
导出用户数据
批量删除用户
复制代码
这块区域没什么特殊功能,只有四个按钮,其功能分别为:表格数据刷新(一直会携带当前查询和分页条件)、表格数据打印、表格列设置(列显隐、列排序)、表格搜索区域显隐(方便展示更多的数据信息)。可通过 toolButton 属性控制这块区域的显隐。
image.png表格打印功能基于 PrintJs 实现,因 PrintJs 不支持多级表头打印,所以当页面存在多级表头时,只会打印最后一级表头。表格打印功能可根据显示的列和勾选的数据动态打印,默认打印当前显示的所有数据。
该区域是最重要的数据展示区域,对于使用最多的功能就是表头和单元格内容可以自定义渲染,在第 1.0 版本中,自定义表头只支持传入
renderHeader
方法,自定义单元格内容只支持slot
插槽。
目前 2.0 版本中,表头支持
headerRender
方法(避免与 el-table-column 上的属性重名导致报错)、作用域插槽(column.prop + 'Header'
)两种方式自定义,单元格内容支持render
方法和作用域插槽(column 上的 prop 属性
)两种方式自定义。
使用作用域插槽:
{{ scope.row.username }}
{{ scope.row.label }}
复制代码
使用 tsx 语法:
复制代码
最强大的功能:如果你想使用
el-table
的任何属性、事件,目前通过属性透传都能支持。如果你还不了解属性透传,请阅读 vue 官方文档:cn.vuejs.org/guide/compo…[5]
ProTable 组件上的绑定的所有属性和事件都会通过 v-bind="$attrs"
透传到 el-table 上。
ProTable 组件内部暴露了 el-table DOM,可通过 proTable.value.element.方法名
调用其方法。
复制代码
分页区域也没有什么特殊的功能,该支持的都支持了(页面上使用 ProTable 组件完全不存在分页逻辑)
复制代码
使用
v-bind="$atts"
通过属性透传将 ProTable 组件属性全部透传到 el-table 上,所以我们支持 el-table 的所有 Props 属性。在此基础上,还扩展了以下 Props:
属性名 | 类型 | 是否必传 | 默认值 | 属性描述 |
---|---|---|---|---|
columns | ColumnProps | ✅ | — | ProTable 组件会根据此字段渲染搜索表单与表格列,详情见 ColumnProps |
requestApi | Function | ✅ | — | 获取表格数据的请求 API |
dataCallback | Function | ❌ | — | 后台返回数据的回调函数,可对后台返回数据进行处理 |
title | String | ❌ | — | 表格标题,目前只在打印的时候用到 |
pagination | Boolean | ❌ | true | 是否显示分页组件 |
initParam | Object | ❌ | {} | 表格请求的初始化参数 |
toolButton | Boolean | ❌ | true | 是否显示表格功能按钮 |
selectId | String | ❌ | 'id' | 当表格数据多选时,所指定的 id |
searchCol | Object | ❌ | { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 } | 表格搜索项每列占比配置 |
使用
v-bind="column"
通过属性透传将每一项 column 属性全部透传到 el-table-column 上,所以我们支持 el-table-column 的所有 Props 属性。在此基础上,还扩展了以下 Props:
属性名 | 类型 | 是否必传 | 默认值 | 属性描述 |
---|---|---|---|---|
tag | Boolean | ❌ | false | 当前单元格值是否为标签展示 |
isShow | Boolean | ❌ | true | 当前列是否显示在表格内 |
search | SearchProps | ❌ | — | 搜索项配置,详情见 SearchProps |
enum | Object | Function | ❌ | — | 字典,可格式化单元格内容,还可以作为搜索框的下拉选项(字典可以为API请求函数,内部会自动执行) |
isFilterEnum | Boolean | ❌ | true | 当前单元格值是否根据 enum 格式化(例如 enum 只作为搜索项数据,不参与内容格式化) |
fieldNames | Object | ❌ | — | 指定字典 label && value 的 key 值 |
headerRender | Function | ❌ | — | 自定义表头内容渲染(tsx 语法、h 语法) |
render | Function | ❌ | — | 自定义单元格内容渲染(tsx 语法、h 语法) |
_children | ColumnProps | ❌ | — | 多级表头 |
使用
v-bind="column.search.props“
通过属性透传将 search.props 属性全部透传到每一项搜索组件上,所以我们支持 input、select、tree-select、date-packer、time-picker、time-select、swicth 大部分属性,并在其基础上还扩展了以下 Props:
属性名 | 类型 | 是否必传 | 默认值 | 属性描述 |
---|---|---|---|---|
el | String | ✅ | — | 当前项搜索框的类型,支持:input、select、tree-select、cascader、date-packer、time-picker、time-select、swicth |
props | Object | ❌ | — | 根据 element plus 官方文档来传递,该属性所有值会透传到组件 |
defaultValue | Any | ❌ | — | 搜索项默认值 |
key | String | ❌ | — | 当搜索项 key 不为 prop 属性时,可通过 key 指定 |
order | Number | ❌ | — | 搜索项排序(从大到小) |
span | Number | ❌ | 1 | 搜索项所占用的列数,默认为 1 列 |
offset | Number | ❌ | — | 搜索字段左侧偏移列数 |
根据 ElementPlus Table 文档在 ProTable 组件上绑定事件即可,组件会通过 $attrs 透传给 el-table。
el-table 事件文档链接[6]
ProTable 组件暴露了 el-table 实例和一些组件内部的参数和方法:
el-table 方法文档链接[7]
方法名 | 描述 |
---|---|
element | el-table 实例,可以通过element.方法名 来调用 el-table 的所有方法 |
tableData | 当前页面所展示的数据 |
searchParam | 所有的搜索参数,不包含分页 |
pageable | 当前表格的分页数据 |
getTableList | 获取、刷新表格数据的方法(携带所有参数) |
clearSelection | 清空表格所选择的数据,除此方法之外还可使用 element.clearSelection() |
enumMap | 当前表格使用的所有字典数据(Map 数据结构) |
插槽名 | 描述 |
---|---|
— | 默认插槽,支持直接写 el-table-column |
tableHeader | 自定义表格头部左侧区域的插槽,一般情况该区域放操作按钮 |
append | 插入至表格最后一行之后的内容, 如果需要对表格的内容进行无限滚动操作,可能需要用到这个 slot。若表格有合计行,该 slot 会位于合计行之上。 |
empty | 当表格数据为空时自定义的内容 |
column.prop |
单元格的作用域插槽 |
column.prop + "Header" |
表头的作用域插槽 |
前提:首先我们在封装 ProTable 组件的时候,在不影响 el-table 原有的属性、事件、方法的前提下,然后在其基础上做二次封装,否则做得再好,也不太完美。
思路:把一个表格页面所有重复的功能 (表格多选、查询、重置、刷新、分页、数据操作二次确认、文件下载、文件上传) 都封装成 Hooks 函数钩子或组件,然后在 ProTable 组件中使用这些函数钩子或组件。在页面中使用的时,只需传给 ProTable 当前表格数据的请求 API、表格配置项 columns 就行了,数据传输都使用 作用域插槽 或 tsx 语法从 ProTable 传递给父组件就能在页面上获取到了。
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,
initParam: object = {},
isPageable: boolean = true,
dataCallBack?: (data: any) => any
) => {
const state = reactive({
// 表格数据
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(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,
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 = (
api: (params: P) => Promise,
params: Parameters[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);
});
});
};
复制代码
ProTable:
暂无数据
复制代码
TableColumn:
复制代码
新增用户
批量添加用户
导出用户数据
批量删除用户
{{ scope.row }}
{{ scope.row.label }}
{{ scope.row.createTime }}
查看
编辑
重置密码
删除
复制代码
HalseySpicy[8]
denganjia[9]
关于本文
来自:SpicyBoy
https://juejin.cn/post/7166068828202336263
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波