第六节:Table组件封装和基于Table组件的PageContent组件封装
一. 整体说明
1. 整体规划
首先利用el-table组件 和 el-pagination组件 封装自己ypf-table组件,实现通过传入配置,预留顶部区域插槽,通过传入配置实现表格的显示。
然后封装 page-content组件,该组件基于ypf-table组件。
各组件的调用顺序:
ypf-table:表格组件 → page-content:页面内容区域组件 → user.vue 用户管理页面 → content.config.ts 配置文件
2. 组件介绍
(1). ypf-table 组件
将el-table组件和el-pagination组件整合到一起。
(2).page-content组件
单独抽离出来page-content,然后不同的页面(user.vue、role.vue) 都调用pageContent,然后传递一个页面的唯一标记pageName,在pageContent中根据这个标记调用store,从而实现请求不同的接口。
二. Table组件的封装
1. 封装思路
(1). 该组件组件由三部分组成,顶部区域提供两个插槽,可以用来扩展表格标题和一些按钮;中间是el-table表格内容区域;底部是el-pagination分页插件区域,同时用一个footer插槽包裹,支持自定义扩展。
(2). 该组件接收到参数有:
A. listData:表格数据源
B. listCount:表格总条数
C. title:表格标题
D. propList: 表格属性配置
E. showSelectColumn:控制表格是否多选
F. showIndexColumn:控制表格是否显示索引列
G. showFooter: 控制是否显示
H. page:表格的当前页和每页条数
G. childrenProps:处理表格展开属性 (详见菜单模块)
(3). 中间区域的表格组件:监听选中列变化,对外通过emit暴露;其中每一列都通过具名插槽支持对外扩展,插槽名称为属性中的slotName,同时还提供一个row1属性,用来对外传递数据。
(4). 底部区域的分页组件:绑定当前页current-change 和 每页条数page-size,同时监听二者的变化(@current-page、@page-size),并通过emit对外暴露。
组件封装代码:
<template> <div class="hy-table"> <!-- 1.表格顶部区域 --> <div class="myHeader"> <slot name="header"> <div class="title">{{ title }}</div> <div class="handler"> <slot name="headerHandler"></slot> </div> </slot> </div> <!-- 2.表格内容区域 --> <el-table border style="width: 100%" v-bind="childrenProps" :data="listData" @selection-change="handleSelectionChange" > <el-table-column v-if="showSelectColumn" type="selection" align="center" width="60"></el-table-column> <el-table-column v-if="showIndexColumn" type="index" label="序号" align="center" width="80"></el-table-column> <template v-for="propItem in propList" :key="propItem.prop"> <el-table-column v-bind="propItem" align="center"> <!-- 首先使用template,说明内容值要自定义 --> <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 as any).slotName" :row1="scope.row"> <!-- 下面是默认值,只有当没有提供插入内容的时候才会显示 --> {{ scope.row[(propItem as any).prop] }} </slot> </template> </el-table-column> </template> </el-table> <!-- 3.表格尾部区域 --> <div class="myFooter" v-if="showFooter"> <slot name="footer"> <el-pagination layout="total, sizes, prev, pager, next, jumper" :page-sizes="[5, 10, 20, 30]" :total="listCount" :current-page="page.currentPage" :page-size="page.pageSize" @current-change="handleCurrentChange" @size-change="handleSizeChange" > </el-pagination> </slot> </div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: { // 表格数据源 listData: { type: Array, required: true, }, // 表格总条数 listCount: { type: Number, default: 0, }, // 表格标题 title: { type: String, default: '', }, // 表格属性源 propList: { type: Array, required: true, }, // 表格多选 showSelectColumn: { type: Boolean, default: false, }, // 表格索引 showIndexColumn: { type: Boolean, default: false, }, // 是否显示底部分页 showFooter: { type: Boolean, default: true, }, // 表格当前页 和 每页条数 page: { type: Object, default: () => ({ currentPage: 1, pageSize: 5 }), }, //处理表格是否可以展开树的属性 childrenProps: { type: Object, default: () => ({}), }, }, emits: ['selectionChange', 'update:page'], setup(props, { emit }) { // 1. 监听表格选择事件 const handleSelectionChange = (val: any) => { emit('selectionChange', val); }; //2. 分页组件-监听每页数目变化 const handleSizeChange = (pageSize: number) => { console.log(pageSize); console.log({ pageSize }); console.log({ ...props.page }); emit('update:page', { ...props.page, pageSize }); }; //3. 分页组件-监听当前页码变化 const handleCurrentChange = (currentPage: number) => { // console.log({ ...props.page, currentPage }); emit('update:page', { ...props.page, currentPage }); }; return { handleSelectionChange, handleSizeChange, handleCurrentChange, }; }, }); </script> <style scoped lang="less"> .myHeader { display: flex; height: 45px; padding: 0 5px; justify-content: space-between; align-items: center; .title { font-size: 20px; font-weight: 700; } .handler { align-items: center; } } .myFooter { margin-top: 15px; .el-pagination { text-align: right; } } </style>
配置文件代码:
export const contentTableConfig = { title: '用户列表', propList: [ { prop: 'name', label: '用户名', minWidth: '100', slotName: 'name' }, { prop: 'realname', label: '真实姓名', minWidth: '100' }, { prop: 'cellphone', label: '手机号码', minWidth: '100' }, { prop: 'enable', label: '状态', minWidth: '100', slotName: 'status' }, { prop: 'createAt', label: '创建时间', minWidth: '250', slotName: 'createAt', //这里的slotName是自己起名的,用来动态定义封装的插槽名称 }, { prop: 'updateAt', label: '更新时间', minWidth: '250', slotName: 'updateAt' }, { label: '操作', minWidth: '120', slotName: 'handler' }, ], // 开启多选列 showSelectColumn: true, // 开启索引列 showIndexColumn: true, // 是否显示底部分页 showFooter: true, };
含展开菜单的配置:
export const contentTableConfig = { title: '菜单列表', propList: [ { prop: 'name', label: '菜单名称', minWidth: '100' }, { prop: 'type', label: '类型', minWidth: '60' }, { prop: 'url', label: '菜单url', minWidth: '100' }, { prop: 'icon', label: '菜单icon', minWidth: '100' }, { prop: 'permission', label: '按钮权限', minWidth: '100' }, { prop: 'createAt', label: '创建时间', minWidth: '220', slotName: 'createAt', }, { prop: 'updateAt', label: '更新时间', minWidth: '220', slotName: 'updateAt', }, { label: '操作', minWidth: '120', slotName: 'handler' }, ], showIndexColumn: false, showSelectColumn: false, showFooter: false, childrenProps: { rowKey: 'id', treeProp: { children: 'children', }, }, };
数据源:
{ "code": 0, "data": { "list": [ { "id": 12863, "name": "3", "realname": "3", "cellphone": 3, "enable": 1, "departmentId": 2, "roleId": 3, "createAt": "2022-01-05T00:31:21.000Z", "updateAt": "2022-01-05T00:32:13.000Z" }, { "id": 12862, "name": "1", "realname": "1", "cellphone": 1, "enable": 1, "departmentId": 5, "roleId": 4, "createAt": "2022-01-05T00:30:16.000Z", "updateAt": "2022-01-05T00:30:16.000Z" }, { "id": 12861, "name": "james", "realname": "詹姆斯", "cellphone": 13322223338, "enable": 1, "departmentId": 1, "roleId": 1, "createAt": "2022-01-04T16:06:02.000Z", "updateAt": "2022-01-04T16:06:02.000Z" }, { "id": 12860, "name": "12345", "realname": "12345", "cellphone": 123456789, "enable": 1, "departmentId": 5, "roleId": 4, "createAt": "2022-01-04T14:51:16.000Z", "updateAt": "2022-01-04T14:51:16.000Z" }, { "id": 9, "name": "lyh", "realname": "李银河", "cellphone": 17754456666, "enable": 1, "departmentId": 2, "roleId": 3, "createAt": "2021-05-02T07:24:12.000Z", "updateAt": "2021-08-20T04:07:23.000Z" }, { "id": 8, "name": "wxb", "realname": "王小波", "cellphone": 18855556666, "enable": 1, "departmentId": 2, "roleId": 3, "createAt": "2021-05-02T07:24:12.000Z", "updateAt": "2021-05-02T07:26:20.000Z" }, { "id": 7, "name": "kobe", "realname": "kobe", "cellphone": 16655556666, "enable": 1, "departmentId": 2, "roleId": 3, "createAt": "2021-05-02T07:24:12.000Z", "updateAt": "2021-05-02T07:26:20.000Z" }, { "id": 6, "name": "lily", "realname": "lily", "cellphone": 13355556666, "enable": 1, "departmentId": 2, "roleId": 3, "createAt": "2021-05-02T07:24:12.000Z", "updateAt": "2021-05-02T07:26:20.000Z" }, { "id": 5, "name": "coderdemo", "realname": "demo", "cellphone": 17766665555, "enable": 1, "departmentId": 5, "roleId": 4, "createAt": "2021-08-23T07:24:12.000Z", "updateAt": "2021-08-23T07:24:21.000Z" }, { "id": 4, "name": "codertest", "realname": "test", "cellphone": 16655552222, "enable": 1, "departmentId": 3, "roleId": 3, "createAt": "2021-08-23T07:25:02.000Z", "updateAt": "2021-08-23T07:25:09.000Z" } ], "totalCount": 13 } }
2. 重难点剖析
(1). 表格列具名插槽的灵活使用?
(首先要仔细看一下关于插槽的使用:https://www.cnblogs.com/yaopengfei/p/15338752.html)
A. 先使用el-table组件提供的默认插槽#default,获取scope
B. 再根据配置属性中的slotName设置具名插槽,同时通过row1属性,对外传递scope.row值
C. 该插槽中设置默认值,就是配置文件中的prop属性对应的值。
D. 通过上述就可以实现slotName该属性存在且有值得时候才有具名插槽。
(2). 如何监听当前页数和每页的条数,并传递给父组件?
在监听方法中,通过emit对外暴露,这里需要注意的是,默认传递过来的prop.page这是没有更新的,所以需要在这回掉函数中,拿到最新的pageSize或者currentPage,然后合并一下再对外暴露。
三. PageContent组件封装
1. 封装思路
(1). 该组件主要是调用上述封装的ypf-table组件,由三部分组成:
A. 使用#headerHandler插槽,传入新增按钮,新增按钮需要弹框,即需要调用page-modal组件,则通过emit继续对外传递。
B. 固定列的展示,对于状态、开始时间、更新时间、操作(编辑、删除)四列,是通用的内容,所以在该组件中,直接实例化。
C. 动态插入其它插槽,便于其父组件(user.vue)可以使用。
(2). 通用固定列的说明:
A. 状态:转换为启用 或 禁用
B. 两个时间:格式转换一下
C. 操作: 编辑 和 删除两个按钮,其中删除就是根据id删除,在该组件中直接调用相关方法删除即可;编辑可能需要弹框,则继续通过emit对外传递。
(3). 该组件接收到参数有:
A. contentTableConfig: 实际上就是ypf-table组件需要的表格配置数据哦。
B. pageName:页面标记,不同模块传递不同的标记(如:users、role),通过该标记组装地址,从而请求不同的接口,实现该组件的通用。
(4). 按钮权限
前面已经介绍,详见https://www.cnblogs.com/yaopengfei/p/15640965.html
组件代码:
<template> <div class="main"> <Ypf-Table :listData="dataList" :listCount="dataCount" v-bind="contentTableConfig" v-model:page="pageInfo" @selectionChange="mySelectionChange" > <!-- 1.header中的插槽 --> <template #headerHandler> <el-button v-if="isCreate" type="primary" size="small" @click="addHandle">新建用户</el-button> </template> <!-- 2.列中的插槽 --> <!-- v-slot:status="myScope" 简写为 #status="myScope"--> <template #status="myScope"> <el-button size="small" plain :type="myScope.row1.enable ? 'success' : 'danger'"> {{ myScope.row1.enable ? '启用' : '禁用' }} </el-button> </template> <template #createAt="myScope"> <strong>{{ $filters.formatTime(myScope.row1.createAt) }}</strong> </template> <template #updateAt="myScope"> <strong>{{ $filters.formatTime(myScope.row1.updateAt) }}</strong> </template> <template #handler="myScope"> <div class="handle-btns"> <el-button v-if="isUpdate" :icon="Edit" size="mini" type="text" @click="editHandle(myScope.row1)"> 编辑 </el-button> <el-button v-if="isDelete" :icon="Delete" size="mini" type="text" @click="deleteHandle(myScope.row1)"> 删除 </el-button> </div> </template> <!-- 3.动态添加其它插槽 --> <template v-for="item in otherPropSlots" :key="item.prop" #[item.slotName]="myScope"> <template v-if="item.slotName"> <slot :name="item.slotName" :row2="myScope.row1"></slot> </template> </template> </Ypf-Table> </div> </template> <script lang="ts"> import { defineComponent, computed, ref, watch, getCurrentInstance } from 'vue'; import { useStore } from '@/store'; import YpfTable from '@/base-ui/table'; import YpfForm from '@/base-ui/form'; import { Edit, Delete } from '@element-plus/icons'; import { usePermission } from '@/hooks/use-permission'; import { ElMessage, ElMessageBox } from 'element-plus'; export default defineComponent({ components: { YpfTable, YpfForm, Edit, Delete, }, emits: ['newBtnClick', 'editBtnClick'], props: { // 表格数据 contentTableConfig: { type: Object, require: true, }, // 页面标记,作用是store中用来拼接请求不同的地址 pageName: { type: String, required: true, }, }, setup(props, { emit }) { const { proxy } = getCurrentInstance(); // 发送请求,保存数据到vuex const store = useStore(); // 0.获取操作权限 const isCreate = usePermission(props.pageName as string, 'create'); const isUpdate = usePermission(props.pageName as string, 'update'); const isDelete = usePermission(props.pageName as string, 'delete'); const isQuery = usePermission(props.pageName as string, 'query'); // 1. 双向绑定pageInfo const pageInfo = ref({ currentPage: 1, pageSize: 5 }); watch(pageInfo, () => getPageData()); // 2. 封装发送网络请求的方法 // @queryInfo:搜索框中数据 const getPageData = (queryInfo: any = {}) => { if (!isQuery) return; store.dispatch('system/getPageListAction', { pageName: props.pageName, queryInfo: { offset: (pageInfo.value.currentPage - 1) * pageInfo.value.pageSize, size: pageInfo.value.pageSize, ...queryInfo, }, }); }; //进行发送 getPageData(); // 3. 从vuex中获取数据 // 3.1 表格内容 const dataList = computed(() => { return store.getters[`system/pageListData`](props.pageName); // (props.pageName) 进行参数传递 }); // 3.2 表格条数 const dataCount = computed(() => store.getters[`system/pageListCount`](props.pageName)); //4.监听表格选择事件 const mySelectionChange = (val: any) => { // 得到是一个数组对象,for循环,遍历获取里面的属性即可 console.log(val); }; // 5.获取没有绑定的其它插槽 const otherPropSlots = props.contentTableConfig?.propList.filter((item: any) => { if (item.slotName === 'status') return false; if (item.slotName === 'createAt') return false; if (item.slotName === 'updateAt') return false; if (item.slotName === 'handler') return false; return true; }); //6. 测试另一种写法 // 上面的 v-model:page='pageInfo' 等价于 :page="pageInfo" @update:page="handelTest" const handelTest = (val: any) => { console.log(1, val); pageInfo.value = val; }; // 7. 删除方法 const deleteHandle = (item: any) => { ElMessageBox.confirm('您确定要删除吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'info', }).then(() => { store.dispatch('system/deletePageDataAction', { pageName: props.pageName, id: item.id, }); }); }; // 8. 新增方法 const addHandle = () => { emit('newBtnClick'); }; // 9. 修改方法 const editHandle = (item: any) => { emit('editBtnClick', item); }; return { dataList, Edit, Delete, mySelectionChange, getPageData, pageInfo, dataCount, otherPropSlots, isCreate, isUpdate, isDelete, handelTest, deleteHandle, addHandle, editHandle, }; }, }); </script> <style scoped> .searchForm { padding: 5px; margin-bottom: 4px; height: 80px; } .main { border-top: 10px solid #f5f5f5; } </style>
配置代码:
export const contentTableConfig = { title: '用户列表', propList: [ { prop: 'name', label: '用户名', minWidth: '100', slotName: 'name' }, { prop: 'realname', label: '真实姓名', minWidth: '100' }, { prop: 'cellphone', label: '手机号码', minWidth: '100' }, { prop: 'enable', label: '状态', minWidth: '100', slotName: 'status' }, { prop: 'createAt', label: '创建时间', minWidth: '250', slotName: 'createAt', //这里的slotName是自己起名的,用来动态定义封装的插槽名称 }, { prop: 'updateAt', label: '更新时间', minWidth: '250', slotName: 'updateAt' }, { label: '操作', minWidth: '120', slotName: 'handler' }, ], // 开启多选列 showSelectColumn: true, // 开启索引列 showIndexColumn: true, // 是否显示底部分页 showFooter: true, };
2. 重难点剖析
(1). 如何实现组件的通用,即user和role可以请求不同的数据源?
A. page-content组件接收到pageName标记,通过dispatch调用getPageListAction方法,将pageName传递进去,然后进行拼接,请求不同的方法,比如:users/list 或者 rolesList
B. 然后调用的不同的mutations中声明的方法,changeUsersList (或 changeRolersList),将数据存储到state中。
C. 然后组装getters,用于pageContent中通过dispatch调用获取不同的state数据,如: usersList 或 rolesList。
、
(2). 父组件page-content中如何获取子组件传递的pageSize和currentPage呢?
(再次复习 v-model:page 相当于省略了什么?等价于什么?)
上述组件中的封装代码 v-model:page="pageInfo",对于父组件page-content而言,相当于干了两件事:① 给page赋值 ② 监听子组件中的变化,并自动赋值给pageInfo对象。
等价于:
:page="pageInfo",@update:page="handleTest",handleTest代码如下:
(3). 如何实现pageize或currentPage更新后自动加载数据呢?
当子组件的种的pageSize和currentPage发生变化时候,父组件无论采用哪种绑定方式,pageInfo都会发生变化,所以这里只需要对pageInfo进行watch监听,当发生变化的时候,调用加载数据的方法getPageData。
(4). page-content组件中为什么要动态插入其它插槽呢?
首先要知道调用顺序 ypf-table → page-content → user.vue,其中page-content是一个中间中转的组件,除了user调用,role也会调用,所以在pageContent里针对公共的字段比如:状态、更新时间、创建时间、操作 4列,进行了统一的插槽处理,但这就引入了其它一个问题,那么对于user.vue页面而言,除了以上四列,比如 name一列我想处理一下怎么办呢,比如给name只显示5个字符,虽然table中每一列都都支持插槽写法,但是user直接调用的是pageContent,pageContent是不支持插槽列,所以这里就需要根据传入的配置文件给pageContent动态插入剩余插槽!! (也就是所谓的跨组件调用)
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。