| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121 |
- let currentCategory = '';
- let currentDoc = '';
- let docList = [];
- let currentMarkdownContent = ''; // 保存当前文档的markdown源码
- let isEditMode = false; // 是否处于编辑模式
- let savedScrollPosition = 0; // 保存的滚动位置
- // 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
- };
- // 配置 marked
- marked.setOptions({
- highlight: function(code, lang) {
- if (lang && hljs.getLanguage(lang)) {
- try {
- return hljs.highlight(code, { language: lang }).value;
- } catch (err) {
- console.error('Highlight error:', err);
- }
- }
- return hljs.highlightAuto(code).value;
- },
- breaks: true,
- gfm: true
- });
- // 从 URL 获取参数
- function getQueryParams() {
- const params = new URLSearchParams(window.location.search);
- return {
- category: params.get('category'),
- doc: params.get('doc')
- };
- }
- // 加载文档列表
- async function loadDocList() {
- const { category } = getQueryParams();
- if (!category) {
- window.location.href = '/';
- return;
- }
- currentCategory = category;
- const categoryNameSpan = document.querySelector('#category-title .category-name');
- if (categoryNameSpan) {
- 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;
- 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.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 response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodeURIComponent(docName)}`);
- 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;
- // 生成 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');
- const headings = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6');
- DOM.toc.innerHTML = '';
- if (headings.length === 0) {
- DOM.toc.innerHTML = '<p class="toc-empty">本文档没有标题</p>';
- 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) {
- 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));
- }
- // 更新 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;
- const docNavigation = document.getElementById('doc-navigation');
- if (!docNavigation) return;
- // 找到当前文档在列表中的索引
- const currentIndex = docList.findIndex(doc => doc.name === currentDoc);
- if (currentIndex === -1) {
- DOM.navPrev.style.display = 'none';
- DOM.navNext.style.display = 'none';
- docNavigation.className = 'doc-navigation';
- return;
- }
- const hasPrev = currentIndex > 0;
- const hasNext = currentIndex < docList.length - 1;
- // 处理上一页
- if (hasPrev) {
- const prevDoc = docList[currentIndex - 1];
- DOM.navPrev.style.display = 'flex';
- DOM.navPrev.querySelector('.nav-doc-name').textContent = prevDoc.name;
- DOM.navPrev.onclick = () => {
- loadDocument(prevDoc.name);
- updateURL(currentCategory, prevDoc.name);
- // 滚动到顶部
- const contentArea = document.getElementById('content-area');
- if (contentArea) {
- contentArea.scrollTo({ top: 0, behavior: 'smooth' });
- }
- };
- } else {
- DOM.navPrev.style.display = 'none';
- }
- // 处理下一页
- if (hasNext) {
- const nextDoc = docList[currentIndex + 1];
- DOM.navNext.style.display = 'flex';
- DOM.navNext.querySelector('.nav-doc-name').textContent = nextDoc.name;
- DOM.navNext.onclick = () => {
- loadDocument(nextDoc.name);
- updateURL(currentCategory, nextDoc.name);
- // 滚动到顶部
- const contentArea = document.getElementById('content-area');
- if (contentArea) {
- contentArea.scrollTo({ top: 0, behavior: 'smooth' });
- }
- };
- } else {
- DOM.navNext.style.display = 'none';
- }
- // 根据导航按钮状态添加类,用于CSS定位
- if (hasPrev && hasNext) {
- docNavigation.className = 'doc-navigation has-both';
- } else if (hasPrev) {
- docNavigation.className = 'doc-navigation has-prev-only';
- } else if (hasNext) {
- docNavigation.className = 'doc-navigation has-next-only';
- } else {
- docNavigation.className = 'doc-navigation';
- }
- }
- // 显示错误
- function showError(message) {
- const content = document.getElementById('markdown-content');
- content.innerHTML = `<div class="error-message">${message}</div>`;
- }
- // 切换侧边栏(移动端)
- function setupSidebarToggles() {
- const toggleLeft = document.getElementById('toggle-left');
- const toggleRight = document.getElementById('toggle-right');
- // 使用 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() {
- const searchContainer = document.getElementById('search-container');
- const searchToggleBtn = document.getElementById('search-toggle-btn');
- const searchInput = document.getElementById('search-input');
- const searchBtn = document.getElementById('search-btn');
- const searchResults = document.getElementById('search-results');
- const closeSearchBoxBtn = document.getElementById('close-search-box');
- // 检查元素是否存在
- if (!searchContainer || !searchToggleBtn || !searchInput || !searchBtn || !searchResults || !closeSearchBoxBtn) {
- console.error('Search elements not found:', {
- searchContainer: !!searchContainer,
- searchToggleBtn: !!searchToggleBtn,
- searchInput: !!searchInput,
- searchBtn: !!searchBtn,
- searchResults: !!searchResults,
- closeSearchBoxBtn: !!closeSearchBoxBtn
- });
- return;
- }
- 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();
- console.log('Search button clicked');
- const isActive = searchContainer.classList.toggle('active');
- console.log('Search container active:', isActive);
- 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) => {
- const searchToggle = document.getElementById('search-toggle-btn');
- if (!searchResults.contains(e.target) &&
- !searchInput.contains(e.target) &&
- !searchBtn.contains(e.target) &&
- !searchContainer.contains(e.target) &&
- !searchToggle.contains(e.target)) {
- searchResults.style.display = 'none';
- }
- });
- // 初始化时清除其他分类的历史
- clearOtherCategoryHistory();
- }
- // 显示搜索错误
- function displaySearchError(message) {
- const currentDocSection = document.querySelector('#current-doc-results .results-list');
- const otherDocsSection = document.querySelector('#other-docs-results .results-list');
- const searchResults = document.getElementById('search-results');
- // 清空之前的结果
- currentDocSection.innerHTML = '';
- otherDocsSection.innerHTML = '';
- // 隐藏分组标题
- document.getElementById('current-doc-results').style.display = 'none';
- document.getElementById('other-docs-results').style.display = 'none';
- // 显示错误信息
- currentDocSection.innerHTML = `<p class="search-error" style="color: #d73a49; padding: 20px; text-align: center;">${message}</p>`;
- document.getElementById('current-doc-results').style.display = 'block';
- // 显示搜索结果面板
- searchResults.style.display = 'block';
- }
- // 显示搜索结果
- function displaySearchResults(data) {
- const currentDocSection = document.querySelector('#current-doc-results .results-list');
- const otherDocsSection = document.querySelector('#other-docs-results .results-list');
- // 清空之前的结果
- currentDocSection.innerHTML = '';
- otherDocsSection.innerHTML = '';
- // 隐藏/显示分组标题
- document.getElementById('current-doc-results').style.display =
- data.currentDoc.length > 0 ? 'block' : 'none';
- document.getElementById('other-docs-results').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);
- });
- } else {
- currentDocSection.innerHTML = '<p class="no-results">当前文档无匹配结果</p>';
- }
- // 渲染其他文档结果
- if (data.otherDocs.length > 0) {
- data.otherDocs.forEach(doc => {
- const docResults = createDocResultElement(doc, false);
- otherDocsSection.appendChild(docResults);
- });
- } else if (data.currentDoc.length === 0) {
- otherDocsSection.innerHTML = '<p class="no-results">未找到匹配结果</p>';
- }
- }
- // 创建搜索结果元素
- 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 = `
- <span class="result-doc-name">${doc.docName}</span>
- <span class="result-count">${doc.matchCount} 个匹配</span>
- `;
- container.appendChild(header);
- // 添加匹配片段
- doc.matches.forEach(match => {
- const matchItem = document.createElement('div');
- matchItem.className = 'result-match';
- // 高亮搜索词
- const highlightedSnippet = highlightSearchTerm(match.snippet, document.getElementById('search-input').value);
- matchItem.innerHTML = `
- <div class="match-line-number">行 ${match.line}</div>
- <div class="match-snippet">${highlightedSnippet}</div>
- `;
- // 点击跳转
- matchItem.onclick = () => {
- // 隐藏搜索框和搜索结果
- const searchContainer = document.querySelector('.search-container');
- const searchResults = document.getElementById('search-results');
- searchContainer.classList.remove('active');
- 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, '<mark class="search-highlight">$1</mark>');
- }
- // 滚动到搜索匹配位置
- 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() {
- const backToTopBtn = document.getElementById('back-to-top');
- 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');
- const saveBtn = document.getElementById('save-btn');
- const cancelBtn = document.getElementById('cancel-edit-btn');
- const contentArea = document.getElementById('content-area');
- // 编辑按钮点击事件 - 切换编辑/查看模式
- 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 = '退出编辑';
- // 保存当前滚动位置
- const contentArea = document.getElementById('content-area');
- savedScrollPosition = 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 / contentArea.scrollHeight;
- DOM.markdownEditor.scrollTop = DOM.markdownEditor.scrollHeight * scrollRatio;
- }
- DOM.markdownEditor.focus();
- }, 50);
- }
- // 找到当前视口中可见的元素
- function findVisibleElement() {
- const contentArea = document.getElementById('content-area');
- const viewportTop = contentArea.scrollTop;
- const viewportMiddle = viewportTop + 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';
- // 恢复滚动位置
- const contentArea = document.getElementById('content-area');
- setTimeout(() => {
- contentArea.scrollTop = savedScrollPosition;
- }, 0);
- }
- // 保存文档
- async function saveDocument() {
- const newContent = DOM.markdownEditor.value;
- if (!currentCategory || !currentDoc) {
- alert('无法保存:缺少文档信息');
- return;
- }
- // 禁用保存按钮,防止重复点击
- const saveBtn = document.getElementById('save-btn');
- saveBtn.disabled = true;
- saveBtn.textContent = '保存中...';
- try {
- const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodeURIComponent(currentDoc)}`, {
- 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 {
- // 恢复保存按钮
- saveBtn.disabled = false;
- 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);
- }
- // 初始化
- document.addEventListener('DOMContentLoaded', () => {
- loadDocList();
- setupSidebarToggles();
- initSearch();
- setupBackToTop();
- setupEditFeature();
- });
- // 处理浏览器后退/前进
- window.addEventListener('popstate', () => {
- const { doc } = getQueryParams();
- if (doc) {
- loadDocument(doc);
- }
- });
|