import { useState, useRef, useEffect, useCallback } from "react"; import { Search, Network, ZoomIn, ZoomOut, Maximize2, Plus, Download, Trash2, Edit, Eye } from "lucide-react"; import { motion } from "motion/react"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../components/ui/dialog"; import * as d3 from "d3"; interface GraphNode extends d3.SimulationNodeDatum { id: number; label: string; type: string; description: string; connections: number; } interface GraphEdge extends d3.SimulationLinkDatum { source: number | GraphNode; target: number | GraphNode; label: string; } const steelNodeTypeConfig = { fault: { label: "故障", color: "#ef4444", gradient: "from-red-500 to-orange-500" }, symptom: { label: "症状", color: "#f59e0b", gradient: "from-yellow-500 to-amber-500" }, cause: { label: "原因", color: "#3b82f6", gradient: "from-blue-500 to-cyan-500" }, solution: { label: "解决方案", color: "#10b981", gradient: "from-green-500 to-emerald-500" }, prevention: { label: "预防措施", color: "#a855f7", gradient: "from-purple-500 to-pink-500" }, }; const aluminaNodeTypeConfig = { equipment: { label: "设备", color: "#0ea5e9", gradient: "from-sky-500 to-blue-500" }, material: { label: "原料", color: "#22c55e", gradient: "from-green-500 to-emerald-500" }, parameter: { label: "工艺参数", color: "#f59e0b", gradient: "from-amber-500 to-orange-500" }, quality: { label: "质量指标", color: "#a855f7", gradient: "from-purple-500 to-pink-500" }, process: { label: "工序", color: "#ec4899", gradient: "from-pink-500 to-rose-500" }, }; const initialNodes: GraphNode[] = [ { id: 1, label: "给煤机故障", type: "fault", description: "给煤机无法正常启动或运行中断", connections: 4 }, { id: 2, label: "跳闸", type: "symptom", description: "设备意外停机保护动作", connections: 3 }, { id: 3, label: "皮带松紧不当", type: "cause", description: "输送带张力异常导致打滑", connections: 2 }, { id: 4, label: "检查皮带", type: "solution", description: "调整皮带张紧装置", connections: 1 }, { id: 5, label: "定期维护", type: "prevention", description: "建立周期性检修计划", connections: 1 }, { id: 6, label: "电机过热", type: "fault", description: "电机温度超过额定值", connections: 3 }, { id: 7, label: "停机", type: "symptom", description: "系统停止运行", connections: 2 }, { id: 8, label: "散热不良", type: "cause", description: "冷却系统效率下降", connections: 2 }, { id: 9, label: "清理风扇", type: "solution", description: "清除散热器灰尘杂物", connections: 1 }, { id: 10, label: "冷却水不足", type: "cause", description: "冷却液位过低或循环不畅", connections: 2 }, { id: 11, label: "补充冷却液", type: "solution", description: "添加或更换冷却液", connections: 1 }, { id: 12, label: "磨损严重", type: "fault", description: "部件磨损导致性能下降", connections: 2 }, ]; const initialEdges: GraphEdge[] = [ { source: 1, target: 2, label: "表现" }, { source: 1, target: 3, label: "由于" }, { source: 2, target: 4, label: "对策" }, { source: 3, target: 5, label: "预防" }, { source: 6, target: 7, label: "表现" }, { source: 6, target: 8, label: "由于" }, { source: 8, target: 9, label: "对策" }, { source: 6, target: 10, label: "由于" }, { source: 10, target: 11, label: "对策" }, { source: 12, target: 7, label: "导致" }, ]; const aluminaNodes: GraphNode[] = [ { id: 1, label: "溶出釜", type: "equipment", description: "高压溶出铝土矿的核心设备", connections: 5 }, { id: 2, label: "铝土矿", type: "material", description: "主要原料铝硅比A/S=8.5", connections: 4 }, { id: 3, label: "配碱量", type: "parameter", description: "碱液与矿石比例210kg/t", connections: 3 }, { id: 4, label: "溶出率", type: "quality", description: "目标溶出率≥95%", connections: 4 }, { id: 5, label: "溶出工序", type: "process", description: "高压碱液溶出铝土矿", connections: 3 }, { id: 6, label: "沉降槽", type: "equipment", description: "分离赤泥和铝酸钠溶液", connections: 4 }, { id: 7, label: "种子分解", type: "process", description: "添加晶种分解氢氧化铝", connections: 3 }, { id: 8, label: "蒸发器", type: "equipment", description: "浓缩母液的设备", connections: 3 }, { id: 9, label: "煅烧炉", type: "equipment", description: "焙烧氢氧化铝成氧化铝", connections: 4 }, { id: 10, label: "电解槽", type: "equipment", description: "电解生产金属铝的核心设备", connections: 5 }, { id: 11, label: "槽电压", type: "parameter", description: "正常范围3.8-4.2V", connections: 4 }, { id: 12, label: "分子比", type: "parameter", description: "电解质分子比2.2-2.4", connections: 3 }, ]; const aluminaEdges: GraphEdge[] = [ { source: 1, target: 5, label: "属于" }, { source: 2, target: 1, label: "输入" }, { source: 3, target: 1, label: "作用于" }, { source: 1, target: 4, label: "产出" }, { source: 4, target: 6, label: "输送至" }, { source: 6, target: 7, label: "进入" }, { source: 7, target: 8, label: "产出" }, { source: 8, target: 9, label: "输送至" }, { source: 9, target: 10, label: "原料" }, { source: 10, target: 11, label: "监测" }, { source: 10, target: 12, label: "影响" }, ]; const statsDataSteel = [ { label: "实体总数", value: "1,067" }, { label: "关系总数", value: "1,249" }, { label: "故障类型", value: "45种" }, { label: "图谱类型", value: "钢铁" }, ]; const statsDataAlumina = [ { label: "实体总数", value: "2,458" }, { label: "关系总数", value: "3,102" }, { label: "工序覆盖", value: "6大工序" }, { label: "图谱类型", value: "铝冶炼" }, ]; const queryHistoryData = [ { query: "给煤机皮带跑偏", result: "已找到5个相关节点", timestamp: "10:30" }, { query: "过热器故障", result: "已找到3个相关节点", timestamp: "10:25" }, { query: "磨煤机跳闸", result: "已找到4个相关节点", timestamp: "10:20" }, ]; export function KnowledgeGraph() { const [searchQuery, setSearchQuery] = useState(""); const [selectedNode, setSelectedNode] = useState(null); const [showNodeDialog, setShowNodeDialog] = useState(false); const [searchHistory, setSearchHistory] = useState(queryHistoryData); const [isSearching, setIsSearching] = useState(false); const [hoveredNode, setHoveredNode] = useState(null); const [highlightedNodes, setHighlightedNodes] = useState>(new Set()); const [typeFilter, setTypeFilter] = useState>(new Set(Object.keys(steelNodeTypeConfig))); const [simulation, setSimulation] = useState | null>(null); const [industry, setIndustry] = useState<"steel" | "alumina">("steel"); const svgRef = useRef(null); const containerRef = useRef(null); const currentStatsData = industry === "steel" ? statsDataSteel : statsDataAlumina; const currentNodes = industry === "steel" ? initialNodes : aluminaNodes; const currentEdges = industry === "steel" ? initialEdges : aluminaEdges; const currentNodeTypeConfig = industry === "steel" ? steelNodeTypeConfig : aluminaNodeTypeConfig; const initGraph = useCallback(() => { if (!svgRef.current || !containerRef.current) return; const svg = d3.select(svgRef.current); svg.selectAll("*").remove(); const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; svg.attr("width", width).attr("height", height); const g = svg.append("g"); const zoom = d3.zoom() .scaleExtent([0.3, 3]) .on("zoom", (event) => { g.attr("transform", event.transform); }); svg.call(zoom); const defs = svg.append("defs"); const configToUse = industry === "steel" ? steelNodeTypeConfig : aluminaNodeTypeConfig; Object.entries(configToUse).forEach(([type, config]) => { const gradient = defs.append("radialGradient") .attr("id", `gradient-${type}`) .attr("cx", "30%") .attr("cy", "30%"); gradient.append("stop") .attr("offset", "0%") .attr("stop-color", config.color) .attr("stop-opacity", 0.8); gradient.append("stop") .attr("offset", "100%") .attr("stop-color", config.color) .attr("stop-opacity", 0.4); }); const nodesToUse = industry === "steel" ? initialNodes : aluminaNodes; const edgesToUse = industry === "steel" ? initialEdges : aluminaEdges; const filteredNodes = nodesToUse.filter(n => typeFilter.has(n.type)); const filteredNodeIds = new Set(filteredNodes.map(n => n.id)); const filteredEdges = edgesToUse.filter(e => { const sourceId = typeof e.source === 'number' ? e.source : (e.source as GraphNode).id; const targetId = typeof e.target === 'number' ? e.target : (e.target as GraphNode).id; return filteredNodeIds.has(sourceId) && filteredNodeIds.has(targetId); }); const nodes = filteredNodes.map(n => ({ ...n })); const links = filteredEdges.map(e => ({ ...e })); const newSimulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id((d: any) => d.id).distance(150)) .force("charge", d3.forceManyBody().strength(-400)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collision", d3.forceCollide().radius(60)); setSimulation(newSimulation); const link = g.append("g") .attr("class", "links") .selectAll("line") .data(links) .enter() .append("g") .attr("class", "link-group"); const linkLine = link.append("line") .attr("stroke", "#475569") .attr("stroke-width", 2) .attr("stroke-opacity", 0.6); const linkLabel = link.append("text") .attr("fill", "#94a3b8") .attr("font-size", 10) .attr("text-anchor", "middle") .attr("dy", -5) .text((d: any) => d.label); const nodeGroup = g.append("g") .attr("class", "nodes") .selectAll("g") .data(nodes) .enter() .append("g") .attr("class", "node-group") .style("cursor", "pointer") .call(d3.drag() .on("start", (event, d: any) => { if (!event.active) newSimulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }) .on("drag", (event, d: any) => { d.fx = event.x; d.fy = event.y; }) .on("end", (event, d: any) => { if (!event.active) newSimulation.alphaTarget(0); d.fx = null; d.fy = null; }) ); nodeGroup.append("circle") .attr("r", 40) .attr("fill", (d: any) => `url(#gradient-${d.type})`) .attr("stroke", (d: any) => configToUse[d.type as keyof typeof configToUse]?.color || "#fff") .attr("stroke-width", 3) .attr("stroke-opacity", 0.8); nodeGroup.append("text") .attr("text-anchor", "middle") .attr("dy", "0.35em") .attr("fill", "#fff") .attr("font-size", 11) .attr("font-weight", "bold") .text((d: any) => d.label.length > 8 ? d.label.slice(0, 8) + "..." : d.label); nodeGroup.on("mouseover", function(event, d: any) { d3.select(this).select("circle") .transition() .duration(200) .attr("r", 50) .attr("stroke-width", 4); setHoveredNode(d.id); const connectedIds = new Set(); connectedIds.add(d.id); links.forEach((l: any) => { if (l.source.id === d.id) connectedIds.add(l.target.id); if (l.target.id === d.id) connectedIds.add(l.source.id); }); setHighlightedNodes(connectedIds); }); nodeGroup.on("mouseout", function() { d3.select(this).select("circle") .transition() .duration(200) .attr("r", 40) .attr("stroke-width", 3); setHoveredNode(null); setHighlightedNodes(new Set()); }); nodeGroup.on("click", function(event, d: any) { event.stopPropagation(); setSelectedNode(d); setShowNodeDialog(true); toast.success(`已选择节点: ${d.label}`, { duration: 2000 }); }); nodeGroup.on("dblclick", function(event, d: any) { event.stopPropagation(); const connectedIds = new Set(); connectedIds.add(d.id); links.forEach((l: any) => { if (l.source.id === d.id) connectedIds.add(l.target.id); if (l.target.id === d.id) connectedIds.add(l.source.id); }); nodes.forEach(n => { const nodeG = g.selectAll(".node-group").filter((nd: any) => nd.id === n.id); if (connectedIds.has(n.id)) { nodeG.select("circle") .transition() .duration(300) .attr("stroke-width", 4) .attr("stroke", configToUse[n.type as keyof typeof configToUse]?.color || "#fff"); } else { nodeG.select("circle") .transition() .duration(300) .attr("stroke-width", 1) .attr("stroke", "#475569") .attr("opacity", 0.3); } }); toast.info(`已展开 ${d.label} 的关联节点`, { duration: 1500 }); }); svg.on("click", () => { setSelectedNode(null); setShowNodeDialog(false); nodeGroup.select("circle") .transition() .duration(300) .attr("stroke-width", 3) .attr("stroke", (d: any) => configToUse[d.type as keyof typeof configToUse]?.color || "#fff") .attr("opacity", 1); }); newSimulation.on("tick", () => { linkLine .attr("x1", (d: any) => d.source.x) .attr("y1", (d: any) => d.source.y) .attr("x2", (d: any) => d.target.x) .attr("y2", (d: any) => d.target.y); linkLabel .attr("x", (d: any) => (d.source.x + d.target.x) / 2) .attr("y", (d: any) => (d.source.y + d.target.y) / 2); nodeGroup.attr("transform", (d: any) => `translate(${d.x},${d.y})`); }); svg.call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale(1)); }, [typeFilter, industry]); useEffect(() => { initGraph(); }, [industry, initGraph]); useEffect(() => { if (!simulation) return; return () => { simulation.stop(); }; }, [simulation]); useEffect(() => { const handleResize = () => { if (containerRef.current && svgRef.current) { const svg = d3.select(svgRef.current); svg.attr("width", containerRef.current.clientWidth) .attr("height", containerRef.current.clientHeight); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); const handleSearch = () => { if (searchQuery.trim()) { setIsSearching(true); toast.promise( new Promise((resolve) => setTimeout(resolve, 1200)), { loading: "正在搜索知识图谱...", success: () => { const newEntry = { query: searchQuery, result: `已找到 ${Math.floor(Math.random() * 5) + 3} 个相关节点`, timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }), }; setSearchHistory((prev) => [newEntry, ...prev.slice(0, 4)]); const foundNode = initialNodes.find( (n) => n.label.includes(searchQuery) || n.description.includes(searchQuery) ); if (foundNode) { setSelectedNode(foundNode); setShowNodeDialog(true); } setSearchQuery(""); setIsSearching(false); return `找到相关节点`; }, error: "搜索失败", } ); } }; const handleExport = () => { toast.promise( new Promise((resolve) => setTimeout(resolve, 1500)), { loading: "正在导出图谱数据...", success: "导出成功!文件已保存", error: "导出失败", } ); }; const handleNewTask = () => { toast.info("新建抽取任务窗口即将打开", { duration: 2000 }); }; const handleViewResults = () => { toast.info("查看抽取结果", { duration: 2000 }); }; const handleKnowledgeFusion = () => { toast.promise( new Promise((resolve) => setTimeout(resolve, 2000)), { loading: "正在执行知识融合...", success: "知识融合完成!新增实体 12 个", error: "融合失败", } ); }; const handleQualityEval = () => { toast.promise( new Promise((resolve) => setTimeout(resolve, 1800)), { loading: "正在正在进行质量评估...", success: "评估完成!质量得分: 92/100", error: "评估失败", } ); }; const handleTypeFilterToggle = (type: string) => { setTypeFilter((prev) => { const newSet = new Set(prev); if (newSet.has(type)) { if (newSet.size > 1) { newSet.delete(type); } } else { newSet.add(type); } return newSet; }); toast.info("图谱筛选已更新", { duration: 1000 }); }; const getConnectedNodes = (nodeId: number) => { const connected: number[] = []; initialEdges.forEach((e) => { const sourceId = typeof e.source === 'number' ? e.source : (e.source as GraphNode).id; const targetId = typeof e.target === 'number' ? e.target : (e.target as GraphNode).id; if (sourceId === nodeId) connected.push(targetId); if (targetId === nodeId) connected.push(sourceId); }); return connected; }; return (

