1560 lines
51 KiB
JavaScript
1560 lines
51 KiB
JavaScript
(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 = '<div class="export-config-placeholder">请选择导出格式以查看设置</div>';
|
||
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(`
|
||
<div class="export-config-section">
|
||
<label class="export-option">
|
||
<input type="checkbox" id="configIncludeBranding" ${exportState.common.includeBranding ? 'checked' : ''}>
|
||
<span>附带 Paper Burner X 标识</span>
|
||
</label>
|
||
</div>
|
||
`);
|
||
|
||
if (format === 'pdf') {
|
||
sections.push(`
|
||
<div class="export-config-section">
|
||
<div class="export-field">
|
||
<span class="export-field-label">纸张大小</span>
|
||
<select id="configPdfPaper">
|
||
<option value="A4">A4 (210 × 297 mm)</option>
|
||
<option value="A3">A3 (297 × 420 mm)</option>
|
||
<option value="A2">A2 (420 × 594 mm)</option>
|
||
</select>
|
||
</div>
|
||
<div class="export-field-row">
|
||
<div class="export-field export-field-half">
|
||
<span class="export-field-label">上边距 (mm)</span>
|
||
<input type="number" id="configPdfMarginTop" min="0" step="1">
|
||
</div>
|
||
<div class="export-field export-field-half">
|
||
<span class="export-field-label">下边距 (mm)</span>
|
||
<input type="number" id="configPdfMarginBottom" min="0" step="1">
|
||
</div>
|
||
</div>
|
||
<div class="export-field-row">
|
||
<div class="export-field export-field-half">
|
||
<span class="export-field-label">左边距 (mm)</span>
|
||
<input type="number" id="configPdfMarginLeft" min="0" step="1">
|
||
</div>
|
||
<div class="export-field export-field-half">
|
||
<span class="export-field-label">右边距 (mm)</span>
|
||
<input type="number" id="configPdfMarginRight" min="0" step="1">
|
||
</div>
|
||
</div>
|
||
<div class="export-field-row export-field-row-single">
|
||
<div class="export-field export-field-half">
|
||
<span class="export-field-label">缩放 (%)</span>
|
||
<input type="number" id="configPdfScale" min="10" max="400" step="5">
|
||
</div>
|
||
</div>
|
||
<div class="export-tip">可根据输出需要调整画布尺寸与边距。</div>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
if (format === 'markdown') {
|
||
sections.push(`
|
||
<div class="export-config-section">
|
||
<label class="export-option">
|
||
<input type="checkbox" id="configMarkdownEmbed" ${exportState.markdown.embedImages ? 'checked' : ''}>
|
||
<span>内嵌图片 (Base64)</span>
|
||
</label>
|
||
<div class="export-tip">开启后图片将转换为 Base64,适合单文件分享。</div>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
if (format === 'html' || format === 'docx') {
|
||
sections.push('<div class="export-tip">导出将保留当前视图的内容结构。</div>');
|
||
}
|
||
|
||
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 = `<i class="fa fa-spinner fa-spin"></i><span>${loadingLabel}</span>`;
|
||
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 [
|
||
'<section class="export-section">',
|
||
` <h2>${escapeHtml(title)}</h2>`,
|
||
' <div class="export-markdown markdown-body">',
|
||
rendered || '<p>(无内容)</p>',
|
||
' </div>',
|
||
'</section>'
|
||
].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) || '<p class="empty-text">(无内容)</p>';
|
||
const translationHtml = renderMarkdown(pair.translation, images) || '<p class="empty-text">(无内容)</p>';
|
||
const hasTable = /<table\b/i.test(originalHtml) || /<table\b/i.test(translationHtml);
|
||
const hasImage = /<img\b/i.test(originalHtml) || /<img\b/i.test(translationHtml);
|
||
let type = 'text';
|
||
if (hasTable) {
|
||
type = 'table';
|
||
} else if (hasImage) {
|
||
type = 'image';
|
||
}
|
||
return renderChunkCompareSection(pair.index, type, originalHtml, translationHtml);
|
||
}).join('\n');
|
||
|
||
return [
|
||
'<section class="export-section export-chunk-compare">',
|
||
` <h2>${escapeHtml(title)}</h2>`,
|
||
sections,
|
||
'</section>'
|
||
].join('\n');
|
||
}
|
||
|
||
function renderChunkCompareSection(index, type, leftHtml, rightHtml) {
|
||
const pairClass = `export-pair export-pair-${type}`;
|
||
return [
|
||
`<section class="export-chunk" data-chunk-index="${index}">`,
|
||
` <h3>区块 ${index}</h3>`,
|
||
` <div class="${pairClass}">`,
|
||
renderExportSide('原文', leftHtml, 'ocr'),
|
||
renderExportSide('译文', rightHtml, 'trans'),
|
||
' </div>',
|
||
'</section>'
|
||
].join('\n');
|
||
}
|
||
|
||
function renderExportSide(title, html, role) {
|
||
const safeTitle = escapeHtml(title || (role === 'ocr' ? '原文' : '译文'));
|
||
const content = html && html.trim() ? html : '<p class="empty-text">(无内容)</p>';
|
||
return [
|
||
` <div class="export-side export-side-${role}">`,
|
||
` <div class="export-side-title">${safeTitle}</div>`,
|
||
` <div class="export-side-content">${content}</div>`,
|
||
' </div>'
|
||
].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 = `<div class="history-export-root">${mainHtml}</div>`;
|
||
|
||
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 [
|
||
'<!DOCTYPE html>',
|
||
'<html lang="zh">',
|
||
'<head>',
|
||
' <meta charset="UTF-8">',
|
||
` <title>${escapeHtml(payload.fileNameBase)} - ${escapeHtml(payload.modeLabel)}导出</title>`,
|
||
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
||
` <link rel="stylesheet" href="${KATEX_CDN}">`,
|
||
` <style>${styles}</style>`,
|
||
'</head>',
|
||
'<body class="history-export-root">',
|
||
content,
|
||
'</body>',
|
||
'</html>'
|
||
].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 = ['<div class="export-wrapper">'];
|
||
const brandingNote = buildBrandingBadgeHtml(options);
|
||
if (brandingNote) {
|
||
segments.push(brandingNote);
|
||
}
|
||
segments.push(buildHeaderHtml(payload));
|
||
segments.push(payload.bodyHtml);
|
||
segments.push('</div>');
|
||
return segments.join('\n');
|
||
}
|
||
|
||
function buildHeaderHtml(payload) {
|
||
const { data, modeLabel, exportTime } = payload;
|
||
const metaItems = [];
|
||
if (data && data.name) {
|
||
metaItems.push(`<li><span class="meta-label">原始文件</span><span class="meta-value">${escapeHtml(data.name)}</span></li>`);
|
||
}
|
||
metaItems.push(`<li><span class="meta-label">导出模式</span><span class="meta-value">${escapeHtml(modeLabel)}</span></li>`);
|
||
metaItems.push(`<li><span class="meta-label">导出时间</span><span class="meta-value">${escapeHtml(formatDateTime(exportTime))}</span></li>`);
|
||
if (data && data.id) {
|
||
metaItems.push(`<li><span class="meta-label">记录ID</span><span class="meta-value">${escapeHtml(data.id)}</span></li>`);
|
||
}
|
||
|
||
return [
|
||
'<header class="export-header">',
|
||
` <h1>${escapeHtml(data && data.name ? data.name : '历史记录导出')}</h1>`,
|
||
' <ul class="export-meta">',
|
||
metaItems.join('\n'),
|
||
' </ul>',
|
||
'</header>'
|
||
].join('\n');
|
||
}
|
||
|
||
function buildBrandingBadgeHtml(options = {}) {
|
||
if (options.includeBranding === false) return '';
|
||
return '<div class="export-brand-note">by <a href="' + BRAND_LINK + '" target="_blank" rel="noopener">Paper Burner X</a></div>';
|
||
}
|
||
|
||
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 `<pre>${escapeHtml(source)}</pre>`;
|
||
}
|
||
|
||
/**
|
||
* 对表格内的 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);
|