From 98418ec5c31212868968fd1465b0c1fbbeb21c9a Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 26 May 2026 09:07:03 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(ui):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=8B=96=E6=8B=BD=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E7=BA=BF=E4=BD=8D=E7=BD=AE=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拖拽修复:右键点击侧边栏宽度区域不再触发拖拽预览线 - 定位修复:预览线改为基于 Sider 实际 DOM 右边界定位 - 宽度修复:拖拽计算读取 CSS min/max 宽度限制,避免状态宽度与实际渲染宽度不一致 - 回归测试:补充右键阻断和预览线真实边界定位测试 --- frontend/src/App.tool-center.test.ts | 26 ++++++++++ frontend/src/App.tsx | 72 ++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index 92b0c08..712369d 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -86,6 +86,32 @@ describe('tool center menu entries', () => { expect(appSource).toContain("darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)'"); }); + it('does not start sidebar resize from right-clicking the resize handle', () => { + expect(appSource).toContain('if (e.button !== 0)'); + expect(appSource).toContain('onContextMenu={(event) => {'); + expect(appSource).toContain('event.preventDefault();'); + expect(appSource).toContain('event.stopPropagation();'); + + const guardIndex = appSource.indexOf('if (e.button !== 0)'); + const ghostDisplayIndex = appSource.indexOf("ghostRef.current.style.display = 'block'", guardIndex); + const dragStartIndex = appSource.indexOf('sidebarDragRef.current = {', guardIndex); + + expect(guardIndex).toBeGreaterThan(-1); + expect(ghostDisplayIndex).toBeGreaterThan(guardIndex); + expect(dragStartIndex).toBeGreaterThan(guardIndex); + }); + + it('positions sidebar resize guide from the rendered sider edge', () => { + expect(appSource).toContain('const siderRef = React.useRef(null);'); + expect(appSource).toContain('ref={siderRef}'); + expect(appSource).toContain('const siderRect = siderRef.current?.getBoundingClientRect();'); + expect(appSource).toContain('const startGuideLeft = siderRect?.right ?? sidebarWidth;'); + expect(appSource).toContain('const startWidth = siderRect?.width ?? sidebarWidth;'); + expect(appSource).toContain('resolveSidebarResizeBounds(siderRef.current)'); + expect(appSource).toContain('ghostRef.current.style.left = `${startGuideLeft}px`;'); + expect(appSource).toContain('ghostRef.current.style.left = `${startGuideLeft + (newWidth - startWidth)}px`;'); + }); + it('mounts heavyweight modals only while they are open', () => { expect(appSource).toContain('{isModalOpen && ('); expect(appSource).toContain('{isToolsModalOpen && ('); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 76dce9a..c0179c6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -96,12 +96,42 @@ import './v2-theme.css'; const AIChatPanel = React.lazy(() => import('./components/AIChatPanel')); const { Sider, Content } = Layout; +const SIDEBAR_RESIZE_MIN_WIDTH = 200; +const SIDEBAR_RESIZE_MAX_WIDTH = 600; const MIN_UI_SCALE = 0.8; const MAX_UI_SCALE = 1.25; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 20; const DEFAULT_UI_SCALE = 1.0; const DEFAULT_FONT_SIZE = 14; +type SidebarResizeBounds = { minWidth: number; maxWidth: number }; +type SidebarResizeDragState = SidebarResizeBounds & { + startX: number; + startWidth: number; + startGuideLeft: number; +}; + +const parseCssPixelValue = (value: string | null | undefined): number | null => { + const parsed = Number.parseFloat(String(value || '')); + return Number.isFinite(parsed) ? parsed : null; +}; + +const resolveSidebarResizeBounds = (siderElement: Element | null): SidebarResizeBounds => { + if (typeof window === 'undefined' || !(siderElement instanceof HTMLElement)) { + return { minWidth: SIDEBAR_RESIZE_MIN_WIDTH, maxWidth: SIDEBAR_RESIZE_MAX_WIDTH }; + } + const computed = window.getComputedStyle(siderElement); + const cssMinWidth = parseCssPixelValue(computed.minWidth); + const cssMaxWidth = parseCssPixelValue(computed.maxWidth); + const minWidth = Math.max(SIDEBAR_RESIZE_MIN_WIDTH, cssMinWidth && cssMinWidth > 0 ? cssMinWidth : SIDEBAR_RESIZE_MIN_WIDTH); + const maxWidth = Math.max(minWidth, Math.min(SIDEBAR_RESIZE_MAX_WIDTH, cssMaxWidth && cssMaxWidth > 0 ? cssMaxWidth : SIDEBAR_RESIZE_MAX_WIDTH)); + return { minWidth, maxWidth }; +}; + +const clampSidebarResizeWidth = (width: number, bounds: SidebarResizeBounds): number => ( + Math.max(bounds.minWidth, Math.min(bounds.maxWidth, width)) +); + const createEmptySecurityUpdateStatus = (): SecurityUpdateStatus => ({ overallStatus: 'not_detected', summary: { @@ -2413,9 +2443,10 @@ function App() { }, []); // Sidebar Resizing - const sidebarDragRef = React.useRef<{ startX: number, startWidth: number } | null>(null); + const sidebarDragRef = React.useRef(null); const rafRef = React.useRef(null); const ghostRef = React.useRef(null); + const siderRef = React.useRef(null); const sidebarDragBodyStyleRef = React.useRef<{ cursor: string; userSelect: string; webkitUserSelect: string } | null>(null); const latestMouseX = React.useRef(0); // Store latest mouse position const sidebarResizeHandleWidth = Math.max(16, Math.round(16 * effectiveUiScale)); @@ -2434,6 +2465,12 @@ function App() { }; const handleSidebarMouseDown = (e: React.MouseEvent) => { + if (e.button !== 0) { + e.preventDefault(); + e.stopPropagation(); + return; + } + e.preventDefault(); e.stopPropagation(); @@ -2448,12 +2485,22 @@ function App() { (document.body.style as any).WebkitUserSelect = 'none'; } + const siderRect = siderRef.current?.getBoundingClientRect(); + const startGuideLeft = siderRect?.right ?? sidebarWidth; + const startWidth = siderRect?.width ?? sidebarWidth; + const resizeBounds = resolveSidebarResizeBounds(siderRef.current); + if (ghostRef.current) { - ghostRef.current.style.left = `${sidebarWidth}px`; + ghostRef.current.style.left = `${startGuideLeft}px`; ghostRef.current.style.display = 'block'; } - - sidebarDragRef.current = { startX: e.clientX, startWidth: sidebarWidth }; + + sidebarDragRef.current = { + startX: e.clientX, + startWidth, + startGuideLeft, + ...resizeBounds, + }; latestMouseX.current = e.clientX; // Init document.addEventListener('mousemove', handleSidebarMouseMove); document.addEventListener('mouseup', handleSidebarMouseUp); @@ -2469,9 +2516,10 @@ function App() { rafRef.current = requestAnimationFrame(() => { if (!sidebarDragRef.current || !ghostRef.current) return; // Use latestMouseX.current instead of stale closure 'e.clientX' - const delta = latestMouseX.current - sidebarDragRef.current.startX; - const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); - ghostRef.current.style.left = `${newWidth}px`; + const { startX, startWidth, startGuideLeft, minWidth, maxWidth } = sidebarDragRef.current; + const delta = latestMouseX.current - startX; + const newWidth = clampSidebarResizeWidth(startWidth + delta, { minWidth, maxWidth }); + ghostRef.current.style.left = `${startGuideLeft + (newWidth - startWidth)}px`; rafRef.current = null; }); }; @@ -2484,8 +2532,9 @@ function App() { if (sidebarDragRef.current) { // Use latest position for final commit too - const delta = e.clientX - sidebarDragRef.current.startX; - const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); + const { startX, startWidth, minWidth, maxWidth } = sidebarDragRef.current; + const delta = e.clientX - startX; + const newWidth = clampSidebarResizeWidth(startWidth + delta, { minWidth, maxWidth }); setSidebarWidth(newWidth); } @@ -2921,6 +2970,7 @@ function App() { { + event.preventDefault(); + event.stopPropagation(); + }} role="separator" aria-orientation="vertical" title="拖动调整宽度"