2 커밋 f73cda778b ... a4b77d29a3

작성자 SHA1 메시지 날짜
  caijynb a4b77d29a3 '搜索结果为空样式修改' 1 개월 전
  caijynb f9b08be10a '修复子目录渲染问题' 1 개월 전
3개의 변경된 파일169개의 추가작업 그리고 23개의 파일을 삭제
  1. 65 0
      public/css/style.css
  2. 67 12
      public/js/reader.js
  3. 37 11
      server.js

+ 65 - 0
public/css/style.css

@@ -419,6 +419,53 @@ body {
     font-style: italic;
 }
 
+/* 搜索结果空状态样式 */
+.empty-search-state {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+    text-align: center;
+    animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(-10px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.empty-icon {
+    margin-bottom: 24px;
+    color: #8b949e;
+    opacity: 0.6;
+}
+
+.empty-icon svg {
+    width: 64px;
+    height: 64px;
+}
+
+.empty-title {
+    font-size: 1.2rem;
+    font-weight: 600;
+    color: var(--text-color);
+    margin: 0 0 12px 0;
+}
+
+.empty-text {
+    font-size: 0.95rem;
+    color: #656d76;
+    margin: 0;
+    line-height: 1.5;
+}
+
 /* 临时高亮效果 */
 .temp-highlight {
     animation: highlightFade 2s ease-out;
@@ -951,6 +998,24 @@ body {
         font-size: 0.85rem;
     }
 
+    /* 移动端空状态样式调整 */
+    .empty-search-state {
+        padding: 40px 20px;
+    }
+
+    .empty-icon svg {
+        width: 48px;
+        height: 48px;
+    }
+
+    .empty-title {
+        font-size: 1.1rem;
+    }
+
+    .empty-text {
+        font-size: 0.9rem;
+    }
+
     .toggle-sidebar-btn {
         display: block;
         width: 56px;

+ 67 - 12
public/js/reader.js

@@ -173,7 +173,9 @@ async function loadDocument(docName, scrollToText = null) {
     DOM.content.innerHTML = '';
 
     try {
-        const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodeURIComponent(docName)}`);
+        // 对于包含子目录的文档名,需要分段编码
+        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(() => ({}));
@@ -739,19 +741,74 @@ function displaySearchError(message) {
 
 // 显示搜索结果
 function displaySearchResults(data) {
-    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;
+    // 获取搜索结果内容容器
+    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 = `
+            <div class="empty-icon">
+                <svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                    <circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                    <path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                    <path d="M8 11h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+                </svg>
+            </div>
+            <h3 class="empty-title">没有找到相关内容</h3>
+            <p class="empty-text">试试其他关键词,或者检查拼写是否正确</p>
+        `;
+
+        searchResultsContent.appendChild(emptyState);
+        return;
+    }
+
+    // 有搜索结果时,需要恢复正常的DOM结构
+    // 先检查是否需要重建结构(从空状态恢复)
+    let currentDocResults = document.getElementById('current-doc-results');
+    let otherDocResults = document.getElementById('other-docs-results');
+
+    // 如果结构不存在(被空状态替换了),重新创建
+    if (!currentDocResults || !otherDocResults) {
+        searchResultsContent.innerHTML = `
+            <div id="current-doc-results" class="results-section">
+                <h3 class="results-section-title">当前文档</h3>
+                <div class="results-list"></div>
+            </div>
+            <div id="other-docs-results" class="results-section">
+                <h3 class="results-section-title">其他文档</h3>
+                <div class="results-list"></div>
+            </div>
+        `;
+
+        // 重新获取引用
+        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 = '';
 
     // 隐藏/显示分组标题
-    DOM.currentDocResults.style.display =
+    currentDocResults.style.display =
         data.currentDoc.length > 0 ? 'block' : 'none';
-    DOM.otherDocResults.style.display =
+    otherDocResults.style.display =
         data.otherDocs.length > 0 ? 'block' : 'none';
 
     // 渲染当前文档结果
@@ -760,8 +817,6 @@ function displaySearchResults(data) {
             const docResults = createDocResultElement(doc, true);
             currentDocSection.appendChild(docResults);
         });
-    } else {
-        currentDocSection.innerHTML = '<p class="no-results">当前文档无匹配结果</p>';
     }
 
     // 渲染其他文档结果
@@ -770,8 +825,6 @@ function displaySearchResults(data) {
             const docResults = createDocResultElement(doc, false);
             otherDocsSection.appendChild(docResults);
         });
-    } else if (data.currentDoc.length === 0) {
-        otherDocsSection.innerHTML = '<p class="no-results">未找到匹配结果</p>';
     }
 }
 
@@ -1151,7 +1204,9 @@ async function saveDocument() {
     DOM.saveBtn.textContent = '保存中...';
 
     try {
-        const response = await fetch(`/api/doc/${encodeURIComponent(currentCategory)}/${encodeURIComponent(currentDoc)}`, {
+        // 对于包含子目录的文档名,需要分段编码
+        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'

+ 37 - 11
server.js

@@ -120,16 +120,18 @@ app.get('/api/category/:category', async (req, res) => {
 });
 
 // API: 获取指定文档的内容
-app.get('/api/doc/:category/:docName', async (req, res) => {
+// 修改为支持通配符路径参数,以正确处理子目录
+app.get('/api/doc/:category/*', async (req, res) => {
   try {
     const category = sanitizePath(req.params.category);
-    const docName = sanitizePath(req.params.docName);
+    const docName = req.params[0]; // 获取剩余的路径部分,支持子目录
 
     if (!category || !docName) {
       return res.status(400).json({ error: '无效的分类或文档名称' });
     }
 
     const docsDir = path.join(__dirname, 'docs');
+    // 直接使用 docName,它可能包含子目录路径
     const docPath = path.join(docsDir, category, `${docName}.md`);
 
     // 验证路径是否在 docs 目录内
@@ -145,10 +147,11 @@ app.get('/api/doc/:category/:docName', async (req, res) => {
 });
 
 // API: 保存文档内容
-app.put('/api/doc/:category/:docName', async (req, res) => {
+// 修改为支持通配符路径参数,以正确处理子目录
+app.put('/api/doc/:category/*', async (req, res) => {
   try {
     const category = sanitizePath(req.params.category);
-    const docName = sanitizePath(req.params.docName);
+    const docName = req.params[0]; // 获取剩余的路径部分,支持子目录
     const { content } = req.body;
 
     // 验证输入
@@ -161,6 +164,7 @@ app.put('/api/doc/:category/:docName', async (req, res) => {
     }
 
     const docsDir = path.join(__dirname, 'docs');
+    // 直接使用 docName,它可能包含子目录路径
     const docPath = path.join(docsDir, category, `${docName}.md`);
 
     // 验证路径是否在 docs 目录内
@@ -194,7 +198,8 @@ app.put('/api/doc/:category/:docName', async (req, res) => {
 app.get('/api/search/:category', async (req, res) => {
   try {
     const category = sanitizePath(req.params.category);
-    const currentDoc = sanitizePath(req.query.currentDoc);
+    // currentDoc 可能包含子目录路径,不要使用 sanitizePath
+    const currentDoc = req.query.currentDoc || '';
     const { q } = req.query;
 
     if (!category) {
@@ -214,17 +219,36 @@ app.get('/api/search/:category', async (req, res) => {
       return res.status(403).json({ error: '拒绝访问:无效的路径' });
     }
 
-    // 读取分类下的所有文档
-    const files = await fs.readdir(categoryPath);
-    const mdFiles = files.filter(file => file.endsWith('.md'));
+    // 从 index.md 获取当前分类下应该搜索的文档列表
+    const structure = await getIndexStructure();
+    const categoryData = structure.find(cat => cat.name === category);
+
+    if (!categoryData) {
+      return res.status(404).json({ error: '分类不存在' });
+    }
+
+    // 只搜索 index.md 中列出的文档
+    const mdFiles = categoryData.docs.map(doc => {
+      // doc.name 可能包含子目录路径,如 "测试/11"
+      return doc.name + '.md';
+    });
 
     const currentDocResults = [];
     const otherDocsResults = [];
 
     // 搜索每个文档
     for (const file of mdFiles) {
-      const docName = file.replace('.md', '');
-      const filePath = path.join(categoryPath, file);
+      const docName = file.replace('.md', '').replace(/\\/g, '/'); // 统一使用正斜杠
+      const filePath = path.join(categoryPath, file.replace(/\//g, path.sep)); // 处理路径分隔符
+
+      // 检查文件是否存在(以防 index.md 中列出的文件实际不存在)
+      try {
+        await fs.access(filePath);
+      } catch (err) {
+        console.warn(`文档不存在,跳过搜索: ${filePath}`);
+        continue;
+      }
+
       const content = await fs.readFile(filePath, 'utf-8');
 
       // 搜索匹配的行
@@ -259,7 +283,9 @@ app.get('/api/search/:category', async (req, res) => {
         };
 
         // 区分当前文档和其他文档
-        if (docName === currentDoc) {
+        // 处理 URL 解码后的 currentDoc(支持斜杠路径)
+        const decodedCurrentDoc = decodeURIComponent(currentDoc).replace(/\\/g, '/');
+        if (docName === decodedCurrentDoc) {
           currentDocResults.push(result);
         } else {
           otherDocsResults.push(result);