"use client"; import { useEffect, useMemo, useState } from "react"; import { Background, BackgroundVariant, Controls, ReactFlow, type Edge, type Node, } from "@xyflow/react"; import { Users } from "lucide-react"; // React Flow 样式必须显式引入(组件经 dynamic ssr:false 加载,样式随该 chunk 打包)。 import "@xyflow/react/dist/style.css"; import { EmptyState } from "@/components/ui/EmptyState"; import { buildRelationshipGraph, NODE_WIDTH, type GraphNode, } from "@/lib/characters/graphLayout"; import type { CharacterCardView } from "@/lib/api/types"; interface RelationshipGraphProps { characters: CharacterCardView[]; } // 纸感节点样式:面板底 + 细线框 + 左侧按分组配色的强调条(色值走主题 CSS 变量)。 function paperNodeStyle(colorVar: string): React.CSSProperties { return { width: NODE_WIDTH, padding: "8px 10px", borderRadius: 6, background: "var(--color-panel)", color: "var(--color-ink)", border: "1px solid var(--color-line)", borderLeft: `4px solid var(${colorVar})`, boxShadow: "0 1px 3px var(--shadow-paper)", fontSize: 12, textAlign: "left", }; } // 节点内容:角色名(宋体)+ 定位副标题。 function NodeLabel({ name, role }: { name: string; role: string }) { return (

{name}

{role ?

{role}

: null}
); } function toReactFlowNodes(nodes: GraphNode[]): Node[] { return nodes.map((node) => ({ id: node.id, position: node.position, data: { label: }, style: paperNodeStyle(node.data.colorVar), // 只读可视化:可拖动重排,不可新建连线。 connectable: false, })); } function toReactFlowEdges( edges: { id: string; source: string; target: string; label: string }[], ): Edge[] { return edges.map((edge) => ({ id: edge.id, source: edge.source, target: edge.target, label: edge.label, labelStyle: { fill: "var(--color-ink-soft)", fontSize: 11 }, labelBgStyle: { fill: "var(--color-bg)", fillOpacity: 0.9 }, labelBgPadding: [4, 2] as [number, number], labelBgBorderRadius: 4, style: { stroke: "var(--color-line)", strokeWidth: 1.5 }, })); } // 尊重 prefers-reduced-motion:减动偏好下关闭 fitView 平滑动画与边流动动画。 function usePrefersReducedMotion(): boolean { const [reduced, setReduced] = useState(false); useEffect(() => { const media = window.matchMedia("(prefers-reduced-motion: reduce)"); const update = () => setReduced(media.matches); update(); media.addEventListener("change", update); return () => media.removeEventListener("change", update); }, []); return reduced; } // 角色关系图谱(灵感⑥ 缩减版 / D4):只用现有 relations 数据,节点按 role 上色、 // 边按 name-join、kind 作中文标签。悬空边(引用未入库角色)已在 graphLayout 略过并计数。 export function RelationshipGraph({ characters }: RelationshipGraphProps) { const reducedMotion = usePrefersReducedMotion(); const graph = useMemo( () => buildRelationshipGraph(characters), [characters], ); const nodes = useMemo(() => toReactFlowNodes(graph.nodes), [graph.nodes]); const edges = useMemo(() => toReactFlowEdges(graph.edges), [graph.edges]); if (graph.nodes.length === 0) { return ( ); } return (
{/* React Flow 必须有显式尺寸的容器,否则画布高度为 0。 */}
{graph.danglingCount > 0 ? (

有 {graph.danglingCount} 条关系引用了未入库的角色,已略过。

) : null}
); }