很多中后台业务的系统中,表格是最高频的组件之一,其中一般包括搜索条件、表格展示、表格操作列、分页等。那么我们二次封装的这个表格组件就需要包含以下几个功能点:
1、数据自动获取和刷新
2、自定义列配置
3、分页功能
4、根据搜索条件进行搜索功能
5、加载中状态和空数据状态
一、先上页面最终效果
二、创建目录yxt-table如下图
index.vue为父级页面
yxt-table.vue为表格组件
二、数据自动获取和刷新
因为表格的数据一般都比较简单,就是根据搜索条件,调用接口请求一个到列表,然后将列表数据一一展示到表格上,再对特定的列进行一些过滤转化等个性化操作。但是万变不离其宗,这个步骤基本每个表格都要进行一遍,所以考虑到通用性(其实是为了偷懒),将请求接口获取数据这一步放在组件里面实现。
created () {
this.getData()
},
methods: {
getData () {
const fun = this.apiUrl
fun().then(res => {
this.tableData = res[this.otherConfig.list] || []
this.tableTotal = res.pageInfo?.total || 0
})
}
}
三、自定义列配置
组件接收一个数组作为自定义列
tableColumn: [
{ prop: 'name', label: '名称' },
{ prop: 'code', label: '编码' },
{ prop: 'status', label: '状态' }
]
index.vue
<!-- index.vue -->
<template>
<div>
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"></yxt-table>
</div>
</template>
<!-- index.vue -->
<script>
import yxtTable from './yxt-table.vue'
import { yxtTableList } from 'https/yxtDemo.js'
export default {
name: 'yxtDemoTable',
components: {
yxtTable
},
data () {
return {
yxtTableList,
tableColumn: [
{ prop: 'name', label: '名称' },
{ prop: 'code', label: '编码' },
{ prop: 'status', label: '状态' }
]
}
}
}
</script>
yxt-table.vue
<!-- yxt-table.vue -->
<template>
<div>
<el-table :data="tableData">
<el-table-column v-for="item in tableColumn"
:key="item.prop"
:prop="item.prop"
:label="item.label"></el-table-column>
</el-table>
</div>
</template>
<!-- yxt-table.vue -->
<script>
export default {
name: 'yxtTable',
props: {
apiUrl: { // 列表接口(必填)
type: Function,
required: true
},
tableColumn: { // 自定义列配置
type: Array,
default: () => []
},
otherConfig: { //
type: Object,
default: () => {
return {
list: 'list'
}
}
}
},
data () {
return {
tableData: []
}
},
created () {
this.getData()
},
methods: {
getData () {
const fun = this.apiUrl
fun().then(res => {
this.tableData = res[this.otherConfig.list] || []
this.tableTotal = res.pageInfo?.total || 0
})
}
}
}
</script>
至此,一个表格可以实现了
1、otherConfig说明
由于我们的接口请求放在组件里面了,但是我们对接的接口可能由于业务的不同项目组的不同开发人员的不同,而导致接口返回的列表的字段名不同,这里通过传参的形式做一下兼容
2、上面这样只能实现简单的展示功能,还有些数据比如 状态1 需要转化为打卡成功,状态0 需要转化为打卡失败进行显示,这类需求可以通过filter进行转化
<!-- yxt-table.vue -->
<el-table-column v-for="item in tableColumn"
:key="item.prop"
:prop="item.prop"
:label="item.label">
<template v-slot:default="scope">
<div v-if="item.dictCode">
{{ scope.row[item.prop] | filterStatus(dict[item.dictCode]) }}
</div>
<div v-else>
{{ scope.row[item.prop] }}
</div>
</template>
</el-table-column>
props: {
dict: { // 全部字典
type: Object,
default: () => {}
}
},
filters: {
filterStatus (value, array, code = 'code', name = 'name') {
if (!value && value !== 0) { // 要把0摘出来,一般0都是正常的数据,所以不能只用 !value
return ''
}
const find = array.find(e => (e[code] === value.toString()) || (e[code] === +value)) // 字符型数值型都得匹配
if (find) {
return find[name]
} else { // 没有匹配的就原样返回
return value
}
}
},
<!-- index.vue -->
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:dict="dict"></yxt-table>
data () {
return {
tableColumn: [
{ prop: 'name', label: '名称' },
{ prop: 'code', label: '编码' },
{ prop: 'status', label: '状态', dictCode: 'status' }
],
dict: {
status: [
{ code: 0, name: '打卡失败' },
{ code: 1, name: '打卡成功' }
]
}
}
}
这里dict设置为对象的原因是为了装进更多字典
3、思考一下,如果要在表格中展示这样的自定义图标怎么办?
使用插槽slot,在tableColumn里面设置某行属性的slot为true,改造el-table-column如下:
<!-- yxt-table.vue -->
<el-table-column v-for="(item, index) in tableColumn"
:key="index"
:prop="item.prop"
:label="item.label">
<template v-if="item.slot"
v-slot:default="scope">
<slot :name="item.prop"
:row="scope.row"
:index="scope.$index"></slot>
</template>
<template v-else v-slot:default="scope">
<div v-if="item.dictCode">
{{ scope.row[item.prop] | filterStatus(dict[item.dictCode]) }}
</div>
<div v-else>
{{ scope.row[item.prop] }}
</div>
</template>
</el-table-column>
<!-- index.vue -->
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:otherConfig="otherConfig"
:dict="dict">
<template v-slot:icon="{row, index}">
<i :class="row.status ? 'el-icon-circle-check' : 'el-icon-circle-close'"></i>
</template>
</yxt-table>
data () {
return {
tableColumn: [
{ prop: 'name', label: '名称' },
{ prop: 'code', label: '编码' },
{ prop: 'status', label: '状态', dictCode: 'status' },
{ prop: 'icon', label: '图标', slot: true }
]
}
}
4、在实际项目中,除了字典转化,还有一些比较定制化的展示需求,这个可以通过传入一个函数format进行计算,然后在这个方法里面将最后的计算结果return
<!-- yxt-table.vue -->
<el-table-column v-for="(item, index) in tableColumn"
:key="index"
:prop="item.prop"
:label="item.label">
<template v-if="item.slot"
v-slot:default="scope">
<slot :name="item.prop"
:row="scope.row"
:index="scope.$index"></slot>
</template>
<template v-else v-slot:default="scope">
<div v-if="item.dictCode">
{{ scope.row[item.prop] | filterStatus(dict[item.dictCode]) }}
</div>
<div v-else-if="item.format">
{{ item.format(scope.row) }}
</div>
<div v-else>
{{ scope.row[item.prop] }}
</div>
</template>
</el-table-column>
<!-- index.vue -->
data () {
return {
tableColumn: [
{ prop: 'name', label: '名称' },
{ prop: 'code', label: '编码' },
{ prop: 'status', label: '状态', dictCode: 'status' },
{ prop: 'icon', label: '图标', slot: true },
{ prop: 'phone',
label: '电话号码',
format: (row) => {
return `${row.name}-${row.code}(${row.phone})`
} }
]
}
}
5、表格一般还有批量操作,所以需要多选和单选以及针对特定场景设置禁选
yxt-table.vue
<!-- yxt-table.vue -->
<template>
<div class="yxt-table">
<!-- 批量操作按钮,因为每个需求不同,批量操作的功能也不同,所以这里只放一个插槽,不设置默认内容,所有按钮均在父级设置 -->
<div class="multiple-operation">
<slot name="multiple-operation"
:selectionData="selectionData"></slot>
</div>
<!-- 页面主表格 -->
<el-table :data="tableData"
:row-key="rowKey"
@selection-change="selectionChange">
<!-- 可选框(多选) -->
<el-table-column v-if="selection === 'multiple'"
type="selection"
align="center"
width="55"
:reserve-selection="rowKey ? true : false"
:selectable="selectable"/>
<!-- 可选框(单选) -->
<el-table-column v-else-if="selection === 'single'"
align="center"
width="30">
<template v-slot:default="scope">
<el-radio v-model="selectionRadio"
:label="scope.$index"
:disabled="selectable ? !selectable(scope.row) : false"
@change="selectionChangeSingle(scope.row)">
{{ '' }}
</el-radio>
</template>
</el-table-column>
<el-table-column v-for="(item, index) in tableColumn"
:key="index"
:prop="item.prop"
:label="item.label">
<template v-if="item.slot"
v-slot:default="scope">
<slot :name="item.prop"
:row="scope.row"
:index="scope.$index"></slot>
</template>
<template v-else v-slot:default="scope">
<div v-if="item.dictCode">
{{ scope.row[item.prop] | filterStatus(dict[item.dictCode]) }}
</div>
<div v-else-if="item.format">
{{ item.format(scope.row) }}
</div>
<div v-else>
{{ scope.row[item.prop] }}
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<!-- yxt-table.vue -->
<script>
export default {
name: 'yxtTable',
props: {
apiUrl: { // 列表接口(必填)
type: Function,
required: true
},
tableColumn: { // 自定义列配置
type: Array,
default: () => []
},
otherConfig: { // 其他配置
type: Object,
default: () => {
return {
list: 'list' // 接口返回数据的列表字段的字段名(因为在组件里面调接口,可能不同业务不同项目组不同一个开发者返回给前端的参数名不一致,这里进行兼容)
}
}
},
dict: { // 全部字典
type: [Array, Object],
default: () => []
},
selection: { // 是否显示可选框(多选-multiple 、单选-single )
type: String
},
selectable: { // 当前行是否可选择
type: Function
},
rowKey: { // 表格唯一key(适用于分页多选表格,保留之前的选择,不传则为单页选择)
type: [Number, String, Function],
default: ''
}
},
filters: {
filterStatus (value, array, code = 'code', name = 'name') {
if (!value && value !== 0) { // 要把0摘出来,一般0都是正常的数据,所以不能只用 !value
return ''
}
const find = array.find(e => (e[code] === value.toString()) || (e[code] === +value)) // 字符型数值型都得匹配
if (find) {
return find[name]
} else { // 没有匹配的就原样返回
return value
}
}
},
data () {
return {
tableData: [],
tableTotal: 0,
selectionRadio: '',
selectionData: []
}
},
created () {
this.getData()
},
methods: {
getData () {
const fun = this.apiUrl
fun().then(res => {
this.tableData = res[this.otherConfig.list] || []
this.tableTotal = res.pageInfo?.total || 0
})
},
// 多选,选择行数据change
selectionChange (selection) {
this.selectionData = selection
},
// 单选,选择行数据change
selectionChangeSingle (selection) {
this.selectionData = [selection]
}
}
}
</script>
<style scoped lang="scss">
.yxt-table {
margin: 30px;
.multiple-operation {
margin-bottom: 10px;
}
}
</style>
index.vue
<!-- index.vue -->
<template>
<div>
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:otherConfig="otherConfig"
:dict="dict"
selection="multiple"
:selectable="isSelectable">
<!-- 图标插槽 -->
<template v-slot:icon="{row, index}">
<i :class="row.status ? 'el-icon-circle-check' : 'el-icon-circle-close'"></i>
</template>
<!-- 批量操作按钮插槽 -->
<template v-slot:multiple-operation="{selectionData}">
<el-button type="primary"
size="small"
@click="handleClick1(selectionData)">批量操作1</el-button>
<el-button type="success"
size="small"
@click="handleClick2(selectionData)">批量操作2</el-button>
</template>
</yxt-table>
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:otherConfig="otherConfig"
:dict="dict"
selection="single"
:selectable="isSelectable">
<!-- 图标插槽 -->
<template v-slot:icon="{row, index}">
<i :class="row.status ? 'el-icon-circle-check' : 'el-icon-circle-close'"></i>
</template>
<!-- 批量操作按钮插槽 -->
<template v-slot:multiple-operation="{selectionData}">
<el-button type="primary"
size="small"
@click="handleClick1(selectionData)">单选操作</el-button>
</template>
</yxt-table>
</div>
</template>
<!-- index.vue -->
<script>
import yxtTable from './yxt-table.vue'
import { yxtTableList } from 'https/yxtDemo.js'
export default {
name: 'yxtDemoTable',
components: {
yxtTable
},
data () {
return {
yxtTableList,
tableColumn: [
{ prop: 'name', label: '名称' },
{ prop: 'code', label: '编码' },
{ prop: 'status', label: '状态', dictCode: 'status' },
{ prop: 'icon', label: '图标', slot: true },
{ prop: 'phone',
label: '电话号码',
format: (row) => {
return `${row.name}-${row.code}(${row.phone})`
} }
],
tableConfig: {
stripe: 'stripe',
border: 'border',
height: '200',
maxHeight: '200',
showHeader: true
},
otherConfig: {
list: 'tasks'
},
dict: {
status: [
{ code: 0, name: '打卡失败' },
{ code: 1, name: '打卡成功' }
]
}
}
},
methods: {
handleClick1 (selectionData) {
console.log('1', selectionData)
},
handleClick2 (selectionData) {
console.log('2', selectionData)
},
isSelectable (row) {
return row.selectable !== 0
}
}
}
</script>
<style scoped lang="scss">
.el-icon-circle-check {
font-size: 28px;
color: #67C23A;
}
.el-icon-circle-close {
font-size: 28px;
color: #F00;
}
</style>
6、操作列
根据业务需求,可以在操作列设置几个默认按钮,通过setupConfig设置开关,如果有除了默认按钮之外的操作需求,再通过插槽slot进行插入
<!-- yxt-table.vue -->
<!-- 操作列 -->
<el-table-column v-if="setupConfig.width !== 0"
:fixed="setupConfig.fixed"
:width="setupConfig.width"
label="操作">
<template v-slot:default="scope">
<slot name="setup"
:row="scope.row"
:index="scope.$index"></slot>
<!-- 查看 -->
<el-button v-if="setupConfig.view"
type="text"
@click="setupEvents('view', scope.row)">查看</el-button>
<!-- 编辑 -->
<el-button v-if="setupConfig.edit"
type="text"
@click="setupEvents('edit', scope.row)">编辑</el-button>
<!-- 删除 -->
<el-button v-if="setupConfig.del"
type="text"
@click="setupEvents('del', scope.row)">删除</el-button>
<!-- 操作日志 -->
<el-button v-if="setupConfig.log"
type="text"
@click="setupEvents('log', scope.row)">操作日志</el-button>
</template>
</el-table-column>
props: {
setupConfig: {
type: Object,
default: () => {
return {
width: 'auto'
}
}
}
},
methods: {
setupEvents (setupType, row) { // 操作列方法 查看/编辑/删除/操作日志
this.$emit(setupType, row)
}
}
index.vue做相应的处理,这里不贴代码了
7、分页
pagination控制是否需要分页组件,如果不需要分页则设置为false。根据业务需求,可传入pageSizes控制条数下拉框的条数选项
<!-- yxt-table.vue -->
<!-- 分页 -->
<el-pagination v-if="pagination"
class="pagination tablePage"
:pager-count="5"
:page-sizes="pageSizes || [10, 20, 50, 100]"
:total="tableTotal || 0"
:page-size="pageInfo.pageSize || 10"
:current-page="pageInfo.startPage || 1"
layout="total, sizes, prev, pager, next, jumper"
@size-change="sizeChange"
@current-change="pageChange"></el-pagination>
props: {
pagination: { // 是否需要分页,默认需要
type: Boolean,
default: true
},
pageSizes: {
type: Array
}
},
methods: {
getData () {
const fun = this.apiUrl
const pageInfo = { // 分页信息
pageSize: this.pageInfo.pageSize,
startPage: this.pageInfo.startPage
}
let param = { // 其他的搜素条件
}
if (this.pagination) { // 如果需要分页,则传分页信息
param = { ...param, ...pageInfo }
}
fun(param).then(res => {
this.tableData = res[this.otherConfig.list] || []
this.tableTotal = res.pageInfo?.total || 0
})
},
// 条数变化
sizeChange (size) {
this.pageInfo.startPage = 1
this.pageInfo.pageSize = size
this.getData()
},
// 页码变化
pageChange (page) {
this.pageInfo.startPage = page
this.getData()
}
}
8、el-table还有一个展开行功能expand,根据业务需求,也可以加进组件里
<!-- yxt-table.vue -->
<!-- 展开行 -->
<el-table-column v-if="expand"
type="expand">
<template v-slot:default="scope">
<slot name="expand"
:row="scope.row"
:index="scope.$index"></slot>
</template>
</el-table-column>
props: {
expand: { // 是否展开行
type: Boolean,
default: false
}
}
<!-- index.vue -->
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:expand="true">
<template v-slot:expand="{row, index}">
<div>
<p>序号:{{index}}</p>
<p>内容:{{row}}</p>
</div>
</template>
</yxt-table>
四、根据搜索条件进行搜索更新表格数据
新增一个yxt-search.vue
<!-- yxt-search.vue -->
<template>
<div class="yxt-search">
<div v-for="(item,index) in searchConfig"
:key="index"
class="yxt-search-item">
<el-input v-if="item.type==='input'"
v-model="searchModel[item.key]"
size="medium"
:clearable="item.clearable || true"
:placeholder="item.placeholder || '请输入'"
:maxlength="item.maxlength"></el-input>
<el-select v-if="item.type==='select'"
v-model="searchModel[item.key]"
size="medium"
style="width: 100%"
:clearable="item.clearable || true"
:filterable="item.filterable || true"
:disabled="item.disabled || false"
:multiple="item.multiple || false"
:allow-create="item.allowCreate"
:placeholder="item.placeholder || '请选择'">
<el-option v-for="(selectItem, selectIndex) in item.selectList"
:key="selectIndex"
:label="selectItem[item.listLabel]"
:value="selectItem[item.listValue]"></el-option>
</el-select>
</div>
<div v-if="searchConfig.length" class="yxt-search-button">
<el-button size="medium" type="primary" @click="search">搜索</el-button>
<el-button size="medium" type="primary" plain @click="reset">重置</el-button>
<!-- 其他的按钮需求通过插槽传入 -->
<slot name="searchBtn" :searchData="searchModel"></slot>
</div>
</div>
</template>
<!-- yxt-search.vue -->
<script>
export default {
name: 'yxtSearch',
props: {
searchConfig: { // 搜索条件配置项
type: Array,
required: true,
default () {
return []
}
},
searchModel: { // 搜索条件绑定值
type: Object,
required: true,
default () {
return {}
}
},
searchReset: { // 搜索条件默认值重置值
type: Object
}
},
data () {
return {
}
},
methods: {
search () {
this.$emit('search', this.searchModel)
},
reset () {
if (this.searchReset) { // 如果传入有默认值,则重置后为默认值
Object.keys(this.searchModel).forEach((item) => {
this.searchModel[item] = this.searchReset[item]
})
} else {
Object.keys(this.searchModel).forEach((item) => {
this.searchModel[item] = ''
})
}
}
}
}
</script>
<style scoped lang="scss">
.yxt-search {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
.yxt-search-item {
flex: 1;
margin: 0 10px 10px 0;
width: calc((100% - 30px) / 4); // 这里的30px = (分布个数4-1)*间隙1px, 可以根据实际的分布个数和间隙区调整
min-width: calc((100% - 30px) / 4);
max-width: calc((100% - 30px) / 4);
&:nth-child(4n) { // 去除每行最后一个(第4n个)的margin-right
margin-right: 0;
}
}
.yxt-search-button {
margin: 0 0 10px 0;
width: 100%;
text-align: right;
}
}
</style>
<!-- yxt-table.vue -->
<yxt-search :searchConfig="searchConfig"
:searchModel="searchModel"
:searchReset="searchReset"
@search="getData(1)">
<template v-slot:searchBtn="{searchData}">
<!-- 其他的按钮需求通过插槽传入 -->
<slot name="searchBtn" :searchData="searchData"></slot>
</template>
</yxt-search>
props: {
searchConfig: { // 搜索条件配置项
type: Array,
default () {
return []
}
},
searchReset: { // 搜索条件默认值重置值
type: Object
}
},
data () {
return {
searchModel: this.searchReset ? JSON.parse(JSON.stringify(this.searchReset)) : {}
}
},
methods: {
getData (startPage) {
if (startPage) { // 如果传入值,则从改值的页码数开始
this.pageInfo.startPage = startPage
}
let param = { // 其他的搜素条件
...this.searchModel
}
...
}
}
<!-- index.vue -->
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:searchConfig="searchConfig"
:searchReset="searchReset">
<template v-slot:searchBtn="{searchData}">
<el-button size="medium" type="success" @click="handleClickExport(searchData)">导出</el-button>
</template>
</yxt-table>
data () {
return {
searchConfig: [
{ type: 'input', key: 'name' },
{ type: 'input', key: 'code' },
{ type: 'select',
key: 'status',
selectList: [
{ code: 0, name: '打卡失败' },
{ code: 1, name: '打卡成功' }
],
listLabel: 'name',
listValue: 'code' }
],
searchReset: {
name: '张三',
code: '',
status: 1
}
}
},
methods: {
handleClickExport (data) {
console.log(data)
}
}
五、加载中状态和空数据状态
加载中:el-table 添加 v-loading="loading",getData里面,发送请求之前设置为true,获得数据后设置为false
空数据:通过插槽empty设置
六、完整代码:
index.vue
<!-- index.vue -->
<template>
<div>
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:otherConfig="otherConfig"
:dict="dict"
selection="multiple"
:selectable="isSelectable"
:setupConfig="setupConfig"
:searchConfig="searchConfig"
:searchReset="searchReset"
@view="view"
@log="log">
<!-- 图标插槽 -->
<template v-slot:icon="{row, index}">
<i :class="row.status ? 'el-icon-circle-check' : 'el-icon-circle-close'"></i>
</template>
<!-- 批量操作按钮插槽 -->
<template v-slot:multiple-operation="{selectionData}">
<el-button type="primary"
size="small"
@click="handleClick1(selectionData)">批量操作1</el-button>
<el-button type="success"
size="small"
@click="handleClick2(selectionData)">批量操作2</el-button>
</template>
<template v-slot:searchBtn="{searchData}">
<el-button size="medium" type="success" @click="handleClickExport(searchData)">导出</el-button>
</template>
</yxt-table>
<yxt-table :apiUrl="yxtTableList"
:tableColumn="tableColumn"
:otherConfig="otherConfig"
:dict="dict"
selection="single"
:selectable="isSelectable"
:setupConfig="setupConfig2"
:pagination="false"
:expand="true"
:emptyText="'没有数据的展示文字'">
<!-- 图标插槽 -->
<template v-slot:icon="{row, index}">
<i :class="row.status ? 'el-icon-circle-check' : 'el-icon-circle-close'"></i>
</template>
<!-- 批量操作按钮插槽 -->
<template v-slot:multiple-operation="{selectionData}">
<el-button type="primary"
size="small"
@click="handleClick1(selectionData)">单选操作</el-button>
</template>
<template v-slot:expand="{row, index}">
<div>
<p>序号:{{index}}</p>
<p>内容:{{row}}</p>
</div>
</template>
</yxt-table>
</div>
</template>
<!-- index.vue -->
<script>
import yxtTable from './yxt-table.vue'
import { yxtTableList } from 'https/yxtDemo.js'
export default {
name: 'yxtDemoTable',
components: {
yxtTable
},
data () {
return {
yxtTableList,
tableColumn: [
{ prop: 'name', label: '名称' },
{ prop: 'code', label: '编码' },
{ prop: 'status', label: '状态', dictCode: 'status' },
{ prop: 'icon', label: '图标', slot: true },
{ prop: 'phone',
label: '电话号码',
format: (row) => {
return `${row.name}-${row.code}(${row.phone})`
} }
],
tableConfig: {
stripe: 'stripe',
border: 'border',
height: '200',
maxHeight: '200',
showHeader: true
},
otherConfig: {
list: 'tasks'
},
setupConfig: {
width: 100,
view: true,
log: true
},
setupConfig2: {
edit: true,
del: true,
log: true
},
dict: {
status: [
{ code: 0, name: '打卡失败' },
{ code: 1, name: '打卡成功' }
]
},
searchConfig: [
{ type: 'input', key: 'name' },
{ type: 'input', key: 'code' },
{ type: 'select',
key: 'status',
selectList: [
{ code: 0, name: '打卡失败' },
{ code: 1, name: '打卡成功' }
],
listLabel: 'name',
listValue: 'code' }
],
searchReset: {
name: '张三',
code: '',
status: 1
}
}
},
methods: {
handleClick1 (selectionData) {
console.log('1', selectionData)
},
handleClick2 (selectionData) {
console.log('2', selectionData)
},
handleClickExport (data) {
console.log(data)
},
isSelectable (row) {
return row.selectable !== 0
},
view (row) {
console.log('view', row)
},
log (row) {
console.log('log', row)
}
}
}
</script>
<style scoped lang="scss">
.el-icon-circle-check {
font-size: 28px;
color: #67C23A;
}
.el-icon-circle-close {
font-size: 28px;
color: #F00;
}
</style>
yxt-table.vue
<!-- yxt-table.vue -->
<template>
<div class="yxt-table">
<yxt-search :searchConfig="searchConfig"
:searchModel="searchModel"
:searchReset="searchReset"
@search="getData(1)">
<template v-slot:searchBtn="{searchData}">
<!-- 其他的按钮需求通过插槽传入 -->
<slot name="searchBtn" :searchData="searchData"></slot>
</template>
</yxt-search>
<!-- 批量操作按钮,因为每个需求不同,批量操作的功能也不同,所以这里只放一个插槽,不设置默认内容,所有按钮均在父级设置 -->
<div class="multiple-operation">
<slot name="multiple-operation"
:selectionData="selectionData"></slot>
</div>
<!-- 页面主表格 -->
<el-table :data="tableData"
:row-key="rowKey"
v-loading="loading"
@selection-change="selectionChange">
<!-- 可选框(多选) -->
<el-table-column v-if="selection === 'multiple'"
type="selection"
align="center"
width="55"
:reserve-selection="rowKey ? true : false"
:selectable="selectable"/>
<!-- 可选框(单选) -->
<el-table-column v-else-if="selection === 'single'"
align="center"
width="30">
<template v-slot:default="scope">
<el-radio v-model="selectionRadio"
:label="scope.$index"
:disabled="selectable ? !selectable(scope.row) : false"
@change="selectionChangeSingle(scope.row)">
{{ '' }}
</el-radio>
</template>
</el-table-column>
<!-- 展开行 -->
<el-table-column v-if="expand"
type="expand">
<template v-slot:default="scope">
<slot name="expand"
:row="scope.row"
:index="scope.$index"></slot>
</template>
</el-table-column>
<el-table-column v-for="(item, index) in tableColumn"
:key="index"
:prop="item.prop"
:label="item.label">
<template v-if="item.slot"
v-slot:default="scope">
<slot :name="item.prop"
:row="scope.row"
:index="scope.$index"></slot>
</template>
<template v-else v-slot:default="scope">
<div v-if="item.dictCode">
{{ scope.row[item.prop] | filterStatus(dict[item.dictCode]) }}
</div>
<div v-else-if="item.format">
{{ item.format(scope.row) }}
</div>
<div v-else>
{{ scope.row[item.prop] }}
</div>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column v-if="setupConfig.width !== 0"
:fixed="setupConfig.fixed"
:width="setupConfig.width"
label="操作">
<template v-slot:default="scope">
<slot name="setup"
:row="scope.row"
:index="scope.$index"></slot>
<!-- 查看 -->
<el-button v-if="setupConfig.view"
type="text"
@click="setupEvents('view', scope.row)">查看</el-button>
<!-- 编辑 -->
<el-button v-if="setupConfig.edit"
type="text"
@click="setupEvents('edit', scope.row)">编辑</el-button>
<!-- 删除 -->
<el-button v-if="setupConfig.del"
type="text"
@click="setupEvents('del', scope.row)">删除</el-button>
<!-- 操作日志 -->
<el-button v-if="setupConfig.log"
type="text"
@click="setupEvents('log', scope.row)">操作日志</el-button>
</template>
</el-table-column>
<!-- 空状态 -->
<template slot="empty">
<p>{{ emptyText }}</p>
</template>
</el-table>
<!-- 分页 -->
<el-pagination v-if="pagination"
class="pagination tablePage"
:pager-count="5"
:page-sizes="pageSizes || [10, 20, 50, 100]"
:total="tableTotal || 0"
:page-size="pageInfo.pageSize || 10"
:current-page="pageInfo.startPage || 1"
layout="total, sizes, prev, pager, next, jumper"
@size-change="sizeChange"
@current-change="pageChange"></el-pagination>
</div>
</template>
<!-- yxt-table.vue -->
<script>
import yxtSearch from './yxt-search'
export default {
name: 'yxtTable',
components: {
yxtSearch
},
props: {
apiUrl: { // 列表接口(必填)
type: Function,
required: true
},
tableColumn: { // 自定义列配置
type: Array,
default: () => []
},
otherConfig: { // 其他配置
type: Object,
default: () => {
return {
list: 'list' // 接口返回数据的列表字段的字段名(因为在组件里面调接口,可能不同业务不同项目组不同一个开发者返回给前端的参数名不一致,这里进行兼容)
}
}
},
dict: { // 全部字典
type: [Array, Object],
default: () => []
},
selection: { // 是否显示可选框(多选-multiple 、单选-single )
type: String
},
selectable: { // 当前行是否可选择
type: Function
},
rowKey: { // 表格唯一key(适用于分页多选表格,保留之前的选择,不传则为单页选择)
type: [Number, String, Function],
default: ''
},
setupConfig: {
type: Object,
default: () => {
return {
width: 'auto'
}
}
},
pagination: { // 是否需要分页,默认需要
type: Boolean,
default: true
},
pageSizes: { // 分页的下拉框选项
type: Array
},
expand: { // 是否展开行
type: Boolean,
default: false
},
searchConfig: { // 搜索条件配置项
type: Array,
default () {
return []
}
},
searchReset: { // 搜索条件默认值重置值
type: Object
},
emptyText: {
type: String
}
},
filters: {
filterStatus (value, array, code = 'code', name = 'name') {
if (!value && value !== 0) { // 要把0摘出来,一般0都是正常的数据,所以不能只用 !value
return ''
}
const find = array.find(e => (e[code] === value.toString()) || (e[code] === +value)) // 字符型数值型都得匹配
if (find) {
return find[name]
} else { // 没有匹配的就原样返回
return value
}
}
},
data () {
return {
loading: true,
tableData: [],
tableTotal: 0,
pageInfo: {
pageSize: 10,
startPage: 1
},
selectionRadio: '',
selectionData: [],
searchModel: this.searchReset ? JSON.parse(JSON.stringify(this.searchReset)) : {}
}
},
created () {
this.getData()
},
methods: {
getData (startPage) {
if (startPage) { // 如果传入值,则从改值的页码数开始
this.pageInfo.startPage = startPage
}
this.loading = true
const fun = this.apiUrl
const pageInfo = { // 分页信息
pageSize: this.pageInfo.pageSize,
startPage: this.pageInfo.startPage
}
let param = { // 其他的搜素条件
...this.searchModel
}
if (this.pagination) { // 如果需要分页,则传分页信息
param = { ...param, ...pageInfo }
}
fun(param).then(res => {
setTimeout(() => {
this.tableData = res[this.otherConfig.list] || []
this.tableTotal = res.pageInfo?.total || 0
this.loading = false
}, 2000)
})
},
// 多选,选择行数据change
selectionChange (selection) {
this.selectionData = selection
},
// 单选,选择行数据change
selectionChangeSingle (selection) {
this.selectionData = [selection]
},
// 操作列方法 查看/编辑/删除/操作日志
setupEvents (setupType, row) {
this.$emit(setupType, row)
},
// 条数变化
sizeChange (size) {
this.pageInfo.startPage = 1
this.pageInfo.pageSize = size
this.getData()
},
// 页码变化
pageChange (page) {
this.pageInfo.startPage = page
this.getData()
}
}
}
</script>
<style scoped lang="scss">
.yxt-table {
margin: 30px;
.multiple-operation {
margin-bottom: 10px;
}
}
</style>
yxt-search.vue
<!-- yxt-search.vue -->
<template>
<div class="yxt-search">
<div v-for="(item,index) in searchConfig"
:key="index"
class="yxt-search-item">
<el-input v-if="item.type==='input'"
v-model="searchModel[item.key]"
size="medium"
:clearable="item.clearable || true"
:placeholder="item.placeholder || '请输入'"
:maxlength="item.maxlength"></el-input>
<el-select v-if="item.type==='select'"
v-model="searchModel[item.key]"
size="medium"
style="width: 100%"
:clearable="item.clearable || true"
:filterable="item.filterable || true"
:disabled="item.disabled || false"
:multiple="item.multiple || false"
:allow-create="item.allowCreate"
:placeholder="item.placeholder || '请选择'">
<el-option v-for="(selectItem, selectIndex) in item.selectList"
:key="selectIndex"
:label="selectItem[item.listLabel]"
:value="selectItem[item.listValue]"></el-option>
</el-select>
</div>
<div v-if="searchConfig.length" class="yxt-search-button">
<el-button size="medium" type="primary" @click="search">搜索</el-button>
<el-button size="medium" type="primary" plain @click="reset">重置</el-button>
<!-- 其他的按钮需求通过插槽传入 -->
<slot name="searchBtn" :searchData="searchModel"></slot>
</div>
</div>
</template>
<!-- yxt-search.vue -->
<script>
export default {
name: 'yxtSearch',
props: {
searchConfig: { // 搜索条件配置项
type: Array,
required: true,
default () {
return []
}
},
searchModel: { // 搜索条件绑定值
type: Object,
required: true,
default () {
return {}
}
},
searchReset: { // 搜索条件默认值重置值
type: Object
}
},
data () {
return {
}
},
methods: {
search () {
this.$emit('search', this.searchModel)
},
reset () {
if (this.searchReset) { // 如果传入有默认值,则重置后为默认值
Object.keys(this.searchModel).forEach((item) => {
this.searchModel[item] = this.searchReset[item]
})
} else {
Object.keys(this.searchModel).forEach((item) => {
this.searchModel[item] = ''
})
}
}
}
}
</script>
<style scoped lang="scss">
.yxt-search {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
.yxt-search-item {
flex: 1;
margin: 0 10px 10px 0;
width: calc((100% - 30px) / 4); // 这里的30px = (分布个数4-1)*间隙1px, 可以根据实际的分布个数和间隙区调整
min-width: calc((100% - 30px) / 4);
max-width: calc((100% - 30px) / 4);
&:nth-child(4n) { // 去除每行最后一个(第4n个)的margin-right
margin-right: 0;
}
}
.yxt-search-button {
margin: 0 0 10px 0;
width: 100%;
text-align: right;
}
}
</style>
yxtTable.json
{
"retCode": "0",
"retMsg": "success",
"pageInfo": {
"total": 300
},
"tasks": [
{ "name": "张三",
"code": "zhangSan",
"status": 1,
"icon": true,
"phone": "17801010101",
"selectable": 1
},
{ "name": "李四",
"code": "liSi",
"status": 0,
"icon": false,
"phone": "17802020202",
"selectable": 2
},
{ "name": "王五",
"code": "wangWu",
"status": 2,
"icon": true,
"phone": "17803030303",
"selectable": 0
},
{ "name": "马六",
"code": "maLiu",
"status": 1,
"icon": false,
"phone": "17804040404",
"selectable": 2
}
]
}
最后效果