我们在键盘上留下的余温, 也将随时代传递到更远的将来


:D 获取中...

一般做后台管理系统的项目, 用到最多的 UI 组件应该就是表格和表单吧.

一个最基础的例子就是上面一排操作按钮: 增删改查; 下面一个表格, 展示数据; 增改的时候弹出表单; 而且大部分情况下表格和表单的字段都是一样的, 但却要写两套代码, 作为一个崇尚简洁的程序员, 越简洁越好(才不是懒得写呢)

所以就想封装一下这两个组件, 最好一行代码就搞定了

分析现有代码

table

先看 table 例子:

1
2
3
4
5
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="180"> </el-table-column>
<el-table-column prop="name" label="姓名" width="180"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
</el-table>

我们可以把 column 这部分抽出一个数组, 数组中包含所有属性; 和 data 似的传入封装好的组件中, 组件自己遍历生成 el-table-column , 那咱们调用组件的时候是不是就一行代码就搞定了呀:

1
<base-table :data="tableData" :columns="columns" style="width: 100%"></base-table>

form

再看 form 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="活动名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="活动区域">
<el-select v-model="form.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="活动时间">
<el-col :span="11">
<el-date-picker
type="date"
placeholder="选择日期"
v-model="form.date1"
style="width: 100%;"
></el-date-picker>
</el-col>
<el-col class="line" :span="2">-</el-col>
<el-col :span="11">
<el-time-picker
placeholder="选择时间"
v-model="form.date2"
style="width: 100%;"
></el-time-picker>
</el-col>
</el-form-item>
<el-form-item label="即时配送">
<el-switch v-model="form.delivery"></el-switch>
</el-form-item>
<el-form-item label="活动性质">
<el-checkbox-group v-model="form.type">
<el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
<el-checkbox label="地推活动" name="type"></el-checkbox>
<el-checkbox label="线下主题活动" name="type"></el-checkbox>
<el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="特殊资源">
<el-radio-group v-model="form.resource">
<el-radio label="线上品牌商赞助"></el-radio>
<el-radio label="线下场地免费"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="活动形式">
<el-input type="textarea" v-model="form.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">立即创建</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>

form 就比较难一些了: 不仅要判断表单类型( input , select , textarea 等等), select 等表单还需要 option 数组

为了实现一行代码就搞定, 所以我们最终调用的方式应该是这样的:

1
<base-form ref="form" :model="form" :form-items="formItems" label-width="80px"></base-form>

那就需要在 formItems 中做处理了: 首先需要区分表单类型; 对于 select 等这类表单, 还需要传入 option 选择数组

初稿

table

table 就比较简单了

组件源码:

1
2
3
4
5
6
7
8
<el-table v-bind="$attrs">
<el-table-column
v-for="column in columns"
:key="column.prop"
v-bind="column"
>
</el-table-column>
</el-table>
1
2
3
4
5
6
7
8
9
name: 'BaseTable',
props: {
columns: {
type: Array,
default () {
return []
}
}
}

调用:

1
<base-table :data="tableData" :columns="columns" style="width: 100%"></base-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
data() {
return {
tableData: [
{
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}
],
columns: [
{
label: '日期',
prop: 'date',
width: 180
},
{
label: '姓名',
prop: 'name',
width: 180
},
{
label: '地址',
prop: 'address'
}
]
}
}

form

form 需要做的事情比 table 多一些

组件源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<el-form v-bind="$attrs" :model="model">
<el-form-item v-for="item in formItems" :key="item.prop">
<el-input
v-if="item.type === 'input'"
v-model="model[item.prop]"
v-bind="item"
>
</el-input>
<el-select v-if="item.type === 'select'" v-model="model[item.prop]" v-bind="item">
<el-option
v-for="option in item.select"
:key="option.value"
:label="option.label"
:value="option.value">
</el-option>
</el-select>
<el-switch
v-if="item.type === 'switch'"
v-model="model[item.prop]"
v-bind="item"
>
</el-switch>
</el-form-item>
</el-form>

其余的表单就不写了, 都差不多的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name: 'BaseForm',
props: {
modle: {
type: Object,
default() {
return {}
}
},
formItems: {
type: Array,
default() {
return []
}
}
}

调用:

1
<base-form ref="form" :model="form" :form-items="formItems" label-width="80px"></base-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
data() {
return {
form: {
name: '',
region: ''
},
formItems: [
{
type: 'input',
label: '活动名称',
prop: 'name'
},
{
type: 'select',
label: '活动区域',
prop: 'region',
select: [
{
label: '区域一',
value: 'shanghai'
},
{
label: '区域二',
value: 'beijing'
}
]
}
]
}
}

优化

