Files
mixvideo-v2/apps/desktop/src/components/canvas/CanvasContainer.tsx
imeepos 7d8b8a3de1 feat: 实现AI画布工具并隐藏,设置项目为首页
- 新增完整的AI画布工具系统
  - 可视化节点编辑器,支持拖拽连线
  - 多种节点类型:文本输入、图片生成、视频生成等
  - 智能连接验证和数据流转换
  - 异步处理引擎,支持进度追踪和取消
  - 批量处理系统,支持并发处理
  - AI服务集成框架,支持多种AI API

- 用户体验优化
  - 智能弹框定位,防止被遮挡
  - 节点删除功能(悬停删除按钮 + 键盘快捷键)
  - 通知系统和错误处理
  - 快速开始模板
  - 键盘快捷键支持

- 界面调整
  - 暂时隐藏AI画布,保留代码
  - 设置项目列表为首页
  - 简化导航栏结构
2025-08-07 10:12:46 +08:00

249 lines
7.4 KiB
TypeScript

import React, { useCallback, useMemo, useState, useEffect } from 'react';
import ReactFlow, {
Background,
Controls,
MiniMap,
useReactFlow,
ReactFlowProvider,
Connection,
Node
} from 'reactflow';
import 'reactflow/dist/style.css';
import { useCanvasStore } from '../../store/canvasStore';
import { NodeSelector } from './NodeSelector';
import { TextInputNode } from './nodes/TextInputNode';
import { PromptOptimizeNode } from './nodes/PromptOptimizeNode';
import { TextDisplayNode } from './nodes/TextDisplayNode';
import { ImageUploadNode } from './nodes/ImageUploadNode';
import { ImageGenerateNode } from './nodes/ImageGenerateNode';
import { ImageDisplayNode } from './nodes/ImageDisplayNode';
import { ImageEditNode } from './nodes/ImageEditNode';
import { VideoGenerateNode } from './nodes/VideoGenerateNode';
import { VideoPlayerNode } from './nodes/VideoPlayerNode';
import { FolderUploadNode } from './nodes/FolderUploadNode';
import { BatchResultGrid } from './nodes/BatchResultGrid';
import { NodeType } from '../../types/canvas';
import { ConnectionValidator, DataFlowConverter } from '../../utils/connectionValidator';
import { NotificationSystem, useGlobalNotifications, globalNotifications } from './NotificationSystem';
import { ProcessingIndicator, KeyboardShortcutsHint } from './ConnectionIndicator';
// 节点类型映射
const nodeTypes = {
textInput: TextInputNode,
promptOptimize: PromptOptimizeNode,
textDisplay: TextDisplayNode,
imageUpload: ImageUploadNode,
imageGenerate: ImageGenerateNode,
imageDisplay: ImageDisplayNode,
imageEdit: ImageEditNode,
videoGenerate: VideoGenerateNode,
videoPlayer: VideoPlayerNode,
folderUpload: FolderUploadNode,
batchGrid: BatchResultGrid,
};
const CanvasFlow: React.FC = () => {
const [showShortcuts, setShowShortcuts] = useState(false);
const { notifications, removeNotification } = useGlobalNotifications();
const {
nodes,
edges,
onNodesChange,
onEdgesChange,
onConnect: storeOnConnect,
showNodeSelector,
nodeSelectorPosition,
showNodeSelectorAt,
hideNodeSelector,
addNode,
updateNode,
deleteNode,
processingNodes
} = useCanvasStore();
const { screenToFlowPosition } = useReactFlow();
// 处理画布点击
const onPaneClick = useCallback((event: React.MouseEvent) => {
// 单击空白区域关闭节点选择器
if (event.detail === 1 && (event.target as HTMLElement).classList.contains('react-flow__pane')) {
hideNodeSelector();
}
// 双击空白区域显示节点选择器
if (event.detail === 2 && (event.target as HTMLElement).classList.contains('react-flow__pane')) {
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
showNodeSelectorAt(position);
}
}, [screenToFlowPosition, showNodeSelectorAt, hideNodeSelector]);
// 处理连线
const onConnect = useCallback((connection: Connection) => {
// 验证连接是否有效
const validation = ConnectionValidator.validateConnection(connection, nodes);
if (!validation.valid) {
globalNotifications.error('连接失败', validation.reason);
return;
}
// 添加连线到store
storeOnConnect(connection);
// 触发数据流处理
const sourceNode = nodes.find(n => n.id === connection.source);
const targetNode = nodes.find(n => n.id === connection.target);
if (sourceNode && targetNode && sourceNode.data?.result) {
// 使用数据转换器准备数据
const preparedData = DataFlowConverter.prepareDataForNode(
sourceNode.data.result,
sourceNode.type as NodeType,
targetNode.type as NodeType
);
// 更新目标节点数据
updateNode(connection.target!, {
data: {
...targetNode.data,
...preparedData
}
});
}
}, [storeOnConnect, nodes, updateNode]);
// 创建节点
const createNode = useCallback((nodeType: NodeType, position: { x: number; y: number }) => {
const newNode: Node = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
label: nodeType
}
};
addNode(newNode);
globalNotifications.success('节点已创建', `已添加 ${nodeType} 节点`);
}, [addNode]);
// 键盘事件处理
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'F1') {
e.preventDefault();
setShowShortcuts(!showShortcuts);
}
if (e.key === 'Escape') {
hideNodeSelector();
}
if (e.key === 'Delete' || e.key === 'Backspace') {
// 删除选中的节点
const selectedNodes = nodes.filter(node => node.selected);
if (selectedNodes.length > 0) {
const nodeNames = selectedNodes.map(node => node.data?.title || node.type).join(', ');
if (confirm(`确定要删除选中的节点吗?\n${nodeNames}`)) {
selectedNodes.forEach(node => {
deleteNode(node.id);
});
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [showShortcuts, hideNodeSelector, nodes, deleteNode]);
// 自定义边样式
const edgeOptions = useMemo(() => ({
animated: true,
style: { strokeWidth: 2 }
}), []);
return (
<div className="w-full h-full relative" style={{ minHeight: '500px' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
defaultEdgeOptions={edgeOptions}
fitView={nodes.length === 0}
fitViewOptions={{
padding: 0.1,
includeHiddenNodes: false,
minZoom: 0.1,
maxZoom: 2
}}
minZoom={0.1}
maxZoom={2}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
preventScrolling={false}
panOnDrag={true}
zoomOnScroll={true}
zoomOnPinch={true}
zoomOnDoubleClick={false}
className="bg-gray-50 w-full h-full"
>
<Background color="#e5e7eb" gap={20} />
<Controls />
<MiniMap
nodeColor="#3b82f6"
maskColor="rgba(0, 0, 0, 0.1)"
className="bg-white border border-gray-200 rounded-lg"
/>
</ReactFlow>
{/* 节点选择器 */}
{showNodeSelector && (
<NodeSelector
position={nodeSelectorPosition}
onSelect={createNode}
onClose={hideNodeSelector}
/>
)}
{/* 处理进度指示器 */}
{Array.from(processingNodes.entries()).map(([nodeId, info]) => (
<ProcessingIndicator
key={nodeId}
nodeId={nodeId}
progress={info.progress}
status={info.status}
/>
))}
{/* 通知系统 */}
<NotificationSystem
notifications={notifications}
onRemove={removeNotification}
/>
{/* 快捷键提示 */}
<KeyboardShortcutsHint visible={showShortcuts} />
{/* 帮助提示 */}
<button
onClick={() => setShowShortcuts(!showShortcuts)}
className="absolute bottom-4 right-4 bg-blue-600 text-white p-2 rounded-full shadow-lg hover:bg-blue-700 transition-colors"
title="显示快捷键 (F1)"
>
?
</button>
</div>
);
};
export const CanvasContainer: React.FC = () => {
return <CanvasFlow />;
};