feat: 初始化 AstroResearch 核心系统代码及重构技术文档

This commit is contained in:
fmq
2026-06-08 17:23:27 +08:00
commit 307a1c0cee
53 changed files with 45076 additions and 0 deletions
@@ -0,0 +1,272 @@
// dashboard/src/components/CitationGalaxyCanvas.tsx
import { useEffect, useRef } from 'react';
import type { CitationNetwork } from '../types';
interface CanvasProps {
networks: CitationNetwork[];
activeNetwork: CitationNetwork;
onNodeClick: (bibcode: string) => void;
}
interface Node {
id: string;
label: string;
x: number;
y: number;
vx: number;
vy: number;
radius: number;
color: string;
type: 'center' | 'reference' | 'citation';
}
interface Link {
source: string;
target: string;
}
export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: CanvasProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
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 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);
nodes.push({
id: net.bibcode,
label: net.bibcode,
x: rect.width / 2 + (netIdx === 0 ? 0 : (Math.random() - 0.5) * 200),
y: rect.height / 2 + (netIdx === 0 ? 0 : (Math.random() - 0.5) * 200),
vx: 0,
vy: 0,
radius: isActive ? 24 : 16,
color: isActive ? '#a855f7' : '#6366f1',
type: 'center',
});
}
// 添加参考文献节点
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 centerNode = nodes.find(n => n.id === net.bibcode);
nodes.push({
id: ref,
label: ref,
x: (centerNode?.x ?? rect.width / 2) + Math.cos(angle) * dist,
y: (centerNode?.y ?? rect.height / 2) + Math.sin(angle) * dist,
vx: 0,
vy: 0,
radius: 12,
color: '#d97706',
type: 'reference',
});
}
if (allIds.has(ref)) {
links.push({ source: ref, target: net.bibcode });
}
});
// 添加被引文献节点
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 centerNode = nodes.find(n => n.id === net.bibcode);
nodes.push({
id: cit,
label: cit,
x: (centerNode?.x ?? rect.width / 2) + Math.cos(angle) * dist,
y: (centerNode?.y ?? rect.height / 2) + Math.sin(angle) * dist,
vx: 0,
vy: 0,
radius: 12,
color: '#4f46e5',
type: 'citation',
});
}
if (allIds.has(cit)) {
links.push({ source: net.bibcode, target: cit });
}
});
});
let animationFrameId: number;
let hoveredNode: Node | null = null;
// 经典力导向算法迭代
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;
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;
}
if (nodes[j].type !== 'center' || nodes[j].id !== activeNetwork.bibcode) {
nodes[j].vx += fx;
nodes[j].vy += fy;
}
}
}
}
// 2. 引力与向心力:被连线连接的节点之间产生向中心靠拢力
links.forEach(link => {
const sourceNode = nodes.find(n => n.id === link.source);
const targetNode = nodes.find(n => n.id === link.target);
if (sourceNode && targetNode) {
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 fx = (dx / dist) * force;
let fy = (dy / dist) * force;
if (sourceNode.type !== 'center' || sourceNode.id !== activeNetwork.bibcode) {
sourceNode.vx += fx;
sourceNode.vy += fy;
}
if (targetNode.type !== 'center' || targetNode.id !== activeNetwork.bibcode) {
targetNode.vx -= fx;
targetNode.vy -= fy;
}
}
});
// 3. 应用阻尼阻力,限制极限加速
nodes.forEach(node => {
if (node.id !== activeNetwork.bibcode) {
node.x += node.vx;
node.y += node.vy;
node.vx *= 0.85; // 阻尼
node.vy *= 0.85;
}
});
};
// 画布渲染渲染循环
const render = () => {
updatePhysics();
ctx.clearRect(0, 0, rect.width, rect.height);
// 绘制连线
ctx.lineWidth = 1;
links.forEach(link => {
const sourceNode = nodes.find(n => n.id === link.source);
const targetNode = nodes.find(n => n.id === link.target);
if (sourceNode && targetNode) {
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.stroke();
}
});
// 绘制节点
nodes.forEach(node => {
const isHovered = hoveredNode?.id === node.id;
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius + (isHovered ? 4 : 0), 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.stroke();
// 绘制 bibcode 文本说明
ctx.fillStyle = isHovered ? '#0f172a' : '#64748b';
ctx.font = isHovered ? 'bold 10px monospace' : '9px monospace';
ctx.textAlign = 'center';
ctx.fillText(node.label, node.x, node.y + node.radius + (isHovered ? 18 : 14));
});
animationFrameId = requestAnimationFrame(render);
};
render();
// 交互鼠标监听
const handleMouseMove = (e: MouseEvent) => {
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
let found: Node | null = null;
for (const node of nodes) {
let dx = node.x - mouseX;
let dy = node.y - mouseY;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist < node.radius + 5) {
found = node;
break;
}
}
hoveredNode = found;
canvas.style.cursor = found ? 'pointer' : 'default';
};
const handleCanvasClick = () => {
if (hoveredNode && hoveredNode.id !== activeNetwork.bibcode) {
onNodeClick(hoveredNode.id);
}
};
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('click', handleCanvasClick);
return () => {
cancelAnimationFrame(animationFrameId);
canvas.removeEventListener('mousemove', handleMouseMove);
canvas.removeEventListener('click', handleCanvasClick);
};
}, [networks, activeNetwork, onNodeClick]);
return (
<canvas
ref={canvasRef}
className="w-full h-full min-h-[450px]"
style={{ display: 'block' }}
/>
);
}
@@ -0,0 +1,77 @@
// dashboard/src/components/layout/Sidebar.tsx
import { Search, BookOpen, GitFork, Library, Settings } from 'lucide-react';
import type { StandardPaper } from '../../types';
interface SidebarProps {
activeTab: 'search' | 'library' | 'reader' | 'citation' | 'settings';
setActiveTab: (tab: 'search' | 'library' | 'reader' | 'citation' | 'settings') => void;
selectedPaper: StandardPaper | null;
loadCitations: (bibcode: string) => void;
}
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">
<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>
</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>
</div>
</div>
{/* 选项卡导航 */}
<nav className="px-4 space-y-1.5">
{[
{ id: 'search', label: '统一检索', icon: Search },
{ id: 'library', label: '馆藏管理', icon: Library },
{ id: 'reader', label: '双语阅读', icon: BookOpen, disabled: !selectedPaper },
{ id: 'citation', label: '引用星系', icon: GitFork, disabled: !selectedPaper },
{ id: 'settings', label: '系统设置', icon: Settings },
].map(tab => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
disabled={tab.disabled}
onClick={() => {
setActiveTab(tab.id as any);
if (tab.id === 'citation' && selectedPaper) {
loadCitations(selectedPaper.bibcode);
}
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all ${
isActive
? 'bg-gradient-to-r from-purple-600/10 to-indigo-600/10 text-purple-600 border border-purple-500/20'
: tab.disabled
? 'opacity-40 cursor-not-allowed text-slate-400'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<Icon className="w-4 h-4" />
{tab.label}
</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>
</div>
</div>
)}
</aside>
);
}