feat(ui): 添加可拖拽的面板分割线
- 新增 Resizer 组件,支持拖拽调整面板宽度 - IDE 和聊天区域宽度可通过拖拽分割线调整 - 宽度范围限制在 30%-80%,防止面板过小或过大 - 调整后的宽度保存到 localStorage,下次打开时保持
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Resizer Component
|
||||
*
|
||||
* 可拖拽的分割线,用于调整面板宽度
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { cn } from '../utils/cn.js';
|
||||
|
||||
interface ResizerProps {
|
||||
/** 拖拽时的回调,返回拖拽的像素偏移量 */
|
||||
onResize: (delta: number) => void;
|
||||
/** 拖拽结束的回调 */
|
||||
onResizeEnd?: () => void;
|
||||
/** 方向:vertical(左右拖拽)或 horizontal(上下拖拽) */
|
||||
direction?: 'vertical' | 'horizontal';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Resizer({
|
||||
onResize,
|
||||
onResizeEnd,
|
||||
direction = 'vertical',
|
||||
className,
|
||||
}: ResizerProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const startPosRef = useRef(0);
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
startPosRef.current = direction === 'vertical' ? e.clientX : e.clientY;
|
||||
}, [direction]);
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const currentPos = direction === 'vertical' ? e.clientX : e.clientY;
|
||||
const delta = currentPos - startPosRef.current;
|
||||
startPosRef.current = currentPos;
|
||||
onResize(delta);
|
||||
}, [isDragging, direction, onResize]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (isDragging) {
|
||||
setIsDragging(false);
|
||||
onResizeEnd?.();
|
||||
}
|
||||
}, [isDragging, onResizeEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
// 拖拽时禁止选中文本
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = direction === 'vertical' ? 'col-resize' : 'row-resize';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
};
|
||||
}, [isDragging, handleMouseMove, handleMouseUp, direction]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
className={cn(
|
||||
'flex-shrink-0 bg-line hover:bg-primary-500 transition-colors',
|
||||
direction === 'vertical' ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize',
|
||||
isDragging && 'bg-primary-500',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -254,6 +254,7 @@ export { FileExplorer } from './components/FileExplorer.js';
|
||||
export { CodeEditor, getLanguageFromFilename, type EditorTab } from './components/CodeEditor.js';
|
||||
export { IDE } from './components/IDE.js';
|
||||
export { StatusBar } from './components/StatusBar.js';
|
||||
export { Resizer } from './components/Resizer.js';
|
||||
|
||||
// Toast function (re-export from sonner)
|
||||
export { toast } from 'sonner';
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DiagnosticsPanel,
|
||||
SessionPanel,
|
||||
StatusBar,
|
||||
Resizer,
|
||||
Toaster,
|
||||
ThemeProvider,
|
||||
listSessions,
|
||||
@@ -38,6 +39,11 @@ export function App() {
|
||||
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
||||
const [showSessions, setShowSessions] = useState(false);
|
||||
const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null);
|
||||
// IDE 面板宽度(百分比)
|
||||
const [idePanelWidth, setIdePanelWidth] = useState(() => {
|
||||
const saved = localStorage.getItem('ai-assistant-ide-width');
|
||||
return saved ? parseFloat(saved) : 60;
|
||||
});
|
||||
|
||||
// 初始化:加载会话
|
||||
useEffect(() => {
|
||||
@@ -97,6 +103,21 @@ export function App() {
|
||||
setSessionTitleUpdate({ sessionId, name });
|
||||
}, []);
|
||||
|
||||
// 处理面板宽度调整
|
||||
const handleResize = useCallback((delta: number) => {
|
||||
setIdePanelWidth((prev) => {
|
||||
// 计算百分比变化(基于窗口宽度)
|
||||
const percentDelta = (delta / window.innerWidth) * 100;
|
||||
// 限制范围:30% - 80%
|
||||
return Math.min(80, Math.max(30, prev + percentDelta));
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 保存面板宽度到 localStorage
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
localStorage.setItem('ai-assistant-ide-width', String(idePanelWidth));
|
||||
}, [idePanelWidth]);
|
||||
|
||||
if (isInitializing) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
@@ -116,10 +137,20 @@ export function App() {
|
||||
{/* 主内容区域:左侧文件浏览器 + 右侧对话框 */}
|
||||
<div className="flex-1 flex min-w-0 overflow-hidden">
|
||||
{/* 左侧:IDE(文件浏览器 + 代码编辑器) */}
|
||||
<div className="hidden md:flex flex-col border-r border-line w-[50%] lg:w-[60%]">
|
||||
<div
|
||||
className="hidden md:flex flex-col"
|
||||
style={{ width: `${idePanelWidth}%` }}
|
||||
>
|
||||
<IDE />
|
||||
</div>
|
||||
|
||||
{/* 可拖拽分割线 */}
|
||||
<Resizer
|
||||
onResize={handleResize}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
className="hidden md:block"
|
||||
/>
|
||||
|
||||
{/* 右侧:聊天区域 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{currentSessionId ? (
|
||||
|
||||
Reference in New Issue
Block a user