feat(ssa): Complete V11 UI development and frontend-backend integration - Pixel-perfect V11 UI, multi-task support, Word export, input overlay fix, code cleanup. MVP Phase 1 core 95% complete.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
202
frontend-v2/src/shared/components/Layout/ResizableSplitPane.tsx
Normal file
202
frontend-v2/src/shared/components/Layout/ResizableSplitPane.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* ResizableSplitPane - 可拖拽分栏组件
|
||||
*
|
||||
* 从 AIA Protocol Agent 下沉的通用组件
|
||||
*
|
||||
* 支持:
|
||||
* - 动态调整左右面板比例
|
||||
* - 拖拽手柄
|
||||
* - 比例记忆 (localStorage)
|
||||
* - 平滑过渡动画
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
interface ResizableSplitPaneProps {
|
||||
/** 左侧面板 */
|
||||
leftPanel: React.ReactNode;
|
||||
/** 右侧面板 */
|
||||
rightPanel: React.ReactNode;
|
||||
/** 默认左侧宽度百分比 (0-100) */
|
||||
defaultLeftRatio?: number;
|
||||
/** 最小左侧宽度百分比 */
|
||||
minLeftRatio?: number;
|
||||
/** 最大左侧宽度百分比 */
|
||||
maxLeftRatio?: number;
|
||||
/** 是否启用拖拽 */
|
||||
enableDrag?: boolean;
|
||||
/** 比例变化回调 */
|
||||
onRatioChange?: (leftRatio: number) => void;
|
||||
/** localStorage 存储 key */
|
||||
storageKey?: string;
|
||||
/** CSS 类名 */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ResizableSplitPane: React.FC<ResizableSplitPaneProps> = ({
|
||||
leftPanel,
|
||||
rightPanel,
|
||||
defaultLeftRatio = 35,
|
||||
minLeftRatio = 25,
|
||||
maxLeftRatio = 50,
|
||||
enableDrag = true,
|
||||
onRatioChange,
|
||||
storageKey,
|
||||
className = '',
|
||||
}) => {
|
||||
const [leftRatio, setLeftRatio] = useState<number>(() => {
|
||||
if (storageKey) {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved) {
|
||||
const parsed = parseFloat(saved);
|
||||
if (!isNaN(parsed) && parsed >= minLeftRatio && parsed <= maxLeftRatio) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultLeftRatio;
|
||||
});
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLeftRatio(defaultLeftRatio);
|
||||
}, [defaultLeftRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (storageKey && !isDragging) {
|
||||
localStorage.setItem(storageKey, leftRatio.toString());
|
||||
}
|
||||
}, [leftRatio, storageKey, isDragging]);
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
if (!enableDrag) return;
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}, [enableDrag]);
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!isDragging || !containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const rect = container.getBoundingClientRect();
|
||||
const newRatio = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
|
||||
const clampedRatio = Math.max(minLeftRatio, Math.min(maxLeftRatio, newRatio));
|
||||
setLeftRatio(clampedRatio);
|
||||
onRatioChange?.(clampedRatio);
|
||||
}, [isDragging, minLeftRatio, maxLeftRatio, onRatioChange]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`resizable-split-pane ${className} ${isDragging ? 'dragging' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* 左侧面板 */}
|
||||
<div
|
||||
className="split-pane-left"
|
||||
style={{
|
||||
width: `${leftRatio}%`,
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
transition: isDragging ? 'none' : 'width 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{leftPanel}
|
||||
</div>
|
||||
|
||||
{/* 拖拽手柄 */}
|
||||
{enableDrag && (
|
||||
<div
|
||||
className="split-pane-handle"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{
|
||||
width: '6px',
|
||||
cursor: 'col-resize',
|
||||
background: isDragging ? '#6366F1' : 'transparent',
|
||||
position: 'relative',
|
||||
flexShrink: 0,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="split-handle-indicator"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '4px',
|
||||
height: '40px',
|
||||
background: isDragging ? '#fff' : '#94a3b8',
|
||||
borderRadius: '2px',
|
||||
opacity: isDragging ? 1 : 0,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="split-handle-hover-area"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-4px',
|
||||
right: '-4px',
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const handle = e.currentTarget.previousSibling as HTMLElement;
|
||||
if (handle) handle.style.opacity = '1';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isDragging) {
|
||||
const handle = e.currentTarget.previousSibling as HTMLElement;
|
||||
if (handle) handle.style.opacity = '0';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 右侧面板 */}
|
||||
<div
|
||||
className="split-pane-right"
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
transition: isDragging ? 'none' : 'width 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{rightPanel}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResizableSplitPane;
|
||||
5
frontend-v2/src/shared/components/Layout/index.ts
Normal file
5
frontend-v2/src/shared/components/Layout/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Layout 通用布局组件
|
||||
*/
|
||||
export { ResizableSplitPane } from './ResizableSplitPane';
|
||||
export type { default as ResizableSplitPaneType } from './ResizableSplitPane';
|
||||
Reference in New Issue
Block a user