目录
- 前言:
- 实现基础功能表格
- 进一步定制化
- 总结
前言:
对于一个业务前端来讲,工作中用的最多的组件我觉得大概率是table组件了,只要是数据展示就不可避免地用到这个组件。用久了就有点熟悉,就来锻炼自己的封装能力,为了更好的搬砖,封装table组件。
首先,我们要思考我们封装一个怎样的表格,怎样的形式更加方便。先定一个大概目标,往前走,剩下慢慢去完善。一般来说,我们更倾向于通过配置来实现表格,那就是说利用json格式来配置。
实现基础功能表格
el-table中用el-table-column的prop属性来对应对象中的键名即可填入数据,用 label 属性来定义表格的列名。那么这两个主要属性可以通过动态绑定来进行赋值,然后使用v-for来进行循环。
//app.vue
<script setup>
import TableVue from './components/Table.vue';
import {ref,reactive} from 'vue'
const tableData = [
{
date: '-05-03',
name: 'Tom',
address: 'No., Grove St, Los Angeles',
},
]
const options = reactive({
column:[
{
prop:'date',
label:'日期',
width:
},
{
prop:'name',
label:'名字',
width:
},
{
prop:'address',
label:'地址',
}
]
})
</script>
<template>
<TableVue :table-data="tableData" :options="options"></TableVue>
</template>
<style scoped></style>
//table.vue
<script setup>
import { ref,defineProps } from 'vue'
const props= defineProps({
options:Object,
tableData:Array
})
const {column} = props.options
</script>
<template>
<el-table :data="tableData" style="width:vw;">
<el-table-column v-for="(item,index) in column" :prop="item.prop" :label="item.label" :width="item.width??''" />
</el-table>
</template>
<style scoped></style>
好了,我们完成了通过json格式的最简单配置展示了数据,仅仅只有这样是远远不够的,连最基本的增删改查都没有。那么我们接下来就来实现增删改功能。对于增删改的方法基本上就是父子组件之间的方法调用,值的传递使用,以及多了一个对话框来进行值的交互。这里也不难,下面是关键代码
//app.vue
//options里面新增以下属性
index:true,//是否有序号 boolean
indexWidth:,//序号列表宽度 number
indexFixed:true,//序号是否为固定列 boolean
menu:true, //是否有操作栏 boolean
menuWidth:,//操作栏宽度 number
menuTitle:'操作',//操作栏标题 string
menuFixed:true,//操作栏是否为固定列 boolean
menuType:'text',//操作栏按钮样式 button/text
//新增以下方法
//删除
const rowDel = (index,row)=>{
console.log('del',index,row)
}
//编辑
const rowEdit=(type,row)=>{
console.log(type,row)
}
<TableVue :table-data="tableData" :options="options" @row-del="rowDel" @row-edit="rowEdit"></TableVue>
//table.vue新增以下方法
<script setup>
const emits = defineEmits(["rowDel", "rowEdit"])
//把属性解构出来减少代码量
const { options: op } = props
const { column } = props.options
//获取子组件实例
const edit = ref('edit')
//行数据删除时触发该事件
const rowDel = (index, row) => {
emits("rowDel", index, row)
}
//更新数据后确定触发该事件
const editBefore = (row,type) => {
//将行内属性转为普通对象传递
edit.value.openDialog(type,row,toRaw(column))
}
const rowEdit=(type,form)=>{
emits("rowEdit",type,form)
}
</script>
<template>
<div class="menuOp">
<el-button type="danger" :icon="Plus" :size="op.size??'small'" @click="editBefore(_,'add')">新增</el-button>
</div>
<el-table :data="tableData" style="width:vw;" :size="op.size??'small'">
<el-table-column v-if="op.index" type="index" :width="op.indexWidth ??" />
<el-table-column v-for="(item, index) in column" :prop="item.prop" :label="item.label" :width="item.width ?? ''" />
<el-table-column :fixed="op.menuFixed ? 'right' : ''" :label="op.menuTitle" :width="op.menuWidth" v-if="op.menu">
<template #default="scope">
<el-button :type="op.menuType ?? 'primary'" size="small"
@click="editBefore(scope.row,'edit')">编辑</el-button>
<el-button :type="op.menuType ?? 'primary'" size="small"
@click="rowDel(scope.$index, scope.row,'del')">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 对话框 -->
<editDialog ref="edit" @edit-submit="rowEdit"></editDialog>
</template>
<style scoped>
.menuOp{
text-align: left;
}
</style>
进一步定制化
虽然看着我们可以进行简单的增删改,但是如果编辑有下拉框或者其他类型呢。表格行内数据需要多样性展示,而不是单纯的纯文本又该怎么办呢。这时候我们需要用到vue里面的slot(插槽)来解决这个问题了。
插槽(slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。
关键代码截图:
对话框我们利用插槽,那么同理,表格行内的数据展示我们也可以进行修改,来支持插槽
修改的关键代码为:
增删改基本功能到这里其实差不多,剩下就是一些细节性的判断,接下来我们就来完成查询部分的封装。方法也是一样,一般都是input框,其他有需求我们就利用插槽来实现。
//Search.vue
<script setup>
import { defineProps, onMounted, ref, defineEmits, toRaw, useSlots } from "vue";
const emits = defineEmits(["handleQuery", "handleReset"]);
const search = ref({});
const slots = useSlots();
const handleQuery = () => {
emits("handleQuery", search.value);
};
const handleReset = () => {
search.value = {};
emits("handleReset");
};
const props = defineProps({
row: {
type: Object,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
search:{
type:Object,
default:()=>{}
}
});
const column = toRaw(props.options.column);
onMounted(() => {
});
</script>
<template>
<div style="text-align: left; margin-bottom:px">
<el-form :inline="true" :model="search" class="demo-form-inline">
<template v-for="(item, index) in props.row">
<el-form-item
:label="item.label"
:label-width="`${item.searchLabel ?? options.searchLabel ??}px`"
>
<slot
v-if="slots.hasOwnProperty(`${item?.prop}Search`)"
:name="`${item.prop}Search`"
>
<el-input
v-model="search[item.prop]"
:style="{ width: item.searchWidth ?? options.searchWidth + 'px' }"
:placeholder="`请输入${item.label}`"
/>
</slot>
<el-input
v-else
v-model="search[item.prop]"
:style="{ width: item.searchWidth ?? options.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>
</template>
//Table.vue
<SearchVue
:options="op"
:row="slotCloumn"
@handleReset="handleReset"
@handleQuery="handleQuery"
:search="search"
>
<template v-for="(item, index) in slotCloumn" #[item?.prop+`Search`]>
<slot :name="`${item?.prop}Search`"></slot>
</template>
</SearchVue>
就暂时先写到这里了,最开始是想通过封装一个简单的组件,来巩固自己的vue3语法熟悉程度。后来发现因为水平有限加上是自己独立思考,有很多地方其实卡住了。比如值的传递是用provide(inject)比较好还是直接props来方便。值的改动需不需要用到computed或者watch。插槽的写法能否更加简便,不需要一级一级传递的那种,越想越多问题。自己技术能力不够这是最大的问题,想法过于宏大,能力却不行,打算日后精进自己的技术,然后约上几个伙伴继续去完善,只有思维碰撞才能产出好的代码。
最后,把全部代码附上
//App.vue
<script setup>
import TableVue from "./components/Table.vue";
import { ref, reactive,toRaw } from "vue";
const tableData = [
{
date: "-05-03",
name: "Tom",
address: "No., Grove St, Los Angeles",
},
];
const options = reactive({
index: true, //是否有序号 boolean
indexWidth:, //序号列表宽度 number
indexFixed: true, //序号是否为固定列 boolean
menu: true, //是否有操作栏 boolean
menuWidth:, //操作栏宽度 number
menuTitle: "操作", //操作栏标题 string
menuFixed: true, //操作栏是否为固定列 boolean
menuType: "text", //操作栏按钮样式 button/text
searchLabel:,//查询框label的宽度
searchWidth:,//查询框组件的宽度
column: [
{
prop: "date",
label: "日期",
width:,
searchWidth:,
searchLabel:,//行内的设置优先级高于全局
},
{
prop: "name",
label: "名字",
width:,
searchWidth:
},
{
prop: "address",
label: "地址",
//是否在表单弹窗中显示
editDisplay: false,
searchWidth:
},
],
});
const form = ref({})
const search = ref({})
//删除
const rowDel = (index, row) => {
console.log("del", index, row);
};
//编辑
const rowEdit = (type, row) => {
// console.log(type, row);
//这里因为没有思考明白到底如何利用v-model属性进行所有组件绑定传递,就使用这种蹩脚方法
console.log(Object.assign(row.value,form.value))
};
const tip=(row)=>{
console.log(row)
}
const handleReset=()=>{
console.log('reset')
}
const handleQuery=(param)=>{
let params = Object.assign(search.value,param)
console.log(params)
}
</script>
<template>
<TableVue
:table-data="tableData"
:options="options"
@row-del="rowDel"
@row-edit="rowEdit"
v-model="form"
@handleQuery="handleQuery"
@handleReset="handleReset"
:search="search"
>
<!-- 查询框插槽 -->
<template #dateSearch>
<el-date-picker
v-model="search.date"
type="datetime"
placeholder="Select date and time"
/>
</template>
<!-- 表格内的插槽,插槽名为字段名 -->
<template #date="{scope}">
<el-tag>{{scope.row.date}}</el-tag>
</template>
<!-- 操作栏插槽 -->
<template #menu="{scope}" >
<el-button icon="el-icon-check" @click="tip(scope.row)">自定义菜单按钮</el-button>
</template>
<!-- 对话框插槽,插槽名字为对应的字段名加上Form -->
<template #dateForm>
<el-date-picker
v-model="form.date"
type="datetime"
placeholder="Select date and time"
/>
</template>
</TableVue>
</template>
<style scoped></style>
//Search.vue
<script setup>
import { defineProps, onMounted, ref, defineEmits, toRaw, useSlots } from "vue";
const emits = defineEmits(["handleQuery", "handleReset"]);
const search = ref({});
const slots = useSlots();
const handleQuery = () => {
emits("handleQuery", search.value);
};
const handleReset = () => {
search.value = {};
emits("handleReset");
};
const props = defineProps({
row: {
type: Object,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
search:{
type:Object,
default:()=>{}
}
});
const column = toRaw(props.options.column);
onMounted(() => {
});
</script>
<template>
<div style="text-align: left; margin-bottom:px">
<el-form :inline="true" :model="search" class="demo-form-inline">
<template v-for="(item, index) in props.row">
<el-form-item
:label="item.label"
:label-width="`${item.searchLabel ?? options.searchLabel ??}px`"
>
<slot
v-if="slots.hasOwnProperty(`${item?.prop}Search`)"
:name="`${item.prop}Search`"
>
<el-input
v-model="search[item.prop]"
:style="{ width: item.searchWidth ?? options.searchWidth + 'px' }"
:placeholder="`请输入${item.label}`"
/>
</slot>
<el-input
v-else
v-model="search[item.prop]"
:style="{ width: item.searchWidth ?? options.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>
</template>
//table.vue
<script setup>
import { ref, defineProps, defineEmits, toRaw, onMounted, useSlots } from "vue";
import { Delete, Plus } from "@element-plus/icons-vue";
import editDialog from "./Dialog.vue";
import SearchVue from "./Search.vue";
const props = defineProps({
options: Object,
tableData: Array,
modelValue: {
type: Object,
default: () => {},
},
});
const emits = defineEmits([
"rowDel",
"rowEdit",
"update:modelValue",
"handleQuery",
]);
//把属性解构出来减少代码量
const { options: op } = props;
const { column } = props.options;
//获取编辑对话框里面所有属性,用于动态生成插槽
const slotCloumn = toRaw(column);
//获取子组件实例
const edit = ref("edit");
//获取插槽实例
const slots = useSlots();
//行数据删除时触发该事件
const rowDel = (index, row) => {
emits("rowDel", index, row);
};
//更新数据后确定触发该事件
const editBefore = (row, type) => {
//将行内属性转为普通对象传递
edit.value.openDialog(type, row, toRaw(column));
};
const rowEdit = (type, form) => {
emits("rowEdit", type, form);
emits("update:modelValue", form);
};
const handleQuery = (search) => {
emits("handleQuery", search);
};
const handleReset = () => {
emits("handleReset");
};
onMounted(() => {
console.log("slots", slots);
});
</script>
<template>
<div>
<SearchVue
:options="op"
:row="slotCloumn"
@handleReset="handleReset"
@handleQuery="handleQuery"
:search="search"
>
<template v-for="(item, index) in slotCloumn" #[item?.prop+`Search`]>
<slot :name="`${item?.prop}Search`"></slot>
</template>
</SearchVue>
</div>
<div class="menuOp">
<el-button
type="danger"
:icon="Plus"
:size="op.size ?? 'small'"
@click="editBefore(_, 'add')"
>新增</el-button
>
</div>
<el-table :data="tableData" style="width:vw" :size="op.size ?? 'small'">
<el-table-column
v-if="op.index"
type="index"
:width="op.indexWidth ??"
/>
<template v-for="(item, index) in column">
<el-table-column
:label="item.label"
v-if="slots.hasOwnProperty(item?.prop)"
>
<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 ?? ''"
/>
</template>
<el-table-column
:fixed="op.menuFixed ? 'right' : ''"
:label="op.menuTitle"
:width="op.menuWidth"
v-if="op.menu"
>
<template #default="scope">
<el-button
:type="op.menuType ?? 'primary'"
size="small"
@click="editBefore(scope.row, 'edit')"
>编辑</el-button
>
<el-button
:type="op.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>
<!-- 对话框 -->
<editDialog ref="edit" @edit-submit="rowEdit">
<template v-for="(item, index) in slotCloumn" #[item?.prop+`Form`]="scope">
<slot :name="`${item?.prop}Form`" v-bind="scope"></slot>
</template>
</editDialog>
</template>
<style scoped>
.menuOp {
text-align: left;
}
</style>
//Dialog.vue
<script setup>
import { ref, defineExpose, defineEmits, useSlots, onMounted } from "vue";
const emits = defineEmits(["editSubmit"]);
const dialogFormVisible = ref(false);
const form = ref({});
const tp = ref("");
let columns = [];
//获取当前已实例化的插槽,然后去判断插槽是否使用了
const slots = useSlots();
const openDialog = (type, row, column) => {
dialogFormVisible.value = true;
columns = column;
tp.value = type;
if (type === "edit") {
//如果编辑框设置了editDisplay为false,则删除该属性
columns.map((x) => {
if (x.editDisplay === false) {
delete row[x.prop];
}
});
form.value = JSON.parse(JSON.stringify(row));
}else{
form.value={}
}
};
const handleSubmit = () => {
emits("editSubmit", tp, form);
};
onMounted(() => {
console.log("===", slots);
});
defineExpose({
openDialog,
});
</script>
<template>
<el-dialog v-model="dialogFormVisible" append-to-body :title="tp==='add'?'新增':'编辑'">
<el-form :model="form">
<template v-for="(item, index) in columns">
<el-form-item
v-if="tp==='add'?item.addDisplay??true:item.editDisplay ?? true"
:key="index"
:label="item.label"
label-width="px"
>
<slot
:name="`${item?.prop}Form`"
v-if="slots.hasOwnProperty(`${item?.prop}Form`)"
>
<!-- 因为在table组件已经开始生成所有关于编辑的插槽,所以全部都有实例,需要给个默认显示,否则会空白 -->
<el-input v-model="form[item?.prop]" />
</slot>
<el-input v-model="form[item?.prop]" v-else />
</el-form-item>
</template>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit"> 确认 </el-button>
</span>
</template>
</el-dialog>
</template>