ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

流式会话导出为 Markdown/PDF 的样式统一化

流式会话导出为 Markdown/PDF 的样式统一化 流式会话导出为 Markdown/PDF 的样式统一化在 AI 知识库与智能对话产品中“会话导出”是一个高频使用的核心功能。很多团队在实现导出时往往采用两套完全割裂的逻辑网页上用 React Markdown 组件配合 highlight.js 渲染流式气泡点击“导出 Markdown”时直接把原始字符串拼装下载点击“导出 PDF”时则调用window.print()或引入html2canvas强行将 DOM 截图转为 PDF。这种粗放的实现方式会引发严重的排版与体验灾难html2canvas生成的 PDF 本质是位图文字无法选中、无法检索在视网膜屏幕或打印时边缘模糊发虚长表格与代码块在跨页处被生硬腰斩一行代码上一半在第一页下一半在第二页网页端的主题色、背景与字体在打印机纸张A4/Letter上呈现暗色大色块耗费墨水且对比度崩塌。构建一套企业级的流式会话导出系统必须从 AST抽象语法树同构转换、CSS Paged Media 打印分页控制以及离线矢量 PDF 生成三大维度进行系统性治理。统一源头统一基于 Markdown ASTmdast的多端发射器无论是网页端流式显示、导出标准 Markdown 文本还是生成打印级 PDF数据源必须统一收敛到Markdown AST (mdast)。我们采用remark体系构建了多端发射器架构import { unified } from unified; import remarkParse from remark-parse; import remarkGfm from remark-gfm; import remarkMath from remark-math; import remarkStringify from remark-stringify; import type { Node, Root } from mdast; export class ChatExportEmitter { private parser: any; constructor() { this.parser unified() .use(remarkParse) .use(remarkGfm) .use(remarkMath); } /** * 将会话消息列表格式化为标准 Markdown 纯文本 */ public exportToMarkdown(messages: Array{ role: string; content: string; createdAt: number }): string { const astTree: Root { type: root, children: [], }; messages.forEach((msg) { const headingText msg.role user ? 用户提问 : AI 助手回答; const timeStr new Date(msg.createdAt).toLocaleString(); // 注入会话分段标题 astTree.children.push({ type: heading, depth: 3, children: [{ type: text, value: ${headingText} (${timeStr}) }], }); // 解析单条消息正文并挂载到树中 const msgAst this.parser.parse(msg.content); astTree.children.push(...msgAst.children); // 分隔线 astTree.children.push({ type: thematicBreak }); }); // 序列化回标准 Markdown 字符串 const stringifier unified().use(remarkStringify).use(remarkGfm); return stringifier.stringify(astTree); } }打印样式隔离与 CSS Paged Media 规范直接调用浏览器的window.print()生成 PDF 是生成高保真、可选中矢量文档最轻量高效的途径。但必须通过media print媒体查询彻底重置屏幕端的 UI 布局适配 A4 纸张的物理版心。1. 消除暗色背景与重置排版尺寸打印场景必须强制切换为白底黑字、高对比度排版并重构字体大小使用pt物理单位media print { /* 隐藏非正文无关 UI */ .chat-sidebar, .chat-input-bar, .action-buttons, .no-print { display: none !important; } /* 设置 A4 纸张尺寸与边距 */ page { size: A4 portrait; margin: 20mm 15mm 20mm 15mm; /* 注入页眉页脚 */ top-right { content: AI 知识库会话导出报告; font-size: 8pt; color: #666; } bottom-center { content: 第 counter(page) 页 / 共 counter(pages) 页; font-size: 8pt; color: #666; } } body { background: #ffffff !important; color: #111111 !important; font-size: 10.5pt; /* 对应五号字适合纸张阅读 */ line-height: 1.6; font-family: PingFang SC, Songti SC, serif; } /* 消除屏幕端阴影与边框 */ .message-bubble { border: none !important; box-shadow: none !important; background: transparent !important; padding: 0 !important; margin-bottom: 24pt !important; } }2. 防跨页腰斩控制Page Break Avoidance代码块precode、数据表格table、公式块KaTeX / MathJax以及单条问答卡片必须严禁在中间发生物理分页media print { /* 避免代码块与表格在跨页时被横向切断 */ pre, table, blockquote, .katex-display, .message-turn-container { break-inside: avoid; page-break-inside: avoid; } /* 标题后严禁立刻分页避免标题孤立在上一页页底 */ h1, h2, h3, h4 { break-after: avoid; page-break-after: avoid; } /* 代码块在纸张上的紧凑排版 */ pre { background-color: #f6f8fa !important; border: 1px solid #d0d7de !important; border-radius: 4px; padding: 8pt; white-space: pre-wrap !important; /* 强制换行严禁横向溢出纸张 */ word-break: break-all; font-size: 9pt; } }纯前端无弹窗的隐藏 IFrame 打印触发器在实际交互中如果直接在主窗口调用window.print()会导致宿主页面发生重排Reflow用户体验极为突兀。优雅的实现方式是在后台创建一个不可见的iframe将同构生成的纯净 HTML 与 Print CSS 写入iframe中并在其就绪后调起打印窗口export function printSessionContent(htmlContent: string, title 会话导出) { const iframe document.createElement(iframe); iframe.style.position fixed; iframe.style.right 0; iframe.style.bottom 0; iframe.style.width 0; iframe.style.height 0; iframe.style.border 0; document.body.appendChild(iframe); const doc iframe.contentWindow?.document; if (!doc) return; doc.open(); doc.write( !DOCTYPE html html head title${title}/title link relstylesheet href/static/css/print-theme.css / link relstylesheet href/static/css/katex.min.css / /head body div classprint-container ${htmlContent} /div /body /html ); doc.close(); // 等待样式与字体加载完毕后调起打印 iframe.contentWindow?.addEventListener(load, () { iframe.contentWindow?.focus(); iframe.contentWindow?.print(); // 延迟销毁 iframe 释放内存 setTimeout(() { document.body.removeChild(iframe); }, 1000); }); }通过将 AST 解析、排版媒体查询与沙箱打印流程收敛为统一的工程链路导出的 Markdown 语法规范、PDF 纸张版面如墨宝成卷真正实现了数字会话向永久归档载体的高质量流转。
返回列表