import type React from "react"; import { useState, useRef, useEffect } from "react"; import { Button, Badge, Checkbox, message } from "antd"; import { Square, Circle, MousePointer, ZoomIn, ZoomOut, RotateCcw, ArrowLeft, ArrowRight, MoreHorizontal, } from "lucide-react"; interface Annotation { id: string; type: "rectangle" | "circle" | "polygon"; label: string; color: string; coordinates: number[]; visible: boolean; } interface ImageAnnotationWorkspaceProps { task: any; currentFileIndex: number; onSaveAndNext: () => void; onSkipAndNext: () => void; } // 模拟医学图像数据 const mockMedicalImages = [ { id: "1", name: "2024-123456", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide1", url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02oi_e6dd5540-9ca4-4277-ad2b-4debaa1c8ddg.jpg-oibLbUmFpZMkLTmwZB7lT1UWKFlOLA.jpeg", }, { id: "2", name: "2024-234567", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide2", url: "/placeholder.svg?height=600&width=800&text=Medical Image 2", }, { id: "3", name: "2025-345678", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide3", url: "/placeholder.svg?height=600&width=800&text=Medical Image 3", }, { id: "4", name: "1234-123456", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide4", url: "/placeholder.svg?height=600&width=800&text=Medical Image 4", }, { id: "5", name: "2025-456789", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide5", url: "/placeholder.svg?height=600&width=800&text=Medical Image 5", }, { id: "6", name: "2025-567890", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide6", url: "/placeholder.svg?height=600&width=800&text=Medical Image 6", }, { id: "7", name: "2025-678901", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide7", url: "/placeholder.svg?height=600&width=800&text=Medical Image 7", }, { id: "8", name: "2025-789012", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide8", url: "/placeholder.svg?height=600&width=800&text=Medical Image 8", }, { id: "9", name: "2025-890123", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide9", url: "/placeholder.svg?height=600&width=800&text=Medical Image 9", }, { id: "10", name: "2025-901234", thumbnail: "/placeholder.svg?height=60&width=60&text=Slide10", url: "/placeholder.svg?height=600&width=800&text=Medical Image 10", }, ]; // 医学标注选项 const medicalAnnotationOptions = [ { id: "tumor_present", label: "是否有肿瘤", type: "radio", options: ["是", "否"], }, { id: "tumor_type", label: "肿瘤形成", type: "checkbox", options: ["腺管形成"], }, { id: "grade_1", label: "1级", type: "checkbox", options: ["1[x]"] }, { id: "grade_2", label: "2级", type: "checkbox", options: ["2[x]"] }, { id: "remarks", label: "备注", type: "textarea" }, { id: "nuclear_polymorphism", label: "核多形性", type: "checkbox", options: ["核分裂象"], }, { id: "histological_type", label: "组织学类型", type: "checkbox", options: ["1[b]", "2[y]", "3[t]"], }, { id: "small_time_lesion", label: "小时病位置[3]", type: "checkbox", options: ["1[b]", "2[y]", "3[t]"], }, { id: "ductal_position", label: "导管原位置[4]", type: "checkbox", options: ["1[o]", "2[p]", "3[t]"], }, { id: "ductal_position_large", label: "导管原位置件大于腺分", type: "checkbox", options: ["腺分裂象"], }, { id: "mitosis", label: "化[5]", type: "checkbox", options: ["1[o]", "2[p]", "3[t]"], }, { id: "original_position", label: "原位实性乳头状[6]", type: "checkbox", options: ["1[o]", "2[p]", "3[t]"], }, { id: "infiltrating_lesion", label: "浸润性病(非特殊型)[7]", type: "checkbox", options: ["1[o]", "2[p]", "3[t]"], }, { id: "infiltrating_small", label: "浸润性小叶癌[8]", type: "checkbox", options: ["脉管侵犯"], }, { id: "infiltrating_real", label: "浸润实性乳头状癌[9]", type: "checkbox", options: ["1[o]", "2[p]", "3[t]"], }, { id: "other_lesion", label: "其他病[0]", type: "checkbox", options: ["+[k]"], }, ]; export default function ImageAnnotationWorkspace({ currentFileIndex, }: ImageAnnotationWorkspaceProps) { const canvasRef = useRef(null); const [selectedImageIndex, setSelectedImageIndex] = useState( currentFileIndex || 0 ); const [currentImage, setCurrentImage] = useState( mockMedicalImages[selectedImageIndex] ); const [annotations, setAnnotations] = useState([]); const [selectedTool, setSelectedTool] = useState< "select" | "rectangle" | "circle" >("select"); const [isDrawing, setIsDrawing] = useState(false); const [startPoint, setStartPoint] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); const [pan, setPan] = useState({ x: 0, y: 0 }); const [selectedAnnotation, setSelectedAnnotation] = useState( null ); const [annotationValues, setAnnotationValues] = useState>( {} ); useEffect(() => { setCurrentImage(mockMedicalImages[selectedImageIndex]); drawCanvas(); }, [selectedImageIndex, annotations, zoom, pan]); const drawCanvas = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { ctx.save(); ctx.scale(zoom, zoom); ctx.translate(pan.x, pan.y); ctx.drawImage(img, 0, 0, canvas.width / zoom, canvas.height / zoom); // 绘制标注 annotations.forEach((annotation) => { if (!annotation.visible) return; ctx.strokeStyle = annotation.color; ctx.fillStyle = annotation.color + "20"; ctx.lineWidth = 2; if (annotation.type === "rectangle") { const [x, y, width, height] = annotation.coordinates; ctx.strokeRect(x, y, width, height); ctx.fillRect(x, y, width, height); } else if (annotation.type === "circle") { const [centerX, centerY, radius] = annotation.coordinates; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); ctx.stroke(); ctx.fill(); } if (selectedAnnotation === annotation.id) { ctx.strokeStyle = "#FF0000"; ctx.lineWidth = 3; ctx.setLineDash([5, 5]); if (annotation.type === "rectangle") { const [x, y, width, height] = annotation.coordinates; ctx.strokeRect(x - 2, y - 2, width + 4, height + 4); } else if (annotation.type === "circle") { const [centerX, centerY, radius] = annotation.coordinates; ctx.beginPath(); ctx.arc(centerX, centerY, radius + 2, 0, 2 * Math.PI); ctx.stroke(); } ctx.setLineDash([]); } }); ctx.restore(); }; img.src = currentImage.url; }; const handleCanvasMouseDown = (e: React.MouseEvent) => { const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left - pan.x) / zoom; const y = (e.clientY - rect.top - pan.y) / zoom; if (selectedTool === "rectangle" || selectedTool === "circle") { setIsDrawing(true); setStartPoint({ x, y }); } else if (selectedTool === "select") { const clickedAnnotation = annotations.find((annotation) => { if (annotation.type === "rectangle") { const [ax, ay, width, height] = annotation.coordinates; return x >= ax && x <= ax + width && y >= ay && y <= ay + height; } else if (annotation.type === "circle") { const [centerX, centerY, radius] = annotation.coordinates; const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2); return distance <= radius; } return false; }); setSelectedAnnotation(clickedAnnotation?.id || null); } }; const handleCanvasMouseMove = (e: React.MouseEvent) => { if (!isDrawing) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left - pan.x) / zoom; const y = (e.clientY - rect.top - pan.y) / zoom; drawCanvas(); const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.save(); ctx.scale(zoom, zoom); ctx.translate(pan.x, pan.y); ctx.strokeStyle = "#3B82F6"; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); if (selectedTool === "rectangle") { const width = x - startPoint.x; const height = y - startPoint.y; ctx.strokeRect(startPoint.x, startPoint.y, width, height); } else if (selectedTool === "circle") { const radius = Math.sqrt( (x - startPoint.x) ** 2 + (y - startPoint.y) ** 2 ); ctx.beginPath(); ctx.arc(startPoint.x, startPoint.y, radius, 0, 2 * Math.PI); ctx.stroke(); } ctx.restore(); }; const handleCanvasMouseUp = (e: React.MouseEvent) => { if (!isDrawing) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left - pan.x) / zoom; const y = (e.clientY - rect.top - pan.y) / zoom; let coordinates: number[] = []; if (selectedTool === "rectangle") { const width = x - startPoint.x; const height = y - startPoint.y; coordinates = [startPoint.x, startPoint.y, width, height]; } else if (selectedTool === "circle") { const radius = Math.sqrt( (x - startPoint.x) ** 2 + (y - startPoint.y) ** 2 ); coordinates = [startPoint.x, startPoint.y, radius]; } if (coordinates.length > 0) { const newAnnotation: Annotation = { id: Date.now().toString(), type: selectedTool as "rectangle" | "circle", label: "标注", color: "#3B82F6", coordinates, visible: true, }; setAnnotations([...annotations, newAnnotation]); } setIsDrawing(false); }; const handleAnnotationValueChange = (optionId: string, value: any) => { setAnnotationValues((prev) => ({ ...prev, [optionId]: value, })); }; const handleUpdate = () => { message({ title: "标注已更新", description: "医学标注信息已保存", }); }; return (
{/* Left Sidebar - Image List */}
{/* Header */}
image img
case_id #13754
DE
de #14803
11 days ago
{/* Image List */}
{mockMedicalImages.map((image, index) => (
setSelectedImageIndex(index)} >
{index + 1}
{`Slide
{image.name}
))}
{/* Main Content Area */}
{/* Main Image Display */}

WSI图像预览

病理号: 1234-123456
取材部位: 余乳
{/* Zoom Controls */}
{Math.round(zoom * 100)}%
{/* Tool Selection */}
{/* Navigation Controls */}
{/* Right Sidebar - Annotation Panel */}

标注

{medicalAnnotationOptions.map((option) => (
{option.label} {option.type === "radio" && (
{option.options?.map((opt) => (
handleAnnotationValueChange( option.id, e.target.value ) } className="w-4 h-4" /> {opt}
))}
)} {option.type === "checkbox" && (
{option.options?.map((opt) => (
handleAnnotationValueChange( `${option.id}_${opt}`, checked ) } /> {opt}
))}
)} {option.type === "textarea" && (