import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; interface ScreenshotSelectorProps { isSelectionMode: boolean; setIsSelectionMode: (mode: boolean) => void; containerRef: React.RefObject; } export const ScreenshotSelector = memo( ({ isSelectionMode, setIsSelectionMode, containerRef }: ScreenshotSelectorProps) => { const [isCapturing, setIsCapturing] = useState(false); const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null); const [selectionEnd, setSelectionEnd] = useState<{ x: number; y: number } | null>(null); const mediaStreamRef = useRef(null); const videoRef = useRef(null); const hasRequestedPermissionRef = useRef(false); // 清理流和视频资源的函数 const cleanupResources = useCallback(() => { if (videoRef.current) { videoRef.current.pause(); videoRef.current.srcObject = null; videoRef.current.remove(); videoRef.current = null; } if (mediaStreamRef.current) { mediaStreamRef.current.getTracks().forEach((track) => track.stop()); mediaStreamRef.current = null; } hasRequestedPermissionRef.current = false; }, []); // 当组件卸载时清理资源 useEffect(() => { return cleanupResources; }, [cleanupResources]); // 当选择模式关闭时清理资源 useEffect(() => { if (!isSelectionMode && mediaStreamRef.current) { cleanupResources(); } }, [isSelectionMode, cleanupResources]); const initializeStream = async () => { // 如果已经有流,直接返回 if (mediaStreamRef.current) { return mediaStreamRef.current; } // 如果已经请求了权限,等待并返回null(避免重复请求) if (hasRequestedPermissionRef.current) { return null; } hasRequestedPermissionRef.current = true; try { // 请求显示媒体权限 const stream = await navigator.mediaDevices.getDisplayMedia({ audio: false, video: { displaySurface: 'window', preferCurrentTab: true, surfaceSwitching: 'include', systemAudio: 'exclude', }, } as MediaStreamConstraints); // 添加流失效时的处理器 stream.addEventListener('inactive', () => { cleanupResources(); setIsSelectionMode(false); setSelectionStart(null); setSelectionEnd(null); setIsCapturing(false); }); mediaStreamRef.current = stream; // 初始化视频元素 if (!videoRef.current) { const video = document.createElement('video'); video.style.opacity = '0'; video.style.position = 'fixed'; video.style.pointerEvents = 'none'; video.style.zIndex = '-1'; document.body.appendChild(video); videoRef.current = video; } // 设置视频流并播放 videoRef.current.srcObject = stream; await videoRef.current.play(); return stream; } catch (error) { console.error('Failed to initialize stream:', error); setIsSelectionMode(false); toast.error('初始化屏幕捕获失败'); hasRequestedPermissionRef.current = false; return null; } }; const handleCopySelection = useCallback(async () => { if (!isSelectionMode || !selectionStart || !selectionEnd || !containerRef.current) { return; } setIsCapturing(true); try { const stream = await initializeStream(); if (!stream || !videoRef.current) { setIsCapturing(false); return; } // 等待视频准备好 await new Promise((resolve) => setTimeout(resolve, 300)); // 创建临时画布进行全屏截图 const tempCanvas = document.createElement('canvas'); tempCanvas.width = videoRef.current.videoWidth; tempCanvas.height = videoRef.current.videoHeight; const tempCtx = tempCanvas.getContext('2d'); if (!tempCtx) { throw new Error('无法获取临时画布上下文'); } // 绘制完整视频帧 tempCtx.drawImage(videoRef.current, 0, 0); // 计算视频和屏幕之间的比例因子 const scaleX = videoRef.current.videoWidth / window.innerWidth; const scaleY = videoRef.current.videoHeight / window.innerHeight; // 获取窗口滚动位置 const scrollX = window.scrollX; const scrollY = window.scrollY + 40; // 获取容器在页面上的位置 const containerRect = containerRef.current.getBoundingClientRect(); // 精确剪裁的偏移调整 const leftOffset = -9; // 调整左侧位置 const bottomOffset = -14; // 调整底部位置 // 计算缩放后的坐标(带滚动偏移和调整) const scaledX = Math.round( (containerRect.left + Math.min(selectionStart.x, selectionEnd.x) + scrollX + leftOffset) * scaleX, ); const scaledY = Math.round( (containerRect.top + Math.min(selectionStart.y, selectionEnd.y) + scrollY + bottomOffset) * scaleY, ); const scaledWidth = Math.round(Math.abs(selectionEnd.x - selectionStart.x) * scaleX); const scaledHeight = Math.round(Math.abs(selectionEnd.y - selectionStart.y) * scaleY); // 为裁剪区域创建最终画布 const canvas = document.createElement('canvas'); canvas.width = Math.round(Math.abs(selectionEnd.x - selectionStart.x)); canvas.height = Math.round(Math.abs(selectionEnd.y - selectionStart.y)); const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('无法获取画布上下文'); } // 绘制裁剪区域 ctx.drawImage(tempCanvas, scaledX, scaledY, scaledWidth, scaledHeight, 0, 0, canvas.width, canvas.height); // 转换为 blob const blob = await new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error('无法创建 blob')); } }, 'image/png'); }); // 创建 FileReader 将 blob 转换为 base64 const reader = new FileReader(); reader.onload = () => { // 查找 textarea 元素 const textarea = document.querySelector('textarea'); if (textarea) { // 从 BaseChat 组件获取 setters const setUploadedFiles = (window as any).__UPAGE_SET_UPLOADED_FILES__; const uploadedFiles = (window as any).__UPAGE_UPLOADED_FILES__ || []; if (setUploadedFiles) { // 更新文件和图像数据 const file = new File([blob], 'screenshot.png', { type: 'image/png' }); setUploadedFiles([...uploadedFiles, file]); toast.success('截图已添加到聊天中'); } else { toast.error('无法将截图添加到聊天中'); } } }; reader.readAsDataURL(blob); } catch (error) { console.error('Failed to capture screenshot:', error); toast.error('截图失败'); cleanupResources(); } finally { setIsCapturing(false); setSelectionStart(null); setSelectionEnd(null); setIsSelectionMode(false); // 捕获后关闭选择模式 } }, [isSelectionMode, selectionStart, selectionEnd, containerRef, setIsSelectionMode, cleanupResources]); const handleSelectionStart = useCallback( (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!isSelectionMode) { return; } const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setSelectionStart({ x, y }); setSelectionEnd({ x, y }); }, [isSelectionMode], ); const handleSelectionMove = useCallback( (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!isSelectionMode || !selectionStart) { return; } const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setSelectionEnd({ x, y }); }, [isSelectionMode, selectionStart], ); if (!isSelectionMode) { return null; } return (
{ if (selectionStart) { setSelectionStart(null); } }} style={{ backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)', userSelect: 'none', WebkitUserSelect: 'none', pointerEvents: 'all', opacity: isCapturing ? 0 : 1, zIndex: 50, transition: 'opacity 0.1s ease-in-out', }} > {selectionStart && selectionEnd && !isCapturing && (
)}
); }, );