目前使用我们封装好的组件, 确实可以实现一行代码就搞定的效果, 而且如果 formtable 字段一样的话, formItemscolumns 完全可以合并为一个数组. 但还有几个问题:

  1. BaseTable 功能过于单一, 只能展示数据, 我们实际工作中还有可编辑的表格
  2. BaseForm 封装的组件重复代码太多, 判断太多, 很多时候只是组件名字不同, 里面内容完全一致, 比如 el-inputel-switch; 现在把 ElementUI 所有表单元素都写进去了, 但实际上就 el-input 用的比较多, 而且可扩展性不好, 想要再加表单, 就需要修改封装的组件的源码

综合以上两个问题我们不难看出目前的主要问题就是这些可编辑的表单元素, 那我们解决了这个问题就可以了呀

只是组件名字不同, 里面内容完全一致

从这句话想到了什么吗? 对! 就是这个 Vue 内置组件: component

我们完全可以去掉那些 v-if 判断, 而只使用 component 即可, 这样可扩展性问题也解决了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<el-form v-bind="$attrs" :model="model">
<el-form-item v-for="item in formItems" :key="item.prop">
<component :is="item.type" v-model="model[item.prop]" v-bind="item">
<el-option
v-for="option in item.select"
:key="option.value"
:label="option.label"
:value="option.value"
>
</el-option>
<el-radio
v-for="radio in item.radio"
:key="radio[item.value]"
:label="radio[item.value]"
>
{{ radio[item.label] }}
</el-radio>
</component>
</el-form-item>
</el-form>

是不是一下就简单多了?

那么现在 item.type 就是组件的名字了, 比如 el-input; 关于 el-select 这类还需要有选择数据的组件(el-option, el-radio 等), 我目前是全部放到 component 里面的, 一来是不传这些选择数组的时候, 就不会渲染, 二来是就算渲染了, 组件内部 slot 如果不接收, 也不会显示出来, 三呢就是这些组件已经在项目中使用了, 改动的话有点麻烦, 所以如果是新项目, 这些也可以使用 component

form 的改完了, table 是不是也可以使用这个呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<el-table v-bind="$attrs" v-on="$listeners">
<template v-for="(column, index) in columns">
<el-table-column v-if="column.editable" :key="column.prop" v-bind="column">
<component
slot-scope="{ row }"
:is="column.type"
v-model="row[column.prop]"
v-bind="column"
>
<el-option
v-for="option in column.select"
:key="option.value"
:label="option.label"
:value="option.value"
>
</el-option>
<el-radio
v-for="radio in column.radio"
:key="radio[column.value]"
:label="radio[column.value]"
>
{{ radio[column.label] }}
</el-radio>
</component>
</el-table-column>
<el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column>
</template>
</el-table>

一想到这个组件可能被很多地方使用, 那咱们是不是也需要将这个可编辑的组件也封装为一个组件呢?

封装可编辑组件

这个就没啥好说的了, 就是把上面的拿下来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<component :is="item.type" v-model="model[item.prop]" v-bind="item">
<el-option
v-for="option in item.select"
:key="option.value"
:label="option.label"
:value="option.value"
>
</el-option>
<el-radio
v-for="radio in item.radio"
:key="radio[item.value]"
:label="radio[item.value]"
>
{{ radio[item.label] }}
</el-radio>
</component>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name: 'EditableElements',
props: {
item: {
type: Object,
default() {
return {}
}
},
model: {
type: Object,
default() {
return {}
}
}
}

优化一下:

