第六节: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>
View Code

配置文件代码:

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,
};
View Code

含展开菜单的配置:

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',
        },
    },
};
View Code

数据源:

{
    "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
    }
}
View Code

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>
View Code

配置代码:

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,
};
View Code

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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2021-12-14 20:04  Yaopengfei  阅读(717)  评论(1编辑  收藏  举报