/* global React, ReactDOM, ForceGraph2D */ // GraphViewer — 通用时序知识图谱查看组件 (喂 {nodes,edges} 就能看) // 数据契约(标准 Neo4j 风格): // node: { id, labels:[], properties:{ entity_type?, name?, description? } } // edge: { id, source, target, type?, properties?:{ keywords?, invalid_at?, valid_at? } } // 三种数据来源: mount(el,{data}) | mount(el,{api:'url'}) | iframe内 postMessage({type:'gv-data',data}) (function(root, factory){ if(typeof exports==='object'&&typeof module==='object'){module.exports=factory();} else{root.GraphViewer=factory();} })(typeof self!=='undefined'?self:this,function(){ 'use strict'; var R = window.React, RD = window.ReactDOM; var PALETTE = ["#60a5fa","#f87171","#fbbf24","#34d399","#a78bfa","#fb7185","#22d3ee","#818cf8","#fb923c","#4ade80"]; function trunc(s,n){ s=s==null?'':String(s); return s.length>n?s.slice(0,n)+'…':s; } var h = R.createElement; var useState=R.useState, useMemo=R.useMemo, useRef=R.useRef, useEffect=R.useEffect; function normalize(data){ var nodes=(data&&data.nodes||[]).map(function(n){return n;}); var edges=(data&&(data.edges||data.links)||[]).map(function(e){ return {id:e.id,source:e.source,target:e.target,type:e.type||'', keywords:(e.properties&&e.properties.keywords)||e.type||'', invalid:!!(e.properties&&e.properties.invalid_at)}; }); return {nodes:nodes,edges:edges}; } function Viewer(props){ var data=props.data||{nodes:[],edges:[]}; var sel=props.selected, onSelect=props.onSelect; var norm=useMemo(function(){return normalize(data);},[data]); var colorMap=useMemo(function(){ var m=new Map(); var types=[]; norm.nodes.forEach(function(n){var t=(n.properties&&n.properties.entity_type)||(n.labels&&n.labels[n.labels.length-1])||'default';if(types.indexOf(t)<0)types.push(t);}); types.forEach(function(t,i){m.set(t,PALETTE[i%PALETTE.length]);}); return m; },[norm]); var gd=useMemo(function(){ return {nodes:norm.nodes.map(function(n){var t=(n.properties&&n.properties.entity_type)||(n.labels&&n.labels[n.labels.length-1])||'default'; return Object.assign({},n,{color:colorMap.get(t)||'#888',val:6});}), links:norm.edges}; },[norm,colorMap]); var ref=useRef(null), box=useRef(null), dim=useState({width:0,height:0}); useEffect(function(){ if(!box.current)return; var ro=new ResizeObserver(function(es){for(var i=0;iMath.PI/2||ang<-Math.PI/2); ctx.save(); ctx.translate(mx,my); ctx.rotate(flip?ang+Math.PI:ang); ctx.font=fs+"px Sans-Serif"; ctx.textAlign="center"; ctx.textBaseline="middle"; var txt=trunc(label,24), tw=ctx.measureText(txt).width; ctx.fillStyle="rgba(1,3,15,.7)"; ctx.fillRect(-tw/2-1,-fs/2,tw+2,fs); ctx.fillStyle=l.invalid?"#8a8a94":"#94e2d5"; ctx.fillText(txt,0,0); ctx.restore(); }, nodeCanvasObject:function(n,ctx,gs){var r=n.val||6;ctx.save();ctx.beginPath();ctx.arc(n.x,n.y,r,0,2*Math.PI);ctx.fillStyle=n.color;ctx.shadowBlur=8/gs;ctx.shadowColor=n.color;ctx.fill(); if(gs>.5){ctx.font=(11/gs)+'px Sans-Serif';ctx.textAlign='center';ctx.textBaseline='top';ctx.fillStyle='#fff';ctx.shadowBlur=0;ctx.fillText(trunc((n.properties&&n.properties.name)||n.id,20),n.x,n.y+r+2);}ctx.restore();}, onNodeClick:function(n){onSelect&&onSelect({type:'node',data:n});}, onLinkClick:function(l){onSelect&&onSelect({type:'edge',data:l});}, cooldownTicks:120})), h('div',{style:{width:280,padding:14,borderLeft:'1px solid #14203a',overflowY:'auto',background:'#050a1a',fontSize:12}}, h('div',{style:{color:'#94e2d5',marginBottom:8}},'节点 '+norm.nodes.length+' · 边 '+norm.edges.length), h('div',{style:{marginBottom:8}},h('span',{style:{display:'inline-block',width:22,borderTop:'2px solid #fff'}}),' 有效 ',h('span',{className:'dash',style:{borderTopStyle:'dashed',borderColor:'#8a8a94',display:'inline-block',width:22}}),' 失效'), h('div',{style:{display:'flex',flexWrap:'wrap',gap:6,marginBottom:12}},types.map(function(t){return h('span',{key:t,style:{padding:'2px 8px',borderRadius:10,fontSize:11,color:'#04101f',fontWeight:600,background:colorMap.get(t)}},t);})), sel?h('div',null,h('div',{style:{color:'#7c8aa0',textTransform:'uppercase',letterSpacing:1,fontSize:11}},sel.type==='node'?'实体':'关系'), h('div',{style:{color:'#94e2d5',margin:'6px 0',wordBreak:'break-all'}},trunc(sel.type==='node'?((sel.data.properties&&sel.data.properties.name)||sel.data.id):(sel.data.keywords||sel.data.type),60)), sel.type==='node'&&sel.data.properties&&sel.data.properties.description?h('div',{style:{marginTop:8,padding:10,background:'#0b1428',borderRadius:8,whiteSpace:'pre-wrap',wordBreak:'break-word'}},sel.data.properties.description):null, sel.type==='edge'?h('div',{style:{marginTop:8}},'valid:',(sel.data.properties&&sel.data.properties.valid_at)||'-',' / invalid:',(sel.data.properties&&sel.data.properties.invalid_at)||'-'):null) :h('div',{style:{color:'#7c8aa0'}},'点击节点/连线查看详情'))); } function resolveData(opts,cb){ if(opts.data)return cb(opts.data); if(opts.api){fetch(opts.api).then(function(r){return r.json();}).then(cb).catch(function(e){cb({error:String(e)});});return;} cb({nodes:[],edges:[]}); } function App(opts){ var state={data:{nodes:[],edges:[]},sel:null}; return { _el:null,_root:null, render:function(el){this._el=el;this._root=RD.createRoot(el);var self=this; resolveData(opts,function(d){ if(d.error){el.textContent='加载失败:'+d.error;return;} self.state_data=d; self.paint(); }); if(opts.listenPostMessage!==false){window.addEventListener('message',function(ev){var m=ev.data;if(m&&m.type==='gv-data'){self.state_data=m.data||{nodes:[],edges:[]};self.paint();}});} return this;}, setData:function(d){this.state_data=d;this.paint();}, paint:function(){var self=this;this._root.render(h(Viewer,{data:this.state_data||{nodes:[],edges:[]},selected:this.sel,onSelect:function(s){self.sel=s;self.paint();}}));} }; } return { mount:function(el,opts){ return new App(opts||{}).render(el); }, VERSION:'1.0.0' }; });