/**
* @namespace TocFeature
* @description 管理页面右侧浮动的目录 (Table of Contents) 功能。
* 包括TOC按钮的点击事件、TOC悬浮窗的显示/隐藏、
* 以及动态生成TOC列表项。
*/
(function TocFeature(){
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 tocModeSelector = document.createElement('div');
tocModeSelector.className = 'toc-mode-selector';
tocModeSelector.innerHTML = `
`;
// 当前 TOC 显示模式:both, ocr, translation
let currentTocMode = 'both';
// 将模式选择器插入到 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;
currentTocMode = mode;
// 更新按钮状态
tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(b => {
b.classList.remove('active');
});
this.classList.add('active');
// 重新渲染 TOC 列表
renderTocList();
});
});
// 添加底部控制区域(合并展开目录按钮和全部展开/折叠按钮)
const tocControls = document.createElement('div');
tocControls.className = 'toc-controls';
tocControls.innerHTML = `
`;
// 将控制区域添加到TOC弹窗
if (tocPopup) {
tocPopup.appendChild(tocControls);
}
/**
* @const {Object} tocMap
* @description 用于TOC中标题的中文到英文的简单映射表。
*/
const tocMap = {
'历史详情': 'History Detail',
'OCR内容': 'OCR Content',
'仅OCR': 'OCR Only',
'翻译内容': 'Translation',
'仅翻译': 'Translation Only',
'分块对比': 'Chunk Compare',
};
/**
* @type {Array}
* @description 存储TOC列表项对应的页面内标题DOM元素。
*/
let tocNodes = []; // 存储目录对应的标题DOM元素
/**
* 判断两个文本是否相似
* @param {string} text1 - 第一个文本
* @param {string} text2 - 第二个文本
* @returns {boolean} 是否相似
*/
function areTextsSimilar(text1, text2) {
// 如果两个字符串长度差距大于2,认为不相似
if (Math.abs(text1.length - text2.length) > 2) {
return false;
}
// 简单的模糊相似度判断
let similarity = 0;
const minLength = Math.min(text1.length, text2.length);
for (let i = 0; i < minLength; i++) {
if (text1[i] === text2[i]) {
similarity++;
}
}
const similarityRatio = similarity / minLength;
return similarityRatio > 0.8; // 相似度大于80%认为是相似的
}
/**
* 智能截断长文本
* @param {string} text - 要截断的文本
* @returns {string} 截断后的文本
*/
function truncateText(text) {
// 如果文本不超过35个字符,直接返回
if (text.length <= 35) {
return text;
}
// 检查文本是否是图表标题(以"图"或"表"开头,后跟数字)
const isChartTitle = /^(图|表)\s*\d+\.?\s*/.test(text);
// 如果是图表标题,优先在第一个句号或逗号处截断
if (isChartTitle) {
const dotIndex = text.indexOf('。');
const commaIndex = text.indexOf(',');
// 也检查英文句号和逗号
const enDotIndex = text.indexOf('.');
const enCommaIndex = text.indexOf(',');
// 找到第一个有效的截断标点位置
let firstCutIndex = -1;
// 优先使用句号,其次使用逗号
if (dotIndex !== -1) {
firstCutIndex = dotIndex;
} else if (enDotIndex !== -1) {
// 确保英文句号不是数字的一部分
const charBeforeDot = text.charAt(enDotIndex-1);
const charAfterDot = text.charAt(enDotIndex+1);
// 如果句号前后都是数字,可能是小数点,继续检查其他标点
if (!(/\d/.test(charBeforeDot) && /\d/.test(charAfterDot))) {
firstCutIndex = enDotIndex;
}
}
// 如果没有找到句号,尝试使用逗号
if (firstCutIndex === -1) {
if (commaIndex !== -1) {
firstCutIndex = commaIndex;
} else if (enCommaIndex !== -1) {
firstCutIndex = enCommaIndex;
}
}
// 如果仍然没有找到有效的截断点,使用所有标点中最早的一个
if (firstCutIndex === -1) {
const allPunctIndices = [dotIndex, commaIndex, enDotIndex, enCommaIndex].filter(idx => idx !== -1);
if (allPunctIndices.length > 0) {
firstCutIndex = Math.min(...allPunctIndices);
}
}
// 确保标点不是图表编号的一部分(如"图5."中的句号)
if (firstCutIndex > 5) {
// 特殊处理英文句号,可能是小数点
if (firstCutIndex === enDotIndex) {
const charBeforeDot = text.charAt(firstCutIndex-1);
const charAfterDot = text.charAt(firstCutIndex+1);
// 如果句号前后都是数字,这可能是编号的一部分
if (/\d/.test(charBeforeDot) && /\d/.test(charAfterDot)) {
// 继续寻找下一个标点,同样优先使用句号
const nextText = text.substring(firstCutIndex + 1);
const nextDotIndex = nextText.indexOf('。');
const nextEnDotIndex = nextText.indexOf('.');
// 优先检查中文句号
if (nextDotIndex !== -1) {
return text.substring(0, firstCutIndex + 1 + nextDotIndex + 1);
}
// 再检查英文句号
else if (nextEnDotIndex !== -1) {
// 确保这个英文句号不是小数点
const nextCharBefore = firstCutIndex + 1 + nextEnDotIndex - 1 < text.length ?
text.charAt(firstCutIndex + 1 + nextEnDotIndex - 1) : '';
const nextCharAfter = firstCutIndex + 1 + nextEnDotIndex + 1 < text.length ?
text.charAt(firstCutIndex + 1 + nextEnDotIndex + 1) : '';
if (!(/\d/.test(nextCharBefore) && /\d/.test(nextCharAfter))) {
return text.substring(0, firstCutIndex + 1 + nextEnDotIndex + 1);
}
}
// 如果没有找到句号,检查逗号
const nextCommaIndex = nextText.indexOf(',');
const nextEnCommaIndex = nextText.indexOf(',');
if (nextCommaIndex !== -1) {
return text.substring(0, firstCutIndex + 1 + nextCommaIndex + 1);
} else if (nextEnCommaIndex !== -1) {
return text.substring(0, firstCutIndex + 1 + nextEnCommaIndex + 1);
}
// 如果都没找到,使用所有下一个标点中最早的一个
const nextPunctIndices = [nextDotIndex, nextCommaIndex, nextEnDotIndex, nextEnCommaIndex].filter(idx => idx !== -1);
if (nextPunctIndices.length > 0) {
const nextCutIndex = Math.min(...nextPunctIndices);
return text.substring(0, firstCutIndex + 1 + nextCutIndex + 1);
}
} else {
return text.substring(0, firstCutIndex + 1);
}
} else {
return text.substring(0, firstCutIndex + 1);
}
}
}
// 优先使用中文句号作为截断点
const chineseDotIndex = text.substring(0, 35).indexOf('。');
if (chineseDotIndex !== -1) {
return text.substring(0, chineseDotIndex + 1);
}
// 其次使用英文句号(确保不是小数点)
const englishDotIndex = text.substring(0, 35).indexOf('.');
if (englishDotIndex !== -1 && englishDotIndex > 0) {
const charBeforeDot = text.charAt(englishDotIndex - 1);
const charAfterDot = englishDotIndex + 1 < text.length ? text.charAt(englishDotIndex + 1) : '';
// 如果不是小数点,使用英文句号截断
if (!(/\d/.test(charBeforeDot) && /\d/.test(charAfterDot))) {
return text.substring(0, englishDotIndex + 1);
}
}
// 再使用中文逗号
const chineseCommaIndex = text.substring(0, 35).indexOf(',');
if (chineseCommaIndex !== -1) {
return text.substring(0, chineseCommaIndex + 1);
}
// 最后使用英文逗号
const englishCommaIndex = text.substring(0, 35).indexOf(',');
if (englishCommaIndex !== -1) {
return text.substring(0, englishCommaIndex + 1);
}
// 如果以上都没找到,使用其他中文标点
const otherChinesePunctuationRegex = /[;:!?]/;
const otherChinesePunctuationMatch = text.substring(0, 35).match(otherChinesePunctuationRegex);
if (otherChinesePunctuationMatch) {
return text.substring(0, otherChinesePunctuationMatch.index + 1);
}
// 如果中文标点都没有,尝试使用其他英文标点
const otherEnglishPunctuationRegex = /[;:!?]/;
const otherEnglishPunctuationMatch = text.substring(0, 35).match(otherEnglishPunctuationRegex);
if (otherEnglishPunctuationMatch) {
return text.substring(0, otherEnglishPunctuationMatch.index + 1);
}
// 如果都没有找到合适的标点符号,截取前32个字符加省略号
return text.substring(0, 32) + "...";
}
/**
* 在TOC导航时,如果目标章节与当前视口距离较远,显示一个临时的加载/导航效果。
* @param {string} sectionName - 正在导航到的章节名称。
*/
function showTemporaryLoadingEffect(sectionName) {
let effectDiv = document.getElementById('toc-loading-effect');
const mainContainer = document.querySelector('.container');
if (!effectDiv) {
effectDiv = document.createElement('div');
effectDiv.id = 'toc-loading-effect';
// 使用CSS类而不是直接设置样式
effectDiv.className = 'loading-effect';
document.body.appendChild(effectDiv);
}
if (mainContainer) {
// 使用CSS类而不是直接设置样式
mainContainer.classList.add('content-blurred');
}
// 确保截断显示的章节名
const truncatedSectionName = truncateText(sectionName);
effectDiv.textContent = `正在前往: ${truncatedSectionName}`;
// 使用CSS类管理可见性
requestAnimationFrame(() => {
effectDiv.classList.add('loading-effect-visible');
});
setTimeout(() => {
effectDiv.classList.remove('loading-effect-visible');
if (mainContainer) {
mainContainer.classList.remove('content-blurred');
}
}, 1500); // 效果持续时间
}
/**
* 切换TOC项的折叠状态
* @param {HTMLElement} toggleBtn - 折叠/展开按钮元素
* @param {HTMLElement} childrenContainer - 子项容器元素
*/
function toggleTocItem(toggleBtn, childrenContainer) {
const isCollapsed = toggleBtn.classList.contains('collapsed');
if (isCollapsed) {
// 展开
toggleBtn.classList.remove('collapsed');
childrenContainer.classList.remove('collapsed');
// 设置高度以实现动画效果
const originalHeight = childrenContainer.scrollHeight;
childrenContainer.style.height = '0';
// 触发回流
childrenContainer.offsetHeight;
childrenContainer.style.height = originalHeight + 'px';
// 延迟后移除固定高度,允许自动调整
setTimeout(() => {
childrenContainer.style.height = 'auto';
}, 300);
} else {
// 折叠
toggleBtn.classList.add('collapsed');
// 先设置当前高度,然后过渡到0
childrenContainer.style.height = childrenContainer.scrollHeight + 'px';
// 强制回流
childrenContainer.offsetHeight;
childrenContainer.style.height = '0';
childrenContainer.classList.add('collapsed');
}
}
// 打开/关闭悬浮窗
tocBtn.onclick = function() {
const isOpen = tocPopup.classList.contains('toc-popup-visible');
if (isOpen) {
// 使用CSS类管理状态
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');
}, 200);
} else {
// 检查当前显示的Tab是否为分块对比
updateTocModeSelectorVisibility();
renderTocList(); // 每次打开时重新渲染,确保内容最新
tocPopup.classList.remove('toc-popup-hidden', 'toc-popup-hiding');
tocPopup.classList.add('toc-popup-visible');
}
};
// 关闭悬浮窗按钮
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');
}, 200);
};
/**
* 更新TOC模式选择器的可见性,仅在分块对比模式下显示
*/
function updateTocModeSelectorVisibility() {
// 获取当前显示的Tab内容
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';
// 如果不是分块对比模式,强制使用both模式
if (currentTocMode !== 'both') {
currentTocMode = 'both';
// 更新按钮状态
tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(b => {
b.classList.remove('active');
});
tocModeSelector.querySelector('[data-mode="both"]').classList.add('active');
}
}
}
/**
* 生成并渲染TOC目录列表。
* - 清空现有列表。
* - 从 `.container` 中查找所有 `h1`, `h2`, `h3`, `h4`, `h5`, `h6` 元素作为TOC条目。
* - 为每个标题元素生成一个列表项,包含其文本和可选的英文翻译(来自 `tocMap`)。
* - 列表项链接到对应标题的ID,点击时平滑滚动到该标题,并根据距离触发加载效果。
* - 存储标题DOM节点到 `tocNodes` 数组。
* - 新增:构建层级结构并支持折叠/展开功能。
* - 新增:智能识别标题格式,根据标题格式自动调整层级。
*/
function renderTocList() {
tocList.innerHTML = '';
tocNodes = []; // 每次渲染时清空并重新填充
const container = document.querySelector('.container');
if (!container) return;
let potentialHeadings = [];
// 1. 获取标准的 Hx 标题和被转换的长标题
container.querySelectorAll('h1, h2:not(#fileName), h3, h4, h5, h6, p.converted-from-heading').forEach(h => {
potentialHeadings.push(h);
});
// 2. 获取可能是图表标题的 P 标签
// 正则表达式:匹配 "图/表/Figure/Table" + 空格 + 数字/字母/./- + 单词边界 (确保是独立编号)
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);
}
});
// 3. 按文档顺序对所有潜在标题进行排序
potentialHeadings.sort((a, b) => {
if (a === b) return 0;
const position = a.compareDocumentPosition(b);
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
return -1; // a 在 b 之前
} else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
return 1; // a 在 b 之后
}
return 0; // 通常不应发生,除非元素不在同一文档树或存在包含关系
});
const headingElements = potentialHeadings;
// 根据当前模式过滤标题
let filteredHeadings = [];
if (currentTocMode === 'both') {
filteredHeadings = headingElements;
} else {
// 获取当前显示的Tab内容
const visibleTab = document.querySelector('.tab-btn.active');
const currentTabId = visibleTab ? visibleTab.id : null;
// 判断当前是否在分块对比模式
const isChunkCompareMode = currentTabId === 'tab-chunk-compare';
if (isChunkCompareMode) {
// 在分块对比模式下,根据currentTocMode筛选标题
if (currentTocMode === 'ocr') {
// 筛选左侧原文块的标题
filteredHeadings = Array.from(headingElements).filter(el => {
const closestAlignBlock = el.closest('.align-block-ocr');
return closestAlignBlock !== null;
});
} else if (currentTocMode === 'translation') {
// 筛选右侧译文块的标题
filteredHeadings = Array.from(headingElements).filter(el => {
const closestAlignBlock = el.closest('.align-block-trans');
return closestAlignBlock !== null;
});
}
} else {
// 非分块对比模式下,检查当前显示的是否是与所选模式匹配的标签页
if ((currentTabId === 'tab-ocr' && currentTocMode === 'ocr') ||
(currentTabId === 'tab-translation' && currentTocMode === 'translation')) {
filteredHeadings = headingElements;
} else {
// 如果当前标签页与所选模式不匹配,显示一个提示
const li = document.createElement('li');
li.className = 'toc-info';
li.textContent = `请切换到${currentTocMode === 'ocr' ? '原文' : '译文'}标签页查看对应目录`;
tocList.appendChild(li);
return;
}
}
}
// 创建一个层级结构对象
const tocStructure = {
root: true,
children: []
};
// 存储上一个处理过的TOC项,用于比较和合并
let previousTocItem = null;
let previousHeadingLevel = 0;
let currentPath = [tocStructure]; // 当前路径,从根开始
// 结构化标题格式的正则表达式
const chapterPattern = /^第[一二三四五六七八九十百千]+[章节篇部]/; // 匹配"第一章"、"第二节"等
const numericPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?/; // 匹配"1"、"1.1"、"1.1.1"等
const romanPattern = /^([IVX]+)(?:\.([IVX]+))?(?:\.([IVX]+))?/i; // 匹配罗马数字标题
const letterPattern = /^([A-Za-z])(?:\.([A-Za-z]))?(?:\.([A-Za-z]))?/; // 匹配字母标题如"A"、"A.1"
// 新的正则表达式,增强匹配能力
const spacedNumericPattern = /^(\d+)\.?\s+(\d+(?:\.?\d+)*)\s+/; // 匹配"3. 1.1 xxxx"和"4. 5 xxxx"格式
// 新增:多种列表项匹配模式
const bulletListPattern = /^[•\*\-]\s+/; // 匹配"• xxx"、"* xxx"、"- xxx"等无序列表
const numberedListPattern = /^(\d+)(?:[\.、]|\s*[\(\(])\s*/; // 匹配"1. xxx"、"2、xxx"、"3) xxx"、"4(xxx"等
const chineseNumberedListPattern = /^[一二三四五六七八九十]+[\.、]/; // 匹配"一、xxx"等中文编号
const alphaListPattern = /^[((]?([a-zA-Z])[\))\.、]\s*/; // 匹配"(a) xxx"、"(A) xxx"、"a. xxx"、"A、xxx"等
const specialSymbolListPattern = /^[((]?[\*\#\+\-][\))]\s*/; // 匹配"(*) xxx"、"(#) xxx"等特殊符号列表
// 论文特殊章节标题模式
const specialSectionPattern = /^(摘要|Abstract|引言|Introduction|参考文献|References|附录|Appendix|致谢|Acknowledgements|结论|Conclusion|讨论|Discussion|实验|Experiment|方法|Methods|材料|Materials)/i;
// 智能层级管理对象
let levelManager = {
// 结构化前缀到层级的映射
prefixMapping: {},
// 当前处理到的章节编号
currentChapter: null,
currentSection: null,
currentSubsection: null,
// 新增属性,用于跟踪更复杂的上下文
lastStructureType: null, // 上一个结构化标题的类型
lastStructureLevel: 0, // 上一个结构化标题的层级
lastNumericPrefix: null, // 上一个数字前缀,如"1.5"
lastSimpleNumber: null, // 上一个简单数字,如"2."中的"2"
inSimpleList: false, // 是否在简单数字列表中(如"1. 2. 3.")
simpleListParentLevel: 0, // 简单数字列表的父级层级
// 分析标题文本,提取结构化信息
analyzeHeading: function(text) {
let structureInfo = {
type: 'normal', // 默认为普通标题
level: null, // 结构化层级
prefix: '', // 标题前缀
content: text, // 标题内容
isSimpleNumbered: false // 是否是简单数字编号(如"1. 2. 3.")
};
// 检查是否为论文特殊章节标题
const specialSectionMatch = text.match(specialSectionPattern);
if (specialSectionMatch) {
this.lastStructureType = 'special';
this.lastStructureLevel = 1;
this.inSimpleList = false;
structureInfo.type = 'special';
structureInfo.level = 1; // 特殊章节通常是顶级
structureInfo.prefix = specialSectionMatch[0];
structureInfo.content = text;
return structureInfo;
}
// 检查是否为章节标题(如"第一章")
const chapterMatch = text.match(chapterPattern);
if (chapterMatch) {
this.lastStructureType = 'chapter';
this.lastStructureLevel = 1;
this.inSimpleList = false;
structureInfo.type = 'chapter';
structureInfo.level = 1; // 章节一般是顶级
structureInfo.prefix = chapterMatch[0];
structureInfo.content = text.substring(chapterMatch[0].length).trim();
return structureInfo;
}
// 检查是否为带空格的多级编号格式(如"3. 1.1 xxxx"或"4. 5 xxxx")
const spacedNumericMatch = text.match(spacedNumericPattern);
if (spacedNumericMatch) {
// 提取主要数字部分
const mainNumber = spacedNumericMatch[1];
const subNumbers = spacedNumericMatch[2];
// 检查子编号是否已经包含点号,如果没有,需要添加
let formattedSubNumbers = subNumbers;
if (!subNumbers.includes('.')) {
formattedSubNumbers = subNumbers; // 如"4. 5 xxxx"中的"5"
}
// 组合成标准格式,确保格式正确
const combinedNumber = mainNumber + "." + formattedSubNumbers.replace(/\s+/g, '');
// 计算层级(根据点的数量+1)
const dots = (combinedNumber.match(/\./g) || []).length;
// 检查是否可能是章节的子节
let isSubSection = false;
if (this.lastStructureType === 'chapter' && this.currentChapter === mainNumber) {
isSubSection = true;
}
// 设置适当的层级
let level = dots + 1;
if (isSubSection) {
// 如果是章节的子节,层级应该是章节层级+1
level = this.lastStructureLevel + 1;
}
// 更新状态
this.lastStructureType = 'numeric';
this.lastStructureLevel = level;
this.lastNumericPrefix = combinedNumber;
this.inSimpleList = false;
structureInfo.type = 'numeric';
structureInfo.level = level;
structureInfo.prefix = spacedNumericMatch[0]; // 保留原始前缀,包括空格
structureInfo.originalPrefix = combinedNumber; // 保存标准化的前缀
structureInfo.content = text.substring(spacedNumericMatch[0].length).trim();
structureInfo.isSubSection = isSubSection; // 标记是否为章节的子节
// 更新当前处理到的章节编号
const numParts = combinedNumber.split('.');
if (numParts.length > 0) this.currentChapter = numParts[0];
if (numParts.length > 1) this.currentSection = numParts[1];
if (numParts.length > 2) this.currentSubsection = numParts[2];
return structureInfo;
}
// 新增:检查各种列表项格式
// 无序列表项
const bulletMatch = text.match(bulletListPattern);
if (bulletMatch) {
// 无序列表通常是当前层级的子层级
const parentLevel = this.lastStructureLevel || 1;
this.inSimpleList = true;
this.simpleListParentLevel = parentLevel;
structureInfo.type = 'bullet-list';
structureInfo.level = parentLevel + 1;
structureInfo.prefix = bulletMatch[0];
structureInfo.content = text.substring(bulletMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
// 检查是否为各种数字编号列表项(如"1. "、"2、"、"3) "等)
const numberedMatch = text.match(numberedListPattern);
if (numberedMatch) {
const number = numberedMatch[1];
const parentLevel = this.lastStructureLevel || 1;
// 判断是否是简单数字列表的开始或延续
if (!this.inSimpleList) {
// 如果前一个标题是结构化的,那么这个简单数字可能是其子项
if (this.lastStructureType && this.lastStructureType !== 'bullet-list') {
// 设置为简单数字列表模式
this.inSimpleList = true;
this.simpleListParentLevel = parentLevel;
this.lastSimpleNumber = number;
// 层级为父级层级+1
structureInfo.level = parentLevel + 1;
structureInfo.type = 'simple-numbered';
structureInfo.prefix = numberedMatch[0];
structureInfo.content = text.substring(numberedMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
} else {
// 已经在简单数字列表中,继续使用相同的层级
this.lastSimpleNumber = number;
structureInfo.level = this.simpleListParentLevel + 1;
structureInfo.type = 'simple-numbered';
structureInfo.prefix = numberedMatch[0];
structureInfo.content = text.substring(numberedMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
}
// 检查中文数字编号列表项(如"一、")
const chineseNumberedMatch = text.match(chineseNumberedListPattern);
if (chineseNumberedMatch) {
const parentLevel = this.lastStructureLevel || 1;
this.inSimpleList = true;
this.simpleListParentLevel = parentLevel;
structureInfo.type = 'chinese-numbered';
structureInfo.level = parentLevel + 1;
structureInfo.prefix = chineseNumberedMatch[0];
structureInfo.content = text.substring(chineseNumberedMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
// 检查字母编号列表项(如"(a) "、"A. ")
const alphaMatch = text.match(alphaListPattern);
if (alphaMatch) {
const parentLevel = this.lastStructureLevel || 1;
this.inSimpleList = true;
this.simpleListParentLevel = parentLevel;
structureInfo.type = 'alpha-list';
structureInfo.level = parentLevel + 1;
structureInfo.prefix = alphaMatch[0];
structureInfo.content = text.substring(alphaMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
// 检查特殊符号列表项(如"(*) ")
const specialSymbolMatch = text.match(specialSymbolListPattern);
if (specialSymbolMatch) {
const parentLevel = this.lastStructureLevel || 1;
this.inSimpleList = true;
this.simpleListParentLevel = parentLevel;
structureInfo.type = 'special-symbol-list';
structureInfo.level = parentLevel + 1;
structureInfo.prefix = specialSymbolMatch[0];
structureInfo.content = text.substring(specialSymbolMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
// 检查是否为简单数字列表项(如"1. "、"2. ",不包含子编号)
const simpleNumberMatch = text.match(/^(\d+)\.\s+/);
if (simpleNumberMatch) {
const number = simpleNumberMatch[1];
// 判断是否是简单数字列表的开始或延续
if (!this.inSimpleList) {
// 如果前一个标题是结构化的(如"1.5"),那么这个简单数字可能是其子项
if (this.lastStructureType === 'numeric' && this.lastNumericPrefix) {
// 设置为简单数字列表模式
this.inSimpleList = true;
this.simpleListParentLevel = this.lastStructureLevel;
this.lastSimpleNumber = number;
// 层级为父级层级+1
structureInfo.level = this.simpleListParentLevel + 1;
structureInfo.type = 'simple-numbered';
structureInfo.prefix = simpleNumberMatch[0];
structureInfo.content = text.substring(simpleNumberMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
} else {
// 已经在简单数字列表中,继续使用相同的层级
this.lastSimpleNumber = number;
structureInfo.level = this.simpleListParentLevel + 1;
structureInfo.type = 'simple-numbered';
structureInfo.prefix = simpleNumberMatch[0];
structureInfo.content = text.substring(simpleNumberMatch[0].length).trim();
structureInfo.isSimpleNumbered = true;
return structureInfo;
}
}
// 检查是否为数字编号标题(如"1.1")
const numericMatch = text.match(numericPattern);
if (numericMatch) {
// 计算层级(根据实际匹配到的数字段数)
let level = 0;
for (let i = 1; i < numericMatch.length; i++) {
if (numericMatch[i]) {
level++;
}
}
// 获取当前编号的主部分(如"1.5"中的"1")
const mainNumber = numericMatch[1];
// 如果之前在简单数字列表中,但现在遇到了正式的结构化编号
// 例如从"1. 2. 3."列表跳转到"1.6"
if (this.inSimpleList) {
// 检查当前编号是否是与父级编号相同的系列
// 例如,如果父级是"1.5",那么当前"1.6"应该是同级的
if (this.lastNumericPrefix && this.lastNumericPrefix.startsWith(mainNumber + '.')) {
// 退出简单数字列表模式
this.inSimpleList = false;
}
}
// 更新状态
this.lastStructureType = 'numeric';
this.lastStructureLevel = level;
this.lastNumericPrefix = numericMatch[0];
structureInfo.type = 'numeric';
structureInfo.level = level;
structureInfo.prefix = numericMatch[0];
structureInfo.content = text.substring(numericMatch[0].length).trim();
// 更新当前处理到的章节编号
if (level === 1) {
this.currentChapter = numericMatch[1];
this.currentSection = null;
this.currentSubsection = null;
} else if (level === 2) {
this.currentSection = numericMatch[2];
this.currentSubsection = null;
} else if (level === 3) {
this.currentSubsection = numericMatch[3];
}
return structureInfo;
}
// 检查是否为罗马数字标题
const romanMatch = text.match(romanPattern);
if (romanMatch) {
// 类似处理逻辑...
this.lastStructureType = 'roman';
this.inSimpleList = false;
let dots = 0;
for (let i = 1; i < romanMatch.length; i++) {
if (romanMatch[i]) dots++;
}
this.lastStructureLevel = dots + 1;
structureInfo.type = 'roman';
structureInfo.level = dots + 1;
structureInfo.prefix = romanMatch[0];
structureInfo.content = text.substring(romanMatch[0].length).trim();
return structureInfo;
}
// 检查是否为字母标题
const letterMatch = text.match(letterPattern);
if (letterMatch) {
// 类似处理逻辑...
this.lastStructureType = 'letter';
this.inSimpleList = false;
let dots = 0;
for (let i = 1; i < letterMatch.length; i++) {
if (letterMatch[i]) dots++;
}
this.lastStructureLevel = dots + 1;
structureInfo.type = 'letter';
structureInfo.level = dots + 1;
structureInfo.prefix = letterMatch[0];
structureInfo.content = text.substring(letterMatch[0].length).trim();
return structureInfo;
}
// 无法识别结构,重置简单列表状态
this.inSimpleList = false;
// 无法识别结构,返回默认值
return structureInfo;
},
// 根据标题文本和标签确定层级
determineLevel: function(text, tagName) {
// 首先分析标题结构
const structureInfo = this.analyzeHeading(text);
// 如果能识别结构化层级,则使用识别的层级
if (structureInfo.level !== null) {
return {
level: structureInfo.level,
structureInfo: structureInfo
};
}
// 无法识别结构,则根据标签名确定层级
let headingLevel = 0;
if (tagName.match(/^h[1-6]$/)) {
headingLevel = parseInt(tagName.substring(1));
} else {
headingLevel = 3; // 默认级别
}
return {
level: headingLevel,
structureInfo: structureInfo
};
}
};
filteredHeadings.forEach((nodeEl, idx) => {
// 补丁1:强制给没有 id 的标题分配唯一 id
if (!nodeEl.id) nodeEl.id = 'toc-auto-' + idx;
tocNodes.push(nodeEl); // 存储DOM节点
let zh = nodeEl.textContent.trim();
// 过滤掉 "原文块" 或 "译文块" 标题
if (zh.includes('原文块') || zh.includes('译文块')) {
return;
}
// 应用智能截断
let displayText = truncateText(zh);
let en = tocMap[zh]; // 获取英文翻译
// 检查与前一个TOC项是否相似,如果相似则合并
if (previousTocItem && areTextsSimilar(previousTocItem, zh)) {
// 不创建新的TOC项,而是更新前一个的引用
const lastItem = currentPath[currentPath.length - 1].children[currentPath[currentPath.length - 1].children.length - 1];
if (lastItem) {
lastItem.additionalTargetId = nodeEl.id;
}
return; // 跳过创建新的TOC项
}
// 记录当前项以供下一次比较
previousTocItem = zh;
// 确定标题级别
let headingLevel = 0;
let nodeTagName = nodeEl.tagName.toLowerCase();
let isChartCaption = nodeEl.dataset.isChartCaption === "true";
if (nodeEl.classList.contains('converted-from-heading') && nodeEl.dataset.originalTag) {
nodeTagName = nodeEl.dataset.originalTag; // 使用原始标签名决定TOC层级
}
// 图表标题处理逻辑
if (nodeEl.dataset.isCaptionToc === "true") {
// 图表标题默认为其父章节的下一级,先使用默认值
headingLevel = 4;
isChartCaption = true;
} else {
// 使用结构化识别确定层级
const { level, structureInfo } = levelManager.determineLevel(zh, nodeTagName);
headingLevel = level;
// 如果标题有结构化前缀,保存原始文本用于显示
if (structureInfo.prefix) {
nodeEl.dataset.structuredPrefix = structureInfo.prefix;
// 保存结构信息
nodeEl.dataset.structureType = structureInfo.type;
}
}
// 如果是图表标题,应用特殊的截断逻辑
if (isChartCaption) {
// 首先识别图表标题的前缀部分(如"图5.")
const titlePrefixMatch = zh.match(/^(图|表)\s*\d+\.?\s*/);
const titlePrefix = titlePrefixMatch ? titlePrefixMatch[0] : '';
const contentStart = titlePrefix.length;
// 在图表标题内容部分查找标点符号作为截断点
const dotIndex = zh.indexOf('。', contentStart);
const commaIndex = zh.indexOf(',', contentStart);
// 找到最近的中文标点
let firstPunctIndex = -1;
if (dotIndex !== -1 && commaIndex !== -1) {
firstPunctIndex = Math.min(dotIndex, commaIndex);
} else if (dotIndex !== -1) {
firstPunctIndex = dotIndex;
} else if (commaIndex !== -1) {
firstPunctIndex = commaIndex;
}
if (firstPunctIndex !== -1) {
// 找到了中文标点,在此处截断
displayText = zh.substring(0, firstPunctIndex + 1);
} else {
// 尝试查找英文标点
const enDotIndex = zh.indexOf('.', contentStart);
const enCommaIndex = zh.indexOf(',', contentStart);
let firstEnPunctIndex = -1;
if (enDotIndex !== -1 && enCommaIndex !== -1) {
firstEnPunctIndex = Math.min(enDotIndex, enCommaIndex);
} else if (enDotIndex !== -1) {
firstEnPunctIndex = enDotIndex;
} else if (enCommaIndex !== -1) {
firstEnPunctIndex = enCommaIndex;
}
// 确保句号不是数字后的小数点(如:图5.1中的点)
if (firstEnPunctIndex === enDotIndex && firstEnPunctIndex !== -1) {
let validDotIndex = firstEnPunctIndex;
while (validDotIndex !== -1) {
const charBeforeDot = zh.charAt(validDotIndex - 1);
const charAfterDot = zh.charAt(validDotIndex + 1);
// 如果句号前是数字,后也是数字,那么这可能是小数点,继续寻找下一个句号
if (/\d/.test(charBeforeDot) && /\d/.test(charAfterDot)) {
validDotIndex = zh.indexOf('.', validDotIndex + 1);
} else {
// 找到了有效的句号
break;
}
}
if (validDotIndex !== -1) {
displayText = zh.substring(0, validDotIndex + 1);
} else if (enCommaIndex !== -1) {
// 如果没有有效的句号但有逗号,使用逗号截断
displayText = zh.substring(0, enCommaIndex + 1);
}
} else if (firstEnPunctIndex !== -1) {
displayText = zh.substring(0, firstEnPunctIndex + 1);
}
}
// 图表标题特殊处理:将其归属到当前层级的下一级
// 计算其应该属于的层级
headingLevel = previousHeadingLevel + 1;
// 限制最大层级,防止层级过深
if (headingLevel > 6) headingLevel = 6;
}
// 新增:读取真实的 data-block-index
let realBlockIndex = nodeEl.dataset.blockIndex ? parseInt(nodeEl.dataset.blockIndex, 10) : null;
// 根据标题级别调整当前路径
if (headingLevel > previousHeadingLevel) {
// 进入更深层级
// 确保有父节点
if (currentPath[currentPath.length - 1].children.length === 0) {
// 如果当前路径的最后一个节点没有子节点,添加一个占位节点
// 获取当前文件名作为未命名章节的替代文本
const fileNameElement = document.getElementById('fileName');
const fileName = fileNameElement ? fileNameElement.textContent : '未命名章节';
const placeholderItem = {
id: 'placeholder-' + idx,
text: fileName,
level: previousHeadingLevel,
children: []
};
currentPath[currentPath.length - 1].children.push(placeholderItem);
}
// 将最后一个子节点作为新的当前节点
currentPath.push(currentPath[currentPath.length - 1].children[currentPath[currentPath.length - 1].children.length - 1]);
} else if (headingLevel < previousHeadingLevel) {
// 返回上层
const levelsToGoUp = previousHeadingLevel - headingLevel;
for (let i = 0; i < levelsToGoUp && currentPath.length > 1; i++) {
currentPath.pop();
}
}
// 创建新的TOC项
const tocItem = {
id: nodeEl.id,
text: displayText,
originalText: zh,
translation: en,
level: headingLevel,
children: [],
isChartCaption: isChartCaption,
structuredPrefix: nodeEl.dataset.structuredPrefix || null,
structureType: nodeEl.dataset.structureType || null,
structureInfo: levelManager.analyzeHeading(zh),
blockIndex: realBlockIndex // 新增,真实内容流 blockIndex
};
// 将TOC项添加到当前路径的最后一个节点
currentPath[currentPath.length - 1].children.push(tocItem);
previousHeadingLevel = headingLevel;
});
// === 在原有 TOC 节点生成后,补充 blockIndex/startBlockIndex/endBlockIndex 字段 ===
function parseBlockIndexFromId(id) {
if (!id) return null;
let match = id.match(/block-(\d+)/);
if (match) return parseInt(match[1], 10);
match = id.match(/toc-anchor-(\d+)/);
if (match) return parseInt(match[1], 10);
match = id.match(/toc-auto-(\d+)/);
if (match) return parseInt(match[1], 10);
match = id.match(/auto-hx-(\d+)/); // 新增支持 auto-hx-数字
if (match) return parseInt(match[1], 10);
return null;
}
function supplementBlockIndexRecursive(nodes) {
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
node.blockIndex = parseBlockIndexFromId(node.id);
node.startBlockIndex = node.blockIndex;
if (i < nodes.length - 1) {
node.endBlockIndex = nodes[i + 1].blockIndex !== null ? nodes[i + 1].blockIndex - 1 : null;
} else {
node.endBlockIndex = null;
}
if (node.children && node.children.length > 0) {
supplementBlockIndexRecursive(node.children);
}
if (node.blockIndex == null && node.el && node.el.dataset && node.el.dataset.blockIndex) {
node.blockIndex = parseInt(node.el.dataset.blockIndex, 10);
}
}
}
if (tocStructure && tocStructure.children && tocStructure.children.length > 0) {
supplementBlockIndexRecursive(tocStructure.children);
}
// 递归构建TOC HTML
function buildTocHtml(items, parentElement) {
items.forEach(item => {
// 跳过占位符和空标题
// 1. 空标题
// 2. "未命名章节"占位符
// 3. placeholder- 开头的ID(占位符标识)
// 4. undefined/null 文本
if (!item.text ||
item.text === '未命名章节' ||
item.text === 'undefined' ||
item.text === 'null' ||
(item.id && item.id.indexOf('placeholder-') === 0)) {
return;
}
const li = document.createElement('li');
const hasChildren = item.children && item.children.length > 0;
// 设置CSS类
if (item.level) {
// 只基于item属性判断
if (item.isCaption || item.isChartCaption) {
li.className = 'toc-caption';
} else {
li.className = `toc-h${item.level}`;
}
// 检查是否为简单数字列表项或带空格的多级编号标题
const isSimpleNumbered = item.structureInfo && item.structureInfo.isSimpleNumbered;
const hasSpacedNumeric = item.structureInfo && item.structureInfo.originalPrefix;
const structureType = item.structureInfo && item.structureInfo.type;
// 如果有结构化前缀、是简单数字列表项或带空格的多级编号,添加结构化样式类
if (item.structuredPrefix || isSimpleNumbered || hasSpacedNumeric ||
(structureType && structureType !== 'normal')) {
li.classList.add(`toc-structured`);
// 对于简单数字列表项,使用特殊样式类
if (isSimpleNumbered) {
li.classList.add('toc-simple-numbered');
li.classList.add(`toc-structure-simple-numbered`);
}
// 对于带空格的多级编号,使用标准数字编号样式
else if (hasSpacedNumeric) {
li.classList.add(`toc-structure-numeric`);
li.classList.add('toc-spaced-numeric');
}
// 处理各种新增的列表项类型
else if (structureType) {
li.classList.add(`toc-structure-${structureType}`);
}
else {
li.classList.add(`toc-structure-${item.structureType || 'normal'}`);
}
}
}
if (hasChildren) {
li.classList.add('has-children');
}
// 构建链接HTML
let linkHTML = '';
if (hasChildren) {
linkHTML += `▼`;
}
// 包装文本内容在一个span中,以便更好地控制多行显示
linkHTML += ``;
// 如果有结构化前缀、是简单数字列表项或带空格的多级编号,特殊显示前缀
if (item.structuredPrefix || (item.structureInfo && item.structureInfo.prefix)) {
// 对于带空格的多级编号,优先使用标准化的前缀
let prefix = item.structuredPrefix || item.structureInfo.prefix;
if (item.structureInfo && item.structureInfo.originalPrefix) {
prefix = item.structureInfo.originalPrefix; // 使用标准化的格式,如"3.1.1"
}
linkHTML += `${prefix} `;
}
// 如果是图表标题,添加特殊图标
if (item.isChartCaption) {
// 根据图表类型显示不同图标
const isTable = item.originalText && item.originalText.trim().startsWith('表');
const icon = isTable ? '📊' : '📈';
linkHTML += `${icon} `;
}
// 显示主要文本内容
let displayText = item.text;
// 如果有结构化前缀,从显示文本中移除
if (item.structuredPrefix && displayText.indexOf(item.structuredPrefix) === 0) {
displayText = displayText.replace(item.structuredPrefix, '').trim();
}
// 如果是简单数字列表项,从显示文本中移除前缀
else if (item.structureInfo && item.structureInfo.prefix &&
displayText.indexOf(item.structureInfo.prefix) === 0) {
displayText = displayText.replace(item.structureInfo.prefix, '').trim();
}
// 将文本内容包装在span中,确保正确显示
linkHTML += `${displayText}`;
if (item.translation && item.translation !== item.originalText) {
linkHTML += ` / ${item.translation}`;
}
linkHTML += ``;
const link = document.createElement('a');
link.href = `#${item.id}`;
link.innerHTML = linkHTML;
if (item.originalText) {
link.dataset.originalText = item.originalText;
}
if (item.additionalTargetId) {
link.dataset.additionalTargetId = item.additionalTargetId;
}
// 添加点击事件
link.onclick = function(e) {
e.preventDefault();
const targetElement = document.getElementById(item.id);
if (targetElement) {
const clickedNodeIndex = tocNodes.findIndex(n => n.id === item.id);
let currentTopNodeIndex = 0;
if (tocNodes.length > 0) {
let minPositiveTop = Infinity;
let foundPositive = false;
for (let i = 0; i < tocNodes.length; i++) {
const rect = tocNodes[i].getBoundingClientRect();
if (rect.top >= 0 && rect.top < minPositiveTop) {
minPositiveTop = rect.top;
currentTopNodeIndex = i;
foundPositive = true;
}
}
if (!foundPositive) {
let maxNegativeTop = -Infinity;
let foundNegative = false;
for (let i = 0; i < tocNodes.length; i++) {
const rect = tocNodes[i].getBoundingClientRect();
if (rect.top < 0 && rect.top > maxNegativeTop) {
maxNegativeTop = rect.top;
currentTopNodeIndex = i;
foundNegative = true;
}
}
if (!foundNegative && tocNodes.length > 0) {
currentTopNodeIndex = 0;
}
}
}
const indexDifference = Math.abs(clickedNodeIndex - currentTopNodeIndex);
if (indexDifference >= 6) {
// 使用原始文本而非截断后的文本显示加载效果
const originalText = this.dataset.originalText || item.originalText;
showTemporaryLoadingEffect(originalText || "目标章节");
}
// 修复:在沉浸模式下使用自定义滚动逻辑,避免布局偏移
if (window.ImmersiveLayout && window.ImmersiveLayout.isActive()) {
// 沉浸模式下使用自定义滚动定位
const scrollContainer = document.querySelector('#immersive-main-content-area .tab-content');
if (scrollContainer && scrollContainer.style.overflowY === 'auto') {
// 计算目标元素相对于滚动容器的位置
const containerRect = scrollContainer.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();
const currentScrollTop = scrollContainer.scrollTop;
// 计算目标位置(将元素置于容器中心)
const targetScrollTop = currentScrollTop + targetRect.top - containerRect.top - (containerRect.height / 2) + (targetRect.height / 2);
// 平滑滚动到目标位置
scrollContainer.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
});
} else {
// 备用方案:使用原生scrollIntoView
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
} else {
// 普通模式下使用原生scrollIntoView
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
// 添加临时高亮效果
targetElement.classList.add('toc-target-highlight');
// 3秒后移除高亮效果
setTimeout(() => {
targetElement.classList.remove('toc-target-highlight');
}, 3000);
// 检查是否有额外的目标节点
const additionalTargetId = this.dataset.additionalTargetId;
if (additionalTargetId) {
const additionalTarget = document.getElementById(additionalTargetId);
if (additionalTarget) {
// 也为额外目标添加高亮效果
additionalTarget.classList.add('toc-target-highlight');
setTimeout(() => {
additionalTarget.classList.remove('toc-target-highlight');
}, 3000);
}
}
}
};
li.appendChild(link);
// 如果有子项,创建子项容器
if (hasChildren) {
const childrenContainer = document.createElement('ul');
childrenContainer.className = 'toc-children';
buildTocHtml(item.children, childrenContainer);
li.appendChild(childrenContainer);
// 为折叠按钮添加点击事件,确保事件正确处理
const toggleBtn = li.querySelector('.toc-toggle');
if (toggleBtn) {
// 移除现有的事件监听器(如果有)
toggleBtn.replaceWith(toggleBtn.cloneNode(true));
// 重新获取按钮并添加事件
const newToggleBtn = li.querySelector('.toc-toggle');
newToggleBtn.addEventListener('click', function(e) {
e.stopPropagation(); // 阻止事件冒泡
e.preventDefault(); // 防止链接被点击
// 获取子项容器
const childContainer = this.closest('li').querySelector('.toc-children');
if (childContainer) {
toggleTocItem(this, childContainer);
}
});
}
}
parentElement.appendChild(li);
});
}
// 构建TOC HTML
buildTocHtml(tocStructure.children, tocList);
window.getCurrentTocStructure = function() {
return tocStructure;
};
// Expose getTocNodes to window
window.getTocNodes = function() {
return tocNodes;
};
}
// 点击页面其他地方关闭目录 (可选,如果需要请取消注释)
/*
document.addEventListener('click', function(event) {
if (tocPopup.classList.contains('toc-popup-visible') &&
!tocPopup.contains(event.target) &&
!tocBtn.contains(event.target)) {
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');
}, 200);
}
});
*/
window.refreshTocList = function() {
updateTocModeSelectorVisibility();
renderTocList();
};
// 初始化TOC界面
updateTocModeSelectorVisibility();
// 监听标签页切换事件,当标签页切换时更新TOC模式选择器可见性
document.querySelectorAll('.tab-btn').forEach(tab => {
tab.addEventListener('click', updateTocModeSelectorVisibility);
});
// 展开/收起TOC的功能
document.getElementById('toc-expand-btn').addEventListener('click', function() {
const isExpanded = tocPopup.classList.contains('toc-expanded');
const icon = this.querySelector('i');
if (isExpanded) {
tocPopup.classList.remove('toc-expanded');
icon.classList.remove('fa-angles-left');
icon.classList.add('fa-angles-right');
this.title = '展开目录';
} else {
tocPopup.classList.add('toc-expanded');
icon.classList.remove('fa-angles-right');
icon.classList.add('fa-angles-left');
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);
}
});
});
// 目前的结构中,TocFeature 是一个IIFE,它会立即执行。
// 它将 refreshTocList 函数暴露到 window 对象。
// 在 history_detail.html 中,showTab 函数会调用 window.refreshTocList()。
// 因此,只要 toc_logic.js 在调用 showTab 的主脚本之前加载,这个设置就应该能工作。
})();