瀏覽代碼

'修复图片不加载的bug;修复代码块显示'

admincjy 1 月之前
父節點
當前提交
539ce86b20
共有 5 個文件被更改,包括 649 次插入8 次删除
  1. 91 0
      public/css/style.css
  2. 316 0
      public/js/highlight.min.js
  3. 199 3
      public/js/reader.js
  4. 1 1
      public/reader.html
  5. 42 4
      server.js

+ 91 - 0
public/css/style.css

@@ -1279,3 +1279,94 @@ body {
         font-size: 13px;
     }
 }
+
+/* ==================== 代码复制功能样式 ==================== */
+/* 代码块容器 */
+.code-block-wrapper {
+    position: relative;
+    margin: 1em 0;
+}
+
+/* 复制按钮 */
+.code-copy-btn {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    background: rgba(255, 255, 255, 0.9);
+    border: 1px solid var(--border-color);
+    border-radius: 6px;
+    padding: 4px 8px;
+    cursor: pointer;
+    font-size: 12px;
+    color: var(--text-color);
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    transition: all 0.2s ease;
+    opacity: 0;
+    z-index: 10;
+}
+
+/* 代码块悬停时显示复制按钮 */
+.code-block-wrapper:hover .code-copy-btn {
+    opacity: 1;
+}
+
+.code-copy-btn:hover {
+    background: var(--primary-color);
+    color: white;
+    border-color: var(--primary-color);
+    transform: scale(1.05);
+}
+
+.code-copy-btn:active {
+    transform: scale(0.95);
+}
+
+/* 复制按钮图标 */
+.code-copy-btn svg {
+    width: 14px;
+    height: 14px;
+}
+
+/* 复制成功状态 */
+.code-copy-btn.copied {
+    background: var(--primary-color);
+    color: white;
+    border-color: var(--primary-color);
+}
+
+/* 确保代码块有正确的样式 */
+pre {
+    position: relative;
+    background: #f6f8fa;
+    border-radius: 6px;
+    padding: 16px;
+    overflow-x: auto;
+    margin: 1em 0;
+}
+
+pre code {
+    background: transparent;
+    padding: 0;
+    border-radius: 0;
+    font-size: 14px;
+    line-height: 1.45;
+}
+
+/* ==================== 图片样式优化 ==================== */
+.markdown-body img {
+    max-width: 100%;
+    height: auto;
+    border-radius: 6px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    margin: 1em 0;
+    display: block;
+}
+
+.markdown-body img:hover {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    transform: scale(1.02);
+    transition: all 0.3s ease;
+}
+

文件差異過大導致無法顯示
+ 316 - 0
public/js/highlight.min.js


+ 199 - 3
public/js/reader.js

@@ -4,6 +4,7 @@ let docList = [];
 let currentMarkdownContent = ''; // 保存当前文档的markdown源码
 let isEditMode = false; // 是否处于编辑模式
 let savedScrollPosition = 0; // 保存的滚动位置
+let currentScrollObserver = null; // 保存当前的滚动监听器
 
 // DOM 元素缓存
 const DOM = {
@@ -26,12 +27,16 @@ marked.setOptions({
     highlight: function(code, lang) {
         if (lang && hljs.getLanguage(lang)) {
             try {
-                return hljs.highlight(code, { language: lang }).value;
+                const result = hljs.highlight(code, { language: lang });
+                return result.value;
             } catch (err) {
                 console.error('Highlight error:', err);
             }
         }
-        return hljs.highlightAuto(code).value;
+
+        // 自动检测语言
+        const autoResult = hljs.highlightAuto(code);
+        return autoResult.value;
     },
     breaks: true,
     gfm: true
@@ -158,6 +163,67 @@ async function loadDocument(docName, scrollToText = null) {
         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.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';
+                };
+            }
+        });
+
+        // 手动为所有代码块添加 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();
 
