mirror of
https://github.com/KwiTsukasa/kt-template-admin.git
synced 2026-05-27 16:35:47 +08:00
perf: 优化KtTable列宽拖拽性能
This commit is contained in:
parent
eb77dff6a6
commit
84b0b995d2
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
if (!transitionTimer) return;
|
||||
|
||||
window.clearTimeout(transitionTimer);
|
||||
transitionTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理搜索动画 requestAnimationFrame。
|
||||
*/
|
||||
function clearAnimationFrame() {
|
||||
if (animationFrame) {
|
||||
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,22 +201,21 @@ 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-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" />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user