diff --git a/CODEBASE.md b/CODEBASE.md index 4fbdb72..f86db60 100644 --- a/CODEBASE.md +++ b/CODEBASE.md @@ -148,8 +148,8 @@ ### 4. 翻译覆盖率 翻译 PDF 存储在容器内 `/app/papers/translated/`,通过 volume 挂载持久化。翻译 API (`POST /api/translate/{paper_id}`) 会检查是否已翻译,避免重复。 -### 5. 搜索是简单子串匹配 -`GET /api/papers?q=...` 用 Python `in` 操作符在 title + authors 中做子串匹配。不支持模糊搜索、中文分词或 relevance ranking。对于 189 篇论文规模够用,但超过 500 篇可能需要改进。 +### 5. 搜索实现(2026-06-09 已修复) +搜索已在 2026-06-09 升级为**实时下拉结果面板**。输入 ≥2 字符后,200ms debounce 后调用 `/api/papers?q=`,在下拉面板中显示匹配论文(标题高亮、模块/领域、年份、标签)。卡片同步绿框高亮。点击结果项跳转到对应模块弹窗。点外部或 Escape 关闭。 ### 6. 数据一致性 `papers.json` 是唯一数据源。新增论文通过 API 写操作 → 自动更新 JSON → 前端下次请求时自动获取最新数据。**不要手动编辑 production 上的 papers.json 绕过 API**,可能导致并发写入问题。 diff --git a/static/app.js b/static/app.js index 9d9eb2f..3168c68 100644 --- a/static/app.js +++ b/static/app.js @@ -35,9 +35,15 @@ async function init() { document.addEventListener('keydown', e => { if (e.key === 'Escape') { if ($('#pdfOverlay')?.classList.contains('open')) closePdf(); + else if ($('#searchResults')?.style.display === 'block') closeSearchResults(); else if ($('#overlay').classList.contains('open')) closeModal(); } }); + + // Close search results on outside click + document.addEventListener('click', e => { + if (!e.target.closest('.search-wrap')) closeSearchResults(); + }); } // ═══════════════ STATUS BAR ════════════════════════════ @@ -260,16 +266,97 @@ function closePdf() { } // ═══════════════ SEARCH ═══════════════════════════════ +let searchTimer = null; + function searchPapers(q) { - q = (q||'').toLowerCase().trim(); - if (q.length<2) { $$('.card').forEach(c=>c.style.outline=''); return; } - fetch(`${API}/papers?q=${encodeURIComponent(q)}&limit=10`).then(r=>r.json()).then(ps=>{ - const matched = new Set(ps.map(p=>p.module_id)); - $$('.card').forEach(c=>{ - const m=[...c.classList].find(x=>x.startsWith('card-'))?.replace('card-',''); - c.style.outline=matched.has(m)?'2px solid var(--green)':''; + q = (q||'').trim(); + if (q.length < 2) { + closeSearchResults(); + $$('.card').forEach(c => c.style.outline = ''); + return; + } + + clearTimeout(searchTimer); + searchTimer = setTimeout(() => { + fetch(`${API}/papers?q=${encodeURIComponent(q)}&limit=15`) + .then(r => r.json()) + .then(ps => { + // Highlight matching module cards + const matched = new Set(ps.map(p => p.module_id)); + $$('.card').forEach(c => { + const m = [...c.classList].find(x => x.startsWith('card-'))?.replace('card-', ''); + c.style.outline = matched.has(m) ? '2px solid var(--green)' : ''; + }); + // Show paper-level results dropdown + renderSearchResults(ps, q); + }); + }, 200); // 200ms debounce +} + +function renderSearchResults(papers, query) { + let panel = $('#searchResults'); + if (!panel) { + panel = document.createElement('div'); + panel.id = 'searchResults'; + panel.className = 'search-results'; + $('.search-wrap').appendChild(panel); + panel.addEventListener('click', e => { + const item = e.target.closest('.search-result-item'); + if (item && item.dataset.mid) { + closeSearchResults(); + openModule(item.dataset.mid); + // After modal opens, switch to the right tab + setTimeout(() => { + const areaIdx = parseInt(item.dataset.aidx || '0'); + switchArea(areaIdx); + }, 300); + } }); + } + + if (papers.length === 0) { + panel.innerHTML = '