Vue实战技巧Element Table二次封装

我心飞翔 分类:javascript

前言

由于重构后台管理项目中有好多表格页面,
举个栗子

k-table-demo.png

这表格看着还挺好看,写起来叫人直呼XX,多动脑子少掉发,少走弯路多省鞋。

写了一个后感觉太麻烦了,于是我奋笔疾书,利用Vue+Element Table重新封装出了一套表格组件

3分钟就可以实现一个表格页面的骚操作,你值得拥有

思考

看了表格咱们简单的把它们划分了下功能区

  • 最先入场的选手是最上方一排整整齐齐有点严肃的搜索功能区

  • 其次入场的是略带风骚的表格功能区

  • 然后入场的是全能综合性型选手表格内容区域

  • 最后入场的是有着长腿带着长队的分页组件区域

诶,让我们思考下最复杂的地方在哪里?

下面我用个图来标注下咱们接下来需要拿下的高地

总的来说表格内容这一块最不可控,他可能有多选、序号、图片、状态、时间、操作列......

k-table.png

实践

咱们把搜索层写成一个组件filterPane.vue

把表格分成一个组件tablePane.vue

表格组件tablePane.vue包括功能区、表格内容区、分页

filterPane.vue

明确目标

搜索层一般包括日期选择器、输入框、select下拉选择器等+搜索功能、重置功能

传入数据结构整理

// 搜索栏组件
 filterData:{
   timeSelect:true,    //是否显示日期控件
   elinput:[          
     {
       name:'姓名',    //提示语
       key:'userName'  //字段名
     }
   ],
   elselect:[
     {
       name:'部门',
       key:'department'
       option:[{
         key:1,
         value:'技术部'
       }]
     }
   ]
 }
 

timeSelect

  • 类型 Boolean

是否显示时间选择器

elinput

  • 类型 Array

输入框选项,子对象内

name为输入框的placeholder

key为字段名

elselect

  • 类型 Array

select下拉框选项,子对象内

name为输入框的placeholder

key为字段名

option为select下拉选项

开始封装

<template>
<div>
<div class="filter-container">
<el-date-picker
v-if="filterData.timeSelect"
v-model="dateRange"
style="width: 300px"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['', '']"
:picker-options="pickerOptions"
class="filter-item"
/>
<template v-if="filterData.elinput">
<el-input
v-for="(item,index) in filterData.elinput"
:key="index"
v-model="listQuery[item.key]"
:placeholder="item.name"
:style="{'width':item.width?item.width+'px':'200px'}"
class="filter-item"
/>
</template>
<template v-if="filterData.elselect">
<el-select
v-for="(item,index) in filterData.elselect"
:key="index"
v-model="listQuery[item.key]"
:placeholder="item.name"
clearable
:style="{'width':item.width?item.width+'px':'90px'}"
class="filter-item"
>
<el-option
v-for="i in item.option"
:key="i.key"
:label="i.value"
:value="i.key"
/>
</el-select>
</template>
<div class="btn">
<el-button class="filter-item" type="primary" @click="handleSearch">
搜索
</el-button>
<el-button class="filter-item" type="warning" @click="handleRest">
重置
</el-button>
</div>
</div>
</div>
</template>
<script>
// 搜索栏组件
// filterData:{
//   timeSelect:true,
//   elinput:[
//     {
//       name:'姓名',
//       key:'userName'
//     }
//   ],
//   elselect:[
//     {
//       name:'部门',
//       key:'department'
//       option:[{
//         key:1,
//         value:'技术部'
//       }]
//     }
//   ]
// }
export default {
props: {
// eslint-disable-next-line vue/require-default-prop
filterData: {
type: Object
}
},
data() {
return {
pickerOptions: {
disabledDate(time) {
return time.getTime() > Date.now()
}
},
dateRange: ['', ''],
listQuery: {}
}
},
watch: {
'filterData'(val) {
console.log(val)
if (val.elinput.length > 0) {
val.elinput.map(item => {
this.listQuery[item.key] = ''
})
}
if (val.elselect.length > 0) {
val.elinput.map(item => {
this.listQuery[item.key] = ''
})
}
},
//缓存进页面想清空可用
'filterData.rest': {
handler: function(val) {
if (val) {
this.handleRest()
}
},
deep: true
}
},
methods: {
handleSearch() {
console.log('搜索成功', this.listQuery)
const data = this.$global.deepClone(this.listQuery)
if (this.dateRange && this.dateRange[0] !== '') {
const startTime = this.$moment(this.dateRange[0]).format('YYYY-MM-DD') + ' 00:00:00'
const endTime = this.$moment(this.dateRange[1]).format('YYYY-MM-DD') + ' 23:59:59'
data.beginDate = startTime
data.endDate = endTime
}
Object.keys(data).forEach(function(key) {
if (data[key] === '') {
delete data[key]
}
})
this.$emit('filterMsg', data)
},
handleRest() {
const data = this.$global.deepClone(this.listQuery)
Object.keys(data).forEach(function(key) {
data[key] = ''
})
this.listQuery = data
this.dateRange = ['', '']
console.log('重置成功', this.listQuery)
}
}
}
</script>
<style  scoped lang='scss'>
.filter-item{
margin-left: 10px;
display: inline-block;
}
.filter-container .filter-item:nth-of-type(1){
margin-left: 0px;
}
.btn{
display: inline-block;
margin-left: 10px;
}
</style>

