重构项目详情页面

This commit is contained in:
2025-08-13 00:23:37 +08:00
parent ef0c895f1e
commit 4943e48254
9 changed files with 712 additions and 420 deletions

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

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

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

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