vue 动态表格组件

作者: 牛会骑自行车 | 来源:发表于2022-03-07 23:52 被阅读0次
    效果图 👇

    看着很朴素是不是哈哈哈哈哈写得还挺费劲呢~

    需求:
    1.表格分页始终居于页面底部;
    2.监听页面高度变化,实现表格高度自适应。

    整体思路:
    1.页面通常分为三部分:固定框架部分,搜索部分,表格部分;
    2.用监听到的页面高度,减去固定的框架高度,再减去搜索部分的高度,即为表格高度;
    3.给表格设置动态高度,监听页面resize,当页面高度发生变化时,表格高度随即改变,以保证分页在页面最底部;
    4.表格的数据需要做些处理。

    后期完善:
    1.每列的min-width一般按照表头的字数给定,根据具体情况,涉及到时间及身份证号类的数据,要展示完全;

    组件表格:
    1.el-table-column采用v-for的方式;
    2.有些不是常规显示的行,先判断,再做成插槽的形式根据每页不同自行设置。

    其中会用到监听客户屏幕高度的方法,和防抖的方法debounce ↓

    /**
     * 监听浏览器屏幕高度
     * @return {Number} 
     */
    export function getDynamicHeight(ref) {
        let fixedHeight = 132;
    
        let containerHeight = window.innerHeight - fixedHeight || document.documentElement.clientHeight - fixedHeight || document.body.clientHeight - fixedHeight;
        let listHeight = ref ? containerHeight - parseInt(window.getComputedStyle(ref).height) : containerHeight;
    
        return {
            listHeight
        }
    };
    
    /**
     * 防抖:从别的框架抄来的。。。一看注释就很专业绝对不是我自己写的哈哈哈哈哈哈哈
     * @param {Function} func
     * @param {number} wait
     * @param {boolean} immediate
     * @return {*}
     */
    export function debounce(func, wait, immediate) {
        let timeout, args, context, timestamp, result
    
        const later = function () {
            // 据上一次触发时间间隔
            const last = +new Date() - timestamp
    
            // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
            if (last < wait && last > 0) {
                timeout = setTimeout(later, wait - last)
            } else {
                timeout = null
                // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
                if (!immediate) {
                    result = func.apply(context, args)
                    if (!timeout) context = args = null
                }
            }
        }
    
        return function (...args) {
            context = this
            timestamp = +new Date()
            const callNow = immediate && !timeout
            // 如果延时不存在,重新设定延时
            if (!timeout) timeout = setTimeout(later, wait)
            if (callNow) {
                result = func.apply(context, args)
                context = args = null
            }
    
            return result
        }
    }
    

    组件BaseTable正式代码 👇

    <template>
        <div>
            <div class="list-container">
                <el-table :data="data.list" v-loading="loading" border :height="listHeight">
                    <!--
                        ↓ 表格行,较常用的仅展示的部分 
                        一些内容必须显示完全,需要设置min-width
                        因为是固定值,放在computed中判断即可
                        不用显示完全的行可以使用show-overflow-tooltip将溢出部分...
    
                        其中listInfo部分就是表格显示时需要用到的label和prop,
                        每页都不同,需要根据视图与数据不同进行转换
                    -->
                    <el-table-column
                        v-for="(row, index) in listInfo"
                        :key="row.label"
                        :label="row.label"
                        :prop="row.prop"
                        v-if="!row.slot"
                        :min-width="columnMinWidth(row.prop, row.label)"
                        show-overflow-tooltip
                    >
                    </el-table-column>
                    <!-- 
                        ↓ 操作栏:通常位于最右侧
                        操作栏通常有三种规格,一个两个三个按钮,对应不同的min-width
                        判断条件放computed中    
                        常规按钮“编辑”和“删除”放在组件中,可以选择需不需要
                        如果有其它的需求可以在页面中用slot自行设置
                    -->
                    <el-table-column
                        v-else-if="row.slot === 'operation'"
                        :label="row.label"
                        fixed="right"
                        :min-width="operationMinWidth(row.operationNumber)"
                    >
                        <template v-slot="scope">
                            <slot name="operation" :row="data.list[scope.$index]" />
                            <el-button v-if="commonOperation.edit" @click="handleEdit(data.list[scope.$index])" type="text">编辑</el-button>
                            <el-button
                                v-if="commonOperation.del"
                                @click="handleDelete(data.list[scope.$index])"
                                type="text"
                                class="btn-row-delete"
                                >删除</el-button
                            >
                        </template>
                    </el-table-column>
                    <!-- 
                        ↓ 有图片的列
                     -->
                    <el-table-column v-else-if="row.slot === 'image'" :label="row.label" :min-width="columnMinWidth(row.prop, row.label)">
                        <template v-slot="scope">
                            <slot name="image" :row="data.list[scope.$index]" />
                        </template>
                    </el-table-column>
                    <!-- 
                        为可能出现的其它情况预留的默认插槽
                     -->
                    <el-table-column v-else :label="row.label" :min-width="columnMinWidth(row.prop, row.label)">
                        <template v-slot="scope">
                            <slot :row="data.list[scope.$index]" />
                        </template>
                    </el-table-column>
                </el-table>
                <!-- 
                    ↓ 分页
                 -->
                <el-pagination
                    background
                    :currentPage="params.pageNum"
                    :page-size="params.pageSize"
                    :page-sizes="[10, 20, 50, 100]"
                    layout="total, sizes, prev, pager, next, jumper"
                    :total="data.total"
                    @size-change="handlePageSizeChange"
                    @current-change="handleCurrentPageChange"
                >
                </el-pagination>
            </div>
        </div>
    </template>
    
    <script>
    // 引入方法:获取列表高度的方法
    import { getListHeight } from '@/utils/utils';
    // 引入方法:防抖
    import { debounce } from '@/utils/utils';
    
    export default {
        props: {
            // 页面中使用此组件,需要更新数据时,将signal的值赋反即可
            signal: Boolean,
            /**
             * ↓ 将组件中需要展示的值放在一个对象里
             * list是列表数据
             * total是分页需要用到的数据总条数
             */
            data: {
                type: Object,
                required: true,
                default: () => {
                    return { list: [], total: 0 };
                }
            },
            // ↓ 获取数据需要用到的pageNum和pageSize
            params: {
                type: Object,
                required: true,
                default: () => {
                    return {
                        pageNum: 1,
                        pageSize: 10
                    };
                }
            },
            loading: Boolean,
            // ↓ 需要把父页面通过接口获取的数据进行转换,是一个对象数组
            listInfo: Array,
            /**
             *  ↓ 操作栏常规按钮:删除、编辑
             *  可以设置需不需要常规按钮
             */
            commonOperation: {
                type: Object,
                default: () => {
                    return { del: false, edit: false };
                }
            }
        },
        data() {
            return {
                // 列表高度
                listHeight: 0
            };
        },
        computed: {
            /**
             * ↓ 操作栏最小宽度
             * 涉及一个小知识~~
             * 需要传参的computed,需要用return一个function的形式
             */
            operationMinWidth() {
                return (num) => {
                    // 一个按钮60,再多的话依次往下加30
                    return 60 + (num - 1) * 30;
                };
            },
            // 需要完全展示的列需要设置最小宽度
            columnMinWidth() {
                return (prop, label) => {
                    // 每列的minWidth根据label长度,再加表格cell左右的padding
                    let columnMinWidth = label.length * 14 + 20;
                    let str = `${prop}`;
                    /**
                     * 涉及到时间的属性名,有各种,不过基本上都会有time
                     * 先将属性名都转换成小写字母,再indexOf查看有没有time
                     */
                    let timeNum = str.toLowerCase().indexOf('time');
                    /**
                     * 涉及到身份证号(我们一般都是idNumber)
                     * 因为后台的数据也不规范哈哈哈所以先转成小写先
                     */
                    let idNum = str.toLowerCase().indexOf('idnumber');
    
                    if (timeNum != -1) {
                        return 135;
                    }
                    if (idNum != -1) {
                        return 160;
                    }
                    return columnMinWidth;
                };
            }
        },
        methods: {
            monitorScreen() {
                let resize = debounce(() => {
                    this.listHeight = getListHeight().listHeight;
                }, 100);
    
                resize();
                // 页面监听
                window.addEventListener('resize', resize, true);
                // 组件销毁前移除页面监听事件
                this.$once('hook:beforeDestroy', () => {
                    window.removeEventListener('resize', resize, true);
                });
            },
            handlePageSizeChange(val) {
                this.params.pageSize = val;
                this.$emit('changeParamas', {
                    type: 'pageSize',
                    value: val
                });
            },
            handleCurrentPageChange(val) {
                this.params.pageNum = val;
                this.$emit('changeParamas', {
                    type: 'pageNum',
                    value: val
                });
            },
            handleDelete(row) {
                this.$confirm('此操作不可恢复,是否继续?', '提示', {
                    type: 'warning'
                }).then(() => {
                    this.$emit('deleteRow', row);
                });
            },
            handleEdit(row) {
                this.$emit('editRow', row);
            }
        },
        mounted() {
            this.monitorScreen();
        }
    };
    </script>
    

    需要用到该组件的父级页面代码 👇
    我实在是写不动注释了。。都是语义化,强行写注释也是翻译凑合看吧哈哈哈哈哈哈哈

    <template>
        <div>
            <BaseTable
                :loading="loading"
                :params="params"
                :data="data"
                :listInfo="listInfo"
                :commonOperation="{ del: true, edit: true }"
                @changeParamas="changeParamas"
                @deleteRow="handleDelete"
                @editRow="handleEdit"
            >
                <template #image="{ row }">
                    <img :src="`data:image/png;base64,${row.thumurl}`" class="table-image" v-if="row.thumurl" />
                    <div v-else class="">暂无</div>
                </template>
                <template #operation="{ row }">
                    <el-button type="text" @click="handleDetail(row)">详情</el-button>
                </template>
                <template #default="{ row }">
                    <div>默认插槽,可以传值哟~</div>
                </template>
            </BaseTable>
        </div>
    </template>
    
    <script>
    import BaseTable from '@/components/page/BaseTable';
    import { getServiceList } from '@/api/serviceApi';
    
    export default {
        components: {
            BaseTable
        },
        data() {
            return {
                params: {
                    pageNum: 1,
                    pageSize: 10
                },
                data: {
                    list: [],
                    total: 0
                },
                loading: false,
                listInfo: []
            };
        },
        methods: {
            async getData() {
                this.loading = true;
                const { content: data } = await getServiceList(this.params);
                this.data.list = data.list;
                this.data.total = data.total;
                /**
                 * listInfo的数据转换:
                 * 和接口配合,将label和prop设置好后
                 * 需要用插槽的数据们将prop去掉,加一个slot
                 * eg: 图片的slot值为image,操作的slot为operation
                 * 后续如果有其它特别的列,可以再多写一个default
                 */
                this.listInfo = [
                    {
                        label: '服务名称',
                        prop: 'title'
                    },
                    {
                        label: '服务图片',
                        slot: 'image'
                    },
                    {
                        label: 'DEEFAULT SLOT',
                        slot: 'other'
                    },
                    {
                        label: '创建时间',
                        prop: 'createTime'
                    },
                    {
                        label: '操作',
                        slot: 'operation',
                        operationNumber: 3
                    }
                ];
                this.loading = false;
            },
            changeParamas(val) {
                val.type === 'pageNum' ? (this.params.pageNum = val.value) : (this.params.pageSize = val.value);
                this.getData();
            },
            handleDetail(row) {
                console.log(row, '详情行数据');
            },
            handleDelete(row) {
                console.log(row, '删除行数据');
            },
            handleEdit(row) {
                console.log(row, '编辑行数据');
            }
        },
        mounted() {
            this.getData();
        }
    };
    </script>
    
    <style scoped lang="scss">
    .table-image {
        width: 30px;
    }
    </style>
    

    tada~一个列表组件就完成啦

    相关文章

      网友评论

        本文标题:vue 动态表格组件

        本文链接:https://www.haomeiwen.com/subject/anynrrtx.html