tablePane.vue

明确目标

实现表格功能行、实现表格基本功能、实现分页功能

传入数据结构整理

  dataSource: {
tool:[
{
name: '新增用户', //按钮名称
key: 1,  // 唯一标识符
permission: 2010106, // 权限点
type: '',  // 使用element自带按钮类型
bgColor: '#67c23a', // 自定义背景色
handleClick: this.handleAdd //自定义事件
},
]
data: [], // 表格数据
cols: [], // 表格的列数据
isSelection: false, // 表格有多选时设置
handleSelectionChange:(val)=>{} //点击行选中多选返回选中数组
isOperation: true, // 表格有操作列时设置
isIndex: true, // 列表序号
loading: true, // loading
pageData: {
total: 0, // 总条数
pageSize: 10, // 每页数量
pageNum: 1 // 页码
}
operation: {
// 表格有操作列时设置
label: '操作', // 列名
width: '350', // 根据实际情况给宽度
data: [
{
label: '冻结', // 操作名称
permission:'' //权限点
type: 'info', //按钮类型
handleRow: function(){} // 自定义事件
},
]
}
},

tool

  • 类型 Array
  • 默认值 [ ]

配置表格工具列

 dataSource: {
tool:[
{
name: '新增用户', //按钮名称
key: 1,  // 唯一标识符
permission: 2010106, // 权限点
type: '',  // 使用element自带按钮类型
bgColor: '#67c23a', // 自定义背景色
handleClick: this.handleAdd //自定义事件
},
]
}

cols

  • 类型 Array
  • 默认值 [ ]

配置表头

 dataSource: {
cols:[
{
label: '标题',                       //列名
prop: 'belongUserId',                //字段名称
width: 100                           //列宽度
},
{
label: '副标题(季)',
prop: 'subtitle',
isCodeTableFormatter: function(val) {//过滤器
if (val.subtitle === 0) {
return '无'
} else {
return val.subtitle
}
},
width: 100
},
{
label: '创建时间',
prop: 'createTime',
isCodeTableFormatter: function(val) {//时间过滤器
return timeFormat(val.createTime)
},
width: 150
}
]
}

pageData

  • 类型 Object
  • 默认值 { }

配置分页

 dataSource: {
pageData: {
total: 0, // 总条数
pageSize: 10, // 每页数量
pageNum: 1, // 页码
pageSize:[5,10,15,20]// 每页数量
}
}

operation

  • 类型 Object
  • 默认值 { }

配置操作列

