这两天在公司遇到了一个需求,需要在移动端展示报表。我之前在网上简单调研过,移动端的表格没找到几个好用的,ag-grid还算不错。但是我们往往都是需要根据ui的设计来定制好看的移动端报表,又要支持横向、竖向的滚动等等功能,所以只能自己去封装实现。
1、需要实现的功能和想法
功能:表格行/列合并、固定头和列、样式自定义等等,想法:基于better-scroll、单元格宽度计算设置
2、技术栈
vue3+typescript+vant
3、table组件
<template>
<div :style="{ height: `${height}px` }">
<template v-if="list && list.length">
<header class="table-header">
<section class="table-header-fixed">
<div
v-for="column in fixedColumns"
:key="column.key"
:id="column.key"
class="normal-text normal-padding table-header-unit">{{ column.title }}</div>
</section>
<Scroll
ref="headerScroll"
:scrollX="true"
:scrollY="false"
:bounce="false"
:probeType="3"
:useTransition="false"
class="table-header-flow"
@scroll="handleScroll($event, 'headerScroll')">
<section class="table-header-flow-content">
<div
v-for="column in flowColumns"
:key="column.key"
:id="column.key"
class="normal-text normal-padding table-header-unit">{{ column.title }}</div>
</section>
</Scroll>
</header>
<Scroll class="table-article">
<section class="table-article-content">
<section id="table-article-fixed" class="table-article-fixed">
<div
v-for="(item, index) in list"
:key="`fixed${index}`"
class="normal-text table-article-fixed-list"
:class="index === list.length - 1 ? 'table-article-fixed-list-last' : ''">
<div
v-for="column in fixedColumns"
:key="column.key"
:class="column.key"
class="normal-padding table-article-unit">
<template v-if="column.scopedSlots">
<slot :name="column.scopedSlots.customRender" :value="item[column.key]" :item="item">
{{ item[column.key] === null ? '-' : item[column.key] }}
</slot>
</template>
<span v-else>{{ item[column.key] === null ? '-' : item[column.key] }}</span>
</div>
</div>
<!-- 这里合并单元格,我想到的方法是指定覆盖 -->
<template v-if="colSpans && colSpans.length">
<div
v-for="item in colSpans"
:key="item.id"
:data-row-index="item.rowIndex"
:data-col-index="item.colIndex"
:data-content-index="item.contentIndex"
:data-spans="item.spans"
:data-direction="item.direction"
class="normal-text col-row-span"
:style="{ backgroundColor: item.bgColor ? item.bgColor : '', color: item.color ? item.color : '' }"></div>
</template>
</section>
<Scroll
ref="tableScroll"
:scrollX="true"
:scrollY="false"
:bounce="false"
:probeType="3"
:useTransition="false"
class="table-article-flow"
@scroll="handleScroll($event, 'tableScroll')">
<section class="table-article-flow-content">
<div
v-for="(item, index) in list"
:key="`flow${index}`"
class="normal-text table-article-flow-list">
<div
v-for="column in flowColumns"
:key="column.key"
:class="column.key"
class="normal-padding table-article-unit">
<template v-if="column.scopedSlots">
<slot :name="column.scopedSlots.customRender" :value="item[column.key]" :item="item">
{{ item[column.key] === null ? '-' : item[column.key] }}
</slot>
</template>
<span v-else>{{ item[column.key] === null ? '-' : item[column.key] }}</span>
</div>
</div>
</section>
</Scroll>
</section>
</Scroll>
</template>
<div v-else class="no-data-wrap">
<img :src="require('@/assets/images/icon_no_data.png')" alt="暂无数据">
<span>暂无数据~</span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, computed, onUpdated, onMounted } from 'vue'
import { useWindowSize } from '@vant/use'
import Scroll from '@/components/Scroll.vue'
import { ColumnProps, ListProps, SpanProps } from '@/props'
import useTableWidth from '@/hooks/useTableWidth'
import useTableColSpan from '@/hooks/useTableSpan'
import { Pos } from '@/hooks/useScroll'
export default defineComponent({
name: 'Table',
components: {
Scroll
},
props: {
topHeight: {
type: Number,
required: true
},
columns: {
type: Array as PropType<ColumnProps[]>,
required: true
},
list: {
type: Array as PropType<ListProps[] | null>,
required: true
},
colSpans: {
type: Array as PropType<SpanProps[] | null>,
required: false
},
isClone: {
type: Boolean,
default: false
}
},
setup(props) {
const windowHeight = useWindowSize().height
const height = ref(windowHeight.value - props.topHeight)
const fixedColumns = computed(() => {
return props.columns.filter(v => v.fixed)
})
const flowColumns = computed(() => {
return props.columns.filter(v => !v.fixed)
})
onMounted(() => {
useTableWidth(props.columns)
useTableColSpan(true, props.isClone, props.colSpans)
})
onUpdated(() => {
useTableWidth(props.columns)
useTableColSpan(true, props.isClone, props.colSpans)
})
const headerScroll = ref()
const tableScroll = ref()
const handleScroll = (pos: Pos, key: 'headerScroll' | 'tableScroll') => {
if (key === 'headerScroll') {
tableScroll.value?.scroll.scrollTo(pos.x, 0, 0, undefined)
} else {
headerScroll.value?.scroll.scrollTo(pos.x, 0, 0, undefined)
}
}
return {
height,
fixedColumns,
flowColumns,
headerScroll,
tableScroll,
handleScroll
}
}
})
</script>
<style lang="stylus" scoped>
.col-row-span
position: absolute
background-color: #fff
overflow: hidden
display: flex
align-items: center
justify-content: center
z-index 100
.normal-text
color: #555555
font-size: 12px
.normal-padding
padding: 0 6px
.table-header
height: 39px
display: flex
align-items: center
&-fixed
display: flex
align-items: center
height: 100%
background-color: #F4F8FD
position relative
&::after
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 0 solid #EDEDED;
-webkit-transform: scale(.5);
transform: scale(.5);
border-bottom-width: 1px;
&-flow
min-width 1px
flex: 1
height: 100%
overflow: hidden
&-content
display: inline-block
height: 100%
white-space nowrap
background-color: #F4F8FD
position relative
&::after
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 0 solid #EDEDED;
-webkit-transform: scale(.5);
transform: scale(.5);
border-bottom-width: 1px;
&-unit
display: inline-block
height: 39px
line-height 39px
text-align: center
white-space: nowrap
box-sizing: border-box
position relative
&::after
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 0 solid #EDEDED;
-webkit-transform: scale(.5);
transform: scale(.5);
border-left-width: 1px;
.table-article
height: calc(100% - 39px)
overflow: hidden
&-content
display: flex
&-fixed
position relative
&-list
display: flex
align-items: center
height: 32px
&-flow
min-width 1px
flex: 1
overflow: hidden
position relative
&-list
display: flex
align-items: center
height: 32px
&::after
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 0 solid #EDEDED;
-webkit-transform: scale(.5);
transform: scale(.5);
border-right-width: 1px;
&-content
display: inline-block
white-space nowrap
&-unit
display: inline-block
height: 100%
line-height 32px
text-align: center
white-space: nowrap
box-sizing: border-box
position relative
&::after
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 0 solid #EDEDED;
-webkit-transform: scale(.5);
transform: scale(.5);
border-left-width: 1px;
border-bottom-width: 1px;
.no-data-wrap
display: flex
flex-direction: column
align-items: center
padding-top: 30px
img
width: 132px
height: 113.5px
margin-bottom: 19px
span
font-size: 15px
color: #888888
margin-left: 5px
</style>
4、Scroll组件
<template>
<div ref="rootRef">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import useScroll from '@/hooks/useScroll'
export default defineComponent({
name: 'Scroll',
props: {
click: {
type: Boolean,
default: true
},
probeType: {
type: Number,
default: 0
},
scrollX: {
type: Boolean,
default: false
},
scrollY: {
type: Boolean,
default: true
},
bounce: {
type: Boolean,
default: true
},
useTransition: {
type: Boolean,
default: true
}
},
emits: ['scroll'],
setup(props, { emit }) {
const rootRef = ref(null)
const scroll = useScroll(rootRef, props, emit)
return {
rootRef,
scroll
}
}
})
</script>
5、useScroll hook
import BScroll, { Options, BScrollInstance } from '@better-scroll/core'
import ObserveDOM from '@better-scroll/observe-dom'
import { onMounted, onUnmounted, onActivated, onDeactivated, ref, Ref } from 'vue'
BScroll.use(ObserveDOM)
export interface Pos {
x: number;
y: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function useScroll(wrapperRef: Ref, options: Options, emit: (event: "scroll", ...args: any[]) => void): Ref {
const scroll = ref()
onMounted(() => {
const scrollVal: BScrollInstance = scroll.value = new BScroll(wrapperRef.value, {
observeDOM: true,
...options
})
if (options.probeType && options.probeType > 0) {
scrollVal.on('scroll', (pos: Pos) => {
emit('scroll', pos)
})
}
})
onUnmounted(() => {
scroll.value.destroy()
})
onActivated(() => {
scroll.value.enable()
scroll.value.refresh()
})
onDeactivated(() => {
scroll.value.disable()
})
return scroll
}
6、useTableWidth hook
import { ColumnProps } from '@/props'
/**
* 先获取header的宽度,然后获取这个header下面的每个内容的宽度的最大值,然后做对比最后设置
*/
export default (columns: ColumnProps[]):void => {
columns.forEach(column => {
const headerKey: string = column.key
const headerDiv: HTMLDivElement | null = document.querySelector(`#${headerKey}`)
const tableDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll(`.${headerKey}`)
if (headerDiv) {
const headerUnitWidth: number = headerDiv.clientWidth
let maxedUnitWidth: number = headerUnitWidth
const length = tableDivs.length
for (let index = 0; index < length; index++) {
const tableDiv = tableDivs[index]
const tableUnitWidth: number = tableDiv.clientWidth
if (tableUnitWidth > maxedUnitWidth) {
maxedUnitWidth = tableUnitWidth
continue
}
}
// 设置
headerDiv.style.width = maxedUnitWidth + 'px'
tableDivs.forEach(tableDiv => {
tableDiv.style.width = maxedUnitWidth + 'px'
})
}
})
}
7、useTableSpan hook
import { SpanProps } from '@/props'
export default (hasFixed: boolean, isClone: boolean, colSpans: SpanProps[] | null | undefined): void => {
if (hasFixed) {
const wrap: HTMLDivElement | null = document.querySelector('#table-article-fixed')
const list: NodeListOf<HTMLDivElement> = document.querySelectorAll('.table-article-fixed-list')
if (wrap) {
// 找到所有的colspan div
const spanDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll('.col-row-span')
spanDivs.forEach(spanItem => {
// 先拿到rowIndex、colIndex、contentIndex、spans这些属性
const rowIndex: string | undefined = spanItem.dataset.rowIndex
const colIndex: string | undefined = spanItem.dataset.colIndex
const contentIndex: string | undefined = spanItem.dataset.contentIndex
const spans: string | undefined = spanItem.dataset.spans
const direction: string | undefined = spanItem.dataset.direction
// 根据上面这些属性计算出left、top、width、height
let left = 0
let top = 0
let width = 0
let height = 0
let contentElement = null
const rowIndexNum = Number(rowIndex)
const colIndexNum = Number(colIndex)
const spansNum = Number(spans)
const contentIndexNum = Number(contentIndex)
if (rowIndex && colIndex && contentIndex && spans) {
if (direction === 'col') {
// 计算出height和top
const listItem = list[rowIndexNum]
height += listItem.clientHeight
for (let index = 0; index < list.length; index++) {
const element = list[index]
if (rowIndexNum > index) {
top += element.clientHeight
} else {
break
}
}
let count = 0
const spanDivs: HTMLCollection = listItem.children
for (let index = 0; index < spanDivs.length; index++) {
const spanDiv = spanDivs.item(index)
if (index >= colIndexNum && spanDiv && count < spansNum) {
width += spanDiv.clientWidth
count++
}
if (colIndexNum > index && spanDiv) {
left += spanDiv.clientWidth
}
}
// 根据contentIndex拿到内容
contentElement = list[rowIndexNum].children.item(contentIndexNum)
}
if (direction === 'row') {
// 计算出height和top
let count = 0
for (let index = 0; index < list.length; index++) {
const element = list[index]
if (index >= rowIndexNum && count < spansNum) {
height += element.clientHeight
count++
}
if (rowIndexNum > index) {
top += element.clientHeight
}
}
// 计算width和left
const listItem = list[rowIndexNum]
const spanDivs: HTMLCollection = listItem.children
const spanDiv = spanDivs.item(colIndexNum)
if (spanDiv) {
width += spanDiv.clientWidth
}
for (let index = 0; index < spanDivs.length; index++) {
const spanDiv = spanDivs.item(index)
if (colIndexNum > index && spanDiv) {
left += spanDiv.clientWidth
}
}
// 根据contentIndex拿到内容
contentElement = list[rowIndexNum].children.item(contentIndexNum)
}
// 设置样式和内容
spanItem.style.width = (width - 4) + 'px'
spanItem.style.height = (height - 4) + 'px'
spanItem.style.top = (top + 2) + 'px'
spanItem.style.left = (left + 2) + 'px'
// 这里有个问题如果是动态切换一个表格的话
if (contentElement) {
for (let index = 0; index < contentElement.children.length; index++) {
const element = contentElement.children[index]
const copyElement = element.cloneNode(true)
if (isClone) {
spanItem.appendChild(copyElement)
} else {
spanItem.appendChild(element)
}
}
}
}
})
}
if (colSpans) {
colSpans.forEach(colSpan => {
if (colSpan.bgColor) {
if (colSpan.direction === 'col') {
// 找到rowIndex所标注的行变色
const tableArticleFixedElementList: NodeListOf<HTMLDivElement> = document.querySelectorAll('.table-article-fixed-list')
const tableArticleFixedElementSure = tableArticleFixedElementList.item(colSpan.rowIndex)
tableArticleFixedElementSure.style.backgroundColor = colSpan.bgColor
tableArticleFixedElementSure.style.color = colSpan.color || ''
const tableArticleFlowElementList: NodeListOf<HTMLDivElement> = document.querySelectorAll('.table-article-flow-list')
const tableArticleFlowElementSure = tableArticleFlowElementList.item(colSpan.rowIndex)
tableArticleFlowElementSure.style.backgroundColor = colSpan.bgColor
tableArticleFlowElementSure.style.color = colSpan.color || ''
}
if (colSpan.direction === 'row') {
// 找到rowIndex所标注的行加上span的数量变色
const spans = colSpan.spans
const tableArticleFixedElementList: NodeListOf<HTMLDivElement> = document.querySelectorAll('.table-article-fixed-list')
const tableArticleFlowElementList: NodeListOf<HTMLDivElement> = document.querySelectorAll('.table-article-flow-list')
for (let index = 0; index < spans; index++) {
const tableArticleFixedElementSure = tableArticleFixedElementList.item(colSpan.rowIndex + index)
tableArticleFixedElementSure.style.backgroundColor = colSpan.bgColor
tableArticleFixedElementSure.style.color = colSpan.color || ''
const tableArticleFlowElementSure = tableArticleFlowElementList.item(colSpan.rowIndex + index)
tableArticleFlowElementSure.style.backgroundColor = colSpan.bgColor
tableArticleFlowElementSure.style.color = colSpan.color || ''
}
}
}
})
}
}
}
8、接口
export interface ColumnProps {
title: string;
key: string;
fixed?: boolean;
width?: number;
align?: 'left' | 'right' | 'center';
scopedSlots?: { customRender: string; };
}
export interface ListProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
export interface RespData<T = {}> {
code: number;
data: T;
message?: string;
}
export interface DDLoginRequestData {
requestAuthCode: string;
}
export interface DDLoginResponseData {
token: string;
userName: string;
}
export interface DailySalesRequestData {
statisticDate: string;
}
export interface ProjectDailySalesRequestData {
city: string;
startTime: string;
endTime: string;
propertyType: string;
}
export interface DailySalesResponseData {
index: number;
city: string;
propertyType: string | null;
cityName: string;
propertyName: string;
daySubCount: number | null; // 今日销售数据-认购套数
daySubAmount: number | null; // 今日销售数据-认购金额
daySaleCount: number | null; // 今日销售数据-销售套数
daySaleAmount: number | null; // 今日销售数据-销售金额
dayHkAmount: number | null; // 今日销售数据-回款金额
monthSubCount: number | null; // 本月销售数据-认购套数
monthSubAmount: number | null; // 本月销售数据-认购金额
monthSaleCount: number | null; // 本月销售数据-销售套数
monthSaleAmount: number | null; // 本月销售数据-销售金额
monthHkAmount: number | null; // 本月销售数据-回款金额
monthTarget: number | null; // 本月销售数据-销售目标
monthPercent: number | null; // 本月销售数据-完成比例
yearSubCount: number | null; // 本年销售数据-认购套数
yearSubAmount: number | null; // 本年销售数据-认购金额
yearSaleCount: number | null; // 本年销售数据-销售套数
yearSaleAmount: number | null; // 本年销售数据-销售金额
yearHkAmount: number | null; // 本年销售数据-回款金额
yearTarget: number | null; // 本年销售数据-销售目标
yearPercent: number | null; // 本年销售数据-完成比例
}
export interface ProjectDailySalesResponseData {
index: number;
cityName: string;
projectName: string;
propertyName: string;
subCount: number | null; // 认购套数
subAmount: number | null; // 认购金额
saleCount: number | null; // 销售套数
saleAmount: number | null; // 销售金额
saleNoLicenseCount: number | null; // 无证销售套数
saleNoLicenseAmount: number | null; // 无证销售金额
tfCount: number | null; // 退房套数
tfAmount: number | null; // 退房金额
bgCount: number | null; // 变更套数
bgAmount: number | null; // 变更金额
hkxjAmount: number | null; // 回款金额小计
xsxjCount: number | null; // 销售套数小计
xsxjAmount: number | null; // 销售金额小计
monthSaleAmount: number | null; // 本月销售数据-销售金额
monthTarget: number | null; // 本月销售数据-销售目标
monthPercent: number | null; // 本月销售数据-完成比例
yearSaleAmount: number | null; // 本年销售数据-销售金额
yearTarget: number | null; // 本年销售数据-销售目标
yearPercent: number | null; // 本年销售数据-完成比例
}
export interface ProjectDailySalesRouteQuery {
city: string;
active: 0 | 1 | 2;
propertyType: string;
date: string;
}
export interface DailySalesRouteQuery {
date: string;
}
export interface SpanProps {
rowIndex: number;
colIndex: number;
contentIndex: number;
spans: number;
id: string;
direction: 'col' | 'row';
bgColor?: string;
color?: string;
}
export interface EstateDailySalesResponseData {
index: number;
city: string;
cityName: string;
propertyType: string | null;
propertyName: string | null;
projectName: string;
daySubCount: number | null; // 今日销售数据-认购套数
daySubAmount: number | null; // 今日销售数据-认购金额
daySaleCount: number | null; // 今日销售数据-销售套数
daySaleAmount: number | null; // 今日销售数据-销售金额
dayHkAmount: number | null; // 今日销售数据-回款金额
monthSubCount: number | null; // 本月销售数据-认购套数
monthSubAmount: number | null; // 本月销售数据-认购金额
monthSaleCount: number | null; // 本月销售数据-销售套数
monthSaleAmount: number | null; // 本月销售数据-销售金额
monthHkAmount: number | null; // 本月销售数据-回款金额
monthTarget: number | null; // 本月销售数据-销售目标
monthPercent: number | null; // 本月销售数据-完成比例
yearSubCount: number | null; // 本年销售数据-认购套数
yearSubAmount: number | null; // 本年销售数据-认购金额
yearSaleCount: number | null; // 本年销售数据-销售套数
yearSaleAmount: number | null; // 本年销售数据-销售金额
yearHkAmount: number | null; // 本年销售数据-回款金额
yearTarget: number | null; // 本年销售数据-销售目标
yearPercent: number | null; // 本年销售数据-完成比例
}
export interface InvestDailySalesResponseData {
index: number;
city: string;
cityName: string;
propertyType: string | null;
projectName: string;
daySubCount: number | null; // 今日销售数据-认购套数
daySubAmount: number | null; // 今日销售数据-认购金额
daySaleCount: number | null; // 今日销售数据-销售套数
daySaleAmount: number | null; // 今日销售数据-销售金额
dayHkAmount: number | null; // 今日销售数据-回款金额
monthSubCount: number | null; // 本月销售数据-认购套数
monthSubAmount: number | null; // 本月销售数据-认购金额
monthSaleCount: number | null; // 本月销售数据-销售套数
monthSaleAmount: number | null; // 本月销售数据-销售金额
monthHkAmount: number | null; // 本月销售数据-回款金额
monthTarget: number | null; // 本月销售数据-销售目标
monthPercent: number | null; // 本月销售数据-完成比例
yearSubCount: number | null; // 本年销售数据-认购套数
yearSubAmount: number | null; // 本年销售数据-认购金额
yearSaleCount: number | null; // 本年销售数据-销售套数
yearSaleAmount: number | null; // 本年销售数据-销售金额
yearHkAmount: number | null; // 本年销售数据-回款金额
yearTarget: number | null; // 本年销售数据-销售目标
yearPercent: number | null; // 本年销售数据-完成比例
}
export interface ParkingDailySalesResponseData {
index: number;
city: string;
cityName: string;
propertyType: string | null;
projectName: string;
daySubCount: number | null; // 今日销售数据-认购套数
daySubAmount: number | null; // 今日销售数据-认购金额
daySaleCount: number | null; // 今日销售数据-销售套数
daySaleAmount: number | null; // 今日销售数据-销售金额
dayHkAmount: number | null; // 今日销售数据-回款金额
monthSubCount: number | null; // 本月销售数据-认购套数
monthSubAmount: number | null; // 本月销售数据-认购金额
monthSaleCount: number | null; // 本月销售数据-销售套数
monthSaleAmount: number | null; // 本月销售数据-销售金额
monthHkAmount: number | null; // 本月销售数据-回款金额
monthTarget: number | null; // 本月销售数据-销售目标
monthPercent: number | null; // 本月销售数据-完成比例
yearSubCount: number | null; // 本年销售数据-认购套数
yearSubAmount: number | null; // 本年销售数据-认购金额
yearSaleCount: number | null; // 本年销售数据-销售套数
yearSaleAmount: number | null; // 本年销售数据-销售金额
yearHkAmount: number | null; // 本年销售数据-回款金额
yearTarget: number | null; // 本年销售数据-销售目标
yearPercent: number | null; // 本年销售数据-完成比例
}
9、用例
<Table :topHeight="186.5" :columns="columns" :list="list" :colSpans="colSpans" :isClone="true">
<template v-slot:index="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:propertyName="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:subCount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:subAmount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value.toFixed(2) }}</span>
</template>
<template v-slot:saleCount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:saleAmount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value.toFixed(2) }}</span>
</template>
<template v-slot:saleNoLicenseCount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:saleNoLicenseAmount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value.toFixed(2) }}</span>
</template>
<template v-slot:tfCount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:tfAmount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value.toFixed(2) }}</span>
</template>
<template v-slot:bgCount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:bgAmount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value.toFixed(2) }}</span>
</template>
<template v-slot:hkxjAmount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value.toFixed(2) }}</span>
</template>
<template v-slot:xsxjCount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value }}</span>
</template>
<template v-slot:xsxjAmount="{ value, item }">
<span :style="{ fontWeight: item.propertyType === '-1' ? 'bold' : 'normal' }">{{ value === null ? '-' : value.toFixed(2) }}</span>
</template>
</Table>
export const columns: ColumnProps[] = [
{
title: '序号',
key: 'index',
scopedSlots: { customRender: 'index' },
fixed: true
},
{
title: '城市',
key: 'cityName',
fixed: true
},
{
title: '项目',
key: 'projectName',
fixed: true
},
{
title: '业态',
key: 'propertyName',
scopedSlots: { customRender: 'propertyName' },
// fixed: true
},
{
title: '认购套数',
key: 'subCount',
scopedSlots: { customRender: 'subCount' }
},
{
title: '认购金额',
key: 'subAmount',
scopedSlots: { customRender: 'subAmount' }
},
{
title: '销售套数',
key: 'saleCount',
scopedSlots: { customRender: 'saleCount' }
},
{
title: '销售金额',
key: 'saleAmount',
scopedSlots: { customRender: 'saleAmount' }
},
{
title: '无证销售套数',
key: 'saleNoLicenseCount',
scopedSlots: { customRender: 'saleNoLicenseCount' }
},
{
title: '无证销售金额',
key: 'saleNoLicenseAmount',
scopedSlots: { customRender: 'saleNoLicenseAmount' }
},
{
title: '退房套数',
key: 'tfCount',
scopedSlots: { customRender: 'tfCount' }
},
{
title: '退房金额',
key: 'tfAmount',
scopedSlots: { customRender: 'tfAmount' }
},
{
title: '变更套数',
key: 'bgCount',
scopedSlots: { customRender: 'bgCount' }
},
{
title: '变更金额',
key: 'bgAmount',
scopedSlots: { customRender: 'bgAmount' }
},
{
title: '回款金额小计',
key: 'hkxjAmount',
scopedSlots: { customRender: 'hkxjAmount' }
},
{
title: '销售套数小计',
key: 'xsxjCount',
scopedSlots: { customRender: 'xsxjCount' }
},
{
title: '销售金额小计',
key: 'xsxjAmount',
scopedSlots: { customRender: 'xsxjAmount' }
},
]