let currentCategory = ''; let currentDoc = ''; let docList = []; let currentMarkdownContent = ''; // 保存当前文档的markdown源码 let isEditMode = false; // 是否处于编辑模式 let savedScrollPosition = 0; // 保存的滚动位置 let currentScrollObserver = null; // 保存当前的滚动监听器 // DOM 元素缓存 const DOM = { loading: null, content: null, docNav: null, toc: null, leftSidebar: null, rightSidebar: null, editBtn: null, editorContainer: null, markdownEditor: null, editorDocName: null, navPrev: null, navNext: null, contentArea: null, searchContainer: null, searchToggleBtn: null, searchInput: null, searchBtn: null, searchResults: null, closeSearchBoxBtn: null, saveBtn: null, cancelBtn: null, backToTopBtn: null, docNavigation: null, toggleLeft: null, toggleRight: null, categoryNameSpan: null, currentDocResults: null, otherDocResults: null }; // 配置 marked marked.setOptions({ highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { try { const result = hljs.highlight(code, { language: lang }); return result.value; } catch (err) { console.error('Highlight error:', err); } } // 自动检测语言 const autoResult = hljs.highlightAuto(code); return autoResult.value; }, breaks: true, gfm: true }); // 从 URL 获取参数 function getQueryParams() { const params = new URLSearchParams(window.location.search); return { category: params.get('category'), doc: params.get('doc') }; } // 根据文档路径获取展示名称(仅保留最后一段) function getDocDisplayName(docName) { if (!docName || typeof docName !== 'string') { return ''; } const segments = docName.split(/[\\\/]/); const name = segments.pop(); return name || docName; } // 加载文档列表 async function loadDocList() { const { category } = getQueryParams(); if (!category) { window.location.href = '/'; return; } currentCategory = category; if (!DOM.categoryNameSpan) DOM.categoryNameSpan = document.querySelector('#category-title .category-name'); if (DOM.categoryNameSpan) { DOM.categoryNameSpan.textContent = category; } try { const response = await fetch(`/api/category/${encodeURIComponent(category)}`); if (!response.ok) throw new Error('获取文档列表失败'); const categoryData = await response.json(); docList = categoryData.docs.map(doc => ({ ...doc, displayName: getDocDisplayName(doc.name) })); renderDocNav(docList); // 加载第一篇文档(或 URL 指定的文档) const { doc } = getQueryParams(); const firstDoc = doc || docList[0]?.name; if (firstDoc) { loadDocument(firstDoc); } } catch (err) { console.error('Error loading doc list:', err); showError('加载文档列表失败'); } } // 渲染文档导航 function renderDocNav(docs) { const nav = DOM.docNav || document.getElementById('doc-nav'); if (!DOM.docNav) DOM.docNav = nav; nav.innerHTML = ''; docs.forEach(doc => { const item = document.createElement('div'); item.className = `nav-item level-${doc.level}`; item.dataset.docName = doc.name; const link = document.createElement('a'); link.href = '#'; link.className = 'nav-link'; link.textContent = doc.displayName || getDocDisplayName(doc.name); link.title = doc.name; item.appendChild(link); nav.appendChild(item); }); // 使用事件委托 - 只添加一个监听器 nav.removeEventListener('click', handleNavClick); nav.addEventListener('click', handleNavClick); } // 导航点击处理函数 function handleNavClick(e) { if (e.target.classList.contains('nav-link')) { e.preventDefault(); const item = e.target.parentElement; const docName = item.dataset.docName; if (docName) { loadDocument(docName); updateURL(currentCategory, docName); // 更新活动状态 - 使用缓存的查询结果 const navItems = DOM.docNav.querySelectorAll('.nav-item'); navItems.forEach(el => el.classList.toggle('active', el === item)); } } } // 加载文档内容 async function loadDocument(docName, scrollToText = null) { // 使用 DOM 缓存 if (!DOM.loading) DOM.loading = document.getElementById('loading'); if (!DOM.content) DOM.content = document.getElementById('markdown-content'); DOM.loading.style.display = 'flex'; DOM.content.innerHTML = ''; try { // 对于包含子目录的文档名,需要分段编码 const encodedDocName = docName.split('/').map(part => encodeURIComponent(part)).join('/'); const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodedDocName}`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || `加载失败 (${response.status})`); } const data = await response.json(); // 验证数据有效性 if (!data.content || typeof data.content !== 'string') { throw new Error('文档内容格式错误'); } currentDoc = docName; currentMarkdownContent = data.content; // 保存markdown源码 // 渲染 Markdown const html = marked.parse(data.content); DOM.content.innerHTML = html; // 处理图片路径,将相对路径转换为正确的URL DOM.content.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src'); // 如果是相对路径(不以 http/https 开头) if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('/')) { // 使用新的API路径,避免中文路径问题 const newSrc = `/api/image/${encodeURIComponent(currentCategory)}/${src}`; img.setAttribute('src', newSrc); // 添加错误处理 img.onerror = function() { console.error('Failed to load image:', newSrc); // 显示占位图片或错误提示 this.alt = `图片加载失败: ${src}`; this.classList.add('img-error'); }; } }); // 手动为所有代码块添加 hljs 类并确保高亮(修复 marked 高亮可能失败的问题) DOM.content.querySelectorAll('pre code').forEach((block) => { // 如果代码块没有 hljs 类,添加它 if (!block.classList.contains('hljs')) { block.classList.add('hljs'); } // 如果没有高亮的标记(没有 span 元素),尝试重新高亮 if (!block.querySelector('span')) { const lang = block.className.match(/language-(\w+)/)?.[1]; if (lang && hljs.getLanguage(lang)) { try { const highlighted = hljs.highlight(block.textContent, { language: lang }); block.innerHTML = highlighted.value; block.classList.add('hljs', `language-${lang}`); } catch (err) { console.error('Error re-highlighting:', err); } } else { // 尝试自动检测 try { const highlighted = hljs.highlightAuto(block.textContent); block.innerHTML = highlighted.value; block.classList.add('hljs'); if (highlighted.language) { block.classList.add(`language-${highlighted.language}`); } } catch (err) { console.error('Error auto-highlighting:', err); } } } }); // 为所有代码块添加复制按钮 addCopyButtonsToCodeBlocks(); // 生成 TOC generateTOC(); // 更新文档导航(上一页/下一页) updateDocNavigation(); // 更新活动文档 - 使用 toggle 优化 if (DOM.docNav) { const navItems = DOM.docNav.querySelectorAll('.nav-item'); navItems.forEach(el => el.classList.toggle('active', el.dataset.docName === docName)); } DOM.loading.style.display = 'none'; // 如果指定了要滚动到的文本,等待渲染完成后滚动 if (scrollToText) { setTimeout(() => { scrollToSearchMatch(scrollToText); }, 100); } } catch (err) { console.error('Error loading document:', err); DOM.loading.style.display = 'none'; showError(`加载文档失败:${err.message || '请稍后重试'}`); } } // 生成 TOC function generateTOC() { // 使用 DOM 缓存 if (!DOM.toc) DOM.toc = document.getElementById('toc'); // 确保 DOM.content 也被正确缓存 if (!DOM.content) DOM.content = document.getElementById('markdown-content'); // 先清空旧的TOC内容和事件监听器 DOM.toc.innerHTML = ''; DOM.toc.removeEventListener('click', handleTocClick); const headings = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6'); if (headings.length === 0) { DOM.toc.innerHTML = '

