核心新增:
- RAG 问答系统:Markdown 安全切片器 (LaTeX 保护) + 向量化 + sqlite-vec 检索 + LLM 生成
- 天体目标识别:IAU 标准正则提取 15+ 星表标识符,CDS SIMBAD/Sesame 查询与本地缓存
- 多模态 LLM:chat_completion_with_image 支持图表视觉分析
- CLI Skills Agent (cli.rs):对外暴露 rag/target/ingest 等 5 个子命令
- 解析器模块化重构:单体 778 行 → 按期刊拆分 (A&A/ar5iv/IOP/Generic/PDF) +
common.rs 静态正则工具库
管线与 Schema:
- AssetSync→AssetBatch 重命名,批量管线新增 embed/target 两个处理阶段
- 新增 paper_chunks_content (RAG 切片) 和 paper_targets (天体缓存) 两张表
- StandardPaper 新增 has_vector 字段,所有查询同步更新
前端:
- 新增 AI 助手侧边栏 (RAG 问答 + 来源跳转高亮)
- 最近浏览文献列表 (localStorage 持久化)、跨面板无缝导航
- SyncPanel 批量阶段扩展为下拉选项,支持向量化/天体识别
测试与清理:
- 集成测试合并至 ads.rs 和 llm.rs,新增 chunker + target 单元测试 15 个
- 删除旧单体 parser.rs、独立测试文件及过期 scratch 脚本
1132 lines
49 KiB
TypeScript
1132 lines
49 KiB
TypeScript
// dashboard/src/App.tsx
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import axios from 'axios';
|
||
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';
|
||
import { ReaderPanel } from './features/reader/ReaderPanel';
|
||
import { CitationPanel } from './features/citation/CitationPanel';
|
||
import { SyncPanel } from './features/sync/SyncPanel';
|
||
import type { StandardPaper, CitationNetwork, NoteRecord } from './types';
|
||
|
||
export default function App() {
|
||
const [activeTab, setActiveTab] = useState<'search' | 'library' | 'reader' | 'citation' | 'sync'>(() => {
|
||
const saved = localStorage.getItem('astro_active_tab');
|
||
return (saved as any) || 'search';
|
||
});
|
||
|
||
useEffect(() => {
|
||
localStorage.setItem('astro_active_tab', activeTab);
|
||
}, [activeTab]);
|
||
|
||
// 全局对话框弹窗状态
|
||
const [dialog, setDialog] = useState<{
|
||
type: 'alert' | 'confirm';
|
||
title: string;
|
||
message: string;
|
||
onConfirm: () => void;
|
||
onCancel?: () => void;
|
||
} | null>(null);
|
||
|
||
const showAlert = useCallback((message: string, title = '系统提示') => {
|
||
setDialog({
|
||
type: 'alert',
|
||
title,
|
||
message,
|
||
onConfirm: () => {},
|
||
});
|
||
}, []);
|
||
|
||
const showConfirm = useCallback((message: string, onConfirm: () => void, title = '确认操作') => {
|
||
setDialog({
|
||
type: 'confirm',
|
||
title,
|
||
message,
|
||
onConfirm,
|
||
});
|
||
}, []);
|
||
|
||
// 共享数据状态
|
||
const [library, setLibrary] = useState<StandardPaper[]>([]);
|
||
const [selectedPaper, setSelectedPaper] = useState<StandardPaper | null>(null);
|
||
const [detailBibcode, setDetailBibcode] = useState<string | null>(null);
|
||
|
||
const [recentlySelected, setRecentlySelected] = useState<StandardPaper[]>(() => {
|
||
try {
|
||
const saved = localStorage.getItem('astro_recently_selected');
|
||
return saved ? JSON.parse(saved) : [];
|
||
} catch (e) {
|
||
console.error('Failed to parse recently selected papers', e);
|
||
return [];
|
||
}
|
||
});
|
||
|
||
useEffect(() => {
|
||
localStorage.setItem('astro_recently_selected', JSON.stringify(recentlySelected));
|
||
}, [recentlySelected]);
|
||
|
||
const addPaperToRecent = (paper: StandardPaper) => {
|
||
setRecentlySelected(prev => {
|
||
const filtered = prev.filter(p => p.bibcode !== paper.bibcode);
|
||
return [paper, ...filtered].slice(0, 5);
|
||
});
|
||
};
|
||
|
||
const openCitation = (paper: StandardPaper) => {
|
||
setSelectedPaper(paper);
|
||
setActiveTab('citation');
|
||
loadCitations(paper.bibcode, true);
|
||
addPaperToRecent(paper);
|
||
};
|
||
|
||
// 检索页状态
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [searchSource, setSearchSource] = useState<'all' | 'ads' | 'arxiv'>('all');
|
||
const [searchResults, setSearchResults] = useState<StandardPaper[]>([]);
|
||
|
||
// 衍生状态计算
|
||
const detailPaper = library.find(p => p.bibcode === detailBibcode) || searchResults.find(p => p.bibcode === detailBibcode);
|
||
const [searching, setSearching] = useState(false);
|
||
const [exportingList, setExportingList] = useState<string[]>([]);
|
||
const [bibtexContent, setBibtexContent] = useState<string | null>(null);
|
||
const [exporting, setExporting] = useState(false);
|
||
const [searchRows, setSearchRows] = useState(15);
|
||
const [searchStart, setSearchStart] = useState(0);
|
||
const [searchSort, setSearchSort] = useState('relevance');
|
||
const [searchCache, setSearchCache] = useState<Record<string, StandardPaper[]>>({});
|
||
|
||
// 读者页状态
|
||
const [englishText, setEnglishText] = useState('');
|
||
const [chineseText, setChineseText] = useState('');
|
||
const [parsing, setParsing] = useState(false);
|
||
const [translating, setTranslating] = useState(false);
|
||
const [vectorizing, setVectorizing] = useState(false);
|
||
|
||
// 引用星系数据状态
|
||
const [citationNetwork, setCitationNetwork] = useState<CitationNetwork | null>(null);
|
||
const [loadingCitations, setLoadingCitations] = useState(false);
|
||
const [citationHistory, setCitationHistory] = useState<CitationNetwork[]>([]); // 多跳历史
|
||
const [uncachedBibcode, setUncachedBibcode] = useState<string | null>(null);
|
||
|
||
// 笔记系统状态
|
||
const [notes, setNotes] = useState<NoteRecord[]>([]);
|
||
const [showNotesPanel, setShowNotesPanel] = useState(false);
|
||
const [newNoteText, setNewNoteText] = useState('');
|
||
const [newNoteColor, setNewNoteColor] = useState('yellow');
|
||
const [selectedParagraphIdx, setSelectedParagraphIdx] = useState<number | null>(null);
|
||
const [selectedText, setSelectedText] = useState('');
|
||
|
||
// 下载进度状态
|
||
const [downloadingBibcodes, setDownloadingBibcodes] = useState<Record<string, boolean>>({});
|
||
const [uploadingBibcode, setUploadingBibcode] = useState<string | null>(null);
|
||
|
||
// 1. 初始化时加载本地文献
|
||
useEffect(() => {
|
||
fetchLibrary();
|
||
}, []);
|
||
|
||
const fetchLibrary = async () => {
|
||
try {
|
||
const res = await axios.get<StandardPaper[]>('/api/library');
|
||
setLibrary(res.data);
|
||
|
||
const lastReadBibcode = localStorage.getItem('last_read_bibcode');
|
||
if (lastReadBibcode) {
|
||
const lastReadPaper = res.data.find(p => p.bibcode === lastReadBibcode);
|
||
if (lastReadPaper) {
|
||
const initialTab = localStorage.getItem('astro_active_tab') || 'search';
|
||
openReader(lastReadPaper, initialTab !== 'reader');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('加载本地文献库失败', e);
|
||
}
|
||
};
|
||
|
||
// 2. 检索文献 (统一执行逻辑,包含前端缓存)
|
||
const executeSearch = async (start: number, rows: number, sort: string) => {
|
||
if (!searchQuery.trim()) return;
|
||
|
||
// 构造缓存 Key
|
||
const cacheKey = `${searchQuery.trim()}_${searchSource}_${start}_${rows}_${sort}`;
|
||
if (searchCache[cacheKey]) {
|
||
setSearchResults(searchCache[cacheKey]);
|
||
return;
|
||
}
|
||
|
||
setSearching(true);
|
||
setBibtexContent(null);
|
||
try {
|
||
const res = await axios.get<StandardPaper[]>('/api/search', {
|
||
params: { q: searchQuery, source: searchSource, rows, start, sort }
|
||
});
|
||
setSearchResults(res.data);
|
||
// 写入缓存
|
||
setSearchCache(prev => ({ ...prev, [cacheKey]: res.data }));
|
||
} catch (e) {
|
||
console.error('检索文献失败', e);
|
||
showAlert('检索失败,请确认后端连接及 API 密钥配置。', '检索出错');
|
||
} finally {
|
||
setSearching(false);
|
||
}
|
||
};
|
||
|
||
const handleSearch = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setSearchStart(0);
|
||
executeSearch(0, searchRows, searchSort);
|
||
};
|
||
|
||
const handlePageChange = (newStart: number) => {
|
||
setSearchStart(newStart);
|
||
executeSearch(newStart, searchRows, searchSort);
|
||
};
|
||
|
||
const handleSortChange = (newSort: string) => {
|
||
setSearchSort(newSort);
|
||
setSearchStart(0);
|
||
executeSearch(0, searchRows, newSort);
|
||
};
|
||
|
||
const handleRowsChange = (newRows: number) => {
|
||
setSearchRows(newRows);
|
||
setSearchStart(0);
|
||
executeSearch(0, newRows, searchSort);
|
||
};
|
||
|
||
// 3. 触发文献双格式下载
|
||
const handleDownload = async (bibcode: string, force = false) => {
|
||
setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: true }));
|
||
try {
|
||
const res = await axios.post<StandardPaper>('/api/download', { bibcode, force });
|
||
// 更新库及检索列表中的状态
|
||
setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p));
|
||
setLibrary(prev => {
|
||
if (prev.some(p => p.bibcode === bibcode)) {
|
||
return prev.map(p => p.bibcode === bibcode ? res.data : p);
|
||
} else {
|
||
return [res.data, ...prev];
|
||
}
|
||
});
|
||
if (selectedPaper?.bibcode === bibcode) {
|
||
setSelectedPaper(res.data);
|
||
}
|
||
} catch (e) {
|
||
console.error('下载文献失败', e);
|
||
showAlert('文献下载失败,请检查 ADS 网络限制与网络代理!', '下载失败');
|
||
} finally {
|
||
setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: false }));
|
||
}
|
||
};
|
||
|
||
// 3b. 手动上传文献文件以绕过反爬/人机验证
|
||
const handleManualUpload = async (bibcode: string, type: 'pdf' | 'html', file: File) => {
|
||
setUploadingBibcode(bibcode);
|
||
const formData = new FormData();
|
||
formData.append('bibcode', bibcode);
|
||
formData.append('type', type);
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const res = await axios.post<StandardPaper>('/api/upload', formData, {
|
||
headers: {
|
||
'Content-Type': 'multipart/form-data',
|
||
},
|
||
});
|
||
// 更新前端库及检索列表状态
|
||
setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p));
|
||
setLibrary(prev => {
|
||
if (prev.some(p => p.bibcode === bibcode)) {
|
||
return prev.map(p => p.bibcode === bibcode ? res.data : p);
|
||
} else {
|
||
return [res.data, ...prev];
|
||
}
|
||
});
|
||
if (selectedPaper?.bibcode === bibcode) {
|
||
setSelectedPaper(res.data);
|
||
}
|
||
showAlert('手动文献文件上传导入成功!', '上传成功');
|
||
} catch (e: any) {
|
||
console.error('手动文件上传失败', e);
|
||
const errMsg = e.response?.data || '请确保上传的是合法且完整的文件。';
|
||
showAlert(`文件上传失败: ${errMsg}`, '上传出错');
|
||
} finally {
|
||
setUploadingBibcode(null);
|
||
}
|
||
};
|
||
|
||
// 3c. 手动标记文献为“无有效全文资源”
|
||
const handleMarkNoResource = async (bibcode: string, clear = false) => {
|
||
const performMark = async () => {
|
||
try {
|
||
const res = await axios.post<StandardPaper>('/api/no_resource', { bibcode, clear });
|
||
// 更新前端库及检索列表状态
|
||
setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p));
|
||
setLibrary(prev => {
|
||
if (prev.some(p => p.bibcode === bibcode)) {
|
||
return prev.map(p => p.bibcode === bibcode ? res.data : p);
|
||
} else {
|
||
return [res.data, ...prev];
|
||
}
|
||
});
|
||
if (selectedPaper?.bibcode === bibcode) {
|
||
setSelectedPaper(res.data);
|
||
}
|
||
showAlert(
|
||
clear
|
||
? '已成功清除“无全文资源”标记,该文献已被重新允许重载!'
|
||
: '已成功将文献标记为“无全文资源”,后续批量任务将自动跳过!',
|
||
'标记更新'
|
||
);
|
||
} catch (e: any) {
|
||
console.error('标记更新失败', e);
|
||
const errMsg = e.response?.data || '请稍后重试。';
|
||
showAlert(`标记失败: ${errMsg}`, '操作出错');
|
||
}
|
||
};
|
||
|
||
if (clear) {
|
||
performMark();
|
||
} else {
|
||
showConfirm(
|
||
'确认将此文献标记为“无有效全文资源”吗?\n标记后,未来的批量下载/解析重试任务将自动跳过此文献。',
|
||
performMark,
|
||
'标记无有效全文资源'
|
||
);
|
||
}
|
||
};
|
||
|
||
// 4. 文献解析成 Markdown (优先 HTML, 其次 PDF MinerU)
|
||
const handleParse = async (bibcode: string, force = false) => {
|
||
setParsing(true);
|
||
try {
|
||
const res = await axios.post<{ markdown: string }>('/api/parse', { bibcode, force });
|
||
setEnglishText(res.data.markdown);
|
||
|
||
// 更新文献状态
|
||
setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_markdown: true } : p));
|
||
if (selectedPaper?.bibcode === bibcode) {
|
||
setSelectedPaper(prev => prev ? { ...prev, has_markdown: true } : null);
|
||
}
|
||
} catch (e) {
|
||
console.error('文献解析失败', e);
|
||
showAlert('文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。', '解析失败');
|
||
} finally {
|
||
setParsing(false);
|
||
}
|
||
};
|
||
|
||
// 5. 对比翻译文本 (带天文学术语修正)
|
||
const handleTranslate = async (bibcode: string, force = false) => {
|
||
setTranslating(true);
|
||
try {
|
||
const res = await axios.post<{ translation: string }>('/api/translate', { bibcode, force });
|
||
setChineseText(res.data.translation);
|
||
|
||
setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_translation: true } : p));
|
||
if (selectedPaper?.bibcode === bibcode) {
|
||
setSelectedPaper(prev => prev ? { ...prev, has_translation: true } : null);
|
||
}
|
||
} catch (e) {
|
||
console.error('文献翻译失败', e);
|
||
showAlert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。', '翻译失败');
|
||
} finally {
|
||
setTranslating(false);
|
||
}
|
||
};
|
||
|
||
// 5.5. 对文献进行向量化分块入库 (独立任务)
|
||
const handleVectorize = async (bibcode: string) => {
|
||
setVectorizing(true);
|
||
try {
|
||
const res = await axios.post<{ chunk_count: number }>('/api/embed', { bibcode });
|
||
|
||
setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_vector: true } : p));
|
||
if (selectedPaper?.bibcode === bibcode) {
|
||
setSelectedPaper(prev => prev ? { ...prev, has_vector: true } : null);
|
||
}
|
||
showAlert(`文献向量化分块入库成功,共切片并录入 ${res.data.chunk_count} 个向量块。`, '向量化成功');
|
||
} catch (e) {
|
||
console.error('文献向量化失败', e);
|
||
showAlert('向量化失败,请检查 .env 中的 Embedding API 配置。', '向量化失败');
|
||
} finally {
|
||
setVectorizing(false);
|
||
}
|
||
};
|
||
|
||
// 6. 加载文献引用关系网络
|
||
const loadCitations = useCallback(async (bibcode: string, reset = false) => {
|
||
setLoadingCitations(true);
|
||
try {
|
||
const res = await axios.get<CitationNetwork>('/api/citations', {
|
||
params: { bibcode }
|
||
});
|
||
setCitationNetwork(res.data);
|
||
setCitationHistory(prev => {
|
||
if (reset) {
|
||
return [res.data];
|
||
}
|
||
if (prev.some(net => net.bibcode === res.data.bibcode)) {
|
||
return prev;
|
||
}
|
||
return [...prev, res.data];
|
||
});
|
||
} catch (e) {
|
||
console.error('加载引用拓扑失败', e);
|
||
} finally {
|
||
setLoadingCitations(false);
|
||
}
|
||
}, []);
|
||
|
||
// 7. 进入阅读器
|
||
const openReader = async (paper: StandardPaper, skipTabSwitch = false) => {
|
||
setSelectedPaper(paper);
|
||
localStorage.setItem('last_read_bibcode', paper.bibcode);
|
||
addPaperToRecent(paper);
|
||
|
||
setEnglishText('');
|
||
setChineseText('');
|
||
setNotes([]);
|
||
setShowNotesPanel(false);
|
||
if (!skipTabSwitch) {
|
||
setActiveTab('reader');
|
||
}
|
||
|
||
// 获取详情 (包含已有原文及翻译)
|
||
try {
|
||
const res = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', {
|
||
params: { bibcode: paper.bibcode }
|
||
});
|
||
if (res.data.english_content) {
|
||
setEnglishText(res.data.english_content);
|
||
}
|
||
if (res.data.translation_content) {
|
||
setChineseText(res.data.translation_content);
|
||
}
|
||
} catch (e) {
|
||
console.error('加载文献详情失败', e);
|
||
}
|
||
|
||
// 加载该文献的所有笔记
|
||
try {
|
||
const nRes = await axios.get<NoteRecord[]>('/api/notes', { params: { bibcode: paper.bibcode } });
|
||
setNotes(nRes.data);
|
||
} catch (e) {
|
||
console.error('加载笔记失败', e);
|
||
}
|
||
};
|
||
|
||
// 7.5. 从 RAG 问答结果跳转至目标文献段落并高亮
|
||
const handleJumpToSource = async (bibcode: string, paragraphIndex: number) => {
|
||
setActiveTab('reader');
|
||
|
||
// 如果当前选中的不是目标文献,则先加载它
|
||
if (!selectedPaper || selectedPaper.bibcode !== bibcode) {
|
||
setEnglishText('');
|
||
setChineseText('');
|
||
setNotes([]);
|
||
setShowNotesPanel(false);
|
||
|
||
try {
|
||
// 获取 StandardPaper 基础信息与正文
|
||
const paperRes = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', {
|
||
params: { bibcode }
|
||
});
|
||
|
||
setSelectedPaper(paperRes.data.paper);
|
||
if (paperRes.data.english_content) {
|
||
setEnglishText(paperRes.data.english_content);
|
||
}
|
||
if (paperRes.data.translation_content) {
|
||
setChineseText(paperRes.data.translation_content);
|
||
}
|
||
|
||
// 加载该文献的所有笔记
|
||
const nRes = await axios.get<NoteRecord[]>('/api/notes', { params: { bibcode } });
|
||
setNotes(nRes.data);
|
||
} catch (e) {
|
||
console.error('加载跳转文献失败:', e);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 强制开启侧边栏
|
||
setShowNotesPanel(true);
|
||
|
||
// 延迟等 DOM 渲染完成进行滚动高亮
|
||
setTimeout(() => {
|
||
const element = document.getElementById(`paragraph-block-${paragraphIndex}`);
|
||
if (element) {
|
||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
element.classList.add('bg-amber-100', 'transition-all');
|
||
setTimeout(() => {
|
||
element.classList.remove('bg-amber-100');
|
||
}, 2500);
|
||
}
|
||
}, 400);
|
||
};
|
||
|
||
// 8. 批量导出 BibTeX
|
||
const handleExportBibtex = async () => {
|
||
if (exportingList.length === 0) return;
|
||
setExporting(true);
|
||
try {
|
||
const res = await axios.post<{ bibtex: string }>('/api/export', { bibcodes: exportingList });
|
||
setBibtexContent(res.data.bibtex);
|
||
} catch (e) {
|
||
console.error('导出 BibTeX 失败', e);
|
||
showAlert('导出 BibTeX 失败,请检查 ADS Token。', '导出失败');
|
||
} finally {
|
||
setExporting(false);
|
||
}
|
||
};
|
||
|
||
// 批量选择引文
|
||
const toggleExportItem = (bibcode: string) => {
|
||
setExportingList(prev =>
|
||
prev.includes(bibcode) ? prev.filter(b => b !== bibcode) : [...prev, bibcode]
|
||
);
|
||
};
|
||
|
||
// 笔记相关操作
|
||
const handleCreateNote = async () => {
|
||
if (!selectedPaper || selectedParagraphIdx === null || !newNoteText.trim()) return;
|
||
try {
|
||
const res = await axios.post<NoteRecord>('/api/notes', {
|
||
bibcode: selectedPaper.bibcode,
|
||
paragraph_index: selectedParagraphIdx,
|
||
note_text: newNoteText.trim(),
|
||
highlight_color: newNoteColor,
|
||
selected_text: selectedText,
|
||
});
|
||
setNotes(prev => [...prev, res.data]);
|
||
setNewNoteText('');
|
||
setSelectedParagraphIdx(null);
|
||
setSelectedText('');
|
||
} catch (e) {
|
||
console.error('保存笔记失败', e);
|
||
}
|
||
};
|
||
|
||
const handleDeleteNote = async (id: number) => {
|
||
try {
|
||
await axios.delete('/api/notes', { params: { id } });
|
||
setNotes(prev => prev.filter(n => n.id !== id));
|
||
} catch (e) {
|
||
console.error('删除笔记失败', e);
|
||
}
|
||
};
|
||
|
||
// 选中文本时弹出笔记添加面板
|
||
const handleTextSelection = (paragraphIdx: number) => {
|
||
const sel = window.getSelection();
|
||
if (sel && sel.toString().trim().length > 3) {
|
||
setSelectedText(sel.toString().trim());
|
||
setSelectedParagraphIdx(paragraphIdx);
|
||
setShowNotesPanel(true);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="flex h-screen overflow-hidden text-slate-800 bg-slate-100 select-text">
|
||
|
||
{/* 导航左侧栏 */}
|
||
<Sidebar
|
||
activeTab={activeTab}
|
||
setActiveTab={setActiveTab}
|
||
selectedPaper={selectedPaper}
|
||
loadCitations={loadCitations}
|
||
/>
|
||
|
||
{/* 主工作区 */}
|
||
<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 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}
|
||
setSearchQuery={setSearchQuery}
|
||
searchSource={searchSource}
|
||
setSearchSource={setSearchSource}
|
||
searchRows={searchRows}
|
||
searchStart={searchStart}
|
||
searchSort={searchSort}
|
||
handlePageChange={handlePageChange}
|
||
handleSortChange={handleSortChange}
|
||
handleRowsChange={handleRowsChange}
|
||
searching={searching}
|
||
handleSearch={handleSearch}
|
||
searchResults={searchResults}
|
||
exportingList={exportingList}
|
||
toggleExportItem={toggleExportItem}
|
||
handleExportBibtex={handleExportBibtex}
|
||
exporting={exporting}
|
||
bibtexContent={bibtexContent}
|
||
downloadingBibcodes={downloadingBibcodes}
|
||
handleDownload={handleDownload}
|
||
selectedPaper={selectedPaper}
|
||
setSelectedPaper={setSelectedPaper}
|
||
openReader={openReader}
|
||
setActiveTab={setActiveTab}
|
||
loadCitations={loadCitations}
|
||
showAlert={showAlert}
|
||
onShowDetail={(paper) => setDetailBibcode(paper.bibcode)}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'library' && (
|
||
<LibraryPanel
|
||
library={library}
|
||
fetchLibrary={fetchLibrary}
|
||
setActiveTab={setActiveTab}
|
||
onShowDetail={(paper) => setDetailBibcode(paper.bibcode)}
|
||
onOpenReader={openReader}
|
||
onOpenCitation={openCitation}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'reader' && (
|
||
selectedPaper ? (
|
||
<ReaderPanel
|
||
selectedPaper={selectedPaper}
|
||
library={library}
|
||
recentlySelected={recentlySelected}
|
||
onSwitchPaper={openReader}
|
||
parsing={parsing}
|
||
handleParse={handleParse}
|
||
translating={translating}
|
||
handleTranslate={handleTranslate}
|
||
vectorizing={vectorizing}
|
||
handleVectorize={handleVectorize}
|
||
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}
|
||
onJumpToSource={handleJumpToSource}
|
||
/>
|
||
) : (
|
||
<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' && (
|
||
selectedPaper ? (
|
||
<CitationPanel
|
||
selectedPaper={selectedPaper}
|
||
library={library}
|
||
recentlySelected={recentlySelected}
|
||
onSwitchPaper={openCitation}
|
||
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' && (
|
||
<SyncPanel />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
{/* 全局统一样式弹窗 */}
|
||
{dialog && (
|
||
<div
|
||
onClick={() => {
|
||
if (dialog.onCancel) dialog.onCancel();
|
||
setDialog(null);
|
||
}}
|
||
className="fixed inset-0 z-[60] flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all"
|
||
>
|
||
<div
|
||
onClick={(e) => e.stopPropagation()}
|
||
className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4"
|
||
>
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-2.5">
|
||
<h3 className="text-xs font-bold text-slate-900 flex items-center gap-1.5">
|
||
<span className={`w-2 h-2 rounded-full ${dialog.type === 'confirm' ? 'bg-sky-500' : 'bg-red-500'} animate-pulse`} />
|
||
<span>{dialog.title}</span>
|
||
</h3>
|
||
<button
|
||
onClick={() => {
|
||
if (dialog.onCancel) dialog.onCancel();
|
||
setDialog(null);
|
||
}}
|
||
className="text-slate-400 hover:text-slate-600 p-1 rounded-lg hover:bg-slate-100 transition-colors cursor-pointer"
|
||
title="关闭"
|
||
>
|
||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<p className="text-xs text-slate-705 leading-relaxed">{dialog.message}</p>
|
||
</div>
|
||
<div className="flex gap-2 pt-2">
|
||
<button
|
||
onClick={() => {
|
||
dialog.onConfirm();
|
||
setDialog(null);
|
||
}}
|
||
className="flex-1 btn-console btn-console-primary py-2 rounded-lg text-[11px] font-bold text-center cursor-pointer"
|
||
>
|
||
确定
|
||
</button>
|
||
{dialog.type === 'confirm' && (
|
||
<button
|
||
onClick={() => {
|
||
if (dialog.onCancel) dialog.onCancel();
|
||
setDialog(null);
|
||
}}
|
||
className="px-4 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg text-[11px] font-bold text-center transition-all cursor-pointer"
|
||
>
|
||
取消
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 未入库文献操作弹窗 */}
|
||
{uncachedBibcode && (
|
||
<div
|
||
onClick={() => setUncachedBibcode(null)}
|
||
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in"
|
||
>
|
||
<div
|
||
onClick={(e) => e.stopPropagation()}
|
||
className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4"
|
||
>
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-2.5">
|
||
<h3 className="text-xs font-bold text-slate-900 flex items-center gap-1.5">
|
||
<span className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" />
|
||
<span>文献尚未入库</span>
|
||
</h3>
|
||
<button
|
||
onClick={() => setUncachedBibcode(null)}
|
||
className="text-slate-400 hover:text-slate-600 p-1 rounded-lg hover:bg-slate-100 transition-colors cursor-pointer"
|
||
title="关闭"
|
||
>
|
||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<p className="text-xs text-slate-700 leading-relaxed">
|
||
文献 <span className="font-mono bg-slate-100 px-1.5 py-0.5 rounded text-sky-700 font-bold select-all">{uncachedBibcode}</span> 尚未收录在本地数据库中。
|
||
</p>
|
||
<p className="text-[11px] text-slate-400 leading-relaxed">
|
||
您可以选择在线拉取该文献元数据并入库,或是直接跳转至 NASA ADS 平台查看其原始页面。
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2 pt-2">
|
||
<button
|
||
onClick={() => {
|
||
loadCitations(uncachedBibcode, false);
|
||
setUncachedBibcode(null);
|
||
}}
|
||
className="flex-1 btn-console btn-console-primary py-2 rounded-lg text-[11px] font-bold text-center cursor-pointer"
|
||
>
|
||
拉取入库并展开
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
axios.post('/api/active_bibcode', { bibcode: uncachedBibcode }).catch(() => {});
|
||
window.open(`https://ui.adsabs.harvard.edu/abs/${uncachedBibcode}/abstract`, '_blank');
|
||
}}
|
||
className="flex-1 bg-white hover:bg-slate-50 text-slate-700 border border-slate-250 py-2 rounded-lg text-[11px] font-bold text-center transition-all shadow-sm cursor-pointer"
|
||
>
|
||
跳转到 ADS
|
||
</button>
|
||
<button
|
||
onClick={() => setUncachedBibcode(null)}
|
||
className="px-3 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg text-xs font-medium text-center transition-all cursor-pointer"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 文献详情弹窗 */}
|
||
{detailPaper && (
|
||
<div
|
||
onClick={() => setDetailBibcode(null)}
|
||
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in cursor-pointer"
|
||
>
|
||
<div
|
||
onClick={(e) => e.stopPropagation()}
|
||
className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-lg w-full p-6 space-y-4 cursor-default animate-fade-in"
|
||
>
|
||
{/* 标题 & 关闭 */}
|
||
<div className="flex items-start justify-between border-b border-slate-100 pb-3">
|
||
<div className="space-y-1 pr-4">
|
||
<span className="text-[10px] font-bold text-sky-700 uppercase tracking-wider">文献详情元数据</span>
|
||
<h3 className="text-xs font-bold text-slate-900 leading-snug">
|
||
{getDoctypeBadge(detailPaper.doctype)}
|
||
<span className="align-middle">{detailPaper.title}</span>
|
||
</h3>
|
||
</div>
|
||
<button
|
||
onClick={() => setDetailBibcode(null)}
|
||
className="text-slate-400 hover:text-slate-655 p-1 rounded-lg hover:bg-slate-100 transition-colors shrink-0 cursor-pointer"
|
||
title="关闭"
|
||
>
|
||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* 详情内容 */}
|
||
<div className="space-y-4 h-[460px] flex flex-col overflow-y-auto pr-1 text-xs scrollbar-thin">
|
||
{/* 作者 */}
|
||
<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-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-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 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 flex-1 rounded-lg border border-slate-200 text-justify overflow-y-auto scrollbar-thin select-text">
|
||
{detailPaper.abstract_text || '该文献暂无摘要数据。'}
|
||
</p>
|
||
</div>
|
||
|
||
{/* 关键字 */}
|
||
<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 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 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}>
|
||
{detailPaper.bibcode === detailPaper.arxiv_id ? '暂无' : (
|
||
<a
|
||
href={`https://ui.adsabs.harvard.edu/abs/${detailPaper.bibcode}/abstract`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
onClick={() => axios.post('/api/active_bibcode', { bibcode: detailPaper.bibcode }).catch(() => {})}
|
||
className="hover:underline text-sky-600"
|
||
>
|
||
{detailPaper.bibcode}
|
||
</a>
|
||
)}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-50 px-2.5 py-1.5 rounded border border-slate-150">
|
||
<span className="text-slate-400 font-bold block">DOI</span>
|
||
<span className="text-slate-700 font-semibold select-all truncate block" title={detailPaper.doi || '无'}>
|
||
{detailPaper.doi ? (
|
||
<a
|
||
href={`https://doi.org/${detailPaper.doi}`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
onClick={() => axios.post('/api/active_bibcode', { bibcode: detailPaper.bibcode }).catch(() => {})}
|
||
className="hover:underline text-sky-600"
|
||
>
|
||
{detailPaper.doi}
|
||
</a>
|
||
) : '无'}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-50 px-2.5 py-1.5 rounded border border-slate-150">
|
||
<span className="text-slate-450 font-bold block">ARXIV ID</span>
|
||
<span className="text-slate-700 font-semibold select-all truncate block" title={detailPaper.arxiv_id || '无'}>
|
||
{detailPaper.arxiv_id ? (
|
||
<a
|
||
href={`https://arxiv.org/abs/${detailPaper.arxiv_id}`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
onClick={() => axios.post('/api/active_bibcode', { bibcode: detailPaper.bibcode }).catch(() => {})}
|
||
className="hover:underline text-sky-600"
|
||
>
|
||
{detailPaper.arxiv_id}
|
||
</a>
|
||
) : '无'}
|
||
</span>
|
||
</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.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'
|
||
: 'bg-rose-50 border border-rose-150 text-rose-850'
|
||
}`}>
|
||
{detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource' ? (
|
||
<div className="leading-relaxed">
|
||
<div className="font-bold flex items-center gap-1.5 text-amber-900 text-xs mb-1">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse" />
|
||
已手动标记为【无有效全文资源】
|
||
</div>
|
||
该文献已从物理文献下载重试队列中排除,后续批量下载时系统将自动跳过此文献。
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="font-bold flex items-center gap-1.5 text-rose-900">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse" />
|
||
自动下载失败诊断信息:
|
||
</div>
|
||
{detailPaper.pdf_error && (
|
||
<div className="leading-relaxed">
|
||
<span className="font-semibold text-rose-750">PDF 错误: </span>
|
||
{detailPaper.pdf_error}
|
||
</div>
|
||
)}
|
||
{detailPaper.html_error && (
|
||
<div className="leading-relaxed mt-0.5">
|
||
<span className="font-semibold text-rose-750">HTML 错误: </span>
|
||
{detailPaper.html_error}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 底部操作:整合所有动作(阅读、图谱、下载) */}
|
||
<div className="flex flex-wrap gap-2 pt-3 border-t border-slate-100">
|
||
{detailPaper.is_downloaded ? (
|
||
<>
|
||
<button
|
||
onClick={() => {
|
||
setDetailBibcode(null);
|
||
openReader(detailPaper);
|
||
}}
|
||
className="flex-1 btn-console btn-console-primary py-2.5 rounded-lg text-xs font-bold text-center cursor-pointer"
|
||
>
|
||
打开阅读器
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setDetailBibcode(null);
|
||
setSelectedPaper(detailPaper);
|
||
setActiveTab('citation');
|
||
loadCitations(detailPaper.bibcode);
|
||
}}
|
||
className="flex-1 bg-white hover:bg-slate-50 text-slate-700 border border-slate-250 py-2.5 rounded-lg text-xs font-bold text-center transition-all cursor-pointer shadow-sm"
|
||
>
|
||
查看引用图谱
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
showConfirm('确定要强制重新下载吗?这会覆盖本地文件。', () => {
|
||
handleDownload(detailPaper.bibcode, true);
|
||
}, '确认重新下载');
|
||
}}
|
||
disabled={downloadingBibcodes[detailPaper.bibcode]}
|
||
className="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 text-amber-700 rounded-lg text-xs font-bold transition-all cursor-pointer disabled:opacity-50"
|
||
>
|
||
{downloadingBibcodes[detailPaper.bibcode] ? '重下中...' : '重新下载'}
|
||
</button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<button
|
||
onClick={() => {
|
||
handleDownload(detailPaper.bibcode);
|
||
}}
|
||
disabled={downloadingBibcodes[detailPaper.bibcode]}
|
||
className="flex-1 btn-console btn-console-primary py-2.5 rounded-lg text-xs font-bold flex items-center justify-center gap-1.5 cursor-pointer disabled:opacity-50"
|
||
>
|
||
{downloadingBibcodes[detailPaper.bibcode] ? (
|
||
<>
|
||
<Loader className="w-3.5 h-3.5 animate-spin" />
|
||
<span>正在下载同步...</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Download className="w-3.5 h-3.5" />
|
||
<span>下载文献到本地馆藏</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setDetailBibcode(null);
|
||
setSelectedPaper(detailPaper);
|
||
setActiveTab('citation');
|
||
loadCitations(detailPaper.bibcode);
|
||
}}
|
||
className="px-6 bg-white hover:bg-slate-50 text-slate-700 border border-slate-250 py-2.5 rounded-lg text-xs font-bold text-center transition-all cursor-pointer shadow-sm"
|
||
>
|
||
引用图谱
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|