From 23a84844a4d03446059171f230674bc22eec1eaa Mon Sep 17 00:00:00 2001 From: saller Date: Thu, 28 Dec 2023 03:36:34 +0800 Subject: [PATCH] feat(comp:table): add virtualHorizontal support (#1776) --- .../table/demo/AutoHeightVirtual.md | 2 +- packages/components/table/demo/VirtualBoth.md | 6 + .../components/table/demo/VirtualBoth.vue | 57 ++++++ .../table/demo/VirtualHorizontal.md | 14 ++ .../table/demo/VirtualHorizontal.vue | 38 ++++ packages/components/table/docs/Api.zh.md | 6 +- packages/components/table/src/Table.tsx | 21 +- .../table/src/composables/useColumns.ts | 177 +++++++++------- .../table/src/composables/useScroll.ts | 3 +- .../table/src/composables/useTableLayout.ts | 12 +- .../components/table/src/main/ColGroup.tsx | 56 +++--- .../components/table/src/main/FixedHolder.tsx | 189 ++++++++++++++++-- .../components/table/src/main/MainTable.tsx | 155 +++++++++++--- .../components/table/src/main/body/Body.tsx | 36 +--- .../table/src/main/body/BodyCell.tsx | 91 ++++++--- .../table/src/main/body/BodyRow.tsx | 88 ++------ .../table/src/main/body/MeasureCell.tsx | 6 +- .../table/src/main/body/MeasureRow.tsx | 11 +- .../table/src/main/body/RenderBodyCells.tsx | 51 +++++ .../table/src/main/body/RenderBodyRow.tsx | 3 +- .../components/table/src/main/head/Head.tsx | 25 ++- .../table/src/main/head/HeadCell.tsx | 11 +- .../table/src/main/head/HeadRow.tsx | 9 +- .../table/src/main/head/RenderHeaderCells.tsx | 23 +++ .../table/src/main/head/RenderHeaderRow.tsx | 23 +++ packages/components/table/src/token.ts | 17 +- packages/components/table/src/types.ts | 19 +- packages/components/table/src/utils/index.ts | 55 +++++ packages/components/table/style/index.less | 10 +- packages/pro/table/demo/MasiveColumns.md | 12 ++ packages/pro/table/demo/MasiveColumns.vue | 132 ++++++++++++ packages/pro/table/demo/Resizable.vue | 1 + 32 files changed, 1034 insertions(+), 325 deletions(-) create mode 100644 packages/components/table/demo/VirtualBoth.md create mode 100644 packages/components/table/demo/VirtualBoth.vue create mode 100644 packages/components/table/demo/VirtualHorizontal.md create mode 100644 packages/components/table/demo/VirtualHorizontal.vue create mode 100644 packages/components/table/src/main/body/RenderBodyCells.tsx create mode 100644 packages/components/table/src/main/head/RenderHeaderCells.tsx create mode 100644 packages/components/table/src/main/head/RenderHeaderRow.tsx create mode 100644 packages/pro/table/demo/MasiveColumns.md create mode 100644 packages/pro/table/demo/MasiveColumns.vue diff --git a/packages/components/table/demo/AutoHeightVirtual.md b/packages/components/table/demo/AutoHeightVirtual.md index 367f5b1f5..ca6e3c4b6 100644 --- a/packages/components/table/demo/AutoHeightVirtual.md +++ b/packages/components/table/demo/AutoHeightVirtual.md @@ -2,6 +2,6 @@ title: zh: 自动高度 + 虚拟滚动 en: Auto height + Virtual -order: 91 +order: 93 hidden: true --- diff --git a/packages/components/table/demo/VirtualBoth.md b/packages/components/table/demo/VirtualBoth.md new file mode 100644 index 000000000..197d9c4e8 --- /dev/null +++ b/packages/components/table/demo/VirtualBoth.md @@ -0,0 +1,6 @@ +--- +title: + zh: 横向&竖向虚拟滚动 + en: Virtual horizontal and vertical +order: 92 +--- diff --git a/packages/components/table/demo/VirtualBoth.vue b/packages/components/table/demo/VirtualBoth.vue new file mode 100644 index 000000000..debfacf46 --- /dev/null +++ b/packages/components/table/demo/VirtualBoth.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/components/table/demo/VirtualHorizontal.md b/packages/components/table/demo/VirtualHorizontal.md new file mode 100644 index 000000000..450d39fbc --- /dev/null +++ b/packages/components/table/demo/VirtualHorizontal.md @@ -0,0 +1,14 @@ +--- +title: + zh: 横向虚拟滚动 + en: Virtual horizontal +order: 91 +--- + +## zh + +通过 `virtualHorizontal` 开启横向虚拟滚动,横向虚拟滚动必须指定列宽 + +## en + +Enable horizontal virtual scroll via `virtualHorizontal`, column width must be provided. diff --git a/packages/components/table/demo/VirtualHorizontal.vue b/packages/components/table/demo/VirtualHorizontal.vue new file mode 100644 index 000000000..f22b9ad95 --- /dev/null +++ b/packages/components/table/demo/VirtualHorizontal.vue @@ -0,0 +1,38 @@ + + + diff --git a/packages/components/table/docs/Api.zh.md b/packages/components/table/docs/Api.zh.md index 8a33e5822..015069bad 100644 --- a/packages/components/table/docs/Api.zh.md +++ b/packages/components/table/docs/Api.zh.md @@ -25,8 +25,12 @@ | `size` | 表格大小 | `'lg' \| 'md' \| 'sm'` | `md` | ✅ |- | | `spin` | 表格是否加载中 | `boolean \| SpinProps` | - | - | - | | `tableLayout` | 表格元素的 [table-layout](https://developer.mozilla.org/zh-CN/docs/Web/CSS/table-layout) 属性 | `'auto' \| 'fixed'` | - | - | 固定表头/列或设置了 `column.ellipsis` 时,默认值为 `fixed` | -| `virtual` | 是否开启虚拟滚动 | `boolean` | `false` | - | 需要设置 `scroll.height` | +| `virtual` | 是否开启纵向虚拟滚动 | `boolean` | `false` | - | 需要设置 `scroll.height` | +| `virtualHorizontal` | 是否开启横向虚拟滚动 | `boolean` | `false` | - | 不可以设置 `scroll.width`,并且每列的宽度必须配置 | | `virtualItemHeight` | 虚拟滚动每一行的高度 | `number` | - | - | 标准大小的表格不需要设置,会自动设置。如果有非标准大小的表格,可以设置一个准确的值来提高性能。 | +| `virtualColWidth` | 虚拟滚动每一列的宽度 | `number` | - | - | 需要设置一个默认的值。 | +| `virtualBufferSize` | 虚拟滚动的buffer大小 | `number` | - | - | - | +| `virtualBufferOffset` | 虚拟滚动的buffer边界offset | `number` | - | - | - | | `onScroll` | 滚动事件 | `(evt: Event) => void` | - | - | - | | `onScrolledChange` | 滚动的位置发生变化 | `(startIndex: number, endIndex: number, visibleNodes: TreeNode[]) => void` | - | - | 仅 `virtual` 模式下可用 | | `onScrolledBottom` | 滚动到底部时触发 | `() => void` | - | - | 仅 `virtual` 模式下可用 | diff --git a/packages/components/table/src/Table.tsx b/packages/components/table/src/Table.tsx index a32a66996..776d83790 100644 --- a/packages/components/table/src/Table.tsx +++ b/packages/components/table/src/Table.tsx @@ -5,6 +5,8 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +import type { VirtualScrollEnabled } from '@idux/cdk/scroll' + import { type VNode, computed, defineComponent, normalizeClass, provide } from 'vue' import { isBoolean } from 'lodash-es' @@ -34,6 +36,7 @@ import { tableProps } from './types' import { getThemeTokens } from '../theme' const virtualItemHeight = { sm: 32, md: 40, lg: 56 } as const +const virtualColWidth = 150 export default defineComponent({ name: 'IxTable', @@ -53,7 +56,14 @@ export default defineComponent({ const mergedEmptyCell = computed(() => props.emptyCell ?? config.emptyCell) const mergedInsetShadow = computed(() => props.insetShadow ?? config.insetShadow) const mergedSize = computed(() => props.size ?? config.size) + const mergedVirtual = computed(() => { + return { + horizontal: props.virtualHorizontal, + vertical: props.virtual, + } + }) const mergedVirtualItemHeight = computed(() => props.virtualItemHeight ?? virtualItemHeight[mergedSize.value]) + const mergedVirtualColWidth = computed(() => props.virtualColWidth ?? virtualColWidth) const { mergedPagination } = usePagination(props, config, mergedSize) const stickyContext = useSticky(props) @@ -62,7 +72,14 @@ export default defineComponent({ const sortableContext = useSortable(columnsContext.flattedColumns) const filterableContext = useFilterable(columnsContext.flattedColumns) const expandableContext = useExpandable(props, columnsContext.flattedColumns) - const tableLayout = useTableLayout(props, columnsContext, scrollContext, stickyContext.isSticky, mergedAutoHeight) + const tableLayout = useTableLayout( + props, + columnsContext, + scrollContext, + stickyContext.isSticky, + mergedVirtual, + mergedAutoHeight, + ) const { activeSorters } = sortableContext const { activeFilters } = filterableContext @@ -88,7 +105,9 @@ export default defineComponent({ mergedPrefixCls, mergedEmptyCell, mergedInsetShadow, + mergedVirtual, mergedVirtualItemHeight, + mergedVirtualColWidth, mergedAutoHeight, ...columnsContext, ...scrollContext, diff --git a/packages/components/table/src/composables/useColumns.ts b/packages/components/table/src/composables/useColumns.ts index 1a19cb674..230ea026a 100644 --- a/packages/components/table/src/composables/useColumns.ts +++ b/packages/components/table/src/composables/useColumns.ts @@ -12,7 +12,6 @@ import { type VNode, type VNodeChild, computed, - reactive, ref, watch, watchEffect, @@ -49,17 +48,16 @@ export function useColumns( mergedColumns, scrollBarSizeOnFixedHolder, ) - const fixedColumnKeys = useFixedColumnKeys(flattedColumnsWithScrollBar) + const { fixedColumns, fixedColumnKeys } = useFixedColumns(flattedColumnsWithScrollBar) const hasEllipsis = computed( () => !!props.ellipsis || flattedColumns.value.some(column => (column as TableColumnBase).ellipsis), ) const hasFixed = computed(() => flattedColumns.value.some(column => column.fixed)) - const { columnWidths, columnWidthsWithScrollBar, changeColumnWidth } = useColumnWidths( - flattedColumns, - scrollBarColumn, - ) - const { columnOffsets, columnOffsetsWithScrollBar } = useColumnOffsets(columnWidths, columnWidthsWithScrollBar) + const columnCount = computed(() => flattedColumnsWithScrollBar.value.length) + + const { columnWidthMap, columnWidths, changeColumnWidth, clearColumnWidth } = useColumnWidths(flattedColumns) + const { columnOffsets, columnOffsetsWithScrollBar } = useColumnOffsets(fixedColumns, columnWidthMap, columnCount) const mergedRows = computed(() => mergeRows(mergedColumns.value, scrollBarColumn.value)) @@ -67,12 +65,14 @@ export function useColumns( flattedColumns, scrollBarColumn, flattedColumnsWithScrollBar, + fixedColumns, fixedColumnKeys, hasEllipsis, hasFixed, + columnWidthMap, columnWidths, - columnWidthsWithScrollBar, changeColumnWidth, + clearColumnWidth, columnOffsets, columnOffsetsWithScrollBar, mergedRows, @@ -83,18 +83,32 @@ export interface ColumnsContext { flattedColumns: ComputedRef scrollBarColumn: ComputedRef flattedColumnsWithScrollBar: ComputedRef<(TableColumnMerged | TableColumnScrollBar)[]> + fixedColumns: ComputedRef<{ + fixedStartColumns: (TableColumnMerged | TableColumnScrollBar)[] + fixedEndColumns: (TableColumnMerged | TableColumnScrollBar)[] + }> fixedColumnKeys: ComputedRef<{ lastStartKey: VKey | undefined firstEndKey: VKey | undefined }> hasEllipsis: ComputedRef hasFixed: ComputedRef + columnWidthMap: Ref> columnWidths: Ref - columnWidthsWithScrollBar: ComputedRef changeColumnWidth: (key: VKey, width: number | false) => void - columnOffsets: ComputedRef<{ starts: number[]; ends: number[] }> - columnOffsetsWithScrollBar: ComputedRef<{ starts: number[]; ends: number[] }> - mergedRows: ComputedRef + clearColumnWidth: () => void + columnOffsets: ComputedRef<{ + starts: Record + ends: Record + }> + columnOffsetsWithScrollBar: ComputedRef<{ + starts: Record + ends: Record + }> + mergedRows: ComputedRef<{ + rows: TableColumnMergedExtra[][] + offsetIndexMap: Record + }> } export type TableColumnMerged = (TableColumnMergedBase | TableColumnMergedExpandable | TableColumnMergedSelectable) & { @@ -225,10 +239,10 @@ function useFlattedColumns( const scrollBarColumn = computed(() => { const scrollBarSize = scrollBarSizeOnFixedHolder.value - if (scrollBarSize === 0) { + const columns = flattedColumns.value + if (scrollBarSize === 0 || columns.length === 0) { return undefined } - const columns = flattedColumns.value const lastColumn = columns[columns.length - 1] return { key: '__IDUX_table_column_key_scroll-bar', @@ -240,9 +254,6 @@ function useFlattedColumns( const flattedColumnsWithScrollBar = computed(() => { const columns = flattedColumns.value - if (columns.length === 0) { - return columns - } const scrollBar = scrollBarColumn.value return scrollBar ? [...columns, scrollBar] : columns }) @@ -250,12 +261,12 @@ function useFlattedColumns( return { flattedColumns, scrollBarColumn, flattedColumnsWithScrollBar } } -function flatColumns(columns: TableColumnMerged[]) { - const result: TableColumnMerged[] = [] +export function flatColumns(columns: Col[]): Col[] { + const result: Col[] = [] columns.forEach(column => { const { fixed, children: subColumns } = column as TableColumnBase if (subColumns?.length) { - let subFlattedColumns = flatColumns(subColumns as TableColumnMerged[]) + let subFlattedColumns = flatColumns(subColumns as Col[]) if (fixed) { subFlattedColumns = subFlattedColumns.map(item => ({ fixed, ...item })) } @@ -267,94 +278,116 @@ function flatColumns(columns: TableColumnMerged[]) { return result } -function useFixedColumnKeys(flattedColumnsWithScrollBar: ComputedRef<(TableColumnMerged | TableColumnScrollBar)[]>) { - return computed(() => { - let lastStartKey: VKey | undefined - let firstEndKey: VKey | undefined +function useFixedColumns(flattedColumnsWithScrollBar: ComputedRef<(TableColumnMerged | TableColumnScrollBar)[]>) { + const fixedColumns = computed(() => { + const fixedStartColumns: (TableColumnMerged | TableColumnScrollBar)[] = [] + const fixedEndColumns: (TableColumnMerged | TableColumnScrollBar)[] = [] + flattedColumnsWithScrollBar.value.forEach(column => { - const { fixed, key } = column + const { fixed } = column if (fixed === 'start') { - lastStartKey = key + fixedStartColumns.push(column) } else if (fixed === 'end') { - if (!firstEndKey) { - firstEndKey = key - } + fixedEndColumns.push(column) } }) - return { lastStartKey, firstEndKey } + return { fixedStartColumns, fixedEndColumns } + }) + const fixedColumnKeys = computed(() => { + const { fixedStartColumns, fixedEndColumns } = fixedColumns.value + + return { lastStartKey: fixedStartColumns[fixedStartColumns.length - 1]?.key, firstEndKey: fixedEndColumns[0]?.key } }) + + return { fixedColumns, fixedColumnKeys } } -function useColumnWidths( - flattedColumns: ComputedRef, - scrollBarColumn: ComputedRef, -) { - const widthMap = reactive>({}) +function useColumnWidths(flattedColumns: ComputedRef) { + const widthMap = ref>({}) const widthString = ref() const columnWidths = ref([]) watch( widthString, // resizable: 列宽设置百分比的情况下,拖拽会改变多列的宽度,用 debounce 来减少重复渲染次数。 debounce(widths => { - columnWidths.value = widths ? widths.split('-').map(Number) : [] + columnWidths.value = widths ? widths.split('-').filter(Boolean).map(Number) : [] }, 16), ) watchEffect(() => { - const keys = Object.keys(widthMap) const columns = flattedColumns.value - if (keys.length !== columns.length) { - widthString.value = undefined - return - } - - widthString.value = columns.map(column => widthMap[column.key]).join('-') - }) - - const columnWidthsWithScrollBar = computed(() => { - const widths = columnWidths.value - if (widths.length === 0) { - return widths - } - const scrollBar = scrollBarColumn.value - return scrollBar ? [...widths, scrollBar.width] : widths + widthString.value = columns.map(column => widthMap.value[column.key]).join('-') }) const changeColumnWidth = (key: VKey, width: number | false) => { if (width === false) { - delete widthMap[key] + delete widthMap.value[key] } else { - widthMap[key] = width + widthMap.value[key] = width } } - return { columnWidths, columnWidthsWithScrollBar, changeColumnWidth } + const clearColumnWidth = () => { + widthMap.value = {} + } + + return { columnWidthMap: widthMap, columnWidths, changeColumnWidth, clearColumnWidth } } -function useColumnOffsets(columnWidths: Ref, columnWidthsWithScrollBar: ComputedRef) { - const columnOffsets = computed(() => calculateOffsets(columnWidths.value)) - const columnOffsetsWithScrollBar = computed(() => calculateOffsets(columnWidthsWithScrollBar.value)) +function useColumnOffsets( + fixedColumns: ComputedRef<{ + fixedStartColumns: (TableColumnMerged | TableColumnScrollBar)[] + fixedEndColumns: (TableColumnMerged | TableColumnScrollBar)[] + }>, + columnWidthsMap: Ref>, + columnCount: Ref, +) { + const columnOffsets = computed(() => + calculateOffsets( + fixedColumns.value.fixedStartColumns, + fixedColumns.value.fixedEndColumns.filter(column => column.type !== 'scroll-bar'), + columnWidthsMap.value, + columnCount.value - 1, + ), + ) + const columnOffsetsWithScrollBar = computed(() => + calculateOffsets( + fixedColumns.value.fixedStartColumns, + fixedColumns.value.fixedEndColumns, + columnWidthsMap.value, + columnCount.value, + ), + ) return { columnOffsets, columnOffsetsWithScrollBar } } -function calculateOffsets(widths: number[]) { - const count = widths.length - const startOffsets: number[] = [] - const endOffsets: number[] = [] +function calculateOffsets( + startColumns: (TableColumnMerged | TableColumnScrollBar)[], + endColumns: (TableColumnMerged | TableColumnScrollBar)[], + columnWidthsMap: Record, + columnCount: number, +) { + const startOffsets: Record = {} + const endOffsets: Record = {} let startOffset = 0 let endOffset = 0 - for (let start = 0; start < count; start++) { - // Start offset - startOffsets[start] = startOffset - startOffset += widths[start] || 0 + for (let index = 0; index < startColumns.length; index++) { + const column = startColumns[index] + const width = columnWidthsMap[column.key] ?? column.width ?? 0 + + startOffsets[column.key] = { index, offset: startOffset } + startOffset += width + } + + for (let index = 0; index < endColumns.length; index++) { + const column = endColumns[endColumns.length - index - 1] + const width = columnWidthsMap[column.key] ?? column.width ?? 0 - // End offset - const end = count - start - 1 - endOffsets[end] = endOffset - endOffset += widths[end] || 0 + endOffsets[column.key] = { index: columnCount - index - 1, offset: endOffset } + endOffset += width } return { @@ -365,6 +398,7 @@ function calculateOffsets(widths: number[]) { function mergeRows(mergedColumns: TableColumnMerged[], scrollBarColumn: TableColumnScrollBar | undefined) { const rows: TableColumnMergedExtra[][] = [] + const offsetIndexMap: Record = {} function calculateColSpans(columns: TableColumnMerged[], colIndex: number, rowIndex: number) { rows[rowIndex] ??= [] @@ -385,6 +419,7 @@ function mergeRows(mergedColumns: TableColumnMerged[], scrollBarColumn: TableCol const colEnd = colStart + titleColSpan - 1 rows[rowIndex].push({ ...column, titleColSpan, colStart, colEnd, hasChildren } as TableColumnMergedExtra) + offsetIndexMap[column.key] = { colStart, colEnd } colStart += titleColSpan @@ -408,5 +443,5 @@ function mergeRows(mergedColumns: TableColumnMerged[], scrollBarColumn: TableCol }) }) - return rows + return { rows, offsetIndexMap } } diff --git a/packages/components/table/src/composables/useScroll.ts b/packages/components/table/src/composables/useScroll.ts index 17b0c6cab..3cc95f7c2 100644 --- a/packages/components/table/src/composables/useScroll.ts +++ b/packages/components/table/src/composables/useScroll.ts @@ -28,7 +28,7 @@ export function useScroll( watch(virtualScrollRef, instance => { if (instance) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - scrollBodyRef.value = (instance as any).holderRef.value + scrollBodyRef.value = (instance as any).getHolderElement() } }) @@ -141,6 +141,7 @@ function useScrollRef( if (!target) { return } + if (target.scrollLeft !== scrollLeft) { target.scrollLeft = scrollLeft } diff --git a/packages/components/table/src/composables/useTableLayout.ts b/packages/components/table/src/composables/useTableLayout.ts index e1de0da3c..c9d316484 100644 --- a/packages/components/table/src/composables/useTableLayout.ts +++ b/packages/components/table/src/composables/useTableLayout.ts @@ -5,6 +5,8 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +import type { VirtualScrollEnabled } from '@idux/cdk/scroll' + import { type ComputedRef, computed } from 'vue' import { type ColumnsContext } from './useColumns' @@ -16,6 +18,7 @@ export function useTableLayout( { hasEllipsis, hasFixed }: ColumnsContext, { scrollWidth, scrollHeight }: ScrollContext, isSticky: ComputedRef, + mergedVirtual: ComputedRef, mergedAutoHeight: ComputedRef, ): ComputedRef<'auto' | 'fixed'> { return computed(() => { @@ -25,7 +28,14 @@ export function useTableLayout( if (scrollWidth.value && hasFixed.value) { return scrollWidth.value === 'max-content' ? 'auto' : 'fixed' } - if (scrollHeight.value || mergedAutoHeight.value || isSticky.value || hasEllipsis.value || props.virtual) { + if ( + scrollHeight.value || + mergedAutoHeight.value || + isSticky.value || + hasEllipsis.value || + mergedVirtual.value.horizontal || + mergedVirtual.value.vertical + ) { return 'fixed' } return 'auto' diff --git a/packages/components/table/src/main/ColGroup.tsx b/packages/components/table/src/main/ColGroup.tsx index 1b1cfb863..a4c074539 100644 --- a/packages/components/table/src/main/ColGroup.tsx +++ b/packages/components/table/src/main/ColGroup.tsx @@ -6,63 +6,59 @@ */ /* eslint-disable indent */ +import type { TableColumnMerged } from '../composables/useColumns' -import { defineComponent, inject, normalizeClass } from 'vue' +import { type PropType, computed, defineComponent, inject, normalizeClass } from 'vue' import { convertCssPixel } from '@idux/cdk/utils' import { TABLE_TOKEN } from '../token' export default defineComponent({ - props: { isFixedHolder: Boolean }, + props: { isFixedHolder: Boolean, columns: Array as PropType }, setup(props) { - const { - flattedColumns, - flattedColumnsWithScrollBar, - columnWidthsWithScrollBar, - mergedSelectableMenus, - mergedPrefixCls, - } = inject(TABLE_TOKEN)! + const { flattedColumns, flattedColumnsWithScrollBar, columnWidthMap, mergedSelectableMenus, mergedPrefixCls } = + inject(TABLE_TOKEN)! - return () => { - const { isFixedHolder } = props - const columns = isFixedHolder ? flattedColumnsWithScrollBar.value : flattedColumns.value + const resolvedColumns = computed(() => { + const { isFixedHolder, columns } = props + if (!columns) { + return isFixedHolder ? flattedColumnsWithScrollBar.value : flattedColumns.value + } + + return columns + }) + return () => { // 所有列的宽度都不存在且没有特殊列的时候,跳过渲染 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - if (columns.every(column => !column.width && !column.type)) { + if (resolvedColumns.value.every(column => !column.width && !column.type)) { return } const prefixCls = mergedPrefixCls.value - const children = columns.map((column, colIndex) => { + const children = resolvedColumns.value.map(column => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const { key, type } = column - const mergedWidth = isFixedHolder ? columnWidthsWithScrollBar.value[colIndex] : column.width + const mergedWidth = column.width ?? columnWidthMap.value[key] const className = type ? normalizeClass({ [`${prefixCls}-col-${type}`]: true, [`${prefixCls}-col-with-dropdown`]: type === 'selectable' && mergedSelectableMenus.value.length > 0, }) : undefined - let style: string | Record | undefined - if (isFixedHolder) { - style = mergedWidth ? `width: ${convertCssPixel(mergedWidth)}` : undefined - } else { - style = { - width: convertCssPixel(mergedWidth), - // for proTable: resizable, minWidth and maxWidth - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - minWidth: convertCssPixel(column.minWidth), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - maxWidth: convertCssPixel(column.maxWidth), - } + const style = { + width: convertCssPixel(mergedWidth), + // for proTable: resizable, minWidth and maxWidth + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + minWidth: convertCssPixel(column.minWidth), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + maxWidth: convertCssPixel(column.maxWidth), } - return }) diff --git a/packages/components/table/src/main/FixedHolder.tsx b/packages/components/table/src/main/FixedHolder.tsx index f3d574d8f..18b6b4146 100644 --- a/packages/components/table/src/main/FixedHolder.tsx +++ b/packages/components/table/src/main/FixedHolder.tsx @@ -5,21 +5,76 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { type CSSProperties, type Ref, computed, defineComponent, inject, onBeforeUnmount, onMounted } from 'vue' +import type { FlattedData } from '../composables/useDataSource' +import { + type CSSProperties, + type Ref, + computed, + defineComponent, + inject, + onBeforeUnmount, + onMounted, + ref, + watch, +} from 'vue' + +import { + CdkVirtualScroll, + type VirtualColRenderFn, + type VirtualContentRenderFn, + type VirtualRowRenderFn, + type VirtualScrollInstance, + type VirtualScrollRowData, +} from '@idux/cdk/scroll' import { convertCssPixel, off, on } from '@idux/cdk/utils' import ColGroup from './ColGroup' +import Head from './head/Head' +import { renderHeaderCell } from './head/RenderHeaderCells' +import { renderHeaderRow } from './head/RenderHeaderRow' +import { type TableColumnMergedExtra, flatColumns } from '../composables/useColumns' import { TABLE_TOKEN } from '../token' +import { modifyVirtualData } from '../utils' export default defineComponent({ props: { offsetTop: { type: Number, default: undefined }, offsetBottom: { type: Number, default: undefined }, }, - setup(props, { slots }) { - const { mergedPrefixCls, scrollHeadRef, handleScroll, scrollWidth, flattedData, isSticky, columnWidths } = - inject(TABLE_TOKEN)! + setup(props) { + const { + props: tableProps, + mergedPrefixCls, + mergedVirtual, + mergedVirtualColWidth, + scrollHeadRef, + handleScroll, + scrollWidth, + flattedData, + flattedColumns, + fixedColumns, + mergedRows, + isSticky, + columnWidths, + } = inject(TABLE_TOKEN)! + + const virtualScrollRef = ref() + + onMounted(() => { + watch( + virtualScrollRef, + virtualScroll => { + if (virtualScroll) { + scrollHeadRef.value = virtualScroll.getHolderElement() + scrollHeadRef.value.style.overflow = 'hidden' + } + }, + { + immediate: true, + }, + ) + }) useScrollEvents(scrollHeadRef, handleScroll) @@ -43,20 +98,103 @@ export default defineComponent({ }) const tableStyle = computed(() => { - const visibility = hasData.value && !columnWidths.value.length ? 'hidden' : undefined + const visibility = hasData.value && scrollWidth.value && !columnWidths.value.length ? 'hidden' : undefined return { tableLayout: 'fixed', visibility, } }) + const virtualData = computed[]>(() => { + if (!mergedVirtual.value.horizontal) { + return [] + } + + return mergedRows.value.rows.map((columns, rowIdx) => { + return { + key: rowIdx, + data: columns, + } + }) + }) + return () => { + const showColGroup = hasData.value || !isMaxContent.value + if (mergedVirtual.value.horizontal) { + const contentRender: VirtualContentRenderFn = (children, { renderedData }) => { + let flattedColumns: TableColumnMergedExtra[] = [] + + if (showColGroup) { + const columns: TableColumnMergedExtra[] = [] + ;(renderedData as VirtualScrollRowData[]).forEach(row => { + columns.push(...row.data) + }) + + flattedColumns = flatColumns(columns) + } + + return ( + + {showColGroup && } + {children} +
+ ) + } + + const rowRender: VirtualRowRenderFn> = ({ + item, + index, + children, + }) => renderHeaderRow(item.data, index, children) + const colRneder: VirtualColRenderFn> = ({ item }) => + renderHeaderCell(item) + + const colModifier = (renderedRow: FlattedData, renderedCols: TableColumnMergedExtra[]) => { + const { fixedStartColumns, fixedEndColumns } = fixedColumns.value + return modifyVirtualData( + renderedRow, + renderedCols, + flattedColumns.value, + flattedData.value, + fixedStartColumns, + fixedEndColumns, + true, + ) + } + + const { virtualBufferSize, virtualBufferOffset } = tableProps + + return ( +
+ { + + } +
+ ) + } + return (
- - {(hasData.value || !isMaxContent.value) && } - {slots.default && slots.default()} -
+ { + + {showColGroup && } + +
+ }
) } @@ -68,15 +206,36 @@ function useScrollEvents( handleScroll: (evt?: Event, scrollLeft?: number) => void, ) { const onWheel = (evt: WheelEvent) => { - const deltaX = evt.deltaX + if (!evt.shiftKey) { + return + } + + const delta = evt.deltaY const currentTarget = evt.currentTarget as HTMLDivElement - if (deltaX) { - const scrollLeft = currentTarget.scrollLeft + deltaX + if (delta) { + const scrollLeft = currentTarget.scrollLeft + delta handleScroll(evt, scrollLeft) - evt.preventDefault() } } - onMounted(() => on(scrollHeadRef.value, 'wheel', onWheel, { passive: true })) - onBeforeUnmount(() => off(scrollHeadRef.value, 'wheel', onWheel)) + const bindWheelEvt = (el: HTMLElement | undefined) => { + on(el, 'wheel', onWheel, { passive: true }) + } + const unbindWheelEvt = (el: HTMLElement | undefined) => { + off(el, 'wheel', onWheel) + } + + onMounted(() => { + watch( + scrollHeadRef, + (scrollHead, oldScrollHead) => { + unbindWheelEvt(oldScrollHead) + bindWheelEvt(scrollHead) + }, + { + immediate: true, + }, + ) + }) + onBeforeUnmount(() => unbindWheelEvt(scrollHeadRef.value)) } diff --git a/packages/components/table/src/main/MainTable.tsx b/packages/components/table/src/main/MainTable.tsx index e8da50c42..7df75b95c 100644 --- a/packages/components/table/src/main/MainTable.tsx +++ b/packages/components/table/src/main/MainTable.tsx @@ -5,8 +5,11 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +import type { TableColumnMerged } from '../composables/useColumns' + import { type CSSProperties, + type VNodeChild, computed, defineComponent, inject, @@ -21,18 +24,27 @@ import { import { debounce, isNumber } from 'lodash-es' import { offResize, onResize } from '@idux/cdk/resize' -import { CdkVirtualScroll, type VirtualContentRenderFn, type VirtualItemRenderFn } from '@idux/cdk/scroll' -import { Logger, type VKey, callEmit, convertElement, isVisibleElement } from '@idux/cdk/utils' +import { + CdkVirtualScroll, + type VirtualColRenderFn, + type VirtualContentRenderFn, + type VirtualRowRenderFn, + type VirtualScrollRowData, +} from '@idux/cdk/scroll' +import { Logger, type VKey, callEmit, convertArray, convertElement, isVisibleElement } from '@idux/cdk/utils' import ColGroup from './ColGroup' import FixedHolder from './FixedHolder' import StickyScroll from './StickyScroll' import Body from './body/Body' +import MeasureRow from './body/MeasureRow' +import { renderBodyCell, renderBodyCells } from './body/RenderBodyCells' import { renderBodyRow } from './body/RenderBodyRow' import Head from './head/Head' import Foot from './tfoot/Foot' import { type FlattedData } from '../composables/useDataSource' import { TABLE_TOKEN, tableBodyToken } from '../token' +import { modifyVirtualData } from '../utils' export default defineComponent({ setup() { @@ -42,11 +54,16 @@ export default defineComponent({ expandable, mergedPrefixCls, mergedInsetShadow, + mergedVirtual, mergedVirtualItemHeight, + mergedVirtualColWidth, mergedAutoHeight, columnWidths, changeColumnWidth, + clearColumnWidth, flattedData, + flattedColumns, + fixedColumns, isSticky, mergedSticky, virtualScrollRef, @@ -64,6 +81,10 @@ export default defineComponent({ const mainTableRef = ref() const mainTableWidth = ref(0) + const showMeasure = computed( + () => mergedAutoHeight.value || !!scrollWidth.value || isSticky.value || mergedVirtual.value.horizontal, + ) + const _changeColumnWidth = (key: VKey, width: number | false) => { if (isVisibleElement(mainTableRef.value)) { changeColumnWidth(key, width) @@ -89,6 +110,12 @@ export default defineComponent({ onMounted(() => { triggerScroll() + watch( + () => flattedColumns.value.length, + () => { + clearColumnWidth() + }, + ) watch([() => props.dataSource, scrollWidth], ([, width]) => { if (width) { triggerScroll() @@ -149,52 +176,132 @@ export default defineComponent({ ) } + const virtualData = computed[]>(() => { + if (!mergedVirtual.value.vertical && !mergedVirtual.value.horizontal) { + return [] + } + + return flattedData.value.map(data => { + return { + ...data, + data: flattedColumns.value, + } + }) + }) + + const renderMeasureRow = (columns: TableColumnMerged[] | undefined) => { + if (!showMeasure.value || !columns) { + return + } + + return + } + + const _renderBody = ( + columns: TableColumnMerged[] | undefined, + data: FlattedData[] | undefined, + children: VNodeChild | undefined, + ) => { + let contentNodes: VNodeChild + + if (children) { + contentNodes = children + } else { + const rows: VNodeChild[] = [] + data?.forEach((item, rowIndex) => { + const cells = renderBodyCells(columns ?? [], item, rowIndex) + rows.push( + ...convertArray(renderBodyRow(item, rowIndex, slots, expandable.value, mergedPrefixCls.value, cells)), + ) + }) + + contentNodes = rows + } + + return ( + + {renderMeasureRow(columns)} + {contentNodes} + + ) + } + + const renderBody = () => _renderBody(flattedColumns.value, flattedData.value) + const renderVirtualBody = (columns: TableColumnMerged[] | undefined, children: VNodeChild) => + _renderBody(columns, undefined, children) + return () => { const prefixCls = mergedPrefixCls.value const autoHeight = mergedAutoHeight.value + const virtual = mergedVirtual.value const children = slots.default ? slots.default() : [] - if (autoHeight || scrollHeight.value || isSticky.value) { + if (autoHeight || scrollHeight.value || isSticky.value || virtual.horizontal) { const { offsetTop } = mergedSticky.value if (!props.headless) { - children.push( - - - , - ) + children.push() } - if (props.virtual && (props.scroll || autoHeight)) { - const itemRender: VirtualItemRenderFn = ({ item, index }) => - renderBodyRow(item, index, slots, expandable.value, prefixCls) + if ((virtual.vertical && (props.scroll || autoHeight)) || (!virtual.vertical && virtual.horizontal)) { + const rowRender: VirtualRowRenderFn = ({ item, index, children }) => + renderBodyRow(item, index, slots, expandable.value, prefixCls, children) + const colRender: VirtualColRenderFn> = ({ + row, + item, + index, + rowIndex, + }) => renderBodyCell(item, row, rowIndex, index) + + const contentRender: VirtualContentRenderFn = (children, { renderedData }) => { + const columns = (renderedData[0] as VirtualScrollRowData | undefined)?.data - const contentRender: VirtualContentRenderFn = children => { return ( - - {children} + {columns ? : undefined} + {renderVirtualBody(columns, children)} {false && }
) } + const colModifier = (renderedRow: FlattedData, renderedCols: TableColumnMerged[]) => { + const { fixedStartColumns, fixedEndColumns } = fixedColumns.value + return modifyVirtualData( + renderedRow, + renderedCols, + flattedColumns.value, + flattedData.value, + fixedStartColumns, + fixedEndColumns, + false, + ) + } const { scroll, onScrolledBottom } = props - if (__DEV__ && !autoHeight && !isNumber(scroll?.height)) { - Logger.warn('components/table', '`scroll.height` must is a valid number when enable virtual scroll') + if (__DEV__ && virtual.vertical && !autoHeight && !isNumber(scroll?.height)) { + Logger.warn( + 'components/table', + '`scroll.height` must is a valid number when enable vertical virtual scroll', + ) } children.push( item.rowKey ?? item.key} height={mergedAutoHeight.value ? '100%' : (scroll?.height as number)} - itemHeight={mergedVirtualItemHeight.value} - itemRender={itemRender} + width={'100%'} + rowHeight={mergedVirtualItemHeight.value} + colWidth={mergedVirtualColWidth.value} + rowRender={rowRender} + colRender={colRender} contentRender={contentRender} - virtual + virtual={mergedVirtual.value} + bufferSize={props.virtualBufferSize} + bufferOffset={props.virtualBufferOffset} onScroll={handleScroll} onScrolledBottom={onScrolledBottom} onScrolledChange={handleScrolledChange} @@ -205,7 +312,7 @@ export default defineComponent({
- + {renderBody()} {false && }
, @@ -221,7 +328,7 @@ export default defineComponent({ {!props.headless && } - + {renderBody()} {false && }
, diff --git a/packages/components/table/src/main/body/Body.tsx b/packages/components/table/src/main/body/Body.tsx index a471996ea..a2ad28570 100644 --- a/packages/components/table/src/main/body/Body.tsx +++ b/packages/components/table/src/main/body/Body.tsx @@ -5,33 +5,16 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { VNodeChild, computed, defineComponent, inject } from 'vue' +import { VNodeChild, defineComponent, inject } from 'vue' -import { convertArray } from '@idux/cdk/utils' import { ɵEmpty } from '@idux/components/_private/empty' import BodyRowSingle from './BodyRowSingle' -import MeasureRow from './MeasureRow' -import { renderBodyRow } from './RenderBodyRow' import { TABLE_TOKEN } from '../../token' export default defineComponent({ setup(_, { slots }) { - const { - props, - slots: tableSlots, - mergedPrefixCls, - mergedAutoHeight, - flattedData, - expandable, - scrollWidth, - scrollHeight, - isSticky, - } = inject(TABLE_TOKEN)! - - const showMeasure = computed( - () => mergedAutoHeight.value || scrollWidth.value || scrollHeight.value || isSticky.value, - ) + const { props, slots: tableSlots, mergedPrefixCls, flattedData } = inject(TABLE_TOKEN)! return () => { const prefixCls = mergedPrefixCls.value @@ -41,13 +24,7 @@ export default defineComponent({ } const data = flattedData.value if (data.length > 0) { - if (slots.default) { - children.push(...slots.default()) - } else { - data.forEach((item, rowIndex) => { - children.push(...convertArray(renderBodyRow(item, rowIndex, tableSlots, expandable.value, prefixCls))) - }) - } + slots.default && children.push(...slots.default()) } else { children.push( @@ -56,12 +33,7 @@ export default defineComponent({ ) } - return ( - - {showMeasure.value && } - {children} - - ) + return {children} } }, }) diff --git a/packages/components/table/src/main/body/BodyCell.tsx b/packages/components/table/src/main/body/BodyCell.tsx index 560fa3232..58fb977b5 100644 --- a/packages/components/table/src/main/body/BodyCell.tsx +++ b/packages/components/table/src/main/body/BodyCell.tsx @@ -20,7 +20,7 @@ import { type TableColumnMergedExtra, type TableColumnMergedSelectable, } from '../../composables/useColumns' -import { TABLE_TOKEN } from '../../token' +import { TABLE_TOKEN, type TableBodyRowContext, tableBodyRowToken } from '../../token' import { type TableBodyCellProps, type TableColumnIndexable, @@ -51,8 +51,10 @@ export default defineComponent({ selectable, mergedPagination, } = inject(TABLE_TOKEN)! + const rowContext = inject(tableBodyRowToken)! + const rowProps = rowContext.props const activeSortOrderBy = computed(() => activeOrderByMap[props.column.key]) - const dataValue = useDataValue(props) + const dataValue = useDataValue(rowContext, props) const isFixStartLast = computed(() => fixedColumnKeys.value.lastStartKey === props.column.key) const isFixEndFirst = computed(() => fixedColumnKeys.value.firstEndKey === props.column.key) @@ -91,7 +93,7 @@ export default defineComponent({ } const { starts, ends } = columnOffsets.value const offsets = fixed === 'start' ? starts : ends - const fixedOffset = convertCssPixel(offsets[props.colIndex]) + const fixedOffset = convertCssPixel(offsets[props.column.key].offset) return { position: 'sticky', left: fixed === 'start' ? fixedOffset : undefined, @@ -107,19 +109,19 @@ export default defineComponent({ let title: string | undefined if (type === 'selectable') { - children = renderSelectableChildren(props, slots, selectable, config, mergedPagination) + children = renderSelectableChildren(rowContext, slots, selectable, config, mergedPagination) } else if (type === 'indexable') { - children = renderIndexableChildren(props, slots, column as TableColumnIndexable, mergedPagination) + children = renderIndexableChildren(rowContext, slots, column as TableColumnIndexable, mergedPagination) } else { const text = dataValue.value - children = renderChildren(props, slots, text) + children = renderChildren(rowContext, props, slots, text) title = getColTitle(mergedEllipsis.value, children, text) // emptyCell 仅支持普通列 if (!type && (isNil(children) || children === '')) { const emptyCellRender = slots.emptyCell || mergedEmptyCell.value children = isFunction(emptyCellRender) - ? emptyCellRender({ column, record: props.record, rowIndex: props.rowIndex }) + ? emptyCellRender({ column, record: rowProps.record, rowIndex: rowProps.rowIndex }) : emptyCellRender } } @@ -131,7 +133,7 @@ export default defineComponent({ const customAdditionalFn = tableProps.customAdditional?.bodyCell const customAdditional = customAdditionalFn - ? customAdditionalFn({ column, record: props.record, rowIndex: props.rowIndex }) + ? customAdditionalFn({ column, record: rowProps.record, rowIndex: rowProps.rowIndex }) : undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any const Tag = (tableProps.customTag?.bodyCell ?? 'td') as any @@ -139,7 +141,7 @@ export default defineComponent({ const contentNode = type === 'expandable' ? (
- {renderExpandableChildren(props, slots, expandable, isTreeData, mergedPrefixCls.value)} + {renderExpandableChildren(rowContext, slots, expandable, isTreeData, mergedPrefixCls.value)} {!isEmptyNode(children) && } {children}
@@ -148,7 +150,14 @@ export default defineComponent({ ) return ( - + {contentNode} ) @@ -156,9 +165,12 @@ export default defineComponent({ }, }) -function useDataValue(props: TableBodyCellProps) { +function useDataValue(context: TableBodyRowContext, props: TableBodyCellProps) { return computed(() => { - const { column, record } = props + const { + props: { record }, + } = context + const { column } = props const dataKeys = convertArray(column.dataKey) if (dataKeys.length <= 0) { return undefined @@ -177,22 +189,29 @@ function useDataValue(props: TableBodyCellProps) { }) } -function renderChildren(props: TableBodyCellProps, slots: Slots, value: string) { - const { record, rowIndex, column } = props +function renderChildren(context: TableBodyRowContext, props: TableBodyCellProps, slots: Slots, value: string) { + const { + props: { record, rowIndex }, + } = context + const { column } = props const { customCell } = column const cellRender = isString(customCell) ? slots[customCell] : customCell return cellRender ? cellRender({ value, record, rowIndex }) : value } function renderExpandableChildren( - props: TableBodyCellProps, + context: TableBodyRowContext, slots: Slots, expandable: ComputedRef, isTreeData: ComputedRef, prefixCls: string, ) { const { icon, customIcon, indent, showLine } = expandable.value! - const { record, expanded, level = 0, hasPrevSibling, hasNextSibling, showLineIndentIndexList } = props + const { + props: { record, expanded, level = 0, hasPrevSibling, hasNextSibling, showLineIndentIndexList }, + expandDisabled, + handleExpend, + } = context const hasParent = level > 0 const mergedShowLine = isTreeData.value && showLine && indent @@ -209,7 +228,7 @@ function renderExpandableChildren( const triggerCls = { [`${prefixCls}-expandable-trigger`]: true, [`${prefixCls}-expandable-trigger-show-line`]: mergedShowLine, - [`${prefixCls}-expandable-trigger-disabled`]: props.disabled, + [`${prefixCls}-expandable-trigger-disabled`]: expandDisabled.value, } const indents = [] @@ -234,7 +253,7 @@ function renderExpandableChildren( @@ -248,48 +267,64 @@ function renderExpandableChildren( } function renderSelectableChildren( - props: TableBodyCellProps, + rowContext: TableBodyRowContext, slots: Slots, selectable: ComputedRef, config: TableConfig, mergedPagination: ComputedRef, ) { - const { record, rowIndex, selected: checked, indeterminate, disabled, isHover, handleSelect: onChange } = props + const { + props: { record, rowIndex }, + isSelected, + isIndeterminate, + selectDisabled, + isHover, + handleSelect: onChange, + } = rowContext const { showIndex, multiple, customCell } = selectable.value! const onClick = (evt: Event) => { // see https://github.com/IDuxFE/idux/issues/547 evt.stopPropagation() // radio 支持反选 - if (!multiple && checked && !disabled && onChange) { + if (!multiple && isSelected.value && !selectDisabled.value && onChange) { onChange() } } - if (!checked && !isHover && showIndex) { - return renderIndexableChildren(props, slots, config.columnIndexable as TableColumnIndexable, mergedPagination) + if (!isSelected.value && !isHover && showIndex) { + return renderIndexableChildren(rowContext, slots, config.columnIndexable as TableColumnIndexable, mergedPagination) } const customRender = isString(customCell) ? slots[customCell] : customCell if (multiple) { - const checkboxProps = { checked, disabled, indeterminate, onChange, onClick } + const checkboxProps = { + checked: isSelected.value, + disabled: selectDisabled.value, + indeterminate: isIndeterminate.value, + onChange, + onClick, + } return customRender ? ( customRender({ ...checkboxProps, record, rowIndex }) ) : ( ) } else { - const radioProps = { checked, disabled, onChange, onClick } + const radioProps = { checked: isSelected.value, disabled: selectDisabled.value, onChange, onClick } return customRender ? customRender({ ...radioProps, record, rowIndex }) : } } function renderIndexableChildren( - props: TableBodyCellProps, + context: TableBodyRowContext, slots: Slots, indexable: TableColumnIndexable, mergedPagination: ComputedRef, ) { - const { record, rowIndex, disabled } = props + const { + props: { record, rowIndex }, + selectDisabled, + } = context const { customCell } = indexable const { pageIndex = 1, pageSize = 0 } = mergedPagination.value || {} const customRender = isString(customCell) ? slots[customCell] : customCell @@ -297,6 +332,6 @@ function renderIndexableChildren( __DEV__ && Logger.warn('components/table', 'invalid customCell, please check the column is correct') return undefined } - const style = disabled ? 'cursor: not-allowed' : 'cursor: pointer' + const style = selectDisabled.value ? 'cursor: not-allowed' : 'cursor: pointer' return {customRender({ record, rowIndex, pageIndex, pageSize })} } diff --git a/packages/components/table/src/main/body/BodyRow.tsx b/packages/components/table/src/main/body/BodyRow.tsx index 829e26ff1..430b7c4af 100644 --- a/packages/components/table/src/main/body/BodyRow.tsx +++ b/packages/components/table/src/main/body/BodyRow.tsx @@ -5,27 +5,21 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { type ComputedRef, Ref, type VNodeTypes, computed, defineComponent, inject, normalizeClass, ref } from 'vue' +import { type ComputedRef, computed, defineComponent, inject, normalizeClass, provide, ref } from 'vue' import { type VKey } from '@idux/cdk/utils' -import BodyCell from './BodyCell' -import { - type TableColumnMerged, - type TableColumnMergedExpandable, - type TableColumnMergedSelectable, -} from '../../composables/useColumns' +import { type TableColumnMergedExpandable, type TableColumnMergedSelectable } from '../../composables/useColumns' import { FlattedData } from '../../composables/useDataSource' -import { TABLE_TOKEN } from '../../token' +import { TABLE_TOKEN, tableBodyRowToken } from '../../token' import { type TableBodyRowProps, tableBodyRowProps } from '../../types' export default defineComponent({ props: tableBodyRowProps, - setup(props) { + setup(props, { slots }) { const { props: tableProps, mergedPrefixCls, - flattedColumns, expandable, handleExpandChange, checkExpandDisabled, @@ -61,6 +55,17 @@ export default defineComponent({ }) }) + provide(tableBodyRowToken, { + props, + expandDisabled, + handleExpend, + isSelected, + isIndeterminate, + selectDisabled, + handleSelect, + isHover, + }) + return () => { const customAdditionalFn = tableProps.customAdditional?.bodyRow const customAdditional = customAdditionalFn @@ -71,17 +76,7 @@ export default defineComponent({ return ( - {renderChildren( - props, - flattedColumns, - expandDisabled, - handleExpend, - isSelected, - isIndeterminate, - selectDisabled, - handleSelect, - isHover, - )} + {slots.default?.()} ) } @@ -156,54 +151,3 @@ function useEvents( return { expandDisabled, handleExpend, selectDisabled, handleSelect, isHover, attrs } } - -function renderChildren( - props: TableBodyRowProps, - flattedColumns: ComputedRef, - expandDisabled: ComputedRef, - handleExpend: () => void, - isSelected: ComputedRef, - isIndeterminate: ComputedRef, - selectDisabled: ComputedRef, - handleSelect: () => void, - isHover: Ref, -) { - const children: VNodeTypes[] = [] - const { rowIndex, record, level, hasNextSibling, hasPrevSibling, showLineIndentIndexList } = props - flattedColumns.value.forEach((column, colIndex) => { - const { type, colSpan: getColSpan, rowSpan: getRowSpan, key } = column - const colSpan = getColSpan?.(record, rowIndex) - const rowSpan = getRowSpan?.(record, rowIndex) - if (colSpan === 0 || rowSpan === 0) { - return - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const colProps: any = { - colSpan: colSpan === 1 ? undefined : colSpan, - rowSpan: rowSpan === 1 ? undefined : rowSpan, - rowIndex, - colIndex, - record, - column, - level, - hasNextSibling, - hasPrevSibling, - showLineIndentIndexList, - key, - } - if (type === 'expandable') { - colProps.expanded = props.expanded - colProps.disabled = expandDisabled.value - colProps.handleExpend = handleExpend - } else if (type === 'selectable') { - colProps.selected = isSelected.value - colProps.indeterminate = isIndeterminate.value - colProps.disabled = selectDisabled.value - colProps.handleSelect = handleSelect - colProps.isHover = isHover.value - } - children.push() - }) - - return children -} diff --git a/packages/components/table/src/main/body/MeasureCell.tsx b/packages/components/table/src/main/body/MeasureCell.tsx index 34e81d135..ce4e273a2 100644 --- a/packages/components/table/src/main/body/MeasureCell.tsx +++ b/packages/components/table/src/main/body/MeasureCell.tsx @@ -17,8 +17,10 @@ export default defineComponent({ const cellRef = ref() const handleResize = (evt: ResizeObserverEntry) => { - const { offsetWidth } = evt.target as HTMLTableCellElement - props.changeColumnWidth(props.cellKey, offsetWidth) + const { + contentRect: { width }, + } = evt + props.changeColumnWidth(props.cellKey, width) } useResizeObserver(cellRef, handleResize) diff --git a/packages/components/table/src/main/body/MeasureRow.tsx b/packages/components/table/src/main/body/MeasureRow.tsx index 67ac882fd..e2f6c8938 100644 --- a/packages/components/table/src/main/body/MeasureRow.tsx +++ b/packages/components/table/src/main/body/MeasureRow.tsx @@ -5,17 +5,20 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { defineComponent, inject } from 'vue' +import type { TableColumnMerged } from '../../composables/useColumns' + +import { type PropType, defineComponent, inject } from 'vue' import MeasureCell from './MeasureCell' import { TABLE_TOKEN, tableBodyToken } from '../../token' export default defineComponent({ - setup() { - const { mergedPrefixCls, flattedColumns } = inject(TABLE_TOKEN)! + props: { columns: Array as PropType }, + setup(props) { + const { mergedPrefixCls } = inject(TABLE_TOKEN)! const { changeColumnWidth } = inject(tableBodyToken)! return () => { - const children = flattedColumns.value.map(column => { + const children = props.columns?.map(column => { const { key } = column const cellProps = { key, cellKey: key, changeColumnWidth } return diff --git a/packages/components/table/src/main/body/RenderBodyCells.tsx b/packages/components/table/src/main/body/RenderBodyCells.tsx new file mode 100644 index 000000000..fa4098357 --- /dev/null +++ b/packages/components/table/src/main/body/RenderBodyCells.tsx @@ -0,0 +1,51 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import type { TableColumnMerged } from '../../composables/useColumns' +import type { VNode } from 'vue' + +import BodyCell from './BodyCell' + +export function renderBodyCells(columns: TableColumnMerged[], record: any, rowIndex: number): VNode[] { + const children: VNode[] = [] + columns.forEach((column, colIndex) => { + const cell = renderBodyCell(column, record, rowIndex, colIndex) + + if (cell) { + children.push(cell) + } + }) + + return children +} + +export function renderBodyCell( + column: TableColumnMerged, + record: any, + rowIndex: number, + colIndex: number, +): VNode | undefined { + const { colSpan: getColSpan, rowSpan: getRowSpan } = column + const colSpan = getColSpan?.(record, rowIndex) + const rowSpan = getRowSpan?.(record, rowIndex) + if (colSpan === 0 || rowSpan === 0) { + return + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const colProps: any = { + colSpan: colSpan === 1 ? undefined : colSpan, + rowSpan: rowSpan === 1 ? undefined : rowSpan, + colIndex, + column, + } + + return +} diff --git a/packages/components/table/src/main/body/RenderBodyRow.tsx b/packages/components/table/src/main/body/RenderBodyRow.tsx index e0e158f90..42f51b42e 100644 --- a/packages/components/table/src/main/body/RenderBodyRow.tsx +++ b/packages/components/table/src/main/body/RenderBodyRow.tsx @@ -21,6 +21,7 @@ export function renderBodyRow( slots: Slots, expandable: TableColumnMergedExpandable | undefined, prefixCls: string, + cols?: VNodeChild, ): VNodeChild { const { children, expanded, level, hasPrevSibling, hasNextSibling, showLineIndentIndexList, record, rowKey } = item const rowProps = { @@ -36,7 +37,7 @@ export function renderBodyRow( rowIndex, rowKey, } - const rowNode = + const rowNode = {cols} const expandedNode = expanded && renderExpandedContext(rowProps, slots, expandable, prefixCls) return expandedNode ? [rowNode, expandedNode] : rowNode diff --git a/packages/components/table/src/main/head/Head.tsx b/packages/components/table/src/main/head/Head.tsx index 9c23bca32..609e04688 100644 --- a/packages/components/table/src/main/head/Head.tsx +++ b/packages/components/table/src/main/head/Head.tsx @@ -5,26 +5,37 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { defineComponent, inject } from 'vue' +import { type VNodeChild, defineComponent, inject } from 'vue' -import HeadRow from './HeadRow' +import { renderHeaderCells } from './RenderHeaderCells' +import { renderHeaderRow } from './RenderHeaderRow' import { TABLE_TOKEN } from '../../token' export default defineComponent({ - setup() { + setup(_, { slots }) { const { props: tableProps, mergedRows, mergedPrefixCls } = inject(TABLE_TOKEN)! return () => { - const rows = mergedRows.value + let children: VNodeChild + + const { rows } = mergedRows.value const customAdditionalFn = tableProps.customAdditional?.head const customAdditional = customAdditionalFn ? customAdditionalFn({ rows }) : undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any const Tag = (tableProps.customTag?.head ?? 'thead') as any + + if (slots.default) { + children = slots.default() + } else { + children = rows.map((columns, rowIndex) => { + const cells = renderHeaderCells(columns) + return renderHeaderRow(columns, rowIndex, cells) + }) + } + return ( - {rows.map((columns, rowIndex) => ( - - ))} + {children} ) } diff --git a/packages/components/table/src/main/head/HeadCell.tsx b/packages/components/table/src/main/head/HeadCell.tsx index 60c270778..c00d1fa33 100644 --- a/packages/components/table/src/main/head/HeadCell.tsx +++ b/packages/components/table/src/main/head/HeadCell.tsx @@ -51,6 +51,7 @@ export default defineComponent({ handleSort, activeOrderByMap, activeFilterByMap, + mergedRows, handleFilter, } = inject(TABLE_TOKEN)! @@ -88,14 +89,16 @@ export default defineComponent({ }) const style = computed(() => { - const { fixed, colStart, colEnd } = props.column as HeadColumn + const { key, fixed } = props.column as HeadColumn + const { offsetIndexMap } = mergedRows.value if (!fixed) { return } const { starts, ends } = columnOffsetsWithScrollBar.value - const offsets = fixed === 'start' ? starts : ends - const offsetIndex = fixed === 'start' ? colStart : colEnd - const fixedOffset = convertCssPixel(offsets[offsetIndex]) + const offsets = Object.values(fixed === 'start' ? starts : ends) + const offsetIndex = offsetIndexMap[key][fixed === 'start' ? 'colStart' : 'colEnd'] + + const fixedOffset = convertCssPixel(offsets.find(offset => offset.index === offsetIndex)?.offset) return { position: 'sticky', left: fixed === 'start' ? fixedOffset : undefined, diff --git a/packages/components/table/src/main/head/HeadRow.tsx b/packages/components/table/src/main/head/HeadRow.tsx index 4517310e4..65ed4b648 100644 --- a/packages/components/table/src/main/head/HeadRow.tsx +++ b/packages/components/table/src/main/head/HeadRow.tsx @@ -7,13 +7,12 @@ import { defineComponent, inject } from 'vue' -import HeadCell from './HeadCell' import { TABLE_TOKEN } from '../../token' import { tableHeadRowProps } from '../../types' export default defineComponent({ props: tableHeadRowProps, - setup(props) { + setup(props, { slots }) { const { props: tableProps, mergedPrefixCls } = inject(TABLE_TOKEN)! return () => { @@ -24,11 +23,7 @@ export default defineComponent({ const Tag = (tableProps.customTag?.headRow ?? 'tr') as any return ( - {columns - .filter(column => column.titleColSpan !== 0) - .map(column => ( - - ))} + {slots.default?.()} ) } diff --git a/packages/components/table/src/main/head/RenderHeaderCells.tsx b/packages/components/table/src/main/head/RenderHeaderCells.tsx new file mode 100644 index 000000000..b2dba6985 --- /dev/null +++ b/packages/components/table/src/main/head/RenderHeaderCells.tsx @@ -0,0 +1,23 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { TableColumnMergedExtra } from '../../composables/useColumns' +import type { VNode, VNodeChild } from 'vue' + +import HeadCell from './HeadCell' + +export function renderHeaderCells(columns: TableColumnMergedExtra[]): VNodeChild { + return columns.map(column => renderHeaderCell(column)).filter(Boolean) +} + +export function renderHeaderCell(column: TableColumnMergedExtra): VNode | undefined { + if (column.titleColSpan === 0) { + return + } + + return +} diff --git a/packages/components/table/src/main/head/RenderHeaderRow.tsx b/packages/components/table/src/main/head/RenderHeaderRow.tsx new file mode 100644 index 000000000..6d6d1a086 --- /dev/null +++ b/packages/components/table/src/main/head/RenderHeaderRow.tsx @@ -0,0 +1,23 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { TableColumnMergedExtra } from '../../composables/useColumns' +import type { VNodeChild } from 'vue' + +import HeaderRow from './HeadRow' + +export function renderHeaderRow( + columns: TableColumnMergedExtra[], + index: number, + headerCells?: VNodeChild, +): VNodeChild { + return ( + + {headerCells} + + ) +} diff --git a/packages/components/table/src/token.ts b/packages/components/table/src/token.ts index 2dacd60bb..484db4edc 100644 --- a/packages/components/table/src/token.ts +++ b/packages/components/table/src/token.ts @@ -14,7 +14,8 @@ import type { ScrollContext } from './composables/useScroll' import type { SelectableContext } from './composables/useSelectable' import type { SortableContext } from './composables/useSortable' import type { StickyContext } from './composables/useSticky' -import type { TableEmptyCellOptions, TableProps } from './types' +import type { TableBodyRowProps, TableEmptyCellOptions, TableProps } from './types' +import type { VirtualScrollEnabled } from '@idux/cdk/scroll' import type { VKey } from '@idux/cdk/utils' import type { TableConfig } from '@idux/components/config' import type { Locale } from '@idux/components/locales' @@ -38,7 +39,9 @@ export interface TableContext mergedAutoHeight: ComputedRef mergedEmptyCell: ComputedRef VNodeChild) | undefined> mergedInsetShadow: ComputedRef + mergedVirtual: ComputedRef mergedVirtualItemHeight: ComputedRef + mergedVirtualColWidth: ComputedRef tableLayout: ComputedRef<'auto' | 'fixed'> } @@ -50,4 +53,16 @@ export interface TableBodyContext { changeColumnWidth: (key: VKey, width: number | false) => void } +export interface TableBodyRowContext { + props: TableBodyRowProps + expandDisabled: ComputedRef + handleExpend: () => void + isSelected: ComputedRef + isIndeterminate: ComputedRef + selectDisabled: ComputedRef + handleSelect: () => void + isHover: Ref +} + export const tableBodyToken: InjectionKey = Symbol('tableBodyToken') +export const tableBodyRowToken: InjectionKey = Symbol('tableBodyRowToken') diff --git a/packages/components/table/src/types.ts b/packages/components/table/src/types.ts index d256c6a39..ac6ae62ef 100644 --- a/packages/components/table/src/types.ts +++ b/packages/components/table/src/types.ts @@ -48,7 +48,11 @@ export const tableProps = { scrollToTopOnChange: { type: Boolean, default: undefined }, tableLayout: { type: String as PropType<'auto' | 'fixed'>, default: undefined }, virtual: { type: Boolean, default: false }, + virtualHorizontal: { type: Boolean, default: false }, virtualItemHeight: { type: Number, default: undefined }, + virtualColWidth: { type: Number, default: undefined }, + virtualBufferSize: { type: Number, default: undefined }, + virtualBufferOffset: { type: Number, default: undefined }, // events 'onUpdate:expandedRowKeys': [Function, Array] as PropType void>>, @@ -258,19 +262,8 @@ export type TableBodyRowProps = ExtractInnerPropTypes export const tableBodyCellProps = { column: { type: Object as PropType, required: true }, colIndex: { type: Number, required: true }, - level: { type: Number, default: undefined }, - record: { type: Object as PropType, required: true }, - rowIndex: { type: Number, required: true }, - disabled: { type: Boolean, default: undefined }, - expanded: { type: Boolean, default: undefined }, - hasPrevSibling: { type: Boolean, default: undefined }, - hasNextSibling: { type: Boolean, default: undefined }, - showLineIndentIndexList: { type: Array as PropType, default: () => [] }, - handleExpend: { type: Function as PropType<() => void>, default: undefined }, - isHover: { type: Boolean, default: undefined }, - selected: { type: Boolean, default: undefined }, - indeterminate: { type: Boolean, default: undefined }, - handleSelect: { type: Function as PropType<() => void>, default: undefined }, + colSpan: Number, + rowSpan: Number, } as const export type TableBodyCellProps = ExtractInnerPropTypes diff --git a/packages/components/table/src/utils/index.ts b/packages/components/table/src/utils/index.ts index 4e673ffd4..60e798c4c 100644 --- a/packages/components/table/src/utils/index.ts +++ b/packages/components/table/src/utils/index.ts @@ -7,12 +7,15 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +import type { TableColumnMerged, TableColumnScrollBar } from '../composables/useColumns' + import { Text, type VNodeChild } from 'vue' import { isNumber, isObject, isString } from 'lodash-es' import { Logger, type VKey, convertArray, flattenNode, uniqueId } from '@idux/cdk/utils' +import { type FlattedData } from '../composables/useDataSource' import { type TableColumn } from '../types' export function getColTitle( @@ -56,3 +59,55 @@ export function getColumnKey(column: TableColumn): VKey { return uniqueId('__IDUX_table_column_key_') } + +interface ModifiedData { + data: TableColumnMerged | TableColumnScrollBar + index: number + poolKey: string +} + +export function modifyVirtualData( + renderedRow: FlattedData, + renderedCols: TableColumnMerged[], + flattedColumns: TableColumnMerged[], + flattedData: FlattedData[], + fixedStartColumns: (TableColumnMerged | TableColumnScrollBar)[], + fixedEndColumns: (TableColumnMerged | TableColumnScrollBar)[], + includeScrollBar = false, +): + | { + start?: ModifiedData[] + end?: ModifiedData[] + } + | undefined { + const filterColumns = (columns: (TableColumnMerged | TableColumnScrollBar)[]) => + columns.filter(column => column.type !== 'scroll-bar') + + const startColumns = includeScrollBar ? fixedStartColumns : filterColumns(fixedStartColumns) + const endColumns = includeScrollBar ? fixedEndColumns : filterColumns(fixedEndColumns) + + if (!startColumns.length && !endColumns.length) { + return + } + + const rowIndex = flattedData.findIndex(data => data.rowKey === renderedRow.rowKey) + + const getAppendedColumn = (column: TableColumnMerged | TableColumnScrollBar) => { + if (renderedCols.findIndex(col => col.key === column.key) > -1) { + return + } + + const index = flattedColumns.findIndex(col => col.key === column.key) + + return { + data: column, + index, + poolKey: `fix-${rowIndex}-${index}`, + } + } + + return { + start: startColumns.map(getAppendedColumn).filter(Boolean) as ModifiedData[], + end: endColumns.map(getAppendedColumn).filter(Boolean) as ModifiedData[], + } +} diff --git a/packages/components/table/style/index.less b/packages/components/table/style/index.less index 863fc5665..f5699d0cd 100644 --- a/packages/components/table/style/index.less +++ b/packages/components/table/style/index.less @@ -41,10 +41,6 @@ &::after { right: 0; } - - .cdk-virtual-scroll-holder { - overflow-y: scroll; - } } table { @@ -190,6 +186,7 @@ flex-grow: 1; } + &-body-virtual-scroll, &-content { flex-basis: 0; height: 0; @@ -200,15 +197,10 @@ flex-shrink: 0; } } - - .cdk-virtual-scroll { - flex: 1 1 0; - } } &-empty&-auto-height, &-empty&-full-height { - .cdk-virtual-scroll-filler, .cdk-virtual-scroll-content, table { height: 100%; diff --git a/packages/pro/table/demo/MasiveColumns.md b/packages/pro/table/demo/MasiveColumns.md new file mode 100644 index 000000000..214ea44af --- /dev/null +++ b/packages/pro/table/demo/MasiveColumns.md @@ -0,0 +1,12 @@ +--- +order: 11 +title: + zh: 大量数据列 + en: Masive Columns +--- + +## zh + +可以通过开启横向虚拟滚动来支持大量数据列渲染的场景 + +## en diff --git a/packages/pro/table/demo/MasiveColumns.vue b/packages/pro/table/demo/MasiveColumns.vue new file mode 100644 index 000000000..2a82683e9 --- /dev/null +++ b/packages/pro/table/demo/MasiveColumns.vue @@ -0,0 +1,132 @@ + + + diff --git a/packages/pro/table/demo/Resizable.vue b/packages/pro/table/demo/Resizable.vue index bdc0b0e2c..42c6288ce 100644 --- a/packages/pro/table/demo/Resizable.vue +++ b/packages/pro/table/demo/Resizable.vue @@ -44,6 +44,7 @@ const columns: ProTableColumn[] = [ { type: 'indexable', changeVisible: false, + width: 60, }, { title: 'Name',