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 }) {
|
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) {
|
||||||
|
|||||||
@ -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);
|
|
||||||
transitionTimer = undefined;
|
window.clearTimeout(transitionTimer);
|
||||||
}
|
transitionTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理搜索动画 requestAnimationFrame。
|
* 清理搜索动画 requestAnimationFrame。
|
||||||
*/
|
*/
|
||||||
function clearAnimationFrame() {
|
function clearAnimationFrame() {
|
||||||
if (animationFrame) {
|
if (!animationFrame) return;
|
||||||
window.cancelAnimationFrame(animationFrame);
|
|
||||||
animationFrame = undefined;
|
window.cancelAnimationFrame(animationFrame);
|
||||||
}
|
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,20 +201,19 @@ 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" ref={contentRef}>
|
<div class="kt-table__search-content-motion">
|
||||||
<div class="kt-table__search-form">{slots.form?.()}</div>
|
<div class="kt-table__search-content" ref={contentRef}>
|
||||||
<div class="kt-table__search-actions">{slots.actions?.()}</div>
|
<div class="kt-table__search-form">{slots.form?.()}</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" />
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user