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 }) {
const dragging = 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 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() {
dragging.value = false;
removeResizeGuide();
document.body.classList.remove('kt-table--column-resizing');
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
@ -59,14 +116,21 @@ export default defineComponent({
if (!dragging.value) return;
stopNextClick.value = true;
const nextWidth = Math.max(startWidth + event.clientX - startX, 40);
props.onResize?.(event, { size: { width: nextWidth } });
currentWidth = Math.max(startWidth + event.clientX - startX, 40);
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();
window.setTimeout(() => {
stopNextClick.value = false;
@ -89,6 +153,10 @@ export default defineComponent({
stopNextClick.value = false;
startX = event.clientX;
startWidth = props.width || headerCell?.offsetWidth || 0;
currentWidth = startWidth;
guideX = startX;
createResizeGuide(headerCell);
moveResizeGuide();
document.body.classList.add('kt-table--column-resizing');
document.addEventListener('mousemove', handleMouseMove);
@ -108,7 +176,10 @@ export default defineComponent({
stopNextClick.value = false;
}
onBeforeUnmount(stopDragging);
onBeforeUnmount(() => {
cancelGuideFrame();
stopDragging();
});
return () => {
if (!props.width) {

View File

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

View File

@ -52,6 +52,11 @@
will-change: height;
}
&__search-content-motion {
min-height: 0;
overflow: hidden;
}
&__search-content {
display: grid;
grid-template-columns: 1fr;
@ -519,6 +524,17 @@
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;