644 lines
14 KiB
Vue
644 lines
14 KiB
Vue
|
|
<template>
|
|||
|
|
<section class="knowledge-graph-space">
|
|||
|
|
<header class="graph-head">
|
|||
|
|
<div>
|
|||
|
|
<span class="graph-eyebrow">LightRAG 知识图谱</span>
|
|||
|
|
<h4>实体关系空间</h4>
|
|||
|
|
</div>
|
|||
|
|
<div class="graph-toolbar">
|
|||
|
|
<label class="graph-search">
|
|||
|
|
<i class="mdi mdi-magnify"></i>
|
|||
|
|
<input
|
|||
|
|
v-model.trim="graphQuery"
|
|||
|
|
type="search"
|
|||
|
|
placeholder="搜索实体"
|
|||
|
|
@keydown.enter.prevent="focusMatchedNode"
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
<button type="button" title="定位匹配实体" @click="focusMatchedNode">
|
|||
|
|
<i class="mdi mdi-crosshairs-gps"></i>
|
|||
|
|
</button>
|
|||
|
|
<button type="button" title="缩小" @click="zoomGraph(0.86)">
|
|||
|
|
<i class="mdi mdi-magnify-minus-outline"></i>
|
|||
|
|
</button>
|
|||
|
|
<button type="button" title="放大" @click="zoomGraph(1.16)">
|
|||
|
|
<i class="mdi mdi-magnify-plus-outline"></i>
|
|||
|
|
</button>
|
|||
|
|
<button type="button" title="适配画布" @click="fitGraph">
|
|||
|
|
<i class="mdi mdi-fit-to-page-outline"></i>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div class="graph-stats">
|
|||
|
|
<span>{{ graphSummary.entityCount }} 实体</span>
|
|||
|
|
<span>{{ graphSummary.relationCount }} 关系</span>
|
|||
|
|
<span>{{ graphSummary.visibleNodeCount }} 节点可见</span>
|
|||
|
|
<span>{{ graphSummary.visibleEdgeCount }} 连线可见</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="graph-body" :class="{ empty: !graphData.nodes.length }">
|
|||
|
|
<div class="graph-theater">
|
|||
|
|
<div v-if="graphData.nodes.length" ref="graphContainer" class="g6-canvas"></div>
|
|||
|
|
<div v-else class="graph-empty">
|
|||
|
|
<strong>暂无图谱数据</strong>
|
|||
|
|
<span>本次归集还没有返回可展示的实体关系。</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="graphData.nodes.length" class="graph-hud">
|
|||
|
|
<span>G6 Force</span>
|
|||
|
|
<span>拖拽节点</span>
|
|||
|
|
<span>滚轮缩放</span>
|
|||
|
|
<span>点击查看详情</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<aside class="graph-inspector">
|
|||
|
|
<template v-if="selectedNode">
|
|||
|
|
<div class="inspector-title">
|
|||
|
|
<span>节点详情</span>
|
|||
|
|
<h5>{{ selectedNode.name }}</h5>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="node-facts">
|
|||
|
|
<div>
|
|||
|
|
<span>关系数</span>
|
|||
|
|
<strong>{{ selectedNode.degree }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span>入边</span>
|
|||
|
|
<strong>{{ selectedNodeIncoming.length }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span>出边</span>
|
|||
|
|
<strong>{{ selectedNodeOutgoing.length }}</strong>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="node-meta">
|
|||
|
|
<span>类型</span>
|
|||
|
|
<strong>{{ selectedNode.type || '实体' }}</strong>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<KnowledgeIngestNodeDetails
|
|||
|
|
:node="selectedNode"
|
|||
|
|
:incoming="selectedNodeIncoming"
|
|||
|
|
:outgoing="selectedNodeOutgoing"
|
|||
|
|
:relations="selectedNodeRelations"
|
|||
|
|
@focus-peer="focusRelationPeer"
|
|||
|
|
/>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<div v-else class="inspector-empty">
|
|||
|
|
<strong>选择一个实体</strong>
|
|||
|
|
<span>点击图谱中的节点,查看它的入边、出边和关系明细。</span>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { Graph, NodeEvent } from '@antv/g6'
|
|||
|
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|||
|
|
|
|||
|
|
import KnowledgeIngestNodeDetails from './KnowledgeIngestNodeDetails.vue'
|
|||
|
|
import { useKnowledgeIngestGraph } from './useKnowledgeIngestGraph.js'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
graph: {
|
|||
|
|
type: Object,
|
|||
|
|
default: () => ({})
|
|||
|
|
},
|
|||
|
|
documents: {
|
|||
|
|
type: Array,
|
|||
|
|
default: () => []
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const graphContainer = ref(null)
|
|||
|
|
let graphInstance = null
|
|||
|
|
let resizeObserver = null
|
|||
|
|
|
|||
|
|
const {
|
|||
|
|
activeNodeId,
|
|||
|
|
graphData,
|
|||
|
|
graphQuery,
|
|||
|
|
graphSummary,
|
|||
|
|
selectNodeById,
|
|||
|
|
focusFirstMatch,
|
|||
|
|
selectRelationPeer,
|
|||
|
|
selectedNode,
|
|||
|
|
selectedNodeIncoming,
|
|||
|
|
selectedNodeOutgoing,
|
|||
|
|
selectedNodeRelations
|
|||
|
|
} = useKnowledgeIngestGraph(props)
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
initGraph()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
resizeObserver?.disconnect()
|
|||
|
|
graphInstance?.destroy()
|
|||
|
|
resizeObserver = null
|
|||
|
|
graphInstance = null
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
graphData,
|
|||
|
|
() => {
|
|||
|
|
renderGraph()
|
|||
|
|
},
|
|||
|
|
{ deep: true }
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
watch(activeNodeId, (nodeId) => {
|
|||
|
|
syncGraphSelection(nodeId)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async function initGraph() {
|
|||
|
|
if (!graphData.value.nodes.length) return
|
|||
|
|
await nextTick()
|
|||
|
|
if (!graphContainer.value || graphInstance) return
|
|||
|
|
|
|||
|
|
graphInstance = new Graph(buildGraphOptions())
|
|||
|
|
graphInstance.on(NodeEvent.CLICK, async (event) => {
|
|||
|
|
const nodeId = String(event?.target?.id || '').trim()
|
|||
|
|
if (!nodeId) return
|
|||
|
|
selectNodeById(nodeId)
|
|||
|
|
await focusGraphNode(nodeId, { focus: false })
|
|||
|
|
})
|
|||
|
|
await graphInstance.render()
|
|||
|
|
await syncGraphSelection(activeNodeId.value)
|
|||
|
|
observeResize()
|
|||
|
|
await fitGraph()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildGraphOptions() {
|
|||
|
|
return {
|
|||
|
|
container: graphContainer.value,
|
|||
|
|
autoResize: true,
|
|||
|
|
background: 'transparent',
|
|||
|
|
zoomRange: [0.22, 2.8],
|
|||
|
|
autoFit: {
|
|||
|
|
type: 'view',
|
|||
|
|
options: { padding: [86, 122, 86, 122] },
|
|||
|
|
animation: { duration: 520, easing: 'ease-in-out' }
|
|||
|
|
},
|
|||
|
|
data: graphData.value,
|
|||
|
|
layout: {
|
|||
|
|
type: 'd3-force',
|
|||
|
|
preventOverlap: true,
|
|||
|
|
centerStrength: 0.34,
|
|||
|
|
linkDistance: (edge) => (edge?.data?.weight > 2 ? 155 : 210),
|
|||
|
|
edgeStrength: 0.22,
|
|||
|
|
nodeStrength: (node) => (node?.data?.degree >= 5 ? -520 : -360),
|
|||
|
|
collide: {
|
|||
|
|
radius: (node) => 40 + Math.sqrt(Math.max(node?.data?.degree || 1, 1)) * 12,
|
|||
|
|
strength: 0.88,
|
|||
|
|
iterations: 3
|
|||
|
|
},
|
|||
|
|
manyBody: {
|
|||
|
|
strength: (node) => (node?.data?.degree >= 5 ? -620 : -420),
|
|||
|
|
distanceMax: 620
|
|||
|
|
},
|
|||
|
|
center: {
|
|||
|
|
strength: 0.24
|
|||
|
|
},
|
|||
|
|
alpha: 0.88,
|
|||
|
|
alphaDecay: 0.035,
|
|||
|
|
velocityDecay: 0.32,
|
|||
|
|
iterations: 360
|
|||
|
|
},
|
|||
|
|
node: {
|
|||
|
|
type: 'circle',
|
|||
|
|
style: (datum) => datum.style,
|
|||
|
|
state: {
|
|||
|
|
selected: {
|
|||
|
|
lineWidth: 4,
|
|||
|
|
stroke: '#1d4ed8',
|
|||
|
|
halo: true,
|
|||
|
|
haloStroke: '#60a5fa',
|
|||
|
|
haloLineWidth: 24,
|
|||
|
|
haloStrokeOpacity: 0.24,
|
|||
|
|
shadowBlur: 22
|
|||
|
|
},
|
|||
|
|
active: {
|
|||
|
|
lineWidth: 3,
|
|||
|
|
halo: true,
|
|||
|
|
haloLineWidth: 16,
|
|||
|
|
haloStrokeOpacity: 0.28
|
|||
|
|
},
|
|||
|
|
inactive: {
|
|||
|
|
opacity: 0.2
|
|||
|
|
},
|
|||
|
|
dimmed: {
|
|||
|
|
opacity: 0.18
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
edge: {
|
|||
|
|
type: 'line',
|
|||
|
|
style: (datum) => datum.style,
|
|||
|
|
state: {
|
|||
|
|
selected: {
|
|||
|
|
stroke: '#2563eb',
|
|||
|
|
lineWidth: 2.8,
|
|||
|
|
opacity: 0.96,
|
|||
|
|
halo: true,
|
|||
|
|
haloStroke: '#93c5fd',
|
|||
|
|
haloLineWidth: 8,
|
|||
|
|
haloStrokeOpacity: 0.2
|
|||
|
|
},
|
|||
|
|
active: {
|
|||
|
|
stroke: '#60a5fa',
|
|||
|
|
lineWidth: 2.4,
|
|||
|
|
opacity: 0.9
|
|||
|
|
},
|
|||
|
|
inactive: {
|
|||
|
|
opacity: 0.14
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
behaviors: [
|
|||
|
|
{ type: 'drag-canvas', key: 'drag-canvas' },
|
|||
|
|
{ type: 'zoom-canvas', key: 'zoom-canvas', sensitivity: 1.15 },
|
|||
|
|
{ type: 'drag-element-force', key: 'drag-element-force', fixed: true }
|
|||
|
|
],
|
|||
|
|
animation: { duration: 420, easing: 'ease-out' }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function renderGraph() {
|
|||
|
|
if (!graphData.value.nodes.length) return
|
|||
|
|
if (!graphInstance) {
|
|||
|
|
await initGraph()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
graphInstance.setData(graphData.value)
|
|||
|
|
await graphInstance.render()
|
|||
|
|
await syncGraphSelection(activeNodeId.value)
|
|||
|
|
await fitGraph()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function syncGraphSelection(nodeId) {
|
|||
|
|
if (!graphInstance || graphInstance.destroyed || !nodeId) return
|
|||
|
|
const stateMap = {}
|
|||
|
|
for (const node of graphData.value.nodes) {
|
|||
|
|
if (node.id === nodeId) {
|
|||
|
|
stateMap[node.id] = ['selected']
|
|||
|
|
} else if (node.states?.includes('dimmed')) {
|
|||
|
|
stateMap[node.id] = ['dimmed']
|
|||
|
|
} else {
|
|||
|
|
stateMap[node.id] = []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for (const edge of graphData.value.edges) {
|
|||
|
|
stateMap[edge.id] = edge.source === nodeId || edge.target === nodeId ? ['selected'] : ['inactive']
|
|||
|
|
}
|
|||
|
|
await graphInstance.setElementState(stateMap, false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function fitGraph() {
|
|||
|
|
if (!graphInstance || graphInstance.destroyed) return
|
|||
|
|
await graphInstance.fitView(
|
|||
|
|
{ padding: [84, 118, 84, 118] },
|
|||
|
|
{ duration: 420, easing: 'ease-in-out' }
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function zoomGraph(ratio) {
|
|||
|
|
if (!graphInstance || graphInstance.destroyed) return
|
|||
|
|
await graphInstance.zoomBy(ratio, { duration: 220, easing: 'ease-out' })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function focusMatchedNode() {
|
|||
|
|
const nodeId = focusFirstMatch()
|
|||
|
|
if (nodeId) await focusGraphNode(nodeId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function focusRelationPeer(relation) {
|
|||
|
|
const nodeId = selectRelationPeer(relation)
|
|||
|
|
if (nodeId) await focusGraphNode(nodeId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function focusGraphNode(nodeId, options = {}) {
|
|||
|
|
if (!graphInstance || graphInstance.destroyed) return
|
|||
|
|
await syncGraphSelection(nodeId)
|
|||
|
|
if (options.focus === false) return
|
|||
|
|
await graphInstance.focusElement(nodeId, { duration: 360, easing: 'ease-in-out' })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function observeResize() {
|
|||
|
|
if (!graphContainer.value || typeof ResizeObserver === 'undefined') return
|
|||
|
|
resizeObserver = new ResizeObserver(() => {
|
|||
|
|
if (!graphInstance || graphInstance.destroyed) return
|
|||
|
|
graphInstance.setSize(graphContainer.value.clientWidth, graphContainer.value.clientHeight)
|
|||
|
|
})
|
|||
|
|
resizeObserver.observe(graphContainer.value)
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.knowledge-graph-space {
|
|||
|
|
min-height: 1080px;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-rows: auto auto minmax(1020px, 1fr);
|
|||
|
|
gap: 12px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-head {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-eyebrow {
|
|||
|
|
color: #0f766e;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 850;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-head h4 {
|
|||
|
|
margin: 4px 0 0;
|
|||
|
|
color: #0f172a;
|
|||
|
|
font-size: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-toolbar,
|
|||
|
|
.graph-stats {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-search {
|
|||
|
|
min-width: 230px;
|
|||
|
|
min-height: 32px;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
padding: 0 9px;
|
|||
|
|
border: 1px solid #dbe6ef;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #fff;
|
|||
|
|
color: #64748b;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-search input {
|
|||
|
|
width: 100%;
|
|||
|
|
min-width: 0;
|
|||
|
|
border: 0;
|
|||
|
|
outline: 0;
|
|||
|
|
background: transparent;
|
|||
|
|
color: #0f172a;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-toolbar button {
|
|||
|
|
width: 32px;
|
|||
|
|
height: 32px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
border: 1px solid #dbe6ef;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #fff;
|
|||
|
|
color: #475569;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-toolbar button:hover {
|
|||
|
|
border-color: #2563eb;
|
|||
|
|
color: #2563eb;
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-stats {
|
|||
|
|
justify-content: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-stats span {
|
|||
|
|
min-height: 26px;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 0 9px;
|
|||
|
|
border: 1px solid #dbe6ef;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: #fff;
|
|||
|
|
color: #475569;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 750;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-body {
|
|||
|
|
min-height: 0;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) 360px;
|
|||
|
|
gap: 14px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-theater {
|
|||
|
|
position: relative;
|
|||
|
|
min-height: 1020px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 54%, #eef4fb 100%);
|
|||
|
|
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.82);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-theater::before {
|
|||
|
|
position: absolute;
|
|||
|
|
inset: 0;
|
|||
|
|
content: '';
|
|||
|
|
pointer-events: none;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(rgba(148, 163, 184, 0.18) 1px, transparent 1px),
|
|||
|
|
linear-gradient(90deg, rgba(148, 163, 184, 0.18) 1px, transparent 1px);
|
|||
|
|
background-size: 34px 34px;
|
|||
|
|
mask-image: radial-gradient(circle at center, black 0%, black 58%, transparent 100%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.g6-canvas {
|
|||
|
|
position: absolute;
|
|||
|
|
inset: 0;
|
|||
|
|
z-index: 1;
|
|||
|
|
cursor: grab;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.g6-canvas:active {
|
|||
|
|
cursor: grabbing;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-hud {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 14px;
|
|||
|
|
right: 14px;
|
|||
|
|
bottom: 12px;
|
|||
|
|
z-index: 2;
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 8px;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-hud span {
|
|||
|
|
min-height: 24px;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 0 8px;
|
|||
|
|
border: 1px solid rgba(203, 213, 225, 0.86);
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: rgba(255, 255, 255, 0.76);
|
|||
|
|
color: #334155;
|
|||
|
|
font-size: 11px;
|
|||
|
|
font-weight: 750;
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-inspector {
|
|||
|
|
min-height: 0;
|
|||
|
|
display: grid;
|
|||
|
|
align-content: start;
|
|||
|
|
gap: 12px;
|
|||
|
|
overflow: auto;
|
|||
|
|
padding: 12px;
|
|||
|
|
border: 1px solid #dbe6ef;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inspector-title span {
|
|||
|
|
color: #0f766e;
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: 850;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inspector-title h5 {
|
|||
|
|
margin: 4px 0 0;
|
|||
|
|
color: #0f172a;
|
|||
|
|
font-size: 15px;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
word-break: break-word;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-facts {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-facts div,
|
|||
|
|
.node-meta {
|
|||
|
|
min-width: 0;
|
|||
|
|
display: grid;
|
|||
|
|
gap: 4px;
|
|||
|
|
padding: 9px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #f8fafc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-meta {
|
|||
|
|
grid-template-columns: auto minmax(0, 1fr);
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-facts span,
|
|||
|
|
.node-meta span {
|
|||
|
|
color: #64748b;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-facts strong,
|
|||
|
|
.node-meta strong {
|
|||
|
|
min-width: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: #0f172a;
|
|||
|
|
font-size: 15px;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-empty,
|
|||
|
|
.inspector-empty {
|
|||
|
|
min-height: 120px;
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
align-content: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
color: #64748b;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-empty {
|
|||
|
|
position: absolute;
|
|||
|
|
inset: 0;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-empty strong,
|
|||
|
|
.inspector-empty strong {
|
|||
|
|
color: #0f172a;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inspector-empty strong {
|
|||
|
|
color: #0f172a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-empty span,
|
|||
|
|
.inspector-empty span {
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (prefers-reduced-motion: reduce) {
|
|||
|
|
.graph-toolbar button {
|
|||
|
|
transition: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 980px) {
|
|||
|
|
.knowledge-graph-space {
|
|||
|
|
min-height: 980px;
|
|||
|
|
grid-template-rows: auto auto minmax(900px, 1fr);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-head {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-toolbar {
|
|||
|
|
justify-content: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-body {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
grid-template-rows: minmax(780px, 1fr) minmax(220px, auto);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-theater {
|
|||
|
|
min-height: 780px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-inspector {
|
|||
|
|
max-height: 240px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 620px) {
|
|||
|
|
.graph-search {
|
|||
|
|
min-width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.graph-toolbar {
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|