feat: LLM/Embedding 客户端模块化、侧边栏折叠交互、arXiv→ADS 下载回退与前端体验重构

**后端架构**
  - 抽取翻译服务中内嵌的 LLM HTTP 调用为独立的 LlmClient /
    EmbeddingClient(src/clients/llm.rs),翻译模块改为委托调用,消除
    对 reqwest/serde 的直接耦合
  - Config 新增 EMBEDDING_API_KEY/EMBEDDING_API_BASE/EMBEDDING_MODEL
    三项配置,默认 fallback 至 LLM 对应值,补齐向量嵌入基础设施

  **下载策略优化**
  - arXiv 直连下载失败后自动回退至 ADS 网关 PUB_PDF→EPRINT_PDF→CrossRef
    多级通道,替换此前单路径策略;批量同步同步应用此逻辑
  - PDF/HTML 任一方成功时,失败方的 path 字段不再存储 "error:" 报错字符串,
    改为置 NULL,防止日志污染数据

  **前端交互增强**
  - 侧边栏支持折叠/展开:收起为仅图标模式(w-16),展开恢复完整模式(w-64);
    收起后点击 Logo 展开,含流畅 cubic-bezier 过渡动画
  - 阅读面板新增 PDF 内嵌预览:已下载 PDF 时可通过 iframe 切换查看
    /api/files 下的本地文献
  - reader/citation 面板未选文献时展示带图标的空状态引导页,替代空白页
  - 文献详情面板改为固定高度弹性布局(h-[460px]),各区块按比例分配避免
    内容挤压;期刊名过长截断+悬停tooltip;关键词无数据显式占位
  - 全局移除 emoji Unicode,统一替换为 lucide-react 图标组件,
    消除跨平台字体渲染差异

  **反爬检测精细化**
  - 按响应长度分层:>150KB 跳过检测(完整文献),<5KB 才扫描通用 HTTP
    错误关键字,杜绝长文献误触 Cloudflare/503 模式匹配
  - 新增 Radware Bot Manager、ShieldSquare WAF 特征识别

  **健壮性**
  - Obscura 下载校验失败后自动清理硬盘残留坏文件
  - 健康检查工具:文献已有有效 HTML 但 PDF 字段为旧报错时自动判定可修复
  - 上传接口 body limit 提升至 100MB,新增 /api/files 静态文件服务路由
  - StandardPaper 新增 has_pdf/has_html 字段区分格式级下载状态
