重构项目详情页面
This commit is contained in:
50
src/components/layout/ChatView.tsx
Normal file
50
src/components/layout/ChatView.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ChatViewProps {
|
||||
projectPathInput?: React.ReactNode;
|
||||
messagesList: React.ReactNode;
|
||||
floatingInput?: React.ReactNode;
|
||||
floatingElements?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ChatView: React.FC<ChatViewProps> = ({
|
||||
projectPathInput,
|
||||
messagesList,
|
||||
floatingInput,
|
||||
floatingElements,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('h-full w-full flex flex-col relative', className)}>
|
||||
{/* 项目路径输入(如果提供) */}
|
||||
{projectPathInput && (
|
||||
<div className="shrink-0">
|
||||
{projectPathInput}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 消息列表区域 - 占据大部分空间 */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{messagesList}
|
||||
</div>
|
||||
|
||||
{/* 浮动输入框 - 最小化高度 */}
|
||||
{floatingInput && (
|
||||
<div className="shrink-0 relative">
|
||||
{floatingInput}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 其他浮动元素(如队列提示、Token计数器等) */}
|
||||
{floatingElements && (
|
||||
<div className="absolute inset-0 pointer-events-none z-30">
|
||||
<div className="relative h-full w-full">
|
||||
{floatingElements}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
144
src/components/layout/FlexLayoutContainer.tsx
Normal file
144
src/components/layout/FlexLayoutContainer.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { ReactNode, useState, useCallback, useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface LayoutPanel {
|
||||
id: string;
|
||||
content: ReactNode;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
resizable?: boolean;
|
||||
visible?: boolean;
|
||||
position?: 'left' | 'center' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface FlexLayoutContainerProps {
|
||||
panels: LayoutPanel[];
|
||||
className?: string;
|
||||
mainContentId: string;
|
||||
onPanelResize?: (panelId: string, width: number) => void;
|
||||
savedWidths?: Record<string, number>;
|
||||
}
|
||||
|
||||
export const FlexLayoutContainer: React.FC<FlexLayoutContainerProps> = ({
|
||||
panels,
|
||||
className,
|
||||
mainContentId,
|
||||
onPanelResize,
|
||||
savedWidths = {}
|
||||
}) => {
|
||||
const [panelWidths, setPanelWidths] = useState<Record<string, number>>({});
|
||||
const [isDragging, setIsDragging] = useState<string | null>(null);
|
||||
const [dragStartX, setDragStartX] = useState(0);
|
||||
const [dragStartWidth, setDragStartWidth] = useState(0);
|
||||
|
||||
// 初始化面板宽度
|
||||
useEffect(() => {
|
||||
const initialWidths: Record<string, number> = {};
|
||||
panels.forEach(panel => {
|
||||
if (panel.visible !== false && panel.id !== mainContentId) {
|
||||
initialWidths[panel.id] = savedWidths[panel.id] || panel.defaultWidth || 280;
|
||||
}
|
||||
});
|
||||
setPanelWidths(initialWidths);
|
||||
}, [panels, mainContentId, savedWidths]);
|
||||
|
||||
// 处理拖拽开始
|
||||
const handleDragStart = useCallback((e: React.MouseEvent, panelId: string) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(panelId);
|
||||
setDragStartX(e.clientX);
|
||||
setDragStartWidth(panelWidths[panelId] || 280);
|
||||
}, [panelWidths]);
|
||||
|
||||
// 处理拖拽移动
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const panel = panels.find(p => p.id === isDragging);
|
||||
if (!panel) return;
|
||||
|
||||
const delta = panel.position === 'left'
|
||||
? e.clientX - dragStartX
|
||||
: dragStartX - e.clientX;
|
||||
|
||||
const newWidth = Math.max(
|
||||
panel.minWidth || 200,
|
||||
Math.min(panel.maxWidth || 600, dragStartWidth + delta)
|
||||
);
|
||||
|
||||
setPanelWidths(prev => ({
|
||||
...prev,
|
||||
[isDragging]: newWidth
|
||||
}));
|
||||
|
||||
if (onPanelResize) {
|
||||
onPanelResize(isDragging, newWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(null);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging, dragStartX, dragStartWidth, panels, onPanelResize]);
|
||||
|
||||
// 渲染面板
|
||||
const renderPanel = (panel: LayoutPanel) => {
|
||||
if (panel.visible === false) return null;
|
||||
|
||||
const isMain = panel.id === mainContentId;
|
||||
const width = isMain ? 'flex-1' : `${panelWidths[panel.id] || panel.defaultWidth || 280}px`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={panel.id}
|
||||
className={cn(
|
||||
'relative h-full',
|
||||
isMain ? 'flex-1 min-w-0' : 'overflow-hidden',
|
||||
panel.className
|
||||
)}
|
||||
style={!isMain ? { width, flexShrink: 0 } : undefined}
|
||||
>
|
||||
{panel.content}
|
||||
|
||||
{/* 调整手柄 */}
|
||||
{!isMain && panel.resizable !== false && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-0 w-1 h-full cursor-col-resize hover:bg-primary/20 transition-colors z-50',
|
||||
panel.position === 'left' ? 'right-0' : 'left-0',
|
||||
isDragging === panel.id && 'bg-primary/40'
|
||||
)}
|
||||
onMouseDown={(e) => handleDragStart(e, panel.id)}
|
||||
>
|
||||
<div className="absolute inset-y-0 w-4 -left-1.5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 按位置排序面板
|
||||
const sortedPanels = [...panels].sort((a, b) => {
|
||||
const positionOrder = { left: 0, center: 1, right: 2 };
|
||||
const aPos = a.position || (a.id === mainContentId ? 'center' : 'left');
|
||||
const bPos = b.position || (b.id === mainContentId ? 'center' : 'left');
|
||||
return positionOrder[aPos] - positionOrder[bPos];
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full w-full', className)}>
|
||||
{sortedPanels.map(renderPanel)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
src/components/layout/MainContentArea.tsx
Normal file
25
src/components/layout/MainContentArea.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MainContentAreaProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export const MainContentArea: React.FC<MainContentAreaProps> = ({
|
||||
children,
|
||||
className,
|
||||
isEditing = false
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'h-full w-full flex flex-col',
|
||||
'bg-background',
|
||||
isEditing && 'relative',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
49
src/components/layout/SidePanel.tsx
Normal file
49
src/components/layout/SidePanel.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface SidePanelProps {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
position?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export const SidePanel: React.FC<SidePanelProps> = ({
|
||||
children,
|
||||
title,
|
||||
onClose,
|
||||
className,
|
||||
position = 'left'
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex flex-col h-full',
|
||||
'bg-background',
|
||||
position === 'left' ? 'border-r' : 'border-l',
|
||||
'border-border',
|
||||
className
|
||||
)}>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between p-3 border-b border-border">
|
||||
<h3 className="text-sm font-semibold">{title}</h3>
|
||||
{onClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user