Vue 表格封装 BasicTableVue
本文最后更新于:2020年12月30日 下午
场景
后台项目中大量使用表格,我们使用的 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
生命周期函数,请务必调用它!
自定义表格组件
/**
* 自定义表格组件
*/
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);
},
},
});
定义一些公共的实体
/**
* 分页信息,多次使用到所以定义一个公共的
*/
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
/**
* 基本的表格数据配置
*/
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
<!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 部分
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("这里想做一些自定义的初始化操作");
},
},
});
这里需要注意一些要点
- 如果需要在
data
中调用methods
中的函数,则data
必须是一个函数并返回对象- 不要直接重写
mounted()
生命周期函数,而是在重写的init()
中进行自定义操作- 任何实体都需要有
...args
属性以避免一些没有声明的属性找不到
那么,关于 BasicTableVue
的封装便到此结束了。这是一个相当简陋的封装,如果有什么更好的方式,后面也会更新。
Vue 表格封装 BasicTableVue
https://blog.rxliuli.com/p/90548a371a16435799bcbfae3e4dbfb6/