This commit is contained in:
fmq
2026-06-13 11:11:33 +08:00
parent 2a5b1c0c91
commit 3f1935678b
27 changed files with 1275 additions and 473 deletions
+179 -122
View File
@@ -1,7 +1,7 @@
// dashboard/src/App.tsx
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import { Loader, Download } from 'lucide-react';
import { Loader, Download, BookOpen, GitFork, RefreshCw, AlertTriangle } from 'lucide-react';
import { Sidebar } from './components/layout/Sidebar';
import { SearchPanel, getDoctypeBadge } from './features/search/SearchPanel';
import { LibraryPanel } from './features/library/LibraryPanel';
@@ -431,7 +431,9 @@ export default function App() {
<main className="flex-1 flex flex-col overflow-hidden relative">
{/* 选项卡容器 */}
<div className="flex-1 overflow-y-auto p-4 sm:p-6 md:p-8 relative z-10 w-full flex flex-col">
<div className="w-full max-w-7xl mx-auto flex-1 flex flex-col min-h-0">
<div className={`w-full flex-1 flex flex-col min-h-0 ${
(activeTab === 'reader' || activeTab === 'citation') ? 'max-w-none' : 'max-w-7xl mx-auto'
}`}>
{activeTab === 'search' && (
<SearchPanel
searchQuery={searchQuery}
@@ -473,42 +475,78 @@ export default function App() {
/>
)}
{activeTab === 'reader' && selectedPaper && (
<ReaderPanel
selectedPaper={selectedPaper}
parsing={parsing}
handleParse={handleParse}
translating={translating}
handleTranslate={handleTranslate}
showNotesPanel={showNotesPanel}
setShowNotesPanel={setShowNotesPanel}
notes={notes}
englishText={englishText}
chineseText={chineseText}
handleTextSelection={handleTextSelection}
selectedParagraphIdx={selectedParagraphIdx}
setSelectedParagraphIdx={setSelectedParagraphIdx}
selectedText={selectedText}
setSelectedText={setSelectedText}
newNoteColor={newNoteColor}
setNewNoteColor={setNewNoteColor}
newNoteText={newNoteText}
setNewNoteText={setNewNoteText}
handleCreateNote={handleCreateNote}
handleDeleteNote={handleDeleteNote}
showConfirm={showConfirm}
/>
{activeTab === 'reader' && (
selectedPaper ? (
<ReaderPanel
selectedPaper={selectedPaper}
parsing={parsing}
handleParse={handleParse}
translating={translating}
handleTranslate={handleTranslate}
showNotesPanel={showNotesPanel}
setShowNotesPanel={setShowNotesPanel}
notes={notes}
englishText={englishText}
chineseText={chineseText}
handleTextSelection={handleTextSelection}
selectedParagraphIdx={selectedParagraphIdx}
setSelectedParagraphIdx={setSelectedParagraphIdx}
selectedText={selectedText}
setSelectedText={setSelectedText}
newNoteColor={newNoteColor}
setNewNoteColor={setNewNoteColor}
newNoteText={newNoteText}
setNewNoteText={setNewNoteText}
handleCreateNote={handleCreateNote}
handleDeleteNote={handleDeleteNote}
showConfirm={showConfirm}
/>
) : (
<div className="w-full flex-1 flex flex-col items-center justify-center p-12 bg-white rounded-2xl border border-slate-200 shadow-xs min-h-[450px]">
<div className="w-16 h-16 rounded-2xl bg-sky-50 border border-sky-100 flex items-center justify-center text-sky-600 mb-5 shadow-2xs">
<BookOpen className="w-8 h-8" />
</div>
<h3 className="text-base font-bold text-slate-800 tracking-wide mb-2"></h3>
<p className="text-xs text-slate-500 max-w-sm text-center leading-relaxed mb-6">
</p>
<button
onClick={() => setActiveTab('library')}
className="px-6 py-2.5 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-all shadow-xs hover:shadow-md cursor-pointer hover:scale-102 active:scale-98"
>
</button>
</div>
)
)}
{activeTab === 'citation' && (
<CitationPanel
selectedPaper={selectedPaper}
loadingCitations={loadingCitations}
citationNetwork={citationNetwork}
citationHistory={citationHistory}
loadCitations={loadCitations}
onUncachedClick={setUncachedBibcode}
/>
selectedPaper ? (
<CitationPanel
selectedPaper={selectedPaper}
loadingCitations={loadingCitations}
citationNetwork={citationNetwork}
citationHistory={citationHistory}
loadCitations={loadCitations}
onUncachedClick={setUncachedBibcode}
/>
) : (
<div className="w-full flex-1 flex flex-col items-center justify-center p-12 bg-white rounded-2xl border border-slate-200 shadow-xs min-h-[450px]">
<div className="w-16 h-16 rounded-2xl bg-sky-50 border border-sky-100 flex items-center justify-center text-sky-600 mb-5 shadow-2xs">
<GitFork className="w-8 h-8" />
</div>
<h3 className="text-base font-bold text-slate-800 tracking-wide mb-2"></h3>
<p className="text-xs text-slate-500 max-w-sm text-center leading-relaxed mb-6">
-
</p>
<button
onClick={() => setActiveTab('library')}
className="px-6 py-2.5 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-all shadow-xs hover:shadow-md cursor-pointer hover:scale-102 active:scale-98"
>
</button>
</div>
)
)}
{activeTab === 'sync' && (
@@ -672,49 +710,58 @@ export default function App() {
</div>
{/* 详情内容 */}
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1 text-xs scrollbar-thin">
<div className="space-y-4 h-[460px] flex flex-col overflow-y-auto pr-1 text-xs scrollbar-thin">
{/* 作者 */}
<div className="space-y-1">
<div className="space-y-1 h-12 overflow-y-auto scrollbar-thin shrink-0">
<span className="text-slate-450 font-bold"></span>
<p className="text-slate-800 leading-relaxed font-semibold">{detailPaper.authors.join(', ')}</p>
</div>
{/* 期刊 & 年份 */}
<div className="grid grid-cols-2 gap-4 border-y border-slate-100 py-3">
<div className="space-y-1">
<span className="text-slate-450 font-bold block"></span>
<span className="text-slate-800 font-bold italic">{detailPaper.pub_journal || '未标注'}</span>
<div className="grid grid-cols-5 gap-4 border-y border-slate-100 py-2.5 shrink-0 h-14">
<div className="space-y-0.5 flex flex-col justify-center min-w-0 col-span-4">
<span className="text-slate-450 font-bold block text-[10px] leading-tight"></span>
<span
className="text-slate-800 font-bold italic truncate block text-[11px] leading-tight"
title={detailPaper.pub_journal || '未标注'}
>
{detailPaper.pub_journal || '未标注'}
</span>
</div>
<div className="space-y-1">
<span className="text-slate-450 font-bold block"></span>
<span className="text-slate-850 font-extrabold">{detailPaper.year}</span>
<div className="space-y-0.5 flex flex-col justify-center col-span-1">
<span className="text-slate-450 font-bold block text-[10px] leading-tight"></span>
<span className="text-slate-850 font-extrabold block text-[11px] leading-tight">{detailPaper.year}</span>
</div>
</div>
{/* 摘要 */}
<div className="space-y-1.5">
<div className="space-y-1.5 flex flex-col h-40 shrink-0">
<span className="text-slate-450 font-bold block"></span>
<p className="text-slate-700 leading-relaxed font-normal bg-slate-50 p-3.5 rounded-lg border border-slate-200 text-justify max-h-48 overflow-y-auto scrollbar-thin select-text">
<p className="text-slate-700 leading-relaxed font-normal bg-slate-50 p-3 flex-1 rounded-lg border border-slate-200 text-justify overflow-y-auto scrollbar-thin select-text">
{detailPaper.abstract_text || '该文献暂无摘要数据。'}
</p>
</div>
{/* 关键字 */}
{detailPaper.keywords && detailPaper.keywords.length > 0 && (
<div className="space-y-1.5">
<span className="text-slate-450 font-bold block"></span>
<div className="flex flex-wrap gap-1.5">
<div className="space-y-1.5 flex flex-col h-16 shrink-0">
<span className="text-slate-450 font-bold block"></span>
{detailPaper.keywords && detailPaper.keywords.length > 0 ? (
<div className="flex flex-wrap gap-1.5 flex-1 content-start items-start overflow-y-auto scrollbar-thin pr-1">
{detailPaper.keywords.map(kw => (
<span key={kw} className="px-2 py-0.5 rounded bg-slate-100 border border-slate-200 text-slate-600 font-bold text-[9px]">
{kw}
</span>
))}
</div>
</div>
)}
) : (
<div className="flex items-center justify-center flex-1 bg-slate-50 border border-slate-200 rounded-lg text-[10px] text-slate-400 font-bold select-none">
</div>
)}
</div>
{/* 标识符 */}
<div className="border-t border-slate-100 pt-3 grid grid-cols-1 sm:grid-cols-3 gap-2 text-[10px] font-mono">
<div className="border-t border-slate-100 pt-3 grid grid-cols-1 sm:grid-cols-3 gap-2 text-[10px] font-mono mt-auto shrink-0">
<div className="bg-slate-50 px-2.5 py-1.5 rounded border border-slate-150">
<span className="text-slate-400 font-bold block">BIBCODE</span>
<span className="text-slate-700 font-semibold select-all truncate block" title={detailPaper.bibcode === detailPaper.arxiv_id ? '暂无' : detailPaper.bibcode}>
@@ -765,8 +812,86 @@ export default function App() {
</div>
</div>
{/* 手动上传文件(应对防爬阻断) */}
<div className="border-t border-slate-100 pt-3 space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-450 font-bold block">线</span>
<span className="text-[9px] text-amber-700 font-bold bg-amber-50 px-2 py-0.5 rounded border border-amber-200">/</span>
</div>
<p className="text-[10px] text-slate-450 leading-relaxed">
PDF HTML
</p>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col items-center justify-center border border-dashed border-slate-300 rounded-lg p-2 hover:bg-slate-50 transition-colors relative cursor-pointer group min-h-[50px]">
<input
type="file"
accept="application/pdf"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleManualUpload(detailPaper.bibcode, 'pdf', file);
}}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
disabled={uploadingBibcode === detailPaper.bibcode}
/>
{detailPaper.has_pdf && (
<span className="absolute top-1 right-1 text-[8px] font-bold text-emerald-600 bg-emerald-50 border border-emerald-200 px-1 py-0.2 rounded select-none pointer-events-none z-10">
</span>
)}
<span className="text-[10px] font-bold text-sky-600 group-hover:underline">
{uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 PDF 文献'}
</span>
<span className="text-[8px] text-slate-400"> .pdf </span>
</div>
<div className="flex flex-col items-center justify-center border border-dashed border-slate-300 rounded-lg p-2 hover:bg-slate-50 transition-colors relative cursor-pointer group min-h-[50px]">
<input
type="file"
accept="text/html,.html"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleManualUpload(detailPaper.bibcode, 'html', file);
}}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
disabled={uploadingBibcode === detailPaper.bibcode}
/>
{detailPaper.has_html && (
<span className="absolute top-1 right-1 text-[8px] font-bold text-emerald-600 bg-emerald-50 border border-emerald-200 px-1 py-0.2 rounded select-none pointer-events-none z-10">
</span>
)}
<span className="text-[10px] font-bold text-sky-600 group-hover:underline">
{uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 HTML 文献'}
</span>
<span className="text-[8px] text-slate-400"> .html </span>
</div>
</div>
{!detailPaper.is_downloaded && (
<div className="pt-1">
{detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource' ? (
<button
type="button"
onClick={() => handleMarkNoResource(detailPaper.bibcode, true)}
className="w-full py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-[10px] font-bold transition-all border border-slate-250 cursor-pointer flex items-center justify-center gap-1.5"
>
<RefreshCw className="w-3 h-3" /> ()
</button>
) : (
<button
type="button"
onClick={() => handleMarkNoResource(detailPaper.bibcode, false)}
className="w-full py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-[10px] font-bold transition-all border border-slate-250 cursor-pointer flex items-center justify-center gap-1.5"
>
<AlertTriangle className="w-3 h-3 text-amber-500" /> ()
</button>
)}
</div>
)}
</div>
{/* 自动下载失败诊断 */}
{(detailPaper.pdf_error || detailPaper.html_error) && (
{!detailPaper.is_downloaded && (detailPaper.pdf_error || detailPaper.html_error) && (
<div className={`rounded-lg p-3 text-[10px] space-y-1 ${
(detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource')
? 'bg-amber-50 border border-amber-150 text-amber-850'
@@ -802,74 +927,6 @@ export default function App() {
)}
</div>
)}
{/* 手动上传文件(应对防爬阻断) */}
<div className="border-t border-slate-100 pt-3 space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-450 font-bold block">线</span>
<span className="text-[9px] text-amber-700 font-bold bg-amber-50 px-2 py-0.5 rounded border border-amber-200">/</span>
</div>
<p className="text-[10px] text-slate-450 leading-relaxed">
PDF HTML
</p>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col items-center justify-center border border-dashed border-slate-300 rounded-lg p-2 hover:bg-slate-50 transition-colors relative cursor-pointer group min-h-[50px]">
<input
type="file"
accept="application/pdf"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleManualUpload(detailPaper.bibcode, 'pdf', file);
}}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
disabled={uploadingBibcode === detailPaper.bibcode}
/>
<span className="text-[10px] font-bold text-sky-600 group-hover:underline">
{uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 PDF 文献'}
</span>
<span className="text-[8px] text-slate-400"> .pdf </span>
</div>
<div className="flex flex-col items-center justify-center border border-dashed border-slate-300 rounded-lg p-2 hover:bg-slate-50 transition-colors relative cursor-pointer group min-h-[50px]">
<input
type="file"
accept="text/html,.html"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleManualUpload(detailPaper.bibcode, 'html', file);
}}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
disabled={uploadingBibcode === detailPaper.bibcode}
/>
<span className="text-[10px] font-bold text-sky-600 group-hover:underline">
{uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 HTML 文献'}
</span>
<span className="text-[8px] text-slate-400"> .html </span>
</div>
</div>
{!detailPaper.is_downloaded && (
<div className="pt-1">
{detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource' ? (
<button
type="button"
onClick={() => handleMarkNoResource(detailPaper.bibcode, true)}
className="w-full py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-[10px] font-bold transition-all border border-slate-250 cursor-pointer flex items-center justify-center gap-1.5"
>
🔄 ()
</button>
) : (
<button
type="button"
onClick={() => handleMarkNoResource(detailPaper.bibcode, false)}
className="w-full py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-[10px] font-bold transition-all border border-slate-250 cursor-pointer flex items-center justify-center gap-1.5"
>
()
</button>
)}
</div>
)}
</div>
</div>
{/* 底部操作:整合所有动作(阅读、图谱、下载) */}
+161 -43
View File
@@ -1,5 +1,6 @@
// dashboard/src/components/layout/Sidebar.tsx
import { Search, BookOpen, GitFork, Library, RefreshCw } from 'lucide-react';
import { useState } from 'react';
import { Search, BookOpen, GitFork, Library, RefreshCw, ChevronLeft } from 'lucide-react';
import type { StandardPaper } from '../../types';
interface SidebarProps {
@@ -10,31 +11,81 @@ interface SidebarProps {
}
export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations }: SidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(false);
const renderLogo = () => (
<svg width="100%" height="100%" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
<circle cx="24" cy="24" r="18" stroke="#bae6fd" strokeWidth="1.5" />
<circle cx="24" cy="24" r="21" stroke="#0284c7" strokeWidth="1.5" strokeDasharray="2 3" />
<path d="M24 9C24 18 24 18 33 24C24 24 24 24 24 33C24 24 24 24 15 24C24 18 24 18 24 9Z" fill="url(#sidebarStarGrad)" />
<ellipse cx="24" cy="24" rx="20" ry="7" transform="rotate(-28 24 24)" stroke="#0284c7" strokeWidth="2" />
<circle cx="38" cy="16" r="4.5" fill="#0284c7" stroke="#ffffff" strokeWidth="1.5" />
<circle cx="10" cy="32" r="2.5" fill="#38bdf8" />
<defs>
<linearGradient id="sidebarStarGrad" x1="15" y1="9" x2="33" y2="33" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#0284c7" />
<stop offset="100%" stopColor="#0369a1" />
</linearGradient>
</defs>
</svg>
);
return (
<aside className="w-64 bg-slate-50 border-r border-slate-200 flex flex-col justify-between py-6 px-4 z-10 select-none">
<aside
className={`bg-slate-50 border-r border-slate-200 flex flex-col justify-between py-6 z-20 select-none transition-all duration-355 cubic-bezier(0.4, 0, 0.2, 1) ${
isCollapsed ? 'w-16 px-2' : 'w-64 px-4'
}`}
>
<div>
{/* 系统LOGO区 */}
<div className="px-3 mb-8 flex items-center gap-3">
<div className="w-9 h-9 flex items-center justify-center">
<svg className="w-9 h-9" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="18" stroke="#bae6fd" strokeWidth="1.5" />
<circle cx="24" cy="24" r="21" stroke="#0284c7" strokeWidth="1.5" strokeDasharray="2 3" />
<path d="M24 9C24 18 24 18 33 24C24 24 24 24 24 33C24 24 24 24 15 24C24 18 24 18 24 9Z" fill="url(#sidebarStarGrad)" />
<ellipse cx="24" cy="24" rx="20" ry="7" transform="rotate(-28 24 24)" stroke="#0284c7" strokeWidth="2" />
<circle cx="38" cy="16" r="4.5" fill="#0284c7" stroke="#ffffff" strokeWidth="1.5" />
<circle cx="10" cy="32" r="2.5" fill="#38bdf8" />
<defs>
<linearGradient id="sidebarStarGrad" x1="15" y1="9" x2="33" y2="33" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#0284c7" />
<stop offset="100%" stopColor="#0369a1" />
</linearGradient>
</defs>
</svg>
</div>
<div>
<h1 className="text-sm font-bold text-slate-800 tracking-wider">AstroResearch</h1>
<span className="text-[11px] text-slate-500 block font-medium"></span>
{/* 系统LOGO与折叠控制区 */}
<div className={`mb-8 flex items-center transition-all duration-300 ${
isCollapsed ? 'justify-center px-0' : 'justify-between px-3'
}`}>
{/* Logo & 标题文字 */}
<div className="flex items-center gap-3 min-w-0">
{/* Logo按钮 (只在折叠状态下可点击展开) */}
<button
type="button"
disabled={!isCollapsed}
onClick={() => setIsCollapsed(false)}
className={`flex items-center justify-center shrink-0 rounded-xl transition-all duration-300 ${
isCollapsed
? 'w-11 h-11 bg-white hover:bg-sky-50/50 border border-slate-200 cursor-pointer shadow-xs hover:shadow-sm hover:scale-105 active:scale-95'
: 'w-9 h-9 bg-transparent border border-transparent cursor-default'
}`}
title={isCollapsed ? "展开导航" : undefined}
>
<div className={`transition-all duration-300 flex items-center justify-center ${isCollapsed ? 'w-8 h-8' : 'w-9 h-9'}`}>
{renderLogo()}
</div>
</button>
{/* 系统说明文字 */}
<div
className={`flex flex-col transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 scale-95 -translate-x-2 pointer-events-none select-none overflow-hidden h-0'
: 'opacity-100 max-w-[150px] scale-100 translate-x-0'
}`}
>
<h1 className="text-sm font-bold text-slate-800 tracking-wider whitespace-nowrap">AstroResearch</h1>
<span className="text-[11px] text-slate-500 block font-medium font-sans whitespace-nowrap"></span>
</div>
</div>
{/* 折叠控制按钮 (圆润悬浮效果) */}
<button
type="button"
onClick={() => setIsCollapsed(true)}
className={`p-1.5 rounded-full bg-slate-100 hover:bg-slate-200 border border-slate-200 text-slate-500 hover:text-slate-800 transition-all duration-300 cursor-pointer shadow-2xs shrink-0 flex items-center justify-center hover:scale-105 active:scale-95 ${
isCollapsed
? 'opacity-0 scale-75 pointer-events-none w-0 h-0 p-0 border-0 overflow-hidden'
: 'opacity-100 scale-100'
}`}
title="收起导航"
>
<ChevronLeft className="w-3.5 h-3.5" />
</button>
</div>
{/* 导航菜单列表 */}
@@ -43,22 +94,25 @@ export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations
{ id: 'search', label: '统一检索', icon: Search },
{ id: 'library', label: '馆藏管理', icon: Library },
{ id: 'sync', label: '批量任务', icon: RefreshCw },
{ id: 'reader', label: '双语阅读', icon: BookOpen, disabled: !selectedPaper },
{ id: 'citation', label: '引用星系', icon: GitFork, disabled: !selectedPaper },
].map(tab => {
{ id: 'reader', label: '双语阅读', icon: BookOpen },
{ id: 'citation', label: '引用星系', icon: GitFork },
].map((tab: { id: string; label: string; icon: any; disabled?: boolean }) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
disabled={tab.disabled}
title={isCollapsed ? tab.label : undefined}
onClick={() => {
setActiveTab(tab.id as any);
if (tab.id === 'citation' && selectedPaper) {
loadCitations(selectedPaper.bibcode);
}
}}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-xs font-semibold tracking-wider transition-all border ${
className={`w-full flex items-center rounded-lg text-xs font-semibold tracking-wider transition-all duration-300 border ${
isCollapsed ? 'px-2 py-2.5 justify-center' : 'px-3 py-2.5'
} ${
isActive
? 'bg-sky-50 border-sky-200 text-sky-700 shadow-sm'
: tab.disabled
@@ -66,29 +120,93 @@ export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations
: 'border-transparent text-slate-650 hover:bg-slate-100 hover:text-slate-800'
}`}
>
<Icon className={`w-4 h-4 ${isActive ? 'text-sky-600' : 'text-slate-500'}`} />
<span>{tab.label}</span>
<Icon className={`w-4 h-4 shrink-0 transition-colors duration-300 ${isActive ? 'text-sky-600' : 'text-slate-500'}`} />
<span
className={`truncate transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 pointer-events-none select-none overflow-hidden scale-90 -translate-x-2'
: 'opacity-100 max-w-[150px] scale-100 translate-x-0 ml-3'
}`}
>
{tab.label}
</span>
</button>
);
})}
</nav>
</div>
{/* 底部当前选定文献提示 */}
{selectedPaper ? (
<div className="p-3.5 rounded-lg border border-sky-100 bg-sky-50/50">
<span className="text-[9px] font-bold text-sky-600 tracking-widest block mb-1"></span>
<h4 className="text-xs text-slate-800 font-bold line-clamp-2 mb-2 leading-relaxed">{selectedPaper.title}</h4>
<div className="flex items-center justify-between text-[10px] font-medium text-slate-500">
<span>: {selectedPaper.year}</span>
<span className="truncate max-w-[90px] font-mono">{selectedPaper.bibcode}</span>
{/* 底部当前选定文献提示 (平滑动画版本) */}
<div className="space-y-4">
{selectedPaper ? (
<div
className={`relative overflow-hidden transition-all duration-300 border rounded-lg ${
isCollapsed
? 'p-0 border-transparent bg-transparent flex justify-center'
: 'p-3.5 border-sky-100 bg-sky-50/50'
}`}
title={isCollapsed ? `当前选定文献: ${selectedPaper.title}` : undefined}
>
{/* 折叠下的微型图书图标 */}
<div
className={`transition-all duration-300 flex items-center justify-center rounded-lg border border-sky-100 bg-sky-50 text-sky-600 shrink-0 shadow-sm ${
isCollapsed
? 'w-9 h-9 opacity-100 scale-100'
: 'w-0 h-0 opacity-0 scale-75 overflow-hidden'
}`}
>
<BookOpen className="w-4 h-4" />
</div>
{/* 展开下的完整详情 */}
<div
className={`transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 max-h-0 pointer-events-none select-none overflow-hidden'
: 'opacity-100 max-w-[200px] max-h-40'
}`}
>
<span className="text-[9px] font-bold text-sky-600 tracking-widest block mb-1"></span>
<h4 className="text-xs text-slate-800 font-bold line-clamp-2 mb-2 leading-relaxed">{selectedPaper.title}</h4>
<div className="flex items-center justify-between text-[10px] font-medium text-slate-500 gap-2">
<span className="shrink-0">: {selectedPaper.year}</span>
<span className="truncate max-w-[90px] font-mono">{selectedPaper.bibcode}</span>
</div>
</div>
</div>
</div>
) : (
<div className="p-3 rounded-lg border border-slate-200 bg-slate-100/30 text-center">
<span className="text-[10px] text-slate-400 font-medium tracking-wide"></span>
</div>
)}
) : (
<div
className={`relative overflow-hidden transition-all duration-300 border rounded-lg ${
isCollapsed
? 'p-0 border-transparent bg-transparent flex justify-center'
: 'p-3 border-slate-200 bg-slate-100/30'
}`}
title={isCollapsed ? "未选定研究目标" : undefined}
>
{/* 折叠下的微型馆藏图标 */}
<div
className={`transition-all duration-300 flex items-center justify-center rounded-lg border border-slate-200 bg-slate-100/30 text-slate-400 shrink-0 ${
isCollapsed
? 'w-9 h-9 opacity-100 scale-100'
: 'w-0 h-0 opacity-0 scale-75 overflow-hidden'
}`}
>
<Library className="w-4 h-4" />
</div>
{/* 展开下的提示文字 */}
<div
className={`text-center transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 max-h-0 pointer-events-none select-none overflow-hidden'
: 'opacity-100 max-w-[200px] max-h-12'
}`}
>
<span className="text-[10px] text-slate-400 font-medium tracking-wide"></span>
</div>
</div>
)}
</div>
</aside>
);
}
@@ -388,7 +388,7 @@ export function LibraryPanel({
<div className="flex justify-between items-center text-xs text-slate-500 font-bold px-1">
<span> {sortedLibrary.length} / {library.length} </span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sortedLibrary.map(paper => {
return (
<div
+34 -4
View File
@@ -5,7 +5,7 @@ import remarkMath from 'remark-math';
import remarkGfm from 'remark-gfm';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
import { FileText, Loader, Languages, RotateCw, Pencil, X, PlusCircle, Trash2 } from 'lucide-react';
import { FileText, Loader, Languages, RotateCw, Pencil, X, PlusCircle, Trash2, BookOpen } from 'lucide-react';
import type { StandardPaper, NoteRecord } from '../../types';
interface ReaderPanelProps {
@@ -70,6 +70,7 @@ export function ReaderPanel({
}
return 'bilingual';
});
const [showPdf, setShowPdf] = useState(false);
const englishRef = useRef<HTMLDivElement>(null);
const chineseRef = useRef<HTMLDivElement>(null);
@@ -232,10 +233,31 @@ export function ReaderPanel({
<div
ref={englishRef}
onScroll={handleEnglishScroll}
className="console-panel rounded-xl p-6 overflow-y-auto bg-white border border-slate-200 relative flex flex-col"
className={`console-panel rounded-xl p-6 bg-white border border-slate-200 relative flex flex-col ${
showPdf ? 'overflow-hidden' : 'overflow-y-auto'
}`}
>
<div className="flex items-center justify-between mb-4 border-b border-slate-100 pb-2.5">
<span className="text-xs font-bold text-slate-800"></span>
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-slate-800"></span>
{selectedPaper.has_pdf && (
<button
type="button"
onClick={() => setShowPdf(!showPdf)}
className="text-[9px] font-bold px-2 py-0.5 rounded bg-sky-50 text-sky-700 border border-sky-200 hover:bg-sky-100 transition-all cursor-pointer flex items-center gap-1"
>
{showPdf ? (
<>
<FileText className="w-3 h-3" />
</>
) : (
<>
<BookOpen className="w-3 h-3" /> PDF
</>
)}
</button>
)}
</div>
<button
onClick={() => setShowNotesPanel(!showNotesPanel)}
className={`flex items-center gap-1 text-xs font-bold px-2.5 py-1 rounded-lg border transition-all ${
@@ -247,7 +269,15 @@ export function ReaderPanel({
</button>
</div>
{parsing ? (
{showPdf ? (
<div className="flex-1 min-h-0 w-full rounded-lg overflow-hidden border border-slate-200 bg-slate-50">
<iframe
src={`/api/files/PDF/${selectedPaper.bibcode}.pdf#navpanes=0&pagemode=none&view=FitH`}
className="w-full h-full border-none"
title="文献 PDF"
/>
</div>
) : parsing ? (
<div className="flex-1 flex flex-col items-center justify-center text-slate-500 space-y-3">
<Loader className="w-8 h-8 animate-spin text-sky-600" />
<p className="text-xs font-bold"> Markdown ...</p>
+10 -7
View File
@@ -1,6 +1,6 @@
// dashboard/src/features/search/SearchPanel.tsx
import React from 'react';
import { Search, Loader, CheckCircle, Copy, Download, ChevronLeft, ChevronRight, SlidersHorizontal } from 'lucide-react';
import { Search, Loader, CheckCircle, Copy, Download, ChevronLeft, ChevronRight, SlidersHorizontal, AlertTriangle, Lightbulb } from 'lucide-react';
import type { StandardPaper } from '../../types';
import { CustomSelect } from '../../components/CustomSelect';
@@ -266,8 +266,11 @@ export function SearchPanel({
</div>
)}
<div className="text-[11px] text-slate-500 flex flex-wrap gap-x-4 gap-y-1 px-1">
<span className="font-semibold text-slate-650">💡 :</span>
<div className="text-[11px] text-slate-500 flex flex-wrap gap-x-4 gap-y-1 px-1 items-center">
<span className="font-semibold text-slate-650 flex items-center gap-1">
<Lightbulb className="w-3.5 h-3.5 text-amber-500" />
:
</span>
<span>: <code className="bg-slate-200 px-1 py-0.5 rounded text-slate-800 font-mono text-[10px]">author:"Althaus"</code></span>
<span>: <code className="bg-slate-200 px-1 py-0.5 rounded text-slate-800 font-mono text-[10px]">title:"hot subdwarf"</code></span>
<span>: <code className="bg-slate-200 px-1 py-0.5 rounded text-slate-800 font-mono text-[10px]">year:2020-2023</code></span>
@@ -419,16 +422,16 @@ export function SearchPanel({
paper.pdf_error === 'no_resource' && paper.html_error === 'no_resource' ? (
<span
title="已手动标记为【无有效全文资源】,批量下载时自动跳过"
className="px-2 py-1 bg-slate-50 text-slate-600 border border-slate-200 text-[10px] font-bold rounded-lg cursor-help flex items-center gap-0.5"
className="px-2 py-1 bg-slate-50 text-slate-600 border border-slate-200 text-[10px] font-bold rounded-lg cursor-help flex items-center gap-1"
>
<AlertTriangle className="w-3 h-3 text-slate-400" />
</span>
) : (
<span
title={`自动下载失败:${[paper.pdf_error, paper.html_error].filter(Boolean).join('; ')}`}
className="px-2 py-1 bg-rose-50 text-rose-700 border border-rose-200 text-[10px] font-bold rounded-lg cursor-help flex items-center gap-0.5"
className="px-2 py-1 bg-rose-50 text-rose-700 border border-rose-200 text-[10px] font-bold rounded-lg cursor-help flex items-center gap-1"
>
<AlertTriangle className="w-3 h-3 text-rose-500" />
</span>
)
)}
+4 -3
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { RefreshCw, Play, Info, AlertTriangle, CheckCircle, Loader, StopCircle, Download, FileText, SlidersHorizontal } from 'lucide-react';
import { RefreshCw, Play, Info, AlertTriangle, CheckCircle, Loader, StopCircle, Download, FileText, SlidersHorizontal, Lightbulb } from 'lucide-react';
import type { SavedSyncQuery } from '../../types';
import { CustomSelect } from '../../components/CustomSelect';
@@ -559,8 +559,9 @@ export function SyncPanel() {
</div>
{status.active && (status.source === 'all' || status.source === 'arxiv') ? (
<div className="p-3 rounded-lg bg-amber-50 border border-amber-200 text-xs text-amber-700">
💡 arXiv 3000
<div className="p-3 rounded-lg bg-amber-50 border border-amber-200 text-xs text-amber-700 flex items-start gap-1.5">
<Lightbulb className="w-4 h-4 shrink-0 mt-0.5" />
<span> arXiv 3000 </span>
</div>
) : null}
</div>
+2
View File
@@ -13,6 +13,8 @@ export interface StandardPaper {
citation_count: number;
reference_count: number;
is_downloaded: boolean;
has_pdf: boolean;
has_html: boolean;
has_markdown: boolean;
has_translation: boolean;
doctype: string;