设计一个常用SearchForm组件

966 阅读2分钟

方法论

首先我设计所有页面或者组件的思路是 数据决定界面,界面不过是数据的一种形式 掌握了这种方法,其实不管是什么页面什么语言都能通过自己的思考来完成需求

数据设计

所以在我们的常见的飞花项目里,SearchForm是一个什么样的数据结构呢

  • searchFormData即搜索项的数据结构:searchFormData是一个key - value对应的对象 ,大部分valuestringnumber类型,未来可能会出现复杂结构

  • pageData即分页信息:作为一个完整的系统,列表类的分页数据结构是一定的,当然如果做成通用组件应该是外传的,这里因为效率原因选择写在组件内部,不过其有一个特点,除了当前页的值变化的情况下,其他任何搜索项变化都应默认将当前页的值置为1

  • tableData即表格的展示数据:tableData是一个数组,再同一个项目中是相同的数据结构,当然可能需要format,所以我们可以对组件传一个tableDataFormat方法

所以这个时候我们发现 pageDatatableData格式固定 可以放到组件里做状态 searchFormData现阶段固定,未来如果有复杂需求可以放到外面做状态,这次我们都放到组件里

页面设计

上筛选,下列表没什么好说的

代码设计

下面我们就来写一下这个组件

首先是搜索表单这边,组件需要能接收到父组件的form列表,这里可以使用data的形式,不过我们的search-form有时候需要是受控的,所以使用vue element的形式,传入一个个el-form-item然后在组件内包上el-form并且渲染,在搜索的时候通过form对象获取结果,但是这里有个format问题,可以通过传一个formFormat方法解决

// 父组件
<SearchTable>
    <template #extraBar>
        <el-button type="primary" @click="handleAdd">
            新建课程
        </el-button>
    </template>
    <template #searchBar>
        <el-form-item>
            <el-input v-model="searchForm.name" placeholder="姓名" />
        </el-form-item>
        <el-form-item>
            <el-select v-model="searchForm.product_id" placeholder="产品">
                <el-option
                    v-for="item in productList"
                    :key="item.id"
                    :value="item.id"
                    :label="item.name"
                />
            </el-select>
        </el-form-item> 
    </template>
</SearchTable>

// 组件内 
renderSearchBar() {
    if (this.noSearchBar) {
        return '';
    }
    return (
        <el-form class="seatch-form" inline={true} label-width="100">
            {typeof this.$slots.searchBar === 'function' && this.$slots.searchBar()}
            {typeof this.$slots.searchBar === 'function'
                && <span>
                    <el-button
                        class="filter-item"
                        icon="el-icon-search"
                        type="primary"
                        onClick={this.handleSearch}
                    >
                    查询
                    </el-button>
                    <el-button
                        class="filter-item"
                        icon="el-icon-refresh"
                        onClick={this.handleReset}
                    >
                    重置
                    </el-button>
                </span>}

            {typeof this.$slots.extraBar === 'function'
                && <span class="extra-bar">
                    {this.$slots.extraBar()}
                </span>
            }
        </el-form>
    );
},

这里有一个问题因为我们的search-form受控的,所以我们的reset重置操作也需要父组件的传一个默认的handleResetForm方法进来使用

// 父组件
<SearchTable 
    :search-form="searchForm"
    :form-fomat="formFomat"
    :handle-reset-form="handleResetForm"
>

// 组件内
handleResetForm() {
     this.searchForm = {...DEFAULT_SEARCH_FORM};
},

接下来是数据请求,放到组件里,所以需要传api,这里需要保证返回的数据结构和入参结构不变,通过searchFunc传入接口方法,并且处理分页搜索等功能

// 父组件
<SearchTable 
    :search-func="searchFunc" 
>

// 组件内
methods: {
    async getTableData() {
        const {data: {list, page, pageSize, total}}
            = await this.searchFunc({...this.searchForm, ...this.pageDto});
        this.tableData = list;
        this.pageDto = {
            page,
            pageSize,
            total
        };
    },
} 

接下来是列表,分为数据两部分。

这一部分通过对象的数据格式传入,直接渲染成el-table-column

// 父组件
data() {
    return {
        headers: [
            {
                label: 'id',
                prop: 'id',
                width: '80',
            }, 
            {
                label: '操作',
                width: '150',
                fixed: 'right',
                formatter: row => (
                    <div>
                        <el-button
                            onClick={() => this.handleEdit(row)}
                            size="small"
                            type="primary"
                            icon="el-icon-edit"
                        >
                            编辑
                        </el-button> 
                    </div>
                )
            }

        ],
    };
}

<SearchTable 
    :headers="headers"
>

// 组件内
renderTableSlot() {
    const customeColumns = this.$slots.tableColumn
        ? this.$slots.tableColumn()
        : [];
    return this.headers.map(item => {
        // 根据item.prop判断是否使用传入的插槽内容
        const foundItem = customeColumns.find(
            ele =>
                ele.props
                && ele.props.prop === item.prop
        );
        return foundItem || (
            <el-table-column {...item}>
            </el-table-column>
        );
    });
},

