fix: 修复画布图片重复添加和工具栏位置问题
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

1. 首次加载时不将历史消息中的图片添加到画布(避免重复)
2. 检查画布中是否已存在相同 URL 的图片
3. 工具栏位置在渲染时实时计算(跟随画布平移缩放)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name 2026-02-01 15:35:09 +08:00
parent dbdd188c9e
commit 2524ec551b
2 changed files with 40 additions and 19 deletions

View File

@ -286,9 +286,24 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
}, [historyData, setMessages]);
// 监听消息变化,提取图片并添加到画布
// 只处理新生成的图片,不处理历史消息中的图片
const processedMessageIds = useRef<Set<string>>(new Set());
const isInitialLoad = useRef(true);
useEffect(() => {
// 首次加载历史消息时,只记录已处理的消息 ID,不添加图片到画布
// (画布数据应该从 canvasData 中加载,而不是从消息中提取)
if (isInitialLoad.current) {
messages.forEach(message => {
if (message.role === 'assistant') {
processedMessageIds.current.add(message.id);
}
});
isInitialLoad.current = false;
return;
}
// 只处理新消息
messages.forEach(message => {
if (message.role === 'assistant' && !processedMessageIds.current.has(message.id)) {
const content = message.parts?.map((p: any) => p.text || '').join('') || '';
@ -297,8 +312,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
if (imageUrls.length > 0) {
processedMessageIds.current.add(message.id);
// 为每张图片创建画布节点
imageUrls.forEach((url, index) => {
// 检查画布中是否已存在相同 URL 的图片
const existingUrls = new Set(
nodes.filter(n => n.type === 'image' && (n.data as any)?.src)
.map(n => (n.data as any).src)
);
// 只添加不存在的图片
const newUrls = imageUrls.filter(url => !existingUrls.has(url));
newUrls.forEach((url, index) => {
// 计算位置(避免重叠)
const existingNodes = nodes.length;
const x = 100 + ((existingNodes + index) % 4) * 320;
@ -317,13 +340,13 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
});
});
if (imageUrls.length > 0) {
toast.success(`已将 ${imageUrls.length} 张图片添加到画布`);
if (newUrls.length > 0) {
toast.success(`已将 ${newUrls.length} 张图片添加到画布`);
}
}
}
});
}, [messages, addNode, nodes.length]);
}, [messages, addNode, nodes]);
// 滚动到底部
useEffect(() => {

View File

@ -43,7 +43,6 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
const [resizeWidth, setResizeWidth] = useState(1024);
const [resizeHeight, setResizeHeight] = useState(1024);
const [isUploading, setIsUploading] = useState(false);
const [toolbarPosition, setToolbarPosition] = useState<{ top: number; left: number } | null>(null);
// 文件输入的 ref
const fileInputRef = useRef<HTMLInputElement>(null);
@ -53,18 +52,17 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
const deleteNode = useCanvasStore((state) => state.deleteNode);
const addNode = useCanvasStore((state) => state.addNode);
// 计算工具栏位置
useEffect(() => {
if (isSelected && containerRef.current) {
// 获取工具栏位置 - 在渲染时直接计算
const getToolbarPosition = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setToolbarPosition({
top: rect.top - 50, // 工具栏在图片上方 50px
left: rect.left + rect.width / 2, // 居中
});
} else {
setToolbarPosition(null);
return {
top: rect.top - 50,
left: rect.left + rect.width / 2,
};
}
}, [isSelected, node.position, node.size]);
return { top: 0, left: 0 };
};
// 触发文件选择
const handleSelectFile = (e: React.MouseEvent) => {
@ -536,12 +534,12 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
</Dialog>
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
{isSelected && data.src && !isProcessing && toolbarPosition && typeof document !== 'undefined' && createPortal(
{isSelected && data.src && !isProcessing && typeof document !== 'undefined' && createPortal(
<div
className="fixed flex items-center gap-0.5 bg-white border border-gray-200 rounded-full shadow-xl px-3 py-1.5 z-[9999]"
style={{
top: toolbarPosition.top,
left: toolbarPosition.left,
top: getToolbarPosition().top,
left: getToolbarPosition().left,
transform: 'translateX(-50%)',
pointerEvents: 'auto',
}}