({ initialFilters = {} }: GraphViewProps)
| 269 | } |
| 270 | |
| 271 | export function GraphView({ initialFilters = {} }: GraphViewProps) { |
| 272 | const t = useT(); |
| 273 | const router = useRouter(); |
| 274 | |
| 275 | const [filterType, setFilterType] = useState<string>(initialFilters.type || ""); |
| 276 | const [filterTag, setFilterTag] = useState<string>(initialFilters.tag || ""); |
| 277 | const [includeArchive, setIncludeArchive] = useState<boolean>(!!initialFilters.includeArchive); |
| 278 | const [theme, setTheme] = useState<"light" | "dark">("dark"); // 科技感默认深色 |
| 279 | |
| 280 | const [data, setData] = useState<ApiGraph | null>(null); |
| 281 | const [error, setError] = useState<string | null>(null); |
| 282 | const [loading, setLoading] = useState(true); |
| 283 | |
| 284 | const [hoverCard, setHoverCard] = useState<{ api: ApiNode; sx: number; sy: number } | null>(null); |
| 285 | |
| 286 | const wrapRef = useRef<HTMLDivElement>(null); |
| 287 | const canvasRef = useRef<HTMLCanvasElement>(null); |
| 288 | const simRef = useRef<Simulation<SimNode, SimLink> | null>(null); |
| 289 | const nodesRef = useRef<SimNode[]>([]); |
| 290 | const linksRef = useRef<SimLink[]>([]); |
| 291 | const neighborRef = useRef<Map<string, Set<string>>>(new Map()); |
| 292 | const transformRef = useRef<ZoomTransform>(zoomIdentity); |
| 293 | const zoomRef = useRef<ZoomBehavior<HTMLCanvasElement, unknown> | null>(null); |
| 294 | const hoverIdRef = useRef<string | null>(null); |
| 295 | const selectedIdRef = useRef<string | null>(null); // 点击选中并持久高亮的节点 |
| 296 | const draggingRef = useRef<SimNode | null>(null); |
| 297 | const dprRef = useRef<number>(1); |
| 298 | const sizeRef = useRef<{ w: number; h: number }>({ w: 1, h: 1 }); |
| 299 | const themeRef = useRef<Palette>(DARK); |
| 300 | const pointerDownRef = useRef<{ x: number; y: number; moved: boolean } | null>(null); |
| 301 | |
| 302 | themeRef.current = theme === "dark" ? DARK : LIGHT; |
| 303 | |
| 304 | // ---------- 数据加载 ---------- |
| 305 | useEffect(() => { |
| 306 | let cancelled = false; |
| 307 | setLoading(true); |
| 308 | setError(null); |
| 309 | const sp = new URLSearchParams(); |
| 310 | if (filterType) sp.set("type", filterType); |
| 311 | if (filterTag) sp.set("tag", filterTag); |
| 312 | if (includeArchive) sp.set("include_archive", "1"); |
| 313 | fetch(`/api/graph${sp.toString() ? "?" + sp.toString() : ""}`) |
| 314 | .then((r) => r.json()) |
| 315 | .then((d: ApiGraph & { error?: string }) => { |
| 316 | if (cancelled) return; |
| 317 | if (d.error) { |
| 318 | setError(d.error); |
| 319 | setData({ nodes: [], edges: [] }); |
| 320 | return; |
| 321 | } |
| 322 | setData({ nodes: d.nodes || [], edges: d.edges || [] }); |
| 323 | }) |
| 324 | .catch((e: unknown) => { |
| 325 | if (!cancelled) setError(String(e)); |
| 326 | }) |
| 327 | .finally(() => { |
| 328 | if (!cancelled) setLoading(false); |
nothing calls this directly
no test coverage detected