Vue3+TS 如何二次封装el-table el-form el-dialog

前言:项目中如果很多页面都会使用到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数据进行过滤,更新表格数据
};
相关推荐
浪裡遊20 分钟前
uniapp常用组件
开发语言·前端·uni-app
五点六六六21 分钟前
Restful API 前端接口模型架构浅析
前端·javascript·设计模式
筱筱°23 分钟前
Vue 路由守卫
前端·javascript·vue.js
customer0827 分钟前
【开源免费】基于SpringBoot+Vue.JS智慧生活商城系统(JAVA毕业设计)
java·vue.js·spring boot
前端小张同学40 分钟前
前端Vue后端Nodejs 实现 pdf下载和预览,如何实现?
前端·javascript·node.js
独孤求败Ace42 分钟前
第59天:Web攻防-XSS跨站&反射型&存储型&DOM型&接受输出&JS执行&标签操作&SRC复盘
前端·xss
天空之枫44 分钟前
node-sass替换成Dart-sass(全是坑)
前端·css·sass
SecPulse1 小时前
xss注入实验(xss-lab)
服务器·前端·人工智能·网络安全·智能路由器·github·xss
路遥努力吧1 小时前
el-input 不可编辑,但是点击的时候出现弹窗/或其他操作面板,并且带可清除按钮
前端·vue.js·elementui
绝顶少年1 小时前
确保刷新页面后用户登录状态不会失效,永久化存储用户登录信息
前端