import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Code, Download, Printer, Trash2 } from 'lucide-react'; import { useLazyTranslation } from '@/utils/useLazyTranslation'; import PageHeader from '@/components/PageHeader'; import CopyButton from '@/components/CopyButton'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import { Button } from '@/components/ui/button'; // 💡 1. 全面回归原子组件规范 import { useStorageState } from '@/utils/useStorageState'; import type { MarkdownToHtmlPreviewMode } from '@/types/storage'; import { downloadHtmlFile, markdownToHtml, printHtml, SAMPLE_MARKDOWN, wrapHtmlDocument, } from '@/utils/markdownToHtml'; import { cn } from '@/lib/utils'; const isValidPreviewMode = (val: unknown): val is MarkdownToHtmlPreviewMode => typeof val === 'string' && ['split', 'preview', 'html'].includes(val); // 💡 2. 样式超进化:将实色系全部改为标准 rgba 通道,确保在 iframe 内部能跟随宿主主题自适应混色 const PREVIEW_STYLES = ` .markdown-body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: var(--md-foreground, #1f2328); background-color: transparent; } .markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; } .markdown-body h1 { font-size: 1.6em; border-bottom: 1px solid var(--md-border, rgba(128,128,128,0.2)); padding-bottom: 0.3em; } .markdown-body h2 { font-size: 1.35em; border-bottom: 1px solid var(--md-border, rgba(128,128,128,0.2)); padding-bottom: 0.3em; } .markdown-body p { margin-top: 0; margin-bottom: 16px; font-size: 14px; } .markdown-body a { color: #2563eb; text-decoration: none; } .markdown-body a:hover { text-decoration: underline; } .markdown-body code { background-color: var(--md-code-bg, rgba(128,128,128,0.08)); border-radius: 4px; font-size: 85%; padding: 0.2em 0.4em; font-family: Menlo, Consolas, monospace; } .markdown-body pre { background-color: var(--md-pre-bg, rgba(128,128,128,0.05)); border-radius: 8px; font-size: 85%; line-height: 1.45; overflow: auto; padding: 16px; margin: 0 0 16px; border: 1px solid var(--md-border, rgba(128,128,128,0.15)); } .markdown-body pre code { background-color: transparent; border: 0; padding: 0; } .markdown-body blockquote { border-left: 0.25em solid var(--md-quote-line, rgba(128,128,128,0.3)); color: var(--md-muted, rgba(128,128,128,0.6)); margin: 0 0 16px; padding: 0 1em; } .markdown-body table { border-collapse: collapse; width: 100%; margin-bottom: 16px; font-size: 13px; } .markdown-body table th, .markdown-body table td { border: 1px solid var(--md-border, rgba(128,128,128,0.2)); padding: 6px 13px; } .markdown-body table tr:nth-child(2n) { background-color: var(--md-code-bg, rgba(128,128,128,0.03)); } .markdown-body table th { font-weight: 600; background-color: var(--md-code-bg, rgba(128,128,128,0.05)); } `; export default function MarkdownToHtmlPage() { const { t } = useLazyTranslation('markdownToHtml'); const [previewMode, setPreviewMode] = useStorageState( 'markdownToHtml/previewMode', 'split' as MarkdownToHtmlPreviewMode, isValidPreviewMode, ); const [markdown, setMarkdown] = useState(SAMPLE_MARKDOWN); const iframeRef = useRef(null); const result = useMemo(() => markdownToHtml(markdown), [markdown]); const error = result.hasError ? (result.error ?? null) : null; // 💡 3. 沙箱暗黑模式自适应守卫(Sandbox Dark Mode Synchronizer): // 根据宿主最外层的 HTML 是否包含 .dark 类名,动态向 iframe 注入对应的明暗模式 CSS 变量色轴 useEffect(() => { const iframe = iframeRef.current; if (!iframe) return; const isDarkMode = document.documentElement.classList.contains('dark'); const themeVariables = isDarkMode ? `:root { --md-foreground: #e6edf3; --md-border: rgba(255,255,255,0.15); --md-code-bg: rgba(255,255,255,0.1); --md-pre-bg: rgba(255,255,255,0.05); --md-muted: #8b949e; }` : `:root { --md-foreground: #1f2328; --md-border: rgba(128,128,128,0.2); --md-code-bg: rgba(128,128,128,0.08); --md-pre-bg: rgba(128,128,128,0.04); --md-muted: #636c76; }`; iframe.srcdoc = ` ${result.html} `; }, [result.html, previewMode]); // 当切换预览模式或 HTML 改变时,自发刷新沙箱 const handleModeChange = useCallback( (newMode: MarkdownToHtmlPreviewMode) => { setPreviewMode(newMode); }, [setPreviewMode], ); const handleClear = useCallback(() => { setMarkdown(''); }, []); const handlePrint = useCallback(() => { printHtml(result.html, t('pageTitle')); }, [result.html, t]); const handleDownload = useCallback(() => { const doc = wrapHtmlDocument(result.html, t('pageTitle')); downloadHtmlFile(doc, 'markdown-export.html'); }, [result.html, t]); const showInput = previewMode !== 'preview'; const showPreview = previewMode !== 'html'; return (
} />
{/* 工具集成控制护栏 */}
{/* 错误拦截提示框 */} {error && (
{error}
)} {/* 主框架多栏联动排版轴 */}
{/* Markdown 输入翼终端 */} {showInput && (
{t('inputLabel')} {t('charCount', { count: markdown.length })}