1 前言
由于项目中有很多菜单都是列表数据的展示,为避免太多重复代码,故将 Element Plus 的 Table 表格进行封装,实现通过配置展示列表数据
2 功能
- 支持自动获取表格数据
- 支持数据列配置及插槽
- 支持操作列配置及插槽
- 支持多选框配置
- 支持表尾配置及插槽
- 支持分页显示
3 实现步骤
3.1 复制基本表格
到 Element Plus官网复制一份最简单的 Table 代码,并删除多余代码
<template>
<el-table :data="tableData">
<el-table-column prop="date" label="Date" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="state" label="State" />
<el-table-column prop="city" label="City" />
<el-table-column prop="address" label="Address" />
<el-table-column prop="zip" label="Zip" />
<el-table-column fixed="right" label="Operations">
<template #default>
<el-button link type="primary" size="small" @click="handleClick">Detail</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036',
tag: 'Home'
},
{
date: '2016-05-02',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036',
tag: 'Office'
},
{
date: '2016-05-04',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036',
tag: 'Home'
},
{
date: '2016-05-01',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036',
tag: 'Office'
}
]
const handleClick = () => {
console.log('click')
}
</script>
3.2 支持自动获取表格数据
tableData 数据改为从 props.api 接口获取
3.3 支持数据列配置及插槽
3.3.1 自动生成列
数据列(除多选框与操作列外)通过 props.columns 自动生成
<el-table-column
v-for="item in props.columns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
:sortable="item.sortable ? 'custom' : false"
:width="item.width"
>
</el-table-column>
interface TableConfigInterface {
api: string // 表格数据获取接口
columns: {
// 显示列
prop: string // 键名
label?: string // 表头显示名称
formatter?: (row: unknown) => string // 自定义单元格格式化方法,参数为当前行数据
tooltip?: string // 表头 tooltip
sortable?: boolean // 是否可以排序
width?: number | string // 宽度
style?: string // 单元格样式
labelStyle?: string // 表头样式
}[]
}
3.2.2 支持表头自定义及插槽
- el-table-column 使用
<template #header>
自定义表头<slot :name="item.prop + 'Header'">
支持插槽- labelStyle 支持样式自定义
- 支持 tooltip
<el-table-column
v-for="item in props.columns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
:sortable="item.sortable ? 'custom' : false"
:width="item.width"
>
<template #header>
<slot :name="item.prop + 'Header'">
<div class="inline-flex" :style="item.labelStyle">
<span>{{ item.label }}</span>
<el-tooltip
popper-class="table-tooltip"
effect="dark"
placement="top-start"
:content="item.tooltip"
v-if="item.tooltip"
>
<el-icon><i-ep-Warning /></el-icon>
</el-tooltip>
</div>
</slot>
</template>
</el-table-column>
3.2.3 支持单元格自定义及插槽
- el-table-column 使用
<template #default="scope">
自定义单元格<slot :name="item.prop" :row="scope.row">
支持插槽- style 属性支持自定义样式
- formatter 方法支持自定义显示内容
<el-table-column
v-for="item in props.columns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
:sortable="item.sortable ? 'custom' : false"
:width="item.width"
>
<template #header>
<slot :name="item.prop + 'Header'">
<div class="inline-flex" :style="item.labelStyle">
<span>{{ item.label }}</span>
<el-tooltip
popper-class="table-tooltip"
effect="dark"
placement="top-start"
:content="item.tooltip"
v-if="item.tooltip"
>
<el-icon><i-ep-Warning /></el-icon>
</el-tooltip>
</div>
</slot>
</template>
<template #default="scope">
<slot :name="item.prop" :row="scope.row">
<div :style="item.style">
<span v-if="item.formatter">{{ item.formatter(scope.row) }}</span>
<span v-else>{{ scope.row[item.prop] }}</span>
</div>
</slot>
</template>
</el-table-column>
3.3 支持操作列配置及插槽
- el-table-column 使用
<template #default="scope">
自定义操作列<slot :name="item.prop" :row="scope.row">
支持插槽- visible 方法支持自定义按钮显示逻辑
<el-table-column
fixed="right"
label="操作"
:width="props.operation?.width"
v-if="props.operation?.columns"
>
<template #default="scope">
<slot name="operations" :row="scope.row">
<span v-for="item in props.operation?.columns" :key="item.text || item.icon">
<el-button
v-if="setVisible(scope.row, item.visible)"
:type="item.type"
:link="item.link"
:plain="item.plain"
@click="item.click(scope.row)"
size="small"
class="margin-right: 4px"
>
<el-icon v-if="item.icon" :class="item.icon"></el-icon>
{{ item.text }}
</el-button>
</span>
</slot>
</template>
</el-table-column>
// 操作框逻辑
const setVisible = (row: unknown, visible?: (row: unknown) => boolean) => {
if (!visible || visible(row)) {
return true
}
return false
}
3.4 支持多选框配置
<el-table>
增加 selection 列
<el-table-column fixed :selectable="setSelectable" type="selection" v-if="showSelectBox" />
TableConfigInterface 增加 rowKey、selectable
interface TableConfigInterface {
// ......
rowKey?: string // 行数据的 Key
selectable?: boolean | ((row: unknown) => boolean) // 当前行多选框是否可以勾选,参数为当前行数据,默认为 false
}
const props = withDefaults(defineProps<TableConfigInterface>(), {
rowKey: 'id',
})
// 多选框逻辑
const disabledList = reactive<string[]>([]) // 禁止勾选的数据
const showSelectBox = ***puted(() => props.selectable && disabledList.length < tableData.length)
const setSelectable = (row: unknown) => {
const selectable =
typeof props.selectable === 'boolean' ? props.selectable : props.selectable?.(row)
if (!selectable && !disabledList.includes(row?.[props.rowKey])) {
disabledList.push(row?.[props.rowKey])
}
return selectable
}
3.5 支持表尾配置及插槽
<el-table>
增加 @selection-change 及 ref 配置<slot name="footer">
支持插槽- visible 方法支持自定义按钮显示逻辑
<el-table
v-loading="loading"
:data="tableData"
@selection-change="handleSelectionChange"
table-layout="auto"
ref="tableRef"
>
<!-- ...... -->
</el-table>
<div v-if="showSelectBox" class="p-14">
<el-checkbox
v-model="isSelected"
@click="tableRef?.toggleAllSelection()"
:indeterminate="indeterminate"
label="全选"
style="vertical-align: middle; margin-right: 10px"
/>
<slot name="footer" :rows="selectionRows">
<span v-for="item in props.footer?.operations" :key="item.text || item.icon">
<el-button
v-if="item.visible ? item.visible() : true"
:type="item.type || 'primary'"
:link="item.link"
:plain="item.plain"
:disabled="!selectionRows.length"
@click="item.click(selectionRows)"
style="margin-left: 10px"
>
<el-icon v-if="item.icon" :class="item.icon"></el-icon>
{{ item.text }}
</el-button>
</span>
</slot>
</div>
const tableRef = ref()
const isSelected = ref(false) // 是否有选中数据
const selectionRows = ref<unknown[]>([]) // 当前选中的数据
const handleSelectionChange = (rows: unknown[]) => {
selectionRows.value = rows
isSelected.value = rows.length > 0
}
const indeterminate = ***puted(
() =>
selectionRows.value.length > 0 &&
selectionRows.value.length < tableData.length - disabledList.length
)
3.6 支持分页显示
最底部增加
<el-pagination></el-pagination>
<el-pagination
background
:total="tableData.length"
:layout="props.layout"
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@current-change="getTableData"
@size-change="getTableData"
class="p-y-20"
>
</el-pagination>
interface TableConfigInterface {
// ......
layout?: string // 组件布局
}
const pagination = ref({
currentPage: 1,
pageSize: 10
})
4 使用方法
<template>
<Table***ponent v-bind="tableConfig">
<template #nameHeader>
<div>姓名</div>
</template>
<template #name>
<div>Yana</div>
</template>
</Table***ponent>
</template>
<script setup lang="ts">
const tableConfig: TableConfigInterface = {
api: 'getTableData',
columns: [
{
prop: 'date',
label: 'Date',
tooltip: 'This is Date'
},
{
prop: 'name',
label: 'Name'
},
{
prop: 'state',
label: 'State'
},
{
prop: 'city',
label: 'City'
},
{
prop: 'address',
label: 'Address'
},
{
prop: 'zip',
label: 'Zip',
style: 'color: red',
labelStyle: 'color: red',
sortable: true
}
],
operation: {
columns: [
{
text: '编辑',
click: () => {},
visible: (row) => row.date === '2016-05-07'
}
]
},
rowKey: 'date',
selectable: (row) => row.date === '2016-05-07',
footer: {
operations: [
{
text: '删除',
click: () => {}
}
]
}
}
</script>
5 源码
<template>
<div v-loading="loading" class="table-wrapper">
<el-table
:data="tableData"
:max-height="props.maxHeight"
:default-sort="props.defaultSort"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
@row-click="goDetail"
:row-style="{ cursor: 'pointer' }"
table-layout="auto"
ref="tableRef"
>
<el-table-column fixed :selectable="setSelectable" type="selection" v-if="showSelectBox" />
<el-table-column
v-for="item in props.columns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
:sortable="item.sortable ? 'custom' : false"
:width="item.width"
>
<template #header>
<slot name="header">
<div class="inline-flex" :style="item.labelStyle">
<span>{{ item.label }}</span>
<el-tooltip
popper-class="table-tooltip"
effect="dark"
placement="top-start"
:content="item.tooltip"
v-if="item.tooltip"
>
<el-icon><i-ep-Warning /></el-icon>
</el-tooltip>
</div>
</slot>
</template>
<template #default="scope">
<slot :name="item.prop" :row="scope.row">
<div :style="item.style">
<span v-if="item.formatter">{{ item.formatter(scope.row) }}</span>
<span v-else>{{ scope.row[item.prop] }}</span>
</div>
</slot>
</template>
</el-table-column>
<el-table-column
fixed="right"
label="操作"
:width="props.operation?.width"
v-if="props.operation?.columns"
>
<template #default="scope">
<slot name="operations" :row="scope.row">
<span v-for="item in props.operation?.columns" :key="item.text || item.icon">
<el-button
v-if="setVisible(scope.row, item.visible)"
:type="item.type"
:link="item.link"
:plain="item.plain"
@click="item.click(scope.row)"
size="small"
style="margin-right: 4px"
>
<el-icon v-if="item.icon" :class="item.icon"></el-icon>
{{ item.text }}
</el-button>
</span>
</slot>
</template>
</el-table-column>
</el-table>
<div v-if="showSelectBox" class="p-14">
<el-checkbox
v-model="isSelected"
@click="tableRef?.toggleAllSelection()"
:indeterminate="indeterminate"
label="全选"
style="vertical-align: middle; margin-right: 10px"
/>
<slot name="footer" :rows="selectionRows">
<span v-for="item in props.footer?.operations" :key="item.text || item.icon">
<el-button
v-if="item.visible ? item.visible() : true"
:type="item.type || 'primary'"
:link="item.link"
:plain="item.plain"
:disabled="!selectionRows.length"
@click="item.click(selectionRows)"
style="margin-left: 10px"
>
<el-icon v-if="item.icon" :class="item.icon"></el-icon>
{{ item.text }}
</el-button>
</span>
</slot>
</div>
</div>
<el-pagination
background
:total="tableData.length"
:layout="props.layout"
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@current-change="getTableData"
@size-change="getTableData"
class="p-y-20"
/>
</template>
<script lang="ts" setup>
interface OperationInterface {
click: (row: unknown) => void // 按钮点击方法,参数为当前行数据
text?: string // 按钮显示文字
icon?: string // 按钮 icon
visible?: (row?: unknown) => boolean // 设置按钮是否可见,参数为当前行数据,默认为 true
type?: string // 按钮类型['primary'| 'su***ess'| 'warning'| 'danger'| 'info']
link?: boolean // 是否为链接按钮
plain?: boolean // 是否为朴素按钮
}
interface TableConfigInterface {
api: string // 表格数据获取接口
rowKey?: string // 行数据的 Key
columns: {
// 显示列
prop: string // 键名
label?: string // 表头显示名称
formatter?: (row: unknown) => string // 自定义单元格格式化方法,参数为当前行数据
tooltip?: string // 表头 tooltip
sortable?: boolean // 是否可以排序
width?: number | string // 宽度
style?: string // 单元格样式
labelStyle?: string // 表头样式
}[]
selectable?: boolean | ((row: unknown) => boolean) // 当前行多选框是否可以勾选,参数为当前行数据,默认为 false
operation?: {
// 操作列
columns: OperationInterface[]
width?: number | string // 宽度
}
footer?: {
// 操作列
operations: OperationInterface[]
}
defaultSort?: {
// 默认排序
prop: string // 默认排序的列
order?: string // ['ascending'| 'descending'], 没有指定 order, 则默认顺序是 ascending
}
maxHeight?: number | string // 表格最大高度
layout?: string
}
const props = withDefaults(defineProps<TableConfigInterface>(), {
rowKey: 'id',
layout: 'prev, pager, next, total'
})
const pagination = ref({
currentPage: 1,
pageSize: 10
})
const tableRef = ref()
let tableData = reactive<unknown[]>([])
// 多选框逻辑
const isSelected = ref(false) // 是否有选中数据
const selectionRows = ref<unknown[]>([]) // 当前选中的数据
const handleSelectionChange = (rows: unknown[]) => {
selectionRows.value = rows
isSelected.value = rows.length > 0
}
const disabledList = reactive<string[]>([]) // 禁止勾选的数据
const setSelectable = (row: unknown) => {
const selectable =
typeof props.selectable === 'boolean' ? props.selectable : props.selectable?.(row)
if (!selectable && !disabledList.includes(row?.[props.rowKey])) {
disabledList.push(row?.[props.rowKey])
}
return selectable
}
const indeterminate = ***puted(
() =>
selectionRows.value.length > 0 &&
selectionRows.value.length < tableData.length - disabledList.length
)
const showSelectBox = ***puted(() => props.selectable && disabledList.length < tableData.length)
// 操作框逻辑
const showOperation = ref(false)
const setVisible = (row: unknown, visible?: (row: unknown) => boolean) => {
if (!visible || visible(row)) {
showOperation.value = true
return true
}
return false
}
// 排序
const handleSortChange = (data: { prop: string; order: string | null }) => {
const { prop, order } = data
console.log(prop, order)
// getTableData
}
// 跳转详情页
const goDetail = (row: unknown) => {
console.log(row)
}
// 发送接口
const loading = ref(true)
const getTableData = () => {
loading.value = true
showOperation.value = false
tableData = [
{
date: '2016-05-02',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-03',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-04',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-05',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-06',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-07',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-08',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-09',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-10',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
},
{
date: '2016-05-11',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}
]
loading.value = false
}
getTableData()
</script>
<style lang="scss" scoped>
.table-wrapper {
border-top: 1px solid #eaeaea;
border-left: 1px solid #eaeaea;
border-right: 1px solid #eaeaea;
}
.inline-flex {
display: inline-flex;
align-items: center;
}
.p-14 {
border-bottom: 1px solid #eaeaea;
padding: 14px;
}
.p-y-20 {
padding-top: 20px;
padding-bottom: 20px;
justify-content: center;
}
</style>
<style lang="scss">
.table-tooltip {
max-width: 220px;
}
</style>