Vue2+ElementUI列表组件的封装
前言
本文要实现的表格组件功能包括是否有序号、是否可以勾选、是否需要多选、日期格式转换、字典值进行转换、金额进行千分符转换、是否需要小计、合计、是否需要设置列功能、是否不许拖动列宽、操作栏宽度、操作栏文本这是都可以通过父组件中进行设置,来满足表格的需求。
引言
在日常开发中,我们经常会遇到需要展示列表数据的场景。ElementUI 提供的 el-table 组件是一个功能强大的表格组件,可以满足大部分的需求。但是,在实际应用中,我们往往需要根据业务需求对 el-table 组件进行二次封装,以提高开发效率和代码的复用性。
本文将介绍 Vue2+ElementUI 列表组件的封装方法,包括:
- 封装思路
- 常见功能的实现
- 代码示例
- 注意点
封装思路 封装 el-table 组件的思路是将其作为一个独立的组件,对外暴露必要的属性和方法,并通过插槽机制来定制表格的内容。
具体来说,我们可以将 el-table 组件的封装分为以下几个步骤:
- 定义组件的 props,用于接收外部传入的数据和配置。
- 在组件内部,使用 el-table 组件来渲染表格。
- 通过插槽机制来定制表格的内容,例如表头、表尾、操作列等。
- 暴露一些方法,用于控制表格的操作,例如刷新、排序、筛选等。
创建组件模板并测试使用
创建组件,名为:V2yuTableComp
,这是子组件
js
<template>
<div class="customer-table-container">
<el-table
ref="table"
v-loading="loading"
:row-key="rowKey"
:data="list"
stripe
border
:class="['autoWidth-table', !ifMultiple ? 'single-select-table' : '']"
:span-method="spanMethod"
@header-dragend="changeColumnWdith"
@selection-change="handleSelectionChange"
>
<el-table-column
v-if="ifCheck"
type="selection"
width="55"
align="center"
/>
<el-table-column
v-if="ifIndex"
type="index"
label="序号"
width="80"
fixed="left"
class-name="customer-table-index"
>
<template slot-scope="scope">
<span>
{{ setFirstColumn(scope.$index) }}
</span>
</template>
</el-table-column>
<template v-for="column in visibleColumns">
<CustomerTableColumn
:key="column.prop"
:column="column"
:list="list"
:copyDict="copyDict"
:ifDragCol="ifDragCol"
>
<template v-slot:[column.prop]="slotProps">
<slot
:name="column.prop"
:prop="column.prop"
:row="slotProps.scope.row"
:index="slotProps.scope.$index"
:originScope="slotProps.scope"
></slot>
</template>
</CustomerTableColumn>
</template>
<el-table-column
v-if="operatorWidth > 0"
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
:width="operatorWidth"
>
<template slot="header">
<span>{{ operatorLabel }}</span>
<ColumnsSettings
v-if="ifSetting"
ref="ColumnsSettings"
:columns="columns"
></ColumnsSettings>
</template>
<template slot-scope="scope">
<slot
v-if="scope.$index < page.pageSize"
:row="scope.row"
:index="scope.$index"
:originScope="scope"
></slot>
</template>
</el-table-column>
</el-table>
<el-button
v-if="!ifNotExport"
type="warning"
plain
icon="el-icon-download"
size="mini"
class="export-button"
@click="handleExport"
>导出</el-button
>
<el-dialog
:visible.sync="exportDialog"
class="export-loading-dialog"
width="200px"
>
<div class="export-loading">
<div v-loading="exportLoading"></div>
<div class="blue">文件下载中...</div>
</div>
</el-dialog>
</div>
</template>
<script>
import { add, bignumber } from "mathjs";
import { exportBlob } from "@/utils/common";
export default {
data() {
return {
ifGetSummary: false, // 是否已获得过合计数据
exportDialog: false,
exportLoading: false,
};
},
props: {
// 字典,父组件固定传dict
copyDict: {
type: Object,
},
rowKey: {
type: String,
default: "id",
},
// 分页,用于序号相关
page: {
type: Object,
default() {
return {
pageSize: 50,
pageNum: 1,
};
},
},
exportPage: {
type: Object,
default() {
return {
pageSize: 100000,
pageNum: 1,
};
},
},
// 其它查询条件,主要用于导出
query: {
type: Object,
default: () => ({}),
},
// 是否至少含有一项查询条件才可以导出
queryNotnull: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
list: {
type: Array,
required: true,
},
columns: {
type: Array,
required: true,
},
// 操作栏宽度
operatorWidth: {
type: Number,
required: true,
},
// 操作栏文本
operatorLabel: {
type: String,
default: "操作",
},
// 是否不许拖动列宽
ifDragCol: {
type: Boolean,
default: true,
},
// 是否需要选择
ifCheck: {
type: Boolean,
default: false,
},
// 是否需要序号
ifIndex: {
type: Boolean,
default: true,
},
// 是否需要多选
ifMultiple: {
type: Boolean,
default: false,
},
// 是否需要设置列功能
ifSetting: {
type: Boolean,
default: false,
},
menuId: {
type: String,
},
// 是否需要小计功能,小计为前端根据当前表计算
ifSubtotal: {
type: Boolean,
default: false,
},
subtotalLabel: {
type: String,
default: "小计",
},
// 是否需要合计功能,合计为后台根据库表计算
ifSummary: {
type: Boolean,
default: false,
},
summaryLabel: {
type: String,
default: "合计",
},
// 是否不需要导出功能
ifNotExport: {
type: Boolean,
default: false,
},
// 合计行或合计列方法
spanMethod: {
type: Function,
},
// 导出的回调接口
exporter: {
type: Function,
},
},
computed: {
visibleColumns() {
return this.ifSetting
? this.columns.filter((column) => column.visible)
: this.columns;
},
},
watch: {
loading(newVal, oldVal) {
// loading由true变为false表示请求完毕,列表加载完成
const subtotalObj = {};
if (this.ifSubtotal && !newVal && oldVal) {
this.columns.forEach((col) => {
// 需要小计的列
if (col.ifSubtotal) {
const sum = this.list.reduce((accumulator, row) => {
// return accumulator + row[col.prop];
return add(
bignumber(accumulator),
bignumber(!row.ifExcept ? row[col.prop] : 0)
);
}, 0);
subtotalObj[col.prop] = sum.toFixed(2);
} else {
subtotalObj[col.prop] = null;
}
});
this.list.push(subtotalObj);
}
if (this.ifSummary && !newVal && oldVal) {
let mockObj = {};
if (!this.ifGetSummary) {
// 模拟请求合计
setTimeout(() => {
mockObj = subtotalObj;
this.list.push(mockObj);
this.ifGetSummary = true;
console.log(mockObj);
console.log("模拟请求合计");
}, 500);
} else {
mockObj = subtotalObj;
this.list.push(mockObj);
}
}
},
},
methods: {
changeColumnWdith(newWidth, oldWidth, currentCol) {
for (let i = 0; i < this.columns.length; i++) {
if (this.columns[i].prop === currentCol.property) {
this.$set(this.columns[i], "width", newWidth);
break;
}
}
const widthColumns = this.columns.map(({ prop, width }) => {
return {
prop,
width,
};
});
if (this.ifDragCol) {
localStorage.setItem(
this.menuId || this.$route.fullPath,
JSON.stringify(widthColumns)
);
}
},
exportPageWithE() {
return {
...this.exportPage,
E: 1,
};
},
handleExport() {
if (this.queryNotnull) {
let notEmpty = false;
const keys = Object.keys(this.query);
for (let i = 0; i < keys.length; i++) {
const value = this.query[keys[i]];
if (!Array.isArray(value) && !this.getIfEmpty(value)) {
notEmpty = true;
break;
}
if (Array.isArray(value) && value.length > 0) {
notEmpty = true;
break;
}
}
if (!notEmpty) {
this.$message.warning("至少选择一项查询条件");
return;
}
}
const exportParams = Object.assign(
{
expColumns: this.columns,
},
this.query
);
this.exportLoading = true;
this.exportDialog = true;
this.exporter(this.exportPageWithE(), exportParams)
.then(async (res) => {
const message = await exportBlob(res);
this.$message.success(message);
this.$emit("exportSuccess");
})
.catch((error) => {
console.log(error);
this.$message.error("导出失败");
this.$emit("exportFail", error);
})
.finally(() => {
this.exportLoading = false;
this.exportDialog = false;
});
},
handleSelectionChange(rows) {
if (!this.ifMultiple && rows.length > 1) {
this.$refs.table.toggleRowSelection(rows[rows.length - 2], false);
return false;
}
if (!this.ifMultiple) {
this.$emit("selection-change", rows[rows.length - 1]);
} else {
this.$emit("selection-change", rows);
}
},
setFirstColumn(index) {
if (this.ifSubtotal && this.ifSummary && index == this.list.length - 2) {
return this.subtotalLabel;
} else if (
this.ifSubtotal &&
!this.ifSummary &&
index == this.list.length - 1
) {
return this.subtotalLabel;
} else if (this.ifSummary && index == this.list.length - 1) {
return this.summaryLabel;
} else {
return (this.page.pageNum - 1) * this.page.pageSize + index + 1;
}
},
},
created() {
this.ifSetting && this.getColumnSettings();
if (this.ifDragCol) {
let widthColumns = localStorage.getItem(
this.menuId || this.$route.fullPath
);
if (widthColumns) {
widthColumns = JSON.parse(widthColumns);
} else {
widthColumns = [];
}
widthColumns.forEach(({ prop, width }) => {
this.columns.forEach((col) => {
if (width && col.prop === prop && col.width != width) {
this.$set(col, "width", width);
}
});
});
}
},
};
</script>
<style scope lang="scss">
</style>
创建测试类并使用上面的组件,方便测试。注意:导入组件时注意路径。这是父组件。
js
<template>
<div class="container" style="min-height: 100%; padding-bottom: 100px;">
<V2yuTableComp</V2yuTableComp>
</div>
</template>
<script>
import V2yuTableComp from "../../components/table/V2yuTableComp";
export default {
props: [],
components: {
V2yuTableComp
},
data() {
return {}
},
watch: {},
computed: {},
beforeCreate() {
},
created() {
},
beforeMount() {
},
mounted() {
},
beforeUpdate() {
},
updated() {
},
destroyed() {
},
methods: {},
}
</script>
<style scoped>
.container {
}
</style>
父、子组件传值
父组件编写表格对象并传递给子组件。
js
<V2yuTableComp :data="tableObj"></V2yuTableComp>
data() {
return {
// 这个对象可以从后端获取,下面是一些示例数据
list:[],
// 数据列 - 即el-table-column组件中的属性
columns: [
{prop: 'date', label: '日期', visible: true, ifDate:true},
{prop: 'name', label: '名字',visible: true, },
{prop: 'address', label: '地址',visible: true, },
{prop: 'money', label: '车费',visible: true, ifMoney:true},
]
}
}
效果图如下:
总结
在开发过程中我们最常用的就是表单和表格,所以对表格组件的封装可以提高代码复用性和开发效率。下面是关于封装 Vue 2 和 Element UI 列表和表格组件的一些总结和最佳实践:
-
封装表格组件:
- 创建一个可复用的表格组件,接受数据和列定义作为 props。
- 使用 Element UI 的
<el-table>
和<el-table-column>
组件来渲染表格。 - 根据列定义动态生成表格的列。
- 提供插槽以支持自定义表格内容,例如自定义表头或单元格内容。
- 添加排序、筛选、分页等功能,可以使用 Element UI 提供的相应组件或自定义实现。
通过上述代码,在使用表格时大大提高了开发效率,在父组件中使用时可以通过配置来满足该功能的需求。