一次用vue3簡單封裝table組件的實戰(zhàn)過程
前言:
對于一個業(yè)務(wù)前端來講,工作中用的最多的組件我覺得大概率是table組件了,只要是數(shù)據(jù)展示就不可避免地用到這個組件。用久了就有點熟悉,就來鍛煉自己的封裝能力,為了更好的搬磚,封裝table組件。
首先,我們要思考我們封裝一個怎樣的表格,怎樣的形式更加方便。先定一個大概目標(biāo),往前走,剩下慢慢去完善。一般來說,我們更傾向于通過配置來實現(xiàn)表格,那就是說利用json格式來配置。
實現(xiàn)基礎(chǔ)功能表格
el-table中用el-table-column的prop屬性來對應(yīng)對象中的鍵名即可填入數(shù)據(jù),用 label 屬性來定義表格的列名。那么這兩個主要屬性可以通過動態(tài)綁定來進(jìn)行賦值,然后使用v-for來進(jìn)行循環(huán)。
//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格式的最簡單配置展示了數(shù)據(jù),僅僅只有這樣是遠(yuǎn)遠(yuǎn)不夠的,連最基本的增刪改查都沒有。那么我們接下來就來實現(xiàn)增刪改功能。對于增刪改的方法基本上就是父子組件之間的方法調(diào)用,值的傳遞使用,以及多了一個對話框來進(jìn)行值的交互。這里也不難,下面是關(guān)鍵代碼
//app.vue
//options里面新增以下屬性
index:true,//是否有序號 boolean
indexWidth:50,//序號列表寬度 number
indexFixed:true,//序號是否為固定列 boolean
menu:true, //是否有操作欄 boolean
menuWidth:140,//操作欄寬度 number
menuTitle:'操作2',//操作欄標(biāo)題 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"])
//把屬性解構(gòu)出來減少代碼量
const { options: op } = props
const { column } = props.options
//獲取子組件實例
const edit = ref('edit')
//行數(shù)據(jù)刪除時觸發(fā)該事件
const rowDel = (index, row) => {
emits("rowDel", index, row)
}
//更新數(shù)據(jù)后確定觸發(fā)該事件
const editBefore = (row,type) => {
//將行內(nèi)屬性轉(zhuǎn)為普通對象傳遞
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>
進(jìn)一步定制化
雖然看著我們可以進(jìn)行簡單的增刪改,但是如果編輯有下拉框或者其他類型呢。表格行內(nèi)數(shù)據(jù)需要多樣性展示,而不是單純的純文本又該怎么辦呢。這時候我們需要用到vue里面的slot(插槽)來解決這個問題了。
插槽(slot)是 vue 為組件的封裝者提供的能力。允許開發(fā)者在封裝組件時,把不確定的、希望由用戶指定的部分定義為插槽。
關(guān)鍵代碼截圖:




對話框我們利用插槽,那么同理,表格行內(nèi)的數(shù)據(jù)展示我們也可以進(jìn)行修改,來支持插槽
修改的關(guān)鍵代碼為:



增刪改基本功能到這里其實差不多,剩下就是一些細(xì)節(jié)性的判斷,接下來我們就來完成查詢部分的封裝。方法也是一樣,一般都是input框,其他有需求我們就利用插槽來實現(xiàn)。
//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語法熟悉程度。后來發(fā)現(xiàn)因為水平有限加上是自己獨立思考,有很多地方其實卡住了。比如值的傳遞是用provide(inject)比較好還是直接props來方便。值的改動需不需要用到computed或者watch。插槽的寫法能否更加簡便,不需要一級一級傳遞的那種,越想越多問題。自己技術(shù)能力不夠這是最大的問題,想法過于宏大,能力卻不行,打算日后精進(jìn)自己的技術(shù),然后約上幾個伙伴繼續(xù)去完善,只有思維碰撞才能產(chǎn)出好的代碼。
最后,把全部代碼附上
//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", //操作欄標(biāo)題 string
menuFixed: true, //操作欄是否為固定列 boolean
menuType: "text", //操作欄按鈕樣式 button/text
searchLabel:150,//查詢框label的寬度
searchWidth:200,//查詢框組件的寬度
column: [
{
prop: "date",
label: "日期",
width: 180,
searchWidth:220,
searchLabel:100,//行內(nèi)的設(shè)置優(yōu)先級高于全局
},
{
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屬性進(jìn)行所有組件綁定傳遞,就使用這種蹩腳方法
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>
<!-- 表格內(nèi)的插槽,插槽名為字段名 -->
<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>
<!-- 對話框插槽,插槽名字為對應(yīng)的字段名加上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",
]);
//把屬性解構(gòu)出來減少代碼量
const { options: op } = props;
const { column } = props.options;
//獲取編輯對話框里面所有屬性,用于動態(tài)生成插槽
const slotCloumn = toRaw(column);
//獲取子組件實例
const edit = ref("edit");
//獲取插槽實例
const slots = useSlots();
//行數(shù)據(jù)刪除時觸發(fā)該事件
const rowDel = (index, row) => {
emits("rowDel", index, row);
};
//更新數(shù)據(jù)后確定觸發(fā)該事件
const editBefore = (row, type) => {
//將行內(nèi)屬性轉(zhuǎn)為普通對象傳遞
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
>
<!-- 利用作用域插槽將數(shù)據(jù)傳遞過去 -->
<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 = [];
//獲取當(dāng)前已實例化的插槽,然后去判斷插槽是否使用了
const slots = useSlots();
const openDialog = (type, row, column) => {
dialogFormVisible.value = true;
columns = column;
tp.value = type;
if (type === "edit") {
//如果編輯框設(shè)置了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組件已經(jīng)開始生成所有關(guān)于編輯的插槽,所以全部都有實例,需要給個默認(rèn)顯示,否則會空白 -->
<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"> 確認(rèn) </el-button>
</span>
</template>
</el-dialog>
</template>總結(jié)
到此這篇關(guān)于用vue3簡單封裝table組件的文章就介紹到這了,更多相關(guān)vue3封裝table組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue官方推薦AJAX組件axios.js使用方法詳解與API
axios是Vue官方推薦AJAX組件,下面為大家介紹axios.js庫的詳細(xì)使用方法與API介紹2018-10-10
vue如何實現(xiàn)路由跳轉(zhuǎn)到外部鏈接界面
這篇文章主要介紹了vue如何實現(xiàn)路由跳轉(zhuǎn)到外部鏈接界面,具有很好的參考價值,希望對大家有所幫助。2022-10-10
Vue.js分頁組件實現(xiàn):diVuePagination的使用詳解
這篇文章主要介紹了Vue.js分頁組件實現(xiàn):diVuePagination的使用詳解,需要的朋友可以參考下2018-01-01

