vue3 + antd + typeScript 封装一个高仿的ProTable

前言

阿里的antd组件说实话真的非常完善了,可是vue版的寥寥无几,vue3的更少了,今天有空就封装一个高仿的ProTable

开始

其实很简单的,我们是基于vue版的antd进行二次封装,我们把表格组件联动到表单组件即可,然后预留一些位置让组件灵活的更高。

介绍

注意点:

  1. antd的表单组件必须是全局组件
  2. ProTable算是一个半成品(满足基本使用)
  3. 需要vuedraggable、screenfull依赖
  4. 把它转成element 组件的话也非常简单

完成度:

  1. 自动生成查询表单(支持antd全部表单组件)
  2. 表格头部、头部下边(可做勾选了多少列)、底部、表单查询、自定义表单查询的卡槽都有空余出来
  3. 表格的列设置、全屏、密度

其实完成的差不多了,满足我们日常开发需求了。

使用

全局组件(main.ts):

import ProTable from './components/ProTable/IndexView.vue'
app.component('ProTable', ProTable);

ProTable组件代码:

<template>
    <div>
        <div style="margin: 10px 0px; padding: 24px 24px; background-color: #fff" >
            <!--自定义查询-->
            <slot name="definitionForm"></slot>
            <!--固定查询-->
            <a-form
                    v-if="formShowPage"
                    ref="formRef"
                    name="advanced_search_form"
                    :model="form"
                    @finish="onFormFinish"
            >
                <a-row :gutter="24">
                    <template v-for="(item, key) in formDataList" :key="key">
                        <a-col v-if="item.search" v-show="formExpand || key <= 1" :span="8" >
                            <a-form-item
                                    :name="`${item.key}`"
                                    :label="`${item.title}`"
                                    :rules="item.rules"
                            >
                                <template v-if="item.valueType">
                                    <component :is="`${item.valueType}`" style="width: 100%"

                                               :options="item.valueEnum"
                                               :tree-data="item.valueEnum"

                                               v-model:value="form[item.key]"

                                               :placeholder="item.placeholder ? item.placeholder : `请输入${item.title}`"

                                               :value-format="item.format"
                                               :show-time="item.showTime"
                                               :picker="item.picker"
                                    ></component>
                                </template>
                                <template v-else>
                                    <a-input v-model:value="form[`${item.key}`]" :placeholder="`请输入${item.title}`"></a-input>
                                </template>
                            </a-form-item>
                        </a-col>
                    </template>
                    <a-col :span="formExpand ? 24 : 8">
                        <a-row type="flex" justify="end">
                            <a-col>
                                <!--固定查询左边-->
                                <slot name="searchFormLeft"></slot>
                            </a-col>
                            <a-col>
                                <a-button type="primary" html-type="submit">查询</a-button>
                                <a-button style="margin: 0 8px" @click="() => formRef.resetFields()">重置</a-button>
                                <a style="font-size: 12px" @click="formExpand = !formExpand">
                                    <template v-if="formExpand">
                                        <UpOutlined />
                                        收起
                                    </template>
                                    <template v-else>
                                        <DownOutlined />
                                        展开
                                    </template>
                                </a>
                            </a-col>
                        </a-row>
                    </a-col>
                </a-row>
            </a-form>
        </div>
        <!--表格全屏容器-->
        <div ref="tableRef" style="background-color: #fff">
            <a-table
                    :row-selection="{ selectedRowKeys: tableSelectedRowKeys, onChange: onTableSelectChange }"
                    :columns="tableColumns"
                    :data-source="tableData"
                    :size="tableDensityCurrent[0]"
                    :loading="tableLoading"
                    :pagination="tablePagination"
                    :scroll="tableScroll"
            >
                <template #title>
                    <a-row type="flex">
                        <a-col :span="12">
                            <!--表格头部左边-->
                            <slot name="tableHeaderLeft"></slot>
                        </a-col>
                        <a-col :span="12" style="text-align: right; cursor: pointer">
                            <a-row type="flex" justify="end">
                                <a-col style="margin-right: 16px;">
                                    <!--表格头部右边-->
                                    <slot name="tableHeaderRight"></slot>
                                </a-col>
                                <a-col style="margin-right: 16px;">
                                    <span>
                                        <a-tooltip>
                                            <template #title>刷新</template>
                                            <RedoOutlined style="font-size: 16px" @click="onTableRefreshClick" />
                                        </a-tooltip>
                                    </span>
                                </a-col>
                                <a-col style="margin-right: 16px;">
                                    <span>
                                        <a-dropdown trigger="click" placement="bottom">

                                    <a-tooltip>
                                        <template #title>密度</template>
                                        <ColumnHeightOutlined style="font-size: 16px" />
                                    </a-tooltip>

                                    <template #overlay>
                                        <a-menu :theme="layoutModule.navBgStyle ? 'dark' : 'light'" style="width: 80px; text-align: center"
                                                v-model:selectedKeys="tableDensityCurrent"
                                                @click="onTableDensityChange"
                                        >
                                            <a-menu-item key="default">
                                                <a href="javascript:;">默认</a>
                                            </a-menu-item>
                                            <a-menu-item key="middle">
                                                <a href="javascript:;">中等</a>
                                            </a-menu-item>
                                            <a-menu-item key="small">
                                                <a href="javascript:;">紧凑</a>
                                            </a-menu-item>
                                        </a-menu>
                                    </template>

                                </a-dropdown>
                                    </span>
                                </a-col>
                                <a-col style="margin-right: 16px;">
                                    <span>
                                        <a-popover trigger="click" placement="bottomRight">
                                             <a-tooltip>
                                                <template #title>列设置</template>
                                                <SettingOutlined style="font-size: 16px" />
                                            </a-tooltip>
                                            <template #title>
                                                <a-checkbox style="padding: 5px 0px" v-model:checked="columnSetCheckAll" :indeterminate="columnSetIndeterminate" @change="onTableColumnSetCheckAllChange">
                                                    <span> 列展示 / 排序 </span>
                                                    <a href="javascript:;" @click="onTableColumnSetResetClick">重置</a>
                                                </a-checkbox>
                                            </template>
                                            <template #content>
                                                <draggable
                                                        v-model="columnSetDataList"
                                                        group="people"
                                                        @start="drag=true"
                                                        @end="drag=false"
                                                        @change="onTableColumnSetDraggableChange"
                                                        item-key="title"
                                                >
                                                   <template #item="{ element }">
                                                       <div style="cursor: pointer; margin-left: -14px;">
                                                           <HolderOutlined style="font-size: 14px; color: rgba(0,0,0,.45)" />
                                                           <a-checkbox v-model:checked="element.checked">{{element.title}}</a-checkbox>
                                                       </div>
                                                    </template>
                                                </draggable>
                                            </template>
                                        </a-popover>
                                    </span>
                                </a-col>
                                <a-col>
                                    <span>
                                         <a-tooltip>
                                            <template #title>全屏</template>
                                            <fullscreenExitOutlined v-if="tableFullscreen" @click="onTableFullScreenClick" style="font-size: 16px"/>
                                            <FullscreenOutlined v-else @click="onTableFullScreenClick" style="font-size: 16px" />
                                        </a-tooltip>
                                    </span>
                                </a-col>
                            </a-row>
                        </a-col>
                    </a-row>
                    <!--表格头部下边-->
                    <slot name="tableHeaderBottom"></slot>
                </template>

                <!--个性化单元格-->
                <template #bodyCell="{ text, record, index, column }">
                    <slot name="tableBodyCell" v-bind="{text, record, index, column}"></slot>
                </template>

                <!--自定义筛选菜单(加个变量控制不然会导致空白)-->
