前言
在做大数据系统应用时,经常需要实现大数据表格渲染、表格勾选、全选、合并单元格显示等功能,这里利用elementplus框架 Virtualized Table 虚拟化表格 组件来实现这些功能,并记录在实现中遇到的问题与解决方法。
elementplus文档地址:element-plus.org/zh-CN/compo…
实现效果如下:
问题与解决办法
1.设置固定行高:设置el-table-v2组件的row-height属性
2.设置动态合并行:添加el-table-v2组件的#row插槽,然后设置动态cells,原理就是设置单元格高度覆盖要合并的单元格,设置zIndex显示在最上层
<el-table-v2
class="pay-table"
fixed
:columns="returnColumns"
:data="returnData"
:width="1334"
:height="500"
:row-height="tableHeight"
:cache="50"
>
<template #row="props">
<ReturnRow v-bind="props" />
</template>
</el-table-v2>
const ReturnRow = ({ rowData, cells, columns }: { rowData: ReturnTableData; rowIndex: number; cells: any; columns: any }) => {
const rowSpan = rowData.rowspan // 这里提前将要合并的单元格行数赋值给行数据
const colNums = [0, 5, 7, 8] // 要合并单元格的列
for (let i = 0; i < columns.length; ++i) {
if (rowSpan > 1 && colNums.includes(i)) {
Object.assign(cells[i].props.style, {
backgroundColor: 'var(--el-color-white)',
height: `${rowSpan * tableHeight - 1}px`,
alignSelf: 'flex-start',
zIndex: 9
})
}
}
return cells
}
3.设置表格加载loading:添加el-table-v2组件的#overlay插槽
<el-table-v2
class="pay-table"
fixed
:columns="returnColumns"
:data="returnData"
:width="1334"
:height="500"
:row-height="tableHeight"
:cache="50"
>
<template #overlay v-if="refundLoading">
<div class="el-loading-mask flex items-center justify-center">
<el-icon class="is-loading" color="var(--el-color-primary)" :size="26">
<loading-icon />
</el-icon>
</div>
</template>
</el-table-v2>
4.当合并的单元格行数太多时就会出现渲染没有合并问题,因为表格行数据是按需加载的,可以设置表格组件的cache属性来设置最大加载的行数据。但是cache设置越大,表格加载性能越差。
5.当需要一个合并总单元格(所有行),就会出现问题4情况,这时候可以采用position:absolute添加一个浮在上层显示总数据的div展示总合并行。
具体代码参考
<template>
<div class="wrapper">
<div class="return">
<div class="title">'选择退货商品'</div>
<div class="return-table">
<!-- <div class="total" v-if="returnData.length > 10">
{{ amountSeparationHasPrefix(getRefundTotalMoney()) }}
</div> -->
<el-table-v2
class="pay-table"
fixed
:columns="returnColumns"
:data="returnData"
:width="1334"
:height="500"
:row-height="tableHeight"
:cache="50"
>
<template #row="props">
<ReturnRow v-bind="props" />
</template>
<template #overlay v-if="refundLoading">
<div class="el-loading-mask flex items-center justify-center">
<el-icon class="is-loading" color="var(--el-color-primary)" :size="26">
<loading-icon />
</el-icon>
</div>
</template>
</el-table-v2>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'batch-after-sale-drawer'
}
</script>
<script lang="ts" setup>
import { ref, onMounted, h } from 'vue'
import { ElMessage, ElCheckbox, CheckboxValueType, ElInputNumber, type Column } from 'element-plus'
import { orderListApi } from '~/api/member-center/order-list'
import type { ReturnTableData } from '~/types/order-list'
import NP from 'number-precision'
import { showHttpErrorMsg } from '~/utils/http-error-msg'
import ProductInfo from '~/components/product-info.vue'
import { amountSeparationHasPrefix } from '~/utils/amount-format'
import OverflowEllipsis from '~/components/overflow-ellipsis.vue'
import { Loading as LoadingIcon } from '@element-plus/icons-vue'
const refundLoading = ref(false)
const getRefundAmount = async () => {
try {
refundLoading.value = true
const params = {
hjsIds: [1, 2, 3],
aftersaleType: 1,
applyReason: 1
}
const { code, msg, data } = await orderListApi.batchAvailableAmount(params)
if (code === 200) {
const result = []
for (let i = 0; i < data.orderList.length; i++) {
const parent = data.orderList[i]
for (let j = 0; j < parent.detailList.length; j++) {
const obj = parent.detailList[j]
result.push(
Object.assign(obj, {
checked: false,
rowspan: j === 0 ? parent.detailList.length : 1,
refundQty: obj.qty,
parent_additionalFreight: j === 0 ? parent.additionalFreight : 0, // parent.additionalFreight,
parent_availableAmount: parent.availableAmount,
parent_availableFreight: j === 0 ? parent.availableFreight : 0, // parent.availableFreight,
parent_goodsDeprecitionAmount: parent.goodsDeprecitionAmount,
parent_goodsUnpackAmount: j === 0 ? parent.goodsUnpackAmount : 0, // parent.goodsUnpackAmount,
together_goodsUnpackAmount: parent.goodsUnpackAmount,
together_additionalFreight: parent.additionalFreight,
together_availableFreight: parent.availableFreight,
parent_hjsOrderId: parent.hjsOrderId,
parent_orderId: parent.orderId,
parent_hjsId: parent.hjsId
})
)
}
}
returnData.value = result as ReturnTableData[]
} else {
ElMessage.error(msg)
}
} catch (error) {
showHttpErrorMsg(error)
} finally {
refundLoading.value = false
}
}
const tableHeight = 63
const returnColumns: Column<any>[] = [
{
key: 'parentOrderId',
dataKey: 'parentOrderId',
title: '商城订单号',
width: 160,
cellRenderer: ({ rowData, rowIndex }: any) => {
const _data = returnData.value
const parentOrderId = _data[rowIndex].parent_orderId
const onChange = (value: CheckboxValueType) => {
returnData.value = _data.map((row) => {
if (parentOrderId === row.parent_orderId) {
row.checked = value
}
return row
})
}
const filterData = _data.filter((v) => v.parent_orderId === parentOrderId)
const allSelected = filterData.every((row) => row.checked)
const containsChecked = filterData.some((row) => row.checked)
const _check = h(ElCheckbox, {
style: { marginRight: '8px' },
onChange,
modelValue: allSelected,
indeterminate: containsChecked && !allSelected
})
const _info = h('div', { style: { fontSize: '12px' } }, h(OverflowEllipsis, { title: rowData.parent_orderId, line: 2 }))
return h('div', { class: 'flex', style: { height: '100%', alignItems: 'center' } }, [_check, _info])
},
headerCellRenderer: () => {
const _data = returnData.value
const onChange = (value: CheckboxValueType) => {
returnData.value = _data.map((row) => {
row.checked = value
return row
})
}
const allSelected = _data.every((row) => row.checked)
const containsChecked = _data.some((row) => row.checked)
return h(
ElCheckbox,
{
onChange,
modelValue: allSelected,
indeterminate: containsChecked && !allSelected
},
'商城订单号'
)
}
},
{
key: 'orderId',
dataKey: 'orderId',
title: '商品信息',
width: 280,
cellRenderer: ({ rowData }: any) => {
const { goodsName, goodsImg, goodsCode } = rowData
const _info = h(ProductInfo, {
name: goodsName,
imgStyle: { width: '40px', height: '40px' },
img: goodsImg,
specs: `商品编码:${goodsCode}`
})
const onChange = (value: CheckboxValueType) => {
rowData.checked = value
}
const _check = h(ElCheckbox, {
style: { marginRight: '8px' },
onChange,
modelValue: rowData.checked
})
return h('div', { class: 'flex', style: { position: 'relative', zIndex: 9 } }, [_check, _info])
}
},
{
key: 'specName',
dataKey: 'specName',
title: '规格',
width: 140,
cellRenderer: ({ rowData }: any) => {
return h('div', { style: { fontSize: '12px' } }, h(OverflowEllipsis, { title: rowData.specName, line: 2 }))
}
},
{
key: 'refundQty',
dataKey: 'refundQty',
title: '数量',
width: 154,
cellRenderer: ({ rowData }: any) => {
const onChange = (value: number) => {
rowData.refundQty = value
}
// @ts-ignore
const _input = h(ElInputNumber, {
style: { marginRight: '8px', marginBottom: '4px', width: '138px' },
onChange,
min: 1,
step: 1,
max: rowData.qty,
modelValue: rowData.refundQty
})
const _info = h('div', { class: 'flex' }, '最多可退数量:' + rowData.qty || 0)
return h('div', { style: { color: '#fba218', fontSize: '12px' } }, [_input, _info])
}
},
{
key: 'availableAmount',
dataKey: 'availableAmount',
title: '可退商品金额(已扣破损费)',
width: 110,
cellRenderer: ({ rowData }: any) => {
let total: string | number = ''
if (String(rowData.refundQty).includes('***') || String(rowData.availableAmount).includes('***')) {
total = '***'
} else {
total = NP.times(rowData.refundQty || 0, rowData.availableAmount)
}
return h('div', [h('div', { style: { fontSize: '12px', marginBottom: '4px' } }, amountSeparationHasPrefix(total))])
},
headerCellRenderer: () => {
return h('div', [h('div', '可退商品金额'), h('div', '(已扣破损费)')])
}
},
{
key: 'parent_availableFreight',
dataKey: 'parent_availableFreight',
title: '可退运费',
width: 120,
cellRenderer: ({ rowData }: any) => {
let total: string | number = ''
if (String(rowData.parent_availableFreight).includes('***') || String(rowData.parent_additionalFreight).includes('***')) {
total = '***'
} else {
total = NP.plus(rowData.parent_availableFreight || 0, rowData.parent_additionalFreight || 0)
}
let text = ''
if (rowData.parent_additionalFreight && rowData.parent_additionalFreight >= 0) {
text = '含补扣运费' + amountSeparationHasPrefix(rowData.parent_additionalFreight)
} else if (rowData.parent_additionalFreight && rowData.parent_additionalFreight < 0) {
text = '已扣减退还运费' + amountSeparationHasPrefix(rowData.parent_additionalFreight)
}
return h('div', [
h('div', { style: { fontSize: '12px' } }, amountSeparationHasPrefix(total)),
h('div', { style: { marginTop: '4px', fontSize: '12px', color: '#5d6064' } }, text)
])
}
},
{
key: 'parent_amount',
dataKey: 'parent_amount',
title: '扣除破损费(已扣)',
width: 130,
cellRenderer: ({ rowData }: any) => {
let total: string | number = ''
if (String(rowData.refundQty).includes('***') || String(rowData.goodsDeprecitionAmount).includes('***')) {
total = '***'
} else {
total = NP.times(rowData.refundQty || 0, rowData.goodsDeprecitionAmount || 0)
}
return h('div', { style: { fontSize: '12px' } }, amountSeparationHasPrefix(total))
}
},
{
key: 'parent_goodsUnpackAmount',
dataKey: 'parent_goodsUnpackAmount',
title: '扣除拆包费',
width: 120,
cellRenderer: ({ rowData }: any) => {
return h('div', { style: { fontSize: '12px' } }, amountSeparationHasPrefix(rowData.parent_goodsUnpackAmount))
}
},
{
key: 'totalMoney',
dataKey: 'totalMoney',
title: '可退总金额',
width: 120,
cellRenderer: ({ rowData }: any) => {
const total = getOrderTotalMoney(rowData.parent_hjsOrderId)
return h('div', { class: 'hn-bold-font', style: { fontSize: '12px' } }, total)
}
}
]
const returnData = ref<ReturnTableData[]>([])
const ReturnRow = ({ rowData, cells, columns }: { rowData: ReturnTableData; rowIndex: number; cells: any; columns: any }) => {
const rowSpan = rowData.rowspan
for (let i = 0; i < columns.length; ++i) {
if (rowSpan > 1 && [0, 5, 7, 8].includes(i)) {
Object.assign(cells[i].props.style, {
backgroundColor: 'var(--el-color-white)',
height: `${rowSpan * tableHeight - 1}px`,
alignSelf: 'flex-start',
zIndex: 9
})
}
}
return cells
}
// 可退总金额
const getOrderTotalMoney = (hjsOrderId: string) => {
const data = returnData.value.filter((v) => v.parent_hjsOrderId === hjsOrderId && v.checked)
let currentHjsOrderId = ''
let total = 0
let hidden = false
for (let i = 0; i < data.length; i++) {
let availableFreight: string | number = 0
let additionalFreight: string | number = 0
let goodsUnpackAmount: string | number = 0
if (currentHjsOrderId !== data[i].parent_hjsOrderId) {
currentHjsOrderId = data[i].parent_hjsOrderId
availableFreight = data[i].together_availableFreight || 0
additionalFreight = data[i].together_additionalFreight || 0
goodsUnpackAmount = data[i].together_goodsUnpackAmount || 0
}
if ([String(data[i].refundQty), String(data[i].availableAmount), availableFreight, additionalFreight, goodsUnpackAmount].includes('***')) {
hidden = true
break
}
const refundAmount = NP.times(Number(data[i].refundQty), data[i].availableAmount ?? 0)
total = NP.plus(total, refundAmount || 0)
// 运费 = 可退运费 + 补扣运费 - 拆包费
total = NP.plus(availableFreight, additionalFreight, total)
total = NP.minus(total, goodsUnpackAmount)
}
if (hidden) return '¥***'
if (total < 0) total = 0
return amountSeparationHasPrefix(total)
}
onMounted(() => {
getRefundAmount()
})
</script>
<style lang="scss" scoped>
.wrapper {
:deep(.table-box) {
padding: 0 0 18px;
.table-title {
@include boldFont;
margin-bottom: 9px;
font-size: 14px;
color: #2e3135;
line-height: 20px;
}
.el-table__cell .cell {
&:empty::before {
content: '--';
}
}
.el-table__empty-text {
padding: 0 !important;
}
}
}
:deep(.return) {
& > .title {
@include boldFont;
height: 20px;
margin: 6px 0 8px;
font-size: 14px;
color: #2e3135;
line-height: 20px;
}
.return-table {
position: relative;
.total {
@include boldFont;
position: absolute;
top: 51px;
right: 1px;
width: 118px;
height: 449px;
z-index: 9;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
color: #2e3135;
font-size: 14px;
color: #323232;
line-height: 20px;
}
}
.el-table__empty-text {
padding: 0 !important;
}
.max-qty {
height: 16px;
margin-top: 4px;
font-size: 12px;
color: #fba218;
line-height: 16px;
}
.refund-money-ipt {
position: relative;
.el-input__icon::before {
content: '¥';
width: 40px;
font-size: 13px;
color: #2e3135;
font-style: normal;
}
.el-input__icon {
margin-right: 0;
}
}
}
</style>