Vue 表格封装 BasicTableVue

Vue 表格封装 BasicTableVue

场景

后台项目中大量使用表格,我们使用的 element-ui 中的表格并不足以满足吾辈的需求,而且使用起来重复的代码实在太多,所以吾辈便对数据表格进行了二次封装。

实现

API 列表

  • [el]: 绑定的选择器。默认为 '#app'
  • data: 数据对象
    • form: 搜索表单绑定对象
    • columns: 表格的列数组。每个列定义参考 TableColumn
    • [formShow]: 是否显示搜索表单
    • [page]: 分页信息,包含分页的数据。具体参考 Page
    • [selectedIdList]: 选中项的 id 列表
    • [fileSelectorShow]: 是否显示导入 Excel 的文件选择器
  • methods: 绑定的函数
    • createForm: 初始化 form 表单,主要是为了自定义初始化逻辑
    • getPage: 获取分页信息
    • exportFile: 导出文件
    • importFile: 导入文件
    • deleteData: 删除选择的数据
    • [init]: 初始化函数,如果可能请使用该函数而非重写 mounted 生命周期函数,该函数会在 mounted 中调用
    • [resetFile]: 重置导入选择的文件,必须为 input:file 绑定属性 ref="fileInput"
    • [searchPage]: 搜索分页信息
    • [resetPage]: 重置分页信息
    • [toggle]: 切换搜索表单显示
    • [selection]: 选择的 id
    • [changeSize]: 改变一页的大小
    • [goto]: 跳转到指定页数
    • [deleteSelected]: 删除选择的数据项
    • [showFileSelector]: 是否显示导入文件选择器
    • [initCommon]: 初始化功能,如果重写了 mounted 生命周期函数,请务必调用它!

自定义表格组件

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
/**
* 自定义表格组件
*/
Vue.component('my-table', {
/**
* 列
*/
props: {
columns: {
type: Array,
default: [],
},
data: {
type: Array,
default: [],
},
},
template: `<el-table
:data="data"
tooltip-effect="dark"
style="width: 100%"
border
@selection-change="handleSelectionChange"
>
<template v-for="column in columns">

<el-table-column
:type="column.type"
:prop="column.prop"
:label="column.title"
:align="column.align"
:sortable="column.sortable"
:width="column.width"
:formatter="column.formatter"
v-if="column.customComponent"
>
<!--suppress HtmlUnknownAttribute -->
<template #default="scope">
<!--这里将传递给模板当前行的数据-->
<slot :name="humpToLine(column.prop)" :row="scope.row"></slot>
</template>
</el-table-column>
<el-table-column
:type="column.type"
:prop="column.prop"
:label="column.title"
:align="column.align"
:sortable="column.sortable"
:width="column.width"
:formatter="column.formatter"
v-else
>
</el-table-column>
</template>

</el-table>`,
methods: {
handleSelectionChange(val) {
this.$emit('handle-selection-change', val)
},
humpToLine(data) {
return toLine(data)
},
},
})

定义一些公共的实体

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
/**
* 分页信息,多次使用到所以定义一个公共的
*/
class Page {
/**
* 构造函数
* @param {Number} current 当前页数,从 1 开始
* @param {Number} size 每页的数量
* @param {Number} total 数据总条数
* @param {Number} pages 数据总页数
* @param {Array} records 一页的数据记录
* @param {...Object} [args] 其他的参数项,这里只是为了避免遗漏
* @returns {Page} 分页对象
*/
constructor({
current = 1,
size = 10,
total = 0,
pages = 0,
records = [],
...args
} = {}) {
this.current = current
this.size = size
this.total = total
this.pages = pages
this.records = records
Object.assign(this, args)
}
}

/**
* 表格的列
*/
class TableColumn {
/**
* 格式化日期事件
* @param value 字段的值
* @returns {String|*} 格式化得到的日期时间字符串 TableColumn.datetimeFormat()
*/
static datetimeFormat(_row, _column, value, _index) {
return !value ? '' : rx.dateFormat(new Date(value), 'yyyy-MM-dd hh:mm:ss')
}

/**
* 构造函数
* @param {String} [prop] 字段名
* @param {String} [title] 标题
* @param {'selection'} [type] 列类型,可以设置为选择列
* @param {Boolean} [sortable=true] 排序方式
* @param {Number} [width] 宽度
* @param {'center'} [align='center'] 水平对齐方式
* @param {Function} [formatter] 格式化列
* @param {Boolean} [customComponent] 是否自定义组件
* @param {...Object} [args] 其他的参数项,这里只是为了避免遗漏
*/
constructor({
prop,
type,
width,
title,
sortable = true,
align = 'center',
formatter,
customComponent,
...args
} = {}) {
this.prop = prop
this.type = type
this.width = width
this.align = align
this.title = title
this.sortable = sortable
this.align = align
this.formatter = formatter
this.customComponent = customComponent
Object.assign(this, args)
}
}

