(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(`
纸张大小
可根据输出需要调整画布尺寸与边距。
`);
}
if (format === 'markdown') {
sections.push(`
`);
}
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 [
''
].join('\n');
}
function buildBrandingBadgeHtml(options = {}) {
if (options.includeBranding === false) return '';
return '';
}
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 ``;
}
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);