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:
+179
-122
@@ -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>
|
||||
|
||||
{/* 底部操作:整合所有动作(阅读、图谱、下载) */}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user