Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | /** * ScreenshotPreview Component * * Displays browser screenshots with zoom and fullscreen capabilities. */ import React from 'react'; import { Expand, RefreshCw, Loader2, Camera, X } from 'lucide-react'; import { cn } from '../../lib/utils'; interface ScreenshotPreviewProps { /** Base64 encoded screenshot data */ base64: string | null; /** Loading state */ isLoading?: boolean; /** Callback when refresh is requested */ onRefresh?: () => void; /** Callback when clicked (for fullscreen) */ onClick?: () => void; /** Alt text when no screenshot */ altText?: string; /** Container class name */ className?: string; } export function ScreenshotPreview({ base64, isLoading = false, onRefresh, onClick, altText = '等待截图', className = '', }: ScreenshotPreviewProps) { const [isFullscreen, setIsFullscreen] = React.useState(false); // Handle keyboard shortcut for fullscreen toggle React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isFullscreen) { setIsFullscreen(false); } }; if (isFullscreen) { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); } }, [isFullscreen]); if (!base64 && !isLoading) { return ( <div className={cn( 'flex items-center justify-center h-48 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 border-dashed', className )} > <Camera className="h-8 w-8 text-gray-400" /> <p className="mt-2 text-sm text-gray-400">{altText}</p> </div> ); } const handleClick = () => { if (onClick) { onClick(); } setIsFullscreen(true); }; return ( <div className={cn( 'relative group', isFullscreen && 'fixed inset-0 z-50 bg-black/90 flex items-center justify-center' )} > {/* Loading overlay */} {isLoading && ( <div className="absolute inset-0 bg-black/50 flex items-center justify-center z-20"> <Loader2 className="h-8 w-8 text-white animate-spin" /> </div> )} {/* Toolbar */} <div className={cn( 'absolute top-2 right-2 flex items-center gap-2 z-10', isFullscreen && 'bg-black/80 rounded-lg p-1' )} > {onRefresh && ( <button onClick={onRefresh} className="p-1.5 rounded-md bg-black/60 hover:bg-black/70 transition-colors text-white" title="刷新截图" > <RefreshCw className="h-4 w-4" /> </button> )} <button onClick={handleClick} className="p-1.5 rounded-md bg-black/60 hover:bg-black/70 transition-colors text-white" title="全屏查看" > <Expand className="h-4 w-4" /> </button> </div> {/* Screenshot image */} <div className={cn( 'w-full h-full overflow-auto bg-gray-900 rounded-lg cursor-pointer' )} onClick={handleClick} > <img src={`data:image/png;base64,${base64}`} alt="Browser screenshot" className={cn( 'max-w-full max-h-full object-contain transition-transform duration-200', isFullscreen ? 'scale-150' : 'scale-100' )} draggable={false} /> </div> {/* Fullscreen modal */} {isFullscreen && ( <div className="fixed inset-0 z-50 bg-black/95 flex items-center justify-center p-4" onClick={() => setIsFullscreen(false)} > <img src={`data:image/png;base64,${base64}`} alt="Browser screenshot fullscreen" className="max-h-[85vh] max-w-[85vw] object-contain shadow-2xl" onClick={(e) => e.stopPropagation()} /> <button onClick={() => setIsFullscreen(false)} className="absolute top-4 right-4 p-2 rounded-full bg-black/60 hover:bg-black/70 transition-colors text-white" > <X className="h-4 w-4" /> </button> </div> )} </div> ); } |