dataSource: {
operation: {
// 表格有操作列时设置
label: '操作', // 列名
width: '350', // 根据实际情况给宽度
data: [
{
label: '修改', // 操作名称
permission:'1001' //权限点
type: 'info', //按钮类型
handleRow: function(){} // 自定义事件
},
]
}
}

开始封装

<template>
<div>
<div v-if="dataSource.tool" class="tool">
<el-button
v-for="(item) in dataSource.tool"
:key="item.key"
v-permission="item.permission"
class="filter-item"
:style="{'background':item.bgColor,borderColor:item.bgColor}"
:type="item.type || 'primary'"
@click="item.handleClick(item.name,$event)"
>
{{ item.name }}
</el-button>
</div>
<el-table
ref="table"
v-loading="dataSource.loading"
style="width: 100%;"
:class="{ 'no-data': !dataSource.data || !dataSource.data.length }"
:data="dataSource.data"
@row-click="getRowData"
@selection-change="dataSource.handleSelectionChange"
>
<!-- 是否有多选 -->
<el-table-column
v-if="dataSource.isSelection"
:selectable="dataSource.selectable"
type="selection"
:width="dataSource.selectionWidth || 50"
align="center"
/>
<!-- 是否需要序号 -->
<el-table-column
v-if="dataSource.isIndex"
type="index"
label="序号"
width="55"
align="center"
/>
<template v-for="item in dataSource.cols">
<!-- 表格的列展示 特殊情况处理 比如要输入框 显示图片 -->
<el-table-column
v-if="item.isTemplate"
:key="item.prop"
v-bind="item"
>
<template slot-scope="scope">
<!-- 比如要输入框 显示图片等等 自己定义 -->
<slot :name="item.prop" :scope="scope" />
</template>
</el-table-column>
<!-- 图片带tooltip -->
<el-table-column
v-if="item.isImagePopover"
:key="item.prop"
v-bind="item"
align="center"
>
<template slot-scope="scope">
<el-popover
placement="right"
title=""
trigger="hover"
>
<img class="image-popover" :src="scope.row[scope.column.property]+'?x-oss-process=image/quality,q_60'" alt="">
<img slot="reference" class="reference-img" :src="scope.row[scope.column.property]+'?x-oss-process=image/quality,q_10'" alt="">
</el-popover>
</template>
</el-table-column>
<!-- 大部分适用 -->
<el-table-column
v-if="!item.isImagePopover && !item.isTemplate && !item.invisible&&!item.timeFormat"
:key="item.prop"
v-bind="item.isCodeTableFormatter ? Object.assign({ formatter: item.isCodeTableFormatter }, item) : item"
align="center"
show-overflow-tooltip
/>
</template>
<!-- 是否有操作列 -->
<!-- 没有数据时候不固定列 -->
<el-table-column
v-if="dataSource.isOperation"
:show-overflow-tooltip="dataSource.operation.overflowTooltip"
v-bind="dataSource.data && dataSource.data.length ? { fixed: 'right' } : null"
style="margin-right:20px"
class-name="handle-td"
label-class-name="tc"
:width="dataSource.operation.width"
:label="dataSource.operation.label"
align="center"
>
<!-- UI统一一排放3个,4个以上出现更多 -->
<template slot-scope="scope">
<!-- 三个一排的情况,去掉隐藏的按钮后的长度 -->
<template v-if="dataSource.operation.data.length > 0">
<div class="btn">
<div v-for="(item) in dataSource.operation.data" :key="item.label">
<template v-if="item.type!=='icon'">
<el-button
v-permission="item.permission"
v-bind="item"
:type="item.type?item.type:''"
size="mini"
@click.native.prevent="item.handleRow(scope.$index, scope.row, item.label)"
>
{{ item.label }}
</el-button>
</template>
<template v-else>
<i class="icon el-icon-plus" v-bind="item" @click="item.handleRow(scope.$index, scope.row, item.label)" />
</template>
</div>
</div>
</template>
</template>
</el-table-column>
</el-table>
<div class="page">
<el-pagination
v-if="dataSource.pageData.total>0"
:current-page="dataSource.pageData.pageNum"
:page-sizes="dataSource.pageData.pageSizes?dataSource.pageData.pageSizes:[5,10,15,20]"
:page-size="dataSource.pageData.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="dataSource.pageData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script>
//  dataSource: {
//          tool:[
//            {
//              name: '新增用户', //按钮名称
//              key: 1,  // 唯一标识符
//              permission: 2010106, // 权限点
//              type: '',  // 使用element自带按钮类型
//              bgColor: '#67c23a', // 自定义背景色
//              handleClick: this.handleAdd //自定义事件
//            },
//          ]
//         data: [], // 表格数据
//         cols: [], // 表格的列数据
//         handleSelectionChange:(val)=>{} //点击行选中多选返回选中数组
//         isSelection: false, // 表格有多选时设置
//         isOperation: true, // 表格有操作列时设置
//         isIndex: true, // 列表序号
//         loading: true, // loading
//         pageData: {
//          total: 0, // 总条数
//          pageSize: 10, // 每页数量
//          pageNum: 1, // 页码
//          pageSize:[5,10,15,20]// 每页数量
//         }
//         operation: {
//           // 表格有操作列时设置
//           label: '操作', // 列名
//           width: '350', // 根据实际情况给宽度
//           data: [
//             {
//               label: '冻结', // 操作名称
//               permission:'' //权限点
//               type: 'info', //按钮类型
//               handleRow: function(){} // 自定义事件
//             },
//           ]
//         }
//       },
export default {
// 接收父组件传递过来的值
props: {
//  表格数据和表格部分属性的对象
// eslint-disable-next-line vue/require-default-prop
dataSource: {
type: Object
}
},
data() {
return {
}
},
watch: {
'dataSource.cols': { // 监听表格列变化
deep: true,
handler() {
// 解决表格列变动的抖动问题
this.$nextTick(this.$refs.table.doLayout)
}
}
},
methods: {
handleAdd(name) {
console.log(name)
this.$emit('toolMsg', name)
},
handleRow(index, row, lable) {
console.log(index, row, lable)
},
handleSizeChange(val) {
this.$emit('changeSize', val)
console.log(`每页 ${val} 条`)
},
handleCurrentChange(val) {
this.$emit('changeNum', val)
console.log(`当前页: ${val}`)
},
// 点击行即可选中
getRowData(row) {
this.$refs.table.toggleRowSelection(row)
}
}
}
</script>
<style lang="scss" scoped>
.page{
margin-top: 20px;
}
.btn{
display: flex;
justify-content: center;
}
.btn div{
margin-left: 5px;
}
.reference-img{
width: 40px;
height: 40px;
background-size:100% 100%;
border-radius: 4px;
}
.image-popover{
width: 200px;
height: 200px;
background-size:100% 100%;
}
.icon {
width: 25px;
font-size: 20px;
font-weight: bold;
}
</style>
 

