第四节:剖析表格组件的封装思路(表格配置、分页条、页面传值、高度自适应、单选、多选等)
一. 前言
1. 封装的表格组件功能
(1). 通过配置文件的形式渲染表格列,并且表格列的格式可以自定义修改
(2). 表格展开功能
(3). 表格列排序
(4). 表格单选、多选
(5). 表格表头支持两行模式
(6). 表格列合并、行合并
(7). 表格高度自适应
(8). 表格动态显示分页条
2. 涉及的技术点
(1). 页面传值
(2). 命名插槽
(3). v-model 等价 :xxx + @update:xxx
(4). 其它等等
二. 表格组件核心封装 【未完】
1. 组成
2. 核心部分
(1). 顶部按钮区域
两个命名插槽,分别为btnLeft、btnRight,一左一右,用来显示按钮区域。
<!-- 1 按钮区域 -->
<div class="myBtnArea">
<div><slot name="btnLeft"></slot></div>
<div><slot name="btnRight"></slot></div>
</div>
调用:
<ypfTable
ref="ypfTableRef"
>
<!-- 2.1 按钮区域 -->
<template #btnLeft>
<el-button size="small" type="success" @click="onOpenAddDialog" v-auth="authList.add"> 新增 </el-button>
<el-button size="small" type="danger" @click="deleteObjs(null)" v-auth="authList.delMany"> 删除 </el-button>
</template>
<template #btnRight>
<el-button size="small" type="primary" round v-auth="authList.excel" @click="exportExcel"> 导出 </el-button>
<el-button size="small" type="success" round v-auth="authList.arrange" @click="openSettingDialog"> 设置 </el-button>
</template>
</ypfTable>
(2). 中间表格区域
date绑定数据源;v-bing绑定表格属性;selection-change监听多选;current-change监听单选;sort-change监听列排序;span-method用来处理表格行、列合并事件。
另外,每一个表格列除了使用el-table-column默认的插槽外,在此之上,又借助传递的配置中的slotName字段,声明自己的命名插槽,便于表格自定义格式。
<!-- 2. 表格区域 -->
<div><slot name="tableTitle"></slot></div>
<el-table
size="default"
ref="tableRef"
:[myHeightProp]="`calc(100vh - ${myDynamicHeight})`"
:data="tableRows"
v-bind="tableProp"
@selection-change="tableSelChange"
@current-change="tableSimpleSelChange"
@sort-change="sortChange"
:span-method="mergeFn"
>
<el-table-column v-if="showSelectColumn" type="selection" width="26" />
<template v-for="propItem in propList" :key="propItem.prop">
<el-table-column v-bind="propItem">
<!-- 2. 普通表格,不含多级表头 -->
<template #default="scope">
<!-- :name,属于具名插槽,有默认值,说明外界可以自己传进来,也可以使用默认值-->
<!-- :row1="scope.row" 是插槽prop,对外传值 -->
<!-- (1).如果是匿名插槽,外界v-slot:default="myInfo"接收,然后myInfo.row1.xxx调用,然后可以省略为v-slot="myInfo" -->
<!-- (2).如果是具名插槽,外界v-slot:name="myInfo"接收, 然后myInfo.row1.xxx调用, 省略为 #name="myInfo",这里的name是实际的propItem.slotName此处是具名插槽! -->
<slot :name="propItem.slotName" :row1="scope.row">
<!-- 下面是默认值,只有当没有提供插入内容的时候才会显示 -->
{{ scope.row[propItem.prop] }}
</slot>
</template>
</el-table-column>
</template>
</el-table>
调用:
<ypfTable
ref="ypfTableRef"
v-bind="contentTableConfig"
:tableRows="tableData.tableRows"
:propList="bindPropList"
:tableTotal="tableData.total"
:page="tableData.param"
@update:page="UpdatePage"
@update:tableSelChange="tableSelChange"
@update:sortChange="sortChange"
>
<!-- 2.2 表格区域 -->
<template #userSex="myInfo">
<el-tag type="success" v-if="myInfo.row1.userSex == 0">男</el-tag>
<el-tag type="info" v-else>女</el-tag>
</template>
<template #addTime="myInfo">
{{ formatDate(new Date(myInfo.row1.addTime), 'YYYY-mm-dd') }}
</template>
<template #handler="myInfo">
<el-button size="small" type="text" @click="onOpenEditDialog(myInfo.row1)" v-auth="authList.edit">修改</el-button>
<el-button size="small" type="text" @click="deleteObjs(myInfo.row1.id)" v-auth="authList.delOne">删除</el-button>
</template>
<!-- 2.3 分页区域 -->
</ypfTable>
(3). 底部分页条区域
具体的传值和监听往后看。
<!-- 2.3 分页区域 -->
<el-pagination
v-if="showPagination"
style="display: flex; justify-content: center"
layout="total, sizes, prev, pager, next, jumper"
class="mt15"
background
:page-sizes="[5, 10, 20, 30]"
:total="tableTotal"
v-model:current-page="myPage.pageNum"
v-model:page-size="myPage.pageSize"
>
</el-pagination>
3. 如何实现表格列的展开?
此处要包含表格展开的写法:核心点,需要在第一个 el-table-colum 上加 type="expend" (结合新船系统,表格嵌套表格的样例)
即配置文件的第一列:
const contentTableConfig = {
tableTitle: 'List of Drawings',
//表格显示列和排列顺序(默认的) ,该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList
originPropList: [
{ type: 'expand', fixed: 'left', prop: 'spread', label: '', align: 'center', slotName: 'spread' },
{ prop: 'x1', label: '*SI No.', align: 'center' },
{ prop: 'x12', label: 'Verified', align: 'center' },
{ prop: 'x13', label: '*Planned Submission Date', align: 'center', width: '110px' },
{ prop: 'x14', label: 'Iteration Count', align: 'center' },
],
// 开启多选列
showSelectColumn: false,
// 是否显示底部分页
showPagination: false,
};
页面调用的时候,即表格组件内部嵌套
#spread="myInfo" 表示表格第一列的命名插槽
<ypfTable v-bind="contentTableConfig" :tableRows="myTableData" :propList="contentTableConfig.originPropList" :tableTotal="1">
<!-- 2.2 表格区域 -->
<template #spread="myInfo">
<div class="mySpread">
<div>
<div>
<el-button size="small" type="primary" @click="onOpenAddDialog(myInfo.row1.x1, 1)">Add Comment</el-button>
<el-button size="small" type="primary" @click="onOpenEditDialog(selRowObj1, myInfo.row1.x1, 1)">Edit comment</el-button>
</div>
<!-- 子表格1 -->
<ypfTable
v-bind="childTable1Config"
:tableRows="myInfo.row1.childData1"
:propList="childTable1Config.originPropList"
:tableTotal="1"
@update:tableSimpleSelChange="GetSimpleSelRow1"
>
<template #handler="myInfo1">
<el-button size="small" type="text" @click="childDeleteObjs(myInfo.row1.x1, myInfo1.row1.id, 1)" class="color-red">Delete</el-button>
<el-upload :show-file-list="false" class="mysub-upload" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15">
<el-button size="small" type="text">Upload</el-button>
</el-upload>
</template>
</ypfTable>
</div>
</div>
</template>
</ypfTable>
4. 如何实现表格多级表头的问题?
配置文件: 详见 x15_Parent
const contentTableConfig = {
tableTitle: 'List of Drawings',
//表格显示列和排列顺序(默认的) ,该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList
originPropList: [
{ type: 'expand', fixed: 'left', prop: 'spread', label: '', align: 'center', slotName: 'spread' },
{ prop: 'x13', label: '*Planned Submission Date', align: 'center', width: '110px' },
{ prop: 'x14', label: 'Iteration Count', align: 'center' },
{
prop: 'x15_Parent',
label: 'EXPERT',
align: 'center',
children: [
{ prop: 'x15', label: 'Received', align: 'center', width: '110px' },
{ prop: 'x16', label: 'Deadline', align: 'center', width: '110px' },
{ prop: 'x17', label: 'Sent', align: 'center', width: '110px' },
],
},
{ prop: 'x18', label: 'Approved Drawing', align: 'center' },
{ prop: 'x19', label: 'Update Approved Drawing', align: 'center' },
],
// 开启多选列
showSelectColumn: false,
// 是否显示底部分页
showPagination: false,
};
5. 最终代码分享
表格组件
<template> <div class="myContentArea"> <!-- 1 按钮区域 --> <div class="myBtnArea"> <div><slot name="btnLeft"></slot></div> <div><slot name="btnRight"></slot></div> </div> <!-- 2. 表格区域 --> <div><slot name="tableTitle"></slot></div> <el-table size="default" ref="tableRef" :[myHeightProp]="`calc(100vh - ${myDynamicHeight})`" :data="tableRows" v-bind="tableProp" @selection-change="tableSelChange" @current-change="tableSimpleSelChange" @sort-change="sortChange" :span-method="mergeFn" > <el-table-column v-if="showSelectColumn" type="selection" width="26" /> <template v-for="propItem in propList" :key="propItem.prop"> <el-table-column v-if="propItem.children" :prop="propItem.prop" :label="propItem.label" :align="propItem.align"> <!-- 1. 下面这一层el-table-column 是应对两级表头问题的封装 --> <el-table-column v-for="childPropItem in propItem.children" :key="childPropItem.prop" v-bind="childPropItem"> <template #default="scope"> <!-- :name,属于具名插槽,有默认值,说明外界可以自己传进来,也可以使用默认值--> <!-- :row1="scope.row" 是插槽prop,对外传值 --> <!-- (1).如果是匿名插槽,外界v-slot:default="myInfo"接收,然后myInfo.row1.xxx调用,然后可以省略为v-slot="myInfo" --> <!-- (2).如果是具名插槽,外界v-slot:name="myInfo"接收, 然后myInfo.row1.xxx调用, 省略为 #name="myInfo",这里的name是实际的propItem.slotName此处是具名插槽! --> <slot :name="childPropItem.slotName" :row1="scope.row"> <!-- 下面是默认值,只有当没有提供插入内容的时候才会显示 --> {{ scope.row[childPropItem.prop] }} </slot> </template> </el-table-column> </el-table-column> <el-table-column v-else v-bind="propItem"> <!-- 2. 普通表格,不含多级表头 --> <template #default="scope"> <!-- 解释:同上 --> <slot :name="propItem.slotName" :row1="scope.row"> <!-- 下面是默认值,只有当没有提供插入内容的时候才会显示 --> {{ scope.row[propItem.prop] }} </slot> </template> </el-table-column> </template> </el-table> <!-- 2.3 分页区域 --> <el-pagination v-if="showPagination" style="display: flex; justify-content: center" layout="total, sizes, prev, pager, next, jumper" class="mt15" background :page-sizes="[5, 10, 20, 30]" :total="tableTotal" v-model:current-page="myPage.pageNum" v-model:page-size="myPage.pageSize" > </el-pagination> </div> </template> <script setup> import { nextTick, onMounted, reactive, watch } from 'vue'; const store = useStore(); // 1. 接收父组件传递过来的植 const props = defineProps({ // 表格数据源 tableRows: { type: Array, default() { return []; }, }, // 表格总条数 tableTotal: { type: Number, default: 0, }, // 表格标题(用处不大) tableTitle: { type: String, default: '', }, // 表格列属性 propList: { type: Array, required: true, }, // 表格是否多选 showSelectColumn: { type: Boolean, default: false, }, // 是否显示分页 showPagination: { type: Boolean, default: false, }, // 分页属性 page: { type: Object, default: () => ({ pageNum: 1, pageSize: 10 }), }, // 合并方法 mergeFn: { type: Function, default() {}, }, // 表格的属性 tableProp: { type: Object, default: () => ({}), }, // 是否启用动态设置高度的功能 isAutoHeight: { type: Boolean, default: true, }, //绑定的表格动态高度属性,当isAutoHeight=true才生效 (取值有:'height'、'max-height') heightProp: { type: String, default: 'max-height', }, }); // 动态决定是否绑定决定表格高度的属性 const myHeightProp = props.isAutoHeight === true ? props.heightProp : ''; const tableRef = ref(null); let myDynamicHeight = ref(''); /** * 计算表格需要减去的高度 */ const calTableHeight = async () => { await nextTick(); let totalHeight = 160; //默认有tagsView,不全屏 let tableoffsetTopHeight = tableRef.value.$el.offsetTop; //表格顶部距离tab(tagsView)底部的距离 let { isTagsview } = store.state.themeConfig.themeConfig; if (!isTagsview) { totalHeight = 126; // 关闭tagsView,单页面模式 } if (store.state.tagsViewRoutes.isTagsViewCurrenFull) { totalHeight = 76; //全屏 } totalHeight += tableoffsetTopHeight; myDynamicHeight.value = totalHeight + 'px'; return myDynamicHeight.value; }; // 页面加载需要初始化一次 onMounted(() => { if (props.isAutoHeight) { //此处需要加个延迟,否则第一次加载的时候会计算错误,导致最外层有滚动条 setTimeout(() => calTableHeight(), 500); // 窗口大小变化需要初始化 window.onresize = () => calTableHeight(); } }); /** * 监听设置开启or关闭tagsView事件 * 监听是否全屏 */ watch([() => store.state.themeConfig.themeConfig.isTagsview, () => store.state.tagsViewRoutes.isTagsViewCurrenFull], () => { if (props.isAutoHeight) { calTableHeight(); } }); // 2. 对外传递事件 const emits = defineEmits(['update:page', 'update:tableSelChange', 'update:tableSimpleSelChange', 'update:sortChange']); //声明一个新对象用于监听props.page的改变,因为要符合单项数据流,不能直接修改props.page const myPage = reactive(props.page); /** * 监听分页的改变(包括每页条数 和 页码) * @param {Number} newPage 变化后的新的分页对象 */ watch(myPage, newPage => { // 例如:上面通过v-model:current-page进行双向绑定,当点击当前页数变化的时候,myPage中的pageNum会自动改变,pageNum同样道理 emits('update:page', { ...newPage }); }); /** * 监听表格选中行变化(服务于多选) * @param {Array} selections 变化后的数组(数组元素为每行的数据) */ const tableSelChange = selections => { emits('update:tableSelChange', selections); }; /** * 监听表格选中行变化(服务于单选) * @param {Object} currentRow 选中行内容 */ const tableSimpleSelChange = (currentRow, oldCurrentRow) => { // console.log(currentRow, oldCurrentRow); emits('update:tableSimpleSelChange', currentRow); }; /** * 监听表格排序 * @param {String} prop 表格排序列名称 * @param {String} order 表格排序方式 descending 或 ascending */ const sortChange = async ({ prop, order }) => { let sortName = prop; let sortDirection = order == 'ascending' ? 'asc' : 'desc'; emits('update:sortChange', { sortName, sortDirection }); }; /* 对外暴露事件 */ defineExpose({ calTableHeight, }); </script> <style scoped lang="scss"> .myContentArea { background-color: var(--el-color-white); padding: 10px 5px 10px 5px; .myBtnArea { // height: 42px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .myBtnArea > div { display: flex; } } </style>
父页面调用
<template> <div> <!-- 1. 搜索栏区域 --> <ypfSearch :activeName="activeName" v-bind="searchFormConfig" v-model="formData" @update:searchClick="searchClick" @update:resetClick="resetClick" @update:handleChange="myHandleChange" > <!-- 此处可以通过具名插槽扩展自己的内容 <template #myOwnArea> </template> --> </ypfSearch> <!-- 2. 内容区域 --> <ypfTable ref="ypfTableRef" v-bind="contentTableConfig" :tableRows="tableData.tableRows" :propList="bindPropList" :tableTotal="tableData.total" :page="tableData.param" @update:page="UpdatePage" @update:tableSelChange="tableSelChange" @update:sortChange="sortChange" > <!-- 2.1 按钮区域 --> <template #btnLeft> <el-button size="small" type="success" @click="onOpenAddDialog" v-auth="authList.add"> 新增 </el-button> <el-button size="small" type="danger" @click="deleteObjs(null)" v-auth="authList.delMany"> 删除 </el-button> </template> <template #btnRight> <el-button size="small" type="primary" round v-auth="authList.excel" @click="exportExcel"> 导出 </el-button> <el-button size="small" type="success" round v-auth="authList.arrange" @click="openSettingDialog"> 设置 </el-button> </template> <!-- 2.2 表格区域 --> <template #userSex="myInfo"> <el-tag type="success" v-if="myInfo.row1.userSex == 0">男</el-tag> <el-tag type="info" v-else>女</el-tag> </template> <template #addTime="myInfo"> {{ formatDate(new Date(myInfo.row1.addTime), 'YYYY-mm-dd') }} </template> <template #handler="myInfo"> <el-button size="small" type="text" @click="onOpenEditDialog(myInfo.row1)" v-auth="authList.edit">修改</el-button> <el-button size="small" type="text" @click="deleteObjs(myInfo.row1.id)" v-auth="authList.delOne">删除</el-button> </template> <!-- 2.3 分页区域 --> </ypfTable> <!-- 3. 弹框区域 --> <AddUer ref="addDialogRef" @updateTableInfo="initTableData" /> <EditUser ref="editDialogRef" @updateTableInfo="initTableData" /> <Setting ref="setDialogRef" @updateTableColumn="initTableColumn" :propList="propList" :pageName="pageName"></Setting> </div> </template> <script setup name="systemUser"> import { getCurrentInstance, nextTick, onMounted, reactive } from 'vue'; import { useStore } from 'vuex'; import { ElMessage } from 'element-plus'; import AddUer from '/@/views/system/user/component/addUser.vue'; import EditUser from '/@/views/system/user/component/editUser.vue'; import Setting from '/@/components/setting/index.vue'; import { myAxios } from '/@/utils/request'; import { formatDate } from '/@/utils/formatTime'; import { auth } from '/@/utils/authFunction'; import { Session } from '/@/utils/storage'; import ypfTable from '/@/components/ypfTable/index.vue'; import ypfSearch from '/@/components/ypfSearch/index.vue'; import { contentTableConfig } from './config/tableConfig'; import { searchFormConfig } from './config/searchFormConfig'; import { TrimObjStrPros } from '/@/utils/myTools'; const { proxy, type } = getCurrentInstance(); const store = useStore(); const pageName = type.name; //当前页面的name值(systemUser) const columnLocalKey = `${store.state.userInfos.userInfos.userAccount}_${pageName}`; //表格显示列数据对应的缓存key, eg: admin_systemUser // 表格对象 const ypfTableRef = ref(null); /** * 监听搜索栏折叠or展开时触发 */ const myHandleChange = () => { // 必须加个延迟,否则获取的高度是折叠前的高度 setTimeout(() => ypfTableRef.value?.calTableHeight(), 500); }; // 按钮权限对象 const authList = reactive({ search: '/system/user/search', add: '/system/user/add', edit: '/system/user/edit', delOne: '/system/user/delOne', delMany: '/system/user/delMany', excel: '/system/user/excel', arrange: '/system/user/arrange', }); // 绑定组件对象区域 const addDialogRef = ref(null); const editDialogRef = ref(null); const setDialogRef = ref(null); // 表格区域 const tableData = reactive({ tableRows: [], //表格数据源 total: 0, selectRowIds: [], //表格选中行的id数组 param: { pageNum: 1, pageSize: 10, sortName: '', //排序字段名称 sortDirection: '', //排序方式 }, }); //表格显示列和排列顺序(默认的) let propList = ref(contentTableConfig.originPropList); // 绑定用的[...propList.value] let bindPropList = ref([]); //搜索栏区域 const activeName = ref('firstCollaspe'); //对应collapse-item的name值,用于默认展开 // 优化:formData中的属性,不需要再写了,完全可以用searchFormConfig配置中的field属性即可 let formOriginData = {}; for (const item of searchFormConfig.formItems) { formOriginData[item.field] = ''; // 下面是处理默认赋值 if (item.field == 'userSex') { formOriginData[item.field] = -1; } } let formData = ref(formOriginData); /** * 初始化表格数据 */ const initTableData = async () => { if (!auth(authList.search)) { ElMessage.error('您没有查询权限'); return; } //将string类型的对象去空格 let myTrimData = TrimObjStrPros(formData.value); const { status, data } = await myAxios({ url: proxy.$url.GetUserInforByConditionUrl, data: { ...tableData.param, ...myTrimData }, }); if (status == 'ok') { tableData.tableRows = data.tableRows; tableData.total = data.total; } }; /** * 初始化表格显示列和顺序 * @param {Array} newColumnData 子页面传递过来的表格显示列和顺序 * 格式: [ { id: '1', label: '账号', columnName: 'userAccount', isSelect: true }, { id: '2', label: '姓名', columnName: 'userRealName', isSelect: false } ] */ const initTableColumn = async newColumnData => { let myColumnData = null; if (newColumnData) { //一.表示从子页面被动调用 myColumnData = newColumnData; } else { // 二. 表示主页面主动加载 //1. 先从缓存中读取 myColumnData = Session.get(columnLocalKey); //2. 缓存中没有,则请求接口 if (!myColumnData) { const { status, data } = await myAxios({ url: proxy.$url.GetUserMenuColumnsUrl, data: { menuName: pageName }, }); if (status == 'ok' && data) { myColumnData = data; Session.set(columnLocalKey, myColumnData); //存到缓存中 } } } //3. 处理数据 if (myColumnData) { //3.1 表示从缓存 或 接口 或子页面传递 中拿到了数据 let latestColumnData = []; myColumnData.forEach(item => { let newItem = propList.value.find(propItem => propItem.prop === item.columnName && item.isSelect == true); if (newItem) latestColumnData.push(newItem); }); bindPropList.value = []; //必须先清空一下,才会触发数据改变 nextTick(() => { bindPropList.value = [...latestColumnData]; }); } else { //3.2 表示缓存或者接口中都没有数据,直接用前端都默认值赋值 nextTick(() => { bindPropList.value = [...propList.value]; }); } }; /** * 搜索事件 */ const searchClick = async () => { // console.log(formData.value); await initTableData(); }; /** * 重置事件 */ const resetClick = async () => { // 清空数据 Object.keys(formData.value).forEach(key => { formData.value[key] = ''; // 下面是处理默认赋值问题 if (key == 'userSex') { formData.value[key] = -1; } }); await initTableData(); }; // 打开新增弹窗 const onOpenAddDialog = () => { addDialogRef.value.openDialog(); }; /** * 打开修改弹窗 * @param {Object} row:该行数据 */ const onOpenEditDialog = row => { editDialogRef.value.openDialog(row); }; /** * 打开设置弹框 */ const openSettingDialog = () => { setDialogRef.value.openDialog(); }; /** * 删除(单个 和 多个) * @param {String} row 单个删除时,删除的id * 对于多个删除,deleteId 为 null */ const deleteObjs = async deleteId => { let delIdsStr = deleteId ?? tableData.selectRowIds; if (delIdsStr.length === 0) { proxy.$message('请选择要删除的行'); return; } let confirmResult = await proxy.$myConfirm('您确定要删除该账户吗?'); if (confirmResult == 'confirm') { const { status, msg } = await myAxios({ url: proxy.$url.DelUserUrl, data: { delIdsStr }, }); if (status === 'ok') { ElMessage.success(msg); await initTableData(); } } }; /** * 监听表格选中行变化 * @param {Array} selections 变化后的数组(数组元素为每行的数据) */ const tableSelChange = selections => { let selectRowIds = selections.map(item => item.id); //返回一个id集合数组 tableData.selectRowIds = selectRowIds; }; /** * 监听子页面传递过来的表格排序参数 * @param {Object} pageInfo {sortName: xx, sortDirection: xx} * 解释:sortName 表格排序列名称, sortDirection 表格排序方式 */ const sortChange = async ({ sortName, sortDirection }) => { if (sortName && sortDirection) { tableData.param.sortName = sortName; tableData.param.sortDirection = sortDirection; await initTableData(); } }; /** * 监听子页面传递的表格分页参数 * @param {Object} pageInfo {pageNum: xx, pageSize: xx} */ const UpdatePage = pageInfo => { if (pageInfo) { //tableData.param 中的sortDirection和sortName被置为 "", 不影响业务 tableData.param = { ...pageInfo }; initTableData(); } }; /** * 按顺序导出显示列的excel表格 */ const exportExcel = async () => { let confirmResult = await proxy.$myConfirm('您确定要导出Excel吗?'); let myTrimData = TrimObjStrPros(formData.value); //将string类型的对象去空格 let searchStr = JSON.stringify(myTrimData); //搜索条件字符串 let pageStr = JSON.stringify(tableData.param); //分页条件字符串 //需要导出列的信息 注:只有含有excelWidth属性的列才是需要导出的列 let bindPropListStr = JSON.stringify(bindPropList.value.filter(item => item.excelWidth)); if (confirmResult == 'confirm') { // 版本1-路径的形式 // window.location.href = proxy.$url.UserDownLoadFileExcelUrl + '?auth=' + Session.get('token') + '&searchStr=' + searchStr + '&pageStr=' + pageStr + '&bindPropListStr=' + bindPropListStr; // 版本2-文件流版本 await myAxios({ url: proxy.$url.UserDownLoadFileExcelUrl2, responseType: 'blob', data: { searchStr, pageStr, bindPropListStr }, }); } }; // 页面实例挂载完成后 onMounted(async () => { // 1. 初始化表格显示列和排列顺序 await initTableColumn(); // 2. 初始化表格数据 await initTableData(); }); </script>
配置代码
const contentTableConfig = { tableTitle: '用户管理', //表格显示列和排列顺序(默认的) , 该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList originPropList: [ { prop: 'userAccount', label: '账号', align: 'center', sortable: 'custom', width: '150', slotName: 'userAccount', excelWidth: 20 }, { prop: 'userRealName', label: '姓名', align: 'center', width: '150', slotName: 'userRealName', excelWidth: 20 }, { prop: 'userSex', label: '性别', align: 'center', width: '100', slotName: 'userSex', excelWidth: 20 }, { prop: 'userPhone', label: '联系方式', align: 'center', width: '200', slotName: 'userPhone', excelWidth: 50 }, { prop: 'userRemark', label: '用户描述', align: 'center', slotName: 'userRemark', 'show-overflow-tooltip': true, excelWidth: 20 }, { prop: 'addTime', label: '创建时间', align: 'center', sortable: 'custom', slotName: 'addTime', excelWidth: 40 }, { prop: 'handler', label: '操作', align: 'center', width: '100', slotName: 'handler' }, // { prop: 'handler2', label: '操作2', align: 'center', width: '100', slotName: 'handler' }, ], // 开启多选列 【启用多选下面的'highlight-current-row',必须设为false,或者不写这个属性】 showSelectColumn: true, // 是否显示底部分页 showPagination: true, // 是否开启表格高度自适应 isAutoHeight: true, // 表格高度自适应模式,当isAutoHeight=true才生效 (取值有:'height'、'max-height'), 默认值为:'max-height' heightProp: 'height', // 表格属性 tableProp: { 'highlight-current-row': false, //设置单选,启用单选必须将上述的showSelectColumn改为false }, }; export { contentTableConfig };
三. 分页组件3种传值方式 【子传父】
1. v-model绑定 + watch监听 【首选方案】
<el-pagination
v-if="showPagination"
style="display: flex; justify-content: center"
layout="total, sizes, prev, pager, next, jumper"
class="mt15"
background
:page-sizes="[5, 10, 20, 30]"
:total="tableTotal"
v-model:current-page="myPage.pageNum"
v-model:page-size="myPage.pageSize"
>
</el-pagination>
<script setup>
// 1. 接收父组件传递过来的植
const props = defineProps({
// 分页属性
page: {
type: Object,
default: () => ({ pageNum: 1, pageSize: 10 }),
},
});
// 2. 对外传递事件
const emits = defineEmits(['update:page']);
//3. 声明一个新对象用于监听props.page的改变,因为要符合单项数据流,不能直接修改props.page
const myPage = reactive(props.page);
/**
* 4. 监听分页的改变(包括每页条数 和 页码)
* @param {Number} newPage 变化后的新的分页对象
*/
watch(myPage, newPage => {
// 例如:上面通过v-model:current-page进行双向绑定,当点击当前页数变化的时候,myPage中的pageNum会自动改变,pageNum同样道理
emits('update:page', { ...newPage });
});
</script>
2. v-model双向绑定 + 监听size-change和current-change事件 【不推荐】
template代码
<el-pagination
v-if="showPagination"
style="display: flex; justify-content: center"
layout="total, sizes, prev, pager, next, jumper"
class="mt15"
background
:page-sizes="[5, 10, 20, 30]"
:total="tableTotal"
v-model:current-page="myPage.pageNum"
v-model:page-size="myPage.pageSize"
@size-change="onPageChange"
@current-change="onPageChange"
>
</el-pagination>
script代码
<script setup>
// 1. 接收父组件传递过来的植
const props = defineProps({
// 分页属性
page: {
type: Object,
default: () => ({ pageNum: 1, pageSize: 10 }),
},
});
// 2. 对外传递事件
const emits = defineEmits(['update:page']);
//3. 声明一个新对象用于监听props.page的改变,因为要符合单项数据流,不能直接修改props.page
const myPage = reactive(props.page);
/**
* 4. 监听分页的改变(包括每页条数 和 页码)
* @param {Number} newPageSize 新的分页条数
*/
const onPageChange = () => {
// 例如:上面通过v-model:current-page进行双向绑定,当点击当前页数变化的时候,page中的pageNum会自动改变, 实际上已经不需要这里返回的参数了
emits('update:page', { ...myPage });
};
</script>
3. current-page和page-size绑定 + 监听size-change和current-change事件 【不推荐】
通过 :current-page="myPage.pageNum",:page-size="myPage.pageSize"绑定,然后@size-change和@current-change对应不同的监听函数,通过监听函数传递的参数拿到最新的页码 或 每页的条数,然后对外暴露 【麻烦,不推荐】
总结: 肯定推荐方案1,另外方案1和方案2,均不影响父页面的写法
四. 父页面_调用封装中的分页组件 【父调子】
使用 :page="tableData.param"传值,然后使用 @update:page="UpdatePage"监听,然后在UpdatePage方法中给tableData.param赋最新值,如下:【详见user/index.vue】
template中代码:
<ypfTable
ref="ypfTableRef"
v-bind="contentTableConfig"
:tableRows="tableData.tableRows"
:propList="bindPropList"
:tableTotal="tableData.total"
:page="tableData.param"
@update:page="UpdatePage"
@update:tableSelChange="tableSelChange"
@update:sortChange="sortChange"
>
</ypfTable>
script中代码:
<script setup>
const tableData = reactive({
param: {
pageNum: 1,
pageSize: 10,
sortName: '', //排序字段名称
sortDirection: '', //排序方式
},
});
/**
* 监听子页面传递的表格分页参数
* @param {Object} pageInfo {pageNum: xx, pageSize: xx}
*/
const UpdatePage = pageInfo => {
if (pageInfo) {
//tableData.param 中的sortDirection和sortName被置为 "", 不影响业务
tableData.param = { ...pageInfo };
initTableData();
}
};
</script>
2. v-mode双向绑定page + watch监听属性pageNum和pageSize 【更推荐--因为写法更简洁】
直接使用语法糖 v-model:page="tableData.param",传值的同时,接收到子页面传过来值的时候自动赋值给table.param,然后使用watch监听 tableData.param即可。
特别注意:这里tableData.param是一个reactive对象的属性,所以要监听的时候要这样写: ()=>tableData.param,但是param中还有排序相关的两个属性(sortName和sortDirection),直接监听param的话会和点击列排序实际同时调用,导致加载了两次数据,所以最终监听直接监听这两个属性,如下:【详见user/v2_index.vue】
template中代码:
<ypfTable
ref="ypfTableRef"
v-bind="contentTableConfig"
:tableRows="tableData.tableRows"
:propList="bindPropList"
:tableTotal="tableData.total"
v-model:page="tableData.param"
@update:tableSelChange="tableSelChange"
@update:sortChange="sortChange"
>
</ypfTable>
script中代码:
<script setup>
const tableData = reactive({
param: {
pageNum: 1,
pageSize: 10,
sortName: '', //排序字段名称
sortDirection: '', //排序方式
},
});
/**
* 监听分页参数的变化
* param中还有排序相关的两个属性,直接监听param的话回和点击列排序实际同时调用,导致加载了两次数据,所以最终监听直接监听这两个属性
*/
watch([() => tableData.param.pageNum, () => tableData.param.pageSize], () => {
console.log(tableData.param);
initTableData();
});
</script>
五. 表格高度自适应方案
1. 原理
(1). 核心点就是动态给el-table组件赋值,修改height属性 或 max-height属性(二者效果是有区别的)
height:表示表格永远占满屏幕
maxHeight:当数据量小的时候,表格还是实际高度,只有超过屏幕,才会占满屏幕,出现滚动条
(2). 在ypf-table组件中:
A. 使用isAutoHeight接收传递过来的值,从而决定是否开启动态计算高度,默认是开启的;即绑定一个空字符串属性,是无效的,若开启,则绑定heightProp ,即 max-height 或者 height 属性。
B. 使用 heightProp 接收传递过来的值,从而决定表格的高度模式采用: max-height 还是 height 属性, 注:默认是max-height,即可以不传递这个属性
// 1. 接收父组件传递过来的植
const props = defineProps({
// 是否启用动态设置高度的功能
isAutoHeight: {
type: Boolean,
default: true,
},
//绑定的表格动态高度属性,当isAutoHeight=true才生效 (取值有:'height'、'max-height')
heightProp: {
type: String,
default: 'max-height',
},
});
// 动态决定是否绑定决定表格高度的属性
const myHeightProp = props.isAutoHeight === true ? props.heightProp : '';
(3). 如何计算表格的高度? (表格高度不包括分页条,不含上面的按钮区域)
a. 这里采用减法,calc(100vh - ${myDynamicHeight}),100vh代表屏幕高度,myDynamicHeight 代表其它非表格位置占的的高度
b. 封装calTableHeight方法,计算非表格位置的高度,这里需要考虑三种情况:默认有tagsView、关闭tagsView、全屏
c. 核心点是使用offsetTop,计算表格顶部距离tab(tagsView)底部的距离,这样就无需分情况计算搜索框的高度了,简化了很多步骤
d. 调用位置:onMounted的时候默认初始化、监听窗口变化、监听是否开启or关闭tagsView
e. 关于calTableHeight方法的几点说明:
① tableoffsetTopHeight:代表表格顶部距离tab(tagsView)底部的距离
② totalHeight= 整个系统的屏幕高度 - 表格高度 - tableoffsetTopHeight (这是一个反向说明)
③ 全屏的时候 : totalHeight=76px
④ 关闭tagView,单页面形式 : totalHeight=76px+50px=126px
⑤ 默认tagView,不全屏:totalHeight=76px+50px+34px=160px
(4). ypf-search组件中需要监听一下handleChange是否折叠 or 展开的变化,对外暴露,在最外层调用页面中,获取变化的时候,重新计算表格高度。
template代码:
<el-table
size="default"
ref="tableRef"
:[myHeightProp]="`calc(100vh - ${myDynamicHeight})`"
:data="tableRows"
v-bind="tableProp"
@selection-change="tableSelChange"
@current-change="tableSimpleSelChange"
@sort-change="sortChange"
:span-method="mergeFn"
>
xxxx
</el-table>
script代码
<script setup>
// 1. 接收父组件传递过来的植
const props = defineProps({
// 是否启用动态设置高度的功能
isAutoHeight: {
type: Boolean,
default: true,
},
//绑定的表格动态高度属性,当isAutoHeight=true才生效 (取值有:'height'、'max-height')
heightProp: {
type: String,
default: 'max-height',
},
});
// 动态决定是否绑定决定表格高度的属性
const myHeightProp = props.isAutoHeight === true ? props.heightProp : '';
const tableRef = ref(null);
let myDynamicHeight = ref('');
/**
* 计算表格需要减去的高度
*/
const calTableHeight = async () => {
await nextTick();
let totalHeight = 160; //默认有tagsView,不全屏
let tableoffsetTopHeight = tableRef.value.$el.offsetTop; //表格顶部距离tab(tagsView)底部的距离
let { isTagsview } = store.state.themeConfig.themeConfig;
if (!isTagsview) {
totalHeight = 126; // 关闭tagsView,单页面模式
}
if (store.state.tagsViewRoutes.isTagsViewCurrenFull) {
totalHeight = 76; //全屏
}
totalHeight += tableoffsetTopHeight;
myDynamicHeight.value = totalHeight + 'px';
return myDynamicHeight.value;
};
// 页面加载需要初始化一次
onMounted(() => {
if (props.isAutoHeight) {
//此处需要加个延迟,否则第一次加载的时候会计算错误,导致最外层有滚动条
setTimeout(() => calTableHeight(), 500);
// 窗口大小变化需要初始化
window.onresize = () => calTableHeight();
}
});
/**
* 监听设置开启or关闭tagsView事件
* 监听是否全屏
*/
watch([() => store.state.themeConfig.themeConfig.isTagsview, () => store.state.tagsViewRoutes.isTagsViewCurrenFull], () => {
if (props.isAutoHeight) {
calTableHeight();
}
});
/*
对外暴露事件
*/
defineExpose({
calTableHeight,
});
</script>
2. 如何使用
(1). 表格的配置文件tableConfig.js 中需要添加
A. isAutoHeight:表示开启表格高度自适应
B. heightProp:表示表格高度自适应的模式
注:这两个属性在表格封装中均有默认值,所以可以根据实际情况省略。
const contentTableConfig = {
// 是否开启表格高度自适应
isAutoHeight: true,
// 表格高度自适应模式,当isAutoHeight=true才生效 (取值有:'height'、'max-height'), 默认值为:'max-height'
heightProp: 'height',
};
(2). 在使用ypf-table组件的时候,通过v-bind绑定属性即可。
(3). 如果主页面中使用了ypf-search组件,需要监听 @update:handleChange="myHandleChange",在myHandleChange方法中,调用ypf-table中的计算高度的方法
/**
* 监听搜索栏折叠or展开时触发
*/
const myHandleChange = () => {
// 必须加个延迟,否则获取的高度是折叠前的高度
setTimeout(() => ypfTableRef.value?.calTableHeight(), 500);
};
六. 其它功能剖析
1. 表格列排序
需将给table-column列上的 sortable
设置为 custom
,同时在 Table 上监听 sort-change
事件, 在事件回调中可以获取当前排序的字段名和排序顺序,从而向接口请求排序后的表格数据。
(1). 数据准备
账号和创建时间两列,配置 sortable: 'custom',支持列排序。
const contentTableConfig = {
//表格显示列和排列顺序(默认的) , 该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList
originPropList: [
{ prop: 'userAccount', label: '账号', align: 'center', sortable: 'custom', width: '150', slotName: 'userAccount', excelWidth: 20 },
{ prop: 'userRealName', label: '姓名', align: 'center', width: '150', slotName: 'userRealName', excelWidth: 20 },
{ prop: 'userSex', label: '性别', align: 'center', width: '100', slotName: 'userSex', excelWidth: 20 },
{ prop: 'userPhone', label: '联系方式', align: 'center', width: '200', slotName: 'userPhone', excelWidth: 50 },
{ prop: 'userRemark', label: '用户描述', align: 'center', slotName: 'userRemark', 'show-overflow-tooltip': true, excelWidth: 20 },
{ prop: 'addTime', label: '创建时间', align: 'center', sortable: 'custom', slotName: 'addTime', excelWidth: 40 },
{ prop: 'handler', label: '操作', align: 'center', width: '100', slotName: 'handler' },
],
};
(2). 组件封装
监听sort-change事件,然后对外传递事件和参数。
<script setup>
// 1. 对外传递事件
const emits = defineEmits([ 'update:sortChange']);
/**
* 2. 监听表格排序
* @param {String} prop 表格排序列名称
* @param {String} order 表格排序方式 descending 或 ascending
*/
const sortChange = async ({ prop, order }) => {
let sortName = prop;
let sortDirection = order == 'ascending' ? 'asc' : 'desc';
emits('update:sortChange', { sortName, sortDirection });
};
</script>
(3). 父页面调用
通过@update:sortChange获取子页面传递,然后接收参数,调用表格加载方法 initTableData();
/**
* 监听子页面传递过来的表格排序参数
* @param {Object} pageInfo {sortName: xx, sortDirection: xx}
* 解释:sortName 表格排序列名称, sortDirection 表格排序方式
*/
const sortChange = async ({ sortName, sortDirection }) => {
if (sortName && sortDirection) {
tableData.param.sortName = sortName;
tableData.param.sortDirection = sortDirection;
await initTableData();
}
};
2. 表格行多选
原理很简单,在table组件内部, 手动添加一个 el-table-column
,设 type
属性为 selection
即可。 这里通过 v-if="showSelectColumn",控制是否显示多选这里一列。然后监听 selection-change 事件,获取选中的行,最后通过emit向父组件传递。
注:必须关掉单选,即 'highlight-current-row': false
(1). 数据准备
const contentTableConfig = {
// 开启多选列 【启用多选下面的'highlight-current-row',必须设为false,或者不写这个属性】
showSelectColumn: true,
// 表格属性
tableProp: {
'highlight-current-row': false, //设置单选,启用单选必须将上述的showSelectColumn改为false
},
};
(2). 组件封装
// 1. 接收父组件传递过来的值
const props = defineProps({
// 表格是否多选
showSelectColumn: {
type: Boolean,
default: false,
},
});
// 2. 对外传递事件
const emits = defineEmits(['update:tableSelChange']);
/**
* 3. 监听表格选中行变化(服务于多选)
* @param {Array} selections 变化后的数组(数组元素为每行的数据)
*/
const tableSelChange = selections => {
emits('update:tableSelChange', selections);
};
(3). 父页面调用
/**
* 监听表格选中行变化
* @param {Array} selections 变化后的数组(数组元素为每行的数据)
*/
const tableSelChange = selections => {
let selectRowIds = selections.map(item => item.id); //返回一个id集合数组
tableData.selectRowIds = selectRowIds;
};
3. 表格行单选
Table 组件提供了单选的支持, 只需要配置 highlight-current-row
属性(高亮显示)即可实现单选。 之后由 current-change
事件来管理选中时触发的事件,它会传入 currentRow
,oldCurrentRow
。
注:这里的单选和上面的多选不能同时配置,考虑如何将多选的模式改为单选,因为现在的单选,只能高亮选中行,没有checkbox。
(1). 数据准备
const contentTableConfig = {
// 开启多选列 【启用多选下面的'highlight-current-row',必须设为false,或者不写这个属性】
showSelectColumn: false,
// 是否开启表格高度自适应
isAutoHeight: true,
// 表格属性
tableProp: {
'highlight-current-row': true, //设置单选,启用单选必须将上述的showSelectColumn改为false
},
};
(2). 组件封装
// 1. 接收父组件传递过来的植
const props = defineProps({
// 表格的属性
tableProp: {
type: Object,
default: () => ({}),
},
});
// 2. 对外传递事件
const emits = defineEmits(['update:tableSimpleSelChange']);
/**
* 3. 监听表格选中行变化(服务于单选)
* @param {Object} currentRow 选中行内容
*/
const tableSimpleSelChange = (currentRow, oldCurrentRow) => {
// console.log(currentRow, oldCurrentRow);
emits('update:tableSimpleSelChange', currentRow);
};
(3). 父页面调用
<ypfTable
ref="ypfTableRef"
v-bind="contentTableConfig"
:tableRows="tableData.tableRows"
:propList="bindPropList"
:tableTotal="tableData.total"
:page="tableData.param"
@update:page="UpdatePage"
@update:tableSimpleSelChange="tableSimpleSelChange"
@update:sortChange="sortChange"
>
/**
* 监听表格选中行变化
* @param {Object} selRow 选中行
*/
const tableSimpleSelChange = selRow => {
//相关业务
};
4. 支持列合并、行合并的方法
调用的时候,传递进来自定义方法即可。
/**
* 合并单元格
* @param {int} rowIndex 当前行号
* @param {int} columnIndex 当前列号
*/
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
// 当前列号等于0,表示只有第一列合并
if (columnIndex === 0) {
if (rowIndex % 3 === 0) {
return { rowspan: 3, colspan: 1 };
} else {
return { rowspan: 0, colspan: 0 };
}
}
};
七. 持续更新...
(1) 去掉了表格封装中搜索栏默认高度,从而实现不传入btn内容的时候,不占位置
(2) 修改表格封装,支出表头合并的情况,注:仅仅支持1级合并[带来了一个新问题:表格默认就有竖直方向的边框了,可以接受,默认就是这样的]
(3) 封装了支持列合并、行合并的方法,至于如何合并,自己编写方法,传递进去即可
(4) 新增了tableProp,用来传递table属性
(5) 删掉了表格上默认样式: style="width: 100%"
(6) 表格上方左右两侧的按钮myBtnArea区域中的div增加flex布局
(7) 新增监听单选表格单选事件,尚不完善,考虑通过个属性来控制是否监听,而不是全部监听 【尚不完善】
(8) 关于多选的监听,目前是在封装中处理,把id返回来了,这个在一些场景中并不是很实用,改为:对外传递整行数据,在调用页面处理业务
(9) css样式的padding和margin都改了
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。