feat: 完善KtTable行高调整能力

This commit is contained in:
sunlei 2026-05-21 23:08:26 +08:00
parent 1c342d3ab3
commit 14880dfcf1
7 changed files with 357 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import type {
import {
computed,
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
ref,
@ -68,6 +69,15 @@ type LoadOptions = {
validateForm?: boolean;
};
type RowResizeState = {
frame?: number;
key: string;
nextHeight: number;
rowElement: HTMLTableRowElement;
startHeight: number;
startY: number;
};
export default defineComponent({
name: 'KtTable',
props: ktTableProps,
@ -100,6 +110,9 @@ export default defineComponent({
const tableSize = ref<KtTableSize>(props.size);
const mounted = ref(false);
const autoLoaded = ref(false);
const rowHeights = reactive<Record<string, number>>({});
let rowResizeGuideElement: HTMLDivElement | null = null;
let rowResizeState: null | RowResizeState = null;
const {
formApi,
@ -255,6 +268,231 @@ export default defineComponent({
return resolveRowIndex(Math.max(rowIndex, 0));
}
/**
* resize
*
* @param record
*/
function resolveRecordKey(record: KtTableRecord) {
const { rowKey } = props;
if (typeof rowKey === 'function') {
return rowKey(record);
}
return record[rowKey] ?? record.key ?? rows.value.indexOf(record);
}
/**
*
*
* @param height
*/
function clampRowHeight(height: number) {
const minHeight = Math.max(24, props.rowResizeMinHeight);
const maxHeight = Math.max(minHeight, props.rowResizeMaxHeight);
return Math.min(maxHeight, Math.max(minHeight, Math.round(height)));
}
/**
* 线线 DOM
*
* @param rowElement
*/
function createRowResizeGuide(rowElement: HTMLTableRowElement) {
const tableBody = rowElement.closest('.kt-table__body');
const bodyRect = tableBody?.getBoundingClientRect();
if (!bodyRect) return;
rowResizeGuideElement = document.createElement('div');
rowResizeGuideElement.className = 'kt-table__row-resize-guide';
rowResizeGuideElement.style.left = `${bodyRect.left}px`;
rowResizeGuideElement.style.width = `${bodyRect.width}px`;
document.body.append(rowResizeGuideElement);
}
/**
* 线
*/
function moveRowResizeGuide() {
const state = rowResizeState;
if (!state || !rowResizeGuideElement) return;
const rowRect = state.rowElement.getBoundingClientRect();
rowResizeGuideElement.style.transform = `translate3d(0, ${Math.round(
rowRect.top + state.nextHeight,
)}px, 0)`;
}
/**
* 线
*/
function removeRowResizeGuide() {
rowResizeGuideElement?.remove();
rowResizeGuideElement = null;
}
/**
*
*
* @param event
* @param rowElement
*/
function isRowResizeHandleHit(
event: MouseEvent,
rowElement: HTMLTableRowElement,
) {
const indexCell = rowElement.querySelector(
'.kt-table__index-column',
) as HTMLElement | null;
if (!indexCell) return false;
const cellRect = indexCell.getBoundingClientRect();
const rowRect = rowElement.getBoundingClientRect();
const inIndexCell =
event.clientX >= cellRect.left && event.clientX <= cellRect.right;
const inBottomHandle =
event.clientY >= rowRect.bottom - 8 && event.clientY <= rowRect.bottom;
return inIndexCell && inBottomHandle;
}
/**
* tr mouseup
*
*/
function applyDraggingRowHeight() {
const state = rowResizeState;
if (!state) return;
state.frame = undefined;
state.rowElement.style.height = `${state.nextHeight}px`;
state.rowElement.style.setProperty(
'--kt-table-row-height',
`${state.nextHeight}px`,
);
moveRowResizeGuide();
}
/**
*
*
* @param event
*/
function handleRowResizeMove(event: MouseEvent) {
const state = rowResizeState;
if (!state) return;
state.nextHeight = clampRowHeight(
state.startHeight + event.clientY - state.startY,
);
if (state.frame) return;
state.frame = window.requestAnimationFrame(applyDraggingRowHeight);
}
/**
*
*/
function stopRowResize() {
const state = rowResizeState;
if (!state) return;
if (state.frame) {
window.cancelAnimationFrame(state.frame);
state.frame = undefined;
}
applyDraggingRowHeight();
rowHeights[state.key] = state.nextHeight;
removeRowResizeGuide();
rowResizeState = null;
document.removeEventListener('mousemove', handleRowResizeMove);
document.removeEventListener('mouseup', stopRowResize);
document.body.classList.remove('kt-table--row-resizing');
}
/**
*
*
* @param event
* @param record
*/
function startRowResize(event: MouseEvent, record: KtTableRecord) {
if (!props.rowResizable) return;
event.preventDefault();
event.stopPropagation();
const rowElement = (event.currentTarget as HTMLElement).closest(
'tr',
) as HTMLTableRowElement | null;
if (!rowElement) return;
const key = String(resolveRecordKey(record));
const currentHeight =
rowHeights[key] || rowElement.getBoundingClientRect().height;
const startHeight = clampRowHeight(currentHeight);
rowResizeState = {
key,
nextHeight: startHeight,
rowElement,
startHeight,
startY: event.clientY,
};
createRowResizeGuide(rowElement);
applyDraggingRowHeight();
document.body.classList.add('kt-table--row-resizing');
document.addEventListener('mousemove', handleRowResizeMove);
document.addEventListener('mouseup', stopRowResize);
}
/**
*
*
* @param event
* @param record
*/
function handleRowResizeMouseDown(
event: MouseEvent,
record: KtTableRecord,
) {
if (!props.rowResizable) return;
const rowElement = (event.currentTarget as HTMLElement).closest(
'tr',
) as HTMLTableRowElement | null;
if (!rowElement || !isRowResizeHandleHit(event, rowElement)) return;
startRowResize(event, record);
}
/**
* class CSS
*
* @param record
*/
function resolveRowProps(record: KtTableRecord) {
if (!props.rowResizable) return {};
const height = rowHeights[String(resolveRecordKey(record))];
return {
class: 'kt-table__row--resizable',
onMousedown: (event: MouseEvent) => {
handleRowResizeMouseDown(event, record);
},
style: height
? {
'--kt-table-row-height': `${height}px`,
height: `${height}px`,
}
: undefined,
};
}
/**
*
*
@ -585,6 +823,10 @@ export default defineComponent({
autoLoadData();
});
onBeforeUnmount(() => {
stopRowResize();
});
watch(api, () => {
if (mounted.value) {
autoLoadData();
@ -635,6 +877,7 @@ export default defineComponent({
dataSource={rows.value}
loading={loading.value}
onChange={handleTableChange}
onRow={resolveRowProps}
pagination={false}
rowKey={props.rowKey}
rowSelection={rowSelection.value}
@ -643,7 +886,23 @@ export default defineComponent({
v-slots={{
bodyCell: ({ column, index, record }: any): VNodeChild => {
if (column.key === KT_TABLE_INDEX_COLUMN_KEY) {
return resolveRecordIndex(record, index);
const rowIndex = resolveRecordIndex(record, index);
if (!props.rowResizable) return rowIndex;
return (
<div class="kt-table__index-cell">
<span>{rowIndex}</span>
<span
aria-label="调整行高"
class="kt-table__row-resize-handle"
onMousedown={(event: MouseEvent) => {
startRowResize(event, record);
}}
role="separator"
/>
</div>
);
}
if (column.key === KT_TABLE_ACTION_COLUMN_KEY) {

View File

@ -8,6 +8,10 @@ export const KT_TABLE_INDEX_COLUMN_KEY = '__kt_table_index__';
export const KT_TABLE_INDEX_COLUMN_WIDTH = 40;
export const KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT = 140;
export const KT_TABLE_DEFAULT_ROW_RESIZE_MIN_HEIGHT = 40;
export const KT_TABLE_ROW_ACTION_OVERFLOW_LIMIT = 3;
export const KT_TABLE_ROW_ACTION_VISIBLE_COUNT = 2;

View File

@ -19,6 +19,8 @@ import type {
import {
KT_TABLE_DEFAULT_PAGE_SIZE,
KT_TABLE_DEFAULT_PAGE_SIZE_OPTIONS,
KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT,
KT_TABLE_DEFAULT_ROW_RESIZE_MIN_HEIGHT,
} from './constants';
export const DEFAULT_TABLE_SETTING: Required<KtTableSetting> = {
@ -43,6 +45,9 @@ export const KT_TABLE_PROP_KEYS = [
'pageSize',
'pageSizeOptions',
'rowActions',
'rowResizeMaxHeight',
'rowResizeMinHeight',
'rowResizable',
'rowKey',
'showDefaultButtons',
'showFooter',
@ -78,6 +83,9 @@ export function createDefaultTableProps(): KtTableResolvedProps<
pageSize: KT_TABLE_DEFAULT_PAGE_SIZE,
pageSizeOptions: KT_TABLE_DEFAULT_PAGE_SIZE_OPTIONS,
rowActions: [],
rowResizeMaxHeight: KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT,
rowResizeMinHeight: KT_TABLE_DEFAULT_ROW_RESIZE_MIN_HEIGHT,
rowResizable: false,
rowKey: 'id',
showDefaultButtons: true,
showFooter: true,
@ -146,6 +154,18 @@ export const ktTableProps = {
default: () => [],
type: Array as PropType<KtTableRowAction[]>,
},
rowResizeMaxHeight: {
default: KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT,
type: Number,
},
rowResizeMinHeight: {
default: KT_TABLE_DEFAULT_ROW_RESIZE_MIN_HEIGHT,
type: Number,
},
rowResizable: {
default: false,
type: Boolean,
},
rowKey: {
default: 'id',
type: [String, Function] as PropType<

View File

@ -19,6 +19,33 @@
&__resizable-handle:hover {
background: hsl(var(--primary) / 18%);
}
&__row-resize-handle {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 4;
height: 8px;
cursor: row-resize;
user-select: none;
}
&__row-resize-handle::after {
position: absolute;
right: 6px;
bottom: 0;
left: 6px;
height: 1px;
content: '';
background: hsl(var(--primary));
opacity: 0;
transition: opacity 0.15s ease;
}
&__row-resize-handle:hover::after {
opacity: 1;
}
}
#{kt.$block}--column-resizing,
@ -27,6 +54,12 @@
user-select: none !important;
}
#{kt.$block}--row-resizing,
#{kt.$block}--row-resizing * {
cursor: row-resize !important;
user-select: none !important;
}
#{kt.$block}__resize-guide {
position: fixed;
left: 0;
@ -37,3 +70,15 @@
box-shadow: 0 0 0 1px hsl(var(--primary) / 18%);
will-change: transform;
}
#{kt.$block}__row-resize-guide {
position: fixed;
top: 0;
left: 0;
z-index: 3000;
height: 1px;
pointer-events: none;
background: hsl(var(--primary));
box-shadow: 0 0 0 1px hsl(var(--primary) / 18%);
will-change: transform;
}

View File

@ -131,6 +131,25 @@
white-space: nowrap;
border-bottom: 0 !important;
}
.ant-table-tbody > tr#{kt.$block}__row--resizable > td {
height: var(--kt-table-row-height);
}
.ant-table-tbody
> tr#{kt.$block}__row--resizable
> td#{kt.$block}__index-column {
position: sticky;
z-index: 16;
}
}
&__index-cell {
position: static;
display: flex;
align-items: center;
justify-content: center;
min-height: 24px;
}
&__index-column {

View File

@ -234,6 +234,9 @@ export interface KtTableProps<
pageSize?: number;
pageSizeOptions?: string[];
rowActions?: Array<KtTableRowAction<Row, SearchValues>>;
rowResizeMaxHeight?: number;
rowResizeMinHeight?: number;
rowResizable?: boolean;
rowKey?: ((row: Row) => string) | keyof Row | string;
showDefaultButtons?: boolean;
showFooter?: boolean;
@ -261,6 +264,9 @@ export type KtTableResolvedProps<
pageSizeOptions: string[];
rowActions: Array<KtTableRowAction<Row, SearchValues>>;
rowKey: ((row: Row) => string) | keyof Row | string;
rowResizable: boolean;
rowResizeMaxHeight: number;
rowResizeMinHeight: number;
showDefaultButtons: boolean;
showFooter: boolean;
showHeader: boolean;

View File

@ -551,6 +551,9 @@ export default defineComponent({
modules: [channelModule],
pageSize: 20,
rowActions,
rowResizable: true,
rowResizeMaxHeight: 300,
rowResizeMinHeight: 44,
showSelection: true,
statistics,
tableTitle: 'KT 组件发布清单 Demo',