From ee9ac5521fd5362b7ee83f958e798e266f9b7074 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Feb 2026 12:28:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E5=BC=A0=E5=9B=BE=E7=89=87=E6=A0=87=E6=B3=A8=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E7=89=A9=E5=93=81=EF=BC=8C=E6=AF=8F=E4=B8=AA=E7=89=A9=E5=93=81?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/canvas/ObjectPickerOverlay.tsx | 145 +++++++++++++++++----- 1 file changed, 116 insertions(+), 29 deletions(-) diff --git a/components/canvas/ObjectPickerOverlay.tsx b/components/canvas/ObjectPickerOverlay.tsx index f6b96ce..88761d3 100644 --- a/components/canvas/ObjectPickerOverlay.tsx +++ b/components/canvas/ObjectPickerOverlay.tsx @@ -2,12 +2,20 @@ import React, { useState, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; -import { X, Loader2, Crosshair } from "lucide-react"; +import { X, Loader2, Crosshair, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useCanvasStore, type PickedObject } from "@/store/canvas"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; +// 已标注的物品(本地状态,用于显示标记) +interface PickedMarker { + clickX: number; + clickY: number; + objectName: string; + index: number; +} + /** * 在图片上绘制标记点,返回带标记的图片 base64 * AI 通过看到这个红色标记来识别用户点击的位置 @@ -113,14 +121,17 @@ export function ObjectPickerOverlay() { const addPendingPickedObject = useCanvasStore((state) => state.addPendingPickedObject); const getNode = useCanvasStore((state) => state.getNode); - const [clickPosition, setClickPosition] = useState<{ x: number; y: number } | null>(null); + // 当前正在识别的点击位置 + const [currentClick, setCurrentClick] = useState<{ x: number; y: number } | null>(null); + // 已成功标注的物品列表(本次会话中) + const [pickedMarkers, setPickedMarkers] = useState([]); const overlayRef = useRef(null); // 获取目标节点 const targetNode = pickerTargetNodeId ? getNode(pickerTargetNodeId) : null; const imageUrl = targetNode?.type === 'image' ? (targetNode.data as any)?.src as string | undefined : undefined; - // 处理图片点击 - 识别成功后直接添加到对话框 + // 处理图片点击 - 识别成功后添加到对话框,但不关闭拾取模式 const handleImageClick = useCallback(async (e: React.MouseEvent) => { if (isPickerLoading || !imageUrl || !pickerTargetNodeId) return; @@ -132,7 +143,7 @@ export function ObjectPickerOverlay() { const clickY = (e.clientY - rect.top) / rect.height; // 显示点击位置 - setClickPosition({ x: clickX, y: clickY }); + setCurrentClick({ x: clickX, y: clickY }); setPickerLoading(true); try { @@ -169,30 +180,49 @@ export function ObjectPickerOverlay() { boundingBox: result.object.boundingBox, }; - // 直接添加到对话框,不询问确认 + // 添加到对话框 addPendingPickedObject(picked); - toast.success(`已添加: ${result.object.name}`); - // 关闭拾取模式 - setPickerMode(false); - setClickPosition(null); + // 添加到已标注列表(用于显示标记) + setPickedMarkers(prev => [...prev, { + clickX, + clickY, + objectName: result.object.name, + index: prev.length + 1, + }]); + + toast.success(`已添加: ${result.object.name},可继续标注其他物品`); + + // 清除当前点击位置,但不关闭拾取模式 + setCurrentClick(null); } else { toast.error(result.error || '未能识别物品'); - setClickPosition(null); + setCurrentClick(null); } } catch (error) { console.error('物品拾取失败:', error); toast.error('识别失败,请重试'); - setClickPosition(null); + setCurrentClick(null); } finally { setPickerLoading(false); } - }, [imageUrl, isPickerLoading, pickerTargetNodeId, setPickerLoading, addPendingPickedObject, setPickerMode]); + }, [imageUrl, isPickerLoading, pickerTargetNodeId, setPickerLoading, addPendingPickedObject]); - // 关闭拾取模式 + // 完成标注,关闭拾取模式 + const handleFinish = useCallback(() => { + setPickerMode(false); + setCurrentClick(null); + setPickedMarkers([]); + if (pickedMarkers.length > 0) { + toast.success(`已完成 ${pickedMarkers.length} 个物品标注`); + } + }, [setPickerMode, pickedMarkers.length]); + + // 关闭拾取模式(不保存) const handleClose = useCallback(() => { setPickerMode(false); - setClickPosition(null); + setCurrentClick(null); + setPickedMarkers([]); }, [setPickerMode]); if (!isPickerMode || !imageUrl) return null; @@ -202,8 +232,8 @@ export function ObjectPickerOverlay() { ref={overlayRef} className="fixed inset-0 z-[9998] bg-black/60 flex items-center justify-center" onClick={(e) => { - // 点击背景关闭 - if (e.target === overlayRef.current) { + // 点击背景不关闭,需要点击完成按钮 + if (e.target === overlayRef.current && pickedMarkers.length === 0) { handleClose(); } }} @@ -212,16 +242,31 @@ export function ObjectPickerOverlay() {
- {isPickerLoading ? '正在识别...' : '点击图片中的物品进行拾取'} + {isPickerLoading + ? '正在识别...' + : pickedMarkers.length > 0 + ? `已标注 ${pickedMarkers.length} 个物品,继续点击或完成` + : '点击图片中的物品进行标注'} - + {pickedMarkers.length > 0 ? ( + + ) : ( + + )}
{/* 图片容器 */} @@ -239,13 +284,37 @@ export function ObjectPickerOverlay() { draggable={false} /> - {/* 点击位置标记(脉冲动画)- 仅在前端显示,不影响发送给 AI 的图片 */} - {clickPosition && ( + {/* 已标注的物品标记 */} + {pickedMarkers.map((marker) => ( +
+ {/* 序号标记 */} +
+
+ {marker.index} +
+
+ {/* 物品名称 */} +
+ {marker.objectName} +
+
+ ))} + + {/* 当前正在识别的点击位置标记(脉冲动画) */} + {currentClick && (
@@ -270,6 +339,24 @@ export function ObjectPickerOverlay() {
)} + + {/* 底部已标注物品列表 */} + {pickedMarkers.length > 0 && ( +
+ 已标注: + {pickedMarkers.map((marker) => ( + + + {marker.index} + + {marker.objectName} + + ))} +
+ )} , document.body );