数据这一部分在组件中处理

// 组件内
async getTableData() {
    const {data: {list, page, pageSize, total}}
        = await this.searchFunc({...this.searchForm, ...this.pageDto});
    this.tableData = this.tableDataFormat(list);
    this.pageDto = {
        page,
        pageSize,
        total
    };
},
renderTable() {
    return (
        <el-table
            // selection-change = {val => {
            //     this.$emit('selection-change', val);
            // }}
            data={this.tableData}
            ref={this.tableRef}
            {...this.$attrs}>
            {this.renderTableSlot()}
        </el-table>
    );
},

最后整体的组件如下

<script>
const DEFAULT_PAGEDTO = {
    page: 1,
    pageSize: 10,
    total: 100
};

export default {
    name: 'TableTemplate',
    props: {
        searchFunc: {
            required: true
        },
        headers: {
            type: Array,
            default: () => [],
            required: true,
        },
        current: {
            type: Number,
            default: 1,
        },
        pageSize: {
            type: Number,
            default: 10,
        },
        total: {
            type: Number,
            default: 0,
        },
        noSearchBar: Boolean,
        showAddBtn: Boolean,
        searchForm: {
            type: () => Object,
            required: true
        },
        handleResetForm: {
            required: true
        },
        tableDataFormat: {
            type: Function,
            default: data => data,
        }
    },
    data() {
        return {
            tableData: [],
            pageDto: DEFAULT_PAGEDTO
        };
    },
    mounted() {
        this.getTableData();
    },
    methods: {
        async getTableData() {
            const {data: {list, page, pageSize, total}}
                = await this.searchFunc({...this.searchForm, ...this.pageDto});
            this.tableData = this.tableDataFormat(list);
            this.pageDto = {
                page,
                pageSize,
                total
            };
        },
        renderSearchBar() {
            if (this.noSearchBar) {
                return '';
            }
            return (
                <el-form class="seatch-form" inline={true} label-width="100">
                    {typeof this.$slots.searchBar === 'function' && this.$slots.searchBar()}
                    {typeof this.$slots.searchBar === 'function'
                        && <span>
                            <el-button
                                class="filter-item"
                                icon="el-icon-search"
                                type="primary"
                                onClick={this.handleSearch}
                            >
                            查询
                            </el-button>
                            <el-button
                                class="filter-item"
                                icon="el-icon-refresh"
                                onClick={this.handleReset}
                            >
                            重置
                            </el-button>
                        </span>}

                    {typeof this.$slots.extraBar === 'function'
                        && <span class="extra-bar">
                            {this.$slots.extraBar()}
                        </span>
                    }
                </el-form>
            );
        },
        renderTableSlot() {
            const customeColumns = this.$slots.tableColumn
                ? this.$slots.tableColumn()
                : [];
            return this.headers.map(item => {
                // 根据item.prop判断是否使用传入的插槽内容
                const foundItem = customeColumns.find(
                    ele =>
                        ele.props
                        && ele.props.prop === item.prop
                );
                return foundItem || (
                    <el-table-column {...item}>
                    </el-table-column>
                );
            });
        },
        renderTable() {
            return (
                <el-table
                    // selection-change = {val => {
                    //     this.$emit('selection-change', val);
                    // }}
                    data={this.tableData}
                    ref={this.tableRef}
                    {...this.$attrs}>
                    {this.renderTableSlot()}
                </el-table>
            );
        },
        renderPagination() {
            return (
                <el-pagination layout="total, sizes, prev, pager, next"
                    // layout="total, sizes, prev, pager, next, jumper"
                    current-page={this.pageDto.page}
                    page-size={this.pageDto.pageSize}
                    total={this.pageDto.total}
                    onCurrentChange={this.handleCurrentChange}
                    onSizeChange={this.handleSizeChange}
                >
                </el-pagination>
            );
        },
        handleCurrentChange(val) {
            this.pageDto.page = val;
            this.getTableData();
        },
        handleSizeChange(val) {
            this.pageDto.pageSize = val;
            this.getTableData();
        },
        handleSearch() {
            this.getTableData();
        },
        handleReset() {
            this.handleResetForm();
            setTimeout(() => {
                this.handleCurrentChange(1);
            }, 0);
        },
        handleAddBtnClick() {
            this.$emit('add');
        },
        getTableRef() {
            return this.$refs.tableRef;
        },
    },
    render() {
        return (
            <div>
                {this.renderSearchBar()}
                {this.renderTable()}
                {this.renderPagination()}
            </div>
        );
    },
};
</script>

<style lang="less">
.seatch-form {
    text-align: left;
    margin-bottom: 20px;

    .el-form-item {
        margin-bottom: 0;
    }

    .extra-bar {
        margin: 0 10px;
    }
}

.cell {
    .el-button {
        margin: 0 10px;
    }
}

.pagination-wrap {
    margin-top: 20px;
    text-align: right;
}
</style>