前言
在后台管理项目中,用到最多的就是表格了,当然也少不了查询、分页、等一些特殊数据的展示。
既然这些功能是比较常见且常用的,那么将它们封装成组件来复用,能节省不的事情。
接下来我们就来封装一个,具有表格、查询、分页、等多种功能的组件
效果展示
代码实现
接下来,我们就来一步一步的实现吧。
首先我们的组件需要一个配置文件
1、配置表格需要展示的字段
2、配置查询条件
详细的解释,都写在注释中。
我们这里就命名为table.ts
/*
首先是我们的 表格的配置(数组对象)
每个对象中有三个必须的属性
prop:对应的字段名称
label:表格表头展示名称
width/min_width:列宽(width和min_width自选其一)(width就是固定款度,min_width最小宽度)
扩展属性
align:列的对齐方式(left、center、right)默认left
isEdit:(默认false,为true时开始单元格双击编辑功能)
type:(列展示格式)具体看以下举例
show:控制列的显示或隐藏(这里不需要单独写出来,在组件里会自己去添加)
type:time(后端返回的字段为时间戳,需要我们自己格式化时间)
{
prop: 'createDate',
label: ' 创建时间',
align: 'center',
type: 'time',
width: 180
}
type:image(改字段需要以图片的形式展示)
{
prop: 'avatar',
label: '头像',
align: 'center',
type: 'image',
width: 80
},
type:switch(switch开关,一般用于控制该条数据的状态)
{
prop: 'userStatus',
label: '用户状态',
align: 'center',
type: 'switch',
activeValue: 1, // switch为打开时的值
inactiveValue: 2,// switch为关闭时的值
width: 100
},
type:status(状态展示,比如这时候后端返回的状态对应的值时1或者是2,肯定是需要转为中文展示的)
{
prop: 'userStatus',
label: '用户状态',
align: 'center',
width: 100,
type: 'status',
option: {
'1': '启用',
'2': '禁用',
},
color: {
'1': 'success',
'2': 'info',
}
}
以上就是表格配置信息
*/
const columnsData: any = [
{
prop: 'menuPowerName',
label: '菜单权限名称',
isEdit: true,
min_width: 120,
},
{
prop: 'menuPowerMark',
label: '菜单权限标识',
align: 'center',
width: 180,
},
{
prop: 'menuName',
label: '所属菜单名称',
align: 'center',
width: 180,
},
{
prop: 'createDate',
label: ' 创建时间',
align: 'center',
type: 'time',
width: 180
}
]
// 表格查询配置中 type:select 的测试数据
let dataList = [
{
"id": 1,
"menuName": "首页",
"menuPath": "/",
"menuType": 2,
"menuStatus": 1,
"parentId": 0,
"parentName": null,
"createDate": 1682435267868
},
{
"id": 2,
"menuName": "系统管理",
"menuPath": "/system",
"menuType": 1,
"menuStatus": 1,
"parentId": 0,
"parentName": null,
"createDate": 1682435338368
},
{
"id": 3,
"menuName": "用户管理",
"menuPath": "user",
"menuType": 2,
"menuStatus": 1,
"parentId": 2,
"parentName": "系统管理",
"createDate": 1682436009335
},
{
"id": 4,
"menuName": "角色管理",
"menuPath": "role",
"menuType": 2,
"menuStatus": 1,
"parentId": 2,
"parentName": "系统管理",
"createDate": 1682436073984
},
{
"id": 5,
"menuName": "菜单管理",
"menuPath": "menu",
"menuType": 2,
"menuStatus": 1,
"parentId": 2,
"parentName": "系统管理",
"createDate": 1682471621692
}
]
/*
接下来我们来说一下查询的配置
我们查询时传给后端的数据大多是是这样的
{
name:'张三,
age:1
}
下面我只配置了三种情况
text 就是用户数据关键字查询
select 是用户选择指定数据查询
dateTime 根据时间查询
text 和 dateTime 没什么可说的,重要的是select
我们之间来解析一条
{ label: "所属菜单", prop: 'menuId', type: 'select', options: dataList, valueKey: 'id', labelKey: 'menuName' },
label 对应的是我们的名称
prop 对应的则是字段
type 类型就不说了,上面说了,目前就三种
options:(数组对象)上面我会贴出测试数据
我们这个查询是作用在select下拉菜单上的,且每次数组对象中的数据都是不同的,我们就需要两个变量去存对应的字段
valueKey:select选择的值
labelKey:select选择值对应的名称
*/
const queryData: any = [
{ label: "权限名称", prop: 'menuPowerName', type: 'text' },
{ label: "所属菜单", prop: 'menuId', type: 'select', options: dataList, valueKey: 'id', labelKey: 'menuName' },
{ label: "创建时间", prop: 'dateTime', type: 'date' },
]
export {
columnsData,
queryData
}
下面就来实现对整个组件的封装
注释依旧在代码中
components下命名为TableModule.vue
<template>
<div class="table-module">
<!-- 查询 -->
<!--
实现:通过我们配置好的 查询条件
首先去创建form表单,根据我们配置的查询条件去做一个循环判断,展示出不用类型所对应不同的输入框
比如:text对应普通的输入框,select对应下拉选择,dateTime对应日期时间选择器
在使用时,父组件会传来一个queryForm空的对象,
循环出来的输入框会绑定表格配置中的prop字段绑定在queryForm对象中
-->
<div class="query" ref="queryRef">
<div class="query-content">
<el-form :model="props.tableModule.queryForm" style="display: flex;" label-position="left"
class="query-form">
<el-form-item :label="item.label" style="margin-right: 20px; margin-bottom: 0px;"
v-for="item in props.tableModule.query" :key="item.prop">
<el-input v-model="props.tableModule.queryForm[item.prop]" :placeholder="'输入' + item.label + '关键字'"
clearable v-if="item.type == 'text'"></el-input>
<el-select v-model="props.tableModule.queryForm[item.prop]" clearable
:placeholder="'选择' + item.label + '关键字'" v-else-if="item.type == 'select'">
<el-option v-for="cItem in item.options" :key="cItem[item.valueKey]"
:label="cItem[item.labelKey]" :value="cItem[item.valueKey]" />
</el-select>
<el-date-picker v-model="props.tableModule.queryForm[item.prop]" clearable type="datetimerange"
format="YYYY/MM/DD hh:mm:ss" value-format="x" range-separator="至" start-placeholder="开始时间"
end-placeholder="结束时间" v-else-if="item.type == 'date'" />
</el-form-item>
</el-form>
</div>
<div class="slot">
<el-button @click="Search" type="primary" plain>查询</el-button>
<el-button @click="Recover">重置</el-button>
<!-- slot插槽(用来添加表格其他操作,比如,新增数据,删除数据等其他操作) -->
<slot name="event"></slot>
<!--
动态表头显示,根据表格每条配置项中的show字段来决定改列是否显示或者隐藏
columns 就是我们表格配置的数组对象
-->
<el-popover placement="bottom" title="表格配置" :width="200" trigger="click">
<div v-for="(item, index) in props.tableModule.columns" :key="index">
<el-checkbox v-model="item.show" :label="item.label" :true-label="1" :false-label="0" />
</div>
<template #reference>
<!-- 一个Element Plus中的图标 -->
<el-button :icon="Operation"></el-button>
</template>
</el-popover>
</div>
</div>
<!-- 表格 -->
<!-- style中是计算表格的高度的 -->
<el-table :data="props.tableModule.dataList" border height="100%"
:style="{ 'height': `calc(100vh - ${queryHeight + 156}px)` }" v-loading="props.tableModule.loading"
:row-class-name="tableRowClassName" :cell-class-name="tableCellClassName" @cell-dblclick="cellDblClick"
id="el-table" ref="tableRef">
<el-table-column type="selection" width="50" align="center" />
<!-- columns表格配置数据 -->
<template v-for="(item, index) in props.tableModule.columns">
<!-- 循环 columns 紧接着判断每个字段的类型 -->
<el-table-column :prop="item.prop" :label="item.label" :align="item.align || 'left'" :width="item.width"
:min-width="item.min_width" :fixed="item.fixed" v-if="item.show">
<template slot-scope="scope" #default="scope">
<!-- switch时使用switch开关组件,并且绑定好我们配置的属性 -->
<div v-if="item.type == 'switch'">
<el-switch v-model="scope.row[item.prop]" :active-value="item.activeValue"
:inactive-value="item.inactiveValue" @change="props.tableModule.switchChange(scope.row)">
</el-switch>
</div>
<!-- status 时 使用fieldChange方法匹配出值对应的名称并返回 -->
<div v-else-if="item.type == 'status'">
<el-tag>{{
fieldChange(scope.row[item.prop], item.option) }}</el-tag>
</div>
<!-- image 就是使用 el-image展示我们的图片咯 -->
<div v-else-if="item.type == 'image'">
<el-image style="width: 60px; height: 60px" :src="scope.row[item.prop]"
:preview-src-list="[scope.row[item.prop]]" :preview-teleported="true">
</el-image>
</div>
<!-- formatDate 方法将时间戳格式化为年月日时分秒的格式 -->
<div v-else-if="item.type == 'time'">{{ formatDate(scope.row[item.prop]) }}</div>
<!-- 可编辑单元格 -->
<div v-else-if="item.isEdit">
<el-input v-model="scope.row[item.prop]" :placeholder="'请输入' + item.label"
@blur="inputBlur(scope.row)" autofocus ref="inputRef"
v-if="scope.row['Indexs'] == rowIndex && scope.column['Indexs'] == columnIndex" />
<div v-else>{{ scope.row[item.prop] }}</div>
</div>
<!-- 类型都不匹配时直接展示 -->
<div v-else>{{ scope.row[item.prop] }}</div>
</template>
</el-table-column>
</template>
<!-- 这里的插槽用于列表的操作列 -->
<slot name="tableColumn"></slot>
</el-table>
<!-- 分页 -->
<!-- 分页这里没什么可说的,父组件传三个参数,当前页,每页条数,总条数就可以了。 -->
<div class="page">
<el-pagination :current-page="props.tableModule.pages.page" :page-size.sync="props.tableModule.pages.limit"
:page-sizes="pageSizes" :layout="layout" :total="props.tableModule.pages.total" @size-change="sizeChange"
@current-change="currentChange" />
</div>
</div>
</template>
<script setup>
import { defineProps, onMounted, reactive, toRefs, ref } from 'vue'
import { formatDate } from '@/utils/index' // 自己的格式化时间的方法
import { ElTable } from 'element-plus';
import { Operation } from '@element-plus/icons-vue'
const props = defineProps({
tableModule: Object, // 由父组件传递而来
layout: { // 分页功能配置
type: String,
default: "total, sizes, prev, pager, next, jumper",
},
pageSizes: { // 分页:每页条数配置
type: Array,
default() {
return [10, 20, 30, 50];
},
},
})
const state = reactive({
rowIndex: 0, // 行索引 用于可编辑单元格
columnIndex: 0, // 列索引 用于可编辑单元格
queryHeight: 0,
})
const {
rowIndex,
columnIndex,
queryHeight,
} = toRefs(state)
const queryRef = ref(null);
onMounted(() => {
// 这里拿到query模块的高度,适配页面高度的
setTimeout(() => {
state.queryHeight = queryRef.value.clientHeight
}, 100);
// 为每个表格配置项添加show属性,默认1为显示状态
props.tableModule.columns.forEach(item => {
item.show = 1
})
})
function fieldChange(row, option) {
if (option[row]) {
return option[row]
}
}
// 编辑单元格 ----------
// 为每一行返回固定的className
function tableRowClassName({ row, rowIndex }) {
row.Indexs = rowIndex;
}
// 为所有单元格设置一个固定的 className
function tableCellClassName({ column, columnIndex }) {
column.Indexs = columnIndex;
}
// el-table单元格双击事件
function cellDblClick(row, column, cell, event) {
state.rowIndex = row.Indexs
state.columnIndex = column.Indexs
}
// input失去焦点
function inputBlur(row) {
state.rowIndex = 0
state.columnIndex = 0
props.tableModule.editInputBlur() // 父组件的方法,
}
// 每页条数切换时触发
function sizeChange(item) {
props.tableModule.pages.limit = item
props.tableModule.callback() // 父组件绑定的回调
}
// 页数切换时触发
function currentChange(item) {
props.tableModule.pages.page = item
props.tableModule.callback()
}
// 点击查询按钮时触发
function Search() {
props.tableModule.callback()
}
// 点击重制触发
function Recover() {
props.tableModule.queryForm = {}
props.tableModule.callback()
}
</script>
<style scoped lang="scss">
.table-module {
.query {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 10px;
.query-content {
display: flex;
align-items: flex-start;
.query-form {
.el-form-item {
margin: 0px;
margin-right: 20px;
}
.el-input {
width: 200px;
}
::v-deep(.el-select) {
.el-input {
width: 200px;
}
}
}
}
.slot {
display: flex;
justify-content: flex-end;
padding-right: 48px;
}
}
.page {
margin-top: 10px;
}
}
</style>
接下来就到我们组件的使用了。
index.vue
<template>
<div class="">
<!-- TableModule tableModule是一个对象,下面有解释 -->
<TableModule :tableModule="tableModule" ref="TableModuleRef">
<!-- #event是插入的新增按钮和删除按钮,具体操作按照自己的业务流程实现 -->
<template #event>
<el-button type="primary" @click="Add">新增</el-button>
<el-button type="danger" @click="Delete">删除</el-button>
</template>
<!-- #tableColumn 插入表格的操作列 -->
<template #tableColumn>
<el-table-column align="center" fixed="right" label="操作" width="120">
<template slot-scope="scope" #default="scope">
<el-button @click="Edit(scope)">编辑</el-button>
</template>
</el-table-column>
</template>
</TableModule>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs, ref, onMounted } from 'vue'
import { columnsData, queryData } from './table' // 引入我们配置好的数据
import { menuPowerList } from '@/api/menuPower/index'
const state = reactive({
columns: columnsData, // 表格配置
query: queryData, // 查询条件配置
queryForm: {}, // 查询form表单
loading: false, // 加载状态
dataList: [], // 列表数据
pages: { // 分页数据
total: 0,
limit: 20,
page: 1,
}
})
const { loading, dataList, columns, pages, query, queryForm } = toRefs(state)
const TableModuleRef = ref()
onMounted(() => {
getDataList()
})
// 传给子组件的
const tableModule = ref<Object>({
editInputBlur: editInputBlur, // 可编辑单元的,失去焦点时的回调
callback: getDataList, // 回调,子组件中可以看到很多调用callback的,这里对应的是获取列表数据的方法
// 以下不说了,上面都给解释了
queryForm: queryForm,
columns: columns,
dataList: dataList,
loading: loading,
pages: pages,
query: query,
})
// 获取列表信息
async function getDataList() {
state.loading = true
// 掉自己的接口,切勿复制粘贴
const res = await menuPowerList({ pages: state.pages, query: state.queryForm })
state.loading = false
state.dataList = res.data
state.pages.total = res.total
}
function Add() { // 新增事件
}
function Edit(row: any) { // 编辑事件
console.log(row)
}
function Delete() { // 删除事件
}
function editInputBlur() {
console.log('失去焦点回调')
}
</script>
<style scoped></style>