diff --git a/apps/web-antdv-next/src/components/ktTable/KtTable.tsx b/apps/web-antdv-next/src/components/ktTable/KtTable.tsx index 146dcca..06ee632 100644 --- a/apps/web-antdv-next/src/components/ktTable/KtTable.tsx +++ b/apps/web-antdv-next/src/components/ktTable/KtTable.tsx @@ -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 ( - + {inlineActions.map((action) => renderRowAction(action, record))} {overflowActions.length > 0 ? ( + [ + 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 () => ( diff --git a/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx b/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx index 1e7fd2c..89a562c 100644 --- a/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx +++ b/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx @@ -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 | undefined), + width: `${props.width}px`, + }, + }, + slots.default?.(), + ); + } + return h( 'th', { diff --git a/apps/web-antdv-next/src/components/ktTable/config/constants.ts b/apps/web-antdv-next/src/components/ktTable/config/constants.ts index b722d5f..e8a2d10 100644 --- a/apps/web-antdv-next/src/components/ktTable/config/constants.ts +++ b/apps/web-antdv-next/src/components/ktTable/config/constants.ts @@ -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; diff --git a/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableColumns.ts b/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableColumns.ts index 2120700..5bbe067 100644 --- a/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableColumns.ts +++ b/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableColumns.ts @@ -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; scheduleTableLayout: () => void; + tableViewportWidth: Ref; } +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>({}); const columnOrderKeys = ref([]); const visibleColumnKeys = ref([]); @@ -60,12 +73,54 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) { .map((key) => columnMap.get(key)) .filter(Boolean) as Array>; }); + const visibleSourceColumns = computed(() => + orderedSourceColumns.value.filter((column) => + visibleColumnKeys.value.includes(getColumnKey(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(() => - orderedSourceColumns.value - .filter((column) => - visibleColumnKeys.value.includes(getColumnKey(column)), - ) - .map((column) => normalizeColumnWidth(column)), + visibleSourceColumns.value.map((column) => + normalizeColumnWidth( + column, + surplusWidthMap.value.get(getColumnKey(column)) || 0, + ), + ), ); const indexColumn = computed>(() => { 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); }); const actionColumn = computed>(() => { 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); }); const columns = computed( @@ -99,16 +153,6 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) { Boolean, ) as Array>, ); - 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) { 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, + 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>, + surplusWidth: number, + ) { + const map = new Map(); + 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, + originalResize?: ColumnResizeHandler, + ) { + if (isFixedSystemColumn(getColumnKey(column))) return undefined; + + return (event: MouseEvent, info: Parameters[1]) => { + originalResize?.(event, info); + resizeColumnWidth(column, info.size.width); + }; } /** * 为列注入可拖拽列宽配置并补齐默认 ellipsis。 * * @param column 当前需要渲染的表格列配置。 + * @param extraWidth 宽屏下分配给当前业务列的额外宽度。 */ - function normalizeColumnWidth(column: TableColumnType) { + function normalizeColumnWidth( + column: TableColumnType, + 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) => { const originalProps = (originalHeaderCell?.(targetColumn) || {}) as Record; + 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), minWidth: `${minWidth}px`, + ...fixedWidthStyle, }, width: nextWidth, }; @@ -283,6 +413,7 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) { style: { ...(originalProps.style as Record), minWidth: `${minWidth}px`, + ...fixedWidthStyle, }, }; }, diff --git a/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableLayout.ts b/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableLayout.ts index 8a09f49..a24f36e 100644 --- a/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableLayout.ts +++ b/apps/web-antdv-next/src/components/ktTable/hooks/useKtTableLayout.ts @@ -17,6 +17,7 @@ export function useKtTableLayout(options: UseKtTableLayoutOptions) { // 搜索区动画期间冻结表格高度重算,等过渡结束后再同步一次,避免频繁重算导致动画卡顿。 const tableBodyRef = ref(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, }; } diff --git a/apps/web-antdv-next/src/components/ktTable/style.scss b/apps/web-antdv-next/src/components/ktTable/style.scss index 794ec7e..60fe0ff 100644 --- a/apps/web-antdv-next/src/components/ktTable/style.scss +++ b/apps/web-antdv-next/src/components/ktTable/style.scss @@ -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'; diff --git a/apps/web-antdv-next/src/components/ktTable/styles/actions.scss b/apps/web-antdv-next/src/components/ktTable/styles/actions.scss new file mode 100644 index 0000000..16fa462 --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/actions.scss @@ -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; + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/footer.scss b/apps/web-antdv-next/src/components/ktTable/styles/footer.scss new file mode 100644 index 0000000..b123705 --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/footer.scss @@ -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; + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/header.scss b/apps/web-antdv-next/src/components/ktTable/styles/header.scss new file mode 100644 index 0000000..59ee4ab --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/header.scss @@ -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; + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/layout.scss b/apps/web-antdv-next/src/components/ktTable/styles/layout.scss new file mode 100644 index 0000000..eafffdf --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/layout.scss @@ -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; + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/resizable.scss b/apps/web-antdv-next/src/components/ktTable/styles/resizable.scss new file mode 100644 index 0000000..8fd518c --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/resizable.scss @@ -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; +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/responsive.scss b/apps/web-antdv-next/src/components/ktTable/styles/responsive.scss new file mode 100644 index 0000000..96319a9 --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/responsive.scss @@ -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; + } + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/search.scss b/apps/web-antdv-next/src/components/ktTable/styles/search.scss new file mode 100644 index 0000000..adb96b6 --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/search.scss @@ -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%; + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/settings.scss b/apps/web-antdv-next/src/components/ktTable/styles/settings.scss new file mode 100644 index 0000000..95854a6 --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/settings.scss @@ -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; + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/table.scss b/apps/web-antdv-next/src/components/ktTable/styles/table.scss new file mode 100644 index 0000000..1533000 --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/table.scss @@ -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; + } +} diff --git a/apps/web-antdv-next/src/components/ktTable/styles/tokens.scss b/apps/web-antdv-next/src/components/ktTable/styles/tokens.scss new file mode 100644 index 0000000..7988763 --- /dev/null +++ b/apps/web-antdv-next/src/components/ktTable/styles/tokens.scss @@ -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; + } +} diff --git a/apps/web-antdv-next/src/views/system/ktTableDemo/style.scss b/apps/web-antdv-next/src/views/system/ktTableDemo/style.scss index 2e625d1..29613c0 100644 --- a/apps/web-antdv-next/src/views/system/ktTableDemo/style.scss +++ b/apps/web-antdv-next/src/views/system/ktTableDemo/style.scss @@ -9,6 +9,7 @@ flex-direction: column; gap: 3px; min-width: 0; + contain: paint; } &__component-title {