前端:
- 新增 PWA manifest、透传 Service Worker、iOS 全屏 meta 标签
- 响应式断点细化为手机(<768px)/平板(<1024px)/桌面三档
- ReaderToolbar 操作按钮在窄屏折叠为下拉菜单,按钮文案响应式精简
- BilingualViewer 手机端强制单列堆叠,100dvh + safe-area-inset-top 适配
- Bookmarklet 去硬编码 localhost,动态注入 origin + bookmarklet_key 完成认证
- 修复 useAutoScroll 改用容器级 scrollTo(scrollHeight)
- PaperCard 操作区 flex-wrap / SearchPanel 移动端垂直布局
后端:
- 登录返回 bookmarklet_key(SHA-1 哈希),auth_middleware 对书签导入路径额外放行 Bearer token
- CORS 白名单扩展到全部私有网段,SameSite cookie 改为 Lax 兼容局域网访问
- 新增 pdf_inline_middleware,PDF 文件强制内联显示防移动端下载
- 修复 meta.rs datetime 列别名缺失 / 登录错误兼容 {error} 对象格式
234 lines
7.8 KiB
TypeScript
234 lines
7.8 KiB
TypeScript
// dashboard/src/features/reader/ReaderPanel.tsx
|
|
import { useState, useEffect } from 'react';
|
|
import type { StandardPaper, NoteRecord } from '../types';
|
|
import { useReaderState } from '../hooks/useReaderState';
|
|
import { ReaderToolbar } from '../components/reader/ReaderToolbar';
|
|
import { BilingualViewer } from '../components/reader/BilingualViewer';
|
|
import { ReaderNotesSidebar } from '../components/reader/ReaderNotesSidebar';
|
|
|
|
interface ReaderPanelProps {
|
|
selectedPaper: StandardPaper;
|
|
library: StandardPaper[];
|
|
recentlySelected: StandardPaper[];
|
|
onSwitchPaper: (paper: StandardPaper) => void;
|
|
parsing: boolean;
|
|
handleParse: (bibcode: string, force?: boolean) => void;
|
|
translating: boolean;
|
|
handleTranslate: (bibcode: string, force?: boolean) => void;
|
|
vectorizing: boolean;
|
|
handleVectorize: (bibcode: string) => void;
|
|
showNotesPanel: boolean;
|
|
setShowNotesPanel: (show: boolean) => void;
|
|
notes: NoteRecord[];
|
|
englishText: string;
|
|
chineseText: string;
|
|
handleTextSelection: (paragraphIdx: number) => void;
|
|
selectedParagraphIdx: number | null;
|
|
setSelectedParagraphIdx: (idx: number | null) => void;
|
|
selectedText: string;
|
|
setSelectedText: (text: string) => void;
|
|
newNoteColor: string;
|
|
setNewNoteColor: (color: string) => void;
|
|
newNoteText: string;
|
|
setNewNoteText: (text: string) => void;
|
|
handleCreateNote: () => void;
|
|
handleDeleteNote: (id: number) => void;
|
|
showConfirm: (message: string, onConfirm: () => void, title?: string) => void;
|
|
onJumpToSource: (bibcode: string, paragraphIndex: number) => void;
|
|
}
|
|
|
|
export function ReaderPanel(props: ReaderPanelProps) {
|
|
const {
|
|
selectedPaper,
|
|
library,
|
|
recentlySelected,
|
|
onSwitchPaper,
|
|
parsing,
|
|
handleParse,
|
|
translating,
|
|
handleTranslate,
|
|
vectorizing,
|
|
handleVectorize,
|
|
showNotesPanel,
|
|
setShowNotesPanel,
|
|
notes,
|
|
englishText,
|
|
chineseText,
|
|
handleTextSelection,
|
|
selectedParagraphIdx,
|
|
setSelectedParagraphIdx,
|
|
selectedText,
|
|
setSelectedText,
|
|
newNoteColor,
|
|
setNewNoteColor,
|
|
newNoteText,
|
|
setNewNoteText,
|
|
handleCreateNote,
|
|
handleDeleteNote,
|
|
showConfirm,
|
|
onJumpToSource,
|
|
} = props;
|
|
|
|
// 局部管理阅读视角模式(原文/中文/对照)
|
|
const [viewMode, setViewMode] = useState<'bilingual' | 'english' | 'chinese'>(() => {
|
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
|
return chineseText ? 'chinese' : 'english';
|
|
}
|
|
if (!chineseText) {
|
|
return 'english';
|
|
}
|
|
return 'bilingual';
|
|
});
|
|
|
|
const [userOverrode, setUserOverrode] = useState(false);
|
|
|
|
// 监听窗口尺寸变化,处理移动端与桌面端的自适应视角切换
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
let prevWidth = window.innerWidth;
|
|
|
|
const handleResize = () => {
|
|
const currentWidth = window.innerWidth;
|
|
const wasMobile = prevWidth < 768;
|
|
const isMobile = currentWidth < 768;
|
|
|
|
if (wasMobile !== isMobile) {
|
|
if (isMobile) {
|
|
// 从宽屏缩放到手机端:若有翻译则默认显示中文译文,否则原文
|
|
setViewMode(chineseText ? 'chinese' : 'english');
|
|
} else {
|
|
// 从手机端拉宽到桌面端/平板:若有翻译则显示双语对照,否则显示原文
|
|
setViewMode(chineseText ? 'bilingual' : 'english');
|
|
}
|
|
}
|
|
prevWidth = currentWidth;
|
|
};
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, [chineseText]);
|
|
|
|
// 无翻译时强制回退到英文模式,有翻译时且在桌面端时自动切换到双语对照
|
|
useEffect(() => {
|
|
if (userOverrode) return;
|
|
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
|
|
if (isMobile) {
|
|
if (!chineseText && viewMode !== 'english') {
|
|
queueMicrotask(() => setViewMode('english'));
|
|
}
|
|
return;
|
|
}
|
|
if (!chineseText && viewMode === 'bilingual') {
|
|
queueMicrotask(() => setViewMode('english'));
|
|
} else if (chineseText && viewMode === 'english') {
|
|
queueMicrotask(() => setViewMode('bilingual'));
|
|
}
|
|
}, [chineseText, viewMode, userOverrode]);
|
|
|
|
const handleViewModeChange = (mode: 'bilingual' | 'english' | 'chinese') => {
|
|
setUserOverrode(true);
|
|
setViewMode(mode);
|
|
};
|
|
|
|
// 切换文献时重置用户手动覆盖标记
|
|
useEffect(() => {
|
|
queueMicrotask(() => setUserOverrode(false));
|
|
}, [selectedPaper?.bibcode]);
|
|
|
|
// 调用封装后的阅读器逻辑状态 Hook
|
|
const state = useReaderState({
|
|
selectedPaper,
|
|
englishText,
|
|
viewMode,
|
|
setViewMode: handleViewModeChange,
|
|
});
|
|
|
|
return (
|
|
<div className="w-full flex-1 flex flex-col space-y-4 min-h-0">
|
|
{/* 顶部控制栏 */}
|
|
<ReaderToolbar
|
|
selectedPaper={selectedPaper}
|
|
library={library}
|
|
recentlySelected={recentlySelected}
|
|
onSwitchPaper={onSwitchPaper}
|
|
parsing={parsing}
|
|
handleParse={handleParse}
|
|
translating={translating}
|
|
handleTranslate={handleTranslate}
|
|
vectorizing={vectorizing}
|
|
handleVectorize={handleVectorize}
|
|
showNotesPanel={showNotesPanel}
|
|
setShowNotesPanel={setShowNotesPanel}
|
|
notesCount={notes.length}
|
|
showConfirm={showConfirm}
|
|
englishText={englishText}
|
|
chineseText={chineseText}
|
|
showSwitchMenu={state.showSwitchMenu}
|
|
setShowSwitchMenu={state.setShowSwitchMenu}
|
|
viewMode={viewMode}
|
|
setViewMode={handleViewModeChange}
|
|
syncScroll={state.syncScroll}
|
|
setSyncScroll={state.setSyncScroll}
|
|
showPdf={state.showPdf}
|
|
/>
|
|
|
|
{/* 并排对比视窗与侧边栏 */}
|
|
<BilingualViewer
|
|
selectedPaper={selectedPaper}
|
|
parsing={parsing}
|
|
translating={translating}
|
|
englishText={englishText}
|
|
chineseText={chineseText}
|
|
notes={notes}
|
|
handleTextSelection={handleTextSelection}
|
|
showNotesPanel={showNotesPanel}
|
|
viewMode={viewMode}
|
|
showPdf={state.showPdf}
|
|
setShowPdf={state.setShowPdf}
|
|
englishRef={state.englishRef}
|
|
chineseRef={state.chineseRef}
|
|
handleEnglishScroll={state.handleEnglishScroll}
|
|
handleChineseScroll={state.handleChineseScroll}
|
|
handleMouseOver={state.handleMouseOver}
|
|
clearHoverTimeout={state.clearHoverTimeout}
|
|
handleContainerMouseLeave={state.handleContainerMouseLeave}
|
|
handleMouseLeaveTarget={state.handleMouseLeaveTarget}
|
|
highlightCandidates={state.highlightCandidates}
|
|
hoveredTarget={state.hoveredTarget}
|
|
hoverCardPos={state.hoverCardPos}
|
|
>
|
|
{/* 学术助手侧边栏 */}
|
|
<ReaderNotesSidebar
|
|
selectedPaper={selectedPaper}
|
|
showNotesPanel={showNotesPanel}
|
|
setShowNotesPanel={setShowNotesPanel}
|
|
notes={notes}
|
|
selectedParagraphIdx={selectedParagraphIdx}
|
|
setSelectedParagraphIdx={setSelectedParagraphIdx}
|
|
selectedText={selectedText}
|
|
setSelectedText={setSelectedText}
|
|
newNoteColor={newNoteColor}
|
|
setNewNoteColor={setNewNoteColor}
|
|
newNoteText={newNoteText}
|
|
setNewNoteText={setNewNoteText}
|
|
handleCreateNote={handleCreateNote}
|
|
handleDeleteNote={handleDeleteNote}
|
|
onJumpToSource={onJumpToSource}
|
|
sidebarTab={state.sidebarTab}
|
|
setSidebarTab={state.setSidebarTab}
|
|
targets={state.targets}
|
|
loadingTargets={state.loadingTargets}
|
|
identifyingTargets={state.identifyingTargets}
|
|
associatingTarget={state.associatingTarget}
|
|
manualTargetName={state.manualTargetName}
|
|
setManualTargetName={state.setManualTargetName}
|
|
handleIdentifyTargets={state.handleIdentifyTargets}
|
|
handleAssociateTarget={state.handleAssociateTarget}
|
|
handleJumpToTarget={state.handleJumpToTarget}
|
|
/>
|
|
</BilingualViewer>
|
|
</div>
|
|
);
|
|
}
|