perf: 优化KtTable布局与滚动性能

This commit is contained in:
sunlei 2026-05-21 13:49:33 +08:00
parent dcb32e6037
commit 1c342d3ab3
17 changed files with 903 additions and 647 deletions

View File

@ -136,6 +136,7 @@ export default defineComponent({
scheduleTableLayout,
tableBodyRef,
tableScrollY,
tableViewportWidth,
} = useKtTableLayout({ hasSummary });
const context: KtTableContext = {
@ -187,6 +188,7 @@ export default defineComponent({
props,
rowActions,
scheduleTableLayout,
tableViewportWidth,
});
watch(
@ -451,7 +453,7 @@ export default defineComponent({
);
return (
<ASpace class="kt-table__row-actions" size={1}>
<ASpace class="kt-table__row-actions" size={0}>
{inlineActions.map((action) => renderRowAction(action, record))}
{overflowActions.length > 0 ? (
<APopover
@ -555,6 +557,27 @@ export default defineComponent({
);
}
/**
*
* Vue/Antdv deep watch
*/
function createLayoutWatchKey() {
return columns.value
.map((column) =>
[
column.key,
Array.isArray(column.dataIndex)
? column.dataIndex.join('.')
: column.dataIndex,
column.width,
column.fixed,
]
.map((value) => String(value ?? ''))
.join(':'),
)
.join('|');
}
expose(registerApi);
onMounted(() => {
@ -569,13 +592,17 @@ export default defineComponent({
});
watch(
[columns, rows, searchVisible, fullscreen, tableSize],
() => [
createLayoutWatchKey(),
rows.value.length,
searchVisible.value,
fullscreen.value,
tableSize.value,
hasSummary.value,
],
() => {
scheduleTableLayout();
},
{
deep: true,
},
);
return () => (

View File

@ -186,6 +186,20 @@ export default defineComponent({
return h('th', attrs, slots.default?.());
}
if (!props.onResize) {
return h(
'th',
{
...attrs,
style: {
...(attrs.style as Record<string, unknown> | undefined),
width: `${props.width}px`,
},
},
slots.default?.(),
);
}
return h(
'th',
{

View File

@ -2,8 +2,12 @@ import type { KtTableFormGridOptions } from '../types';
export const KT_TABLE_ACTION_COLUMN_KEY = '__kt_table_actions__';
export const KT_TABLE_ACTION_COLUMN_WIDTH = 112;
export const KT_TABLE_INDEX_COLUMN_KEY = '__kt_table_index__';
export const KT_TABLE_INDEX_COLUMN_WIDTH = 40;
export const KT_TABLE_ROW_ACTION_OVERFLOW_LIMIT = 3;
export const KT_TABLE_ROW_ACTION_VISIBLE_COUNT = 2;

View File

@ -1,6 +1,6 @@
import type { TableColumnType } from 'antdv-next';
import type { ComputedRef } from 'vue';
import type { ComputedRef, Ref } from 'vue';
import type {
KtTableRecord,
@ -12,8 +12,9 @@ import { computed, reactive, ref, watch } from 'vue';
import {
KT_TABLE_ACTION_COLUMN_KEY,
KT_TABLE_ACTION_COLUMN_WIDTH,
KT_TABLE_INDEX_COLUMN_KEY,
KT_TABLE_ROW_ACTION_OVERFLOW_LIMIT,
KT_TABLE_INDEX_COLUMN_WIDTH,
} from '../config/constants';
import { getColumnKey } from '../utils/index';
@ -21,8 +22,18 @@ interface UseKtTableColumnsOptions {
props: KtTableResolvedProps;
rowActions: ComputedRef<KtTableRowAction[]>;
scheduleTableLayout: () => void;
tableViewportWidth: Ref<number>;
}
type ColumnResizeHandler = (
event: MouseEvent,
info: {
size: {
width: number;
};
},
) => void;
/**
* KtTable
*
@ -30,10 +41,12 @@ interface UseKtTableColumnsOptions {
* @param options.props
* @param options.rowActions
* @param options.scheduleTableLayout
* @param options.tableViewportWidth
*/
export function useKtTableColumns(options: UseKtTableColumnsOptions) {
const { props, rowActions, scheduleTableLayout } = options;
// 列系统集中处理可见列、拖拽宽度和横向滚动宽度,避免主组件继续堆列计算细节。
const { props, rowActions, scheduleTableLayout, tableViewportWidth } =
options;
// 列系统集中处理可见列、拖拽宽度和横向滚动启停,避免主组件继续堆列计算细节。
const columnWidths = reactive<Record<string, number>>({});
const columnOrderKeys = ref<string[]>([]);
const visibleColumnKeys = ref<string[]>([]);
@ -60,12 +73,54 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
.map((key) => columnMap.get(key))
.filter(Boolean) as Array<TableColumnType<KtTableRecord>>;
});
const visibleColumns = computed(() =>
orderedSourceColumns.value
.filter((column) =>
const visibleSourceColumns = computed(() =>
orderedSourceColumns.value.filter((column) =>
visibleColumnKeys.value.includes(getColumnKey(column)),
)
.map((column) => normalizeColumnWidth(column)),
),
);
const rawTableWidth = computed(() => {
const selectionWidth = props.showSelection ? 48 : 0;
const indexWidth = props.showIndex ? KT_TABLE_INDEX_COLUMN_WIDTH : 0;
const actionWidth =
rowActions.value.length > 0 ? KT_TABLE_ACTION_COLUMN_WIDTH : 0;
const businessWidth = visibleSourceColumns.value.reduce(
(total, column) => total + getColumnRenderWidth(column),
0,
);
return selectionWidth + indexWidth + businessWidth + actionWidth;
});
const tableRenderWidth = computed(() => {
const hasFlexibleColumns = visibleSourceColumns.value.length > 0;
if (!hasFlexibleColumns) {
return rawTableWidth.value;
}
return Math.max(rawTableWidth.value, tableViewportWidth.value, 720);
});
const tableScrollX = computed(() => {
const viewportWidth = tableViewportWidth.value;
if (viewportWidth <= 0) return rawTableWidth.value;
return rawTableWidth.value > viewportWidth + 1
? rawTableWidth.value
: undefined;
});
const surplusWidthMap = computed(() =>
createFlexibleSurplusMap(
visibleSourceColumns.value,
Math.max(0, tableRenderWidth.value - rawTableWidth.value),
),
);
const visibleColumns = computed(() =>
visibleSourceColumns.value.map((column) =>
normalizeColumnWidth(
column,
surplusWidthMap.value.get(getColumnKey(column)) || 0,
),
),
);
const indexColumn = computed<null | TableColumnType<KtTableRecord>>(() => {
if (!props.showIndex) return null;
@ -75,22 +130,21 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
align: 'center',
fixed: 'left',
key: KT_TABLE_INDEX_COLUMN_KEY,
minWidth: 40,
minWidth: KT_TABLE_INDEX_COLUMN_WIDTH,
title: '序号',
width: 48,
width: KT_TABLE_INDEX_COLUMN_WIDTH,
} as TableColumnType<KtTableRecord>);
});
const actionColumn = computed<null | TableColumnType<KtTableRecord>>(() => {
if (rowActions.value.length === 0) return null;
const actionColumnWidth = resolveActionColumnWidth(rowActions.value.length);
return normalizeColumnWidth({
className: 'kt-table__action-column',
fixed: 'right',
key: KT_TABLE_ACTION_COLUMN_KEY,
minWidth: actionColumnWidth,
minWidth: KT_TABLE_ACTION_COLUMN_WIDTH,
title: '操作',
width: actionColumnWidth,
width: KT_TABLE_ACTION_COLUMN_WIDTH,
} as TableColumnType<KtTableRecord>);
});
const columns = computed(
@ -99,16 +153,6 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
Boolean,
) as Array<TableColumnType<KtTableRecord>>,
);
const tableScrollX = computed(() =>
Math.max(
columns.value.reduce(
(total, column) => total + readColumnWidth(column.width, 140),
props.showSelection ? 48 : 0,
),
720,
),
);
watch(
sourceColumns,
(nextColumns) => {
@ -127,17 +171,6 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
},
);
/**
*
*/
function resetColumns() {
const sourceKeys = sourceColumns.value
.map((column) => getColumnKey(column))
.filter(Boolean);
columnOrderKeys.value = [...sourceKeys];
visibleColumnKeys.value = [...sourceKeys];
}
/**
* key
*
@ -168,6 +201,28 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
];
}
/**
*
*/
function resetColumnWidths() {
Object.keys(columnWidths).forEach((key) => {
Reflect.deleteProperty(columnWidths, key);
});
}
/**
*
*/
function resetColumns() {
const sourceKeys = sourceColumns.value
.map((column) => getColumnKey(column))
.filter(Boolean);
columnOrderKeys.value = [...sourceKeys];
visibleColumnKeys.value = [...sourceKeys];
resetColumnWidths();
scheduleTableLayout();
}
/**
*
*
@ -195,43 +250,124 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
function getColumnMinWidth(column: TableColumnType<KtTableRecord>) {
const minWidth = Number((column as any).minWidth || 96);
if (getColumnKey(column) === KT_TABLE_INDEX_COLUMN_KEY) {
return Number.isFinite(minWidth) ? Math.max(minWidth, 40) : 40;
return Number.isFinite(minWidth)
? Math.max(minWidth, KT_TABLE_INDEX_COLUMN_WIDTH)
: KT_TABLE_INDEX_COLUMN_WIDTH;
}
if (getColumnKey(column) === KT_TABLE_ACTION_COLUMN_KEY) {
return Number.isFinite(minWidth) ? Math.max(minWidth, 96) : 112;
return Number.isFinite(minWidth)
? Math.max(minWidth, KT_TABLE_ACTION_COLUMN_WIDTH)
: KT_TABLE_ACTION_COLUMN_WIDTH;
}
return Number.isFinite(minWidth) ? Math.max(minWidth, 80) : 96;
}
/**
*
*
*
* @param actionCount
* @param column
* @param extraWidth
*/
function resolveActionColumnWidth(actionCount: number) {
if (actionCount > KT_TABLE_ROW_ACTION_OVERFLOW_LIMIT) return 112;
if (actionCount === KT_TABLE_ROW_ACTION_OVERFLOW_LIMIT) return 112;
if (actionCount === 2) return 96;
function getColumnRenderWidth(
column: TableColumnType<KtTableRecord>,
extraWidth = 0,
) {
const key = getColumnKey(column);
const width =
key && columnWidths[key]
? columnWidths[key]
: readColumnWidth(column.width, 160);
const minWidth = getColumnMinWidth(column);
return 80;
return Math.max(width + extraWidth, minWidth);
}
/**
* Antdv
* scroll.x
*
* @param sourceColumns
* @param surplusWidth
*/
function createFlexibleSurplusMap(
sourceColumns: Array<TableColumnType<KtTableRecord>>,
surplusWidth: number,
) {
const map = new Map<string, number>();
if (surplusWidth <= 0) return map;
const entries = sourceColumns
.map((column) => ({
key: getColumnKey(column),
width: getColumnRenderWidth(column),
}))
.filter(
(entry): entry is { key: string; width: number } =>
!!entry.key && !isFixedSystemColumn(entry.key),
);
const totalWidth = entries.reduce((total, entry) => total + entry.width, 0);
if (totalWidth <= 0) return map;
entries.forEach((entry) => {
map.set(entry.key, (surplusWidth * entry.width) / totalWidth);
});
return map;
}
/**
* KtTable
*
* @param key key
*/
function isFixedSystemColumn(key?: string) {
return (
key === KT_TABLE_INDEX_COLUMN_KEY || key === KT_TABLE_ACTION_COLUMN_KEY
);
}
/**
*
*
* @param column
* @param originalResize resize
*/
function createColumnResizeHandler(
column: TableColumnType<KtTableRecord>,
originalResize?: ColumnResizeHandler,
) {
if (isFixedSystemColumn(getColumnKey(column))) return undefined;
return (event: MouseEvent, info: Parameters<ColumnResizeHandler>[1]) => {
originalResize?.(event, info);
resizeColumnWidth(column, info.size.width);
};
}
/**
* ellipsis
*
* @param column
* @param extraWidth
*/
function normalizeColumnWidth(column: TableColumnType<KtTableRecord>) {
function normalizeColumnWidth(
column: TableColumnType<KtTableRecord>,
extraWidth = 0,
) {
const key = getColumnKey(column);
const width =
key && columnWidths[key]
? columnWidths[key]
: readColumnWidth(column.width, 160);
const originalHeaderCell = column.onHeaderCell;
const originalCell = column.onCell;
const minWidth = getColumnMinWidth(column);
const nextWidth = Math.max(width, minWidth);
const nextWidth = getColumnRenderWidth(column, extraWidth);
const isSystemColumn = isFixedSystemColumn(key);
const fixedWidthStyle = isSystemColumn
? {
maxWidth: `${nextWidth}px`,
minWidth: `${nextWidth}px`,
width: `${nextWidth}px`,
}
: undefined;
return {
...column,
@ -244,24 +380,18 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
onHeaderCell: (targetColumn: TableColumnType<KtTableRecord>) => {
const originalProps = (originalHeaderCell?.(targetColumn) ||
{}) as Record<string, any>;
const resizeHandler = createColumnResizeHandler(
column,
originalProps.onResize,
);
return {
...originalProps,
/**
*
*
* @param event
* @param info
* @param info.size
* @param info.size.width
*/
onResize: (event: MouseEvent, info: { size: { width: number } }) => {
originalProps.onResize?.(event, info);
resizeColumnWidth(column, info.size.width);
},
onResize: resizeHandler,
style: {
...(originalProps.style as Record<string, unknown>),
minWidth: `${minWidth}px`,
...fixedWidthStyle,
},
width: nextWidth,
};
@ -283,6 +413,7 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
style: {
...(originalProps.style as Record<string, unknown>),
minWidth: `${minWidth}px`,
...fixedWidthStyle,
},
};
},

View File

@ -17,6 +17,7 @@ export function useKtTableLayout(options: UseKtTableLayoutOptions) {
// 搜索区动画期间冻结表格高度重算,等过渡结束后再同步一次,避免频繁重算导致动画卡顿。
const tableBodyRef = ref<HTMLElement | null>(null);
const tableScrollY = ref(260);
const tableViewportWidth = ref(0);
const searchTransitioning = ref(false);
let layoutFrame: number | undefined;
let resizeObserver: ResizeObserver | undefined;
@ -51,6 +52,8 @@ export function useKtTableLayout(options: UseKtTableLayoutOptions) {
const wrapper = tableBodyRef.value;
if (!wrapper) return;
tableViewportWidth.value = wrapper.clientWidth;
const header = wrapper.querySelector(
'.ant-table-header',
) as HTMLElement | null;
@ -148,5 +151,6 @@ export function useKtTableLayout(options: UseKtTableLayoutOptions) {
scheduleTableLayout,
tableBodyRef,
tableScrollY,
tableViewportWidth,
};
}

View File

@ -1,580 +1,9 @@
.kt-table {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
&--fullscreen {
position: fixed;
inset: 0;
z-index: 1000;
padding: 16px;
background: hsl(var(--background));
}
&__main {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
overflow: hidden;
background: hsl(var(--card));
}
&__main-content {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
padding: 16px;
}
&__search {
--kt-table-form-action-fr: 6fr;
--kt-table-form-action-min-width: 180px;
--kt-table-form-content-fr: 18fr;
--kt-table-form-content-span: 18;
--kt-table-form-item-span: 4;
--kt-table-form-tablet-columns: 2;
flex-shrink: 0;
padding: 12px 16px 0;
background: hsl(var(--card));
}
&__search-content-shell {
overflow: hidden;
}
&__search-content-shell--transitioning {
transition: height 0.22s cubic-bezier(0.22, 1, 0.36, 1);
will-change: height;
}
&__search-content-motion {
min-height: 0;
overflow: hidden;
}
&__search-content {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
&__search-form {
min-width: 0;
}
&__search-actions {
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
&__search-action-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-start;
width: 100%;
}
&__search-toggle {
padding-inline: 8px;
}
&__search-toggle-icon {
width: 16px;
height: 16px;
transition: transform 0.2s ease;
}
&__search-toggle-icon--expanded {
transform: rotate(180deg);
}
&__search-split {
height: 1px;
margin-top: 12px;
background: hsl(var(--border));
}
&__form-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
&__form-item {
min-width: 0;
padding-bottom: 0;
}
&__form-control {
width: 100%;
}
&__header {
display: flex;
flex-shrink: 0;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
&__header-align {
display: flex;
flex: 1 1 0;
gap: 12px;
align-items: center;
justify-content: flex-start;
min-width: 0;
}
&__header-title {
min-width: 0;
font-size: 16px;
font-weight: 500;
}
&__header-button {
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-start;
}
&__header-toolbar {
display: flex;
flex-shrink: 0;
align-items: center;
}
&__header-divider {
margin-inline: 8px;
}
&__body {
--kt-table-scroll-y: 260px;
position: relative;
flex: 1 1 0;
min-height: 0;
overflow: hidden;
border: 1px solid hsl(var(--border));
border-radius: 6px;
}
&__ant,
&__ant .ant-spin-nested-loading,
&__ant .ant-spin-container {
height: 100%;
min-height: 0;
}
&__ant {
.ant-table,
.ant-table-container,
.ant-table-content,
.ant-table-header,
.ant-table-body {
overscroll-behavior: contain;
}
.ant-table table {
table-layout: fixed !important;
}
.ant-spin-container,
.ant-table {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
}
.ant-table-container {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
}
.ant-table-header {
position: relative;
z-index: 5;
flex: 0 0 auto;
contain: paint;
background: hsl(var(--card));
}
.ant-table-body {
position: relative;
z-index: 1;
min-height: 0;
max-height: var(--kt-table-scroll-y) !important;
contain: paint;
overflow: auto !important;
scrollbar-gutter: stable;
will-change: scroll-position;
}
.ant-table-summary {
flex-shrink: 0;
margin-top: auto;
}
.ant-table-cell {
border-bottom-color: hsl(var(--border)) !important;
}
.ant-table-cell-fix-left,
.ant-table-cell-fix-right,
.ant-table-cell-fix-start,
.ant-table-cell-fix-end {
background: hsl(var(--card)) !important;
}
.ant-table-thead > tr > th.ant-table-cell-fix-left,
.ant-table-thead > tr > th.ant-table-cell-fix-right,
.ant-table-thead > tr > th.ant-table-cell-fix-start,
.ant-table-thead > tr > th.ant-table-cell-fix-end {
z-index: 6;
background: hsl(var(--card)) !important;
}
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-left,
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-right,
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-start,
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-end {
background: hsl(var(--accent)) !important;
}
.ant-table-summary .ant-table-cell-fix-left,
.ant-table-summary .ant-table-cell-fix-right,
.ant-table-summary .ant-table-cell-fix-start,
.ant-table-summary .ant-table-cell-fix-end {
background: hsl(var(--card)) !important;
}
// 固定列阴影会在横向滚动时随 Antdv shadow class 反复重绘KT 表格用边框区分固定列即可
.ant-table-cell-fix-start-shadow::after,
.ant-table-cell-fix-end-shadow::after,
.ant-table-cell-fix-left-last::after,
.ant-table-cell-fix-right-first::after {
box-shadow: none !important;
}
.ant-table-thead > tr > th {
position: relative;
top: 0;
z-index: 5;
background: hsl(var(--card)) !important;
}
tfoot.ant-table-summary .ant-table-cell,
tfoot.ant-table-summary > tr > td {
height: 44px;
padding: 0 16px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
line-height: 20px;
color: hsl(var(--foreground));
white-space: nowrap;
border-bottom: 0 !important;
}
}
&__body + &__footer {
margin-top: 10px;
}
&__footer {
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
gap: 12px;
align-items: center;
min-height: 32px;
}
&__footer-settings {
display: flex;
flex: 1 1 0;
gap: 12px;
align-items: center;
font-size: 14px;
}
&__footer-selection {
color: hsl(var(--muted-foreground));
}
&__footer-selection-count {
margin-inline: 4px;
color: hsl(var(--foreground));
}
&__summary-value {
display: inline-flex;
gap: 4px;
align-items: center;
}
&__summary-label {
color: hsl(var(--muted-foreground));
}
&__summary-title {
font-weight: 500;
}
&__row-actions {
white-space: nowrap;
.ant-btn {
min-width: auto;
padding-inline: 0;
}
}
&__action-column {
min-width: 112px;
}
&__row-action-more {
min-width: 18px;
padding-inline: 2px;
}
&__row-actions &__row-action-more {
min-width: 18px;
padding-inline: 2px;
}
&__row-action-more-icon {
font-size: 14px;
}
&__button-icon,
&__toolbar-icon {
width: 16px;
height: 16px;
}
&__toolbar-button {
width: 32px;
height: 32px;
}
&__settings-popover {
width: 240px;
}
&__settings-popover-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
&__settings-popover-title {
font-weight: 500;
}
&__settings-popover-list {
display: flex;
flex-direction: column;
max-height: 260px;
overflow-y: auto;
}
&__settings-column-item {
position: relative;
display: flex;
gap: 6px;
align-items: center;
min-height: 32px;
padding: 4px 6px;
border-radius: 4px;
transition:
background 0.16s ease,
opacity 0.16s ease;
}
&__settings-column-item:hover {
background: hsl(var(--accent));
}
&__settings-column-item::before,
&__settings-column-item::after {
position: absolute;
right: 6px;
left: 6px;
height: 2px;
content: '';
background: transparent;
border-radius: 999px;
}
&__settings-column-item::before {
top: 0;
}
&__settings-column-item::after {
bottom: 0;
}
&__settings-column-item--dragging {
opacity: 0.45;
}
&__settings-column-item--drop-before::before,
&__settings-column-item--drop-after::after {
background: hsl(var(--primary));
}
&__settings-column-drag {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 20px;
height: 24px;
padding: 0;
color: hsl(var(--muted-foreground));
cursor: grab;
background: transparent;
border: 0;
}
&__settings-column-drag:active {
cursor: grabbing;
}
&__settings-column-drag-icon {
width: 16px;
height: 16px;
}
&__settings-column-checkbox {
flex: 1 1 0;
min-width: 0;
}
&__resizable-title {
position: relative;
}
&__resizable-handle {
position: absolute;
top: 0;
right: -4px;
bottom: 0;
z-index: 4;
width: 8px;
cursor: col-resize;
user-select: none;
}
&__resizable-handle:hover {
background: hsl(var(--primary) / 18%);
}
}
@media (min-width: 1280px) {
.kt-table {
&__search-content {
grid-template-columns:
minmax(0, var(--kt-table-form-content-fr))
minmax(
var(--kt-table-form-action-min-width),
var(--kt-table-form-action-fr)
);
}
&__search-actions,
&__search-action-stack {
justify-content: flex-end;
}
&__form-grid {
grid-template-columns: repeat(
var(--kt-table-form-content-span),
minmax(0, 1fr)
);
}
&__form-item {
grid-column: span var(--kt-table-form-item-span) / span
var(--kt-table-form-item-span);
}
&__form-item--collapsed-visible {
display: flex !important;
}
}
}
@media (min-width: 768px) and (max-width: 1279px) {
.kt-table {
&__form-grid {
grid-template-columns: repeat(
var(--kt-table-form-tablet-columns),
minmax(0, 1fr)
);
}
&__form-item {
grid-column: span 1 / span 1;
}
&__form-item--range {
grid-column: 1 / -1;
}
}
}
.kt-table--column-resizing,
.kt-table--column-resizing * {
cursor: col-resize !important;
user-select: none !important;
}
.kt-table__resize-guide {
position: fixed;
left: 0;
z-index: 3000;
width: 1px;
pointer-events: none;
background: hsl(var(--primary));
box-shadow: 0 0 0 1px hsl(var(--primary) / 18%);
will-change: transform;
}
.kt-table__row-action-popover-content {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 72px;
.ant-btn {
justify-content: flex-start;
padding-inline: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.kt-table {
&__search-content-shell--transitioning {
transition: none;
}
}
}
@use './styles/layout';
@use './styles/search';
@use './styles/header';
@use './styles/table';
@use './styles/footer';
@use './styles/actions';
@use './styles/settings';
@use './styles/resizable';
@use './styles/responsive';

View File

@ -0,0 +1,52 @@
@use './tokens' as kt;
@include kt.block {
&__row-actions {
white-space: nowrap;
.ant-space-item {
display: inline-flex;
align-items: center;
}
.ant-space-item + .ant-space-item::before {
display: inline-block;
width: 1px;
height: 14px;
margin: 0 4px;
content: '';
background: hsl(var(--border));
}
.ant-btn {
min-width: auto;
padding-inline: 0;
}
}
&__row-action-more {
min-width: 18px;
padding-inline: 2px;
}
&__row-actions &__row-action-more {
min-width: 18px;
padding-inline: 2px;
}
&__row-action-more-icon {
font-size: 14px;
}
}
#{kt.$block}__row-action-popover-content {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 72px;
.ant-btn {
justify-content: flex-start;
padding-inline: 8px;
}
}

View File

@ -0,0 +1,47 @@
@use './tokens' as kt;
@include kt.block {
&__body + &__footer {
margin-top: 10px;
}
&__footer {
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
gap: 12px;
align-items: center;
min-height: 32px;
}
&__footer-settings {
display: flex;
flex: 1 1 0;
gap: 12px;
align-items: center;
font-size: 14px;
}
&__footer-selection {
color: hsl(var(--muted-foreground));
}
&__footer-selection-count {
margin-inline: 4px;
color: hsl(var(--foreground));
}
&__summary-value {
display: inline-flex;
gap: 4px;
align-items: center;
}
&__summary-label {
color: hsl(var(--muted-foreground));
}
&__summary-title {
font-weight: 500;
}
}

View File

@ -0,0 +1,57 @@
@use './tokens' as kt;
@include kt.block {
&__header {
display: flex;
flex-shrink: 0;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
&__header-align {
display: flex;
flex: 1 1 0;
gap: 12px;
align-items: center;
justify-content: flex-start;
min-width: 0;
}
&__header-title {
min-width: 0;
font-size: 16px;
font-weight: 500;
}
&__header-button {
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-start;
}
&__header-toolbar {
display: flex;
flex-shrink: 0;
align-items: center;
}
&__header-divider {
margin-inline: 8px;
}
&__button-icon,
&__toolbar-icon {
width: 16px;
height: 16px;
}
&__toolbar-button {
width: 32px;
height: 32px;
}
}

View File

@ -0,0 +1,34 @@
@use './tokens' as kt;
@include kt.block {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
&--fullscreen {
position: fixed;
inset: 0;
z-index: 1000;
padding: 16px;
background: hsl(var(--background));
}
&__main {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
overflow: hidden;
background: hsl(var(--card));
}
&__main-content {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
padding: 16px;
}
}

View File

@ -0,0 +1,39 @@
@use './tokens' as kt;
@include kt.block {
&__resizable-title {
position: relative;
}
&__resizable-handle {
position: absolute;
top: 0;
right: -4px;
bottom: 0;
z-index: 4;
width: 8px;
cursor: col-resize;
user-select: none;
}
&__resizable-handle:hover {
background: hsl(var(--primary) / 18%);
}
}
#{kt.$block}--column-resizing,
#{kt.$block}--column-resizing * {
cursor: col-resize !important;
user-select: none !important;
}
#{kt.$block}__resize-guide {
position: fixed;
left: 0;
z-index: 3000;
width: 1px;
pointer-events: none;
background: hsl(var(--primary));
box-shadow: 0 0 0 1px hsl(var(--primary) / 18%);
will-change: transform;
}

View File

@ -0,0 +1,62 @@
@use './tokens' as kt;
@media (min-width: 1280px) {
@include kt.block {
&__search-content {
grid-template-columns:
minmax(0, var(--kt-table-form-content-fr))
minmax(
var(--kt-table-form-action-min-width),
var(--kt-table-form-action-fr)
);
}
&__search-actions,
&__search-action-stack {
justify-content: flex-end;
}
&__form-grid {
grid-template-columns: repeat(
var(--kt-table-form-content-span),
minmax(0, 1fr)
);
}
&__form-item {
grid-column: span var(--kt-table-form-item-span) / span
var(--kt-table-form-item-span);
}
&__form-item--collapsed-visible {
display: flex !important;
}
}
}
@media (min-width: 768px) and (max-width: 1279px) {
@include kt.block {
&__form-grid {
grid-template-columns: repeat(
var(--kt-table-form-tablet-columns),
minmax(0, 1fr)
);
}
&__form-item {
grid-column: span 1 / span 1;
}
&__form-item--range {
grid-column: 1 / -1;
}
}
}
@media (prefers-reduced-motion: reduce) {
@include kt.block {
&__search-content-shell--transitioning {
transition: none;
}
}
}

View File

@ -0,0 +1,90 @@
@use './tokens' as kt;
@include kt.block {
&__search {
--kt-table-form-action-fr: 6fr;
--kt-table-form-action-min-width: 180px;
--kt-table-form-content-fr: 18fr;
--kt-table-form-content-span: 18;
--kt-table-form-item-span: 4;
--kt-table-form-tablet-columns: 2;
flex-shrink: 0;
padding: 12px 16px 0;
background: hsl(var(--card));
}
&__search-content-shell {
overflow: hidden;
}
&__search-content-shell--transitioning {
transition: height 0.22s cubic-bezier(0.22, 1, 0.36, 1);
will-change: height;
}
&__search-content-motion {
min-height: 0;
overflow: hidden;
}
&__search-content {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
&__search-form {
min-width: 0;
}
&__search-actions {
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
&__search-action-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-start;
width: 100%;
}
&__search-toggle {
padding-inline: 8px;
}
&__search-toggle-icon {
width: 16px;
height: 16px;
transition: transform 0.2s ease;
}
&__search-toggle-icon--expanded {
transform: rotate(180deg);
}
&__search-split {
height: 1px;
margin-top: 12px;
background: hsl(var(--border));
}
&__form-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
&__form-item {
min-width: 0;
padding-bottom: 0;
}
&__form-control {
width: 100%;
}
}

View File

@ -0,0 +1,98 @@
@use './tokens' as kt;
@include kt.block {
&__settings-popover {
width: 240px;
}
&__settings-popover-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
&__settings-popover-title {
font-weight: 500;
}
&__settings-popover-list {
display: flex;
flex-direction: column;
max-height: 260px;
overflow-y: auto;
}
&__settings-column-item {
position: relative;
display: flex;
gap: 6px;
align-items: center;
min-height: 32px;
padding: 4px 6px;
border-radius: 4px;
transition:
background 0.16s ease,
opacity 0.16s ease;
}
&__settings-column-item:hover {
background: hsl(var(--accent));
}
&__settings-column-item::before,
&__settings-column-item::after {
position: absolute;
right: 6px;
left: 6px;
height: 2px;
content: '';
background: transparent;
border-radius: 999px;
}
&__settings-column-item::before {
top: 0;
}
&__settings-column-item::after {
bottom: 0;
}
&__settings-column-item--dragging {
opacity: 0.45;
}
&__settings-column-item--drop-before::before,
&__settings-column-item--drop-after::after {
background: hsl(var(--primary));
}
&__settings-column-drag {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 20px;
height: 24px;
padding: 0;
color: hsl(var(--muted-foreground));
cursor: grab;
background: transparent;
border: 0;
}
&__settings-column-drag:active {
cursor: grabbing;
}
&__settings-column-drag-icon {
width: 16px;
height: 16px;
}
&__settings-column-checkbox {
flex: 1 1 0;
min-width: 0;
}
}

View File

@ -0,0 +1,159 @@
@use './tokens' as kt;
@include kt.block {
&__body {
--kt-table-scroll-y: 260px;
position: relative;
flex: 1 1 0;
min-height: 0;
overflow: hidden;
border: 1px solid hsl(var(--border));
border-radius: 6px;
}
&__ant,
&__ant .ant-spin-nested-loading,
&__ant .ant-spin-container {
height: 100%;
min-height: 0;
}
&__ant {
.ant-table,
.ant-table-container,
.ant-table-content,
.ant-table-header,
.ant-table-body {
overscroll-behavior: contain;
}
.ant-table table {
table-layout: fixed !important;
}
.ant-spin-container,
.ant-table {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
}
.ant-table-container {
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
}
.ant-table-header {
position: relative;
z-index: 5;
flex: 0 0 auto;
contain: paint;
background: hsl(var(--card));
}
.ant-table-body {
position: relative;
z-index: 1;
min-height: 0;
max-height: var(--kt-table-scroll-y) !important;
contain: paint;
overflow: auto !important;
scrollbar-gutter: stable;
will-change: scroll-position;
}
.ant-table-summary {
flex-shrink: 0;
margin-top: auto;
}
.ant-table-cell {
border-bottom-color: hsl(var(--border)) !important;
}
.ant-table-cell-fix-left,
.ant-table-cell-fix-right,
.ant-table-cell-fix-start,
.ant-table-cell-fix-end {
background: hsl(var(--card)) !important;
}
.ant-table-thead > tr > th.ant-table-cell-fix-left,
.ant-table-thead > tr > th.ant-table-cell-fix-right,
.ant-table-thead > tr > th.ant-table-cell-fix-start,
.ant-table-thead > tr > th.ant-table-cell-fix-end {
z-index: 6;
background: hsl(var(--card)) !important;
}
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-left,
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-right,
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-start,
.ant-table-tbody > tr:hover > td.ant-table-cell-fix-end {
background: hsl(var(--accent)) !important;
}
.ant-table-summary .ant-table-cell-fix-left,
.ant-table-summary .ant-table-cell-fix-right,
.ant-table-summary .ant-table-cell-fix-start,
.ant-table-summary .ant-table-cell-fix-end {
background: hsl(var(--card)) !important;
}
// Fixed column shadows repaint while Antdv toggles shadow classes during horizontal scroll.
.ant-table-cell-fix-start-shadow::after,
.ant-table-cell-fix-end-shadow::after,
.ant-table-cell-fix-left-last::after,
.ant-table-cell-fix-right-first::after {
box-shadow: none !important;
}
.ant-table-thead > tr > th {
position: relative;
top: 0;
z-index: 5;
background: hsl(var(--card)) !important;
}
tfoot.ant-table-summary .ant-table-cell,
tfoot.ant-table-summary > tr > td {
height: 44px;
padding: 0 16px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
line-height: 20px;
color: hsl(var(--foreground));
white-space: nowrap;
border-bottom: 0 !important;
}
}
&__index-column {
width: 40px;
min-width: 40px;
max-width: 40px;
padding-inline: 4px !important;
overflow: visible !important;
text-overflow: clip !important;
text-align: center;
white-space: nowrap;
}
&__index-column .ant-table-cell-content,
&__index-column .ant-table-column-title {
min-width: 0;
overflow: visible;
text-overflow: clip;
}
&__action-column {
width: 112px;
min-width: 112px;
max-width: 112px;
}
}

View File

@ -0,0 +1,8 @@
$block: '.kt-table';
// Keep the block selector in one namespace so every partial shares the same BEM root.
@mixin block {
#{$block} {
@content;
}
}

View File

@ -9,6 +9,7 @@
flex-direction: column;
gap: 3px;
min-width: 0;
contain: paint;
}
&__component-title {