diff --git a/src/components/ui/HtmlPreviewModal.tsx b/src/components/ui/HtmlPreviewModal.tsx new file mode 100644 index 0000000..a699f97 --- /dev/null +++ b/src/components/ui/HtmlPreviewModal.tsx @@ -0,0 +1,392 @@ +'use client'; + +import { useState, useCallback, useEffect, useRef } from 'react'; +import { + X, + Monitor, + Tablet, + Smartphone, + Maximize2, + Minimize2, + RefreshCw, + Code, + Eye, + Copy, + Check, + Download, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { CodeBlock } from '@/components/markdown/CodeBlock'; + +interface HtmlPreviewModalProps { + /** HTML 代码内容 */ + htmlCode: string; + /** 是否打开 */ + isOpen: boolean; + /** 关闭回调 */ + onClose: () => void; + /** 语言类型,默认 html */ + language?: string; +} + +// 设备尺寸配置 +type DeviceType = 'desktop' | 'tablet' | 'mobile'; + +interface DeviceConfig { + type: DeviceType; + label: string; + icon: React.ComponentType<{ size?: number; className?: string }>; + width: string; + frameClass: string; +} + +const deviceConfigs: DeviceConfig[] = [ + { + type: 'desktop', + label: '桌面端', + icon: Monitor, + width: '100%', + frameClass: 'rounded', + }, + { + type: 'tablet', + label: '平板', + icon: Tablet, + width: '768px', + frameClass: 'rounded-lg', + }, + { + type: 'mobile', + label: '手机', + icon: Smartphone, + width: '375px', + frameClass: 'rounded-xl', + }, +]; + +// 视图模式 +type ViewMode = 'preview' | 'code'; + +export function HtmlPreviewModal({ + htmlCode, + isOpen, + onClose, + language = 'html', +}: HtmlPreviewModalProps) { + const [device, setDevice] = useState('desktop'); + const [viewMode, setViewMode] = useState('preview'); + const [isFullscreen, setIsFullscreen] = useState(false); + const [copied, setCopied] = useState(false); + const [iframeKey, setIframeKey] = useState(0); // 用于刷新 iframe + const modalRef = useRef(null); + + // 获取当前设备配置 + const currentDevice = deviceConfigs.find((d) => d.type === device) || deviceConfigs[0]; + + // ESC 关闭 / F11 全屏切换 + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + if (isFullscreen) { + setIsFullscreen(false); + } else { + onClose(); + } + } + // F11 切换全屏 + if (e.key === 'F11') { + e.preventDefault(); + setIsFullscreen((prev) => !prev); + } + }; + + window.document.addEventListener('keydown', handleKeyDown); + window.document.body.style.overflow = 'hidden'; + + return () => { + window.document.removeEventListener('keydown', handleKeyDown); + window.document.body.style.overflow = ''; + }; + }, [isOpen, isFullscreen, onClose]); + + // 复制代码 + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(htmlCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error('Failed to copy:', error); + } + }, [htmlCode]); + + // 下载 HTML 文件 + const handleDownload = useCallback(() => { + const blob = new Blob([htmlCode], { type: 'text/html;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const link = window.document.createElement('a'); + link.href = url; + link.download = 'preview.html'; + window.document.body.appendChild(link); + link.click(); + window.document.body.removeChild(link); + URL.revokeObjectURL(url); + }, [htmlCode]); + + // 刷新预览 + const handleRefresh = useCallback(() => { + setIframeKey((prev) => prev + 1); + }, []); + + // 切换全屏 + const toggleFullscreen = useCallback(() => { + setIsFullscreen((prev) => !prev); + }, []); + + if (!isOpen) return null; + + return ( +
+ {/* 主容器 */} +
e.stopPropagation()} + > + {/* 头部工具栏 */} +
+ {/* 左侧:标题和视图切换 */} +
+ {/* 标题 */} +
+ + + HTML 预览 + +
+ + {/* 视图切换 Tab */} +
+ + +
+
+ + {/* 中间:设备切换器(仅预览模式显示) */} + {viewMode === 'preview' && ( +
+ {deviceConfigs.map((config) => { + const Icon = config.icon; + return ( + + ); + })} +
+ )} + + {/* 右侧:操作按钮 */} +
+ {/* 复制按钮 */} + + + {/* 下载按钮 */} + + + {/* 分隔线 */} +
+ + {/* 刷新按钮(仅预览模式) */} + {viewMode === 'preview' && ( + + )} + + {/* 全屏切换按钮 */} + + + {/* 关闭按钮 */} + +
+
+ + {/* 内容区域 */} +
+ {viewMode === 'preview' ? ( + /* 预览模式 */ +
+ {/* 设备框架 */} +
+ {/* 手机顶部刘海 */} + {device === 'mobile' && ( +
+
+
+ )} + + {/* iframe 预览 */} +