前言
在实际项目中,由于业务复杂性,组件不可封装得过于死板,因为个性化修改需求一直存在。但我们希望尽可能减少基础代码的编写,通过配置数据的方式使用组件,并且保留组件的灵活性。
NaiveUI的数据表格也是通过数组配置项配置。并且支持每列使用自定义渲染函数,我们下面封装的不仅支持渲染函数,还要支持注册列插槽。
毕竟代码一多渲染函数的写法写起来比模板慢很多。
本文的表格将仿照 NaiveUI 的数据表格进行封装,并封装 usePage 方法和 搜索栏 组件。
目标:基础代码的重复编写枯燥无聊且容易出错,提高编码效率,减少工作量,使开发过程更加愉快。
提示:最复杂的表单组件 CForm 的封装将在下一篇文章中讨论(使用数据配置+内置常用组件+保留插槽以满足定制化需求)
# 提升前端开发效率:Element Plus 封装CForm表单组件
封装实现后效果
完整在线项目代码(sendbox):codesandbox.io/p/devbox/el…
接下来封装我们自己的CTable
- 支持使用数据配置表格
- 支持表格列、表格列头使用渲染函数render
- 支持列使用插槽(优于渲染函数。设计逻辑是因为:当要使用到模板编写代码时,估计render的写法会繁琐)
- 支持传入分页器配置对象使用分页功能
- 表格保留原有功能
- 表格列保留原有功能
Ctable表格代码实现
使用defineComponent的方式编写而不用SFC的方式是因为这样编写更灵活。
import { ref, h, defineComponent } from "vue";
import { ElTable, ElTableColumn, ElPagination } from "element-plus";
import "./styles/Ctable.scss";
import { get as _get } from "lodash";
const tableProps = {
columns: {
type: Array,
default: () => [],
},
data: {
type: Array,
default: () => [],
},
pagination: {
type: Object,
default: () => {},
},
};
export default defineComponent({
props: tableProps,
setup(props: any, { attrs, slots, expose }: any) {
const $slots = slots;
const tableRef = ref<InstanceType<typeof ElTable>>();
expose({
tableRef,
});
return () =>
h("div", { class: "c_table" }, [
h(
ElTable,
{
ref: tableRef,
data: props.data,
border: true,
style: { width: "100%" },
headerRowClassName: "table_demo",
headerCellStyle: { background: "rgba(247, 248, 250, 1)" },
key: Math.random().toString(),
rowKey: (row: any) => row.id,
// 透传支持使用原有功能
...attrs,
},
{
default: () => {
return props.columns.map((item: any, index: number) => {
let itemProps = { ...item };
// 只扩展了 render和renderTitle属性
delete itemProps.render;
delete itemProps.renderTitle;
let slots: any = {};
// 表格列注册了插槽并传递了插槽(插槽优先于渲染函数)
if (item.slotName && $slots[item.slotName]) {
slots.default = (scope: any) => {
return $slots[item.slotName](scope.row);
};
} else if (!["selection", "index"].includes(item.type)) {
slots.default = (scope: any) => {
return item.render && Object.keys(scope.row).length
? item.render(scope.row)
: h("span", _get(scope.row, item.prop));
};
}
if (item.renderTitle) {
slots.header = (scope: any) => item.renderTitle(scope);
}
if (item.type == "index") {
console.log(Object.keys(slots));
}
return h(
ElTableColumn,
{
// 设置默认值
prop: item.prop,
label: item.label,
align: "center",
key: index,
//透传,同v-bind一样。 支持使用列原有功能
...itemProps,
},
slots,
);
});
},
empty: () =>
h("div", { class: "noData" }, [h("div", [h("div", "暂无数据")])]),
},
),
h("div", { class: "pagination" }, [
h(ElPagination, { ...props.pagination }),
]),
]);
},
});
usePage分页器代码实现
- 把分页组件需要的配置封装,通过usePage函数调用返回一个
分页器配置以及 传递给接口的分页参数。
import { reactive, watch } from 'vue'
// 分页
export const usePage = (actionSearch: any) => {
const pageState = reactive({
page: 1,
page_size: 10,
})
const pagination = reactive({
background: true,
total: 0,
pageSizes: [10, 20, 30, 40, 50],
currentPage: pageState.page,
pageSize: pageState.page_size,
layout: 'total, prev, pager, next, sizes',
onCurrentChange: (page: any) => {
pageState.page = page
actionSearch()
},
onSizeChange: (pageSize: any) => {
pageState.page = 1
pageState.page_size = pageSize
actionSearch()
},
})
watch([() => pageState.page, () => pageState.page_size], ([newPage, newPageSize]) => {
pagination.currentPage = newPage
pagination.pageSize = newPageSize
})
return { pageState, pagination }
}
CSearch 搜索栏代码实现
- 利用表单封装搜索栏。使用el-row布局(一行分成24列)
- 支持表单配置项传入props,给el-col使用,控制当前项所占列数
- 封装
时间范围选择器、输入框、下拉框 - 支持
suffixPre和suffix插槽
<template>
<el-form :model="formVal">
<el-row :gutter="10">
<el-col
v-bind="getColProps(item)"
v-for="(item, index) in props.options"
:key="index"
>
<el-form-item :label="item.label">
<template v-if="item.type === 'daterange'">
<el-date-picker
v-model="formVal[item.key]"
type="daterange"
range-separator="-"
start-placeholder="开始时间"
value-format="YYYY-MM-DD"
end-placeholder="结束时间"
:shortcuts="shortcutsOptions"
:clearable="item.clearable == undefined ? true : item.clearable"
@change="
(val: any) => handleDateChange(val, item.dict[0], item.dict[1])
"
/>
</template>
<template v-if="item.type === 'select'">
<el-select
v-model="formVal[item.key]"
:placeholder="item.tip ? item.tip : `请选择${item.label}`"
:clearable="item.clearable == undefined ? true : item.clearable"
>
<el-option
v-for="option in item.dict.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
<template v-if="item.type === 'input'">
<el-input
v-model="formVal[item.key]"
:placeholder="item.tip ? item.tip : `请输入${item.label}`"
:clearable="item.clearable == undefined ? true : item.clearable"
/>
</template>
</el-form-item>
</el-col>
<!-- 操作按钮区 -->
<el-form-item label="" class="operation">
<slot name="suffixPre"></slot>
<el-button type="primary" @click="handleSearch" class="btn_search">
<el-icon style="margin-right: 4px">
<Search />
</el-icon>
查询
</el-button>
<el-button @click="handleReset" class="btn">
<el-icon><RefreshRight /></el-icon>
重置
</el-button>
<slot name="suffix"></slot>
</el-form-item>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { shortcutsOptions } from "@/utils/options";
import { Search, RefreshRight } from "@element-plus/icons-vue";
const emits = defineEmits<{
(e: "search", form: any, page?: number): void;
}>();
const props = withDefaults(
defineProps<{
options: any[];
}>(),
{
options: () => [],
},
);
// 默认列props
const getColProps = (item: any) => {
if (item.props) {
return item.props;
} else {
return { xs: 24, sm: 12, md: 8, lg: 6, xl: 6 };
}
};
// 表单数据
const formVal = ref<any>({});
// 初始化表单
const initFromVal = () => {
props.options.forEach((item) => {
formVal.value[item.key] =
typeof item.value !== "undefined" ? item.value : null;
// 时间区间
if (item.type === "daterange") {
formVal.value[item.dict[0]] = "";
formVal.value[item.dict[1]] = "";
return;
}
});
};
// 时间区间选择
const handleDateChange = (val: string[] | null, key1: string, key2: string) => {
formVal.value[key1] = val ? val[0] : "";
formVal.value[key2] = val ? val[1] : "";
};
// 搜索
const handleSearch = () => {
emits("search", formVal.value, 1);
};
// 重置
const handleReset = () => {
initFromVal();
emits("search", formVal.value, 1);
};
initFromVal();
emits("search", formVal.value);
</script>
<style lang="scss" scoped>
.operation {
margin-left: 10px;
flex: 1;
}
</style>
封装后的用法
<template>
<c-search ref="searchRef" :options="formSeachOptions" @search="search" />
<c-table :columns="columns" :data="tableData" :single-line="false" :pagination="pagination">
<template #action="row">
<el-button type="primary">操作</el-button>
</template>
</c-table>
</template>
<script lang="ts" setup>
import { reactive, ref, h } from "vue";
import CSearch from "@/components/common/CSearch.vue";
import CTable from "@/components/common/CTable";
import { usePage } from "@/utils/page";
import { demoOption, getNameByValue } from "@/utils/options";
import { getList } from "@/utils/mockapi";
// 搜索栏数据
const searchRef = ref()
const formSeachOptions = ref([
{ type: "daterange", key: Symbol(), label: "创建时间", dict: ["start_time", "end_time"]},
{ type: "input", key: "name", label: "名称" },
{ type: "select",key: "status", label: "状态 ", value: null, dict:{options:demoOption.status}}]);
// 表格数据
const tableData = ref<any[]>([]);
const columns = reactive<any>([
{
type: "index",
label: "序号",
width: 80,
},
{
prop: "name",
label: "名称",
},
{
prop: "pay_amount",
label: "金额",
render: (row: any) => {
return h("div", (row.pay_amount / 100).toFixed(2));
},
renderTitle: () => {
return h("div", "金额(自定义列头)");
},
},
{
prop: "status",
label: "状态",
render: (row: any) => {
return h("div", getNameByValue(row.status, demoOption.status));
},
},
{
label: "操作",
width: 200,
fixed: "right",
slotName: "action",
},
]);
// 请求mock接口获取数据
const search = (query: any = searchRef.value?.getData() || {}, page?: number) => {
page && (pageState.page = 1);
getList({ ...query, ...pageState }).then((res: any) => {
tableData.value = res.data;
pagination.total = res.total;
});
};
// 分页器
const { pageState, pagination } = usePage(search);
</script>
<style lang="scss" scoped></style>