vue3通用表格组件的由来,前面讲过后台管理页面中查询展示表格数据基本上是必备功能。前面我发布过一篇使用vue2+element-ui封装的通用页面组件。原文可访问这篇文章 还在写重复的增删改查的重复代码?还在重复写Ajax请求?Vue2+Element-ui实现常规后台查询展示页面来了
在vue3普及的现在。这篇文章将使用vue3和element-plus。在el-table组件的基础上二次封装表格的常用展示形式。简化我们的代码使用方式。减少重复代码量,提高一定的开发效率。下面我们来一起看看我们的实现思路。
首先我们来初始化一个vue3的项目
初始化vue3的项目我们使用vite+vue3+ts,这里就不多叙述了,大家可以看这篇文章: 初始化
我们先来看看表格的基本效果:
我们来列举下表格的几个常用功能
1.基础文本展示
2.枚举类型展示 如:(状态,分类,标签,等根据status或type等字段展示对应的内容)
3.文本过长超出省略号
4.鼠标悬浮展示
5.操作区域 如:(查看,编辑,删除等操作)
根据以上的功能我们来在el-table的基础上把他封装到一个通用组件中
这里我们使用tsx函数组件的方式来实现,这样避免在template模版中使用大量的if else 哦我
我们先来定义一个table组件 在components文件中定义一个mijiTable.tsx的组件
首先我们在组件中引入element-plus的el-table、el-table-column组件
在这个组件中我们使用defineComponent来定义一个组件,使用组合式api setup来实现内部逻辑
这个组件我们接受两个值,一个是用来渲染el-table-column的数据 columns,一个是表格要渲染的数据 dataSource
我们来看看columns字段都包含了什么:
其实columns的内容字段就是 el-table-column组件的属性我们在这个基础上扩展一个slot字段用来自定义单元格内容
[{
prop: '',
label: '操作',
type: '', //
slot: 'options',
width: 120,
fixed: 'right'
}]
在element-plus中 type属性默认值改成了default我们这里的type不用做el-table-column的type属性。而是用做区分表格列是什么格式。目前我们实现的有以下几种
常规 text、index序号、枚举、和操作栏
我们来看看如何动态渲染el-table-colunm
import {
ElTable,
ElTableColumn
} from 'element-plus'
export default defineComponent({
props: {
columns: {
type: Array,
default: () => []
},
dataSource: {
type: Array,
default: () => []
}
},
setup(props, { slots }: any) {
const { columns, dataSource } = props
console.log('渲染表格列的数据', columns)
console.log('表格数据:', dataSource);
const tableData = ref()
watch(() => props.dataSource, (now) => {
tableData.value = now
})
const initTable = () => {
return <ElTable
data={tableData.value}
style={{ width: "100%" }}>
</ElTable>
}
return () => initTable()
}
})
基础的文本渲染没有什么特殊的还是使用el-table-column组件就好
<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
>
{{ ...tableCellSlot }}
</ElTableColumn>
这里我就不列举官方的这些属性了,大家可以去官方文档查看: el-table-column属性
下面我们看看如何动态渲染不同类型的table-column
我们在el-table中写如下代码:利用数组的map去生成el-table-column组件,在map中我们来判断当前的column是什么类型的 然后动态的返回就好。
{
columns.map((col: any) => {
const colWidth = col.width || '';
const minWidth = col.minWidth || '';
const fixed = col.fixed || false;
const showOverflowTooltip = col.showOverflowTooltip || false
const length = col.options && col.options.length || null
const headerSlot = genHeaderSlot(col); // 自定义表头
const defaultSlot = genDefaultSlot(col); // 自定义表格单元格
const tableCellSlot = {
...headerSlot,
...defaultSlot,
}
// 序号
if (col.prop === 'index') {
return (<ElTableColumn
label={col.label}
type="index"
className={col.className}
width={55}
/>);
}
switch (col.type) {
// 操作栏
case 'options':
return (
<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth || (length > 3 ? '180px' : '160px')}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
formatter={(row) => {
let btns, moreBtn
if (length > 3) {
btns = col.options.slice(0, 2)
moreBtn = col.options.slice(2, length)
} else {
btns = col.options
}
return length > 3 ? renderOptionsAndMore(btns, moreBtn, row) : renderOptions(btns, row)
}}
>
{{ ...tableCellSlot }}
</ElTableColumn>
);
// 枚举类型的表格单元格
case 'map':
if (col.typeMap) { // 状态枚举
return (<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
formatter={(row): any => {
return renderTableMapCell(row, col);
}}
>
{{ ...tableCellSlot }}
</ElTableColumn>);
} else { // 普通枚举
return (<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
formatter={(row) => {
return col.map[row[col.prop]];
}}
>
{{ ...tableCellSlot }}
</ElTableColumn>);
}
default:
return (
<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
>
{{ ...tableCellSlot }}
</ElTableColumn>
);
}
})
}
index类型就不过多解释了,来看看column中的options类型,也就是表格的操作栏,
操作栏动态计算高度或者通过传入的宽度来设置宽度,这里的按钮我们通过 options 类型中的options数组来渲染
const buttonEvents = (v: any, row: any) => {
if (v.clickEvent) {
v.clickEvent(row)
} else {
alert('请传入clickEvent事件参数')
}
}
const renderButton = (v: any, row: any) => {
return <ElButton
text
type={v.type}
disabled={v.disabled && v.disabled(row)}
class={v.className}
style={{ padding: '5px' }}
onClick={() => buttonEvents(v, row)}
>
{v.text}
</ElButton>
}
// 渲染按钮
const renderOptions = (btns: any, row: any) => {
return btns && btns.map((v: any) => {
return renderButton(v, row)
})
}
// 渲染更多按钮
const renderOptionsAndMore = (btns: any, moreBtn: any, row: any) => {
return (<div>
{renderOptions(btns, row)}
{
<ElDropdown v-slots={{
dropdown: () => <ElDropdownMenu>
{moreBtn.map((v: any) => {
return <ElDropdownItem>
{renderButton(v, row)}
</ElDropdownItem>
})}
</ElDropdownMenu>
}}>
<ElButton style={{ marginLeft: '10px', padding: '5px' }} type="primary" text>
更多<ElIcon><ArrowDownBold /></ElIcon>
</ElButton>
</ElDropdown>
}
</div>)
}
除了options类型还有枚举和插槽这两种形式,插槽的就相对简单了。
// 根据自定义表头的配置,动态生成需要的scopedSlot对象
const genHeaderSlot = (col: any) => {
if (col.headerSlot) {
return {
header: () => {
return slots.headerSlot && slots.headerSlot();
}
};
}
return {};
}
// 自定义表格单元格的slot显示
const genDefaultSlot = (col: any) => {
if (col.slot) {
return {
// scope 是当前渲染单元格的数据
default: (scope: any) => {
return slots[col.slot] && slots[col.slot](scope);
}
}
}
}
在table-column 中我们这样使用的
const headerSlot = genHeaderSlot(col); // 自定义表头
const defaultSlot = genDefaultSlot(col); // 自定义表格单元格
const tableCellSlot = {
...headerSlot,
...defaultSlot,
}
<ElTableColumn
prop={col.prop}
label={col.label}
>
{{ ...tableCellSlot }} //这里展开插槽的对象默认渲染的 是 default: () => col.slot()
</ElTableColumn>
枚举的形式我们以普通的和带徽标的为例,枚举的我们要额外接收两个参数,map和typeMap,map为当前数据所要映射的内容,typeMap为带徽标组件的类型'primary' | 'success' | 'warning' | 'danger' | 'info' 你们也可以自行拓展其他的组件,如tag等展示型组件
// 渲染 type: map 的单元格
const renderTableMapCell = (row: any, col: any) => {
if (col.map[row[col.prop]]) {
return <ElBadge
is-dot={true}
type={col.typeMap[row[col.prop]]}
dot-position="left-middle"
>
{col.map[row[col.prop]]}
</ElBadge>;
}
return null;
}
到这里我们基础的使用列已经封装好了,下面我们来看看具体的使用方式。
使用方法
可以全局引入注册全局组件,也可以在单独页面中引入。这里我们看下全局组件
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入表格组件
import MiJiTable from './components/mijiTable'
const app = createApp(App)
// 这里通过app.component注册表格组件为全局组件
app.component(MiJiTable.name, MiJiTable)
app.use(router).use(ElementPlus).mount('#app')
这里MiJiTable.name就是组件中的name属性。miji-table
这样我们在页面中就可以直接使用<miji-table></miji-table>
<template>
<miji-table :columns="columns" :dataSource="dataSource"></miji-table>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
// 我们初始化的时候定义一个columns 定义好要渲染表格的列有哪些。
const columns = ref([
{ prop: "index", label: "序号", width: "80px" },
{ prop: "date", label: "Date" },
{ prop: "name", label: "Name" },
{
prop: "state",
label: "State",
},
{ prop: "city", label: "City", minWidth: "120px" },
{ prop: "address", label: "Address", minWidth: "120px" },
{ prop: "zip", label: "Zip", minWidth: "120px" },
{ prop: "tag", label: "Tag", minWidth: "120px" },
{
prop: "option",
label: "操作",
fixed: "right",
type: "options",
options: [
{
type: "primary",
text: "查看",
clickEvent: (row) => optionsFunction(row),
}
],
},
]);
// 定义 表格数据的变量
const dataSource = ref([]);
const optionsFunction = (row) => {
console.log(row);
};
onMounted(() => {
setTimeout(() => {
dataSource.value = [] // 这里做请求数据填充
}, 1000);
});
</script>
完整代码:如下
import { watch, defineComponent, ref } from 'vue'
import {
ElTable,
ElTableColumn,
ElButton,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ElBadge,
ElIcon
} from 'element-plus'
import { ArrowDownBold } from '@element-plus/icons-vue'
import { tableProps } from './props'
export default defineComponent({
props: tableProps,
setup(props, { slots }: any) {
const { columns, dataSource } = props
console.log('表格数据:', dataSource);
const tableData = ref()
watch(() => props.dataSource, (now) => {
tableData.value = now
})
const buttonEvents = (v: any, row: any) => {
if (v.clickEvent) {
v.clickEvent(row)
} else {
alert('请传入clickEvent事件参数')
}
}
const renderButton = (v: any, row: any) => {
return <ElButton
text
type={v.type}
disabled={v.disabled && v.disabled(row)}
class={v.className}
style={{ padding: '5px' }}
onClick={() => buttonEvents(v, row)}
>
{v.text}
</ElButton>
}
// 渲染按钮
const renderOptions = (btns: any, row: any) => {
return btns && btns.map((v: any) => {
return renderButton(v, row)
})
}
// 渲染更多按钮
const renderOptionsAndMore = (btns: any, moreBtn: any, row: any) => {
return (<div>
{renderOptions(btns, row)}
{
<ElDropdown v-slots={{
dropdown: () => <ElDropdownMenu>
{moreBtn.map((v: any) => {
return <ElDropdownItem>
{renderButton(v, row)}
</ElDropdownItem>
})}
</ElDropdownMenu>
}}>
<ElButton style={{ marginLeft: '10px', padding: '5px' }} type="primary" text>
更多<ElIcon><ArrowDownBold /></ElIcon>
</ElButton>
</ElDropdown>
}
</div>)
}
// 渲染 type: map 的单元格
const renderTableMapCell = (row: any, col: any) => {
if (col.map[row[col.prop]]) {
return <ElBadge
is-dot={true}
type={col.typeMap[row[col.prop]]}
dot-position="left-middle"
>
{col.map[row[col.prop]]}
</ElBadge>;
}
return null;
}
// 根据自定义表头的配置,动态生成需要的scopedSlot对象
const genHeaderSlot = (col: any) => {
if (col.headerSlot) {
return {
header: () => {
return slots.headerSlot && slots.headerSlot();
}
};
}
return {};
}
// 自定义表格单元格的slot显示
const genDefaultSlot = (col: any) => {
if (col.slot) {
return {
// scope 是当前渲染单元格的数据
default: (scope: any) => {
return slots[col.slot] && slots[col.slot](scope);
}
}
}
}
const initTable = () => {
return <ElTable
data={tableData.value}
style={{ width: "100%" }}>
{
columns.map((col: any) => {
const colWidth = col.width || '';
const minWidth = col.minWidth || '';
const fixed = col.fixed || false;
const showOverflowTooltip = col.showOverflowTooltip || false
const length = col.options && col.options.length || null
const headerSlot = genHeaderSlot(col); // 自定义表头
const defaultSlot = genDefaultSlot(col); // 自定义表格单元格
const tableCellSlot = {
...headerSlot,
...defaultSlot,
}
// 序号
if (col.prop === 'index') {
return (<ElTableColumn
label={col.label}
type="index"
className={col.className}
width={55}
/>);
}
switch (col.type) {
// 操作栏
case 'options':
return (
<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth || (length > 3 ? '180px' : '160px')}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
formatter={(row) => {
let btns, moreBtn
if (length > 3) {
btns = col.options.slice(0, 2)
moreBtn = col.options.slice(2, length)
} else {
btns = col.options
}
return length > 3 ? renderOptionsAndMore(btns, moreBtn, row) : renderOptions(btns, row)
}}
>
{{ ...tableCellSlot }}
</ElTableColumn>
);
// 枚举类型的表格单元格
case 'map':
if (col.typeMap) { // 状态枚举
return (<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
formatter={(row): any => {
return renderTableMapCell(row, col);
}}
>
{{ ...tableCellSlot }}
</ElTableColumn>);
} else { // 普通枚举
return (<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
formatter={(row) => {
return col.map[row[col.prop]];
}}
>
{{ ...tableCellSlot }}
</ElTableColumn>);
}
default:
return (
<ElTableColumn
prop={col.prop}
label={col.label}
className={col.className}
width={colWidth}
min-width={minWidth}
show-overflow-tooltip={showOverflowTooltip}
fixed={fixed}
>
{{ ...tableCellSlot }}
</ElTableColumn>
);
}
})
}
</ElTable>
}
return () => initTable()
}
})
以上就是常规功能表格的二次封装,下篇文章我会在这个基础上封装一个常规后台页面级别的组件,
欢迎大家私信留言,多多点评
可以扫描下方二维码关注我的公众号或添加我的微信联系、沟通。