<!--                <template #customFilterDropdown="{ column }">-->
<!--                    <slot name="tableCustomFilterDropdown" v-bind="{column}"></slot>-->
<!--                </template>-->

                <!--自定义筛选图标(加个变量控制不然会导致空白)-->
<!--                <template #customFilterIcon="{ filtered, column }">-->
<!--                    <slot name="tableCustomFilterIcon" v-bind="{ filtered, column }"></slot>-->
<!--                </template>-->

                <!--自定义空数据时的显示内容-->
<!--                <template #emptyText>-->
<!--                    <slot name="tableEmptyText"></slot>-->
<!--                </template>-->

                <!--自定义总结栏-->
<!--                <template #summary>-->
<!--                    <slot name="tableSummary"></slot>-->
<!--                </template>-->

                <template #footer>
                    <!--表格底部-->
                    <slot name="tableFooter"></slot>
                </template>

            </a-table>
        </div>
    </div>
</template>

<script lang="ts">
    import { reactive, watch, toRefs, onMounted, onUnmounted } from "vue"
    import { useStore } from 'vuex'
    import { RedoOutlined, ColumnHeightOutlined, SettingOutlined, FullscreenOutlined,
        FullscreenExitOutlined, HolderOutlined, DownOutlined, UpOutlined  } from '@ant-design/icons-vue';
    import draggable from 'vuedraggable'
    import screenfull from "screenfull";

    export default {
        name: 'ProTable',
        components: {
            RedoOutlined,
            ColumnHeightOutlined,
            SettingOutlined,
            FullscreenOutlined,
            FullscreenExitOutlined,
            HolderOutlined,
            DownOutlined, UpOutlined,
            draggable
        },
        props: {
            formShow: {
                type: Boolean,
                required: false,
                default: () => {
                    return true
                }
            },
            loading: {
                type: Boolean,
                required: false,
                default: () => {
                    return false
                }
            },
            sourceColumns: {
                type: Array,
                required: true,
                default: () => {
                    return []
                }
            },
            sourceData: {
                type: Array,
                required: true,
                default: () => {
                    return []
                }
            },
            pagination: {
                type: Array,
                required: false,
                default: () => {
                    return []
                }
            },
            scroll: {
                type: Object,
                required: false,
                default: () => {
                    return {}
                }
            }
        },
        setup(props: any, ctx: any) {
            // https://www.antdv.com/components/table-cn#Column
            // 与Column相同,下列写自行添加的字段
            interface ColumnsItem {
                search?: boolean;
                valueType?: string;
                initialValue?: any;
                valueEnum?: any;
                rules?: any;
                format?: string;
                showTime?: boolean;
                placeholder?: string | string[];
            }
            const { sourceColumns, sourceData, loading, pagination, scroll, formShow } = toRefs(props);
            const columns:ColumnsItem = sourceColumns.value;
            const tableData:any = sourceData.value;
            const tableLoading:any = loading;
            const formShowPage:any = formShow;
            const tablePagination:any = pagination;
            const tableScroll:any = scroll;

            const store = useStore();
            const layoutModule = store.state.layout;

            const dataCheckedList:any = JSON.parse(JSON.stringify(columns));
            dataCheckedList.forEach((item:any) => {
                item.checked = true
            });

            // 表单
            interface FormStateType {
                formRef: HTMLElement | string;
                formExpand: boolean;
                formDataList: any,
                form: []
            }
            const formState = reactive<FormStateType>({
                formRef: '',
                formExpand: false,
                formDataList: JSON.parse(JSON.stringify(columns)),
                form: []
            });
            formState.formDataList.forEach((item: any) => {
                // 默认是文本框搜索
                if (!item.valueType) {
                    item.valueType = 'a-input'
                }
                // 默认值赋值
                if (item.initialValue) {
                    formState.formDataList[item.key] = item.initialValue
                }
            });
            // 表单提交
            const onFormFinish = (values: any) => {
                ctx.emit("finish", values);
            };
            interface TableStateType {
                tableRef: HTMLElement | undefined;
                tableSelectedRowKeys: [];
                tableDensityCurrent: string[];
                tableFullscreen: boolean;
                tableColumns: any;
            }
            // 表格
            const tableState = reactive<TableStateType>({
                tableRef: undefined,
                tableSelectedRowKeys: [],
                tableDensityCurrent: ['middle'],
                tableFullscreen: false,
                tableColumns: JSON.parse(JSON.stringify(columns))
            });
            // 表格选中切换
            const onTableSelectChange = (selectedRowKeys: []) => {
                tableState.tableSelectedRowKeys = selectedRowKeys;
            };
            // 表格刷新
            const onTableRefreshClick = () => {
                ctx.emit("refresh");
            };
            // 表格密集度
            const onTableDensityChange = (item: any) => {
                tableState.tableDensityCurrent = item.keyPath
            };
            // 表格列设置
            const columnSetState = reactive({
                columnSetIndeterminate: false,
                columnSetCheckAll: true,
                columnSetDataList: JSON.parse(JSON.stringify(dataCheckedList)),
            });
            // 表格列设置重置
            const onTableColumnSetResetClick = () => {
                columnSetState.columnSetIndeterminate = false;
                columnSetState.columnSetCheckAll = true;
                columnSetState.columnSetDataList = JSON.parse(JSON.stringify(dataCheckedList));
            };

            // 表格列设置拖拽
            const onTableColumnSetDraggableChange = () => {
                tableProcessingSelectColumnsData();
            };

            // 数据处理
            const tableProcessingSelectColumnsData = () => {
                let selectData: any[] = [];
                let noSelectData: any[] = [];
                columnSetState.columnSetDataList.forEach((item: any) => {
                    if(item.checked) {
                        selectData.push(item);
                    } else {
                        noSelectData.push(item);
                    }
                });
                if (selectData.length === 0) {
                    selectData.push({});
                }
                tableState.tableColumns = selectData;
            };
            // 表格列设置全选
            const onTableColumnSetCheckAllChange = (e: any) => {
                if (e.target.checked) {
                    // 全选中
                    columnSetState.columnSetDataList.forEach((item: any) => {
                        item.checked = true
                    });
                    columnSetState.columnSetCheckAll = true;
                    columnSetState.columnSetIndeterminate = false;
                } else {
                    // 全取消
                    columnSetState.columnSetDataList.forEach((item: any) => {
                        item.checked = false
                    });
                    columnSetState.columnSetCheckAll = false;
                    columnSetState.columnSetIndeterminate = false;
                }
            };
            watch(() => columnSetState.columnSetDataList, (newVal) => {
                    let checkAllList = [];
                    let indeterminate = false;
                    for(let i =0; i < newVal.length; i++) {
                        let item:any = newVal[i];
                        if (item.checked) {
                            indeterminate = true;
                            checkAllList.push(item);
                        }
                    }
                    if (checkAllList.length == newVal.length) {
                        columnSetState.columnSetCheckAll = true;
                        columnSetState.columnSetIndeterminate = false;
                    } else {
                        columnSetState.columnSetCheckAll = false;
                        if (indeterminate) {
                            columnSetState.columnSetIndeterminate = true;
                        } else {
                            columnSetState.columnSetIndeterminate = false;
                        }
                    }
                    tableProcessingSelectColumnsData()
                },
                {
                    deep: true, // 深度监听
                    immediate: false
                }
            );
            // 表格全屏
            const onTableFullScreenClick = () => {
                if (screenfull.isEnabled) {
                    screenfull.toggle(tableState.tableRef);
                }
            };
            const onTableFullScreenChange = () => {
                tableState.tableFullscreen = screenfull.isFullscreen
            };
            // 设置监听器
            onMounted(() => {
                screenfull.on('change', onTableFullScreenChange)
            });
            // 删除监听器
            onUnmounted(() => {
                screenfull.off('change', onTableFullScreenChange)
            });

            return {
                tableLoading,
                tableScroll,
                tablePagination,
                formShowPage,
                ...toRefs(formState),
                onFormFinish,
                tableData,
                ...toRefs(tableState),
                ...toRefs(columnSetState),
                onTableSelectChange,
                onTableRefreshClick,
                onTableDensityChange,
                onTableColumnSetResetClick,
                onTableColumnSetDraggableChange,
                onTableColumnSetCheckAllChange,
                onTableFullScreenClick,
                layoutModule,
            }
        },
    }
