// dashboard/src/hooks/useLibrary.ts import { useState, useEffect } from 'react'; import axios from 'axios'; import type { StandardPaper } from '../types'; interface UseLibraryProps { isAuthenticated: boolean | null; showAlert: (message: string, title?: string) => void; showConfirm: (message: string, onConfirm: () => void, title?: string) => void; openReader: (paper: StandardPaper, skipTabSwitch?: boolean) => void; } export function useLibrary({ isAuthenticated, showAlert, showConfirm, openReader, }: UseLibraryProps) { const [library, setLibrary] = useState([]); const [selectedPaper, setSelectedPaper] = useState(null); const [detailBibcode, setDetailBibcode] = useState(null); const [recentlySelected, setRecentlySelected] = useState(() => { 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 []; } }); const [downloadingBibcodes, setDownloadingBibcodes] = useState>({}); const [uploadingBibcode, setUploadingBibcode] = useState(null); 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, setActiveTab: (tab: any) => void, loadCitations: (bibcode: string, reset?: boolean) => void ) => { setSelectedPaper(paper); setActiveTab('citation'); loadCitations(paper.bibcode, true); addPaperToRecent(paper); }; const fetchLibrary = async () => { try { const res = await axios.get('/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); } }; // 初始化加载本地文献库 useEffect(() => { if (isAuthenticated === true) { fetchLibrary(); } }, [isAuthenticated]); // 触发文献双格式下载 const handleDownload = async (bibcode: string, force = false, searchResultsSetter?: React.Dispatch>) => { setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: true })); try { const res = await axios.post('/api/download', { bibcode, force }); if (searchResultsSetter) { searchResultsSetter(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 })); } }; // 手动上传文献文件以绕过反爬/人机验证 const handleManualUpload = async (bibcode: string, type: 'pdf' | 'html', file: File, searchResultsSetter?: React.Dispatch>) => { setUploadingBibcode(bibcode); const formData = new FormData(); formData.append('bibcode', bibcode); formData.append('type', type); formData.append('file', file); try { const res = await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); if (searchResultsSetter) { searchResultsSetter(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); } }; // 手动标记文献为“无有效全文资源” const handleMarkNoResource = async (bibcode: string, clear = false, searchResultsSetter?: React.Dispatch>) => { const performMark = async () => { try { const res = await axios.post('/api/no_resource', { bibcode, clear }); if (searchResultsSetter) { searchResultsSetter(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, '标记无有效全文资源' ); } }; // 衍生状态计算 const getDetailPaper = (searchResults: StandardPaper[]) => { return library.find(p => p.bibcode === detailBibcode) || searchResults.find(p => p.bibcode === detailBibcode); }; return { library, setLibrary, selectedPaper, setSelectedPaper, detailBibcode, setDetailBibcode, recentlySelected, setRecentlySelected, downloadingBibcodes, setDownloadingBibcodes, uploadingBibcode, addPaperToRecent, openCitation, fetchLibrary, handleDownload, handleManualUpload, handleMarkNoResource, getDetailPaper, }; }