From 24f7813f588349630b8ab6a85ea51e221520ada8 Mon Sep 17 00:00:00 2001 From: AutumnNazi <104422820+AutumnNazi@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:26:03 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(datagrid):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=88=97=E5=AE=BD=E9=9A=8F=E6=8B=96=E5=8A=A8=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在动画帧内同步提交列宽,使表头与表体实时重排 - 合并高频鼠标事件并跳过重复像素宽度更新 - 增加拖动期间实时更新的交互回归测试 --- ...eDataGridColumnResize.interaction.test.tsx | 17 +++++ .../src/components/useDataGridColumnResize.ts | 65 +++++++++++++------ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/useDataGridColumnResize.interaction.test.tsx b/frontend/src/components/useDataGridColumnResize.interaction.test.tsx index 21af4ece..b701f1b2 100644 --- a/frontend/src/components/useDataGridColumnResize.interaction.test.tsx +++ b/frontend/src/components/useDataGridColumnResize.interaction.test.tsx @@ -88,6 +88,12 @@ describe('useDataGridColumnResize interaction cleanup', () => { expect(update({ name: 120 })).toEqual({ name: width }); }; + const flushAnimationFrames = () => { + const callbacks = [...scheduledFrames.values()]; + scheduledFrames.clear(); + callbacks.forEach((callback) => callback(0)); + }; + beforeEach(() => { vi.useFakeTimers(); scheduledFrames = new Map(); @@ -164,6 +170,17 @@ describe('useDataGridColumnResize interaction cleanup', () => { expect(resize?.isResizingRef.current).toBe(false); }); + it('updates the complete table width on the animation frame while dragging', () => { + beginResize(); + act(() => fakeDocument.dispatch('mousemove', { buttons: 1, clientX: 230 })); + + expect(setColumnWidths).not.toHaveBeenCalled(); + act(() => flushAnimationFrames()); + + expectLastWidthUpdate(150); + expect(ghost.style.transform).toBe('translateX(190px)'); + }); + it('self-heals when movement reports no pressed button', () => { beginResize(); diff --git a/frontend/src/components/useDataGridColumnResize.ts b/frontend/src/components/useDataGridColumnResize.ts index 6d6faf36..48f8d968 100644 --- a/frontend/src/components/useDataGridColumnResize.ts +++ b/frontend/src/components/useDataGridColumnResize.ts @@ -11,12 +11,26 @@ const ROW_NUMBER_MIN_WIDTH = 28; const ROW_NUMBER_MAX_WIDTH = 120; type UseDataGridColumnResizeContext = Record; +type ColumnResizeDragState = { + startX: number; + startWidth: number; + key: string; + containerLeft: number; +}; type ColumnResizeListeners = { blur: () => void; move: (event: MouseEvent) => void; up: (event: MouseEvent) => void; }; +const resolveColumnResizeWidth = (dragState: ColumnResizeDragState, clientX: number): number => { + const deltaX = clientX - dragState.startX; + const isRowNumberColumn = dragState.key === GONAVI_ROW_NUMBER_COLUMN_KEY; + const minWidth = isRowNumberColumn ? ROW_NUMBER_MIN_WIDTH : MIN_DATA_TABLE_COLUMN_WIDTH; + const maxWidth = isRowNumberColumn ? ROW_NUMBER_MAX_WIDTH : Number.POSITIVE_INFINITY; + return Math.min(maxWidth, Math.max(minWidth, dragState.startWidth + deltaX)); +}; + export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => { const { columnMetaMap, @@ -33,12 +47,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => showColumnType, } = ctx; - const draggingRef = useRef<{ - startX: number; - startWidth: number; - key: string; - containerLeft: number; - } | null>(null); + const draggingRef = useRef(null); const ghostRef = useRef(null); const resizeRafRef = useRef(null); const latestClientXRef = useRef(null); @@ -47,17 +56,34 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => const resizeBodyStyleRef = useRef<{ cursor: string; userSelect: string } | null>(null); const resizeListenersRef = useRef(null); const setColumnWidthsRef = useRef(setColumnWidths); + const lastAppliedResizeWidthRef = useRef(null); const autoFitCanvasRef = useRef(null); setColumnWidthsRef.current = setColumnWidths; - const flushGhostPosition = useCallback(() => { - resizeRafRef.current = null; - if (!draggingRef.current || !ghostRef.current) return; - if (latestClientXRef.current === null) return; - const relativeLeft = latestClientXRef.current - draggingRef.current.containerLeft; - ghostRef.current.style.transform = `translateX(${relativeLeft}px)`; + const applyResizeWidth = useCallback((dragState: ColumnResizeDragState, clientX: number, force = false) => { + const newWidth = resolveColumnResizeWidth(dragState, clientX); + if (!force && lastAppliedResizeWidthRef.current === newWidth) return; + lastAppliedResizeWidthRef.current = newWidth; + setColumnWidthsRef.current((prev: Record) => ( + prev[dragState.key] === newWidth + ? prev + : { ...prev, [dragState.key]: newWidth } + )); }, []); + const flushResizeFrame = useCallback(() => { + resizeRafRef.current = null; + if (!draggingRef.current) return; + if (latestClientXRef.current === null) return; + const dragState = draggingRef.current; + const clientX = latestClientXRef.current; + if (ghostRef.current) { + const relativeLeft = clientX - dragState.containerLeft; + ghostRef.current.style.transform = `translateX(${relativeLeft}px)`; + } + applyResizeWidth(dragState, clientX); + }, [applyResizeWidth]); + const detachResizeListeners = useCallback(() => { const listeners = resizeListenersRef.current; if (!listeners) return; @@ -110,14 +136,10 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => if (commit && dragState) { const finalClientX = Number.isFinite(clientX) ? clientX as number : latestClientX ?? dragState.startX; - const deltaX = finalClientX - dragState.startX; - const isRowNumberColumn = dragState.key === GONAVI_ROW_NUMBER_COLUMN_KEY; - const minWidth = isRowNumberColumn ? ROW_NUMBER_MIN_WIDTH : MIN_DATA_TABLE_COLUMN_WIDTH; - const maxWidth = isRowNumberColumn ? ROW_NUMBER_MAX_WIDTH : Number.POSITIVE_INFINITY; - const newWidth = Math.min(maxWidth, Math.max(minWidth, dragState.startWidth + deltaX)); - setColumnWidthsRef.current((prev: Record) => ({ ...prev, [dragState.key]: newWidth })); + applyResizeWidth(dragState, finalClientX, true); } - }, [detachResizeListeners, restoreResizeBodyStyles]); + lastAppliedResizeWidthRef.current = null; + }, [applyResizeWidth, detachResizeListeners, restoreResizeBodyStyles]); const handleResizeStart = useCallback((key: string) => (e: React.MouseEvent) => { e.preventDefault(); @@ -137,6 +159,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => }); const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0; draggingRef.current = { startX, startWidth: currentWidth, key, containerLeft }; + lastAppliedResizeWidthRef.current = currentWidth; latestClientXRef.current = startX; if (ghostRef.current && containerRef.current) { @@ -153,7 +176,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => return; } if (resizeRafRef.current !== null) return; - resizeRafRef.current = requestAnimationFrame(flushGhostPosition); + resizeRafRef.current = requestAnimationFrame(flushResizeFrame); }; const handleUp = (event: MouseEvent) => finishResize(event.clientX); const handleBlur = () => finishResize(); @@ -172,7 +195,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => }; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; - }, [columnWidths, containerRef, dataTableDensity, finishResize, flushGhostPosition]); + }, [columnWidths, containerRef, dataTableDensity, finishResize, flushResizeFrame]); useEffect(() => () => { finishResize(undefined, false, false);