vue表格组件封装
问题
项目中的表格都是用的element-ui中的table组件,现在项目需要做移动端适配,表格在移动端展示需要是卡片形式的,pc端还是table形式的,如下图:
图一:pc端
图二:移动端
由于需要pc上渲染出来的html是
<table><tr><td></td></tr></table>形式的,而移动端上渲染出来是<div class="card"><div class="card-label"></div><div class="card-cont"></div></div>这种形式的,但是下面是element table组件的代码,表格的每一栏都是一个el-table-column模板代码,结构是固定的,所以在移动端上没办法复用这个template,于是我们需要另辟蹊径
<template>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="address"
label="地址">
</el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
tableData: [{
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-04',
name: '王小虎',
address: '上海市普陀区金沙江路 1517 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1519 弄'
}, {
date: '2016-05-03',
name: '王小虎',
address: '上海市普陀区金沙江路 1516 弄'
}]
}
}
}
</script>
方案一
通过slot传参,再搞一个移动端的template,代码如下
<kl-table :data="tableData" :columns="earnRecordColumns" :loading="!isIntervalReload && fetchDataLoading && hasShow" :handleCurrentChange="handleCurrentChange" :handleSizeChange="handleSizeChange" :pagesize="pagesize" :currentPage="currentPage" :pageSizes="pageSize" :totalNum="totalNum">
<template v-slot:profit>
<el-table-column prop="profit" label="收益($)">
<template slot-scope="scope">{{ scope.row.profit | formatCurrency }}</template>
</el-table-column>
</template>
<template v-slot:mobileProfit="scope">
<table-card-column label="收益($)">
<template>{{ scope.row.profit | formatCurrency }}</template>
</table-card-column>
</template>
<template v-slot:profitOfRate>
<el-table-column prop="profit" label="收益率">
<template slot-scope="scope">
<div class="profit-style">{{ scope.row.profitOfRate }}</div>
</template>
</el-table-column>
</template>
<template v-slot:mobileProfitOfRate="scope">
<table-card-column label="收益率">
<template>
<div class="profit-style">{{ scope.row.profitOfRate }}</div>
</template>
</table-card-column>
</template>
</kl-table>
this.earnRecordColumns = [
{
slot: 'profit',
mobileSlot: 'mobileProfit',
},
{
slot: 'profitOfRate',
mobileSlot: 'mobileProfitOfRate',
},
上面代码中v-slot:profit是pc端的template,v-slot:mobileProfit是移动端的template这样就能达到目的,但是这种方式太鸡肋啦,没弄一个字段,如果需要自定义template则需要写一遍pc的,还要写一遍mobile,作为一个有追求的搬砖工绝对不允许这种重复工作(没时间摸鱼了..)。
方案二
思考一下:如果我们能把字段信息和
template分离出来,也就是不用template去设置自定义渲染的字段,我们换成通过jsx的形式把渲染模板结构抽离出来,自定义的模板通过一处jsx传递给pc端和移动端的模板,这样就可以只写一处jsx代码了
<kl-table
:data="positionHistoryData"
:columns="columns"
:loading="!isPositionHistoryIntervalReload && fetchPositionHistoryLoading"
:handleCurrentChange="handleCurrentChange"
:handleSizeChange="handleSizeChange"
:pagesize="pagesize"
:currentPage="currentPage"
:pageSizes="pageSize"
:totalNum="totalNum"
>
</kl-table>
this.columns = [
{
prop: 'profit',
label: '收益',
'min-width': '120',
render: (scope) => {
return formateToUsd(scope.row.profit)
},
},
{
prop: 'profitOfRate',
label: '收益率',
width: '120',
render: (scope) => {
return <div class="profit-style">{scope.row.profitOfRate}</div>
},
},
]
如果字段比较多,那么就会省不少代码
最后看下组件代码,代码中包含了方案一和方案二(一开始使用的方案一,后来迁移到方案二)
klTable.vue
<template>
<table-card :data="data" :loading="loading">
<!-- pc表格 -->
<el-table :data="data" :key="tableKey" :max-height="maxHeight" style="width: 100%" v-loading="loading">
<template v-for="(column, index) in columns">
<template v-if="!('v-if' in column) || column['v-if']">
<slot v-if="column.slot" :name="column.slot"></slot>
<!-- 如果有render,则为jsx传入 -->
<el-table-column v-else-if="column.render" v-bind="column" :key="index">
<template slot-scope="scope">
<Jsx :jsx="column.render" :scope="scope"></Jsx>
</template>
</el-table-column>
<el-table-column v-else v-bind="column" :key="index"> </el-table-column>
</template>
</template>
<slot></slot>
</el-table>
<!-- pc分页 -->
<el-pagination v-if="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange" :page-size="pagesize" :current-page="currentPage" :page-sizes="pageSizes" layout="total, sizes, prev, pager, next, jumper" :total="totalNum" style="margin-top: 25px; text-align: right">
</el-pagination>
<!-- mobile卡片式表格 -->
<template v-slot:mobile="scope">
<template v-for="(column, index) in columns">
<template v-if="!('v-if' in column) || column['v-if']">
<slot v-if="column.mobileSlot" :name="column.mobileSlot" :row="scope.row"></slot>
<table-card-header v-else-if="column.isHeader" :key="index">
<!-- 如果有render,则为jsx传入 -->
<template v-if="column.render">
<Jsx :jsx="column.render" :scope="{ ...scope }"></Jsx>
</template>
<template v-else>
{{ scope.row[column.prop] }}
</template>
</table-card-header>
<table-card-column v-else :label="column.label" :key="index">
<!-- 如果有render,则为jsx传入 -->
<template v-if="column.render">
<Jsx :jsx="column.render" :scope="{ ...scope, $index: scope.index }"></Jsx>
</template>
<template v-else>
{{ scope.row[column.prop] }}
</template>
</table-card-column>
</template>
</template>
<slot name="mobile" :row="scope.row"></slot>
</template>
<!-- mobile分页 -->
<template v-slot:mobilePagination v-if="pagination">
<el-pagination @current-change="handleCurrentChange" small :pager-count="5" :page-size="pagesize" :current-page="currentPage" :page-sizes="pageSizes" layout="total, prev, pager, next" :total="totalNum" style="margin-top: 25px; text-align: right"> </el-pagination>
<!-- <div v-if="loading" class="loading-wrap"><i class="el-icon-loading"></i></div>
<table-card-more v-else :load-more="loadMore" v-show="currentPage != Math.ceil(totalNum / pagesize)"></table-card-more> -->
</template>
</table-card>
</template>
<script>
import Jsx from './jsx.vue'
export default {
name: 'KlTable',
props: {
loading: Boolean,
data: Array,
columns: Array,
handleSizeChange: Function,
handleCurrentChange: Function,
pagesize: Number,
currentPage: Number,
pageSizes: Array,
totalNum: Number,
tableKey: String,
maxHeight: [String, Number],
pagination: {
type: Boolean,
default: true,
},
// loadMore: Function,
},
components: { Jsx },
created() {},
}
</script>
<style lang="scss" scoped>
/deep/ .el-table__fixed,
/deep/ .el-table__fixed-right {
height: 100% !important; //设置高优先,以覆盖内联样式
}
.loading-wrap {
display: flex;
justify-content: center;
}
</style>
jsx.vue
<script>
export default {
props: {
jsx: {
type: Function,
},
scope: Object,
},
render() {
return <div>{this.jsx(this.scope)}</div>
},
name: 'jsx',
}
</script>
tableCard.vue
<template>
<div class="table-container">
<div class="table-wrap">
<slot></slot>
</div>
<div class="table-mobile-wrap">
<template v-if="loading || (data && data.length > 0)">
<div class="tb-card-wrap" v-loading="loading">
<div class="tb-card" v-for="(item, index) in data" :key="index">
<slot name="mobile" :row="item" :index="index"></slot>
</div>
</div>
<slot name="mobilePagination" v-if="data && data.length > 0"></slot>
</template>
<div class="no-data" v-else>
<el-empty :image-size="200"></el-empty>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TableCard',
props: {
data: Array,
loading: Boolean,
},
created() {
// console.log('table $props', this)
},
}
</script>
<style lang="scss" scoped>
.tb-card {
margin-bottom: 20px;
padding: 32px 24px;
background: #fff;
}
.no-data {
background: #fff;
}
.tb-card-wrap {
min-height: 300px;
}
.table-wrap {
display: block;
}
.table-mobile-wrap {
display: none;
}
@media (max-width: 767px) {
.table-wrap {
display: none;
}
.table-mobile-wrap {
display: block;
background-color: #ecf0f5;
margin: -16px -24px;
}
}
.el-dialog__wrapper {
.table-mobile-wrap {
background-color: transparent;
margin: 0;
.tb-card {
background: rgba(151, 160, 168, 0.05);
border-left: 2px solid #97a0a8;
}
}
}
</style>
tableCardColumn.vue
<template>
<div>
<div class="tb-lst-item">
<span class="tb-card-label">{{ label }}</span>
<span class="tb-card-cont">
<slot></slot>
</span>
</div>
</div>
</template>
<script>
export default {
name: 'TableCardColumn',
props: {
label: String,
},
created() {
// console.log('table $props', this)
},
}
</script>
<style lang="scss" scoped>
.tb-lst-item {
align-items: center;
margin-top: 16px;
display: flex;
justify-content: space-between;
font-size: 14px;
line-height: 20px;
}
.tb-card-label {
color: rgba(0, 0, 0, 0.65);
}
.tb-card-cont {
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
text-align: right;
}
</style>
tableCardHeader.vue
<template>
<div class="tb-card-hd">
<h3 class="tb-card-tit"><slot name="default"></slot></h3>
<span class="tb-card-sub"><slot name="subTitle"></slot></span>
</div>
</template>
<script>
export default {
name: 'TableCardHeader',
props: {
label: String,
},
created() {
// console.log('table $props', this)
},
}
</script>
<style lang="scss" scoped>
.tb-card-hd {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #bccae0;
padding-bottom: 16px;
line-height: 24px;
color: rgba(0, 0, 0, 0.85);
}
.tb-card-tit {
font-weight: 600;
font-size: 16px;
}
.tb-card-sub {
font-weight: 400;
font-size: 12px;
}
</style>