import { useState, useRef, useEffect, useCallback } from "react"; import { useNavigate } from "react-router"; import { motion, AnimatePresence } from "motion/react"; import { Camera, X, Check, RotateCw, Zap, Image as ImageIcon, AlertCircle, Loader2, Plus, ChevronRight, Trash2, } from "lucide-react"; import { Button } from "../components/ui/button"; import { uploadReportImage, addImageToReport, type UploadReportResponse, type OcrDataItem, } from "../services/api"; /** 压缩图片:限制最大宽度 1200px,JPEG 质量 0.7 */ function compressImage(file: File, maxWidth = 1200, quality = 0.7): Promise { return new Promise((resolve) => { const img = new Image(); img.onload = () => { let { width, height } = img; if (width > maxWidth) { height = Math.round((height * maxWidth) / width); width = maxWidth; } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d")!; ctx.drawImage(img, 0, 0, width, height); canvas.toBlob( (blob) => { resolve(blob ? new File([blob], "report.jpg", { type: "image/jpeg" }) : file); }, "image/jpeg", quality ); }; img.src = URL.createObjectURL(file); }); } interface ImageEntry { id: string; preview: string; // data URL for thumbnail status: "uploading" | "done" | "error"; error?: string; ocrItems: OcrDataItem[]; reportType: string; } export function CameraCapturePage() { const navigate = useNavigate(); // Multi-image state const [images, setImages] = useState([]); const [reportId, setReportId] = useState(null); const [notPregnancyTip, setNotPregnancyTip] = useState(null); // Camera state const [isCapturing, setIsCapturing] = useState(true); // show camera view const [showTips, setShowTips] = useState(true); const [cameraError, setCameraError] = useState(null); const [cameraReady, setCameraReady] = useState(false); const videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); const fileInputRef = useRef(null); // 启动摄像头 const startCamera = useCallback(async () => { try { setCameraError(null); if (!window.isSecureContext) { setCameraError("摄像头需要 HTTPS 安全连接,请使用「从相册选择」"); return; } if (!navigator.mediaDevices?.getUserMedia) { setCameraError("当前浏览器不支持摄像头,请使用相册上传"); return; } const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment", width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false, }); streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; videoRef.current.onloadedmetadata = () => { videoRef.current?.play(); setCameraReady(true); }; } } catch { setCameraError("无法访问摄像头,请检查权限设置或使用相册上传"); } }, []); const stopCamera = useCallback(() => { if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; } setCameraReady(false); }, []); useEffect(() => { if (isCapturing) startCamera(); return () => stopCamera(); }, [isCapturing, startCamera, stopCamera]); // 上传单张图片(自动判断是首张还是追加) const uploadImage = useCallback( async (file: File, preview: string) => { const entryId = Date.now().toString(); const entry: ImageEntry = { id: entryId, preview, status: "uploading", ocrItems: [], reportType: "", }; setImages((prev) => [...prev, entry]); try { const compressed = await compressImage(file); let ocrItems: OcrDataItem[] = []; let reportType = ""; if (!reportId) { // First image — create report const result: UploadReportResponse = await uploadReportImage(compressed); setReportId(result.report_id); ocrItems = result.ocr_result.data_items; reportType = result.ocr_result.report_type; } else { // Additional image — add to existing report const result = await addImageToReport(reportId, compressed); ocrItems = result.ocr_result.data_items; reportType = result.ocr_result.report_type; } setImages((prev) => prev.map((img) => img.id === entryId ? { ...img, status: "done", ocrItems, reportType } : img ) ); } catch (err: unknown) { const msg = err instanceof Error ? err.message : "识别失败"; // 非孕期报告提示 if (msg.includes("请上传孕期检查报告") || msg.includes("请上传孕期")) { setNotPregnancyTip(msg); // 移除该图片 setImages((prev) => prev.filter((img) => img.id !== entryId)); return; } const friendlyMsg = /load failed|failed to fetch|networkerror/i.test(msg) ? "网络连接失败" : msg.length > 100 ? "AI 识别服务暂时不可用" : msg; setImages((prev) => prev.map((img) => img.id === entryId ? { ...img, status: "error", error: friendlyMsg } : img ) ); } }, [reportId] ); // 拍照 const handleCapture = () => { if (!videoRef.current || !canvasRef.current) return; const video = videoRef.current; const canvas = canvasRef.current; canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.drawImage(video, 0, 0); const dataUrl = canvas.toDataURL("image/jpeg", 0.9); setShowTips(false); canvas.toBlob((blob) => { if (blob) { const file = new File([blob], "report.jpg", { type: "image/jpeg" }); uploadImage(file, dataUrl); } }, "image/jpeg", 0.9); }; // 相册选择(支持多选) const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files; if (!files?.length) return; setShowTips(false); Array.from(files).forEach((file) => { const reader = new FileReader(); reader.onload = (ev) => { const preview = ev.target?.result as string; uploadImage(file, preview); }; reader.readAsDataURL(file); }); e.target.value = ""; }; // 删除某张图片(仅从前端列表移除,不影响已上传的数据) const removeImage = (id: string) => { setImages((prev) => prev.filter((img) => img.id !== id)); }; // 统计 const doneCount = images.filter((img) => img.status === "done").length; const uploadingCount = images.filter((img) => img.status === "uploading").length; const totalOcrItems = images.reduce((sum, img) => sum + img.ocrItems.length, 0); const allDone = images.length > 0 && uploadingCount === 0; // 进入校验页面 const handleGoToVerify = () => { if (!reportId || !allDone) return; // Merge all OCR items for navigation state const mergedItems = images.flatMap((img) => img.ocrItems); const reportTypes = [...new Set(images.map((img) => img.reportType).filter(Boolean))]; navigate("/verify", { state: { reportId, ocrResult: { report_type: reportTypes.join("+"), data_items: mergedItems, patient_name: "", }, imageUrls: images.map((img) => img.preview), }, }); }; const handleAlbumClick = () => { fileInputRef.current?.click(); }; return (
{/* 顶部导航栏 */}

拍摄检查报告

{/* 相机预览区域 */}