文章目的
在后台管理系统中,经常会有列表页,他们的长相基本一致,不同的就是搜索条件、表格列、搜索列等内容。为了提高工作效率我们经常会进行公共列表组件的封装。在网上找了很多资料,都不是很完整,也不是很契合。所以在这里记录一下自己的整合,以及封装好的通用表格组件。
核心技术点
Vue3+ElementPlus+TS+axios
核心思路
尽可能的封装的完善,只给使用着特定的使用方法,将接口的调用、默认值的设置等都通过配置的方式进行设置。以期做到开箱即用,不需要关注组件的具体实现效果。
上代码
组件用到的type
export type CustomConfig = {
defaultValue?: any; // 当前表单项的默认值
selectOptions?: { label: string; value: number | string }[];
url?: string; // 远程搜索select的请求地址
requestType?: string; // 远程搜索的请求类型
};
export const FormType = {
INPUT: "INPUT", // 输入框
SELECT: "SELECT", // 下拉选框
CASCADER: "CASCADER", // 级联选择器
DATEPICKER: "DATEPICKER", // 日期选择器,可以通过
};
export interface FormOption {
// type:当前搜索项的表单类型
type: string;
// 当前搜索表单项的属性名
field: string;
// props:elementplus组件支持的绑定属性
props: any;
// $queryFormatter:提供给父组件用于自定义请求参数,比如可远程搜索的select
$queryFormatter?: (T: any) => any;
// $responseFormatter:有时对于请求返回的数据需要自己处理,可以使用这个方法
$responseFormatter?: (T: any) => any;
// 自定义的一些其他搜索表单相关的信息:比如select的options;当前表单项的默认值等
customConfig: CustomConfig;
}
type RequestConfig = {
searchUrl?: string;
method?: "GET" | "POST";
pageNum: number;
pageSize: number;
otherSearchField?: any; // 不在搜索项展示的其他请求参数
};
export interface FormProps {
formOptions: FormOption[];
// 请求配置
requestConfig: RequestConfig;
}
export interface header {
// props是element plus表格列支持的属性
props: {
label: string;
prop: string;
[key: string]: string;
};
// 这是我们自定义的一些属性
config?: {
isCustom: boolean; // 是否支持自定义表格项;如果为true,那么我们以props中的prop作为具名插槽的name来进行渲染
};
}
export interface TableConfigType {
headers: header[]; // 表格列有哪些
tableData: any[]; // 表格数据
paginationConfig: any; // 分页配置
}
SearchForm组件
<template>
<el-form ref="searchFormRef" :model="searchForm" size="default">
<el-row :gutter="20">
<el-col
:xs="24"
:sm="24"
:md="24"
:lg="12"
:xl="6"
v-for="option in formOptions"
:key="option.field"
>
<el-form-item :label="option.props.label">
<!-- 处理输入框类型 -->
<template v-if="option.type === FormType.INPUT">
<el-input
v-model="searchForm[option.field]"
v-bind="option.props"
></el-input>
</template>
<!-- 处理级联选择器 -->
<template v-if="option.type === FormType.CASCADER">
<el-cascader
v-model="searchForm[option.field]"
v-bind="option.props"
/>
</template>
<!-- 处理下拉选框 -->
<template v-if="option.type === FormType.SELECT">
<el-select
v-model="searchForm[option.field]"
v-bind="option.props"
:remote-method="(query: string) => remoteMethod(query, option)"
fit-input-width
>
<el-option
v-for="item in option.customConfig.selectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<!-- 处理日期选择器 -->
<template v-if="option.type === FormType.DATEPICKER">
<el-date-picker
v-model="searchForm[option.field]"
v-bind="option.props"
/>
</template>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6">
<el-form-item>
<el-button type="default" size="default" @click="reset"
>重置</el-button
>
<el-button type="primary" size="default" @click="search"
>搜索</el-button
>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { FormProps, FormOption, FormType } from "./type";
import axios from "axios";
import { FormInstance } from "element-plus";
const props = defineProps<FormProps>();
// emits接口
interface Emits {
(e: "search", data: any): void;
(e: "reset"): void;
(e: "change"): void;
}
// emits
const emits = defineEmits<Emits>();
const searchForm = ref<any>({});
const searchFormRef = ref<FormInstance>();
// 搜索请求,将搜索出来的结果emit出去,供外部使用
const search = async () => {
if (!props.requestConfig.searchUrl) {
return false;
}
const params: {
pageNum: number;
pageSize: number;
[key: string]: any;
} = {
pageNum: props.requestConfig.pageNum,
pageSize: props.requestConfig.pageSize,
...searchForm.value,
...props.requestConfig.otherSearchField,
};
const { data } = await axios({
method: props.requestConfig.method,
url: props.requestConfig.searchUrl,
data: params,
});
// 将搜索到的结果返回给父组件,用于展示
emits("search", { total: parseInt(data.total), data: data.list });
};
// 重置表单,同时emit出去,方便父组件修改page
const reset = () => {
// 重置将searchForm初始化,防止中间过程有一些污染数据
searchForm.value = {};
searchFormRef.value?.resetFields();
props.formOptions.map((item) => {
searchForm.value[item.field] = item.customConfig.defaultValue || "";
});
emits("reset");
};
// 自定远程搜索逻辑
async function remoteMethod(query: string, item: FormOption) {
const {
customConfig: { url = "", requestType = "GET" },
$queryFormatter,
$responseFormatter,
} = item;
let params =
$queryFormatter && typeof $queryFormatter === "function"
? $queryFormatter(query)
: { name: query };
const data = await axios({
method: requestType,
url,
data: {},
params,
});
item.customConfig.selectOptions =
$responseFormatter && typeof $responseFormatter === "function"
? $responseFormatter(data)
: (data as unknown as Array<{ label: string; value: string }>);
}
onMounted(() => {
// 组件挂载时将搜索选项初始化
props.formOptions.forEach((option: FormOption) => {
// 如果有传入的默认值就赋值,否则默认给定空字符串
searchForm[option.field] = option?.customConfig?.defaultValue
? option?.customConfig?.defaultValue
: "";
});
});
// 暴露出去一些方法供父组件调用 searchForm暴露出去,父组件可直接修改一些属性,方便操作;search方法当父组件的页码改变时方便重新搜索
defineExpose({ reset, search, searchForm });
</script>
SearchTable组件
<template>
<div class="table">
<el-table :data="tableData">
<el-table-column
v-for="{ props, config } in headers"
:key="props.prop"
v-bind="props"
>
<template v-slot="{ row }">
<!-- 处理自定义数据展示:使用具名插槽和作用域插槽,将该行的数据传递出去 -->
<slot v-if="config?.isCustom" :name="props.prop" :row="row"></slot>
<!-- 非自定义,默认展示 -->
<span v-else>{{ row[props.prop] }}</span>
</template>
</el-table-column>
</el-table>
<el-pagination
style="justify-content: center; margin-top: 20px"
small
background
layout="prev, pager, next, jumper, total"
:total="paginationConfig.total"
@size-change="handlerSizeChange"
@current-change="handlerCurrentChange"
@prev-click="handlerCurrentChange"
@next-click="handlerCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { toRefs } from "vue";
import { header } from "./type";
interface TableConfigType {
headers: header[]; // 表格列有哪些
tableData: any[]; // 表格数据
paginationConfig: any; // 分页配置
}
const props = defineProps<TableConfigType>();
const emits = defineEmits(["size-change", "current-change"]);
const { headers, tableData, paginationConfig } = toRefs(props);
const handlerSizeChange = (size: number) => {
emits("size-change", size);
};
const handlerCurrentChange = (page: number) => {
emits("current-change", page);
};
</script>
<style lang="scss" scoped>
.table {
width: 100%;
}
</style>
搜索组件及表格组件的基本使用
<template>
<div class="page">
<div class="page-main">
<SearchForm
ref="searchFormRef"
v-bind="formOptions"
@reset="reset"
@search="search"
></SearchForm>
<SearchTable
v-bind="tableConfigs"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #sex="{ row }">
<el-tag v-if="row.sex === 1" type="success">男</el-tag>
<el-tag v-if="row.sex === 2" type="danger">女</el-tag>
</template>
<template #operator="{ row }">
<el-button type="primary" size="small" @click="edit(row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="del(row)">
删除
</el-button>
</template>
</SearchTable>
</div>
</div>
</template>
<script setup lang="ts" name="home">
import { defineAsyncComponent, ref } from "vue";
import { FormProps, FormType, TableConfigType } from "./type";
const SearchForm = defineAsyncComponent(() => import("./SearchForm.vue"));
const SearchTable = defineAsyncComponent(() => import("./SearchTable.vue"));
const searchFormRef = ref<InstanceType<typeof SearchForm>>();
const formOptions = ref<FormProps>({
formOptions: [
{
type: FormType.INPUT,
field: "name",
props: {
label: "姓名",
placeholder: "请输入姓名",
},
customConfig: {},
},
{
type: FormType.SELECT,
field: "status",
props: {
label: "状态",
placeholder: "请选择",
},
customConfig: {
selectOptions: [
{
label: "全部",
value: "",
},
{
label: "已通过",
value: 1,
},
{
label: "未通过",
value: 0,
},
],
},
},
{
type: FormType.CASCADER,
field: "department",
props: {
label: "所属部门",
placeholder: "请选择",
options: [
{
label: "一级部门",
value: 1,
children: [
{
label: "二级部门",
value: 2,
},
],
},
],
},
customConfig: {},
},
{
type: FormType.DATEPICKER,
field: "createTime",
props: {
label: "创建时间",
placeholder: "请选择",
},
customConfig: {},
},
],
requestConfig: {
searchUrl: "",
method: "POST",
pageNum: 1,
pageSize: 10,
},
});
const tableConfigs = ref<TableConfigType>({
headers: [
{
props: {
label: "姓名",
prop: "name",
},
config: {
isCustom: false,
},
},
{
props: {
label: "性别",
prop: "sex",
},
config: {
isCustom: true,
},
},
{
props: {
label: "操作",
prop: "operator",
},
config: {
isCustom: true,
},
},
],
tableData: [
{
name: "张三",
sex: 1,
},
{
name: "王五",
sex: 2,
},
],
paginationConfig: {
total: 0,
},
});
const reset = () => {
formOptions.value.requestConfig.pageNum = 1;
searchFormRef.value?.search();
};
const search: (data: any) => void = ({ total, data }) => {
tableConfigs.value.tableData = data;
tableConfigs.value.paginationConfig.total = total;
};
const sizeChange = (size: number) => {
formOptions.value.requestConfig.pageSize = size;
searchFormRef.value?.search();
};
const currentChange = (page: number) => {
formOptions.value.requestConfig.pageNum = page;
searchFormRef.value?.search();
};
const edit = (row: any) => {
console.log(row);
};
const del = (row: any) => {
console.log(row);
};
</script>
<style lang="scss" scoped>
.page {
width: 100%;
height: 100%;
&-main {
width: 100%;
height: 100%;
}
}
</style>
思路总结:
1. 将表格或搜索表单需要的操作尽可能封装,做到开箱即用。
2. 如果有定制化的需求,我们提供特定的api给用户自由发挥。
目前使用情况来看,这个组件能够满足我项目中的大部分场景,不过需要跟后端沟通好请求接口的数据格式,保持统一。毕竟我们封装本来的目的就是为了可复用嘛,如果接口变来变去封装也就没有意义了。这算是一个基础版本,根据不同的业务需求,可能组件也需要不断地修改丰富,大家可以根据自己的情况进行优化。
这是自己的一些小经验,大家感觉有不合适的地方欢迎评论区讨论,咱们共同进步。
编码不易点个关注再走咯!!!