perf: 优化KtTable列宽拖拽性能

This commit is contained in:
sunlei 2026-05-19 22:09:24 +08:00
parent eb77dff6a6
commit 84b0b995d2
3 changed files with 142 additions and 75 deletions

View File

@ -37,14 +37,71 @@ export default defineComponent({
setup(props, { attrs, slots }) { setup(props, { attrs, slots }) {
const dragging = ref(false); const dragging = ref(false);
const stopNextClick = ref(false); const stopNextClick = ref(false);
let currentWidth = 0;
let guideFrame: number | undefined;
let guideX = 0;
let resizeGuideElement: HTMLDivElement | null = null;
let startWidth = 0; let startWidth = 0;
let startX = 0; let startX = 0;
/**
* resize guide DOM
*/
function cancelGuideFrame() {
if (!guideFrame) return;
window.cancelAnimationFrame(guideFrame);
guideFrame = undefined;
}
/**
* 线线
*
* @param headerCell
*/
function createResizeGuide(headerCell: HTMLElement | null) {
if (!headerCell) return;
const tableBody = headerCell.closest('.kt-table__body');
const rect = tableBody?.getBoundingClientRect();
if (!rect) return;
resizeGuideElement = document.createElement('div');
resizeGuideElement.className = 'kt-table__resize-guide';
resizeGuideElement.style.top = `${rect.top}px`;
resizeGuideElement.style.height = `${rect.height}px`;
document.body.append(resizeGuideElement);
}
/**
* 线
*/
function moveResizeGuide() {
if (guideFrame) return;
guideFrame = window.requestAnimationFrame(() => {
guideFrame = undefined;
if (resizeGuideElement) {
resizeGuideElement.style.transform = `translate3d(${guideX}px, 0, 0)`;
}
});
}
/**
* 线
*/
function removeResizeGuide() {
resizeGuideElement?.remove();
resizeGuideElement = null;
}
/** /**
* *
*/ */
function stopDragging() { function stopDragging() {
dragging.value = false; dragging.value = false;
removeResizeGuide();
document.body.classList.remove('kt-table--column-resizing'); document.body.classList.remove('kt-table--column-resizing');
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
@ -59,14 +116,21 @@ export default defineComponent({
if (!dragging.value) return; if (!dragging.value) return;
stopNextClick.value = true; stopNextClick.value = true;
const nextWidth = Math.max(startWidth + event.clientX - startX, 40); currentWidth = Math.max(startWidth + event.clientX - startX, 40);
props.onResize?.(event, { size: { width: nextWidth } }); guideX = startX + currentWidth - startWidth;
moveResizeGuide();
} }
/** /**
* *
*
* @param event
*/ */
function handleMouseUp() { function handleMouseUp(event: MouseEvent) {
cancelGuideFrame();
if (dragging.value && Math.abs(currentWidth - startWidth) >= 1) {
props.onResize?.(event, { size: { width: currentWidth } });
}
stopDragging(); stopDragging();
window.setTimeout(() => { window.setTimeout(() => {
stopNextClick.value = false; stopNextClick.value = false;
@ -89,6 +153,10 @@ export default defineComponent({
stopNextClick.value = false; stopNextClick.value = false;
startX = event.clientX; startX = event.clientX;
startWidth = props.width || headerCell?.offsetWidth || 0; startWidth = props.width || headerCell?.offsetWidth || 0;
currentWidth = startWidth;
guideX = startX;
createResizeGuide(headerCell);
moveResizeGuide();
document.body.classList.add('kt-table--column-resizing'); document.body.classList.add('kt-table--column-resizing');
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
@ -108,7 +176,10 @@ export default defineComponent({
stopNextClick.value = false; stopNextClick.value = false;
} }
onBeforeUnmount(stopDragging); onBeforeUnmount(() => {
cancelGuideFrame();
stopDragging();
});
return () => { return () => {
if (!props.width) { if (!props.width) {

View File

@ -44,10 +44,9 @@ export default defineComponent({
setup(props, { emit, slots }) { setup(props, { emit, slots }) {
const shellRef = ref<HTMLElement | null>(null); const shellRef = ref<HTMLElement | null>(null);
const contentRef = ref<HTMLElement | null>(null); const contentRef = ref<HTMLElement | null>(null);
const shellHeight = ref<number>(); const motionHeight = ref<string>();
const transitioning = ref(false); const transitioning = ref(false);
let initialized = false; let lastStableHeight = 0;
let resizeObserver: ResizeObserver | undefined;
let transitionTimer: number | undefined; let transitionTimer: number | undefined;
let animationFrame: number | undefined; let animationFrame: number | undefined;
const gridStyle = computed(() => { const gridStyle = computed(() => {
@ -80,64 +79,65 @@ export default defineComponent({
return shell ? Math.ceil(shell.getBoundingClientRect().height) : 0; return shell ? Math.ceil(shell.getBoundingClientRect().height) : 0;
} }
/**
* /
*/
function rememberStableHeight() {
lastStableHeight = readContentHeight() || readShellHeight();
}
/** /**
* *
*/ */
function clearTransitionTimer() { function clearTransitionTimer() {
if (transitionTimer) { if (!transitionTimer) return;
window.clearTimeout(transitionTimer); window.clearTimeout(transitionTimer);
transitionTimer = undefined; transitionTimer = undefined;
} }
}
/** /**
* requestAnimationFrame * requestAnimationFrame
*/ */
function clearAnimationFrame() { function clearAnimationFrame() {
if (animationFrame) { if (!animationFrame) return;
window.cancelAnimationFrame(animationFrame); window.cancelAnimationFrame(animationFrame);
animationFrame = undefined; animationFrame = undefined;
} }
}
/** /**
* *
*/ */
function endTransition() { function finishTransition() {
if (!transitioning.value && shellHeight.value === undefined) return; if (!transitioning.value && motionHeight.value === undefined) return;
clearTransitionTimer(); clearTransitionTimer();
clearAnimationFrame(); clearAnimationFrame();
rememberStableHeight();
transitioning.value = false; transitioning.value = false;
shellHeight.value = undefined; motionHeight.value = undefined;
emit('transitionEnd'); emit('transitionEnd');
} }
/** /**
* * auto
*/ */
function prepareTransition() { function prepareTransition() {
const shell = shellRef.value; const shell = shellRef.value;
if (!shell) return false; if (!shell) return false;
const currentHeight = readShellHeight();
if (!initialized) {
initialized = true;
return false;
}
clearAnimationFrame(); clearAnimationFrame();
clearTransitionTimer(); clearTransitionTimer();
motionHeight.value = `${lastStableHeight || readShellHeight()}px`;
transitioning.value = false; transitioning.value = true;
shellHeight.value = currentHeight;
emit('transitionStart'); emit('transitionStart');
return true; return true;
} }
/** /**
* *
*/ */
async function animateToNextHeight() { async function animateToNextHeight() {
if (!prepareTransition()) return; if (!prepareTransition()) return;
@ -146,65 +146,46 @@ export default defineComponent({
await nextTick(); await nextTick();
const shell = shellRef.value; const shell = shellRef.value;
const currentHeight = shellHeight.value ?? readShellHeight(); const currentHeight = Number.parseFloat(motionHeight.value || '0');
const targetHeight = readContentHeight(); const targetHeight = readContentHeight();
if (!shell || Math.abs(currentHeight - targetHeight) <= 1) { if (!shell || Math.abs(currentHeight - targetHeight) <= 1) {
endTransition(); finishTransition();
return; return;
} }
transitioning.value = true; // 强制浏览器提交起点高度后再写入终点高度,确保高度过渡真正触发。
await nextTick(); void shell.offsetHeight;
animationFrame = window.requestAnimationFrame(() => { animationFrame = window.requestAnimationFrame(() => {
animationFrame = undefined; animationFrame = undefined;
shellHeight.value = targetHeight; motionHeight.value = `${targetHeight}px`;
transitionTimer = window.setTimeout( transitionTimer = window.setTimeout(
endTransition, finishTransition,
SEARCH_TRANSITION_DURATION + 80, SEARCH_TRANSITION_DURATION + 80,
); );
}); });
} }
/**
*
*/
function syncContentHeight() {
if (
!initialized ||
transitioning.value ||
shellHeight.value !== undefined
) {
return;
}
shellHeight.value = undefined;
}
onMounted(() => {
if (contentRef.value) {
resizeObserver = new ResizeObserver(syncContentHeight);
resizeObserver.observe(contentRef.value);
}
initialized = true;
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
clearTransitionTimer();
clearAnimationFrame();
});
watch( watch(
() => props.collapsed, () => props.collapsed,
() => { () => {
void animateToNextHeight(); void animateToNextHeight();
}, },
{ {
flush: 'pre', flush: 'post',
}, },
); );
onMounted(() => {
nextTick(() => {
rememberStableHeight();
});
});
onBeforeUnmount(() => {
clearTransitionTimer();
clearAnimationFrame();
});
return () => return () =>
props.visible ? ( props.visible ? (
<div class="kt-table__search" style={gridStyle.value}> <div class="kt-table__search" style={gridStyle.value}>
@ -220,22 +201,21 @@ export default defineComponent({
event.currentTarget === event.target && event.currentTarget === event.target &&
event.propertyName === 'height' event.propertyName === 'height'
) { ) {
endTransition(); finishTransition();
} }
}} }}
ref={shellRef} ref={shellRef}
style={{ style={{
height: height: motionHeight.value,
shellHeight.value === undefined
? undefined
: `${shellHeight.value}px`,
}} }}
> >
<div class="kt-table__search-content-motion">
<div class="kt-table__search-content" ref={contentRef}> <div class="kt-table__search-content" ref={contentRef}>
<div class="kt-table__search-form">{slots.form?.()}</div> <div class="kt-table__search-form">{slots.form?.()}</div>
<div class="kt-table__search-actions">{slots.actions?.()}</div> <div class="kt-table__search-actions">{slots.actions?.()}</div>
</div> </div>
</div> </div>
</div>
<div class="kt-table__search-split" /> <div class="kt-table__search-split" />
</div> </div>
) : null; ) : null;

View File

@ -52,6 +52,11 @@
will-change: height; will-change: height;
} }
&__search-content-motion {
min-height: 0;
overflow: hidden;
}
&__search-content { &__search-content {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -519,6 +524,17 @@
user-select: none !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 { .kt-table__row-action-popover-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;