本文档没有标题

'; return; } // 创建标题 ID 映射 const headingMap = new Map(); headings.forEach((heading, index) => { // 给标题添加 ID if (!heading.id) { heading.id = `heading-${index}`; } const level = parseInt(heading.tagName.substring(1)); const link = document.createElement('a'); link.href = `#${heading.id}`; link.className = `toc-link toc-level-${level}`; link.textContent = heading.textContent; headingMap.set(heading.id, heading); DOM.toc.appendChild(link); }); // 使用事件委托 - 只添加一个监听器 DOM.toc.removeEventListener('click', handleTocClick); DOM.toc.addEventListener('click', (e) => handleTocClick(e, headingMap)); // 监听滚动,高亮当前标题 setupScrollSpy(headings); } // TOC 点击处理函数 function handleTocClick(e, headingMap) { if (e.target.classList.contains('toc-link')) { e.preventDefault(); const headingId = e.target.getAttribute('href').substring(1); const heading = headingMap.get(headingId); if (heading) { heading.scrollIntoView({ behavior: 'smooth', block: 'start' }); // 更新活动状态 const tocLinks = DOM.toc.querySelectorAll('.toc-link'); tocLinks.forEach(el => el.classList.toggle('active', el === e.target)); } } } // 滚动监听 function setupScrollSpy(headings) { // 清理之前的 observer if (currentScrollObserver) { currentScrollObserver.disconnect(); currentScrollObserver = null; } // 如果没有标题,直接返回 if (!headings || headings.length === 0) { return; } let activeLink = null; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const id = entry.target.id; const link = DOM.toc.querySelector(`.toc-link[href="#${id}"]`); if (link && link !== activeLink) { // 只在需要时更新,减少 DOM 操作 if (activeLink) activeLink.classList.remove('active'); link.classList.add('active'); activeLink = link; } } }); }, { rootMargin: '-100px 0px -80% 0px', threshold: 0 }); headings.forEach(heading => observer.observe(heading)); // 保存当前的 observer 以便后续清理 currentScrollObserver = observer; } // 更新 URL function updateURL(category, doc) { const url = new URL(window.location); url.searchParams.set('category', category); url.searchParams.set('doc', doc); window.history.pushState({}, '', url); } // 更新文档导航(上一页/下一页) function updateDocNavigation() { // 缓存 DOM 元素 if (!DOM.navPrev) DOM.navPrev = document.getElementById('nav-prev'); if (!DOM.navNext) DOM.navNext = document.getElementById('nav-next'); if (!DOM.navPrev || !DOM.navNext || docList.length === 0) return; if (!DOM.docNavigation) DOM.docNavigation = document.getElementById('doc-navigation'); if (!DOM.docNavigation) return; // 找到当前文档在列表中的索引 const currentIndex = docList.findIndex(doc => doc.name === currentDoc); if (currentIndex === -1) { DOM.navPrev.style.display = 'none'; DOM.navNext.style.display = 'none'; DOM.docNavigation.className = 'doc-navigation'; return; } const hasPrev = currentIndex > 0; const hasNext = currentIndex < docList.length - 1; // 辅助函数:设置导航按钮 const setupNavButton = (button, doc) => { if (doc) { button.style.display = 'flex'; const displayName = doc.displayName || getDocDisplayName(doc.name); button.querySelector('.nav-doc-name').textContent = displayName; button.onclick = () => { loadDocument(doc.name); updateURL(currentCategory, doc.name); // 滚动到顶部 if (!DOM.contentArea) DOM.contentArea = document.getElementById('content-area'); if (DOM.contentArea) { DOM.contentArea.scrollTo({ top: 0, behavior: 'smooth' }); } }; } else { button.style.display = 'none'; } }; // 设置上一页和下一页按钮 setupNavButton(DOM.navPrev, hasPrev ? docList[currentIndex - 1] : null); setupNavButton(DOM.navNext, hasNext ? docList[currentIndex + 1] : null); // 根据导航按钮状态添加类,用于CSS定位 if (hasPrev && hasNext) { DOM.docNavigation.className = 'doc-navigation has-both'; } else if (hasPrev) { DOM.docNavigation.className = 'doc-navigation has-prev-only'; } else if (hasNext) { DOM.docNavigation.className = 'doc-navigation has-next-only'; } else { DOM.docNavigation.className = 'doc-navigation'; } } // 显示错误 function showError(message) { if (!DOM.content) DOM.content = document.getElementById('markdown-content'); if (DOM.content) { DOM.content.innerHTML = `
${message}
`; } } // 切换侧边栏(移动端) function setupSidebarToggles() { if (!DOM.toggleLeft) DOM.toggleLeft = document.getElementById('toggle-left'); if (!DOM.toggleRight) DOM.toggleRight = document.getElementById('toggle-right'); const toggleLeft = DOM.toggleLeft; const toggleRight = DOM.toggleRight; // 使用 DOM 缓存 if (!DOM.leftSidebar) DOM.leftSidebar = document.getElementById('left-sidebar'); if (!DOM.rightSidebar) DOM.rightSidebar = document.getElementById('right-sidebar'); // 移动端/平板初始化:默认折叠侧边栏 if (window.innerWidth <= 768) { DOM.leftSidebar.classList.add('collapsed'); DOM.rightSidebar.classList.add('collapsed'); } else if (window.innerWidth <= 1024) { // 平板:只折叠右侧栏 DOM.rightSidebar.classList.add('collapsed'); } // 监听窗口大小变化 window.addEventListener('resize', () => { if (window.innerWidth <= 768) { // 移动端:确保左右侧边栏都折叠 if (!DOM.leftSidebar.classList.contains('collapsed')) { DOM.leftSidebar.classList.add('collapsed'); } if (!DOM.rightSidebar.classList.contains('collapsed')) { DOM.rightSidebar.classList.add('collapsed'); } } else if (window.innerWidth <= 1024) { // 平板:只折叠右侧栏,展开左侧栏 DOM.leftSidebar.classList.remove('collapsed'); if (!DOM.rightSidebar.classList.contains('collapsed')) { DOM.rightSidebar.classList.add('collapsed'); } } else { // 桌面端:展开所有侧边栏 DOM.leftSidebar.classList.remove('collapsed'); DOM.rightSidebar.classList.remove('collapsed'); } }); toggleLeft.onclick = (e) => { e.stopPropagation(); // 如果右侧栏展开,先隐藏它 if (!DOM.rightSidebar.classList.contains('collapsed')) { DOM.rightSidebar.classList.add('collapsed'); } DOM.leftSidebar.classList.toggle('collapsed'); }; toggleRight.onclick = (e) => { e.stopPropagation(); // 如果左侧栏展开,先隐藏它 if (!DOM.leftSidebar.classList.contains('collapsed')) { DOM.leftSidebar.classList.add('collapsed'); } DOM.rightSidebar.classList.toggle('collapsed'); }; // 点击侧边栏外部时隐藏所有已展开的侧边栏(仅在移动端和平板) document.addEventListener('click', (e) => { // 仅在移动端和平板上启用此功能 if (window.innerWidth > 1024) return; const isClickInsideLeftSidebar = DOM.leftSidebar.contains(e.target); const isClickInsideRightSidebar = DOM.rightSidebar.contains(e.target); const isToggleButton = e.target.closest('#toggle-left') || e.target.closest('#toggle-right'); // 如果点击的不是侧边栏内部,也不是切换按钮,则隐藏所有展开的侧边栏 if (!isClickInsideLeftSidebar && !isClickInsideRightSidebar && !isToggleButton) { if (!DOM.leftSidebar.classList.contains('collapsed')) { DOM.leftSidebar.classList.add('collapsed'); } if (!DOM.rightSidebar.classList.contains('collapsed')) { DOM.rightSidebar.classList.add('collapsed'); } } }); } // 搜索功能 function initSearch() { // 初始化搜索相关的DOM缓存 if (!DOM.searchContainer) DOM.searchContainer = document.getElementById('search-container'); if (!DOM.searchToggleBtn) DOM.searchToggleBtn = document.getElementById('search-toggle-btn'); if (!DOM.searchInput) DOM.searchInput = document.getElementById('search-input'); if (!DOM.searchBtn) DOM.searchBtn = document.getElementById('search-btn'); if (!DOM.searchResults) DOM.searchResults = document.getElementById('search-results'); if (!DOM.closeSearchBoxBtn) DOM.closeSearchBoxBtn = document.getElementById('close-search-box'); // 检查元素是否存在 if (!DOM.searchContainer || !DOM.searchToggleBtn || !DOM.searchInput || !DOM.searchBtn || !DOM.searchResults || !DOM.closeSearchBoxBtn) { console.error('Search elements not found:', { searchContainer: !!DOM.searchContainer, searchToggleBtn: !!DOM.searchToggleBtn, searchInput: !!DOM.searchInput, searchBtn: !!DOM.searchBtn, searchResults: !!DOM.searchResults, closeSearchBoxBtn: !!DOM.closeSearchBoxBtn }); return; } const searchContainer = DOM.searchContainer; const searchToggleBtn = DOM.searchToggleBtn; const searchInput = DOM.searchInput; const searchBtn = DOM.searchBtn; const searchResults = DOM.searchResults; const closeSearchBoxBtn = DOM.closeSearchBoxBtn; let searchTimeout; const SEARCH_HISTORY_KEY = 'cjydocs_search_history'; // 保存搜索历史到localStorage const saveSearchHistory = (query) => { if (!query || query.trim().length < 2) return; try { let history = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '{}'); history[currentCategory] = query; localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history)); } catch (err) { console.error('Failed to save search history:', err); } }; // 搜索函数 const performSearch = async () => { const query = searchInput.value.trim(); if (query.length < 2) { searchResults.style.display = 'none'; return; } // 保存搜索历史 saveSearchHistory(query); try { const response = await fetch( `/api/search/${encodeURIComponent(currentCategory)}?q=${encodeURIComponent(query)}¤tDoc=${encodeURIComponent(currentDoc)}` ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || `搜索失败 (${response.status})`); } const data = await response.json(); displaySearchResults(data); searchResults.style.display = 'block'; } catch (err) { console.error('Search error:', err); // 向用户显示错误信息 displaySearchError(err.message || '搜索失败,请稍后重试'); } }; // 加载搜索历史 const loadSearchHistory = () => { try { const history = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '{}'); const lastQuery = history[currentCategory]; if (lastQuery) { searchInput.value = lastQuery; // 自动搜索上次的内容 performSearch(); } } catch (err) { console.error('Failed to load search history:', err); } }; // 清除其他分类的搜索历史 const clearOtherCategoryHistory = () => { try { let history = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '{}'); // 只保留当前分类 history = { [currentCategory]: history[currentCategory] || '' }; localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history)); } catch (err) { console.error('Failed to clear search history:', err); } }; // 切换搜索框显示/隐藏 searchToggleBtn.addEventListener('click', (e) => { e.stopPropagation(); const isActive = searchContainer.classList.toggle('active'); if (isActive) { // 加载搜索历史 loadSearchHistory(); // 聚焦到搜索框 setTimeout(() => searchInput.focus(), 100); } else { // 隐藏搜索结果 searchResults.style.display = 'none'; } }); // 输入时实时搜索(防抖) - 500ms 减少 API 调用 searchInput.addEventListener('input', () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(performSearch, 500); }); // 点击搜索按钮 searchBtn.addEventListener('click', performSearch); // 按回车搜索 searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { performSearch(); } }); // 关闭搜索框按钮 closeSearchBoxBtn.addEventListener('click', (e) => { e.stopPropagation(); searchContainer.classList.remove('active'); searchResults.style.display = 'none'; }); // 点击搜索结果外部关闭 searchResults.addEventListener('click', (e) => { // 如果点击的是搜索结果容器本身(即遮罩背景),关闭搜索 if (e.target === searchResults) { searchResults.style.display = 'none'; } }); // 点击外部关闭 document.addEventListener('click', (e) => { if (!searchResults.contains(e.target) && !searchInput.contains(e.target) && !searchBtn.contains(e.target) && !searchContainer.contains(e.target) && !searchToggleBtn.contains(e.target)) { searchResults.style.display = 'none'; } }); // 初始化时清除其他分类的历史 clearOtherCategoryHistory(); } // 显示搜索错误 function displaySearchError(message) { if (!DOM.currentDocResults) DOM.currentDocResults = document.getElementById('current-doc-results'); if (!DOM.otherDocResults) DOM.otherDocResults = document.getElementById('other-docs-results'); const currentDocSection = DOM.currentDocResults ? DOM.currentDocResults.querySelector('.results-list') : null; const otherDocsSection = DOM.otherDocResults ? DOM.otherDocResults.querySelector('.results-list') : null; // 清空之前的结果 currentDocSection.innerHTML = ''; otherDocsSection.innerHTML = ''; // 隐藏分组标题 DOM.currentDocResults.style.display = 'none'; DOM.otherDocResults.style.display = 'none'; // 显示错误信息 if (currentDocSection) { currentDocSection.innerHTML = `