定义一个 BasicTableVue 继承 Vue

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/**
* 基本的表格数据配置
*/
class BasicTableData {
/**
* 构造函数
* @param {Object} [form={}] 搜索表单,子类一般需要覆盖(不覆盖的话可能在 html 中没有提示)
* @param {Array<TableColumn>} [columns=[]] 列信息列表,子类必须覆盖
* @param {Boolean} [formShow=false] 是否显示搜索表单
* @param {Page} [page=new Page()] 分页信息,包含数据列表
* @param {Array} [selectedIdList=[]] 选择的列表 id
* @param {Boolean} [fileSelectorShow=false] 导入文件选择器是否需要
*/
constructor({
form = {},
columns = [],
formShow = false,
page = new Page(),
selectedIdList = [],
fileSelectorShow = false,
} = {}) {
this.form = form
this.columns = columns
this.formShow = formShow
this.page = page
this.selectedIdList = selectedIdList
this.fileSelectorShow = fileSelectorShow
}
}

/**
* 基本的表格方法
*/
class BasicTableMethods {
/**
* 构造函数
* @param {Function} createForm 初始化 form 表单,主要是为了自定义初始化逻辑
* @param {Function} getPage 获取分页信息,需要覆盖
* @param {Function} exportFile 导出文件,需要覆盖
* @param {Function} importFile 导入文件,需要覆盖
* @param {Function} deleteData 删除选择的数据,需要覆盖
* @param {Function} init 初始化函数,如果可能请使用该函数而非重写 mounted 生命周期函数,该函数会在 mounted 中调用
* @param {Function} [resetFile] 重置导入选择的文件,必须为 input:file 绑定属性 ref="fileInput"
* @param {Function} [searchPage] 搜索分页信息
* @param {Function} [resetPage] 重置分页信息
* @param {Function} [toggle] 切换搜索表单显示
* @param {Function} [selection] 选择的 id
* @param {Function} [changeSize] 改变一页的大小
* @param {Function} [goto] 跳转到指定页数
* @param {Function} [deleteSelected] 删除选择的数据项
* @param {Function} [showFileSelector] 是否显示导入文件选择器
* @param {Function} [initCommon] 初始化功能,如果重写了 mounted 生命周期函数,请务必调用它!
*/
constructor({
createForm = function() {
throw new Error('如果需要搜索条件,请重写 initForm() 方法')
},
getPage = async function(page, entity) {
throw new Error('如果需要自动分页,请重写 getPage() 方法')
},
exportFile = async function() {
throw new Error('如果需要导出数据,请重写 exportFile() 方法')
},
importFile = function() {
throw new Error('如果需要导入数据,请重写 importFile() 方法')
},
deleteData = async function(idList) {
throw new Error('如果需要删除数据,请重写 deleteData 方法')
},
init = async function() {},
resetFile = function() {
const $el = this.$refs['fileInput']
if (!$el) {
throw new Error(
'如果需要清空选择文件,请为 input:file 绑定属性 ref 的值为 fileInput',
)
}
$el.value = ''
},
searchPage = async function() {
try {
this.page = await this.getPage(this.page, this.form)
} catch (e) {
console.error(e)
await rxPrompt.dangerMsg('查询数据失败,请刷新页面')
}
},
resetPage = async function() {
this.form = this.createForm()
await this.searchPage()
},
toggle = function() {
this.formShow = !this.formShow
},
selection = function(data) {
this.selectedIdList = data.map(({ id }) => id)
},
changeSize = function(size) {
this.page.current = 1
this.page.size = size
this.searchPage()
},
goto = function(current) {
if (!current) {
current = this.page.current
}
if (current < 1) {
return
}
if (current > this.page.pages) {
return
}
this.page.current = current
this.searchPage()
},
deleteSelected = async function() {
const result = await this.deleteData(this.selectedIdList)
if (result.code !== 200 || !result.data) {
await rxPrompt.msg('')
return
}
// noinspection JSIgnoredPromiseFromCall
rxPrompt.msg('删除成功')
this.page.current = 1
await this.searchPage()
},
showFileSelector = function() {
this.fileSelectorShow = !this.fileSelectorShow
},
initCommon = async function() {
this.form = this.createForm()
this.searchPage()
},
} = {}) {
this.createForm = createForm
this.getPage = getPage
this.searchPage = searchPage
this.resetPage = resetPage
this.toggle = toggle
this.selection = selection
this.changeSize = changeSize
this.goto = goto
this.exportFile = exportFile
this.importFile = importFile
this.resetFile = resetFile
this.deleteData = deleteData
this.init = init
this.deleteSelected = deleteSelected
this.showFileSelector = showFileSelector
this.initCommon = initCommon
}
}

