简介
在后台管理系统开发中,表格是一个不可或缺的核心组件,针对不同的业务需求,往往需要编写大量相似的表格代码,这降低了开发效率,也增加了维护的成本。
接下来,我将介绍如何使用Vue3来封装一个可配置的表格组件,提高开发效率和代码质量。
【主要解决了一下几个痛点】
- 通过json配置:支持通过json配置表格,并且支持拓展组件的所有props和event。
- 搜索表单和表格查询的联动:当搜索表单点击确认搜索时,表格自动更新展示数据。
- 自动处理分页:协商统一的接口数据格式,在组件内部自动处理分页数据。
- 自定义插槽: 支持自定义表格样式和功能,包括表头、内容、搜索插槽等。
- 填充url参数:自动填充url参数到搜索表单,实现返回后,仍能保持历史查询条件更新表格数据。
示例
我们先看一下简单的使用示例:options
为表格的配置,columns
为表格的列
html
<template>
<div class="container">
<ProTable :options="tableOptions" :columns="tableColumns">
<template #tools>
<el-space :alignment="'normal'">
<el-button type="primary">新增</el-button>
<el-button>导出</el-button>
</el-space>
</template>
<template #name-default="{ row }">
{{ row?.name }}
</template>
<template #name-header>自定义表头</template>
<template #operations-default>
<el-space>
<el-button type="danger" plain size="small">删除</el-button>
<el-button type="primary" size="small">编辑</el-button>
</el-space>
</template>
</ProTable>
</div>
</template>
实现效果:
配置
表格配置项:支持传入请求api或者直接是表格的数据。
javascript
// 表格数据
const tableData = [
{ id: 1, name: "zs", address: "广东省惠州市" },
{ id: 2, name: "ls", address: "广东省深圳市" },
{ id: 3, name: "ww", address: "广东省广州市" },
]
直接传入data数据
javascript
// 直接传入data数据
const tableOptions = reactive({
data: tableData
});
传入请求api:支持有分页或无分页的接口请求,并且分页数据结构可以与后端人员协商统一字段返回。
javascript
// 直接请求api
// 带分页
const tableOptions = reactive({
api: () => {
return new Promise(resolve => {
resolve({
success: true,
data: {
list: tableData,
pagination: {
page: 1,
pageSize: 10,
total: tableData.length,
},
}
});
})
}
});
// 不带分页
const tableOptions = reactive({
api: () => {
return new Promise(resolve => {
resolve({
success: true,
data: tableData
});
})
}
});
表格列:支持表格列的所有属性和事件。支持隐藏列、配置查询表单,查询默认值,支持自定义表头和插槽。
javascript
const tableColumns = [
{
prop: "status",
label: "状态",
formatter: () => "正常",
search: true,
searchDefaultValue: "0",
searchType: "select",
searchProps: {
placeholder: "请选择",
options: statusOptions,
},
},
{
prop: "name",
label: "姓名",
formatter: (row) => row.name,
search: true,
searchProps: {
placeholder: "请输入",
},
},
{
prop: "age",
label: "年龄",
formatter: () => "12",
},
{
prop: "sex",
label: "性别",
formatter: () => "男",
},
{
prop: "address",
label: "地址",
search: true,
searchDefaultValue: [],
searchType: "cascader",
searchProps: {
placeholder: "请选择",
options: cascaderOptions,
},
},
{
prop: "created_at",
label: "创建时间",
formatter: () => "2024-03-25 12:00:00",
search: true,
searchType: "datePicker",
searchProps: {
placeholder: "请选择",
type: "daterange",
startPlaceholder: "开始时间",
endPlaceholder: "结束时间",
valueFormat: "YYYY-MM-DD",
},
},
{ prop: "operations", label: "操作", width: 200, align: "center" },
];
ProTable
javascript
<template>
<div v-loading="loading">
<el-table :data="tableData" border stripe v-bind="options.tableProps">
<el-table-column
v-for="column in columns"
:key="column.prop"
v-bind="column"
>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { defineProps, ref } from "vue";
const props = defineProps({
columns: {
type: Array,
required: false,
},
options: {
type: Object,
default: () => ({
tableProps: {},
}),
},
});
const loading = ref(false);
const tableData = ref([]);
</script>
<style lang="less" scoped></style>
处理隐藏的列
在传入的column
中,支持传入hide
参数,当hide=true
时,隐藏改列。
javascript
<el-table-column
v-for="column in visibleColumns"
>
...
</el-table-column>
过滤隐藏的列
javascript
// 过滤隐藏的列
const visibleColumns = computed(() => {
return props.columns.filter((column) => !column.hide);
});
分页处理
为了实现分页功能,我们在组件内部定义了 pagination
分页参数,并通过 v-bind
绑定到 options.paginationProps
,从而支持所有的 el-pagination
属性和事件。
html
<div v-loading="loading">
...
<el-pagination
background
style="margin-top: 20px"
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 30, 40, 50]"
layout="prev, pager, next, sizes, jumper, total"
v-bind="options.paginationProps"
/>
</div>
javascript
const pagination = reactive({
page: 1,
pageSize: 10,
total: 0,
});
获取表格数据
获取请求参数 :通过options.requestParams
可以传递请求时的自定义参数,拼接分页参数进行接口查询。
javascript
// 获取请求参数
const getRequestParams = () => {
const { page, pageSize } = pagination;
const { options } = props;
// 合并分页参数和自定义参数
const params = Object.assign({ page, pageSize }, options.requestParams);
return params;
};
获取请求数据 :配置选项可以直接传入表格数据 data
,也可以传入表格请求的 api
,组件会优先选择使用表格数据。
javascript
// 获取表格数据
const getTableData = async () => {
const { options } = props;
const params = getRequestParams();
// 过滤 undefined和null
Object.keys(params).forEach((key) => {
if (typeof params[key] === "undefined" || params[key] == null) {
delete params[key];
}
});
if (options.data) {
// 优先接收传入的data
tableData.value = options.data;
pagination.total = options.data.length;
} else if (options.api) {
// 请求传入的api
loading.value = true;
const { success, data } = await options.api(params);
loading.value = false;
if (!success) {
return;
}
handleResponseData(data);
}
};
处理请求数据 :api
支持直接返回所有数据,也可以返回分页数据。当返回的数据中包含分页信息时,表示为分页请求。您可以根据接口的格式自行进行修改。
javascript
// 处理请求数据
const handleResponseData = (data) => {
if ("pagination" in data) {
// 存在分页信息
tableData.value = data.list;
pagination.total = data.pagination.total;
} else {
// 不存在分页信息的情况
tableData.value = data;
pagination.total = data.length;
}
};
处理切换分页
html
<el-pagination
...
@change="handlePaginationChange"
/>
javascript
// 切换分页
const handlePaginationChange = () => {
getTableData();
};
自定义插槽 : 组件内部传入prop+-default
为自定义内容,prop+-header
为自定义表头,组件还额外支持了tools
插槽。
子组件:
html
<div v-if="$slots['tools']" style="margin-bottom: 20px">
<slot name="tools"></slot>
</div>
<el-table :data="tableData" border stripe v-bind="options.tableProps">
<el-table-column
v-for="column in visibleColumns"
:key="column.prop"
v-bind="column"
>
<!-- 自定义插槽渲染-->
<template #default="scope" v-if="$slots[column.prop + '-default']">
<slot :name="column.prop + '-default'" v-bind="scope"></slot>
</template>
<!-- 自定表头插槽渲染-->
<template #header="scope" v-if="$slots[column.prop + '-header']">
<slot :name="column.prop + '-header'" v-bind="scope"></slot>
</template>
</el-table-column>
</el-table>
父组件调用:
html
<ProTable :options="tableOptions" :columns="tableColumns">
<template #tools>
<el-space :alignment="'normal'">
<el-button type="primary">新增</el-button>
<el-button>导出</el-button>
</el-space>
</template>
<template #name-default="{ row }"> 自定义内容"{{ row.name }}" </template>
<template #name-header>自定义表头</template>
<template #operations-default>
<el-space>
<el-button type="danger" plain size="small">删除</el-button>
<el-button type="primary" size="small">编辑</el-button>
</el-space>
</template>
</ProTable>
查询表单
如何使用:: searchProps为查询组件的props,可以设置查询默认值,组件类型。
javascript
const tableColumns = [
...
{
prop: "status",
label: "状态",
formatter: () => "正常",
search: true,
searchDefaultValue: "0",
searchType: "select",
searchProps: {
placeholder: "请选择",
options: statusOptions.value,
},
}
]
需要查询的列 :在传入的column
中,支持传入search
参数,当search=true
时,表示需要参与查询。
javascript
// 查询的列
const searchColumns = computed(() => {
return props.columns.filter((column) => column.search);
});
设置表单默认值 :在传入的column
中,支持传入searchDefaultValue
参数,该参数为查询时的默认值。
javascript
// ProTable.vue
const formValue = ref({});
// 设置表单默认值
const setFormDefaultValue = () => {
const target = {};
searchColumns.value.forEach((item) => {
let value = item.searchDefaultValue;
target[item.prop] = value;
});
formValue.value = target;
};
onMounted(() => {
setFormDefaultValue();
getTableData();
});
const updateFormValue = (value) => {
formValue.value = value;
getTableData();
};
定义查询表单组件SearchForm
- 表单目前仅支持
input
,select
,datePicker
,cascader
组件,如需其他组件,可自行添加。 - 支持重置表单内容
Javascript
<template>
<el-form
:model="form"
label-width="auto"
label-suffix=":"
class="pro-form"
>
<el-row :gutter="20">
<el-col :span="6" v-for="item in columns" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop">
<component
:is="comMap[item.searchType || 'input']"
style="width: 100%"
clearable
v-bind="item.searchProps"
v-model="form[item.prop]"
>
<template v-if="item.searchType === 'select'">
<el-option
v-for="option in item.searchProps.options"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</template>
<template v-if="item.searchType === 'cascader'" #default="{ data }">
<span>{{ data.label }}</span>
</template>
</component>
</el-form-item>
</el-col>
</el-row>
<div style="margin-top: 20px; text-align: center">
<el-space>
<el-button @click="handleResetSearchForm">重置</el-button>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-space>
</div>
</el-form>
</template>
<script setup>
import {
ElForm,
ElInput,
ElSelect,
ElDatePicker,
ElCascader,
} from "element-plus";
import { defineProps, ref, defineEmits, watch } from "vue";
const props = defineProps({
columns: {
type: Array,
required: false,
},
formValue: {
type: Object,
default: () => ({
tableProps: {},
}),
},
});
const emit = defineEmits(["updateFormValue"]);
const comMap = {
input: ElInput,
select: ElSelect,
datePicker: ElDatePicker,
cascader: ElCascader,
};
const form = ref({});
watch(
[() => props.formValue],
() => {
form.value = props.formValue;
},
{ immediate: true }
);
// 重置查询表单
const handleResetSearchForm = () => {
const map = {};
for (const item of props.columns) {
map[item.prop] = item.searchDefaultValue;
}
Object.keys(form.value).forEach((key) => {
form.value[key] = map[key];
});
handleSearch();
};
// 点击搜索
const handleSearch = () => {
const params = {};
// 过滤空字符串
Object.keys(form.value).forEach((key) => {
params[key] = form.value[key];
});
emit("updateFormValue", params);
};
</script>
<style scoped lang="less"></style>
调整ProTable组件
使用查询表单
javascript
// ProTable.vue
<SearchForm
:columns="searchColumns"
:form-value="formValue"
style="margin-bottom: 20px"
v-if="searchColumns.length"
@update-form-value="updateFormValue"
/>
携带查询参数
javascript
// ProTable.vue
// 获取请求参数
const getRequestParams = () => {
...
// 合并分页、查询参数和自定义参数
const params = Object.assign(
{ page, pageSize },
formValue.value,
options.requestParams
);
...
};
使用useUrlState
使用自定义的hook,useUrlState
实现刷新页面,也能保存页面查询条件。
感兴趣可以看我自定义hook的这篇文章Vue自定义Hook示例:useUrlState
javascript
// ProTable.vue
import useUrlState from "@/hoooks/useUrlState";
// 获取表格数据
const getTableData = async () => {
...
const params = getRequestParams();
setState(params);
...
};
onMounted(() => {
...
if (state.value?.page) {
pagination.page = +state.value?.page;
}
if (state.value?.pageSize) {
pagination.pageSize = +state.value?.pageSize;
}
...
});
// 设置表单默认值
const setFormDefaultValue = () => {
...
searchColumns.value.forEach((item) => {
let value = item.searchDefaultValue;
// 优先重url获取参数
if (state.value[item.prop]) {
value = state.value[item.prop];
}
target[item.prop] = value;
});
...
};
暴露方法
目前仅暴露了刷新表格的方法
javascript
const refresh = () => {
getTableData();
};
defineExpose({
refresh,
});
源代码
感兴趣的可以看看github上的源码,直接装包,然后运行可以看到运行结果。
总结
如果你对文章中的内容感兴趣,可以在 GitHub 上查看源代码。由于功能相对简单,你可以根据自己的业务需求进行修改。希望本文对你有所帮助。