${message}

`; } DOM.currentDocResults.style.display = 'block'; // 显示搜索结果面板 if (DOM.searchResults) { DOM.searchResults.style.display = 'block'; } } // 显示搜索结果 function displaySearchResults(data) { // 获取搜索结果内容容器 const searchResultsContent = document.querySelector('.search-results-content'); // 检查是否完全没有结果 const hasNoResults = data.currentDoc.length === 0 && data.otherDocs.length === 0; if (hasNoResults) { // 完全没有搜索结果时,显示友好的空状态 // 先清空内容区 searchResultsContent.innerHTML = ''; // 创建空状态容器 const emptyState = document.createElement('div'); emptyState.className = 'empty-search-state'; emptyState.innerHTML = `

没有找到相关内容

试试其他关键词,或者检查拼写是否正确

`; searchResultsContent.appendChild(emptyState); return; } // 有搜索结果时,需要恢复正常的DOM结构 // 先检查是否需要重建结构(从空状态恢复) let currentDocResults = document.getElementById('current-doc-results'); let otherDocResults = document.getElementById('other-docs-results'); // 如果结构不存在(被空状态替换了),重新创建 if (!currentDocResults || !otherDocResults) { searchResultsContent.innerHTML = `

当前文档

其他文档

`; // 重新获取引用 currentDocResults = document.getElementById('current-doc-results'); otherDocResults = document.getElementById('other-docs-results'); // 更新DOM缓存 DOM.currentDocResults = currentDocResults; DOM.otherDocResults = otherDocResults; } const currentDocSection = currentDocResults.querySelector('.results-list'); const otherDocsSection = otherDocResults.querySelector('.results-list'); // 清空之前的结果 currentDocSection.innerHTML = ''; otherDocsSection.innerHTML = ''; // 隐藏/显示分组标题 currentDocResults.style.display = data.currentDoc.length > 0 ? 'block' : 'none'; otherDocResults.style.display = data.otherDocs.length > 0 ? 'block' : 'none'; // 渲染当前文档结果 if (data.currentDoc.length > 0) { data.currentDoc.forEach(doc => { const docResults = createDocResultElement(doc, true); currentDocSection.appendChild(docResults); }); } // 渲染其他文档结果 if (data.otherDocs.length > 0) { data.otherDocs.forEach(doc => { const docResults = createDocResultElement(doc, false); otherDocsSection.appendChild(docResults); }); } } // 创建搜索结果元素 function createDocResultElement(doc, isCurrent) { const container = document.createElement('div'); container.className = 'search-result-item'; const header = document.createElement('div'); header.className = 'result-header'; header.innerHTML = ` ${doc.docName} ${doc.matchCount} 个匹配 `; container.appendChild(header); // 添加匹配片段 doc.matches.forEach(match => { const matchItem = document.createElement('div'); matchItem.className = 'result-match'; // 高亮搜索词 const highlightedSnippet = highlightSearchTerm(match.snippet, DOM.searchInput ? DOM.searchInput.value : ''); matchItem.innerHTML = `
行 ${match.line}
${highlightedSnippet}
`; // 点击跳转 matchItem.onclick = () => { // 隐藏搜索框和搜索结果 if (DOM.searchContainer) { DOM.searchContainer.classList.remove('active'); } if (DOM.searchResults) { DOM.searchResults.style.display = 'none'; } // 在移动端和平板上关闭所有侧边栏,确保用户能看到跳转后的内容 if (window.innerWidth <= 1024) { if (DOM.leftSidebar && !DOM.leftSidebar.classList.contains('collapsed')) { DOM.leftSidebar.classList.add('collapsed'); } if (DOM.rightSidebar && !DOM.rightSidebar.classList.contains('collapsed')) { DOM.rightSidebar.classList.add('collapsed'); } } if (isCurrent) { // 当前文档,滚动到对应位置 scrollToSearchMatch(match.fullLine); } else { // 其他文档,加载该文档并跳转到具体位置 loadDocument(doc.docName, match.fullLine); } }; container.appendChild(matchItem); }); return container; } // 高亮搜索词 function highlightSearchTerm(text, searchTerm) { const regex = new RegExp(`(${searchTerm})`, 'gi'); return text.replace(regex, '$1'); } // 滚动到搜索匹配位置 function scrollToSearchMatch(fullLine) { const content = document.getElementById('markdown-content'); const searchText = fullLine.trim(); if (!searchText) return; // 使用TreeWalker遍历所有文本节点 const walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT, null ); let found = false; let node; while (node = walker.nextNode()) { if (node.textContent.includes(searchText)) { const parentElement = node.parentElement; // 滚动到元素 parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 临时高亮父元素 parentElement.classList.add('temp-highlight'); setTimeout(() => { parentElement.classList.remove('temp-highlight'); }, 2000); found = true; break; } } // 如果没有找到精确匹配,尝试部分匹配 if (!found) { const elements = content.querySelectorAll('p, li, td, th, h1, h2, h3, h4, h5, h6, blockquote, pre, code'); for (const element of elements) { const text = element.textContent.trim(); // 尝试匹配至少50%的内容 if (text.length > 10 && searchText.includes(text.substring(0, Math.min(text.length, 50)))) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 临时高亮 element.classList.add('temp-highlight'); setTimeout(() => { element.classList.remove('temp-highlight'); }, 2000); break; } } } } // 设置回到顶部按钮 function setupBackToTop() { if (!DOM.backToTopBtn) DOM.backToTopBtn = document.getElementById('back-to-top'); const backToTopBtn = DOM.backToTopBtn; if (!backToTopBtn) return; // 初始状态:隐藏按钮 backToTopBtn.classList.add('hidden'); // 点击按钮滚动到顶部 backToTopBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // 滚动到顶部 window.scrollTo({ top: 0, behavior: 'smooth' }); }); // 监听窗口滚动事件,根据滚动位置控制按钮显示/隐藏 let scrollTimeout; const handleScroll = () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; // 滚动超过 300px 时显示按钮 if (scrollTop > 300) { backToTopBtn.classList.remove('hidden'); } else { backToTopBtn.classList.add('hidden'); } }, 100); }; window.addEventListener('scroll', handleScroll); } // 编辑功能 function setupEditFeature() { // 缓存DOM元素 if (!DOM.editBtn) DOM.editBtn = document.getElementById('edit-btn'); if (!DOM.editorContainer) DOM.editorContainer = document.getElementById('editor-container'); if (!DOM.markdownEditor) DOM.markdownEditor = document.getElementById('markdown-editor'); if (!DOM.editorDocName) DOM.editorDocName = document.getElementById('editor-doc-name'); if (!DOM.saveBtn) DOM.saveBtn = document.getElementById('save-btn'); if (!DOM.cancelBtn) DOM.cancelBtn = document.getElementById('cancel-edit-btn'); if (!DOM.contentArea) DOM.contentArea = document.getElementById('content-area'); const saveBtn = DOM.saveBtn; const cancelBtn = DOM.cancelBtn; const contentArea = DOM.contentArea; // 编辑按钮点击事件 - 切换编辑/查看模式 DOM.editBtn.addEventListener('click', () => { if (isEditMode) { exitEditMode(); } else { enterEditMode(); } }); // 取消编辑 cancelBtn.addEventListener('click', () => { exitEditMode(); }); // 保存按钮 saveBtn.addEventListener('click', async () => { await saveDocument(); }); } // 进入编辑模式 function enterEditMode() { if (!currentDoc || !currentMarkdownContent) { alert('请先加载一个文档'); return; } isEditMode = true; // 更新编辑按钮状态 DOM.editBtn.classList.add('active'); DOM.editBtn.title = '退出编辑'; // 保存当前滚动位置 if (!DOM.contentArea) DOM.contentArea = document.getElementById('content-area'); savedScrollPosition = DOM.contentArea.scrollTop; // 找到当前视口中心附近的元素 const visibleElement = findVisibleElement(); // 隐藏渲染的内容 if (!DOM.content) DOM.content = document.getElementById('markdown-content'); DOM.content.style.display = 'none'; // 显示编辑器 DOM.editorContainer.style.display = 'flex'; DOM.editorDocName.textContent = `${currentDoc}.md`; DOM.markdownEditor.value = currentMarkdownContent; // 定位光标到对应位置 setTimeout(() => { if (visibleElement) { positionCursorByElement(visibleElement); } else { // 如果找不到元素,使用滚动比例 const scrollRatio = savedScrollPosition / DOM.contentArea.scrollHeight; DOM.markdownEditor.scrollTop = DOM.markdownEditor.scrollHeight * scrollRatio; } DOM.markdownEditor.focus(); }, 50); } // 找到当前视口中可见的元素 function findVisibleElement() { if (!DOM.contentArea) DOM.contentArea = document.getElementById('content-area'); const viewportTop = DOM.contentArea.scrollTop; const viewportMiddle = viewportTop + DOM.contentArea.clientHeight / 3; // 视口上方1/3处 // 查找所有重要元素(标题、段落等) const elements = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, blockquote, pre'); let closestElement = null; let minDistance = Infinity; elements.forEach(el => { const rect = el.getBoundingClientRect(); const elementTop = el.offsetTop; const distance = Math.abs(elementTop - viewportMiddle); if (distance < minDistance && elementTop <= viewportMiddle + 200) { minDistance = distance; closestElement = el; } }); return closestElement; } // 根据元素定位光标 function positionCursorByElement(element) { const elementText = element.textContent.trim(); if (!elementText) { return; } // 获取元素标签类型 const tagName = element.tagName.toLowerCase(); let searchText = elementText; // 如果是标题,添加markdown标记进行搜索 if (tagName.match(/^h[1-6]$/)) { const level = parseInt(tagName[1]); const hashes = '#'.repeat(level); // 尝试匹配标题行 const lines = currentMarkdownContent.split('\n'); let targetLine = -1; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // 匹配 "# 标题" 格式 if (line.startsWith(hashes + ' ') && line.substring(level + 1).trim() === elementText) { targetLine = i; break; } } if (targetLine !== -1) { setEditorCursorToLine(targetLine); return; } } // 对于其他元素,搜索文本内容 const lines = currentMarkdownContent.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // 移除markdown标记进行比较 const cleanLine = line.replace(/^[#\-*>]+\s*/, '').trim(); if (cleanLine.includes(elementText.substring(0, Math.min(50, elementText.length)))) { setEditorCursorToLine(i); return; } } } // 设置编辑器光标到指定行 function setEditorCursorToLine(lineNumber) { const lines = currentMarkdownContent.split('\n'); // 计算目标位置的字符索引 let charPosition = 0; for (let i = 0; i < lineNumber && i < lines.length; i++) { charPosition += lines[i].length + 1; // +1 for newline } // 设置光标位置 DOM.markdownEditor.setSelectionRange(charPosition, charPosition); // 滚动到光标位置 const linesBeforeCursor = lineNumber; const lineHeight = 22; // 大约的行高(px) const targetScrollTop = linesBeforeCursor * lineHeight - DOM.markdownEditor.clientHeight / 3; DOM.markdownEditor.scrollTop = Math.max(0, targetScrollTop); } // 退出编辑模式 function exitEditMode() { isEditMode = false; // 更新编辑按钮状态 DOM.editBtn.classList.remove('active'); DOM.editBtn.title = '编辑文档'; // 隐藏编辑器 DOM.editorContainer.style.display = 'none'; // 显示渲染的内容 DOM.content.style.display = 'block'; // 恢复滚动位置 if (!DOM.contentArea) DOM.contentArea = document.getElementById('content-area'); setTimeout(() => { DOM.contentArea.scrollTop = savedScrollPosition; }, 0); } // 保存文档 async function saveDocument() { const newContent = DOM.markdownEditor.value; if (!currentCategory || !currentDoc) { alert('无法保存:缺少文档信息'); return; } // 禁用保存按钮,防止重复点击 if (!DOM.saveBtn) DOM.saveBtn = document.getElementById('save-btn'); DOM.saveBtn.disabled = true; DOM.saveBtn.textContent = '保存中...'; try { // 对于包含子目录的文档名,需要分段编码 const encodedDocName = currentDoc.split('/').map(part => encodeURIComponent(part)).join('/'); const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodedDocName}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: newContent }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || `保存失败 (${response.status})`); } const result = await response.json(); // 更新本地的markdown内容 currentMarkdownContent = newContent; // 重新渲染内容 const html = marked.parse(newContent); DOM.content.innerHTML = html; // 重新生成TOC generateTOC(); // 退出编辑模式 exitEditMode(); // 显示成功提示 showSuccessMessage('保存成功!'); } catch (error) { console.error('Save error:', error); alert(`保存失败:${error.message || '请稍后重试'}`); } finally { // 恢复保存按钮 DOM.saveBtn.disabled = false; DOM.saveBtn.textContent = '保存'; } } // 显示成功提示 function showSuccessMessage(message) { const toast = document.createElement('div'); toast.className = 'success-toast'; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #28a745; color: white; padding: 12px 24px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10000; animation: slideInRight 0.3s ease-out; `; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease-out'; setTimeout(() => { document.body.removeChild(toast); }, 300); }, 2000); } // 为代码块添加复制按钮 function addCopyButtonsToCodeBlocks() { const codeBlocks = document.querySelectorAll('pre'); codeBlocks.forEach(pre => { // 如果已经有复制按钮,跳过 if (pre.querySelector('.code-copy-btn')) { return; } // 创建包装容器 const wrapper = document.createElement('div'); wrapper.className = 'code-block-wrapper'; // 将pre元素包装起来 pre.parentNode.insertBefore(wrapper, pre); wrapper.appendChild(pre); // 创建复制按钮 const copyBtn = document.createElement('button'); copyBtn.className = 'code-copy-btn'; copyBtn.innerHTML = ` 复制 `; // 添加点击事件 copyBtn.addEventListener('click', () => { copyCodeToClipboard(pre, copyBtn); }); wrapper.appendChild(copyBtn); }); } // 复制代码到剪贴板 async function copyCodeToClipboard(preElement, button) { const codeElement = preElement.querySelector('code'); if (!codeElement) return; // 获取纯文本内容(去除HTML标签) const text = codeElement.textContent || codeElement.innerText; try { // 使用现代API复制 await navigator.clipboard.writeText(text); // 显示成功状态 showCopySuccess(button); } catch (err) { // 降级方案:使用传统方法 const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); showCopySuccess(button); } catch (err) { console.error('复制失败:', err); showCopyError(button); } finally { document.body.removeChild(textArea); } } } // 显示复制成功 function showCopySuccess(button) { // 更新按钮状态 const originalHTML = button.innerHTML; button.classList.add('copied'); button.innerHTML = ` 已复制 `; // 2秒后恢复原状 setTimeout(() => { button.classList.remove('copied'); button.innerHTML = originalHTML; }, 2000); } // 显示复制失败 function showCopyError(button) { const originalHTML = button.innerHTML; button.innerHTML = ` 复制失败 `; setTimeout(() => { button.innerHTML = originalHTML; }, 2000); } // 初始化 document.addEventListener('DOMContentLoaded', () => { loadDocList(); setupSidebarToggles(); initSearch(); setupBackToTop(); setupEditFeature(); }); // 处理浏览器后退/前进 window.addEventListener('popstate', () => { const { doc } = getQueryParams(); if (doc) { loadDocument(doc); } });