/**
* 基本的 vue 表格配置信息
*/
class BasicTableOption {
/**
* 构造函数
* @param {String} [el='#app'] 标签选择器
* @param {BasicTableData} data 数据
* @param {BasicTableMethods} methods 方法
* @param {Function} mounted 初始化方法
*/
constructor({
el = '#app',
data = new BasicTableData(),
methods = new BasicTableMethods(),
mounted = async function() {
await this.initCommon()
await this.init()
},
} = {}) {
this.el = el
this.data = data
this.methods = methods
this.mounted = mounted
}
}

/**
* 基本的表格 vue 类
*/
class BasicTableVue extends Vue {
/**
* 构造函数
* @param {BasicTableOption} option 初始化选项
* @param {BasicTableData|Function} option.data vue 的 data 数据,如果是 {@link Function} 类型,则必须返回 {@link BasicTableData} 的结构
* @param {BasicTableMethods} option.methods vue 中的 methods 属性
* @param {Function} option.mounted 初始化方法,如果覆盖则必须手动初始化表格
*/
constructor({ data, methods, mounted, ...args } = {}) {
//注:这里为了应对 data 既有可能是对象,又有可能是函数的情况
super(
_.merge(new BasicTableOption(), {
data: function() {
return _.merge(
new BasicTableData(),
typeof data === 'function' ? data.call(this) : data,
)
},
methods,
mounted,
...args,
}),
)
}
}

注:这里分开这么多的类是因为便于 IDE 进行提示

使用

下面简单的使用一下 BasicTableVue

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<main>
<h1>用户列表</h1>
<!-- 使用内置函数 toggle 切换表单是否显示 -->
<button @click="toggle">高级搜索</button>
<!-- 使用 formShow 属性控制表单是否显示 -->
<form v-show="formShow">
<div>
<label for="name">名字:</label>
<input v-model="form.name" name="name" type="text" />
</div>
<div>
<label for="age">年龄:</label>
<input v-model="form.age" name="age" type="number" />
</div>
<div>
<!-- 使用 searchPage 查询 -->
<button @click="searchPage">查询</button>
<!-- 使用 resetPage 重置条件并搜索 -->
<button @click="resetPage">重置</button>
</div>
</form>
<div>
<!--
分页数据绑定 page 对象的 records 属性
表格的列绑定 columns 属性(需要自定义覆盖)
选中的项需要将 selection 属性绑定到 @handle-selection-change 事件
-->
<my-table
:data="page.records"
:columns="columns"
@handle-selection-change="selection"
>
<!--
定义自定义操作列
scope 指代当前行的信息
-->
<template #operating="scope">
<span>
<!-- 将自定义的函数绑定到 @click.stop.prevent 上 -->
<button @click.stop.prevent="() => viewInfo(scope.row)">
查看信息
</button>
</span>
</template>
</my-table>
<!--
分页组件
将内置的属性或函数绑定到 el-pagination 组件上
changeSize(): 改变一页数据大小的函数
goto(): 跳转指定页的函数
page: 具体参考 Page 对象
-->
<el-pagination
background
@size-change="changeSize"
@current-change="goto"
:current-page="page.current"
:page-sizes="[10, 20, 30]"
:page-size="page.size"
layout="total, sizes, prev, pager, next, jumper"
:total="page.total"
>
</el-pagination>
</div>
</main>
<script src="/user-info.js"></script>
</body>
</html>

JavaScript 部分

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
class UserInfo {
constructor({ id, name, age, ...args }) {
this.id = id
this.name = name
this.age = age
Object.assign(this, args)
}
}

const app = new BasicTableVue({
data: {
columns: [
new TableColumn({ width: 30, type: 'selection' }),
new TableColumn({ prop: 'name', title: '姓名' }),
new TableColumn({ prop: 'age', title: '年龄' }),
new TableColumn({
prop: 'operating',
title: '操作',
customComponent: true,
}),
],
},
methods: {
createForm() {
return new UserInfo()
},
async getPage(page, entity) {
return await baseUserInfoApi.page(page, entity)
},
deleteData(idList) {
return baseCustomerApi.delete(idList)
},
viewInfo(row) {
forward('/user_info_detail', row)
},
async init() {
console.log('这里想做一些自定义的初始化操作')
},
},
})

这里需要注意一些要点

  1. 如果需要在 data 中调用 methods 中的函数,则 data 必须是一个函数并返回对象
  2. 不要直接重写 mounted() 生命周期函数,而是在重写的 init() 中进行自定义操作
  3. 任何实体都需要有 ...args 属性以避免一些没有声明的属性找不到

那么,关于 BasicTableVue 的封装便到此结束了。这是一个相当简陋的封装,如果有什么更好的方式,后面也会更新。