</script>
<style scoped>
</style>

使用代码:

<template>
    <div>
        <div style="height: 20px; margin: 10px 0px; background-color: #fff">
            首页 / 测试页
        </div>
        <div style="background-color: #f0f2f5; padding: 24px">
            <ProTable :sourceColumns="columns" :sourceData="data" :formShow="true"
                @finish="finish"   @refresh="refresh"
            >
                <!--自定义某字段样式-->
                <template #tableBodyCell="{ column }">
                    <template v-if="column.key === 'operation'">
                        <a>编辑</a>
                    </template>
                </template>
            </ProTable>
        </div>
    </div>

</template>

<script lang="ts">
    const columns = [
        { title: '序号', width: 100, dataIndex: 'age', key: 'age',
            search: true,
            valueType: 'a-input-number',
            initialValue: 100,
        },
        { title: '姓名', width: 100, dataIndex: 'name', key: 'name',
            search: true,
            valueType: 'a-input',
            sorter: true,
            rules: [
                {
                    required: true,
                    message: '请输入序号',
                },
                {
                    min: 3,
                    max: 5,
                    message: '序号长度 3 to 5',
                    trigger: 'blur',
                }
            ]
        },
        { title: '密码', width: 100, dataIndex: 'password', key: 'password',
            search: true,
            filters: [
                {
                    text: 'Joe',
                    value: 'Joe',
                },
                {
                    text: 'John',
                    value: 'John',
                },
            ],
        },
        { title: '日期选择框', width: 100, dataIndex: 'datepicker', key: 'datepicker',
            search: true,
            valueType: 'a-date-picker',
            initialValue: '2022/08/01',
            format: 'YYYY/MM/DD'
        },
        { title: '日期范围选择框', width: 100, dataIndex: 'daterangepicker', key: 'daterangepicker',
            search: true,
            placeholder: ['测试日期开始时间', '测试日期结束时间'],
            valueType: 'a-range-picker',
            showTime: true
            // initialValue: ['09:00:00'],
            // format: 'YYYY:MM:DD'
        },
        { title: '时间选择框', width: 100, dataIndex: 'timeepicker', key: 'timeepicker',
            search: true,
            valueType: 'a-time-picker',
            initialValue: '09:00:00',
            format: 'HH:mm:ss'
        },
        { title: '时间范围选择框', width: 100, dataIndex: 'timerangepicker', key: 'timerangepicker',
            search: true,
            placeholder: ['测试时间开始时间', '测试时间结束时间'],
            valueType: 'a-time-range-picker',
            showTime: true,
            // initialValue: ['09:00:00', '12:00:00'],
            // format: 'HH:mm:ss'
        },

        { title: '单选框', width: 100, dataIndex: 'radio', key: 'radio',
            initialValue: 1,
            valueEnum: [
                {value: 0, label: '测试1'},
                {value: 1, label: '测试2'},
                {value: 2, label: '测试3'}
            ],
            search: true, valueType: 'a-radio-group'
        },
        { title: '测试级联选择框', width: 100, dataIndex: 'cascader', key: 'cascader',
            initialValue: 'zhejiang',
            valueEnum: [
                {
                    value: 'zhejiang',
                    label: 'Zhejiang',
                    children: [
                        {
                            value: 'hangzhou',
                            label: 'Hangzhou',
                            children: [
                                {
                                    value: 'xihu',
                                    label: 'West Lake',
                                },
                            ],
                        },
                    ],
                },
                {
                    value: 'jiangsu',
                    label: 'Jiangsu',
                    children: [
                        {
                            value: 'nanjing',
                            label: 'Nanjing',
                            children: [
                                {
                                    value: 'zhonghuamen',
                                    label: 'Zhong Hua Men',
                                },
                            ],
                        },
                    ]
                }
            ],
            search: true, valueType: 'a-cascader'
        },
        { title: '测试选择框', width: 100, dataIndex: 'select', key: 'select',
            initialValue: 1,
            valueEnum: [
                {value: 0, label: '测试1'},
                {value: 1, label: '测试2'},
                {value: 2, label: '测试3'}
            ],
            search: true, valueType: 'a-select'
        },
        { title: '测试树形下拉框', width: 100, dataIndex: 'treeselect', key: 'treeselect',
            // initialValue: 1,
            valueEnum: [
                {
                    title: 'parent 1',
                    value: 'parent 1',
                    children: [
                        {
                            title: 'parent 1-0',
                            value: 'parent 1-0',
                            children: [
                                {
                                    title: 'my leaf',
                                    value: 'leaf1',
                                },
                                {
                                    title: 'your leaf',
                                    value: 'leaf2',
                                },
                            ],
                        },
                        {
                            title: 'parent 1-1',
                            value: 'parent 1-1',
                        },
                    ],
                },
            ],
            search: true, valueType: 'a-tree-select'
        },
        { title: '性别', dataIndex: 'sex', key: 'sex', width: 150,
            search: true,
            valueType: 'a-checkbox-group',
            initialValue: ['b'],
            valueEnum: ['a', 'b', 'c'],
            rules: [
                {
                    required: true,
                    message: '请输入性别',
                }
            ]
        },
        {
            title: '操作',
            search: false,
            key: 'operation',
            width: 100
        },
    ];
    interface DataItem {
        key: number;
        name: string;
        age: number;
        password: string;
        sex: string;
    }

    const data: DataItem[] = [];
    for (let i = 0; i < 10; i++) {
        data.push({
            key: i,
            name: `测试姓名 ${i}`,
            age: 32,
            password: `密码. ${i}`,
            sex: '男'
        });
    }
    export default {
        mounted(): void {
            console.log("IndexTestView - mounted")
        },
        components: {},
        // data() {
        //     return {
        //         a: 1
        //     }
        // },
        // created() {
        //     console.log(this.a) // 1
        //     console.log(this.$data) // { a: 1 }
        // },
        setup() {
            const finish = (values: any) => {
                console.log(values)
            };
            const refresh = () => {
                console.log("刷新")
            };
            return {
                columns,
                data,
                finish,
                refresh
            }
        },
    }
</script>
<style scoped>
</style>

总结

这篇我们深度的使用了table、form组件进行了二次封装,加深了我们对antd组件库的使用以及增加 了我们对封装组件库的能力。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

An_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值