@@ -191,8 +257,14 @@ function generateTOC() {
     // 使用 DOM 缓存
     if (!DOM.toc) DOM.toc = document.getElementById('toc');
 
-    const headings = DOM.content.querySelectorAll('h1, h2, h3, h4, h5, h6');
+    // 确保 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 = '<p class="toc-empty">本文档没有标题</p>';
@@ -245,6 +317,17 @@ function handleTocClick(e, headingMap) {
 
 // 滚动监听
 function setupScrollSpy(headings) {
+    // 清理之前的 observer
+    if (currentScrollObserver) {
+        currentScrollObserver.disconnect();
+        currentScrollObserver = null;
+    }
+
+    // 如果没有标题,直接返回
+    if (!headings || headings.length === 0) {
+        return;
+    }
+
     let activeLink = null;
 
     const observer = new IntersectionObserver((entries) => {
@@ -267,6 +350,9 @@ function setupScrollSpy(headings) {
     });
 
     headings.forEach(heading => observer.observe(heading));
+
+    // 保存当前的 observer 以便后续清理
+    currentScrollObserver = observer;
 }
 
 // 更新 URL
@@ -1103,6 +1189,116 @@ function showSuccessMessage(message) {
     }, 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 = `
+            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
+                <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+            </svg>
+            <span>复制</span>
+        `;
+
+        // 添加点击事件
+        copyBtn.addEventListener('click', function() {
+            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 = `
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <polyline points="20 6 9 17 4 12"></polyline>
+        </svg>
+        <span>已复制</span>
+    `;
+
+    // 2秒后恢复原状
+    setTimeout(() => {
+        button.classList.remove('copied');
+        button.innerHTML = originalHTML;
+    }, 2000);
+}
+
+// 显示复制失败
+function showCopyError(button) {
+    const originalHTML = button.innerHTML;
+    button.innerHTML = `
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <circle cx="12" cy="12" r="10"></circle>
+            <line x1="12" y1="8" x2="12" y2="12"></line>
+            <line x1="12" y1="16" x2="12.01" y2="16"></line>
+        </svg>
+        <span>复制失败</span>
+    `;
+
+    setTimeout(() => {
+        button.innerHTML = originalHTML;
+    }, 2000);
+}
+
 // 初始化
 document.addEventListener('DOMContentLoaded', () => {
     loadDocList();

+ 1 - 1
public/reader.html

@@ -9,7 +9,7 @@
     <script src="/js/marked.min.js"></script>
     <!-- Highlight.js - 代码高亮 -->
     <link rel="stylesheet" href="/css/github.min.css">
-    <script src="https://cdn.jsdelivr.net/npm/highlight.js"></script>
+    <script src="/js/highlight.min.js"></script>
 </head>
 <body>
     <div class="reader-container">

+ 42 - 4
server.js

@@ -12,17 +12,19 @@ let indexCacheTime = 0;
 const CACHE_DURATION = 5000; // 5秒缓存
 
 // 路径安全验证函数 - 防止路径遍历攻击
+// 注意:此函数仅用于清理单个路径段(如分类名、文件名),不用于完整路径
 function sanitizePath(input) {
     if (!input || typeof input !== 'string') {
         return '';
     }
-    // 移除 ../ 和 ..\ 防止路径遍历
-    // 移除路径分隔符 / 和 \
-    // 保留中文等 Unicode 字符
+    // 安全清理策略:
+    // 1. 移除路径遍历序列
+    // 2. 移除开头的斜杠(防止绝对路径)
+    // 3. 对于单个路径段,不应包含路径分隔符
     return input
         .replace(/\.\./g, '')           // 移除 ..
         .replace(/^[\/\\]+/, '')        // 移除开头的 / 和 \
-        .replace(/[\/\\]/g, '');        // 移除所有 / 和 \
+        .replace(/[\/\\]/g, '');        // 移除路径分隔符(适用于单个段)
 }
 
 // 验证路径是否在允许的目录内
@@ -38,6 +40,42 @@ app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制到10MB
 app.use(express.urlencoded({ limit: '10mb', extended: true }));
 app.use(express.static('public'));
 
+// API: 获取图片资源
+app.get('/api/image/:category/*', async (req, res) => {
+  try {
+    // 对分类名进行处理(移除危险字符但保留中文)
+    const category = req.params.category.replace(/\.\./g, '');
+    const imagePath = req.params[0]; // 获取剩余路径部分,如 'assets/test.png'
+
+    if (!category || !imagePath) {
+      return res.status(400).json({ error: '无效的请求路径' });
+    }
+
+    const docsDir = path.join(__dirname, 'docs');
+    const fullPath = path.join(docsDir, category, imagePath);
+
+    // 验证路径安全性
+    if (!validatePath(fullPath, docsDir)) {
+      return res.status(403).json({ error: '拒绝访问' });
+    }
+
+    // 检查文件是否存在
+    try {
+      await fs.access(fullPath);
+    } catch (err) {
+      return res.status(404).json({ error: '图片不存在' });
+    }
+
+    // 发送图片文件
+    res.sendFile(fullPath);
+
+  } catch (error) {
+    console.error('Error serving image:', error);
+    res.status(500).json({ error: '无法加载图片' });
+  }
+});
+
+
 // 获取 index.md 结构(带缓存)
 async function getIndexStructure() {
   const now = Date.now();

部分文件因文件數量過多而無法顯示