一次用vue3简单封装table组件的实战过程
摸鱼大队队长 人气:0前言:
对于一个业务前端来讲,工作中用的最多的组件我觉得大概率是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: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', }, ] const options = reactive({ column:[ { prop:'date', label:'日期', width:180 }, { prop:'name', label:'名字', width:120 }, { 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: 100vw;"> <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:50,//序号列表宽度 number indexFixed:true,//序号是否为固定列 boolean menu:true, //是否有操作栏 boolean menuWidth:140,//操作栏宽度 number menuTitle:'操作2',//操作栏标题 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: 100vw;" :size="op.size??'small'"> <el-table-column v-if="op.index" type="index" :width="op.indexWidth ?? 50" /> <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: 20px"> <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 ?? 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 ?? 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: "2016-05-03", name: "Tom", address: "No. 189, Grove St, Los Angeles", }, ]; const options = reactive({ index: true, //是否有序号 boolean indexWidth: 50, //序号列表宽度 number indexFixed: true, //序号是否为固定列 boolean menu: true, //是否有操作栏 boolean menuWidth: 180, //操作栏宽度 number menuTitle: "操作2", //操作栏标题 string menuFixed: true, //操作栏是否为固定列 boolean menuType: "text", //操作栏按钮样式 button/text searchLabel:150,//查询框label的宽度 searchWidth:200,//查询框组件的宽度 column: [ { prop: "date", label: "日期", width: 180, searchWidth:220, searchLabel:100,//行内的设置优先级高于全局 }, { prop: "name", label: "名字", width: 120, searchWidth:180 }, { prop: "address", label: "地址", //是否在表单弹窗中显示 editDisplay: false, searchWidth:200 }, ], }); 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: 20px"> <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 ?? 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 ?? 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: 100vw" :size="op.size ?? 'small'"> <el-table-column v-if="op.index" type="index" :width="op.indexWidth ?? 50" /> <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="120px" > <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>
总结
加载全部内容