前言:项目中如果很多页面都会使用到el-table组件
el-dialog
el-form
,并且使用时,会重复写很多次,造成非常多冗余的代码,我们希望能只传递一些配置项到一个组件就渲染出对应的代码。
1. 初步渲染表格
第一步:在vue3的项目中,在components目录下新建一个commonTable.vue
组件,在About.vue中引入该组件
第二步:准备类型和数据
src/types/commonTable.ts
中准备表格公用的类型
js
export type tableColumn = {
prop: string;
label: string;
width: number; // 控制这一列的宽度
};
export type optionsType = {
column: tableColumn[];
};
components/commonTable.vue
中接受一个tableData
是表格具体展示的数据,options
接受表格的一些配置项
js
import type { optionsType } from "@/types/CommonTable";
const props = defineProps<{
tableData: Array<any>;
options: optionsType;
}>();
const { column } = props.options;
渲染数据
html
<el-table style="width: 100%" :data="tableData">
<el-table-column
v-for="item in column"
:key="item.prop"
:prop="item.prop"
:label="item.label"
>
</el-table-column>
</el-table>
父组件About.vue
里面定义数据
ts
// 通过type定义类型
export type aboutType = {
name: string;
age: string;
gender: number;
genderValue: string;
hobby: string;
task: string;
dreaming: string;
food: string;
};
// 表格数据
const tableData = ref<aboutType[]>([
{
name: "xhg1",
age: "21",
gender: 1,
genderValue: "男",
hobby: "sleep",
task: "coding",
dreaming: "sleeping",
food: "",
},
{
name: "xhg2",
age: "21",
gender: 1,
genderValue: "男",
hobby: "sleep",
task: "coding",
dreaming: "sleeping",
food: "",
},
{
name: "xhg3",
age: "23",
gender: 1,
genderValue: "男",
hobby: "read",
task: "coding",
dreaming: "sleeping",
food: "吃蛋挞",
},
]);
const options = ref({
column: [
{
prop: "name",
label: "姓名",
width: 180,
searchWidth: 220,
searchLabel: 100, //行内的设置优先级高于全局
},
{
prop: "age",
label: "年龄",
width: 180,
searchWidth: 220,
searchLabel: 100,
},
{
prop: "gender",
label: "性别",
width: 180,
searchWidth: 220,
searchLabel: 100,
},
{
prop: "task",
label: "任务",
width: 180,
searchWidth: 220,
searchLabel: 100,
},
{
prop: "dreaming",
label: "梦想",
width: 180,
searchWidth: 220,
searchLabel: 100,
},
{
prop: "food",
label: "美食",
width: 180,
searchWidth: 220,
searchLabel: 100,
},
],
});
使用组件
html
<div class="About">
<div class="ab-top">
<commonTable :tableData="tableData" :options="options">
</commonTable>
</div>
</div>
增加一些样式
less
.About {
width: 100%;
height: 100%;
padding: 20px;
}
.ab-top {
width: 50%;
border: 1px solid #dcdfe6;
}
效果如下:
到这一步,我们完成了最基本的封装
2. 序号列
这一节我们来完成更多的配置项,比如说是否展示序号列
,序号列的宽度是多少
在options
配置项里面增加这几个配置:
js
const options = ref({
showIndex: true,
indexWidth: 100,
indexLabel: "序号",
......
})
在类型声明中增加
diff
export type optionsType = {
+ showIndex: boolean;
+ indexWidth: number;
+ indexLabel: string;
column: tableColumn[];
};
diff
<el-table style="width: 100%" :data="tableData">
+ <el-table-column
+ v-if="options.showIndex"
+ type="index"
+ :width="options.indexWidth"
+ :label="options.indexLabel"
+ ></el-table-column>
<el-table-column
v-for="item in column"
:key="item.prop"
:prop="item.prop"
:label="item.label"
>
</el-table-column>
</el-table>
效果如下:
3. 插槽
有时候我们希望展示如下效果而不是仅仅展示文字,该如何在通用组件库里面接受到呢?肯定不能直接写死
引入useSlots方法,该方法能在<script setup>
里面的上下文中接受父组件传递的插槽
js
import { useSlots } from "vue";
const slots = useSlots();
打印出来是key value的形式
我们将封装的commonTable.vue
改造一下
diff
<el-table style="width: 100%" :data="tableData">
......省略一部分代码
<template v-for="item in column" :key="item.prop">
<el-table-column
+ v-if="slots.hasOwnProperty(item.prop)"
:prop="item.prop"
:label="item.label"
:width="item.width"
>
<template #default="scope">
+ <slot :name="item.prop" :scope="scope"></slot>
</template>
</el-table-column>
<el-table-column
+ v-else
:prop="item.prop"
:label="item.label"
:width="item.width"
>
</el-table-column>
</template>
</el-table>
如上,在遍历column
列数组时,判断他是否在插槽对象slots
里面,如果在的话就渲染插槽,不渲染默认文字。
请注意,公共组件里面的第一层<template #default="scope"></template>
是传递给el-table-column
组件的具名插槽入口,
而里面的<slot :name="item.prop"></slot>
是整个commonTable.vue
组件的接受父组件插槽的出口。
传入插槽的内容
diff
<commonTable :tableData="tableData" :options="options">
<template #date="{ scope }">
<el-tag> {{ scope.row.date }}</el-tag>
</template>
</commonTable>
实际上,你也可以换一种写法去判断是否传递了插槽。直接使用$slots,该组件实例能直接访问,检测是否存在插槽
diff
<el-table-column
+ v-if="$slots[item.prop]"
:prop="item.prop"
:label="item.label"
:width="item.width"
>
<template #default="scope">
<slot :name="item.prop" :scope="scope"></slot>
</template>
</el-table-column>
4. 是否显示操作框
操作栏的效果如下:
About.vue
增加如下配置项:
diff
const options = ref<optionsType>({
showIndex: true,
indexWidth: 100,
indexLabel: "序号",
+ menu: true, //是否有操作栏 boolean
+ menuWidth: 180, //操作栏宽度 number
+ menuTitle: "操作2", //操作栏标题 string
+ menuFixed: true, //操作栏是否为固定列 boolean
+ menuType: "primary", //操作栏按钮类型 string
......
})
如果menuType
的值是text
,则是这样的效果:
增加类型
diff
export type optionsType = {
showIndex: boolean;
indexWidth: number;
indexLabel: string;
column: tableColumn[];
+ menu: boolean;
+ menuWidth: number;
+ menuTitle: string;
+ menuFixed: boolean;
+ menuType: string;
};
在CommonTable.vue
中增加如下代码
diff
<el-table style="width: 100%" :data="tableData">
......省略一部分代码
+ <el-table-column
v-if="options.menu"
:width="options.menuWidth"
:label="options.menuTitle"
:fixed="options.menuFixed ? 'right' : ''"
>
<template #default="scope">
<el-button
:type="options.menuType ?? 'primary'"
size="small"
@click="rowEdit(scope.row, 'edit')"
>编辑</el-button
>
<el-button
:type="options.menuType ?? 'primary'"
size="small"
@click="rowDel(scope.$index, scope.row, 'del')"
>删除</el-button
>
<!-- 利用作用域插槽将数据传递过去 -->
<slot name="menu" :scope="scope"></slot>
</template>
</el-table-column>
</el-table>
新增编辑和删除的方法
js
const rowEdit = (row: any, type: string) => {
console.log(row, type);
};
const rowDel = (index: number, row: any, type: string) => {
console.log(index, row, type);
};
5. 封装Dialog
组件
希望点击编辑按钮,能够弹出Dialog弹窗, 效果如下:
初始el-dialog代码:
@/components/CommonDialog.vue
html
<script setup lang="ts">
import { ref } from "vue";
const dialogVisible = ref(false);
const title = ref("");
const openDialog = (row: any, type: string) => {
dialogVisible.value = true;
title.value = type === "edit" ? "编辑" : "新增";
};
defineExpose({
openDialog,
});
</script>
<template>
<div class="CommonDialog">
<el-dialog v-model="dialogVisible" :title="title" width="30%">
</el-dialog>
</div>
</template>
<style lang="less" scoped>
:deep(.el-dialog__header) {
text-align: left;
}
</style>
如上代码中,使用了el-dialog
弹窗组件,然后封装了openDialog
方法来把弹窗打开,通过defineExpose
方法将openDialog
暴露出去,这样在父组件里面能够直接通过 组件实例.value.openDialog()
来把弹窗打开了
在父组件About.vue
里面
js
const rowEdit = (row: any, type: string) => {
dialogVisible.value = true;
commonDialog.value?.openDialog(row, type);
};
此外,我们希望在点击编辑和新增的时候,能够展示和表格相关的字段,并且这个字段可以由我们来控制。
打开弹窗时,记录字段tp
用于判断是新增还是编辑打开;row
字段用来回显当前行的数据,用JSON.pares(JSON.stringify())
来进行深拷贝
diff
const tp = ref("");
const openDialog = (row: any, type: string) => {
dialogVisible.value = true;
tp.value = type;
if (type === "add") {
form.value = {};
} else {
// 深拷贝
form.value = JSON.parse(JSON.stringify(row));
}
};
增加el-form代码
增加el-form
部分的代码,里面的addDisplay
字段表示当新增时这个item字段是否显示,默认是要显示,editDisplay同理
。同样利用$slots
来判断是使用插槽还是默认的input输入框
html
<el-dialog
v-model="dialogVisible"
:title="tp === 'add' ? '新增' : '编辑'"
width="30%"
>
<el-form :model="form">
<template v-for="(item, index) in column" :key="index">
<el-form-item
v-if="
tp === 'add'
? (item?.addDisplay ?? true)
: (item?.editDisplay ?? true)
"
:label="item?.label"
:label-width="item?.formLabelWidth"
>
<slot
v-if="$slots.hasOwnProperty(`${item?.prop}Form`)"
:name="`${item?.prop}Form`"
></slot>
<el-input v-else v-model="form[item.prop]" />
</el-form-item>
</template>
</el-form>
</el-dialog>
这里的form[item.prop]
可能会报错,因为form初始是一个空对象
需要借助interface
接口来定义类型
js
interface FormType {
// 键是字符串,值是字符串或数字
[key: string]: string | number;
}
const form = ref<FormType>({});
6. 封装Search搜索组件
希望封装公共组件展示如下效果,只需要传入指定的配置项,就可以增加新的表单
创建组件@/components/commonSearch.vue
,结构如下:
html
<div class="CommonSearch">
<el-form :inline="true" :model="search" class="demo-form-inline">
<template v-for="(item, index) in searchColumn" :key="index">
<el-form-item
:label="item.label"
:label-width="`${item.searchLabel ?? 120}px`"
>
<slot
v-if="$slots.hasOwnProperty(`${item?.prop}Search`)"
:name="`${item.prop}Search`"
>
<el-input
v-model="search[item.prop]"
:style="{
width: item.searchWidth
? item.searchWidth + 'px'
: '',
}"
:placeholder="`请输入${item.label}`"
/>
</slot>
<el-input
v-else
v-model="search[item.prop]"
:style="{
width: item.searchWidth
? item.searchWidth + 'px'
: '',
}"
:placeholder="`请输入${item.label}`"
/>
</el-form-item>
</template>
</el-form>
<div>
<el-button type="primary" size="small" @click="handleQuery"
>查询</el-button
>
<el-button type="primary" size="small" plain @click="handleReset"
>重置</el-button
>
</div>
</div>
如上代码中,仍旧需要$slots
判断是否传入了插槽。遍历searchColumn
变量来渲染出对应的form表单
js代码如下:
ts
import { ref } from "vue";
defineProps<{
searchColumn: any[];
}>();
export type SearchColumn = {
[key: string]: string | number;
};
const search = ref<SearchColumn>({});
const handleQuery = () => {
console.log(search.value);
};
const handleReset = () => {
search.value = {};
};
增加样式:
less
.CommonSearch {
display: flex;
align-items: center;
}
在父组件中使用该组件:
html
<commonSearch :searchColumn="searchColumnData"></commonSearch>
在父组件中传入数据:
js
const options = ref<optionsType>({
......
searchColumn: [
{
label: "姓名",
prop: "name",
searchWidth: 100,
searchLabel: 50,
},
{
label: "年龄",
prop: "age",
searchWidth: 100,
searchLabel: 50,
},
],
});
// 搜索框的逻辑
const { searchColumn } = options.value;
const searchColumnData = toRaw(searchColumn);
如上就可以渲染出页面了。进一步如何实现筛选表格的功能呢?
在commonSearch.vue
组件中,触发emit
:
js
const emits = defineEmits<{
(e: "query", search: SearchColumn): void;
}>();
const handleQuery = () => {
emits("query", search.value);
};
在About.vue
组件中
html
<commonSearch
:searchColumn="searchColumnData"
@query="handleQuery"
></commonSearch>
就可以实现对表格数据的过滤
js
const handleQuery = (search: any) => {
console.log(search);
// 在这里对tableData数据进行过滤,更新表格数据
};