管理端表格组件提取
一、开始
做过几个管理端需求,很多页面都差不多,上面是几个搜索框,Input或者Select类型,中间一个新建按钮,下面一个Table,最下面是Table的分页,类似下图:

这种模式千篇一律,如果每次新来一个管理端需求,都要从0开始,那多费劲,有没有什么办法可以提高效率呢?
二、组件化
1. 思路
一个简单的想法是把它们抽成组件。组件的一个特征是通用性,它可能冗余了一小部分信息,比如一个Input组件,一些你不需要的属性它也内置了,但好处是适用性更强了。
回到上面的管理端Table页面,它其实是由一些组件组合而成,我们只要定义一些配置,然后生成页面就可以了,其思想是“配置驱动”。
注意,这里存在两种不同级别的组件:
- 一种是基础UI组件,不含逻辑,没有副作用,由基本的组件库组件构成,或者根据业务自己书写。
- 另一种是Module组件,可以存在一些逻辑,比如请求接口等。
对于这里的Table页面,将其作为一个Module组件,其由三个UI组件组成,结构如下:

”新建“比较简单,没有提取UI组件。
2. UI组件
(1)搜索框
对于第一部分——搜索框,我们定义一个searchList,表明有多少搜索框及其类型。比如:
| const searchList = [{
type: 'INPUT',
name: 'nick',
label: '主播昵称',
placeholder: '请输入主播昵称',
width: 200,
}, {
type: 'SELECT',
name: 'platform',
label: '所在平台',
placeholder: '请选择所在平台',
options: [
{
value: '1',
label: '斗鱼',
},
{
value: '2',
label: '虎牙',
},
{
value: '3',
label: '快手',
},
],
width: 200,
}, {
type: 'INPUT',
name: 'weight',
label: '推荐权重',
placeholder: '请输入',
width: '120',
}];
|
目前搜索框支持Input/Select两种类型。
(2)新建
对于第二部分——新建,处理比较简单,只暴露了两个属性,是否显示showAddItemBtn
和跳转地址addItemRouter
。
(3)Table
对于第三部分——Table,定义了两个最基本的属性columns和tableData,columns是列信息,可以携带事件、指定slot,tableData是表格数据。columns如下:
| [{
prop: 'desc',
label: '自我介绍',
width: '',
},
{
prop: 'header',
label: '图片',
width: '100',
slotList: [{
type: 'AVATAR',
}],
},
{
prop: 'status',
label: '状态',
width: '',
classFn: val => `my-status status-${val}`,
formatter: ((val) => {
const statusMap = {
1: '正常',
2: '已下线',
};
return statusMap[val] || '';
}).toString(),
},
{
label: '操作',
slotList: [{
type: 'BUTTON',
emit(row) {
this.$router.push({
path: '/anchor/add',
query: {
anchor: row.streamerId,
},
});
},
text: '修改',
props: {
type: 'text',
size: 'small',
},
}],
}]
|
其中slotList表明这个列是插槽列表,之所以设计成列表,主要是因为“操作”列可能有多个按钮。
Table组件中关于Slot的具体实现如下
| <template v-for="(tSlot, idx) of column.slotList">
<!-- 按钮 -->
<el-button
v-if="tSlot.type === 'BUTTON'"
:key="`slot-${idx}`"
v-bind="tSlot.props || {}"
@click.stop="onEmitEvent(tSlot, tableData[scope.$index])"
>
{{ tSlot.text }}
</el-button>
<!-- 头像 -->
<el-avatar
v-else-if="tSlot.type === 'AVATAR'"
:key="`slot-${idx}`"
:src="scope.row[column.prop]"
/>
<!-- 自定义slot -->
<slot
v-else-if="tSlot.type === 'CUSTOM_SLOT'&& tSlot.name"
:scope="scope"
:name="tSlot.name"
/>
</template>
|
对于一个按钮类型slot来说,其基本结构如下:
| {
type: 'BUTTON', // 指明类型
emit(row) { // 处理事件,组件内部会将this绑定。
this.$router.push({
path: '/anchor/add',
query: {
anchor: row.streamerId,
},
});
},
text: '修改', // 文本
props: { // 传递给el-button的其他属性
type: 'text',
size: 'small',
},
}
|
对于一个Avatar类型的slot来说,其基本结构如下:
| {
type: 'AVATAR', // 指明类型
}
|
除此以外,Table组件还有单独处理class、格式化的需求,这部分也是在columns中指定,比如:
| {
prop: 'status',
label: '状态',
width: '',
classFn: val => `my-status status-${val}`, // 单独处理class
formatter: ((val) => { // 格式化函数,可为Function/String类型,String类型在组件内部会被转成Function
const statusMap = {
1: '正常',
2: '已下线',
};
return statusMap[val] || '';
}).toString(),
}
|
(4)分页
对于第四部分——表格的分页,完全由Module组件自己控制,开发者无需配置。
3. 其他
除了上面的结构需要一些配置/属性外,页面还需要通过接口获取数据,这里提供了一系列属性:
| // 数据获取URL
fetchDataUrl: {
type: String,
default: '',
},
// 数据获取基础URL
baseUrl: {
type: String,
default: '',
},
// 开发环境-数据获取基础URL
devBaseUrl: {
type: String,
default: '',
},
// 解析表格数据函数,入参数为接口返回的res,需返回list、total
parseTableDataFn: {
type: [Function, String],
default: null,
},
// 获取数据参数处理
handleFetchDataParamFn: {
type: Function,
default: null,
},
|
这里我用了云函数来mock数据,这样的模拟数据有个好处是不受登录态、token、refer等影响,保证数据的获取,同时保证筛选/分页等功能可用。这样接入低代码平台时也直观。
三、效果
这样抽离组件、定义配置规范后,再接入管理端需求时,工期可由3小时降为2分钟。
四、使用中的项目
目前在主播杯后台使用了此模板,后续会不断丰富Module组件,更多项目使用。
五、低代码
低代码平台已经接入此组件,在低代码使用的好处是直观、方便修改props调试。
六、关于数据驱动
最近做需求时有些体会:
- 一开始要设计好,否则一开始乱了,后面就没法收拾了。
- 写代码也要有远见,逻辑和UI尽量分离,指不定哪天,这套逻辑就要复用到其他端上,或者UI大变样。
- 前端大部分时间的工作是,处理数据、展示,处理数据花了大量的时间。
下面是个UI和逻辑分离的例子,一开始重构把所有数据都写在了UI上:
| <ul class="rules-form">
<li>
<span class="rule-form-label">晋级名次:</span>
<div class="rule-form-content-right">
<p>这里是内容</p>
</div>
</li>
<li>
<span class="rule-form-label">分组规则:</span>
<div class="rule-form-content-right">
<p>队伍成员x人及以上</p>
</div>
</li>
<li>
<span class="rule-form-label">积分规则:</span>
<div class="rule-form-content-right">
<p>2021-04-21 14:30</p>
</div>
</li>
<li class="no-flex">
<span class="rule-form-label">团队赛排名分系统:</span>
<table class="rule-form-table">
<thead>
<tr>
<th>排名</th>
<th>1</th>
</tr>
</thead>
<tbody>
<tr>
<td>排名分</td>
<td>2</td>
</tr>
</tbody>
</table>
</li>
</ul>
|
经过整理,其实可以抽象成下面的结构:
| <ul class="rules-form">
<li
v-for="(item, index) of briefRule.intro"
:key="`intro-${index}`"
>
<span class="rule-form-label">{{ item.label }}:</span>
<div class="rule-form-content-right">
<p>{{ item.value }}</p>
</div>
</li>
<li
v-for="(item, index) of briefRule.table"
:key="`table-${index}`"
class="no-flex"
>
<span class="rule-form-label">{{ item.title }}:</span>
<table class="rule-form-table">
<thead>
<tr>
<th
v-for="(head, headIdx) of item.heads"
:key="`head-${headIdx}`"
>
{{ head.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(data,dataIdx) of item.data"
:key="`data-${dataIdx}`"
>
<td
v-for="(headData,headDataIdx) of item.heads"
:key="`headData-${headDataIdx}`"
>
{{ data[headData.value] }}
</td>
</tr>
</tbody>
</table>
</li>
</ul>
|

其中用到的数据为briefRule,举例如下:
| {
"intro": [{
"label": "晋级名次",
"value": "分组内第1-10名晋级"
}, {
"label": "分组规则",
"value": "固定分组"
}, {
"label": "积分规则",
"value": "排名分、击杀分"
}],
"table": [{
"title": "个人赛排名分系统",
"heads": [{
"label": "排名",
"value": "rankDesc"
}, {
"label": "排名分",
"value": "rankscore"
}],
"data": [{
"startrank": 1,
"endrank": 1,
"rankscore": 20,
"rankDesc": 1
}, {
"startrank": 2,
"endrank": 2,
"rankscore": 19,
"rankDesc": 2
}]
}, {
"title": "个人赛击杀分系统",
"heads": [{
"label": "圈层",
"value": "areaDesc"
}, {
"label": "击杀分",
"value": "scoreDesc"
}],
"data": [{
"startarea": 0,
"endarea": 1,
"killscore": 0,
"areaDesc": "0 - 1",
"scoreDesc": 0
}]
}]
}
|