工业知识图谱

导出 D3.js 力导向图引擎

图谱统计概览

{currentStatsData.map((stat) => (
{stat.label} {stat.value}
))}

图谱搜索

setSearchQuery(e.target.value)} onKeyPress={(e) => e.key === "Enter" && handleSearch()} placeholder="输入故障现象..." disabled={isSearching} className="w-full px-4 py-2 pl-10 rounded-lg bg-slate-800/50 border border-slate-700/50 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 disabled:opacity-50" />
{isSearching ? "搜索中..." : "🔍 搜索"}

节点类型筛选

{Object.entries(currentNodeTypeConfig).map(([type, config]) => ( handleTypeFilterToggle(type)} className={`w-full px-3 py-2 rounded-lg transition-all flex items-center gap-2 ${ typeFilter.has(type) ? "bg-slate-800/80 border border-slate-600" : "bg-slate-800/30 border border-transparent opacity-50" }`} >
{config.label} {typeFilter.has(type) && ( )} ))}

搜索历史

{searchHistory.map((item, index) => ( setSearchQuery(item.query)} >
{item.query}
{item.result}
{item.timestamp}
))}

知识抽取任务

新建抽取任务 📊 查看抽取结果 🔀 知识融合 ✓ 质量评估

知识图谱可视化区域 {hoveredNode && ( 悬停: {initialNodes.find((n) => n.id === hoveredNode)?.label} )}

