diff --git a/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx b/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx index 179a185..1e7fd2c 100644 --- a/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx +++ b/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx @@ -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) { diff --git a/apps/web-antdv-next/src/components/ktTable/components/KtTableSearch.tsx b/apps/web-antdv-next/src/components/ktTable/components/KtTableSearch.tsx index fe529d2..a18aa58 100644 --- a/apps/web-antdv-next/src/components/ktTable/components/KtTableSearch.tsx +++ b/apps/web-antdv-next/src/components/ktTable/components/KtTableSearch.tsx @@ -44,10 +44,9 @@ export default defineComponent({ setup(props, { emit, slots }) { const shellRef = ref(null); const contentRef = ref(null); - const shellHeight = ref(); + const motionHeight = ref(); 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 ? (