/** * @namespace TocFeature - Enhanced Modern Version * @description 管理页面侧边浮动的现代化目录 (Table of Contents) 功能。 * 包括TOC按钮的点击事件、TOC悬浮窗的显示/隐藏、 * 智能层级识别、平滑滚动导航以及动态生成TOC列表项。 * * 特性: * - 现代化UI设计 * - 智能标题识别 * - 平滑动画过渡 * - 响应式布局 * - 层级可视化 * - 快速导航 */ (function EnhancedTocFeature(){ const tocBtn = document.getElementById('toc-float-btn'); const tocPopup = document.getElementById('toc-popup'); const tocList = document.getElementById('toc-list'); const tocCloseBtn = document.getElementById('toc-popup-close-btn'); // 智能TOC缓存系统 let tocCache = { lastUpdate: 0, structure: null, nodes: [], clearCache: function() { this.lastUpdate = 0; this.structure = null; this.nodes = []; }, isValid: function() { return Date.now() - this.lastUpdate < 30000; // 30秒缓存 } }; // 性能监控 let performanceMetrics = { renderTime: 0, nodeCount: 0, structureComplexity: 0 }; // 智能观察器,监控DOM变化 let contentObserver = null; // 创建内容变化观察器 function initContentObserver() { if (!window.MutationObserver) return; contentObserver = new MutationObserver(function(mutations) { let shouldRefresh = false; mutations.forEach(function(mutation) { if (mutation.type === 'childList') { // 检查是否有标题元素的变化 mutation.addedNodes.forEach(function(node) { if (node.nodeType === 1 && (node.matches && node.matches('h1,h2,h3,h4,h5,h6,p') || node.querySelector && node.querySelector('h1,h2,h3,h4,h5,h6,p'))) { shouldRefresh = true; } }); } }); if (shouldRefresh) { tocCache.clearCache(); if (tocPopup.classList.contains('toc-popup-visible')) { setTimeout(renderTocList, 500); // 延迟刷新避免频繁更新 } } }); const container = document.querySelector('.container'); if (container) { contentObserver.observe(container, { childList: true, subtree: true }); } } // 当前 TOC 显示模式:both, ocr, translation let currentTocMode = 'both'; // TOC 偏好设置 let tocPreferences = { autoExpand: localStorage.getItem('toc-auto-expand') !== 'false', showPreview: localStorage.getItem('toc-show-preview') !== 'false', compactMode: localStorage.getItem('toc-compact-mode') === 'true', smartGrouping: localStorage.getItem('toc-smart-grouping') !== 'false' }; // 添加 TOC 模式切换按钮容器,改为现代化标签页形式 let tocModeSelector = document.createElement('div'); tocModeSelector.className = 'toc-mode-selector'; tocModeSelector.innerHTML = ` `; // 将模式选择器插入到 TOC 弹窗头部下方 if (tocPopup) { const tocHeader = tocPopup.querySelector('#toc-popup-header'); if (tocHeader) { tocHeader.parentNode.insertBefore(tocModeSelector, tocHeader.nextSibling); } } // 绑定模式切换按钮事件 tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(btn => { btn.addEventListener('click', function() { const mode = this.dataset.mode; if (currentTocMode === mode) return; // 避免重复切换 currentTocMode = mode; tocCache.clearCache(); // 清除缓存 // 更新按钮状态 tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(b => { b.classList.remove('active'); }); this.classList.add('active'); // 添加切换动画 const tocListElement = document.getElementById('toc-list'); tocListElement.style.opacity = '0.5'; tocListElement.style.transform = 'translateY(10px)'; // 重新渲染 TOC 列表 setTimeout(() => { renderTocList(); tocListElement.style.opacity = ''; tocListElement.style.transform = ''; }, 150); }); }); // 添加现代化底部控制区域 const tocControls = document.createElement('div'); tocControls.className = 'toc-controls'; tocControls.innerHTML = ` `; // 将控制区域添加到TOC弹窗 if (tocPopup) { tocPopup.appendChild(tocControls); } /** * TOC映射表 - 支持更多语言对照 */ const tocMap = { '历史详情': 'History Detail', 'OCR内容': 'OCR Content', '仅OCR': 'OCR Only', '翻译内容': 'Translation', '仅翻译': 'Translation Only', '分块对比': 'Chunk Compare', '摘要': 'Abstract', '引言': 'Introduction', '方法': 'Methods', '结果': 'Results', '讨论': 'Discussion', '结论': 'Conclusion', '参考文献': 'References', '附录': 'Appendix' }; /** * 存储TOC列表项对应的页面内标题DOM元素 */ let tocNodes = []; /** * 现代化智能文本截断函数 * @param {string} text - 要截断的文本 * @param {number} maxLength - 最大长度 * @returns {string} 截断后的文本 */ function smartTruncateText(text, maxLength = 35) { if (!text || text.length <= maxLength) return text; // 检查是否是图表标题 const isChartTitle = /^(图|表|Figure|Table)\s*\d+/i.test(text); // 对于图表标题,使用更智能的截断策略 if (isChartTitle) { const titleMatch = text.match(/^(图|表|Figure|Table)\s*\d+[\.:\:]?\s*(.*)$/i); if (titleMatch) { const prefix = titleMatch[1]; const content = titleMatch[2] || ''; // 在内容中查找合适的截断点 const sentenceEnd = content.search(/[。!?\.!?]/); if (sentenceEnd > 0 && sentenceEnd <= maxLength - prefix.length - 5) { return prefix + content.substring(0, sentenceEnd + 1); } } } // 智能截断:优先在标点符号处截断 const punctuationRegex = /[。,!?;:、\.,!?;:]/g; let match; let lastPunctIndex = -1; while ((match = punctuationRegex.exec(text)) !== null) { if (match.index < maxLength - 3) { lastPunctIndex = match.index; } else { break; } } if (lastPunctIndex > maxLength * 0.6) { return text.substring(0, lastPunctIndex + 1); } // 在空格处截断 const spaceIndex = text.lastIndexOf(' ', maxLength - 3); if (spaceIndex > maxLength * 0.7) { return text.substring(0, spaceIndex) + '...'; } // 最后使用硬截断 return text.substring(0, maxLength - 3) + '...'; } /** * 现代化临时加载效果,带进度指示 * @param {string} sectionName - 正在导航到的章节名称 */ function showEnhancedLoadingEffect(sectionName) { let effectDiv = document.getElementById('toc-loading-effect'); const mainContainer = document.querySelector('.container'); if (!effectDiv) { effectDiv = document.createElement('div'); effectDiv.id = 'toc-loading-effect'; effectDiv.className = 'loading-effect'; document.body.appendChild(effectDiv); } if (mainContainer) { mainContainer.classList.add('content-blurred'); } const truncatedSectionName = smartTruncateText(sectionName, 30); effectDiv.innerHTML = `
正在前往
${truncatedSectionName}
`; requestAnimationFrame(() => { effectDiv.classList.add('loading-effect-visible'); // 启动进度条动画 const progressBar = effectDiv.querySelector('.progress-bar'); if (progressBar) { progressBar.style.width = '0%'; setTimeout(() => { progressBar.style.width = '100%'; }, 100); } }); setTimeout(() => { effectDiv.classList.remove('loading-effect-visible'); if (mainContainer) { mainContainer.classList.remove('content-blurred'); } }, 1800); } /** * 现代化平滑切换TOC项的折叠状态 * @param {HTMLElement} toggleBtn - 折叠/展开按钮元素 * @param {HTMLElement} childrenContainer - 子项容器元素 */ function toggleTocItem(toggleBtn, childrenContainer) { const isCollapsed = toggleBtn.classList.contains('collapsed'); const listItem = toggleBtn.closest('li'); if (isCollapsed) { // 展开动画 toggleBtn.classList.remove('collapsed'); childrenContainer.classList.remove('collapsed'); // 计算目标高度 childrenContainer.style.height = '0'; childrenContainer.style.opacity = '0'; childrenContainer.style.transform = 'translateY(-10px)'; const targetHeight = Array.from(childrenContainer.children) .reduce((height, child) => height + child.offsetHeight, 0); // 触发动画 requestAnimationFrame(() => { childrenContainer.style.height = targetHeight + 'px'; childrenContainer.style.opacity = '1'; childrenContainer.style.transform = 'translateY(0)'; }); // 动画完成后清理样式 setTimeout(() => { childrenContainer.style.height = 'auto'; }, 300); // 添加展开状态指示 if (listItem) { listItem.classList.add('toc-expanded'); } } else { // 折叠动画 const currentHeight = childrenContainer.offsetHeight; childrenContainer.style.height = currentHeight + 'px'; requestAnimationFrame(() => { toggleBtn.classList.add('collapsed'); childrenContainer.style.height = '0'; childrenContainer.style.opacity = '0'; childrenContainer.style.transform = 'translateY(-10px)'; }); setTimeout(() => { childrenContainer.classList.add('collapsed'); }, 300); // 移除展开状态指示 if (listItem) { listItem.classList.remove('toc-expanded'); } } // 保存用户的折叠偏好 const itemId = listItem?.querySelector('a')?.getAttribute('href'); if (itemId) { const collapsedItems = JSON.parse(localStorage.getItem('toc-collapsed-items') || '[]'); if (isCollapsed) { // 展开:从折叠列表中移除 const index = collapsedItems.indexOf(itemId); if (index > -1) collapsedItems.splice(index, 1); } else { // 折叠:添加到折叠列表 if (!collapsedItems.includes(itemId)) { collapsedItems.push(itemId); } } localStorage.setItem('toc-collapsed-items', JSON.stringify(collapsedItems)); } } // 现代化打开/关闭悬浮窗 tocBtn.onclick = function() { const isOpen = tocPopup.classList.contains('toc-popup-visible'); if (isOpen) { // 关闭动画 tocPopup.classList.remove('toc-popup-visible'); tocPopup.classList.add('toc-popup-hiding'); setTimeout(() => { tocPopup.classList.remove('toc-popup-hiding'); tocPopup.classList.add('toc-popup-hidden'); }, 400); } else { // 打开前检查和更新内容 updateTocModeSelectorVisibility(); // 如果缓存无效,重新渲染 if (!tocCache.isValid()) { renderTocList(); } // 打开动画 tocPopup.classList.remove('toc-popup-hidden', 'toc-popup-hiding'); tocPopup.classList.add('toc-popup-visible'); // 延迟聚焦以改善用户体验 setTimeout(() => { const firstLink = tocPopup.querySelector('#toc-list a'); if (firstLink) { firstLink.focus(); } }, 100); } }; // 关闭悬浮窗按钮 tocCloseBtn.onclick = function() { tocPopup.classList.remove('toc-popup-visible'); tocPopup.classList.add('toc-popup-hiding'); setTimeout(() => { tocPopup.classList.remove('toc-popup-hiding'); tocPopup.classList.add('toc-popup-hidden'); }, 400); }; /** * 更新TOC模式选择器的可见性,仅在分块对比模式下显示 */ function updateTocModeSelectorVisibility() { const visibleTab = document.querySelector('.tab-btn.active'); const currentTabId = visibleTab ? visibleTab.id : null; const isChunkCompareMode = currentTabId === 'tab-chunk-compare'; if (isChunkCompareMode) { tocModeSelector.style.display = 'flex'; } else { tocModeSelector.style.display = 'none'; if (currentTocMode !== 'both') { currentTocMode = 'both'; tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(b => { b.classList.remove('active'); }); tocModeSelector.querySelector('[data-mode="both"]').classList.add('active'); } } } /** * 增强的智能层级管理器 */ let enhancedLevelManager = { prefixMapping: {}, contextStack: [], lastStructureInfo: null, analyzeHeading: function(text) { const patterns = { chapter: /^第[一二三四五六七八九十百千万]+[章节篇部]/, numeric: /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?/, roman: /^([IVX]+)(?:\.([IVX]+))?(?:\.([IVX]+))?/i, bulletList: /^[•\*\-]\s+/, numberedList: /^(\d+)(?:[\.、]|\s*[\(\(])\s*/, specialSection: /^(摘要|Abstract|引言|Introduction|参考文献|References|附录|Appendix|致谢|Acknowledgements|结论|Conclusion|讨论|Discussion|实验|Experiment|方法|Methods|材料|Materials)/i }; let structureInfo = { type: 'normal', level: null, prefix: '', content: text, confidence: 0 }; // 检测各种模式并计算置信度 for (const [type, pattern] of Object.entries(patterns)) { const match = text.match(pattern); if (match) { structureInfo.type = type; structureInfo.prefix = match[0]; structureInfo.content = text.substring(match[0].length).trim(); structureInfo.confidence = this.calculateConfidence(type, match); // 根据类型确定层级 structureInfo.level = this.determineLevelByType(type, match); break; } } // 更新上下文栈 this.updateContextStack(structureInfo); this.lastStructureInfo = structureInfo; return structureInfo; }, calculateConfidence: function(type, match) { // 基于模式复杂度和匹配质量计算置信度 const confidenceMap = { 'specialSection': 0.95, 'chapter': 0.9, 'numeric': 0.85, 'roman': 0.8, 'numberedList': 0.7, 'bulletList': 0.6 }; return confidenceMap[type] || 0.5; }, determineLevelByType: function(type, match) { switch (type) { case 'specialSection': case 'chapter': return 1; case 'numeric': return (match[0].match(/\./g) || []).length + 1; case 'roman': return (match[0].match(/\./g) || []).length + 1; case 'numberedList': case 'bulletList': return (this.lastStructureInfo?.level || 1) + 1; default: return 2; } }, updateContextStack: function(structureInfo) { // 维护结构化上下文栈 if (structureInfo.level) { // 移除更深层级的项目 this.contextStack = this.contextStack.filter(item => item.level < structureInfo.level); this.contextStack.push(structureInfo); } } }; /** * 主要的TOC渲染函数 - 增强版 */ function renderTocList() { const startTime = performance.now(); // 检查缓存 if (tocCache.isValid() && tocCache.structure) { buildTocHtml(tocCache.structure.children, tocList); return; } tocList.innerHTML = ''; tocNodes = []; const container = document.querySelector('.container'); if (!container) return; // 收集所有潜在标题 let potentialHeadings = []; container.querySelectorAll('h1, h2:not(#fileName), h3, h4, h5, h6, p.converted-from-heading').forEach(h => { potentialHeadings.push(h); }); // 添加图表标题 const captionRegex = /^(图|表|Figure|Table)\s*[\d\w.-]+\b/i; container.querySelectorAll('p').forEach(p => { const text = p.textContent.trim(); if (captionRegex.test(text)) { p.dataset.isCaptionToc = "true"; p.dataset.isChartCaption = "true"; potentialHeadings.push(p); } }); // 按文档顺序排序 potentialHeadings.sort((a, b) => { const position = a.compareDocumentPosition(b); return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; }); // 根据当前模式过滤 let filteredHeadings = filterHeadingsByMode(potentialHeadings); // 构建TOC结构 const tocStructure = buildTocStructure(filteredHeadings); // 缓存结果 tocCache.structure = tocStructure; tocCache.nodes = tocNodes; tocCache.lastUpdate = Date.now(); // 渲染HTML buildTocHtml(tocStructure.children, tocList); // 恢复折叠状态 restoreCollapsedState(); // 记录性能指标 performanceMetrics.renderTime = performance.now() - startTime; performanceMetrics.nodeCount = tocNodes.length; console.log(`TOC渲染完成: ${performanceMetrics.nodeCount}个节点, 耗时${performanceMetrics.renderTime.toFixed(2)}ms`); } /** * 根据模式过滤标题 */ function filterHeadingsByMode(headings) { if (currentTocMode === 'both') { return headings; } const visibleTab = document.querySelector('.tab-btn.active'); const currentTabId = visibleTab ? visibleTab.id : null; const isChunkCompareMode = currentTabId === 'tab-chunk-compare'; if (isChunkCompareMode) { const selector = currentTocMode === 'ocr' ? '.align-block-ocr' : '.align-block-trans'; return headings.filter(el => el.closest(selector) !== null); } else { const expectedTabId = currentTocMode === 'ocr' ? 'tab-ocr' : 'tab-translation'; if (currentTabId === expectedTabId) { return headings; } else { // 显示提示信息 const li = document.createElement('li'); li.className = 'toc-info'; li.innerHTML = `
请切换到${currentTocMode === 'ocr' ? '原文' : '译文'}标签页查看对应目录
`; tocList.appendChild(li); return []; } } } /** * 构建TOC层级结构 */ function buildTocStructure(headings) { const structure = { root: true, children: [] }; let currentPath = [structure]; let previousLevel = 0; headings.forEach((nodeEl, idx) => { if (!nodeEl.id) nodeEl.id = 'toc-auto-' + idx; tocNodes.push(nodeEl); const text = nodeEl.textContent.trim(); if (text.includes('原文块') || text.includes('译文块')) return; // 使用增强的层级管理器分析 const structureInfo = enhancedLevelManager.analyzeHeading(text); const level = structureInfo.level || getDefaultLevel(nodeEl); // 调整路径 adjustPath(currentPath, level, previousLevel); // 创建TOC项 const tocItem = createTocItem(nodeEl, text, level, structureInfo); currentPath[currentPath.length - 1].children.push(tocItem); previousLevel = level; }); return structure; } /** * 获取默认层级 */ function getDefaultLevel(element) { const tagName = element.tagName.toLowerCase(); if (tagName.match(/^h[1-6]$/)) { return parseInt(tagName.substring(1)); } return element.dataset.isChartCaption === "true" ? 4 : 3; } /** * 调整当前路径 */ function adjustPath(path, currentLevel, previousLevel) { if (currentLevel > previousLevel) { // 进入更深层级 while (path.length < currentLevel) { if (path[path.length - 1].children.length === 0) { // 创建占位符 const placeholder = { id: 'placeholder-' + Date.now(), text: '未命名章节', level: path.length, children: [] }; path[path.length - 1].children.push(placeholder); } path.push(path[path.length - 1].children[path[path.length - 1].children.length - 1]); } } else if (currentLevel < previousLevel) { // 返回上层 const levelsToGoUp = previousLevel - currentLevel; for (let i = 0; i < levelsToGoUp && path.length > 1; i++) { path.pop(); } } } /** * 创建TOC项 */ function createTocItem(element, text, level, structureInfo) { const displayText = smartTruncateText(text); const translation = tocMap[text]; return { id: element.id, text: displayText, originalText: text, translation: translation, level: level, children: [], isChartCaption: element.dataset.isChartCaption === "true", structureInfo: structureInfo, element: element }; } /** * 构建TOC HTML - 增强版 */ function buildTocHtml(items, parentElement) { items.forEach(item => { if (item.id?.indexOf('placeholder') === 0 && !item.text) return; const li = document.createElement('li'); const hasChildren = item.children && item.children.length > 0; // 设置CSS类 li.className = getTocItemClasses(item, hasChildren); // 构建链接HTML const linkHTML = buildLinkHTML(item, hasChildren); const link = document.createElement('a'); link.href = `#${item.id}`; link.innerHTML = linkHTML; link.dataset.originalText = item.originalText; // 添加现代化点击事件 addEnhancedClickHandler(link, item); li.appendChild(link); // 添加子项 if (hasChildren) { const childrenContainer = document.createElement('ul'); childrenContainer.className = 'toc-children'; buildTocHtml(item.children, childrenContainer); li.appendChild(childrenContainer); // 添加折叠按钮事件 addToggleHandler(li); } parentElement.appendChild(li); }); } /** * 获取TOC项的CSS类 */ function getTocItemClasses(item, hasChildren) { let classes = []; if (item.level) { if (item.isChartCaption) { classes.push('toc-caption'); } else { classes.push(`toc-h${item.level}`); } } if (hasChildren) { classes.push('has-children'); } if (item.structureInfo?.type && item.structureInfo.type !== 'normal') { classes.push('toc-structured', `toc-structure-${item.structureInfo.type}`); } return classes.join(' '); } /** * 构建链接HTML */ function buildLinkHTML(item, hasChildren) { let html = ''; if (hasChildren) { html += ''; } html += ''; // 添加结构化前缀 if (item.structureInfo?.prefix) { html += `${item.structureInfo.prefix}`; } // 添加图表图标 if (item.isChartCaption) { const isTable = item.originalText?.startsWith('表'); const icon = isTable ? '📊' : '📈'; html += `${icon}`; } // 添加内容 let displayText = item.text; if (item.structureInfo?.prefix && displayText.startsWith(item.structureInfo.prefix)) { displayText = displayText.substring(item.structureInfo.prefix.length).trim(); } html += `${displayText}`; // 添加翻译 if (item.translation && item.translation !== item.originalText) { html += `/ ${item.translation}`; } html += ''; return html; } /** * 添加增强的点击处理器 */ function addEnhancedClickHandler(link, item) { link.onclick = function(e) { e.preventDefault(); console.log('[TOC Debug] TOC 点击事件触发:', item.id); const targetElement = document.getElementById(item.id); if (!targetElement) { console.log('[TOC Debug] 未找到目标元素:', item.id); return; } // 计算距离并决定是否显示加载效果 const clickedNodeIndex = tocNodes.findIndex(n => n.id === item.id); const currentTopNodeIndex = getCurrentTopNodeIndex(); const indexDifference = Math.abs(clickedNodeIndex - currentTopNodeIndex); if (indexDifference >= 6) { showEnhancedLoadingEffect(item.originalText || "目标章节"); } console.log('[TOC Debug] 检查沉浸模式:', { hasImmersiveLayout: !!window.ImmersiveLayout, isActive: window.ImmersiveLayout?.isActive() }); // 修复:在沉浸模式下使用自定义滚动逻辑,避免布局偏移 if (window.ImmersiveLayout && window.ImmersiveLayout.isActive()) { console.log('[TOC Debug] 进入沉浸模式分支'); // 沉浸模式下使用自定义滚动定位 // 优先查找 .content-wrapper(真正的滚动容器) let scrollContainer = document.querySelector('#immersive-main-content-area .content-wrapper'); // 后备方案 1:查找 .js-scroll-container 标记 if (!scrollContainer) { scrollContainer = document.querySelector('#immersive-main-content-area .js-scroll-container'); } // 后备方案 2:查找 .tab-content if (!scrollContainer) { scrollContainer = document.querySelector('#immersive-main-content-area .tab-content'); } if (scrollContainer) { // 使用 computed style 检查是否可滚动(而不是检查内联样式) const computedStyle = getComputedStyle(scrollContainer); const overflowY = computedStyle.overflowY; const isScrollable = (overflowY === 'auto' || overflowY === 'scroll'); console.log('[TOC Debug] 沉浸模式滚动检测:', { scrollContainer: scrollContainer.className, overflowY, isScrollable, scrollHeight: scrollContainer.scrollHeight, clientHeight: scrollContainer.clientHeight }); // 只要找到了滚动容器,就尝试滚动(即使当前没有滚动条) if (isScrollable) { // 计算目标元素在滚动容器内的绝对位置 const containerRect = scrollContainer.getBoundingClientRect(); const targetRect = targetElement.getBoundingClientRect(); const currentScrollTop = scrollContainer.scrollTop; // 目标元素相对于容器内容的绝对位置 = 当前滚动位置 + 目标相对于容器视口的位置 const targetOffsetInContainer = currentScrollTop + (targetRect.top - containerRect.top); // 改进的滚动逻辑:确保目标元素可见,但不滚动过头 // 如果目标元素已经在视口内,就不滚动 const viewportTop = containerRect.top; const viewportBottom = containerRect.bottom; const targetTop = targetRect.top; const targetBottom = targetRect.bottom; // 目标元素已经完全可见,不需要滚动 if (targetTop >= viewportTop && targetBottom <= viewportBottom) { return; } // 目标元素在视口上方,需要向上滚动 if (targetTop < viewportTop) { const scrollDelta = targetTop - viewportTop; scrollContainer.scrollTo({ top: currentScrollTop + scrollDelta, behavior: 'smooth' }); return; } // 目标元素在视口下方,需要向下滚动 // 将目标元素滚动到视口底部附近 if (targetBottom > viewportBottom) { const scrollDelta = targetBottom - viewportBottom + 20; // 底部留 20px 空隙 scrollContainer.scrollTo({ top: currentScrollTop + scrollDelta, behavior: 'smooth' }); return; } } } // 如果没有找到滚动容器,不调用 scrollIntoView,避免滚动 overflow:hidden 的祖先容器 return; } else { // 普通模式下,检查是否需要滚动 .tab-content 容器(OCR/翻译模式) const tabContent = document.querySelector('.tab-content'); // 检查 tabContent 是否是滚动容器 if (tabContent) { const computedStyle = getComputedStyle(tabContent); const overflowY = computedStyle.overflowY; const overflow = computedStyle.overflow; // 支持 auto 和 scroll const isScrollable = (overflowY === 'auto' || overflowY === 'scroll' || overflow === 'auto' || overflow === 'scroll'); const hasScroll = tabContent.scrollHeight > tabContent.clientHeight; if (isScrollable && hasScroll) { // 计算目标元素相对于滚动容器的位置 const containerRect = tabContent.getBoundingClientRect(); const targetRect = targetElement.getBoundingClientRect(); const currentScrollTop = tabContent.scrollTop; // 计算目标位置(将元素置于容器中心) const targetScrollTop = currentScrollTop + targetRect.top - containerRect.top - (containerRect.height / 2) + (targetRect.height / 2); // 平滑滚动到目标位置 tabContent.scrollTo({ top: Math.max(0, targetScrollTop), behavior: 'smooth' }); } else { // 如果 tab-content 不是滚动容器或不需要滚动,使用原生 scrollIntoView targetElement.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); } } else { // tab-content 不存在,使用原生 scrollIntoView targetElement.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); } } // 添加高亮效果 addHighlightEffect(targetElement); }; } /** * 获取当前顶部节点索引 */ function getCurrentTopNodeIndex() { if (tocNodes.length === 0) return 0; let minPositiveTop = Infinity; let topIndex = 0; for (let i = 0; i < tocNodes.length; i++) { const rect = tocNodes[i].getBoundingClientRect(); if (rect.top >= 0 && rect.top < minPositiveTop) { minPositiveTop = rect.top; topIndex = i; } } return topIndex; } /** * 添加高亮效果 */ function addHighlightEffect(element) { element.classList.add('toc-target-highlight'); setTimeout(() => { element.classList.remove('toc-target-highlight'); }, 3000); } /** * 添加折叠按钮处理器 */ function addToggleHandler(li) { const toggleBtn = li.querySelector('.toc-toggle'); if (toggleBtn) { toggleBtn.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); const childContainer = this.closest('li').querySelector('.toc-children'); if (childContainer) { toggleTocItem(this, childContainer); } }); } } /** * 恢复折叠状态 */ function restoreCollapsedState() { const collapsedItems = JSON.parse(localStorage.getItem('toc-collapsed-items') || '[]'); collapsedItems.forEach(itemId => { const link = tocList.querySelector(`a[href="${itemId}"]`); if (link) { const li = link.closest('li'); const toggleBtn = li.querySelector('.toc-toggle'); const childrenContainer = li.querySelector('.toc-children'); if (toggleBtn && childrenContainer) { toggleBtn.classList.add('collapsed'); childrenContainer.classList.add('collapsed'); } } }); } // 键盘导航支持 function addKeyboardNavigation() { tocPopup.addEventListener('keydown', function(e) { const focusedElement = document.activeElement; switch (e.key) { case 'ArrowDown': e.preventDefault(); navigateToNext(focusedElement); break; case 'ArrowUp': e.preventDefault(); navigateToPrevious(focusedElement); break; case 'Enter': case ' ': if (focusedElement.classList.contains('toc-toggle')) { e.preventDefault(); focusedElement.click(); } break; case 'Escape': e.preventDefault(); tocCloseBtn.click(); break; } }); } function navigateToNext(current) { const allFocusable = tocPopup.querySelectorAll('a, .toc-toggle, .toc-control-btn'); const currentIndex = Array.from(allFocusable).indexOf(current); const nextElement = allFocusable[currentIndex + 1]; if (nextElement) nextElement.focus(); } function navigateToPrevious(current) { const allFocusable = tocPopup.querySelectorAll('a, .toc-toggle, .toc-control-btn'); const currentIndex = Array.from(allFocusable).indexOf(current); const prevElement = allFocusable[currentIndex - 1]; if (prevElement) prevElement.focus(); } // 控制按钮事件绑定 function bindControlEvents() { // 展开/收起目录 document.getElementById('toc-expand-btn').addEventListener('click', function() { const isExpanded = tocPopup.classList.contains('toc-expanded'); const icon = this.querySelector('i'); const text = this.querySelector('span'); if (isExpanded) { tocPopup.classList.remove('toc-expanded'); icon.className = 'fas fa-expand-arrows-alt'; text.textContent = '展开'; this.title = '展开目录'; } else { tocPopup.classList.add('toc-expanded'); icon.className = 'fas fa-compress-arrows-alt'; text.textContent = '收起'; this.title = '收起目录'; } }); // 全部展开 document.getElementById('toc-expand-all').addEventListener('click', function() { const allToggleButtons = tocList.querySelectorAll('.toc-toggle.collapsed'); allToggleButtons.forEach(btn => { const childrenContainer = btn.closest('li').querySelector('.toc-children'); if (childrenContainer) { toggleTocItem(btn, childrenContainer); } }); }); // 全部折叠 document.getElementById('toc-collapse-all').addEventListener('click', function() { const allToggleButtons = tocList.querySelectorAll('.toc-toggle:not(.collapsed)'); allToggleButtons.forEach(btn => { const childrenContainer = btn.closest('li').querySelector('.toc-children'); if (childrenContainer) { toggleTocItem(btn, childrenContainer); } }); }); } // 全局刷新函数 window.refreshTocList = function() { tocCache.clearCache(); updateTocModeSelectorVisibility(); renderTocList(); }; // 初始化 function init() { updateTocModeSelectorVisibility(); renderTocList(); addKeyboardNavigation(); bindControlEvents(); initContentObserver(); // 监听标签页切换 document.querySelectorAll('.tab-btn').forEach(tab => { tab.addEventListener('click', () => { setTimeout(updateTocModeSelectorVisibility, 100); }); }); console.log('Enhanced TOC initialized successfully'); } // 启动初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 暴露API window.EnhancedTocFeature = { refresh: window.refreshTocList, getNodes: () => tocNodes, getStructure: () => tocCache.structure, getMetrics: () => performanceMetrics, setMode: (mode) => { if (['both', 'ocr', 'translation'].includes(mode)) { currentTocMode = mode; renderTocList(); } } }; })();