(function(window, document) { const KATEX_CDN = 'https://gcore.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css'; const EXPORT_LABELS = { original: '原格式', html: 'HTML', pdf: 'PDF', docx: 'DOCX', markdown: 'Markdown' }; const MODE_LABELS = { 'ocr': '仅OCR', 'translation': '仅翻译', 'chunk-compare': '分块对比' }; const BRAND_LINK = 'https://github.com/Feather-2/paper-burner-x'; const exportState = { common: { includeBranding: true }, pdf: { paper: 'A4', marginTop: 20, marginBottom: 20, marginLeft: 15, marginRight: 15, scalePercent: 100 }, markdown: { embedImages: false } }; let currentFormat = null; const PDF_PAPER_DIMENSIONS = { A4: { width: 210, height: 297 }, A3: { width: 297, height: 420 }, A2: { width: 420, height: 594 } }; document.addEventListener('DOMContentLoaded', function() { const controls = document.getElementById('history-export-controls'); const trigger = document.getElementById('exportTrigger'); const panel = document.getElementById('exportPanel'); const closeBtn = document.getElementById('exportPanelClose'); const backdrop = document.getElementById('exportPanelBackdrop'); const configContent = document.getElementById('exportConfigContent'); const confirmBtn = document.getElementById('exportConfirmBtn'); if (!controls || !trigger || !panel || !configContent || !confirmBtn) return; const formatButtons = Array.from(panel.querySelectorAll('.export-menu-btn')); trigger.setAttribute('aria-expanded', 'false'); const ensureMargin = function(value, fallback) { return Number.isFinite(value) && value >= 0 ? value : fallback; }; const applyPdfPreset = function(paper) { const pdfState = exportState.pdf; if (!pdfState) return; switch (paper) { case 'A3': pdfState.paper = 'A3'; pdfState.marginTop = 10; pdfState.marginBottom = 10; pdfState.marginLeft = 10; pdfState.marginRight = 10; pdfState.scalePercent = 100; break; case 'A2': pdfState.paper = 'A2'; pdfState.marginTop = 15; pdfState.marginBottom = 15; pdfState.marginLeft = 2; pdfState.marginRight = 2; pdfState.scalePercent = 125; break; case 'A4': default: pdfState.paper = 'A4'; pdfState.marginTop = 20; pdfState.marginBottom = 20; pdfState.marginLeft = 15; pdfState.marginRight = 15; pdfState.scalePercent = 100; break; } }; applyPdfPreset(exportState.pdf.paper || 'A4'); const renderConfigPlaceholder = function() { configContent.innerHTML = '
请选择导出格式以查看设置
'; confirmBtn.disabled = true; confirmBtn.textContent = '导出'; formatButtons.forEach(function(button) { button.classList.remove('is-active'); button.setAttribute('aria-pressed', 'false'); }); }; const renderConfigForFormat = function(format) { const sections = []; sections.push(`
`); if (format === 'pdf') { sections.push(`
纸张大小
上边距 (mm)
下边距 (mm)
左边距 (mm)
右边距 (mm)
缩放 (%)
可根据输出需要调整画布尺寸与边距。
`); } if (format === 'markdown') { sections.push(`
开启后图片将转换为 Base64,适合单文件分享。
`); } if (format === 'html' || format === 'docx') { sections.push('
导出将保留当前视图的内容结构。
'); } configContent.innerHTML = sections.join(''); const brandingInput = document.getElementById('configIncludeBranding'); if (brandingInput) { brandingInput.addEventListener('change', function() { exportState.common.includeBranding = brandingInput.checked; }); } if (format === 'pdf') { const pdfState = exportState.pdf; const paperSelect = document.getElementById('configPdfPaper'); if (paperSelect) { paperSelect.value = pdfState.paper; paperSelect.addEventListener('change', function() { const value = paperSelect.value || 'A4'; pdfState.paper = value; applyPdfPreset(value); renderConfigForFormat('pdf'); }); } const marginInputs = { marginTop: document.getElementById('configPdfMarginTop'), marginBottom: document.getElementById('configPdfMarginBottom'), marginLeft: document.getElementById('configPdfMarginLeft'), marginRight: document.getElementById('configPdfMarginRight') }; Object.keys(marginInputs).forEach(function(key) { const input = marginInputs[key]; if (!input) return; const fallback = key === 'marginLeft' || key === 'marginRight' ? 15 : 20; const value = ensureMargin(pdfState[key], fallback); pdfState[key] = value; input.value = value; input.addEventListener('change', function() { const parsed = parseFloat(input.value); if (Number.isFinite(parsed) && parsed >= 0) { pdfState[key] = parsed; } else { input.value = pdfState[key]; } }); }); const scaleInput = document.getElementById('configPdfScale'); if (scaleInput) { const clamped = Math.max(10, Math.min(400, ensureMargin(pdfState.scalePercent, 100))); pdfState.scalePercent = clamped; scaleInput.value = clamped; scaleInput.addEventListener('change', function() { const parsed = parseFloat(scaleInput.value); if (!Number.isFinite(parsed)) { scaleInput.value = pdfState.scalePercent; return; } const normalized = Math.max(10, Math.min(400, parsed)); pdfState.scalePercent = normalized; scaleInput.value = normalized; }); } } if (format === 'markdown') { const embedCheckbox = document.getElementById('configMarkdownEmbed'); if (embedCheckbox) { embedCheckbox.addEventListener('change', function() { exportState.markdown.embedImages = embedCheckbox.checked; }); } } }; const gatherOptionsForFormat = function(format) { const base = { includeBranding: exportState.common.includeBranding !== false }; if (format === 'pdf') { base.pdfPaper = exportState.pdf.paper || 'A4'; base.pdfMargins = { top: ensureMargin(exportState.pdf.marginTop, 20), bottom: ensureMargin(exportState.pdf.marginBottom, 20), left: ensureMargin(exportState.pdf.marginLeft, 15), right: ensureMargin(exportState.pdf.marginRight, 15) }; base.pdfScalePercent = Math.max(10, Math.min(400, ensureMargin(exportState.pdf.scalePercent, 100))); } if (format === 'markdown') { base.markdownEmbedImages = !!exportState.markdown.embedImages; } return base; }; const selectFormat = function(format) { currentFormat = format; formatButtons.forEach(function(button) { const active = button.dataset.format === format; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', active ? 'true' : 'false'); }); renderConfigForFormat(format); confirmBtn.disabled = false; confirmBtn.textContent = `导出 ${EXPORT_LABELS[format] || ''}`; }; const openPanel = function() { controls.classList.add('is-open'); trigger.setAttribute('aria-expanded', 'true'); if (currentFormat) { selectFormat(currentFormat); } else { renderConfigPlaceholder(); } }; const closePanel = function() { controls.classList.remove('is-open'); trigger.setAttribute('aria-expanded', 'false'); }; trigger.addEventListener('click', function(event) { event.preventDefault(); if (controls.classList.contains('is-open')) { closePanel(); } else { openPanel(); } }); if (closeBtn) { closeBtn.addEventListener('click', function(event) { event.preventDefault(); closePanel(); }); } if (backdrop) { backdrop.addEventListener('click', function() { closePanel(); }); } document.addEventListener('keydown', function(event) { if (event.key === 'Escape' && controls.classList.contains('is-open')) { closePanel(); } }); formatButtons.forEach(function(button) { button.setAttribute('aria-pressed', 'false'); button.addEventListener('click', function(event) { event.preventDefault(); const format = button.dataset.format; if (!format) return; selectFormat(format); }); }); confirmBtn.addEventListener('click', async function() { if (!currentFormat) return; const options = gatherOptionsForFormat(currentFormat); try { await handleExport(currentFormat, confirmBtn, options); closePanel(); } catch (error) { console.error('[HistoryExporter] 导出失败:', error); } }); renderConfigPlaceholder(); }); async function handleExport(format, triggerButton, options = {}) { const activeTab = window.currentVisibleTabId || 'ocr'; const data = window.data; if (!data) { alert('历史记录数据尚未加载完成,稍后再试。'); return; } const originalDisabled = triggerButton.disabled; const originalHtml = triggerButton.innerHTML; try { triggerButton.disabled = true; const loadingLabel = `导出${EXPORT_LABELS[format] || ''}中...`; triggerButton.innerHTML = `${loadingLabel}`; if (format === 'original') { const originalAsset = buildOriginalAssetForDetail(data); if (!originalAsset) { alert('当前记录缺少原始内容,无法导出。'); return; } const baseName = sanitizeFileName(data.name || 'document'); const fileName = ensureFileExtension(baseName, originalAsset.extension || 'txt'); saveAs(originalAsset.blob, fileName); } else { const payload = buildExportPayload(activeTab, data); if (!payload) { alert('当前视图没有可导出的内容。'); return; } if (format === 'html') { exportAsHtml(payload, options); } else if (format === 'markdown') { exportAsMarkdown(payload, options); } else if (format === 'docx') { await exportAsDocx(payload, options); } else if (format === 'pdf') { await exportAsPdf(payload, options); } else { console.warn('[HistoryExporter] 未知导出格式:', format); alert('暂不支持该导出格式。'); } } } catch (error) { console.error('[HistoryExporter] 导出失败:', error); alert('导出失败:' + (error && error.message ? error.message : error)); } finally { triggerButton.disabled = originalDisabled; triggerButton.innerHTML = originalHtml; } } function resolveExportImages(data) { if (data && Array.isArray(data.images) && data.images.length > 0) { return data.images; } if (Array.isArray(window.currentImagesData) && window.currentImagesData.length > 0) { return window.currentImagesData; } if (data && data.data && Array.isArray(data.data.images) && data.data.images.length > 0) { return data.data.images; } return []; } function buildExportPayload(tab, data) { const modeLabel = MODE_LABELS[tab] || '未知模式'; const exportTime = new Date(); const fileNameBase = sanitizeFileName(data && data.name ? data.name.replace(/\s+/g, ' ').trim() : '历史记录'); const images = resolveExportImages(data); if (tab === 'ocr') { const markdown = getOcrMarkdown(data); if (!markdown.trim()) return null; const html = buildMarkdownSectionHtml(modeLabel, markdown, images); return { tab, modeLabel, exportTime, fileNameBase, data, images, bodyMarkdown: markdown, bodyHtml: html }; } if (tab === 'translation') { const markdown = getTranslationMarkdown(data); if (!markdown.trim()) return null; const html = buildMarkdownSectionHtml(modeLabel, markdown, images); return { tab, modeLabel, exportTime, fileNameBase, data, images, bodyMarkdown: markdown, bodyHtml: html }; } if (tab === 'chunk-compare') { const pairs = buildChunkPairs(data); if (!pairs.length) return null; const html = buildChunkCompareHtml(modeLabel, pairs, images); const markdown = buildChunkCompareMarkdown(pairs); return { tab, modeLabel, exportTime, fileNameBase, data, images, bodyMarkdown: markdown, bodyHtml: html }; } return null; } function buildMarkdownSectionHtml(title, markdown, images) { const rendered = renderMarkdown(markdown, images); return [ '
', `

${escapeHtml(title)}

`, '
', rendered || '

(无内容)

', '
', '
' ].join('\n'); } function buildChunkPairs(data) { if (!data) return []; const ocrChunks = Array.isArray(data.ocrChunks) ? data.ocrChunks : []; const transChunks = Array.isArray(data.translatedChunks) ? data.translatedChunks : []; if (!ocrChunks.length || ocrChunks.length !== transChunks.length) return []; const pairs = []; for (let i = 0; i < ocrChunks.length; i++) { pairs.push({ index: i + 1, ocr: ocrChunks[i] || '', translation: transChunks[i] || '' }); } return pairs; } function buildChunkCompareHtml(title, pairs, images) { const sections = pairs.map(function(pair) { const originalHtml = renderMarkdown(pair.ocr, images) || '

(无内容)

'; const translationHtml = renderMarkdown(pair.translation, images) || '

(无内容)

'; const hasTable = /', `

${escapeHtml(title)}

`, sections, '' ].join('\n'); } function renderChunkCompareSection(index, type, leftHtml, rightHtml) { const pairClass = `export-pair export-pair-${type}`; return [ `
`, `

区块 ${index}

`, `
`, renderExportSide('原文', leftHtml, 'ocr'), renderExportSide('译文', rightHtml, 'trans'), '
', '
' ].join('\n'); } function renderExportSide(title, html, role) { const safeTitle = escapeHtml(title || (role === 'ocr' ? '原文' : '译文')); const content = html && html.trim() ? html : '

(无内容)

'; return [ `
`, `
${safeTitle}
`, `
${content}
`, '
' ].join('\n'); } function buildChunkCompareMarkdown(pairs) { return pairs.map(function(pair) { return [ `## 区块 ${pair.index}` , '', '### 原文', '', pair.ocr ? pair.ocr.trim() : '(无内容)', '', '### 译文', '', pair.translation ? pair.translation.trim() : '(无内容)', '' ].join('\n'); }).join('\n'); } function exportAsHtml(payload, options = {}) { const documentHtml = buildHtmlDocument(payload, options); const fileName = resolveFileName(payload, 'html', options); if (options.returnContent) { return { fileName, content: documentHtml, mime: 'text/html;charset=utf-8' }; } if (options.returnBlob) { return { fileName, blob: new Blob([documentHtml], { type: 'text/html;charset=utf-8' }) }; } saveBlob(documentHtml, fileName, 'text/html;charset=utf-8'); } function exportAsMarkdown(payload, options = {}) { const markdown = buildMarkdownDocument(payload, options); const fileName = resolveFileName(payload, 'md', options); if (options.returnContent) { return { fileName, content: markdown, mime: 'text/markdown;charset=utf-8' }; } if (options.returnBlob) { return { fileName, blob: new Blob([markdown], { type: 'text/markdown;charset=utf-8' }) }; } saveBlob(markdown, fileName, 'text/markdown;charset=utf-8'); } async function exportAsDocx(payload, options = {}) { const module = window.PBXHistoryExporterDocx; if (!module || typeof module.exportAsDocx !== 'function') { throw new Error('DOCX 导出模块未加载'); } // 检查是否有调试参数(在 URL 中或 localStorage) const urlParams = new URLSearchParams(window.location.search); const enableDebug = urlParams.has('docx-debug') || localStorage.getItem('docx-debug') === 'true'; // 合并选项,如果启用调试则添加调试参数 const finalOptions = { ...options, debug: options.debug || enableDebug, strictValidation: options.strictValidation || enableDebug, validateXml: options.validateXml !== false // 默认启用 }; if (enableDebug) { console.log('🐛 DOCX 调试模式已启用'); console.log('提示: 在控制台查看详细的导出日志'); } const helpers = { resolveFileName, getOcrMarkdown, getTranslationMarkdown, formatDateTime, BRAND_LINK }; return module.exportAsDocx(payload, finalOptions, helpers); } function getPaperDimensions(paper) { if (!paper) return null; const key = paper.toString().trim().toUpperCase(); return PDF_PAPER_DIMENSIONS[key] || null; } function mmToPx(mm) { return mm * 3.7795275591; } function exportAsPdf(payload, options = {}) { const existing = document.querySelector('.history-export-print-root'); if (existing) existing.remove(); const existingStyle = document.querySelector('style[data-history-export-print]'); if (existingStyle) existingStyle.remove(); const styleEl = document.createElement('style'); styleEl.setAttribute('data-history-export-print', 'true'); styleEl.textContent = `${buildExportStyles(options)}\n${buildPrintStyles(options)}`; const container = document.createElement('div'); container.className = 'history-export-print-root'; const mainHtml = buildMainContent(payload, options); container.innerHTML = `
${mainHtml}
`; document.body.appendChild(styleEl); document.body.appendChild(container); document.body.classList.add('history-export-print-mode'); const rootElement = container.querySelector('.history-export-root'); if (rootElement && options.pdfScalePercent) { const clamped = Math.max(10, Math.min(400, options.pdfScalePercent)); rootElement.style.fontSize = clamped + '%'; rootElement.style.transform = ''; rootElement.style.width = ''; } if (rootElement) { const wrapperElement = rootElement.querySelector('.export-wrapper'); const paperDimensions = getPaperDimensions(options.pdfPaper); const margins = Object.assign({ left: 15, right: 15 }, options.pdfMargins || {}); if (wrapperElement && paperDimensions) { const safeLeft = Number.isFinite(margins.left) ? margins.left : 15; const safeRight = Number.isFinite(margins.right) ? margins.right : 15; const printableWidthMm = Math.max(60, paperDimensions.width - safeLeft - safeRight); const printableWidthPx = mmToPx(printableWidthMm); wrapperElement.style.maxWidth = printableWidthPx + 'px'; wrapperElement.style.width = printableWidthPx + 'px'; } else if (wrapperElement) { wrapperElement.style.maxWidth = ''; wrapperElement.style.width = ''; } } // 尝试在打印前对表格内的公式做一次自适应缩放,避免拥挤/遮挡 try { autoscaleFormulasInContainer(rootElement); // 再次触发一次,确保样式应用后尺寸稳定 setTimeout(function(){ autoscaleFormulasInContainer(rootElement); }, 30); } catch (e) { console.warn('[HistoryExporter] autoscaleFormulasInContainer failed:', e); } const cleanup = function() { document.body.classList.remove('history-export-print-mode'); container.remove(); styleEl.remove(); }; return new Promise(function(resolve) { window.addEventListener('afterprint', function handler() { window.removeEventListener('afterprint', handler); cleanup(); resolve(); }, { once: true }); setTimeout(function() { try { // 打印前最后再执行一次自适应,确保分页/布局最终稳定 try { autoscaleFormulasInContainer(rootElement); } catch {} window.print(); } catch (error) { console.error('[HistoryExporter] 调用打印失败:', error); cleanup(); resolve(); } }, 100); }); } function buildHtmlDocument(payload, options = {}) { const styles = buildExportStyles(options); const content = buildMainContent(payload, options); return [ '', '', '', ' ', ` ${escapeHtml(payload.fileNameBase)} - ${escapeHtml(payload.modeLabel)}导出`, ' ', ` `, ` `, '', '', content, '', '' ].join('\n'); } function buildPrintStyles(options = {}) { const paperKey = (options.pdfPaper || 'A4').toString().trim().toUpperCase(); const paperDimensions = getPaperDimensions(paperKey); const pageSizeValue = paperDimensions ? `${paperDimensions.width}mm ${paperDimensions.height}mm` : `${paperKey} portrait`; const margins = Object.assign({ top: 20, bottom: 20, left: 15, right: 15 }, options.pdfMargins || {}); const safeMargin = function(value, fallback) { return Number.isFinite(value) && value >= 0 ? value : fallback; }; const marginTop = safeMargin(margins.top, 20); const marginBottom = safeMargin(margins.bottom, 20); const marginLeft = safeMargin(margins.left, 15); const marginRight = safeMargin(margins.right, 15); return ` @page { size: ${pageSizeValue}; margin: ${marginTop}mm ${marginRight}mm ${marginBottom}mm ${marginLeft}mm; } body.history-export-print-mode { margin: 0; background: #ffffff !important; color: #0f172a; } body.history-export-print-mode > *:not(.history-export-print-root) { display: none !important; } body.history-export-print-mode .history-export-print-root { margin: 0; } body.history-export-print-mode .history-export-root { padding: 0; background: transparent !important; min-height: auto; } body.history-export-print-mode .history-export-root .export-wrapper { margin: 0 auto; box-shadow: none; border-radius: 0; padding: 0; } body.history-export-print-mode .history-export-root .export-header { margin-bottom: 24px; } body.history-export-print-mode .history-export-root .export-meta, body.history-export-print-mode .history-export-root .export-section { page-break-inside: avoid; } `; // end print styles } function buildMainContent(payload, options = {}) { const segments = ['
']; const brandingNote = buildBrandingBadgeHtml(options); if (brandingNote) { segments.push(brandingNote); } segments.push(buildHeaderHtml(payload)); segments.push(payload.bodyHtml); segments.push('
'); return segments.join('\n'); } function buildHeaderHtml(payload) { const { data, modeLabel, exportTime } = payload; const metaItems = []; if (data && data.name) { metaItems.push(`
  • 原始文件${escapeHtml(data.name)}
  • `); } metaItems.push(`
  • 导出模式${escapeHtml(modeLabel)}
  • `); metaItems.push(`
  • 导出时间${escapeHtml(formatDateTime(exportTime))}
  • `); if (data && data.id) { metaItems.push(`
  • 记录ID${escapeHtml(data.id)}
  • `); } return [ '
    ', `

    ${escapeHtml(data && data.name ? data.name : '历史记录导出')}

    `, ' ', '
    ' ].join('\n'); } function buildBrandingBadgeHtml(options = {}) { if (options.includeBranding === false) return ''; return '
    by Paper Burner X
    '; } function embedImagesInMarkdown(markdown, images) { if (!markdown || !Array.isArray(images) || images.length === 0) { return markdown; } const map = new Map(); images.forEach(function(img, index) { if (!img) return; const rawData = typeof img.data === 'string' ? img.data.trim() : ''; if (!rawData) return; const dataUrl = rawData.startsWith('data:') ? rawData : 'data:image/png;base64,' + rawData; const keys = new Set(); if (img.name) { const cleaned = String(img.name).trim(); if (cleaned) { keys.add(cleaned); keys.add(cleaned.replace(/^\.\//, '')); keys.add('images/' + cleaned); } } if (img.id) { const id = String(img.id).trim(); if (id) { keys.add(id); keys.add(id + '.png'); keys.add('images/' + id); keys.add('images/' + id + '.png'); } } keys.add(`img-${index}.jpeg.png`); keys.add(`images/img-${index}.jpeg.png`); keys.forEach(function(key) { map.set(key, dataUrl); }); }); return markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, path) { if (!path) return match; const trimmed = path.trim(); if (!trimmed) return match; const basePath = trimmed.split(/[?#]/)[0].replace(/^\.\//, ''); const direct = map.get(basePath) || map.get(basePath.replace(/^images\//, '')); if (direct) { return `![${alt}](${direct})`; } return match; }); } function buildMarkdownDocument(payload, options = {}) { const { data, modeLabel, exportTime, bodyMarkdown } = payload; const lines = []; lines.push('---'); lines.push(`title: ${data && data.name ? escapeMarkdown(data.name) : '历史记录导出'}`); lines.push(`mode: ${modeLabel}`); lines.push(`exportedAt: ${formatDateTime(exportTime)}`); if (data && data.id) { lines.push(`recordId: ${data.id}`); } lines.push('---'); lines.push(''); if (options.includeBranding !== false) { lines.push(`> by [Paper Burner X](${BRAND_LINK})`); lines.push(''); } lines.push(`# ${modeLabel}导出`); lines.push(''); if (data && data.name) { lines.push(`- 原始文件: ${escapeMarkdown(data.name)}`); } lines.push(`- 导出时间: ${formatDateTime(exportTime)}`); lines.push(''); lines.push('---'); lines.push(''); lines.push(bodyMarkdown.trim()); lines.push(''); let output = lines.join('\n'); if (options.markdownEmbedImages && data && Array.isArray(data.images)) { output = embedImagesInMarkdown(output, data.images); } return output; } function buildExportStyles(options = {}) { return ` .history-export-root { font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; background: #f7f9fc; margin: 0; padding: 32px 16px; color: #0f172a; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; overflow-x: hidden; } .history-export-root .katex-block { margin: 16px 0; text-align: center; overflow-x: auto; padding: 12px 16px; box-sizing: border-box; position: relative; } .history-export-root .katex-block .katex-display { display: inline-block; margin: 0 auto; text-align: left; position: relative; /* 预留更充足的右侧空间给公式右标(如 \tag 或编号) */ padding-right: 4.25em; } .history-export-root .katex-display { position: relative; padding-right: 4.25em; box-sizing: border-box; } .history-export-root .katex-display .katex-tag, .history-export-root .katex-display .tag, .history-export-root .katex .katex-tag, .history-export-root .katex .tag { position: absolute; right: 0; top: 0; height: 100%; display: inline-flex; align-items: center; white-space: nowrap; margin-left: 0.5em; } .history-export-root .katex-display > .katex { max-width: calc(100% - 4.25em); } .history-export-root .katex-block .katex-display .katex-tag { right: 0; } .history-export-root .katex-inline { margin: 0 1px; } .history-export-root .katex-fallback { background-color: #fff5f5; border: 1px dashed #f56565; border-radius: 6px; color: #c53030; font-family: 'Courier New', Consolas, monospace; } .history-export-root .katex-fallback.katex-block { text-align: left; padding: 12px 16px; white-space: normal; } .history-export-root .katex-fallback.katex-inline { display: inline-flex; align-items: center; padding: 0 6px; } .history-export-root .katex-fallback-source { white-space: pre-wrap; word-break: break-word; } /* 覆盖 display 方式:使用块级,使右侧编号在容器最右侧定位,避免与公式主体重叠 */ .history-export-root .katex-display, .history-export-root .katex-block .katex-display { display: block; width: 100%; text-align: center; } .history-export-root .katex-block { page-break-inside: avoid; } .history-export-root .export-wrapper { width: 100%; max-width: 860px; margin: 0 auto; background: #ffffff; border-radius: 12px; box-shadow: 0 18px 45px -24px rgba(30, 64, 175, 0.35); padding: 40px 48px; overflow-x: hidden; } .history-export-root .export-wrapper * { box-sizing: border-box; } .export-brand-note { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; background: rgba(59,130,246,0.12); border: 1px solid rgba(59,130,246,0.18); color: #1d4ed8; font-size: 0.9rem; font-weight: 600; margin-bottom: 20px; } .export-brand-note a { color: #1d4ed8; text-decoration: none; } .export-brand-note a:hover { text-decoration: underline; } .history-export-root .export-header h1 { margin: 0 0 16px; font-size: 1.8rem; color: #1e3a8a; } .history-export-root .export-meta { list-style: none; padding: 16px 20px; margin: 0 0 32px; border-radius: 10px; background: linear-gradient(135deg, rgba(59,130,246,0.12), rgba(37,99,235,0.05)); display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px 16px; } .history-export-root .export-meta li { display: flex; flex-direction: column; gap: 4px; } .history-export-root .meta-label { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; color: #475569; } .history-export-root .meta-value { font-size: 1rem; color: #0f172a; word-break: break-word; } .history-export-root .export-section { margin-bottom: 36px; } .history-export-root .export-section h2 { margin: 0 0 16px; font-size: 1.4rem; color: #1e293b; } .history-export-root .markdown-body { line-height: 1.7; font-size: 1rem; color: #1f2937; } .history-export-root .markdown-body * { max-width: 100%; } .history-export-root .markdown-body p { margin: 0 0 1em; } .history-export-root .markdown-body h1, .history-export-root .markdown-body h2, .history-export-root .markdown-body h3, .history-export-root .markdown-body h4 { margin: 1.5em 0 0.6em; color: #1d4ed8; } .history-export-root .markdown-body code { background: rgba(226,232,240,0.6); padding: 0.2em 0.45em; border-radius: 6px; font-size: 0.9em; word-break: break-word; } .history-export-root .markdown-body pre { padding: 16px; background: #0f172a; color: #f8fafc; border-radius: 10px; overflow: auto; white-space: pre-wrap; } .history-export-root .markdown-body table { width: 100%; border-collapse: collapse; margin: 16px 0; table-layout: fixed; } .history-export-root .markdown-body th, .history-export-root .markdown-body td { border: 1px solid #dbeafe; padding: 10px 12px; vertical-align: top; word-break: break-word; overflow-wrap: anywhere; } .history-export-root .markdown-body th { background: #eff6ff; font-weight: 600; color: #1d4ed8; } .history-export-root .markdown-body img { max-width: 100%; height: auto; } .history-export-root .katex-display { overflow-x: auto; -ms-overflow-style: none; /* IE/Edge */ scrollbar-width: none; /* Firefox */ } .history-export-root .katex-display::-webkit-scrollbar { display: none; } .history-export-root .chunk-section { margin-bottom: 24px; } .history-export-root .chunk-section h3 { margin: 0 0 12px; color: #1e40af; } .history-export-root .chunk-table { width: 100%; border-collapse: collapse; box-shadow: inset 0 0 0 1px rgba(96,165,250,0.3); table-layout: fixed; } .history-export-root .chunk-table th, .history-export-root .chunk-table td { border: 1px solid rgba(148,163,184,0.4); padding: 12px; vertical-align: top; width: 50%; word-break: break-word; overflow-wrap: anywhere; } .history-export-root .chunk-table th { background: rgba(59,130,246,0.12); text-align: left; } .history-export-root .empty-text { color: #94a3b8; margin: 0; } .history-export-root .align-flex { display: flex; flex-wrap: wrap; gap: 18px; width: 100%; margin-bottom: 24px; } .history-export-root .align-flex .align-block { flex: 1 1 calc(50% - 9px); max-width: calc(50% - 9px); margin-bottom: 0; } .history-export-root .align-flex .align-block:only-child { flex-basis: 100%; max-width: 100%; } .history-export-root .align-flex .align-title { margin-bottom: 6px; } .history-export-root .align-flex .align-content { width: 100%; } .history-export-root .align-flex img { max-width: 100%; height: auto; } .history-export-root .align-flex table { width: 100% !important; table-layout: fixed; } .history-export-root .align-flex.table-pair { flex-direction: column; gap: 12px; } .history-export-root .align-flex.table-pair .align-block { flex-basis: 100%; max-width: 100%; } .history-export-root .align-content table { width: 100% !important; table-layout: fixed; } .history-export-root .export-chunk-compare { margin-top: 24px; } .history-export-root .export-chunk { margin-bottom: 32px; } .history-export-root .export-chunk h3 { margin: 0 0 12px; font-size: 1.2rem; color: #1e40af; } .history-export-root .export-pair { display: flex; align-items: stretch; gap: 16px; padding: 16px; border: 1px solid #e2e8f0; border-radius: 10px; background: #ffffff; margin-bottom: 16px; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); } .history-export-root .export-pair.export-pair-table, .history-export-root .export-pair.export-pair-image { flex-direction: column; gap: 12px; background: #f8fbff; } .history-export-root .export-side { flex: 1 1 50%; min-width: 0; } .history-export-root .export-pair.export-pair-table .export-side, .history-export-root .export-pair.export-pair-image .export-side { flex-basis: 100%; max-width: 100%; } .history-export-root .export-side + .export-side { border-left: 1px dashed #cbd5f5; padding-left: 12px; } .history-export-root .export-pair.export-pair-table .export-side + .export-side, .history-export-root .export-pair.export-pair-image .export-side + .export-side { border-left: none; padding-left: 0; border-top: 1px dashed #cbd5f5; padding-top: 10px; } .history-export-root .export-side-title { font-weight: 600; color: #1d4ed8; margin-bottom: 6px; } .history-export-root .export-side-content { line-height: 1.7; } .history-export-root .export-side-content img { max-width: 100%; height: auto; } .history-export-root .export-side-content table { width: 100% !important; table-layout: fixed; overflow-wrap: anywhere; } .history-export-root .export-side-content table .katex-display, .history-export-root .export-side-content table .katex-inline { font-size: 0.95em; /* 默认略缩小,减少拥挤概率 */ } .history-export-root td, .history-export-root th { line-height: 1.6; /* 表格内行距更大,避免上下覆盖 */ } .history-export-root .katex, .history-export-root .katex-display { line-height: 1.35; /* 提高 KaTeX 的行高,打印更稳 */ } .history-export-root .export-side-content td, .history-export-root .export-side-content th { word-break: break-word; } .history-export-root .export-side-content pre { overflow-x: auto; padding: 12px; background: #0f172a; color: #f8fafc; border-radius: 8px; } .history-export-root .export-side-content { overflow-x: auto; } .history-export-root .export-single { margin-bottom: 24px; } .history-export-root .export-single:last-child { margin-bottom: 0; } .history-export-root .align-content td, .history-export-root .align-content th { word-break: break-word; } @media (max-width: 768px) { .history-export-root { padding: 16px; } .history-export-root .export-wrapper { padding: 28px; } .history-export-root .export-meta { grid-template-columns: 1fr; } .history-export-root .align-flex { gap: 12px; } .history-export-root .align-flex .align-block { flex-basis: 100%; max-width: 100%; } .history-export-root .align-flex.table-pair { gap: 8px; } .history-export-root .align-flex.table-pair .align-block { flex-basis: 100%; max-width: 100%; } .history-export-root .export-pair { flex-direction: column; gap: 12px; padding: 12px; } .history-export-root .export-side { flex-basis: 100%; max-width: 100%; } .history-export-root .export-side + .export-side { border-left: none; padding-left: 0; border-top: 1px dashed #cbd5f5; padding-top: 10px; } } `.trim(); } function buildFileName(payload, ext) { const modeKey = payload.tab.replace(/[^a-z\-]/gi, '') || 'export'; const timestamp = formatTimestamp(payload.exportTime); return `${payload.fileNameBase}_${modeKey}_${timestamp}.${ext}`; } function ensureFileExtension(name, ext) { const sanitized = sanitizeFileName(name || ''); const base = sanitized.replace(/(\.[^.]+)?$/, ''); const safeBase = base || 'document'; const normalizedExt = (ext || '').toString().trim().toLowerCase() || 'txt'; return `${safeBase}.${normalizedExt}`; } function resolveFileName(payload, ext, options = {}) { if (options && options.fileName) { const desired = options.fileName; const lower = desired.toLowerCase(); const targetExt = (ext || '').toString().trim().toLowerCase(); if (targetExt && lower.endsWith(`.${targetExt}`)) { return sanitizeFileName(desired); } return ensureFileExtension(desired, targetExt || 'txt'); } if (payload && payload.customFileName) { const desired = payload.customFileName; const lower = desired.toLowerCase(); const targetExt = (ext || '').toString().trim().toLowerCase(); if (targetExt && lower.endsWith(`.${targetExt}`)) { return sanitizeFileName(desired); } return ensureFileExtension(desired, targetExt || 'txt'); } return buildFileName(payload, ext); } function saveBlob(content, fileName, mimeType) { if (typeof saveAs !== 'function') { throw new Error('文件保存组件不可用'); } const blob = new Blob([content], { type: mimeType }); saveAs(blob, fileName); } function renderMarkdown(markdown, images) { const source = typeof markdown === 'string' ? markdown : ''; if (!source.trim()) return ''; if (window.MarkdownProcessor && typeof window.MarkdownProcessor.safeMarkdown === 'function' && typeof window.MarkdownProcessor.renderWithKatexFailback === 'function') { const safeMd = window.MarkdownProcessor.safeMarkdown(source, images); return window.MarkdownProcessor.renderWithKatexFailback(safeMd); } if (window.marked && typeof window.marked.parse === 'function') { return window.marked.parse(source); } return `
    ${escapeHtml(source)}
    `; } /** * 对表格内的 KaTeX 公式做简单的自适应:若宽度超出单元格,则按比例缩小字体。 * 仅用于导出/打印场景,避免公式挤压覆盖。 */ function autoscaleFormulasInContainer(root) { if (!root) return; const candidates = root.querySelectorAll('.export-side-content table .katex, .export-side-content table .katex-display, .export-side-content table .katex-inline'); candidates.forEach(function(el){ el.style.fontSize = ''; el.style.transform = ''; el.style.transformOrigin = ''; el.style.lineHeight = ''; }); candidates.forEach(function(el){ const cell = el.closest('td, th') || el.parentElement; if (!cell) return; const cellWidth = Math.max(0, cell.clientWidth - 8); const rect = el.getBoundingClientRect(); const width = rect.width; if (cellWidth > 0 && width > cellWidth) { const currentFont = parseFloat(window.getComputedStyle(el).fontSize) || 16; const scale = Math.max(0.7, Math.min(1, (cellWidth / width) * 0.98)); el.style.fontSize = (currentFont * scale).toFixed(2) + 'px'; el.style.lineHeight = '1.35'; } }); } function getOcrMarkdown(data) { if (!data) return ''; if (data.ocr && data.ocr.trim()) return data.ocr; if (Array.isArray(data.ocrChunks) && data.ocrChunks.length) { return data.ocrChunks.map(function(chunk) { return (chunk || '').trim(); }).join('\n\n'); } return ''; } function getTranslationMarkdown(data) { if (!data) return ''; if (data.translation && data.translation.trim()) return data.translation; if (Array.isArray(data.translatedChunks) && data.translatedChunks.length) { return data.translatedChunks.map(function(chunk) { return (chunk || '').trim(); }).join('\n\n'); } return ''; } function sanitizeFileName(name) { return (name || 'document').replace(/[\\/:*?"<>|]/g, '_'); } function formatDateTime(date) { const pad = function(num) { return String(num).padStart(2, '0'); }; return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; } function formatTimestamp(date) { const pad = function(num) { return String(num).padStart(2, '0'); }; return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`; } function escapeHtml(str) { return String(str).replace(/[&<>"']/g, function(ch) { switch (ch) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case "'": return '''; default: return ch; } }); } function escapeXml(str) { return String(str).replace(/[&<>"']/g, function(ch) { switch (ch) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case "'": return '''; default: return ch; } }); } function escapeMarkdown(str) { return String(str).replace(/[\\`*_{}\[\]()#+\-.!]/g, '\\$&'); } function buildOriginalAssetForDetail(data) { if (!data) return null; const extension = (data.originalExtension || data.fileType || 'txt').toLowerCase(); if (data.originalEncoding === 'text' && typeof data.originalContent === 'string') { const mime = guessMimeType(extension, true); return { blob: new Blob([data.originalContent], { type: `${mime};charset=utf-8` }), extension }; } if (data.originalEncoding && data.originalEncoding !== 'text' && data.originalBinary) { const buffer = base64ToArrayBuffer(data.originalBinary); if (!buffer) return null; const mime = guessMimeType(extension, false); return { blob: new Blob([buffer], { type: mime }), extension }; } return null; } function guessMimeType(ext, isText) { const lowercase = (ext || '').toLowerCase(); if (isText) { if (lowercase === 'html' || lowercase === 'htm') return 'text/html'; if (lowercase === 'md' || lowercase === 'markdown') return 'text/markdown'; if (lowercase === 'yaml' || lowercase === 'yml') return 'text/yaml'; if (lowercase === 'json') return 'application/json'; if (lowercase === 'txt') return 'text/plain'; return 'text/plain'; } if (lowercase === 'docx') return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; if (lowercase === 'pptx') return 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; if (lowercase === 'epub') return 'application/epub+zip'; if (lowercase === 'pdf') return 'application/pdf'; return 'application/octet-stream'; } function base64ToArrayBuffer(base64) { try { const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } catch (error) { console.warn('[HistoryExporter] base64ToArrayBuffer failed:', error); return null; } } window.PBXHistoryExporter = window.PBXHistoryExporter || {}; Object.assign(window.PBXHistoryExporter, { preparePayload: function(mode, data) { return buildExportPayload(mode, data); }, exportAsHtml, exportAsMarkdown, exportAsDocx, exportAsPdf, resolveFileName, ensureFileExtension, sanitizeFileName, buildExportStyles, buildMainContent, formatTimestamp, katexCdn: KATEX_CDN }); })(window, document);