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:
2026-02-20 14:46:45 +08:00
parent 49b5c37cb1
commit 8d496d1515
38 changed files with 7255 additions and 1074 deletions

View 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;

View File

@@ -0,0 +1,5 @@
/**
* Layout 通用布局组件
*/
export { ResizableSplitPane } from './ResizableSplitPane';
export type { default as ResizableSplitPaneType } from './ResizableSplitPane';