多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

387 lines
11 KiB

<script lang="ts" setup>
import type { CSSProperties } from '@vue/runtime-dom'
interface Props {
columns: NcTableColumnProps[]
data: Record<string, any>[]
headerRowHeight?: CSSProperties['height']
rowHeight?: CSSProperties['height']
orderBy?: Record<string, SordDirectionType>
multiFieldOrderBy?: boolean
bordered?: boolean
isDataLoading?: boolean
stickyHeader?: boolean
stickyFirstColumn?: boolean
headerRowClassName?: string
bodyRowClassName?: string
headerCellClassName?: string
bodyCellClassName?: string
customHeaderRow?: (columns: NcTableColumnProps[]) => Record<string, any>
customRow?: (record: Record<string, any>, recordIndex: number) => Record<string, any>
}
const props = withDefaults(defineProps<Props>(), {
columns: () => [] as NcTableColumnProps[],
data: () => [] as Record<string, any>[],
headerRowHeight: '54px',
rowHeight: '54px',
orderBy: () => ({} as Record<string, SordDirectionType>),
multiFieldOrderBy: false,
bordered: true,
isDataLoading: false,
stickyHeader: true,
headerRowClassName: '',
bodyRowClassName: '',
headerCellClassName: '',
bodyCellClassName: '',
customHeaderRow: () => ({}),
customRow: () => ({}),
})
const emit = defineEmits(['update:orderBy'])
const tableWrapper = ref<HTMLDivElement>()
const tableHeader = ref<HTMLTableElement>()
const tableFooterRef = ref<HTMLDivElement>()
const { height: tableHeadHeight, width: tableHeadWidth } = useElementBounding(tableHeader)
const { height: tableFooterHeight } = useElementBounding(tableFooterRef)
const orderBy = useVModel(props, 'orderBy', emit)
const { columns, data, isDataLoading, customHeaderRow, customRow } = toRefs(props)
const headerRowClassName = computed(() => `nc-table-header-row ${props.headerRowClassName}`)
const bodyRowClassName = computed(() => `nc-table-row ${props.headerRowClassName}`)
const slots = useSlots()
const headerCellWidth = ref<(number | undefined)[]>([])
const updateOrderBy = (field: string) => {
if (!data.value.length || !field) return
const orderCycle = { undefined: 'asc', asc: 'desc', desc: undefined }
if (props.multiFieldOrderBy) {
orderBy.value[field] = orderCycle[`${orderBy.value[field]}`] as SordDirectionType
} else {
orderBy.value = { [field]: orderCycle[`${orderBy.value[field]}`] as SordDirectionType }
}
}
/**
* We are using 2 different table tag to make header sticky,
* so it's imp to keep header cell and body cell width same
*/
watch(
tableHeadWidth,
() => {
if (!tableHeader.value || !tableHeadWidth.value) return
nextTick(() => {
const headerCells = tableHeader.value?.querySelectorAll('th > div')
if (headerCells && headerCells.length) {
headerCells.forEach((el, i) => {
headerCellWidth.value[i] = el.getBoundingClientRect().width || undefined
})
}
})
},
{
immediate: true,
},
)
useEventListener(tableWrapper, 'scroll', () => {
const stickyHeaderCell = tableWrapper.value?.querySelector('th:nth-of-type(1)')
const nonStickyHeaderFirstCell = tableWrapper.value?.querySelector('th:nth-of-type(2)')
if (
!stickyHeaderCell ||
!nonStickyHeaderFirstCell ||
!stickyHeaderCell?.getBoundingClientRect()?.right ||
!nonStickyHeaderFirstCell?.getBoundingClientRect()?.left
) {
return
}
if (nonStickyHeaderFirstCell?.getBoundingClientRect().left < stickyHeaderCell?.getBoundingClientRect().right) {
tableWrapper.value?.classList.add('sticky-border')
} else {
tableWrapper.value?.classList.remove('sticky-border')
}
})
</script>
<template>
<div
class="nc-table-container relative"
:class="{
bordered,
'min-h-120': isDataLoading,
}"
>
<div
ref="tableWrapper"
class="nc-table-wrapper max-h-full relative nc-scrollbar-thin !overflow-auto"
:class="{
'sticky-first-column': stickyFirstColumn,
'h-full': data.length,
}"
:style="{
maxHeight: `calc(100% - ${tableFooterHeight}px)`,
}"
>
<table
ref="tableHeader"
class="w-full max-w-full"
:class="{
'!sticky top-0 z-5': stickyHeader,
}"
>
<thead>
<tr
:style="{
height: headerRowHeight,
}"
:class="[`${headerRowClassName}`]"
v-bind="customHeaderRow ? customHeaderRow(columns) : {}"
>
<th
v-for="(col, index) in columns"
:key="index"
class="nc-table-header-cell"
:class="[
`${headerCellClassName}`,
`nc-table-header-cell-${index}`,
{
'!hover:bg-gray-100 select-none cursor-pointer': col.showOrderBy,
'cursor-not-allowed': col.showOrderBy && !data?.length,
'!text-gray-700': col.showOrderBy && col?.dataIndex && orderBy[col.dataIndex],
'flex-1': !col.width && !col.basis,
},
]"
:style="{
width: col.width,
flexBasis: !col.width ? col.basis : undefined,
maxWidth: col.width ? col.width : undefined,
}"
:data-test-id="`nc-table-header-cell-${col.name || col.key}`"
@click="col.showOrderBy && col?.dataIndex ? updateOrderBy(col.dataIndex) : undefined"
>
<div
class="gap-3"
:class="[`${col.justify || ''}`]"
:style="{
padding: col.padding || '0px 24px',
minWidth: `calc(${col.minWidth}px - 2px)`,
}"
>
<slot name="headerCell" :column="col">
<div>{{ col.title || col.name || '' }}</div>
</slot>
<template v-if="col.showOrderBy && col?.dataIndex">
<GeneralIcon
v-if="orderBy[col.dataIndex]"
icon="chevronDown"
class="flex-none"
:class="{
'transform rotate-180': orderBy[col.dataIndex] === 'asc',
}"
/>
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
</template>
</div>
</th>
</tr>
</thead>
</table>
<template v-if="data.length">
<table
class="w-full h-full"
:style="{
maxHeight: `calc(100% - ${tableHeadHeight}px)`,
}"
>
<tbody>
<tr
v-for="(record, recordIndex) of data"
:key="recordIndex"
:style="{
height: rowHeight,
}"
:class="[`${bodyRowClassName}`, `nc-table-row-${recordIndex}`]"
v-bind="customRow ? customRow(record, recordIndex) : {}"
>
<td
v-for="(col, colIndex) of columns"
:key="colIndex"
class="nc-table-cell"
:class="[
`${bodyCellClassName}`,
`nc-table-cell-${recordIndex}`,
{
'flex-1': !col.width && !col.basis,
},
]"
:style="{
width: col.width,
flexBasis: !col.width ? col.basis : undefined,
maxWidth: col.width ? col.width : undefined,
}"
:data-test-id="`nc-table-cell-${col.name || col.key}`"
>
<div
:class="[`${col.align || 'items-center'} ${col.justify || ''}`]"
:style="{
padding: col.padding || '0px 24px',
minWidth: `calc(${col.minWidth}px - 2px)`,
maxWidth: headerCellWidth[colIndex] ? `${headerCellWidth[colIndex]}px` : undefined,
}"
>
<slot name="bodyCell" :column="col" :record="record" :record-index="recordIndex">
{{ col?.dataIndex && col.key !== 'action' ? record[col.dataIndex] : '' }}
</slot>
</div>
</td>
</tr>
<template v-if="slots.extraRow">
<tr class="nc-table-extra-row">
<slot name="extraRow" />
</tr>
</template>
</tbody>
</table>
</template>
</div>
<div
v-show="isDataLoading"
class="flex items-center justify-center absolute left-0 top-0 w-full h-full z-10 pointer-events-none"
>
<div class="flex flex-col justify-center items-center gap-2">
<GeneralLoader size="xlarge" />
<span class="text-center">{{ $t('general.loading') }}</span>
</div>
</div>
<div
v-if="!isDataLoading && !data?.length"
class="flex-none nc-table-empty flex items-center justify-center py-8 px-6 h-full"
:style="{
maxHeight: `calc(100% - ${headerRowHeight} - ${tableFooterHeight}px)`,
}"
>
<div class="flex-none text-center flex flex-col items-center gap-3">
<slot name="emptyText">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" class="!my-0" />
</slot>
</div>
</div>
<!-- Not scrollable footer -->
<template v-if="slots.tableFooter">
<div ref="tableFooterRef">
<slot name="tableFooter" />
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.nc-table-container {
&.bordered {
@apply border-1 border-gray-200 rounded-lg overflow-hidden w-full;
}
&:not(.bordered) {
@apply overflow-hidden w-full;
}
.nc-table-wrapper {
@apply w-full;
&.sticky-first-column {
th {
&:first-of-type {
@apply bg-gray-50;
}
}
td {
&:first-of-type {
@apply bg-white;
}
}
th,
td {
&:first-of-type {
@apply border-r-1 border-transparent sticky left-0 z-4;
}
}
&.sticky-border {
th,
td {
&:first-of-type {
@apply !border-gray-200;
}
}
}
}
thead {
@apply w-full max-w-full;
th {
@apply bg-gray-50 text-sm text-gray-500 font-weight-500;
&.cell-title {
@apply sticky left-0 z-4 bg-gray-50;
}
}
}
tbody {
@apply w-full max-w-full;
tr {
&:not(.nc-table-extra-row) {
@apply cursor-pointer;
}
td {
@apply text-sm text-gray-600;
}
}
}
tr {
@apply flex w-full max-w-full;
&:not(.nc-table-extra-row) {
@apply border-b-1 border-gray-200;
}
&.selected td {
@apply !bg-[#F0F3FF];
}
&:not(.selected):hover td {
@apply !bg-gray-50;
}
th,
td {
@apply h-full flex;
& > div {
@apply h-full flex-1 flex items-center;
}
}
}
}
}
</style>