【代码背景】
最近写了10+表,上线以后时不时要改需求,一旦改动就是10+的工作量,所以决定把表格抽离出来单独写个组件,方便以后复用修改,这应该是写过最复杂的组件封装了,特此记录,小白一枚,接触vue也没多久,欢迎大家学习交流哦。
开发环境:vue + element ui
【准备工作】
封装组件的目的是方便复用,也就是多个地方引用相同或相似的组件功能,所以在封装之前一定要弄清楚,针对目前的Table需要封装哪些功能,有哪些共性可以抽离出来。这部分的思考非常重要,是影响组件封装质量的重要权衡点,所以多花一点时间是非常有必要的。
针对目前所有的表进行了分析,从样式上大致可分为4个部分:
->1. 序号列,每张表都有。
->2. 单位列,每张表都有,但是字段值label标签是变化的,label会根据后续查询操作进行更改,比如用户点击“险种”后,这里的label会从“单位”变成“险种”。
->3. 查询操作,这个字段是抽取共性最难的部分,几乎没有相似的地方,每张表的查询操作都不太一样,且各自查询逻辑也不一样。
->4. 其他字段渲染,有单层/多层表头之分。
相对应的解决办法如下:
->1. 序号列通过el-table可以自动完成渲染。
<el-table-column label="序号" type="index" align="center" fixed />
->2. 在渲染可变字段的时候,可以通过props传入变量。
<!-- 单位/可变字段 -->
<el-table-column
:label="changeable.label"
:prop="changeable.prop"
align="center"
fixed
sortable
:sort-by="changeable.sort"
:min-width="flexColumnWidth(changeable.label,changeable.prop)"
/>
->3. 针对这个变化最多的字段,之前也考虑过用循环的方式把查询按钮渲染出来,尝试写过固定格式,然后添加条件判断渲染,但是无法达到预期的效果,后来在查找资料的时候突然看到了“插槽”的概念,具体可以参考这篇文章:https://vue-js.com/topic/5f6d68f24590fe0031e591f5 ,瞬间解决了这个问题。把个性化的部分抽出来作为“插槽”部分完美和组件结合使用。
<!-- 子组件 -->
<!-- 查询操作 使用template插槽进行自定义组装 -->
<slot name="child" />
<!-- 父组件 -->
<!-- 自定义具名插槽 -->
<template v-slot:child>
<el-table-column label="查询操作" align="center" width="130px" fixed>
<template slot-scope="scope">
<!-- 按机构下挖 -->
<el-tooltip
v-if="scope.row.sale_no === ''"
class="item"
effect="dark"
:content="scope.row.branch_no.endsWith('00')?'按机构查询':'按业务员查询'"
placement="top"
:enterable="false"
>
<el-button
type="success"
size="mini"
class="btn-column-query"
@click="digByType(scope.row,'机构')"
>{{ scope.row.branch_no.endsWith('00')?'机构':'业务员' }}
</el-button>
</el-tooltip>
<!-- 按险种下挖 -->
<el-tooltip
v-if="queryInfo.select_poltype==='0'"
class="item"
effect="dark"
content="按险种查询"
placement="top"
:enterable="false"
>
<el-button
type="warning"
size="mini"
class="btn-column-query"
@click="digByType(scope.row,'险种')"
>险种
</el-button>
</el-tooltip>
</template>
</el-table-column>
</template>
->4. 通过v-for循环渲染表头数据?
<!-- v-for 循环渲染表头 -->
<template v-for="item in headerData">
<!-- 多层表头 -->
<el-table-column
v-if="item.children && item.children.length"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<el-table-column
v-for="obj in item.children"
:key="obj.prop"
:label="obj.label"
:prop="obj.prop"
align="center"
sortable
:formatter="handleFormatter"
:min-width="flexColumnWidth(obj.label,obj.prop)"
/>
</el-table-column>
<!-- 单层表头 -->
<el-table-column
v-else
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
:fixed="item.fixed"
sortable
:formatter="handleFormatter"
:min-width="flexColumnWidth(item.label,item.prop)"
/>
</template>
【完整代码】?
子组件 ReportTabel.vue
<template>
<!-- 报表模板 -->
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
border
stripe
:header-cell-style="MyHeaderCellStyle"
:cell-style="MyCellStyle"
show-summary
:summary-method="accountSummaries"
:height="TableHeight"
style="margin-top: 0px"
>
<el-table-column label="序号" type="index" align="center" fixed />
<!-- 单位/可变字段 -->
<el-table-column
:label="changeable.label"
:prop="changeable.prop"
align="center"
fixed
sortable
:sort-by="changeable.sort"
:min-width="flexColumnWidth(changeable.label,changeable.prop)"
/>
<!-- 查询操作 使用template插槽进行自定义组装 -->
<slot name="child" />
<!-- v-for 循环渲染表头 -->
<template v-for="item in headerData">
<!-- 多层表头 -->
<el-table-column
v-if="item.children && item.children.length"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<el-table-column
v-for="obj in item.children"
:key="obj.prop"
:label="obj.label"
:prop="obj.prop"
align="center"
sortable
:formatter="handleFormatter"
:min-width="flexColumnWidth(obj.label,obj.prop)"
/>
</el-table-column>
<!-- 单层表头 -->
<el-table-column
v-else
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
:fixed="item.fixed"
sortable
:formatter="handleFormatter"
:min-width="flexColumnWidth(item.label,item.prop)"
/>
</template>
</el-table>
</template>
<script>
export default {
name: 'Index',
props: {
// 表头数据
headerData: {
type: Array,
default: () => [],
require: true
},
// 表头可变字段:单位/业务员/....
changeable: {
type: Object,
default: () => {
return {
label: '单位',
prop: 'branch_name',
sort: 'branch_no'
}
}
},
// 表格数据
tableData: {
type: Array,
default: () => [],
require: true
},
// 自定义合计
accountSummaries: {
type: Function,
default: () => {
return null
}
},
// 控制数据刷新
loading: {
type: Boolean,
default: false
}
},
data () {
return {
TableHeight: 550 // 默认表格高度
}
},
watch: {
// tableData是el-table绑定的数据
tableData: {
// 解决表格显示错位问题
handler () {
this.$nextTick(() => {
// tableRef是el-table绑定的ref属性值
this.$refs.tableRef.doLayout() // 对 Table 进行重新布局
})
},
deep: true
}
},
created () {
// 动态计算表格高度
const windowHeight = document.documentElement.clientHeight || document.bodyclientHeight
this.TableHeight = windowHeight - 150 // 数值"140"根据需要调整
},
methods: {
/**
* 遍历列的所有内容,获取最宽一列的宽度
* @param arr
*/
getMaxLength (arr) {
return arr.reduce((acc, item) => {
if (item) {
const calcLen = this.getTextWidth(item)
if (acc < calcLen) {
acc = calcLen
}
}
return acc
}, 0)
},
/**
* 使用span标签包裹内容,然后计算span的宽度 width: px
* @param valArr
*/
getTextWidth (str) {
let width = 0
const html = document.createElement('span')
html.innerText = str
html.className = 'getTextWidth'
document.querySelector('body').appendChild(html)
width = document.querySelector('.getTextWidth').offsetWidth
document.querySelector('.getTextWidth').remove()
return width
},
/**
* el-table-column 自适应列宽
* @param prop_label: 表名
* @param table_data: 表格数据
*/
flexColumnWidth (label, prop) {
// console.log('label', label)
// console.log('prop', prop)
// console.log(this.tableData)
// 1.获取该列的所有数据
const arr = this.tableData.map(x => x[prop])
arr.push(label) // 把每列的表头也加进去算
// console.log(arr)
// 2.计算每列内容最大的宽度 + 表格的内间距(依据实际情况而定)
return (this.getMaxLength(arr) + 25) + 'px'
},
// 格式化字段 string->number 用于排序
handleFormatter (row, column, cellValue, index) {
// console.log('row', row)
// console.log('column', column)
// console.log('cellValue', cellValue)
// console.log('index', index)
row[column.property] = Number(row[column.property])
return row[column.property]
},
// [表头]设置样式
MyHeaderCellStyle ({ row, column, rowIndex, columnIndex }) {
// console.log('column', column.property)
let style = null
this.headerData.forEach(item => {
if (item.prop === column.property) {
style = item.header_style
}
// 存在子节点
if (item.children && item.children.length) {
for (let i = 0; i < item.children.length; i++) {
if (item.children[i].prop === column.property) {
style = item.header_child_style
}
}
}
})
return style
},
// [表格]设置样式
MyCellStyle ({ row, column, rowIndex, columnIndex }) {
// console.log(column)
let style = null
this.headerData.forEach(item => {
if (item.prop === column.property) {
style = item.cell_style
}
// 存在子节点
if (item.children && item.children.length) {
for (let i = 0; i < item.children.length; i++) {
if (item.children[i].prop === column.property) {
style = item.cell_style
}
}
}
})
return style
}
}
}
</script>
<style scoped>
.el-table /deep/ th {
padding: 0;
white-space: nowrap;
min-width: fit-content;
}
.el-table /deep/ td {
padding: 1px;
white-space: nowrap;
width: fit-content;
}
/** 修改el-card默认paddingL:20px-内边距 **/
>>> .el-card__body {
padding: 10px;
}
.el-table /deep/ .cell {
white-space: nowrap;
width: fit-content;
}
</style>
父组件引用组件
// 引入组件
import ReportTable from '@/components/ReportTable'
<template>
<!--报表数据-->
<report-table
:header-data="headerData"
:changeable="changeable"
:table-data="tableData"
:account-summaries="accountSummaries2"
:loading="loading"
>
<!-- 自定义具名插槽 -->
<template v-slot:child>
<el-table-column label="查询操作" align="center" width="130px" fixed>
<template slot-scope="scope">
<!-- 按机构下挖 -->
<el-tooltip
v-if="scope.row.sale_no === ''"
class="item"
effect="dark"
:content="scope.row.branch_no.endsWith('00')?'按机构查询':'按业务员查询'"
placement="top"
:enterable="false"
>
<el-button
type="success"
size="mini"
class="btn-column-query"
@click="digByType(scope.row,'机构')"
>{{ scope.row.branch_no.endsWith('00')?'机构':'业务员' }}
</el-button>
</el-tooltip>
<!-- 按险种下挖 -->
<el-tooltip
v-if="queryInfo.select_poltype==='0'"
class="item"
effect="dark"
content="按险种查询"
placement="top"
:enterable="false"
>
<el-button
type="warning"
size="mini"
class="btn-column-query"
@click="digByType(scope.row,'险种')"
>险种
</el-button>
</el-tooltip>
</template>
</el-table-column>
</template>
</report-table>
</template>
<script>
// 引入组件
import ReportTable from '@/components/ReportTable'
// 表格头数据
const tableHeaderData = [
{
label: '保费合计',
prop: 'sum_amnt',
fixed: true,
header_style: 'background-color: #FFCCCC;color: #333;',
cell_style: 'background-color: #FFE6E5'
},
{
label: '大个险',
prop: 'gx',
children: [
{ label: '首年保费', prop: 'gx_snbf' },
{ label: '首年标保', prop: 'gx_snbb' },
{ label: '首年期交', prop: 'gx_snqj' },
{ label: '10年及以上期交', prop: 'gx_10qj' },
{ label: '保障型保费', prop: 'gx_bzxbf' },
{ label: '续期保费', prop: 'gx_xqbf' },
{ label: '短险保费', prop: 'gx_dxbf' }
],
header_style: 'background-color: #CCCCFF;color: #333;font-size: 14px',
header_child_style: 'background-color: #CCCCFF;color: #333;',
cell_style: 'background-color: #DBD9FF'
},
{
label: '银保',
prop: 'yb',
children: [
{ label: '首年保费', prop: 'yb_snbf' },
{ label: '首年标保', prop: 'yb_snbb' },
{ label: '首年期交', prop: 'yb_snqj' },
{ label: '保障型保费', prop: 'yb_bzxbf' },
{ label: '续期保费', prop: 'yb_xqbf' },
{ label: '短险保费', prop: 'yb_dxbf' }
],
header_style: 'background-color: #FFCC99;color: #333;font-size: 14px',
header_child_style: 'background-color: #FFCC99;color: #333;',
cell_style: 'background-color: #FFE4AB'
},
{
label: '团险',
prop: 'tx',
children: [
{ label: '首年保费', prop: 'tx_snbf' },
{ label: '首年期交', prop: 'tx_snqj' },
{ label: '续期保费', prop: 'tx_xqbf' },
{ label: '短险保费', prop: 'tx_dxbf' }
],
header_style: 'background-color: #CCFFCC;color: #333;font-size: 14px',
header_child_style: 'background-color: #CCFFCC;color: #333;',
cell_style: 'background-color: #E4FFD9'
}
]
export default {
name: 'Index',
components: { // 组件注册
'report-table': ReportTable
},
data () {
return {
headerData: tableHeaderData, // 表头数据
tableData: [], // 表格数据
tableTotal: [], // 表格合计数组
changeable: { // 可变字段:单位/业务员/险种
label: '单位',
prop: 'branch_name',
sort: 'branch_no'
},
loading: false,
queryInfo: {} // 表格的查询条件
}
},
created () {
this.checkReportData() // 渲染表格
},
methods: {
// 渲染表格数据
checkReportData () {
this.loading = true
checkReport01tb1(this.queryInfo).then(res => {
if (res.data === null) {
this.$message.info('当前查询结果为空')
this.tableData = [] // 清空当前列表
this.tableTotal = [] // 清空合计数组
} else {
this.handleChangeable(res.data.body[0]) // 处理可变字段
this.tableData = res.data.body // 渲染表格数据
this.tableTotal = res.data.total[0] // 渲染表格合计数据
}
this.loading = false
})
},
// 按类型下挖
digByType (rowInfo, type) {
// 根据type 修改queryInfo 查询不同的返回结果
this.checkReportData()
},
// 根据list结果渲染changeable字段
handleChangeable (rowData) {
if (rowData.branch_no !== '' && rowData.branch_name !== '') {
this.changeable = {
label: '单位',
prop: 'branch_name',
sort: 'branch_no'
}
}
if (rowData.sale_no !== '' && rowData.sale_name !== '') {
this.changeable = {
label: '业务员',
prop: 'sale_name',
sort: 'sale_no'
}
}
if (rowData.pol_code !== '' && rowData.pol_name !== '') {
this.changeable = {
label: '险种名称',
prop: 'pol_name',
sort: 'pol_code'
}
}
},
// 自定义合计(后台获取total数据)
accountSummaries2 (param) {
// console.log('param', param)
const { columns } = param
const sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计'
return
}
if (index === 1 || index === 2) {
sums[index] = '-'
return
}
// 如果查询结果为空
if (this.tableTotal.length === 0) {
sums[index] = '0'
} else {
sums[index] = this.tableTotal[column.property]
}
})
return sums
}
}
}
</script>
?【几点说明】
?(1)在渲染表头数据时把当前表头和单元格的样式也加进去了?
?<el-table>的表头跟单元格的样式是通过header-cell-style和cell-style实现的,具体请参考Element - The world's most popular Vue UI framework
?
双层的表头为了区别字号大小所以写了2个style,实际上只需要header_style和cell_style就行了
(2)?在渲染单层/多层表头的时候,这里只写了针对两层的情况,如果需要渲染三层及以上可以参考之前写过的一个demo:【vue】基于ElementUI实现动态表格_爱吃香草冰淇淋的阿喵的博客-CSDN博客_vue动态表格
?(3)flexColumnWidth是为了自适应表格列宽,关于这个可以参考上一篇文:【vue】ElementUI el-table自适应列宽实现_爱吃香草冰淇淋的阿喵的博客-CSDN博客_vue表格宽度自适应
【参考】 https://vue-js.com/topic/5f6d68f24590fe0031e591f5
|