上面只有 radio, 其实 checkbox 也是类似的, 所以对于这类组件应该也使用 component 处理, 而且 valuelabel 也需要做一下映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<template>
<component
:is="item.component"
v-model="model[item.prop]"
:key="item.prop"
v-bind="item"
v-focus="item.focus"
:placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`"
v-on="{ ...$listeners, ...item.events }"
>
<!-- 只展示 -->
<text-ellipsis v-if="item.type === 'info'" :content="model[item.prop]"></text-ellipsis>
<!-- 这里由于当初设计的时候以为 el-select 和 el-radio 等会同时存在当前组件中,
所以规定 el-select 的下拉数据放在 select 中, el-radio 的放在 radio 中,
其实 el-select 和 el-radio 等不会同时存在,
为了 兼容以前写法 和 统一数据结构
现在增加一个参数: options, 存放供选择的数据, 所有通用
当然如果是新项目, 可以去掉 || 后面的代码, 统一使用 options -->
<template v-if="item.type === 'select'">
<el-option
v-for="option in item.options || item.select"
:key="option[listProps.value]"
:value="option[listProps.value]"
:label="option[listProps.label]"
:disabled="option.disabled"
></el-option>
</template>
<!-- radio / checkbox 等 -->
<template v-if="list.includes(item.type)">
<component
:is="`el-${item.type}`"
v-for="option in item.options || item[item.type]"
:key="option[listProps.value]"
:label="option[listProps.value]"
:disabled="option.disabled"
>
{{ ele[listProps.label] }}
</component>
</template>
<slot v-for="(value, key) in item.slots" :name="key" :slot="key">{{ value }}</slot>
</component>
</template>

<script>
import { handlePlaceholder } from 'utils'

export default {
name: 'EditableElements',
inheritAttrs: false,
props: {
item: {
type: Object,
default() {
return {}
}
},
model: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
list: ['radio', 'checkbox']
}
},
computed: {
listProps() {
// 这里的 props 和上面的 options 是相同的原因
const props = this.item.props || this.item[`${this.item.type}Props`]
if (!props) return { label: 'label', value: 'value' }
const { label = 'label', value = 'value', ...rest } = props
return {
label,
value,
...rest
}
}
},
methods: {
handlePlaceholder
},
directives: {
focus: {
// [vue v-focus v-show控制input的显示聚焦,第二次不生效问题_JavaScript_宣城-CSDN博客](https://blog.csdn.net/qq_37361812/article/details/93782340)
// [页面一刷新让文本框自动获取焦点-- 和自定义v-focus指令 - 明月人倚楼 - 博客园](https://www.cnblogs.com/IwishIcould/p/12006378.html)
update(el, { value, oldValue }) {
if (value && value !== oldValue) {
// 重点注意这里 当前元素是 div 所以要查到子元素中的 input
const dom = el.querySelector('input') || el.querySelector('textarea')
dom && dom.focus()
}
},
inserted(el, { value }) {
if (value) {
// 重点注意这里 当前元素是 div 所以要查到子元素中的 input
const dom = el.querySelector('input') || el.querySelector('textarea')
dom && dom.focus()
}
}
}
}
}
</script>

<style lang="less" scoped>
.editable-elements {
.el-select {
width: 100%;
}
}
</style>

终稿

到这里本次封装之旅差不多就结束了, 想想还有点小激动呢!

闲话不多说, 加了亿点点小细节, enjoy

这里是源码: BaseForm, BaseTable, EditableElements

以下为简略代码:

form

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<el-form v-bind="$attrs" :model="model" ref="elForm" class="base-form">
<slot name="prev"></slot>
<el-form-item
v-for="item in items"
:key="item.prop"
v-bind="item"
:rules="[
{
required: !item.noRequired,
message: item.ruleMessage || `${handlePlaceholder(item.type)}${item.label}`,
trigger: item.type === 'select' ? 'change' : 'blur'
},
...(rules[item.prop] || [])
]"
>
<editable-elements :model="model" :item="item" v-on="$listeners"></editable-elements>
</el-form-item>
<slot></slot>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
props: {
keyProps: {
type: Object,
default() {
return null
}
},
model: {
type: Object,
default() {
return {}
}
},
formItems: {
type: Array,
default() {
return []
}
},
rules: {
type: Object,
default() {
return {}
}
}
},
computed: {
items() {
return this.keyProps
? this.formItems.map(item => ({
...item,
prop: item[this.keyProps.prop || 'prop'],
label: item[this.keyProps.label || 'label']
}))
: this.formItems
}
}

table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<el-table ref="elTable" class="base-table" v-bind="$attrs" v-on="$listeners">
<slot name="prev"></slot>
<template v-for="(column, index) in cols">
<el-table-column v-if="column.editable" :key="column.prop" v-bind="column">
<editable-elements
slot-scope="{ row, $index }"
:model="row"
:item="{ ...column, focus: index === focusCol && $index === focusRow }"
@change="change(row, $event, column)"
></editable-elements>
</el-table-column>
<el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column>
</template>
<slot></slot>
</el-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
props: {
keyProps: {
type: Object,
default() {
return null
}
},
columns: {
type: Array,
default() {
return []
}
},
focusRow: {
type: Number,
default: 0
},
focusCol: {
type: Number,
default: 0
}
},
computed: {
cols() {
return this.keyProps
? this.columns.map(column => ({
...column,
prop: column[this.keyProps.prop || 'prop'],
label: column[this.keyProps.label || 'label']
}))
: this.columns
}
}

editable-elements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<component
:is="item.component"
v-model="model[item.prop]"
:key="item.prop"
v-bind="item"
v-focus="item.focus"
:placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`"
v-on="{ ...$listeners, ...item.events }"
>
<!-- 只展示 -->
<text-ellipsis v-if="item.type === 'info'" :content="model[item.prop]"></text-ellipsis>
<template v-if="item.type === 'select'">
<el-option
v-for="option in item.options || item.select"
:key="option[listProps.value]"
:value="option[listProps.value]"
:label="option[listProps.label]"
:disabled="option.disabled"
></el-option>
</template>
<!-- radio / checkbox 等 -->
<template v-if="list.includes(item.type)">
<component
:is="`el-${item.type}`"
v-for="option in item.options || item[item.type]"
:key="option[listProps.value]"
:label="option[listProps.value]"
:disabled="option.disabled"
>
{{ option[listProps.label] }}
</component>
</template>
<slot v-for="(value, key) in item.slots" :name="key" :slot="key">{{ value }}</slot>
</component>
</template>

