feat: 重构 PDF/文献检索同步机制、升级引力图交互与控制台 UI 样式
- [后端/PDF解析] 重构 MinerU PDF 解析流程:引入预签名两阶段直传机制,解决大文件 API 传输限制问题;支持轮询机制与本地 images 备用目录存储。 - [后端/同步与下载] 新增经典 ADS SCAN 扫描件 PDF 和 ADS_PDF 直接通道的下载逻辑;新增常用同步检索配置的持久化存储与去重管理 API。 - [后端/日志] 重构日志系统,支持控制台 pretty 输出与每日滚动文件日志(使用上海 +08:00 时区),引入 HTTP 路由请求链路追踪。 - [前端/引力图] 升级引用星系图 canvas 交互:支持平移拖拽与滚轮缩放,添加引力圈轨道装饰及未导入文献的半透明视觉区分。 - [前端/控制台] 统一重构为扁平高对比度浅色纯中文控制台样式;重新设计文献详情弹窗与状态进度条。 - [数据库] 新增 papers 表的 doctype 字段及 sync_queries 检索配置表。
This commit is contained in:
@@ -5,6 +5,7 @@ import type { CitationNetwork } from '../types';
|
||||
interface CanvasProps {
|
||||
networks: CitationNetwork[];
|
||||
activeNetwork: CitationNetwork;
|
||||
nodeLimit: number;
|
||||
onNodeClick: (bibcode: string) => void;
|
||||
}
|
||||
|
||||
@@ -18,6 +19,7 @@ interface Node {
|
||||
radius: number;
|
||||
color: string;
|
||||
type: 'center' | 'reference' | 'citation';
|
||||
inDb: boolean;
|
||||
}
|
||||
|
||||
interface Link {
|
||||
@@ -25,7 +27,7 @@ interface Link {
|
||||
target: string;
|
||||
}
|
||||
|
||||
export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: CanvasProps) {
|
||||
export function CitationGalaxyCanvas({ networks, activeNetwork, nodeLimit, onNodeClick }: CanvasProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,24 +36,26 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 适配高清屏幕像素比
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
// 合并所有 networks 的节点,去重,最多 50 个
|
||||
const MAX_NODES = 50;
|
||||
const MAX_NODES = nodeLimit;
|
||||
const allIds = new Set<string>();
|
||||
const nodes: Node[] = [];
|
||||
const links: Link[] = [];
|
||||
|
||||
networks.forEach((net, netIdx) => {
|
||||
const isActive = net.bibcode === activeNetwork.bibcode;
|
||||
// 添加中心节点
|
||||
if (!allIds.has(net.bibcode) && nodes.length < MAX_NODES) {
|
||||
allIds.add(net.bibcode);
|
||||
// 根据被引用数量决定中心节点大小 (起步半径 16,按被引量上限 32 比例缩放)
|
||||
const citeCount = net.citation_count || 0;
|
||||
const radius = isActive
|
||||
? Math.min(32, Math.max(18, 16 + citeCount / 50))
|
||||
: Math.min(24, Math.max(12, 11 + citeCount / 100));
|
||||
nodes.push({
|
||||
id: net.bibcode,
|
||||
label: net.bibcode,
|
||||
@@ -59,20 +63,21 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
y: rect.height / 2 + (netIdx === 0 ? 0 : (Math.random() - 0.5) * 200),
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
radius: isActive ? 24 : 16,
|
||||
color: isActive ? '#a855f7' : '#6366f1',
|
||||
radius,
|
||||
color: isActive ? '#0284c7' : '#475569',
|
||||
type: 'center',
|
||||
inDb: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加参考文献节点
|
||||
net.references.forEach((ref, idx) => {
|
||||
if (nodes.length >= MAX_NODES) return;
|
||||
if (!allIds.has(ref)) {
|
||||
allIds.add(ref);
|
||||
const angle = (idx / Math.max(1, net.references.length)) * Math.PI * 2;
|
||||
const dist = 140 + Math.random() * 30;
|
||||
const dist = 120 + Math.random() * 30;
|
||||
const centerNode = nodes.find(n => n.id === net.bibcode);
|
||||
const inDb = activeNetwork.citation_counts ? Object.prototype.hasOwnProperty.call(activeNetwork.citation_counts, ref) : false;
|
||||
nodes.push({
|
||||
id: ref,
|
||||
label: ref,
|
||||
@@ -80,9 +85,10 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
y: (centerNode?.y ?? rect.height / 2) + Math.sin(angle) * dist,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
radius: 12,
|
||||
radius: 8, // 初始值,稍后按连线数重算
|
||||
color: '#d97706',
|
||||
type: 'reference',
|
||||
inDb,
|
||||
});
|
||||
}
|
||||
if (allIds.has(ref)) {
|
||||
@@ -90,14 +96,14 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
}
|
||||
});
|
||||
|
||||
// 添加被引文献节点
|
||||
net.citations.forEach((cit, idx) => {
|
||||
if (nodes.length >= MAX_NODES) return;
|
||||
if (!allIds.has(cit)) {
|
||||
allIds.add(cit);
|
||||
const angle = (idx / Math.max(1, net.citations.length)) * Math.PI * 2 + Math.PI;
|
||||
const dist = 160 + Math.random() * 40;
|
||||
const dist = 140 + Math.random() * 40;
|
||||
const centerNode = nodes.find(n => n.id === net.bibcode);
|
||||
const inDb = activeNetwork.citation_counts ? Object.prototype.hasOwnProperty.call(activeNetwork.citation_counts, cit) : false;
|
||||
nodes.push({
|
||||
id: cit,
|
||||
label: cit,
|
||||
@@ -105,9 +111,10 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
y: (centerNode?.y ?? rect.height / 2) + Math.sin(angle) * dist,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
radius: 12,
|
||||
color: '#4f46e5',
|
||||
radius: 8, // 初始值,稍后按连线数重算
|
||||
color: '#0891b2',
|
||||
type: 'citation',
|
||||
inDb,
|
||||
});
|
||||
}
|
||||
if (allIds.has(cit)) {
|
||||
@@ -116,24 +123,46 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
});
|
||||
});
|
||||
|
||||
// 计算外围节点在当前渲染网络中的度数(连线数),动态微调其半径大小,凸显网络枢纽节点
|
||||
const degrees: Record<string, number> = {};
|
||||
links.forEach(l => {
|
||||
degrees[l.source] = (degrees[l.source] || 0) + 1;
|
||||
degrees[l.target] = (degrees[l.target] || 0) + 1;
|
||||
});
|
||||
|
||||
nodes.forEach(n => {
|
||||
if (n.type !== 'center') {
|
||||
const deg = degrees[n.id] || 0;
|
||||
const citeCount = activeNetwork.citation_counts?.[n.id] || 0;
|
||||
// 融合数据库中该文献的被引数量 (起步+citeCount/40) 与当前渲染网格连线度数,动态决定半径 (最大限制 18)
|
||||
n.radius = Math.min(18, Math.max(6, 6 + citeCount / 40 + Math.min(6, deg * 1.5)));
|
||||
}
|
||||
});
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let scale = 1.0;
|
||||
let isDragging = false;
|
||||
let dragStartX = 0;
|
||||
let dragStartY = 0;
|
||||
let hasDragged = false;
|
||||
|
||||
let animationFrameId: number;
|
||||
let hoveredNode: Node | null = null;
|
||||
let frameCount = 0;
|
||||
|
||||
// 经典力导向算法迭代
|
||||
const updatePhysics = () => {
|
||||
// 1. 斥力:任何两个节点之间均产生反向推力
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
let dx = nodes[j].x - nodes[i].x;
|
||||
let dy = nodes[j].y - nodes[i].y;
|
||||
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
let minDist = nodes[i].radius + nodes[j].radius + 50;
|
||||
let minDist = nodes[i].radius + nodes[j].radius + 60;
|
||||
if (dist < minDist) {
|
||||
let force = (minDist - dist) * 0.08;
|
||||
let fx = (dx / dist) * force;
|
||||
let fy = (dy / dist) * force;
|
||||
|
||||
// 节点不强行推动中心大节点
|
||||
if (nodes[i].type !== 'center' || nodes[i].id !== activeNetwork.bibcode) {
|
||||
nodes[i].vx -= fx;
|
||||
nodes[i].vy -= fy;
|
||||
@@ -146,7 +175,6 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 引力与向心力:被连线连接的节点之间产生向中心靠拢力
|
||||
links.forEach(link => {
|
||||
const sourceNode = nodes.find(n => n.id === link.source);
|
||||
const targetNode = nodes.find(n => n.id === link.target);
|
||||
@@ -154,7 +182,7 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
let dx = targetNode.x - sourceNode.x;
|
||||
let dy = targetNode.y - sourceNode.y;
|
||||
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
let force = dist * 0.003; // 弹性系数
|
||||
let force = dist * 0.003;
|
||||
let fx = (dx / dist) * force;
|
||||
let fy = (dy / dist) * force;
|
||||
|
||||
@@ -169,23 +197,54 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 应用阻尼阻力,限制极限加速
|
||||
nodes.forEach(node => {
|
||||
if (node.id !== activeNetwork.bibcode) {
|
||||
node.x += node.vx;
|
||||
node.y += node.vy;
|
||||
node.vx *= 0.85; // 阻尼
|
||||
node.vx *= 0.85;
|
||||
node.vy *= 0.85;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 画布渲染渲染循环
|
||||
const render = () => {
|
||||
frameCount++;
|
||||
updatePhysics();
|
||||
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
|
||||
ctx.save();
|
||||
// 应用拖拽和缩放的坐标变换
|
||||
const cx = rect.width / 2;
|
||||
const cy = rect.height / 2;
|
||||
ctx.translate(cx + offsetX, cy + offsetY);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.translate(-cx, -cy);
|
||||
|
||||
// 绘制背景宇宙引力线 & 刻度圈
|
||||
const centerNode = nodes.find(n => n.id === activeNetwork.bibcode);
|
||||
if (centerNode) {
|
||||
ctx.strokeStyle = 'rgba(148, 163, 184, 0.15)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 6]);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerNode.x, centerNode.y, 130, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerNode.x, centerNode.y, 180, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// 动态圈
|
||||
ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)';
|
||||
ctx.setLineDash([]);
|
||||
const pulseRadius = 130 + (frameCount % 120) * 0.4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerNode.x, centerNode.y, pulseRadius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 绘制连线
|
||||
ctx.lineWidth = 1;
|
||||
links.forEach(link => {
|
||||
@@ -195,7 +254,7 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(sourceNode.x, sourceNode.y);
|
||||
ctx.lineTo(targetNode.x, targetNode.y);
|
||||
ctx.strokeStyle = sourceNode.type === 'reference' ? 'rgba(245, 158, 11, 0.25)' : 'rgba(129, 140, 248, 0.25)';
|
||||
ctx.strokeStyle = 'rgba(148, 163, 184, 0.25)';
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
@@ -203,62 +262,137 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C
|
||||
// 绘制节点
|
||||
nodes.forEach(node => {
|
||||
const isHovered = hoveredNode?.id === node.id;
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = node.inDb ? 1.0 : 0.35; // 未入库文献透明度降为 0.35
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.radius + (isHovered ? 4 : 0), 0, Math.PI * 2);
|
||||
ctx.arc(node.x, node.y, node.radius + (isHovered ? 6 : 3), 0, Math.PI * 2);
|
||||
ctx.fillStyle = node.color + (isHovered ? '25' : '0f');
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = node.color;
|
||||
ctx.fill();
|
||||
|
||||
// 绘制光晕环绕
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.radius + (isHovered ? 8 : 4), 0, Math.PI * 2);
|
||||
ctx.strokeStyle = node.color + '40'; // 附加透明度光晕
|
||||
ctx.lineWidth = 2;
|
||||
ctx.arc(node.x, node.y, node.radius + (isHovered ? 4 : 2), 0, Math.PI * 2);
|
||||
ctx.strokeStyle = node.color + '40';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制 bibcode 文本说明
|
||||
ctx.fillStyle = isHovered ? '#0f172a' : '#64748b';
|
||||
ctx.font = isHovered ? 'bold 10px monospace' : '9px monospace';
|
||||
ctx.fillStyle = isHovered ? '#0284c7' : '#334155';
|
||||
ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(node.label, node.x, node.y + node.radius + (isHovered ? 18 : 14));
|
||||
ctx.fillText(node.label, node.x, node.y + node.radius + (isHovered ? 16 : 12));
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
|
||||
animationFrameId = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
// 交互鼠标监听
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
isDragging = true;
|
||||
dragStartX = e.clientX - offsetX;
|
||||
dragStartY = e.clientY - offsetY;
|
||||
hasDragged = false;
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
if (isDragging) {
|
||||
const dx = e.clientX - dragStartX;
|
||||
const dy = e.clientY - dragStartY;
|
||||
if (Math.sqrt((dx - offsetX) ** 2 + (dy - offsetY) ** 2) > 3) {
|
||||
hasDragged = true;
|
||||
}
|
||||
offsetX = dx;
|
||||
offsetY = dy;
|
||||
}
|
||||
|
||||
const cx = rect.width / 2;
|
||||
const cy = rect.height / 2;
|
||||
const gx = (mouseX - cx - offsetX) / scale + cx;
|
||||
const gy = (mouseY - cy - offsetY) / scale + cy;
|
||||
|
||||
let found: Node | null = null;
|
||||
for (const node of nodes) {
|
||||
let dx = node.x - mouseX;
|
||||
let dy = node.y - mouseY;
|
||||
let dx = node.x - gx;
|
||||
let dy = node.y - gy;
|
||||
let dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < node.radius + 5) {
|
||||
if (dist < node.radius + 6) {
|
||||
found = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
hoveredNode = found;
|
||||
canvas.style.cursor = found ? 'pointer' : 'default';
|
||||
|
||||
if (isDragging) {
|
||||
canvas.style.cursor = 'grabbing';
|
||||
} else {
|
||||
canvas.style.cursor = found ? 'pointer' : 'grab';
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
const handleCanvasClick = () => {
|
||||
if (hasDragged) return;
|
||||
if (hoveredNode && hoveredNode.id !== activeNetwork.bibcode) {
|
||||
onNodeClick(hoveredNode.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
const cx = rect.width / 2;
|
||||
const cy = rect.height / 2;
|
||||
const gx = (mouseX - cx - offsetX) / scale + cx;
|
||||
const gy = (mouseY - cy - offsetY) / scale + cy;
|
||||
|
||||
if (e.deltaY < 0) {
|
||||
scale = Math.min(5.0, scale * 1.1);
|
||||
} else {
|
||||
scale = Math.max(0.15, scale / 1.1);
|
||||
}
|
||||
|
||||
offsetX = mouseX - cx - (gx - cx) * scale;
|
||||
offsetY = mouseY - cy - (gy - cy) * scale;
|
||||
};
|
||||
|
||||
canvas.addEventListener('mousedown', handleMouseDown);
|
||||
canvas.addEventListener('mousemove', handleMouseMove);
|
||||
canvas.addEventListener('mouseup', handleMouseUp);
|
||||
canvas.addEventListener('mouseleave', handleMouseLeave);
|
||||
canvas.addEventListener('click', handleCanvasClick);
|
||||
canvas.addEventListener('wheel', handleWheel, { passive: false });
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||
canvas.removeEventListener('mouseup', handleMouseUp);
|
||||
canvas.removeEventListener('mouseleave', handleMouseLeave);
|
||||
canvas.removeEventListener('click', handleCanvasClick);
|
||||
canvas.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, [networks, activeNetwork, onNodeClick]);
|
||||
|
||||
|
||||
@@ -11,25 +11,38 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations }: SidebarProps) {
|
||||
return (
|
||||
<aside className="w-64 glass border-r border-slate-200/80 flex flex-col justify-between py-6">
|
||||
<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">
|
||||
<div>
|
||||
{/* Logo */}
|
||||
<div className="px-6 mb-8 flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-purple-500/20">
|
||||
<span className="font-extrabold text-white text-lg tracking-wider">A</span>
|
||||
{/* 系统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-lg font-bold text-slate-800 leading-none font-outfit">AstroResearch</h1>
|
||||
<span className="text-xs text-slate-500">天文学科研辅助系统</span>
|
||||
<h1 className="text-sm font-bold text-slate-800 tracking-wider">AstroResearch</h1>
|
||||
<span className="text-[11px] text-slate-500 block font-medium">天文学科研辅助系统</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 选项卡导航 */}
|
||||
<nav className="px-4 space-y-1.5">
|
||||
{/* 导航菜单列表 */}
|
||||
<nav className="space-y-1">
|
||||
{[
|
||||
{ id: 'search', label: '统一检索', icon: Search },
|
||||
{ id: 'library', label: '馆藏管理', icon: Library },
|
||||
{ id: 'sync', label: '批量同步', icon: RefreshCw },
|
||||
{ id: 'sync', label: '批量任务', icon: RefreshCw },
|
||||
{ id: 'reader', label: '双语阅读', icon: BookOpen, disabled: !selectedPaper },
|
||||
{ id: 'citation', label: '引用星系', icon: GitFork, disabled: !selectedPaper },
|
||||
].map(tab => {
|
||||
@@ -45,32 +58,36 @@ export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations
|
||||
loadCitations(selectedPaper.bibcode);
|
||||
}
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all ${
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-xs font-semibold tracking-wider transition-all border ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-purple-600/10 to-indigo-600/10 text-purple-600 border border-purple-500/20'
|
||||
? 'bg-sky-50 border-sky-200 text-sky-700 shadow-sm'
|
||||
: tab.disabled
|
||||
? 'opacity-40 cursor-not-allowed text-slate-400'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
? 'opacity-30 cursor-not-allowed border-transparent text-slate-400'
|
||||
: 'border-transparent text-slate-650 hover:bg-slate-100 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
<Icon className={`w-4 h-4 ${isActive ? 'text-sky-600' : 'text-slate-500'}`} />
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 底部当前文献卡片 */}
|
||||
{selectedPaper && (
|
||||
<div className="mx-4 p-4 rounded-xl bg-slate-100/50 border border-slate-200/80">
|
||||
<span className="text-[10px] text-purple-600 font-bold uppercase tracking-wider block mb-1">当前选定文献</span>
|
||||
<h4 className="text-xs text-slate-800 font-medium line-clamp-2 mb-2">{selectedPaper.title}</h4>
|
||||
<div className="flex items-center justify-between text-[10px] text-slate-500">
|
||||
<span>{selectedPaper.year}</span>
|
||||
<span className="truncate max-w-[100px] text-slate-400">{selectedPaper.bibcode}</span>
|
||||
{/* 底部当前选定文献提示 */}
|
||||
{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>
|
||||
</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>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user