原因:原文块数量 (${data.ocrChunks.length}) 与译文块数量 (${data.translatedChunks.length}) 不匹配。
`;
} else {
html += ' 但包含表格 Markdown,尝试直接渲染表格
if (hasTableSyntax && htmlStr.trim().startsWith(',尝试直接提取并渲染表格部分');
// 提取
中的文本内容
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlStr;
const pElement = tempDiv.querySelector('p');
if (pElement && pElement.textContent.includes('|')) {
const tableMarkdown = pElement.textContent;
// 重新渲染表格
if (typeof MarkdownProcessorAST !== 'undefined' && MarkdownProcessorAST.render) {
htmlStr = MarkdownProcessorAST.render(tableMarkdown, data.images);
console.log('[renderBatch] 重新渲染表格成功');
}
}
}
const wrap = document.createElement('div');
wrap.innerHTML = htmlStr;
while(wrap.firstChild) fragment.appendChild(wrap.firstChild);
}
contentContainer.appendChild(fragment);
if(startIdx+batchSizerenderBatch(startIdx+batchSize, onDoneAllBatchesCallback),0); // Pass callback along
} else {
// 所有批次渲染完成
if(tab==='ocr') console.timeEnd('[性能] OCR分批渲染');
if(tab==='translation') console.timeEnd('[性能] 翻译分批渲染');
// Use requestAnimationFrame to allow browser to paint/layout before further DOM manipulation
requestAnimationFrame(() => {
console.log(`[showTab - ${tab}] RAF triggered after all batches rendered.`);
const currentTabContentWrapper = document.getElementById(contentContainerId);
if (currentTabContentWrapper) {
adjustLongHeadingsToParagraphs(currentTabContentWrapper);
// 兜底修复:将相对图片 src 替换为 data URL(若前置替换未命中)
try {
const imgs = (window.data && Array.isArray(window.data.images)) ? window.data.images : [];
if (imgs && imgs.length > 0) {
const map = new Map();
imgs.forEach((im, idx) => {
const id = (im.id || '').toString();
const name = (im.name || id || `img-${idx+1}.jpg`).toString();
const base = id || name;
const keys = [
base,
name,
`images/${base}`,
`images/${name}`,
base.replace(/\.[^.]+$/, ''),
name.replace(/\.[^.]+$/, '')
];
const dataUri = (im.data && im.data.startsWith && im.data.startsWith('data:')) ? im.data : `data:image/jpeg;base64,${im.data || ''}`;
keys.forEach(k => { if (k) map.set(k, dataUri); });
});
currentTabContentWrapper.querySelectorAll('img').forEach(imgEl => {
const src = imgEl.getAttribute('src') || '';
if (!src || /^data:|^https?:|^\/\//i.test(src)) return;
const clean = src.split('?')[0].split('#')[0].replace(/^\.\//, '');
const nameOnly = clean.split('/').pop();
const candidates = [clean, nameOnly, `images/${nameOnly}`];
for (const key of candidates) {
if (map.has(key)) {
imgEl.setAttribute('src', map.get(key));
break;
}
}
});
}
} catch (e) { console.warn('[showTab] fix images fallback failed:', e); }
}
// Now, call segmentInBatches on the fully rendered content
activeContentElement = document.getElementById(contentContainerId); // Re-affirm activeContentElement
if (activeContentElement) {
segmentInBatches(activeContentElement, 10, 50, () => {
// This is the onDone callback for segmentInBatches
// All segmentation is complete, now apply annotations and listeners
// 后处理:渲染表格和其他地方的纯文本公式(异步版本,不阻塞主线程)
console.log('[showTab] 开始后处理公式(异步)');
console.time('[性能] 公式渲染');
// 优先使用异步 Worker 版本(页面显示时)
if (typeof FormulaPostProcessorAsync !== 'undefined') {
FormulaPostProcessorAsync.processFormulasInElement(activeContentElement, {
useWorker: true, // 页面显示时使用 Worker,避免阻塞主线程
onComplete: () => {
console.timeEnd('[性能] 公式渲染');
console.log('[showTab] 公式渲染完成');
}
});
} else if (typeof FormulaPostProcessor !== 'undefined' && FormulaPostProcessor.processFormulasInElement) {
// 回退到同步版本(如果异步版本未加载)
console.warn('[showTab] 异步公式处理器不可用,使用同步版本');
FormulaPostProcessor.processFormulasInElement(activeContentElement);
console.timeEnd('[性能] 公式渲染');
}
if (data && data.annotations && typeof window.applyBlockAnnotations === 'function') {
window.applyBlockAnnotations(activeContentElement, data.annotations, contentIdentifier);
}
// Finally, update Dock stats and TOC as content and structure are finalized
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function') {
console.log(`[showTab - ${tab}] OCR/Translation: segmentInBatches done, forcing Dock stats update.`);
window.DockLogic.updateStats(window.data, currentVisibleTabId);
}
if (typeof window.refreshTocList === 'function') {
console.log(`[showTab - ${tab}] OCR/Translation: segmentInBatches done, forcing TOC refresh.`);
window.refreshTocList();
}
// ========== 渲染完成,解锁 ==========='
renderingTab = null;
// ====================================
// ========== 内容加载完成 =============
window.contentReady = true;
console.log('[DEBUG] window.contentReady = true (after OCR/Translation segmentInBatches)');
// Phase 2.3: 初始化批注系统 DOM 缓存
if (window.AnnotationDOMCache) {
window.AnnotationDOMCache.init();
}
// ====================================
});
} else {
// ========== 渲染完成,解锁 ==========='
renderingTab = null;
// ====================================
// ========== 内容加载完成 =============
window.contentReady = true;
console.log('[DEBUG] window.contentReady = true (after OCR/Translation segmentInBatches, no activeContentElement)');
// Phase 2.3: 初始化批注系统 DOM 缓存
if (window.AnnotationDOMCache) {
window.AnnotationDOMCache.init();
}
// ====================================
}
}); // End of requestAnimationFrame
}
}
// Start rendering batches and provide a callback for when all are done
renderBatch(0, () => {
// This callback is executed after all batches for OCR/Translation are rendered
console.log(`[showTab - ${tab}] All batches rendered. Proceeding with DOM processing.`);
// Use requestAnimationFrame to allow browser to paint/layout before further DOM manipulation
requestAnimationFrame(() => {
console.log(`[showTab - ${tab}] RAF triggered after all batches rendered.`);
const currentTabContentWrapper = document.getElementById(contentContainerId);
if (currentTabContentWrapper) {
adjustLongHeadingsToParagraphs(currentTabContentWrapper);
}
// Now, call segmentInBatches on the fully rendered content
activeContentElement = document.getElementById(contentContainerId); // Re-affirm activeContentElement
if (activeContentElement) {
segmentInBatches(activeContentElement, 10, 50, () => {
// This is the onDone callback for segmentInBatches
// All segmentation is complete, now apply annotations and listeners
// 后处理:渲染表格和其他地方的纯文本公式(异步版本,不阻塞主线程)
console.log('[showTab] 开始后处理公式(异步)');
console.time('[性能] 公式渲染');
// 优先使用异步 Worker 版本(页面显示时)
if (typeof FormulaPostProcessorAsync !== 'undefined') {
FormulaPostProcessorAsync.processFormulasInElement(activeContentElement, {
useWorker: true, // 页面显示时使用 Worker,避免阻塞主线程
onComplete: () => {
console.timeEnd('[性能] 公式渲染');
console.log('[showTab] 公式渲染完成');
}
});
} else if (typeof FormulaPostProcessor !== 'undefined' && FormulaPostProcessor.processFormulasInElement) {
// 回退到同步版本(如果异步版本未加载)
console.warn('[showTab] 异步公式处理器不可用,使用同步版本');
FormulaPostProcessor.processFormulasInElement(activeContentElement);
console.timeEnd('[性能] 公式渲染');
}
if (data && data.annotations && typeof window.applyBlockAnnotations === 'function') {
window.applyBlockAnnotations(activeContentElement, data.annotations, contentIdentifier);
}
// Finally, update Dock stats and TOC as content and structure are finalized
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function') {
console.log(`[showTab - ${tab}] OCR/Translation: segmentInBatches done, forcing Dock stats update.`);
window.DockLogic.updateStats(window.data, currentVisibleTabId);
}
if (typeof window.refreshTocList === 'function') {
console.log(`[showTab - ${tab}] OCR/Translation: segmentInBatches done, forcing TOC refresh.`);
window.refreshTocList();
}
// ========== 渲染完成,解锁 ==========='
renderingTab = null;
// ====================================
// ========== 内容加载完成 =============
window.contentReady = true;
console.log('[DEBUG] window.contentReady = true (after OCR/Translation segmentInBatches)');
// Phase 2.3: 初始化批注系统 DOM 缓存
if (window.AnnotationDOMCache) {
window.AnnotationDOMCache.init();
}
try {
// 通知其他模块(例如参考文献管理器)内容已渲染
document.dispatchEvent(new CustomEvent('contentRendered', { detail: { tab } }));
} catch (e) { /* no-op */ }
// ====================================
});
} else {
// ========== 渲染完成,解锁 ==========='
renderingTab = null;
// ====================================
// ========== 内容加载完成 =============
window.contentReady = true;
console.log('[DEBUG] window.contentReady = true (after OCR/Translation segmentInBatches, no activeContentElement)');
// Phase 2.3: 初始化批注系统 DOM 缓存
if (window.AnnotationDOMCache) {
window.AnnotationDOMCache.init();
}
try {
// 通知其他模块(例如参考文献管理器)内容已渲染
document.dispatchEvent(new CustomEvent('contentRendered', { detail: { tab } }));
} catch (e) { /* no-op */ }
// ====================================
}
}); // End of requestAnimationFrame
});
}
// NEW: Adjust long headings. This should be called AFTER innerHTML is set
// and BEFORE refreshTocList is called.
// MOVED: adjustLongHeadingsToParagraphs is now called after renderBatch completes for OCR/Translation
// const tabContentElement = document.getElementById('tabContent');
// if (tabContentElement) {
// adjustLongHeadingsToParagraphs(tabContentElement);
// }
// MOVED: refreshTocList is now called later for OCR/Translation
// if (typeof window.refreshTocList === 'function') {
// window.refreshTocList(); // 更新TOC
// }
// Update reading progress when tab changes and content is rendered - CALLING DOCK_LOGIC
if (window.DockLogic && typeof window.DockLogic.forceUpdateReadingProgress === 'function') {
window.DockLogic.forceUpdateReadingProgress();
}
// 如果是分块对比视图,并且按钮存在,则绑定事件
if (tab === 'chunk-compare') {
const swapBtn = document.getElementById('swap-chunks-btn');
if (swapBtn) {
swapBtn.onclick = function() {
isOriginalFirstInChunkCompare = !isOriginalFirstInChunkCompare;
showTab('chunk-compare'); // 重新渲染分块对比视图
};
}
}
// After tab content is updated, refresh chatbot UI if it's open
if (window.isChatbotOpen && typeof window.ChatbotUI !== 'undefined' && typeof window.ChatbotUI.updateChatbotUI === 'function') {
window.ChatbotUI.updateChatbotUI();
}
// 应用高亮和批注
// MOVED: The logic for applying annotations and adding listeners for OCR/Translation
// is now inside the callback chain starting from renderBatch a few lines above.
/*
if ((tab === 'ocr' || tab === 'translation') && contentContainerId) {
activeContentElement = document.getElementById(contentContainerId);
if (activeContentElement) {
// 分批异步分割子块,避免一次性阻塞
function segmentInBatches(containerElement, batchSize = 10, delay = 50, onDone) {
const blocks = Array.from(containerElement.children).filter(node => node.nodeType === Node.ELEMENT_NODE);
let i = 0;
function runBatch() {
const end = Math.min(i + batchSize, blocks.length);
for (; i < end; i++) {
const el = blocks[i];
el.dataset.blockIndex = String(i);
if (window.SubBlockSegmenter && typeof window.SubBlockSegmenter.segment === 'function') {
// 强制分段,保证英文/短段也有子块,便于精确高亮
window.SubBlockSegmenter.segment(el, i, true);
} else {
console.error("SubBlockSegmenter.segment is not available.");
}
}
if (i < blocks.length) {
setTimeout(runBatch, delay);
} else {
// 所有父块的子块分割完成
onDone && onDone();
}
}
runBatch();
}
segmentInBatches(activeContentElement, 10, 50, () => {
// 所有分割完成后,应用批注和监听器
if (data && data.annotations && typeof window.applyBlockAnnotations === 'function') {
window.applyBlockAnnotations(activeContentElement, data.annotations, window.globalCurrentContentIdentifier);
}
// **** 新增:在所有子块分割和批注应用完成后,再次更新Dock统计 ****
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function') {
console.log(`[showTab - ${tab}] OCR/Translation content and annotations processed, forcing Dock stats update.`);
window.DockLogic.updateStats(window.data, currentVisibleTabId);
}
// TOC refresh will be handled later
});
}
} else if (tab === 'chunk-compare') {
*/
// The chunk-compare logic for annotations and Dock stats remains, as it has its own processing path.
// We only moved the OCR/Translation specific part.
if (tab === 'chunk-compare') { // This is the original start of the else if block
// 对于分块对比视图,为每个原文和译文块单独处理
setTimeout(() => { //确保DOM更新完毕
// 清理工具:移除区域末尾的换行/空白/空段落,避免不可见换行导致高度不齐
function trimTrailingBreaks(area) {
if (!area) return;
try {
function isWhitespaceText(n) {
if (!n || n.nodeType !== Node.TEXT_NODE) return false;
let t = n.textContent || '';
t = t.replace(/\u00A0/g, ' '); // NBSP → space
t = t.replace(/[\u200B-\u200D\uFEFF]/g, ''); // zero-width
return /^\s*$/.test(t);
}
function isEmptyElement(n) {
if (!n || n.nodeType !== Node.ELEMENT_NODE) return false;
// 没有可见文本与可见子节点(如图片/表格)
const hasMedia = n.querySelector('img, table, video, svg');
let txt = (n.textContent || '');
txt = txt.replace(/\u00A0/g, ' ');
txt = txt.replace(/[\u200B-\u200D\uFEFF]/g, '');
txt = txt.replace(/\s+/g, '');
return !hasMedia && txt.length === 0;
}
function removeTrailingIn(el) {
let node = el && el.lastChild;
// 先清理内部子节点的末尾
与空白
if (node && node.nodeType === Node.ELEMENT_NODE) {
while (node.lastChild && (node.lastChild.nodeType === Node.ELEMENT_NODE && node.lastChild.tagName.toLowerCase() === 'br' || isWhitespaceText(node.lastChild))) {
node.removeChild(node.lastChild);
}
}
// 再清理当前容器的末尾
while (node) {
if (isWhitespaceText(node)) {
const prev = node.previousSibling; el.removeChild(node); node = prev; continue;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const tag = node.tagName.toLowerCase();
if (tag === 'br') { const prev = node.previousSibling; el.removeChild(node); node = prev; continue; }
// 清理元素内末尾
以及纯空元素
while (node.lastChild && (node.lastChild.nodeType === Node.ELEMENT_NODE && node.lastChild.tagName.toLowerCase() === 'br' || isWhitespaceText(node.lastChild))) {
node.removeChild(node.lastChild);
}
if (isEmptyElement(node)) { const prev = node.previousSibling; el.removeChild(node); node = prev; continue; }
}
break;
}
}
removeTrailingIn(area);
} catch (e) { /* ignore */ }
}
const ocrContentAreas = document.querySelectorAll('.chunk-compare-container .align-block-ocr .align-content.markdown-body');
const transContentAreas = document.querySelectorAll('.chunk-compare-container .align-block-trans .align-content.markdown-body');
let areasProcessed = 0;
const totalAreasToProcess = ocrContentAreas.length + transContentAreas.length;
function singleAreaProcessed() {
areasProcessed++;
if (areasProcessed === totalAreasToProcess) {
// 所有分块对比区域处理完毕后更新Dock统计和TOC
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function') {
console.log(`[showTab - ${tab}] Chunk-compare: all areas processed, forcing Dock stats update.`);
window.DockLogic.updateStats(window.data, currentVisibleTabId);
}
if (typeof window.refreshTocList === 'function') {
console.log(`[showTab - ${tab}] Chunk-compare: all areas processed, forcing TOC refresh.`);
window.refreshTocList();
}
}
}
function processContentAreaAsync(area, isOcrArea, callback) { // Renamed for clarity
if (!area.id) {
area.id = 'chunk-content-' + _page_generateUUID();
}
// 渲染完成后立即清理尾部换行/空白
trimTrailingBreaks(area);
const effectiveContentIdentifier = isOriginalFirstInChunkCompare ? (isOcrArea ? 'ocr' : 'translation') : (isOcrArea ? 'translation' : 'ocr');
const blockElements = Array.from(area.children).filter(node => node.nodeType === Node.ELEMENT_NODE);
let i = 0;
const batchSize = 5;
function runChunkSubBatch() {
const end = Math.min(i + batchSize, blockElements.length);
for (; i < end; i++) {
const element = blockElements[i];
element.dataset.blockIndex = String(i);
if (typeof window.SubBlockSegmenter !== 'undefined' && typeof window.SubBlockSegmenter.segment === 'function') {
window.SubBlockSegmenter.segment(element, i);
} else {
console.error("SubBlockSegmenter.segment is not available for chunk processing.");
}
}
if (i < blockElements.length) {
setTimeout(runChunkSubBatch, 20);
} else {
// 后处理:渲染表格和其他地方的纯文本公式
if (typeof FormulaPostProcessor !== 'undefined' && FormulaPostProcessor.processFormulasInElement) {
FormulaPostProcessor.processFormulasInElement(area);
}
if (data && data.annotations && typeof window.applyBlockAnnotations === 'function') {
const annotationsToApply = (currentVisibleTabId === 'chunk-compare') ? [] : data.annotations;
window.applyBlockAnnotations(area, annotationsToApply, effectiveContentIdentifier);
}
callback(); // Signal completion for this area
}
}
runChunkSubBatch();
}
if (totalAreasToProcess === 0) { // Handle case with no content areas
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function') {
console.log(`[showTab - ${tab}] Chunk-compare has no content areas, forcing Dock stats update.`);
window.DockLogic.updateStats(window.data, currentVisibleTabId);
}
// TOC refresh will be handled later -> Actually, should be called here too if no areas.
if (typeof window.refreshTocList === 'function') {
console.log(`[showTab - ${tab}] Chunk-compare has no content areas, forcing TOC refresh.`);
window.refreshTocList();
}
} else {
ocrContentAreas.forEach(area => processContentAreaAsync(area, true, singleAreaProcessed));
transContentAreas.forEach(area => processContentAreaAsync(area, false, singleAreaProcessed));
}
}, 0);
// ========== 渲染完成,解锁 ===========
renderingTab = null;
// ====================================
}
// Attempt to restore scroll position for the current tab
if (docIdForLocalStorage && currentVisibleTabId) {
// 添加模式标识到存储键中
const isImmersive = window.ImmersiveLayout && window.ImmersiveLayout.isActive();
const modePrefix = isImmersive ? 'immersive_' : 'normal_';
const scrollKey = `scrollPos_${modePrefix}${docIdForLocalStorage}_${currentVisibleTabId}`;
const savedScrollTop = localStorage.getItem(scrollKey);
console.log(`[showTab] 尝试恢复滚动位置: ${scrollKey}, 保存的值: ${savedScrollTop}, 沉浸模式: ${isImmersive ? '是' : '否'}`);
if (savedScrollTop !== null && !isNaN(parseInt(savedScrollTop, 10))) {
const scrollableElement = getCurrentScrollableElementForHistoryDetail(); // MODIFIED
if (scrollableElement) {
console.log(`[showTab] 找到可滚动元素:`, {
元素ID: scrollableElement.id || '无ID',
元素类名: scrollableElement.className || '无类名',
元素标签: scrollableElement.tagName,
当前scrollTop: scrollableElement.scrollTop,
将要设置的scrollTop: parseInt(savedScrollTop, 10),
scrollHeight: scrollableElement.scrollHeight,
clientHeight: scrollableElement.clientHeight,
路径: getElementPath(scrollableElement)
});
// 使用多次尝试确保滚动位置被正确设置
const scrollTopToSet = parseInt(savedScrollTop, 10);
let attemptCount = 0;
function attemptToSetScroll() {
if (currentVisibleTabId !== tab) {
console.log(`[showTab] 标签已切换,取消恢复滚动位置`);
return;
}
attemptCount++;
console.log(`[showTab] 第${attemptCount}次尝试设置滚动位置: ${scrollTopToSet}`);
scrollableElement.scrollTop = scrollTopToSet;
// 检查是否成功设置
setTimeout(() => {
const currentScrollTop = scrollableElement.scrollTop;
const difference = Math.abs(scrollTopToSet - currentScrollTop);
console.log(`[showTab] 设置后检查: 预期=${scrollTopToSet}, 实际=${currentScrollTop}, 差值=${difference}`);
// 如果差异大于阈值且尝试次数小于最大次数,则重试
if (difference > 5 && attemptCount < 8) {
console.warn(`[showTab] 警告: 滚动位置设置可能未生效! 将在300ms后重试...`);
// 等待布局稳定(scrollHeight 两次相等)后再重试
let checks = 0;
let lastHeight = scrollableElement.scrollHeight;
const interval = setInterval(() => {
const h = scrollableElement.scrollHeight;
if (h === lastHeight || checks > 5) {
clearInterval(interval);
requestAnimationFrame(() => setTimeout(attemptToSetScroll, 50));
} else {
lastHeight = h;
checks++;
}
}, 80);
} else if (difference > 5) {
console.warn(`[showTab] 警告: 滚动位置设置失败,已达到最大尝试次数`);
} else {
console.log(`[showTab] 滚动位置设置成功!`);
}
}, 50);
}
// 使用requestAnimationFrame确保DOM已更新
requestAnimationFrame(() => {
if(currentVisibleTabId === tab) { // Ensure tab hasn't changed during async operation
attemptToSetScroll();
} else {
console.log(`[showTab] 标签已切换,取消恢复滚动位置`);
}
});
} else {
console.warn(`[showTab] 未找到可滚动元素,无法恢复滚动位置`);
}
} else {
console.log(`[showTab] 没有保存的滚动位置或值无效: ${savedScrollTop}`);
}
} else {
console.log(`[showTab] 缺少必要参数: docId=${docIdForLocalStorage}, tabId=${currentVisibleTabId}`);
}
// Call updateReadingProgress after potential scroll restoration and content rendering - CALLING DOCK_LOGIC
// 延迟调用,确保滚动位置恢复后再更新阅读进度
setTimeout(() => {
if (window.DockLogic && typeof window.DockLogic.forceUpdateReadingProgress === 'function') {
console.log("[showTab] 延迟调用 forceUpdateReadingProgress 更新阅读进度");
window.DockLogic.forceUpdateReadingProgress();
}
}, 300);
// 新增:每次渲染后都绑定 scroll 事件到正确容器
if (window.DockLogic && typeof window.DockLogic.bindScrollForCurrentScrollable === 'function') {
window.DockLogic.bindScrollForCurrentScrollable();
}
// 重新绑定滚动事件以保存滚动位置
if (typeof bindScrollForSavePosition === 'function') {
bindScrollForSavePosition();
}
// 性能测试断点 - 总渲染结束
console.timeEnd('[性能] showTab_总渲染');
}