vue 动态表格组件

效果图 👇

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

需求:
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~一个列表组件就完成啦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容