/**
* 分块对比预览性能优化器
* 解决历史详情页分块对比的渲染性能问题
*/
class ChunkCompareOptimizer {
constructor() {
this.renderCache = new Map();
this.visibleChunks = new Set();
this.currentScrollPosition = 0;
this.observer = null;
this.renderQueue = [];
this.isRendering = false;
this.batchSize = 3; // 每批渲染的分块数量
this.chunkHeight = 300; // 估算的分块高度
this.bufferSize = 2; // 缓冲区大小(上下各2个分块)
}
/**
* 初始化优化器
*/
init() {
this.setupIntersectionObserver();
this.setupPerformanceMonitor();
}
/**
* 优化分块对比的渲染性能
* @param {Array} ocrChunks OCR分块数据
* @param {Array} translatedChunks 翻译分块数据
* @param {Object} options 渲染选项
* @returns {string} 优化后的HTML
*/
optimizeChunkComparison(ocrChunks, translatedChunks, options = {}) {
const startTime = performance.now();
const chunkCount = ocrChunks.length;
console.log(`[ChunkOptimizer] 开始优化渲染 ${chunkCount} 个分块`);
// 创建带骨架屏的容器
const containerHTML = this.createSkeletonContainer(chunkCount);
// 异步开始渲染
setTimeout(() => {
this.scheduleProgressiveRender(ocrChunks, translatedChunks, options);
}, 100);
const endTime = performance.now();
console.log(`[ChunkOptimizer] 初始化完成,耗时: ${(endTime - startTime).toFixed(2)}ms`);
return containerHTML;
}
/**
* 创建带骨架屏的容器
* @param {number} chunkCount 分块数量
* @returns {string} 容器HTML
*/
createSkeletonContainer(chunkCount) {
return `
`;
}
/**
* 生成骨架屏分块
* @param {number} count 骨架数量
* @returns {string} 骨架HTML
*/
generateSkeletonChunks(count) {
let skeletonHTML = '';
for (let i = 0; i < count; i++) {
skeletonHTML += `
`;
}
return skeletonHTML;
}
/**
* 为超大文档创建特殊容器
* @param {number} chunkCount 分块数量
* @param {Array} ocrChunks OCR分块
* @param {Array} translatedChunks 翻译分块
* @param {Object} options 选项
* @returns {string} 容器HTML
*/
createLargeDocumentContainer(chunkCount, ocrChunks, translatedChunks, options) {
// 立即保存数据到window对象供后续使用
window.largeDocumentData = {
ocrChunks,
translatedChunks,
options,
currentPage: 0,
pageSize: 10 // 每页显示10个分块
};
const totalPages = Math.ceil(chunkCount / 10);
return `
分块对比 (${chunkCount}块)
检测到大型文档,已启用高效浏览模式。使用分页浏览以获得更好的性能。
${this.renderPageChunks(0, ocrChunks, translatedChunks, options)}
`;
}
/**
* 渲染指定页的分块
* @param {number} pageIndex 页索引
* @param {Array} ocrChunks OCR分块
* @param {Array} translatedChunks 翻译分块
* @param {Object} options 选项
* @returns {string} 分块HTML
*/
renderPageChunks(pageIndex, ocrChunks, translatedChunks, options) {
const pageSize = 10;
const startIndex = pageIndex * pageSize;
const endIndex = Math.min(startIndex + pageSize, ocrChunks.length);
let html = '';
for (let i = startIndex; i < endIndex; i++) {
const chunkElement = this.renderSingleChunkImmediate({
index: i,
ocrChunk: ocrChunks[i],
translatedChunk: translatedChunks[i],
options
});
html += chunkElement;
}
return html;
}
/**
* 立即渲染单个分块(用于分页模式)
* @param {Object} item 分块数据
* @returns {string} 分块HTML
*/
renderSingleChunkImmediate(item) {
const { index, ocrChunk, translatedChunk, options } = item;
return `
`;
}
/**
* 导航到指定页面
* @param {number} direction 方向 (-1上一页, 1下一页)
*/
navigateToPage(direction) {
if (!window.largeDocumentData) return;
const data = window.largeDocumentData;
const totalPages = Math.ceil(data.ocrChunks.length / data.pageSize);
const newPage = Math.max(0, Math.min(totalPages - 1, data.currentPage + direction));
if (newPage === data.currentPage) return;
this.loadPage(newPage);
}
/**
* 跳转到指定页面
*/
jumpToPage() {
const pageInput = document.getElementById('page-input');
if (!pageInput || !window.largeDocumentData) return;
const targetPage = parseInt(pageInput.value) - 1; // 转换为0基索引
this.loadPage(targetPage);
}
/**
* 加载指定页面
* @param {number} pageIndex 页索引
*/
loadPage(pageIndex) {
if (!window.largeDocumentData) return;
const data = window.largeDocumentData;
const totalPages = Math.ceil(data.ocrChunks.length / data.pageSize);
if (pageIndex < 0 || pageIndex >= totalPages) return;
console.log(`[ChunkOptimizer] 加载第 ${pageIndex + 1} 页`);
// 显示加载指示器
const loadingIndicator = document.querySelector('.chunk-loading-indicator');
if (loadingIndicator) loadingIndicator.style.display = 'flex';
// 更新页面内容
setTimeout(() => {
const container = document.getElementById('chunk-compare-container');
if (container) {
container.innerHTML = this.renderPageChunks(
pageIndex,
data.ocrChunks,
data.translatedChunks,
data.options
) + `\n`;
// 观察新插入的分块,进入视口即懒加载完整内容
this.observeChunks(container);
}
// 更新页面状态
data.currentPage = pageIndex;
// 更新UI
this.updatePageUI(pageIndex, totalPages);
// 隐藏加载指示器
if (loadingIndicator) loadingIndicator.style.display = 'none';
// 滚动到顶部
const chunkContainer = document.getElementById('chunk-compare-container');
if (chunkContainer) chunkContainer.scrollTop = 0;
}, 100);
}
/**
* 更新分页UI
* @param {number} currentPage 当前页
* @param {number} totalPages 总页数
*/
updatePageUI(currentPage, totalPages) {
const currentPageSpan = document.getElementById('current-page');
const pageInput = document.getElementById('page-input');
const prevBtn = document.getElementById('prev-page-btn');
const nextBtn = document.getElementById('next-page-btn');
if (currentPageSpan) currentPageSpan.textContent = currentPage + 1;
if (pageInput) pageInput.value = currentPage + 1;
if (prevBtn) prevBtn.disabled = currentPage === 0;
if (nextBtn) nextBtn.disabled = currentPage === totalPages - 1;
}
/**
* 复制分块内容
* @param {number} chunkIndex 分块索引
*/
copyChunkContent(chunkIndex) {
if (!window.largeDocumentData) return;
const data = window.largeDocumentData;
const ocrContent = data.ocrChunks[chunkIndex] || '';
const transContent = data.translatedChunks[chunkIndex] || '';
const contentToCopy = `第 ${chunkIndex + 1} 块内容:\n\n原文:\n${ocrContent}\n\n译文:\n${transContent}`;
navigator.clipboard.writeText(contentToCopy)
.then(() => {
// 显示复制成功提示
this.showTemporaryMessage(`第 ${chunkIndex + 1} 块内容已复制到剪贴板`);
})
.catch(err => {
console.error('复制失败:', err);
this.showTemporaryMessage('复制失败,请手动选择复制', 'error');
});
}
/**
* 显示临时消息
* @param {string} message 消息内容
* @param {string} type 消息类型
*/
showTemporaryMessage(message, type = 'success') {
const messageEl = document.createElement('div');
messageEl.className = `temp-message temp-message-${type}`;
messageEl.textContent = message;
messageEl.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#10b981' : '#ef4444'};
color: white;
padding: 12px 20px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 10000;
font-size: 0.9em;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s ease;
`;
document.body.appendChild(messageEl);
// 显示动画
setTimeout(() => {
messageEl.style.opacity = '1';
messageEl.style.transform = 'translateY(0)';
}, 10);
// 3秒后移除
setTimeout(() => {
messageEl.style.opacity = '0';
messageEl.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (messageEl.parentElement) {
messageEl.remove();
}
}, 300);
}, 3000);
}
/**
* 创建虚拟化容器
* @param {number} totalChunks 总分块数量
* @returns {string} 容器HTML
*/
createVirtualContainer(totalChunks) {
// 对于大量分块,不使用虚拟化高度占位,避免巨大空白
const useVirtualization = totalChunks > 20;
const estimatedHeight = useVirtualization ? 0 : totalChunks * this.chunkHeight;
return `
分块对比 (${totalChunks}块)
`;
}
/**
* 计划分批渲染
* @param {Array} ocrChunks OCR分块
* @param {Array} translatedChunks 翻译分块
* @param {Object} options 选项
*/
scheduleProgressiveRender(ocrChunks, translatedChunks, options) {
this.renderQueue = [];
// 对于大量分块,采用更保守的渲染策略
const isLargeDocument = ocrChunks.length > 50;
const initialRenderCount = isLargeDocument ?
Math.min(3, ocrChunks.length) : // 大文档只渲染前3块
Math.min(this.batchSize, ocrChunks.length);
// 调整批次大小
const dynamicBatchSize = isLargeDocument ? 2 : this.batchSize;
for (let i = 0; i < ocrChunks.length; i++) {
this.renderQueue.push({
index: i,
ocrChunk: ocrChunks[i],
translatedChunk: translatedChunks[i],
priority: i < initialRenderCount ? 'high' : 'normal',
options
});
}
// 更新批次大小
this.currentBatchSize = dynamicBatchSize;
// 开始渲染
this.processRenderQueue();
}
/**
* 处理渲染队列
*/
async processRenderQueue() {
if (this.isRendering) return;
this.isRendering = true;
const container = document.getElementById('chunk-compare-container');
if (!container) {
this.isRendering = false;
return;
}
// 按优先级排序
this.renderQueue.sort((a, b) => {
if (a.priority === 'high' && b.priority !== 'high') return -1;
if (a.priority !== 'high' && b.priority === 'high') return 1;
return a.index - b.index;
});
let rendered = 0;
const total = this.renderQueue.length;
while (this.renderQueue.length > 0) {
const currentBatchSize = this.currentBatchSize || this.batchSize;
const batch = this.renderQueue.splice(0, currentBatchSize);
// 使用 requestIdleCallback 进行空闲时间渲染
const anchor = container.querySelector('.chunk-loading-indicator');
await this.renderBatchWithIdleTime(batch, container, anchor);
rendered += batch.length;
// 给浏览器时间处理其他任务,对大文档增加更多延迟
const delay = total > 100 ? 32 : 16; // 大文档使用更长延迟
await this.delay(delay);
}
this.isRendering = false;
// 设置交叉观察器
this.observeChunks(container);
console.log(`[ChunkOptimizer] 所有分块渲染完成`);
}
/**
* 在空闲时间渲染批次
* @param {Array} batch 待渲染的批次
* @param {Element} container 容器元素
*/
renderBatchWithIdleTime(batch, container, anchor) {
return new Promise((resolve) => {
const renderBatch = (deadline) => {
while (batch.length > 0 && deadline.timeRemaining() > 5) {
const item = batch.shift();
const chunkElement = this.renderSingleChunk(item);
if (chunkElement) {
if (anchor && anchor.parentNode === container) {
container.insertBefore(chunkElement, anchor);
} else {
container.appendChild(chunkElement);
}
}
}
if (batch.length > 0) {
// 还有未完成的渲染,继续下一个空闲周期
if (window.requestIdleCallback) {
requestIdleCallback(renderBatch, { timeout: 100 });
} else {
setTimeout(() => renderBatch({ timeRemaining: () => 16 }), 16);
}
} else {
resolve();
}
};
if (window.requestIdleCallback) {
requestIdleCallback(renderBatch, { timeout: 100 });
} else {
setTimeout(() => renderBatch({ timeRemaining: () => 16 }), 0);
}
});
}
/**
* 渲染单个分块
* @param {Object} item 分块数据
* @returns {Element} 分块DOM元素
*/
renderSingleChunk(item) {
const { index, ocrChunk, translatedChunk, options } = item;
const cacheKey = `${index}_${ocrChunk.length}_${translatedChunk.length}`;
// 检查缓存
if (this.renderCache.has(cacheKey)) {
const cachedElement = this.renderCache.get(cacheKey).cloneNode(true);
this.updateChunkElement(cachedElement, index);
return cachedElement;
}
const startTime = performance.now();
// 创建分块元素(仅使用 chunk-pair 作为容器,不添加多余类)
const chunkElement = document.createElement('div');
chunkElement.className = 'chunk-pair';
chunkElement.dataset.chunkIndex = index;
chunkElement.id = `chunk-${index}`;
// 延迟渲染复杂内容
chunkElement.innerHTML = this.createChunkPlaceholder(index, ocrChunk, translatedChunk);
// 缓存元素
this.renderCache.set(cacheKey, chunkElement.cloneNode(true));
const endTime = performance.now();
console.log(`[ChunkOptimizer] 分块 ${index} 渲染完成,耗时: ${(endTime - startTime).toFixed(2)}ms`);
return chunkElement;
}
/**
* 更新从缓存克隆出来的分块元素的索引相关属性
* @param {Element} el - 分块根元素(.chunk-pair)
* @param {number} index - 目标分块索引
*/
updateChunkElement(el, index) {
try {
if (!el) return;
// 根元素 id 与 dataset
el.id = `chunk-${index}`;
el.dataset.chunkIndex = index;
// 内层 block-outer 的索引
const outer = el.querySelector('.block-outer');
if (outer) outer.setAttribute('data-block-index', String(index));
// 懒加载容器保持即可;更新加载按钮的 onclick(若存在)
const loadBtn = el.querySelector('.load-full-content-btn');
if (loadBtn) {
loadBtn.setAttribute('onclick', `ChunkCompareOptimizer.instance.loadFullChunk(${index})`);
}
// 更新工具栏上的 data-block 标记
el.querySelectorAll('[data-block]').forEach(node => {
node.setAttribute('data-block', String(index));
});
} catch (e) {
console.warn('[ChunkOptimizer] updateChunkElement failed:', e);
}
}
/**
* 创建分块占位符
* @param {number} index 分块索引
* @param {string} ocrChunk OCR内容
* @param {string} translatedChunk 翻译内容
* @returns {string} 占位符HTML
*/
createChunkPlaceholder(index, ocrChunk, translatedChunk) {
return `
`;
}
/**
* 获取内容预览
* @param {string} content 原始内容
* @returns {string} 预览内容
*/
getContentPreview(content) {
if (!content) return '(空内容)';
// 移除Markdown语法和特殊字符,获取纯文本预览
const plainText = content
.replace(/[#*`]/g, '') // 移除Markdown符号
.replace(/\s+/g, ' ') // 合并空白字符
.trim();
return plainText.length > 100
? plainText.substring(0, 100) + '...'
: plainText;
}
/**
* 加载完整分块内容
* @param {number} index 分块索引
*/
async loadFullChunk(index) {
const chunkElement = document.querySelector(`.chunk-pair[data-chunk-index="${index}"]`);
if (!chunkElement) return;
const lazyContainer = chunkElement.querySelector('[data-lazy-load="true"]');
if (!lazyContainer) return;
// 显示加载状态
lazyContainer.innerHTML = '正在加载完整内容...
';
try {
// 获取原始数据
const ocrChunk = window.data?.ocrChunks?.[index] || '';
const translatedChunk = window.data?.translatedChunks?.[index] || '';
// 渲染完整内容
const fullContent = await this.renderFullChunkContent(
ocrChunk,
translatedChunk,
window.data?.images || [],
index,
window.data?.ocrChunks?.length || 0
);
// 用完整内容替换预览容器,避免预览样式残留在原文上方
lazyContainer.outerHTML = fullContent;
// 重新获取 chunkElement(节点结构发生了变化,但根容器不变)
const updatedChunkElement = document.querySelector(`.chunk-pair[data-chunk-index="${index}"]`) || chunkElement;
// 绑定事件
this.bindChunkEvents(updatedChunkElement);
// 应用当前比例到新插入的 align-flex 容器
try {
const ratio = (typeof window.chunkCompareRatio === 'number' && isFinite(window.chunkCompareRatio)) ? window.chunkCompareRatio : 0.5;
updatedChunkElement.querySelectorAll('.align-flex').forEach(flex => {
flex.style.setProperty('--ocr-ratio', (ratio * 100) + '%');
flex.style.setProperty('--trans-ratio', ((1 - ratio) * 100) + '%');
});
} catch (e) { /* ignore */ }
} catch (error) {
console.error(`加载分块 ${index} 失败:`, error);
lazyContainer.innerHTML = '加载失败,请重试
';
}
}
/**
* 渲染完整分块内容
* @param {string} ocrChunk OCR内容
* @param {string} translatedChunk 翻译内容
* @param {Array} images 图片数据
* @param {number} blockIndex 分块索引
* @param {number} totalBlocks 总分块数
* @returns {string} 完整内容HTML
*/
async renderFullChunkContent(ocrChunk, translatedChunk, images, blockIndex, totalBlocks) {
// 使用原有的渲染逻辑,但进行性能优化
const isOriginalFirstInChunkCompare = window.isOriginalFirstInChunkCompare !== false;
// 使用 Web Worker 进行 Markdown 解析(如果可用)
const ocrBlocks = await this.parseMarkdownAsync(ocrChunk);
const transBlocks = await this.parseMarkdownAsync(translatedChunk);
const aligned = this.alignBlocks(ocrBlocks, transBlocks);
let showMode = window[`showMode_block_${blockIndex}`] || 'both';
// 渲染工具栏
let html = `
`;
// 批量渲染对齐的内容
const alignedHTML = await this.renderAlignedContentAsync(aligned, images, blockIndex, isOriginalFirstInChunkCompare);
html += alignedHTML;
// 保存原始内容供复制使用
window[`blockRawContent_${blockIndex}`] = aligned;
return html;
}
/**
* 异步解析Markdown
* @param {string} markdown Markdown内容
* @returns {Promise} 解析结果
*/
parseMarkdownAsync(markdown) {
return new Promise((resolve) => {
// 简化的Markdown解析,避免阻塞主线程
const lines = (markdown || '').split(/\r?\n/);
const blocks = [];
let buffer = [];
let inCode = false;
let isFirstBlock = true;
const parseChunk = (startIndex) => {
const endIndex = Math.min(startIndex + 50, lines.length); // 每次处理50行
for (let i = startIndex; i < endIndex; i++) {
const line = lines[i];
if (/^\s*```/.test(line)) {
inCode = !inCode;
buffer.push(line);
continue;
}
if (inCode) {
buffer.push(line);
continue;
}
if (/^\s*#/.test(line)) {
if (!isFirstBlock && buffer.length) {
blocks.push({ content: buffer.join('\n') });
buffer = [];
}
isFirstBlock = false;
buffer.push(line);
continue;
}
buffer.push(line);
}
if (endIndex < lines.length) {
// 继续下一批
setTimeout(() => parseChunk(endIndex), 0);
} else {
// 完成解析
if (buffer.length) {
blocks.push({ content: buffer.join('\n') });
}
resolve(blocks);
}
};
parseChunk(0);
});
}
/**
* 异步渲染对齐内容
*/
async renderAlignedContentAsync(aligned, images, blockIndex, isOriginalFirstInChunkCompare) {
const alignedHTML = [];
const batchSize = 3; // 每批处理3个对齐块
for (let i = 0; i < aligned.length; i += batchSize) {
const batch = aligned.slice(i, i + batchSize);
const batchHTML = await this.renderAlignedBatch(batch, i, images, blockIndex, isOriginalFirstInChunkCompare);
alignedHTML.push(...batchHTML);
// 让出控制权
if (i + batchSize < aligned.length) {
await this.delay(0);
}
}
return alignedHTML.join('');
}
/**
* 渲染对齐批次
*/
async renderAlignedBatch(batch, startIndex, images, blockIndex, isOriginalFirstInChunkCompare) {
return batch.map((alignedPair, batchIndex) => {
const actualIndex = startIndex + batchIndex;
const showMode = window[`showMode_block_${blockIndex}`] || 'both';
return `
原文
${this.renderContentSafely(alignedPair[0], images)}
译文
${this.renderContentSafely(alignedPair[1], images)}
`;
});
}
/**
* 安全渲染内容
*/
renderContentSafely(content, images) {
try {
if (!content || content.trim() === '') return '';
// 使用简化的渲染避免复杂的KaTeX解析
if (window.MarkdownProcessor?.renderWithKatexFailback) {
const safeContent = window.MarkdownProcessor.safeMarkdown(content, images);
return window.MarkdownProcessor.renderWithKatexFailback(safeContent);
} else {
// 回退到简单的文本渲染
return content.replace(/\n/g, '
');
}
} catch (error) {
console.warn('内容渲染失败,使用简单模式:', error);
return content.replace(/\n/g, '
');
}
}
/**
* 对齐分块
*/
alignBlocks(blocks1, blocks2) {
const maxLen = Math.max(blocks1.length, blocks2.length);
const aligned = [];
for (let i = 0; i < maxLen; i++) {
aligned.push([
blocks1[i] ? blocks1[i].content : '',
blocks2[i] ? blocks2[i].content : ''
]);
}
return aligned;
}
/**
* 绑定分块事件
*/
bindChunkEvents(chunkElement) {
// 这里可以添加特定的事件绑定逻辑
// 例如模式切换、复制按钮等
}
/**
* 设置交叉观察器进行懒加载
*/
setupIntersectionObserver() {
if (!window.IntersectionObserver) return;
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const chunkIndex = parseInt(entry.target.dataset.chunkIndex);
const lazyContainer = entry.target.querySelector('[data-lazy-load="true"]');
if (lazyContainer) {
// 自动加载进入视口的分块
this.loadFullChunk(chunkIndex);
}
}
});
}, {
rootMargin: '200px', // 提前200px开始加载
threshold: 0.1
});
}
/**
* 观察分块元素
*/
observeChunks(container) {
if (!this.observer) return;
const chunks = container.querySelectorAll('.chunk-pair');
chunks.forEach(chunk => {
this.observer.observe(chunk);
});
}
/**
* 设置性能监控
*/
setupPerformanceMonitor() {
// 监控内存使用
if (performance.memory) {
setInterval(() => {
const memory = performance.memory;
console.log(`[ChunkOptimizer] 内存使用: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB`);
}, 30000); // 每30秒检查一次
}
}
/**
* 更新进度
*/
updateProgress(rendered, total) {
const progress = Math.round((rendered / total) * 100);
console.log(`[ChunkOptimizer] 渲染进度: ${progress}% (${rendered}/${total})`);
// 可以在这里更新UI进度条
const progressBar = document.querySelector('.chunk-progress-bar');
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
}
/**
* 延迟函数
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 清理资源
*/
cleanup() {
if (this.observer) {
this.observer.disconnect();
}
this.renderCache.clear();
this.visibleChunks.clear();
}
}
// 创建全局实例
ChunkCompareOptimizer.instance = new ChunkCompareOptimizer();
// 在页面加载时初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
ChunkCompareOptimizer.instance.init();
});
} else {
ChunkCompareOptimizer.instance.init();
}
// 导出供其他模块使用
if (typeof module !== 'undefined' && module.exports) {
module.exports = ChunkCompareOptimizer;
} else {
window.ChunkCompareOptimizer = ChunkCompareOptimizer;
}