ImagePreviewModal.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /**
  2. * 图片预览模态框组件
  3. *
  4. * 支持点击图片放大查看,带缩放、拖拽功能
  5. * 支持直接渲染 React 组件(用于显示 canvas 内容)
  6. */
  7. import React, { useState, useRef, useEffect } from 'react';
  8. import { X, RotateCw, Download } from 'lucide-react';
  9. interface ImagePreviewModalProps {
  10. imageUrl?: string;
  11. isOpen: boolean;
  12. onClose: () => void;
  13. title?: string;
  14. downloadFileName?: string;
  15. renderContent?: () => React.ReactNode; // 自定义渲染内容
  16. }
  17. const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
  18. imageUrl,
  19. isOpen,
  20. onClose,
  21. title = '图片预览',
  22. downloadFileName = 'image.png',
  23. renderContent
  24. }) => {
  25. const [scale, setScale] = useState(1);
  26. const [rotation, setRotation] = useState(0);
  27. const [position, setPosition] = useState({ x: 0, y: 0 });
  28. const [isDragging, setIsDragging] = useState(false);
  29. const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
  30. const imageRef = useRef<HTMLDivElement>(null);
  31. // 重置状态
  32. useEffect(() => {
  33. if (isOpen) {
  34. setScale(1);
  35. setRotation(0);
  36. setPosition({ x: 0, y: 0 });
  37. }
  38. }, [isOpen]);
  39. // ESC 键关闭
  40. useEffect(() => {
  41. const handleEsc = (e: KeyboardEvent) => {
  42. if (e.key === 'Escape' && isOpen) {
  43. onClose();
  44. }
  45. };
  46. window.addEventListener('keydown', handleEsc);
  47. return () => window.removeEventListener('keydown', handleEsc);
  48. }, [isOpen, onClose]);
  49. // 阻止背景滚动
  50. useEffect(() => {
  51. if (isOpen) {
  52. document.body.style.overflow = 'hidden';
  53. } else {
  54. document.body.style.overflow = '';
  55. }
  56. return () => {
  57. document.body.style.overflow = '';
  58. };
  59. }, [isOpen]);
  60. if (!isOpen) return null;
  61. // 缩放控制
  62. const handleZoomIn = () => setScale(prev => Math.min(prev + 0.25, 5));
  63. const handleZoomOut = () => setScale(prev => Math.max(prev - 0.25, 0.25));
  64. const handleResetZoom = () => {
  65. setScale(1);
  66. setPosition({ x: 0, y: 0 });
  67. };
  68. // 旋转控制
  69. const handleRotate = () => setRotation(prev => (prev + 90) % 360);
  70. // 下载图片
  71. const handleDownload = () => {
  72. if (!imageUrl) return;
  73. const link = document.createElement('a');
  74. link.href = imageUrl;
  75. link.download = downloadFileName;
  76. link.target = '_blank';
  77. document.body.appendChild(link);
  78. link.click();
  79. document.body.removeChild(link);
  80. };
  81. // 鼠标拖拽
  82. const handleMouseDown = (e: React.MouseEvent) => {
  83. if (scale > 1) {
  84. setIsDragging(true);
  85. setDragStart({
  86. x: e.clientX - position.x,
  87. y: e.clientY - position.y
  88. });
  89. }
  90. };
  91. const handleMouseMove = (e: React.MouseEvent) => {
  92. if (isDragging && scale > 1) {
  93. setPosition({
  94. x: e.clientX - dragStart.x,
  95. y: e.clientY - dragStart.y
  96. });
  97. }
  98. };
  99. const handleMouseUp = () => {
  100. setIsDragging(false);
  101. };
  102. // 鼠标滚轮缩放
  103. const handleWheel = (e: React.WheelEvent) => {
  104. e.preventDefault();
  105. if (e.deltaY < 0) {
  106. handleZoomIn();
  107. } else {
  108. handleZoomOut();
  109. }
  110. };
  111. return (
  112. <div
  113. className="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
  114. onClick={onClose}
  115. >
  116. {/* 工具栏 */}
  117. <div className="absolute top-0 left-0 right-0 bg-black/50 backdrop-blur-sm p-4 flex items-center justify-between z-10">
  118. <h3 className="text-white font-medium">{title}</h3>
  119. <div className="flex items-center space-x-2">
  120. {/* 旋转 */}
  121. <button
  122. onClick={(e) => {
  123. e.stopPropagation();
  124. handleRotate();
  125. }}
  126. className="p-2 text-white hover:bg-white/20 rounded-lg transition-colors"
  127. title="旋转"
  128. >
  129. <RotateCw className="w-5 h-5" />
  130. </button>
  131. {/* 下载 */}
  132. {imageUrl && (
  133. <button
  134. onClick={(e) => {
  135. e.stopPropagation();
  136. handleDownload();
  137. }}
  138. className="p-2 text-white hover:bg-white/20 rounded-lg transition-colors"
  139. title="下载"
  140. >
  141. <Download className="w-5 h-5" />
  142. </button>
  143. )}
  144. {/* 关闭 */}
  145. <button
  146. onClick={(e) => {
  147. e.stopPropagation();
  148. onClose();
  149. }}
  150. className="p-2 text-white hover:bg-white/20 rounded-lg transition-colors"
  151. title="关闭 (ESC)"
  152. >
  153. <X className="w-5 h-5" />
  154. </button>
  155. </div>
  156. </div>
  157. {/* 图片容器 */}
  158. <div
  159. ref={imageRef}
  160. className="relative w-full h-full flex items-center justify-center overflow-hidden"
  161. onClick={(e) => e.stopPropagation()}
  162. onMouseDown={handleMouseDown}
  163. onMouseMove={handleMouseMove}
  164. onMouseUp={handleMouseUp}
  165. onMouseLeave={handleMouseUp}
  166. onWheel={handleWheel}
  167. style={{
  168. cursor: scale > 1 ? (isDragging ? 'grabbing' : 'grab') : 'default'
  169. }}
  170. >
  171. {renderContent ? (
  172. // 自定义内容渲染(用于 canvas 等)
  173. <div
  174. style={{
  175. transform: `translate(${position.x}px, ${position.y}px) scale(${scale}) rotate(${rotation}deg)`,
  176. transition: isDragging ? 'none' : 'transform 0.2s ease-out',
  177. maxHeight: '90vh',
  178. maxWidth: '90vw'
  179. }}
  180. >
  181. {renderContent()}
  182. </div>
  183. ) : imageUrl ? (
  184. // 普通图片渲染
  185. <img
  186. src={imageUrl}
  187. alt="预览"
  188. className="max-w-none select-none"
  189. draggable={false}
  190. style={{
  191. transform: `translate(${position.x}px, ${position.y}px) scale(${scale}) rotate(${rotation}deg)`,
  192. transition: isDragging ? 'none' : 'transform 0.2s ease-out',
  193. maxHeight: '90vh',
  194. maxWidth: '90vw'
  195. }}
  196. />
  197. ) : null}
  198. </div>
  199. </div>
  200. );
  201. };
  202. export default ImagePreviewModal;