实战

配置某页面,咱先看配置图片是不是省事多了,而且条理清楚

1.png

2.png

<template>
<div class="app-container">
<filter-pane :filter-data="filterData" @filterMsg="filterMsg" />
<table-pane
:data-source="dataSource"
@changeSize="changeSize"
@changeNum="changeNum"
/>
<add :dialog-add="dialogAdd" @childMsg="childMsg" />
</div>
</template>
<script>
import filterPane from '@/components/Table/filterPane'
import tablePane from '@/components/Table/tablePane'
import add from './components/add'
import { getVersionList, delVersion } from '@/api/user'
import { timeFormat } from '@/filters/index'
export default {
name: 'Suggestion',
components: { filterPane, tablePane, add },
data() {
return {
// 搜索栏配置
filterData: {
timeSelect: false,
elselect: [
{
name: '状态',
width: 120,
key: 'platform',
option: [
{
key: '全部',
value: '全部'
},
{
key: 1,
value: 'IOS'
},
{
key: 2,
value: '安卓'
}
]
}
]
},
// 表格配置
dataSource: {
tool: [{
name: '新增版本',
key: 1,
permission: 2010701,
handleClick: this.handleAdd
}],
data: [], // 表格数据
cols: [
{
label: '发布时间',
prop: 'appIssueTime',
isCodeTableFormatter: function(val) {
return timeFormat(val.appIssueTime)
}
},
{
label: 'APP名称',
prop: 'appName'
},
{
label: 'APP版本',
prop: 'appVersion'
},
{
label: '平台',
prop: 'appPlatform',
isCodeTableFormatter: function(val) {
if (val.appPlatform === 1) {
return 'IOS'
} else {
return 'Android'
}
}
},
{
label: '是否自动更新',
prop: 'appAutoUpdate',
isCodeTableFormatter: function(val) {
if (val.appAutoUpdate === 1) {
return '是'
} else {
return '否'
}
}
},
{
label: '更新描述',
prop: 'appDesc',
width: 300
},
{
label: '下载地址',
prop: 'downloadAddr'
},
{
label: '发布人',
prop: 'userName'
}
], // 表格的列数据
handleSelectionChange: this.handleSelectionChange,
isSelection: false, // 表格有多选时设置
isOperation: true, // 表格有操作列时设置
isIndex: true, // 列表序号
loading: true, // loading
pageData: {
total: 0, // 总条数
pageSize: 10, // 每页数量
pageNum: 1 // 页码
},
operation: {
// 表格有操作列时设置
label: '操作', // 列名
width: '100', // 根据实际情况给宽度
data: [
{
label: '删除', // 操作名称
type: 'danger',
permission: '2010702', // 后期这个操作的权限,用来控制权限
handleRow: this.handleRow
}
]
}
},
dialogAdd: false,
msg: {},
selected: []
}
},
created() {
this.getList()
},
methods: {
// 获取列表数据
getList() {
const data = {
pageSize: this.dataSource.pageData.pageSize,
pageNum: this.dataSource.pageData.pageNum
}
if (this.msg) {
if (this.msg.platform === 'IOS') {
data.platform = 1
} else if (this.msg.platform === '安卓') {
data.platform = 2
}
}
this.dataSource.loading = true
getVersionList(data).then(res => {
this.dataSource.loading = false
if (res.succeed) {
if (res.data.total > 0) {
this.dataSource.pageData.total = res.data.total
this.dataSource.data = res.data.data
} else {
this.dataSource.data = []
this.dataSource.pageData.total = 0
}
}
})
},
// 搜索层事件
filterMsg(msg) {
this.msg = msg
if (Object.keys(msg).length > 0) {
this.getList(msg)
} else {
this.getList()
}
},
// 子组件通信
childMsg(msg) {
if (msg.dialogAdd === false) {
this.dialogAdd = false
} else if (msg.refreshList) {
this.getList()
}
},
// 改变每页数量
changeSize(size) {
this.dataSource.pageData.pageSize = size
this.getList()
},
// 改变页码
changeNum(pageNum) {
this.dataSource.pageData.pageNum = pageNum
this.getList()
},
// 多选事件
handleSelectionChange(val) {
this.selected = val
},
// 表格上方工具栏回调
handleAdd(index, row) {
this.dialogAdd = true
},
// 表格操作列回调
handleRow(index, row, lable) {
if (lable === '删除') {
this.$confirm('确认删除该版本?', '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delVersion({ versionId: row.id }).then(res => {
if (res.succeed) {
this.$message.success('删除成功')
this.getList()
}
})
}).catch(() => {
})
}
}
}
}
</script>
<style  scoped lang='scss'>
</style>

结尾

filterPane.vuetablePane.vue已完成,有些特殊页面只需要复制下到当前特殊页面的components里改动下就

可以了,目前还在不断完善中,大家有什么问题可以提出来,也好进一步优化。

完整源文件在gitHub,可以下载直接使用,后续会持续更新

我给起了个名k-tablek开头代表快速的意思

k-table
如果对你有帮助,请点亮你的小星星⭐⭐⭐哦~(疯狂暗示)

超级简单的API,提供手把手教你使用的实例!

回复

我来回复
  • 暂无回复内容