方法论
首先我设计所有页面或者组件的思路是
数据决定界面,界面不过是数据的一种形式
掌握了这种方法,其实不管是什么页面什么语言都能通过自己的思考来完成需求
数据设计
所以在我们的常见的飞花项目里,SearchForm是一个什么样的数据结构呢
-
searchFormData即搜索项的数据结构:searchFormData是一个key - value对应的对象 ,大部分value为string和number类型,未来可能会出现复杂结构 -
pageData即分页信息:作为一个完整的系统,列表类的分页数据结构是一定的,当然如果做成通用组件应该是外传的,这里因为效率原因选择写在组件内部,不过其有一个特点,除了当前页的值变化的情况下,其他任何搜索项变化都应默认将当前页的值置为1 -
tableData即表格的展示数据:tableData是一个数组,再同一个项目中是相同的数据结构,当然可能需要format,所以我们可以对组件传一个tableDataFormat方法
所以这个时候我们发现
pageData和tableData格式固定 可以放到组件里做状态
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>