admincjy před 1 měsícem
rodič
revize
d418cbe2b9
2 změnil soubory, kde provedl 199 přidání a 181 odebrání
  1. 71 70
      public/css/style.css
  2. 128 111
      public/js/reader.js

+ 71 - 70
public/css/style.css

@@ -804,6 +804,11 @@ body {
     text-align: right;
 }
 
+/* 同时有上一页和下一页时,两侧分布 */
+.doc-navigation.has-both {
+    justify-content: space-between;
+}
+
 /* 只有下一页时(首页),下一页按钮固定在右侧 */
 .doc-navigation.has-next-only {
     justify-content: flex-end;
@@ -947,22 +952,65 @@ body {
     }
 
     .toggle-sidebar-btn {
-        display: block !important;
-        width: 56px !important;
-        height: 56px !important;
-        font-size: 1.7rem !important;
-        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4) !important;
-        z-index: 1001 !important;
+        display: block;
+        width: 56px;
+        height: 56px;
+        font-size: 1.7rem;
+        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+        z-index: 1001;
     }
 
     #toggle-left {
-        bottom: 20px !important;
-        left: 20px !important;
+        bottom: 20px;
+        left: 20px;
     }
 
     #toggle-right {
-        bottom: 20px !important;
-        right: 20px !important;
+        bottom: 20px;
+        right: 20px;
+    }
+
+    /* 侧边栏展开/收起状态 */
+    .left-sidebar.collapsed {
+        transform: translateX(-100%);
+    }
+
+    .left-sidebar:not(.collapsed) {
+        transform: translateX(0);
+    }
+
+    .right-sidebar.collapsed {
+        transform: translateX(100%);
+    }
+
+    .right-sidebar:not(.collapsed) {
+        transform: translateX(0);
+    }
+
+    /* 悬浮按钮调整 */
+    .floating-action-btn {
+        width: 45px;
+        height: 45px;
+        z-index: 1002;
+        opacity: 0.45;
+    }
+
+    .floating-action-btn:hover {
+        opacity: 1;
+    }
+
+    .back-to-top {
+        bottom: 100px;
+        right: 20px;
+    }
+
+    .edit-btn {
+        bottom: 100px;
+        right: 75px;
+    }
+
+    .editor-header {
+        padding: 15px 20px;
     }
 
     .markdown-body {
@@ -1104,7 +1152,7 @@ body {
 
 /* 编辑按钮激活状态 */
 .edit-btn.active {
-    opacity: 1 !important;
+    opacity: 1;
     background: #28a745;
     box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
 }
@@ -1221,65 +1269,6 @@ body {
     }
 }
 
-/* 移动端侧边栏展开/收起 */
-@media (max-width: 768px) {
-    .left-sidebar.collapsed {
-        transform: translateX(-100%) !important;
-    }
-
-    .left-sidebar:not(.collapsed) {
-        transform: translateX(0) !important;
-    }
-
-    .right-sidebar.collapsed {
-        transform: translateX(100%) !important;
-    }
-
-    .right-sidebar:not(.collapsed) {
-        transform: translateX(0) !important;
-    }
-
-    /* 移动端悬浮按钮调整 */
-    .floating-action-btn {
-        width: 45px !important;
-        height: 45px !important;
-        z-index: 1002 !important;
-        opacity: 0.45 !important;
-    }
-
-    .floating-action-btn:hover {
-        opacity: 1 !important;
-    }
-
-    .back-to-top {
-        bottom: 100px !important;
-        right: 20px !important;
-    }
-
-    .edit-btn {
-        bottom: 100px !important;
-        right: 75px !important;
-    }
-
-    .editor-header {
-        padding: 15px 20px;
-    }
-
-    .editor-header h3 {
-        font-size: 1rem;
-    }
-
-    .editor-action-btn {
-        padding: 8px 16px;
-        font-size: 0.9rem;
-    }
-
-    .markdown-editor {
-        padding: 20px;
-        font-size: 13px;
-    }
-}
-
 /* ==================== 代码复制功能样式 ==================== */
 /* 代码块容器 */
 .code-block-wrapper {
@@ -1370,3 +1359,15 @@ pre code {
     transition: all 0.3s ease;
 }
 
+/* 图片加载失败样式 */
+.img-error {
+    border: 1px dashed #ccc;
+    padding: 10px;
+    min-height: 100px;
+    display: block;
+    background-color: #f5f5f5;
+    color: #666;
+    text-align: center;
+    line-height: 100px;
+}
+

+ 128 - 111
public/js/reader.js

@@ -19,7 +19,23 @@ const DOM = {
     markdownEditor: null,
     editorDocName: null,
     navPrev: null,
-    navNext: 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
@@ -60,9 +76,9 @@ async function loadDocList() {
     }
 
     currentCategory = category;
-    const categoryNameSpan = document.querySelector('#category-title .category-name');
-    if (categoryNameSpan) {
-        categoryNameSpan.textContent = category;
+    if (!DOM.categoryNameSpan) DOM.categoryNameSpan = document.querySelector('#category-title .category-name');
+    if (DOM.categoryNameSpan) {
+        DOM.categoryNameSpan.textContent = category;
     }
 
     try {
@@ -175,15 +191,8 @@ async function loadDocument(docName, scrollToText = null) {
                 img.onerror = function() {
                     console.error('Failed to load image:', newSrc);
                     // 显示占位图片或错误提示
-                    this.alt = '图片加载失败: ' + src;
-                    this.style.border = '1px dashed #ccc';
-                    this.style.padding = '10px';
-                    this.style.minHeight = '100px';
-                    this.style.display = 'block';
-                    this.style.backgroundColor = '#f5f5f5';
-                    this.style.color = '#666';
-                    this.style.textAlign = 'center';
-                    this.style.lineHeight = '100px';
+                    this.alt = `图片加载失败: ${src}`;
+                    this.classList.add('img-error');
                 };
             }
         });
@@ -371,8 +380,8 @@ function updateDocNavigation() {
 
     if (!DOM.navPrev || !DOM.navNext || docList.length === 0) return;
 
-    const docNavigation = document.getElementById('doc-navigation');
-    if (!docNavigation) return;
+    if (!DOM.docNavigation) DOM.docNavigation = document.getElementById('doc-navigation');
+    if (!DOM.docNavigation) return;
 
     // 找到当前文档在列表中的索引
     const currentIndex = docList.findIndex(doc => doc.name === currentDoc);
@@ -380,71 +389,62 @@ function updateDocNavigation() {
     if (currentIndex === -1) {
         DOM.navPrev.style.display = 'none';
         DOM.navNext.style.display = 'none';
-        docNavigation.className = 'doc-navigation';
+        DOM.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';
-    }
+    // 辅助函数:设置导航按钮
+    const setupNavButton = (button, doc) => {
+        if (doc) {
+            button.style.display = 'flex';
+            button.querySelector('.nav-doc-name').textContent = doc.name;
+            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';
+        }
+    };
 
-    // 处理下一页
-    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';
-    }
+    // 设置上一页和下一页按钮
+    setupNavButton(DOM.navPrev, hasPrev ? docList[currentIndex - 1] : null);
+    setupNavButton(DOM.navNext, hasNext ? docList[currentIndex + 1] : null);
 
     // 根据导航按钮状态添加类,用于CSS定位
     if (hasPrev && hasNext) {
-        docNavigation.className = 'doc-navigation has-both';
+        DOM.docNavigation.className = 'doc-navigation has-both';
     } else if (hasPrev) {
-        docNavigation.className = 'doc-navigation has-prev-only';
+        DOM.docNavigation.className = 'doc-navigation has-prev-only';
     } else if (hasNext) {
-        docNavigation.className = 'doc-navigation has-next-only';
+        DOM.docNavigation.className = 'doc-navigation has-next-only';
     } else {
-        docNavigation.className = 'doc-navigation';
+        DOM.docNavigation.className = 'doc-navigation';
     }
 }
 
 // 显示错误
 function showError(message) {
-    const content = document.getElementById('markdown-content');
-    content.innerHTML = `<div class="error-message">${message}</div>`;
+    if (!DOM.content) DOM.content = document.getElementById('markdown-content');
+    if (DOM.content) {
+        DOM.content.innerHTML = `<div class="error-message">${message}</div>`;
+    }
 }
 
 // 切换侧边栏(移动端)
 function setupSidebarToggles() {
-    const toggleLeft = document.getElementById('toggle-left');
-    const toggleRight = document.getElementById('toggle-right');
+    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');
@@ -527,26 +527,34 @@ function setupSidebarToggles() {
 
 // 搜索功能
 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');
+    // 初始化搜索相关的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 (!searchContainer || !searchToggleBtn || !searchInput || !searchBtn || !searchResults || !closeSearchBoxBtn) {
+    if (!DOM.searchContainer || !DOM.searchToggleBtn || !DOM.searchInput || !DOM.searchBtn || !DOM.searchResults || !DOM.closeSearchBoxBtn) {
         console.error('Search elements not found:', {
-            searchContainer: !!searchContainer,
-            searchToggleBtn: !!searchToggleBtn,
-            searchInput: !!searchInput,
-            searchBtn: !!searchBtn,
-            searchResults: !!searchResults,
-            closeSearchBoxBtn: !!closeSearchBoxBtn
+            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';
 
@@ -627,9 +635,7 @@ function initSearch() {
     // 切换搜索框显示/隐藏
     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) {
             // 加载搜索历史
@@ -675,13 +681,11 @@ function initSearch() {
 
     // 点击外部关闭
     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)) {
+            !searchToggleBtn.contains(e.target)) {
             searchResults.style.display = 'none';
         }
     });
@@ -692,39 +696,46 @@ function initSearch() {
 
 // 显示搜索错误
 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');
+    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 = '';
 
     // 隐藏分组标题
-    document.getElementById('current-doc-results').style.display = 'none';
-    document.getElementById('other-docs-results').style.display = 'none';
+    DOM.currentDocResults.style.display = 'none';
+    DOM.otherDocResults.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';
+    if (currentDocSection) {
+        currentDocSection.innerHTML = `<p class="search-error" style="color: #d73a49; padding: 20px; text-align: center;">${message}</p>`;
+    }
+    DOM.currentDocResults.style.display = 'block';
 
     // 显示搜索结果面板
-    searchResults.style.display = 'block';
+    if (DOM.searchResults) {
+        DOM.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');
+    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 = '';
 
     // 隐藏/显示分组标题
-    document.getElementById('current-doc-results').style.display =
+    DOM.currentDocResults.style.display =
         data.currentDoc.length > 0 ? 'block' : 'none';
-    document.getElementById('other-docs-results').style.display =
+    DOM.otherDocResults.style.display =
         data.otherDocs.length > 0 ? 'block' : 'none';
 
     // 渲染当前文档结果
@@ -767,7 +778,7 @@ function createDocResultElement(doc, isCurrent) {
         matchItem.className = 'result-match';
 
         // 高亮搜索词
-        const highlightedSnippet = highlightSearchTerm(match.snippet, document.getElementById('search-input').value);
+        const highlightedSnippet = highlightSearchTerm(match.snippet, DOM.searchInput ? DOM.searchInput.value : '');
 
         matchItem.innerHTML = `
             <div class="match-line-number">行 ${match.line}</div>
@@ -777,10 +788,12 @@ function createDocResultElement(doc, isCurrent) {
         // 点击跳转
         matchItem.onclick = () => {
             // 隐藏搜索框和搜索结果
-            const searchContainer = document.querySelector('.search-container');
-            const searchResults = document.getElementById('search-results');
-            searchContainer.classList.remove('active');
-            searchResults.style.display = 'none';
+            if (DOM.searchContainer) {
+                DOM.searchContainer.classList.remove('active');
+            }
+            if (DOM.searchResults) {
+                DOM.searchResults.style.display = 'none';
+            }
 
             // 在移动端和平板上关闭所有侧边栏,确保用户能看到跳转后的内容
             if (window.innerWidth <= 1024) {
@@ -872,7 +885,8 @@ function scrollToSearchMatch(fullLine) {
 
 // 设置回到顶部按钮
 function setupBackToTop() {
-    const backToTopBtn = document.getElementById('back-to-top');
+    if (!DOM.backToTopBtn) DOM.backToTopBtn = document.getElementById('back-to-top');
+    const backToTopBtn = DOM.backToTopBtn;
 
     if (!backToTopBtn) return;
 
@@ -919,9 +933,12 @@ function setupEditFeature() {
     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');
+    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', () => {
@@ -957,8 +974,8 @@ function enterEditMode() {
     DOM.editBtn.title = '退出编辑';
 
     // 保存当前滚动位置
-    const contentArea = document.getElementById('content-area');
-    savedScrollPosition = contentArea.scrollTop;
+    if (!DOM.contentArea) DOM.contentArea = document.getElementById('content-area');
+    savedScrollPosition = DOM.contentArea.scrollTop;
 
     // 找到当前视口中心附近的元素
     const visibleElement = findVisibleElement();
@@ -978,7 +995,7 @@ function enterEditMode() {
             positionCursorByElement(visibleElement);
         } else {
             // 如果找不到元素,使用滚动比例
-            const scrollRatio = savedScrollPosition / contentArea.scrollHeight;
+            const scrollRatio = savedScrollPosition / DOM.contentArea.scrollHeight;
             DOM.markdownEditor.scrollTop = DOM.markdownEditor.scrollHeight * scrollRatio;
         }
         DOM.markdownEditor.focus();
@@ -987,9 +1004,9 @@ function enterEditMode() {
 
 // 找到当前视口中可见的元素
 function findVisibleElement() {
-    const contentArea = document.getElementById('content-area');
-    const viewportTop = contentArea.scrollTop;
-    const viewportMiddle = viewportTop + contentArea.clientHeight / 3; // 视口上方1/3处
+    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');
@@ -1097,9 +1114,9 @@ function exitEditMode() {
     DOM.content.style.display = 'block';
 
     // 恢复滚动位置
-    const contentArea = document.getElementById('content-area');
+    if (!DOM.contentArea) DOM.contentArea = document.getElementById('content-area');
     setTimeout(() => {
-        contentArea.scrollTop = savedScrollPosition;
+        DOM.contentArea.scrollTop = savedScrollPosition;
     }, 0);
 }
 
@@ -1113,9 +1130,9 @@ async function saveDocument() {
     }
 
     // 禁用保存按钮,防止重复点击
-    const saveBtn = document.getElementById('save-btn');
-    saveBtn.disabled = true;
-    saveBtn.textContent = '保存中...';
+    if (!DOM.saveBtn) DOM.saveBtn = document.getElementById('save-btn');
+    DOM.saveBtn.disabled = true;
+    DOM.saveBtn.textContent = '保存中...';
 
     try {
         const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodeURIComponent(currentDoc)}`, {
@@ -1156,8 +1173,8 @@ async function saveDocument() {
         alert(`保存失败:${error.message || '请稍后重试'}`);
     } finally {
         // 恢复保存按钮
-        saveBtn.disabled = false;
-        saveBtn.textContent = '保存';
+        DOM.saveBtn.disabled = false;
+        DOM.saveBtn.textContent = '保存';
     }
 }
 
@@ -1219,7 +1236,7 @@ function addCopyButtonsToCodeBlocks() {
         `;
 
         // 添加点击事件
-        copyBtn.addEventListener('click', function() {
+        copyBtn.addEventListener('click', () => {
             copyCodeToClipboard(pre, copyBtn);
         });