拖拽节点移动 | 双击展开关联
{ if (svgRef.current) { const svg = d3.select(svgRef.current); svg.transition().call( d3.zoom().transform as any, d3.zoomIdentity.scale(1.2) ); } }} className="p-2 rounded-lg bg-slate-800/80 hover:bg-slate-700 transition-colors" > { if (svgRef.current) { const svg = d3.select(svgRef.current); svg.transition().call( d3.zoom().transform as any, d3.zoomIdentity.scale(0.8) ); } }} className="p-2 rounded-lg bg-slate-800/80 hover:bg-slate-700 transition-colors" > initGraph()} className="p-2 rounded-lg bg-slate-800/80 hover:bg-slate-700 transition-colors" >
{Object.entries(currentNodeTypeConfig).map(([type, config]) => (
{config.label}
))}
当前显示 {currentNodes.filter((n) => typeFilter.has(n.type)).length} 个节点 |{" "} {currentEdges.filter( (e) => typeFilter.has(currentNodes.find((n) => n.id === e.source)?.type || "") && typeFilter.has(currentNodes.find((n) => n.id === e.target)?.type || "") ).length} 条关系
{selectedNode?.label} 节点详情信息 {selectedNode && (
{currentNodeTypeConfig[selectedNode.type]?.label || selectedNode.type}
关联节点: {getConnectedNodes(selectedNode.id).length} 个
详细描述
{selectedNode.description}
{getConnectedNodes(selectedNode.id).length > 0 && (
关联节点
{getConnectedNodes(selectedNode.id).map((id) => { const node = initialNodes.find((n) => n.id === id); if (!node) return null; return ( { const newNode = initialNodes.find((n) => n.id === id); if (newNode) { setSelectedNode(newNode); } }} > {node.label} ); })}
)}
toast.info("编辑功能开发中")} className="flex-1 px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm transition-colors flex items-center justify-center gap-1" > 编辑节点 { setShowNodeDialog(false); toast.success("已定位节点"); }} className="flex-1 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-white text-sm transition-colors flex items-center justify-center gap-1" > 定位节点 { setShowNodeDialog(false); toast.error("删除功能已禁用"); }} className="px-4 py-2 rounded-lg bg-red-600/50 text-red-300 text-sm transition-colors flex items-center justify-center gap-1 cursor-not-allowed" disabled >
)}
); }