增加功能

BaseTable 增加 defaultCheckedKeys 和 currentNodeKey

最近做项目的时候遇到 table 初始化的时候要有默认选中和默认高亮行的需求, 那就参照 el-tree 的方式做一下

本以为和 el-tree 一样, 传入 key 后, el-table 内部会自己处理, 结果 el-table 只能通过传入的 row 来实现默认选中和默认高亮行, 这也不能理解, 毕竟 el-table 中很少用到 row-key

那咱们就通过传入的 key 遍历找到对应的 row, 当然这种方式就必须传入 row-key 啦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
props: {
data: {
type: Array,
default() {
return []
}
},
defaultCheckedKeys: {
type: Array,
default() {
return []
}
},
currentNodeKey: {
type: [String, Number],
default: ''
}
},
watch: {
data: {
handler() {
this.setDefaultCheckedKeys()
this.setCurrentNodeKey()
},
immediate: true
},
rowKey: {
type: [String, Function],
default: 'id'
},
defaultCheckedKeys: {
handler: 'setDefaultCheckedKeys',
immediate: true
},
currentNodeKey: {
handler: 'setCurrentNodeKey',
immediate: true
}
},
methods: {
// 设置默认选中
setDefaultCheckedKeys() {
this.$nextTick(() => {
if (this.defaultCheckedKeys.length) {
const rows = this.data.filter(item => this.defaultCheckedKeys.includes(item[this.rowKey]))
rows.forEach(row => {
this.$refs.elTable.toggleRowSelection(row, true)
})
} else {
this.$refs.elTable.clearSelection()
}
})
},
setCurrentNodeKey() {
this.$nextTick(() => {
if (this.currentNodeKey) {
const row = this.data.find(item => this.currentNodeKey === item[this.rowKey])
if (row) {
this.$refs.elTable.setCurrentRow(row)
}
} else {
this.$refs.elTable.setCurrentRow(null)
}
})
}
}

实际工作中发现的问题

改变 editable 后页面没变化

最近项目增加了权限控制, 一些可编辑的表格需要先判断权限, 有权限才可以编辑.

所以需要动态修改 editable 的值, 初始为 false, 后续根据权限改变值.

但发现明明有权限, 却还是不可编辑状态, 而且跟踪代码也发现值确实改变了, 但页面却没有改变

只有触发页面重绘后才会变成可编辑状态, 就好像是状态其实已经改变了, 就差最后的绘制了

这时候突然想到是不是表格复用了, 仍然用的是不可编辑状态的表格: 利用 v-if 动态渲染表格时, 在 el-table-column 中添加 key 属性防止表格复用

查看代码发现果然是复用了: 可编辑和不可编辑用的相同的 key

1
2
3
4
5
6
7
8
9
10
<el-table-column
v-if="column.editable"
:key="column.prop"
v-bind="column">
</el-table-column>
<el-table-column
v-else
:key="column.prop"
v-bind="column">
</el-table-column>

当初封装组件的时候想着他俩肯定只能存在一个, 用相同的 key 应该没什么问题, 却忘了切换 editable 后, 相同的 key 会复用的问题了

那稍微修改一下吧:

1
2
3
4
5
6
7
8
9
10
<el-table-column
v-if="column.editable"
:key="`${column.prop}-edit`"
v-bind="column">
</el-table-column>
<el-table-column
v-else
:key="column.prop"
v-bind="column">
</el-table-column>

所以以后尽量还是用不同的 key 吧, 即使是 if else.

可编辑 table 有时候没有滚动条

引起这个问题主要有两个条件:

  1. table 中有可编辑组件
  2. 内容高度刚好超出设置高度一点点

当有可编辑组件时, tr 的高度比不可编辑时高了一点, 而 el-table 还是按不可编辑状态计算高度, 从而认为当前内容并没有超出设置高度, 所以没有出现滚动条

所以解决方法有两种:

要么将 tr 的高度设死, 这样可编辑和不可编辑高度一样, 就可以了; 不过样式可能不算美观

要么让 el-table 重新计算一下高度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
computed: {
isEditable() {
return this.columns.some(item => item.editable || item.editableMethod)
}
},
watch: {
data: {
handler() {
this.setDefaultCheckedKeys()
this.setCurrentNodeKey()
this.refreshLayout()
},
immediate: true
}
},
methods: {
refreshLayout() {
if (!this.isEditable) return
this.$nextTick(() => {
setTimeout(() => {
this.$refs.elTable.doLayout()
}, 200)
})
}
}

 评论

 无法加载Disqus评论系统,请确保您的网络能够正常访问。