/** * 分块对比预览性能优化器 * 解决历史详情页分块对比的渲染性能问题 */ 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}块)

检测到大型文档,已启用高效浏览模式。使用分页浏览以获得更好的性能。
1 页,共 ${totalPages} 页
跳转到第
${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 `

第 ${index + 1} 块

原文: ${ocrChunk.length}字 译文: ${translatedChunk.length}字
正在加载完整内容...
`; } /** * 导航到指定页面 * @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
\n
\n 正在加载分块...\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 `

分块 ${index + 1}

原文: ${ocrChunk.length}字 译文: ${translatedChunk.length}字
正在加载完整内容...
`; } /** * 获取内容预览 * @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 = `
对比 原文 译文
${blockIndex > 0 ? `` : ''} ${blockIndex < totalBlocks-1 ? `` : ''}
`; // 批量渲染对齐的内容 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; }