refactor(components): 移除独立信息面板组件并整合到GraphCanvas中
将GraphInfoPanel和LightRAGInfoPanel组件删除,将其功能整合到GraphCanvas组件中 简化视图层代码,移除GraphView中不必要的模板和计算逻辑
This commit is contained in:
parent
3f7e70db31
commit
c5dde79051
@ -8,6 +8,19 @@
|
|||||||
<div class="canvas-content">
|
<div class="canvas-content">
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Statistical Info Panel -->
|
||||||
|
<div class="graph-stats-panel" v-if="graphData.nodes.length > 0">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">节点</span>
|
||||||
|
<span class="stat-value">{{ graphData.nodes.length }}</span>
|
||||||
|
<span v-if="graphInfo?.node_count" class="stat-total">/ {{ graphInfo.node_count }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">边</span>
|
||||||
|
<span class="stat-value">{{ graphData.edges.length }}</span>
|
||||||
|
<span v-if="graphInfo?.edge_count" class="stat-total">/ {{ graphInfo.edge_count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="$slots.bottom" class="overlay bottom">
|
<div v-if="$slots.bottom" class="overlay bottom">
|
||||||
<slot name="bottom" />
|
<slot name="bottom" />
|
||||||
</div>
|
</div>
|
||||||
@ -26,6 +39,10 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
default: () => ({ nodes: [], edges: [] })
|
default: () => ({ nodes: [], edges: [] })
|
||||||
},
|
},
|
||||||
|
graphInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
labelField: { type: String, default: 'name' },
|
labelField: { type: String, default: 'name' },
|
||||||
autoFit: { type: Boolean, default: true },
|
autoFit: { type: Boolean, default: true },
|
||||||
autoResize: { type: Boolean, default: true },
|
autoResize: { type: Boolean, default: true },
|
||||||
@ -423,6 +440,45 @@ defineExpose({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-stats-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--color-trans-light);
|
||||||
|
border: 1px solid var(--color-border-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 13px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-total {
|
||||||
|
color: var(--color-text-quaternary);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.slots {
|
.slots {
|
||||||
// 让整层覆盖容器默认不接收指针事件(便于穿透到底下画布)
|
// 让整层覆盖容器默认不接收指针事件(便于穿透到底下画布)
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="graph-info-panel">
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">实体</span>
|
|
||||||
<span class="info-value">{{ graphData.nodes.length }}</span>
|
|
||||||
<span v-if="graphInfo?.entity_count" class="info-total">/ {{ graphInfo.entity_count }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">关系</span>
|
|
||||||
<span class="info-value">{{ graphData.edges.length }}</span>
|
|
||||||
<span v-if="graphInfo?.relationship_count" class="info-total">/ {{ graphInfo.relationship_count }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="unindexedCount > 0" class="info-item warning">
|
|
||||||
<span class="info-label">未索引</span>
|
|
||||||
<span class="info-value warning">{{ unindexedCount }}</span>
|
|
||||||
<a-button
|
|
||||||
v-if="modelMatched"
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="$emit('index-nodes')"
|
|
||||||
>
|
|
||||||
添加索引
|
|
||||||
</a-button>
|
|
||||||
<a-tooltip v-else title="向量模型不匹配,无法添加索引">
|
|
||||||
<a-button type="default" size="small" disabled>
|
|
||||||
模型不匹配
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
graphInfo: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
graphData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({ nodes: [], edges: [] })
|
|
||||||
},
|
|
||||||
unindexedCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
modelMatched: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
defineEmits(['index-nodes'])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.graph-info-panel {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: var(--gray-50);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 0;
|
|
||||||
border: 1px solid var(--gray-0);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-self: flex-start;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
gap: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
color: var(--color-warning-500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
color: var(--gray-700);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: var(--gray-1000);
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
color: var(--color-warning-500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-total {
|
|
||||||
color: var(--gray-600);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-btn-primary) {
|
|
||||||
font-size: 12px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--main-500);
|
|
||||||
border-color: var(--main-500);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--main-600);
|
|
||||||
border-color: var(--main-600);
|
|
||||||
box-shadow: 0 2px 4px rgba(1, 97, 121, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="lightrag-info-panel">
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">节点</span>
|
|
||||||
<span class="info-value">{{ stats?.total_nodes || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">边</span>
|
|
||||||
<span class="info-value">{{ stats?.total_edges || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
stats: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
graphData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({ nodes: [], edges: [] })
|
|
||||||
},
|
|
||||||
databaseName: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.lightrag-info-panel {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: var(--gray-50);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 0;
|
|
||||||
border: 1px solid var(--gray-0);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-self: flex-start;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
gap: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
color: var(--gray-700);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: var(--gray-1000);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -50,6 +50,7 @@
|
|||||||
<GraphCanvas
|
<GraphCanvas
|
||||||
ref="graphRef"
|
ref="graphRef"
|
||||||
:graph-data="graph.graphData"
|
:graph-data="graph.graphData"
|
||||||
|
:graph-info="formattedGraphInfo"
|
||||||
:highlight-keywords="[state.searchInput]"
|
:highlight-keywords="[state.searchInput]"
|
||||||
@node-click="graph.handleNodeClick"
|
@node-click="graph.handleNodeClick"
|
||||||
@edge-click="graph.handleEdgeClick"
|
@edge-click="graph.handleEdgeClick"
|
||||||
@ -94,24 +95,6 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<a-empty v-show="graph.graphData.nodes.length === 0" style="padding: 4rem 0;"/>
|
<a-empty v-show="graph.graphData.nodes.length === 0" style="padding: 4rem 0;"/>
|
||||||
</template>
|
</template>
|
||||||
<template #bottom>
|
|
||||||
<div class="footer">
|
|
||||||
<GraphInfoPanel
|
|
||||||
v-if="isNeo4j"
|
|
||||||
:graph-info="graphInfo"
|
|
||||||
:graph-data="graph.graphData"
|
|
||||||
:unindexed-count="unindexedCount"
|
|
||||||
:model-matched="modelMatched"
|
|
||||||
@index-nodes="indexNodes"
|
|
||||||
/>
|
|
||||||
<LightRAGInfoPanel
|
|
||||||
v-else
|
|
||||||
:stats="state.lightragStats"
|
|
||||||
:graph-data="graph.graphData"
|
|
||||||
:database-name="getDatabaseName()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</GraphCanvas>
|
</GraphCanvas>
|
||||||
<!-- 详情浮动卡片 -->
|
<!-- 详情浮动卡片 -->
|
||||||
<GraphDetailPanel
|
<GraphDetailPanel
|
||||||
@ -125,12 +108,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
:open="state.showModal" title="上传文件"
|
:open="state.showModal" title="上传文件"
|
||||||
@ok="addDocumentByFile"
|
@ok="addDocumentByFile"
|
||||||
@cancel="handleModalCancel"
|
@cancel="handleModalCancel"
|
||||||
ok-text="添加到图数据库" cancel-text="取消"
|
ok-text="添加到图数据库" cancel-text="取消"
|
||||||
:confirm-loading="state.processing"
|
:confirm-loading="state.processing"
|
||||||
:ok-button-props="{ disabled: !hasValidFile }">
|
:ok-button-props="{ disabled: !hasValidFile }">
|
||||||
<div class="upload">
|
<div class="upload">
|
||||||
<div class="note">
|
<div class="note">
|
||||||
<p>上传的文件内容参考 test/data/A_Dream_of_Red_Mansions_tiny.jsonl 中的格式:</p>
|
<p>上传的文件内容参考 test/data/A_Dream_of_Red_Mansions_tiny.jsonl 中的格式:</p>
|
||||||
@ -193,8 +176,6 @@ import HeaderComponent from '@/components/HeaderComponent.vue';
|
|||||||
import { neo4jApi, unifiedApi } from '@/apis/graph_api';
|
import { neo4jApi, unifiedApi } from '@/apis/graph_api';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
import GraphCanvas from '@/components/GraphCanvas.vue';
|
import GraphCanvas from '@/components/GraphCanvas.vue';
|
||||||
import GraphInfoPanel from '@/components/GraphInfoPanel.vue';
|
|
||||||
import LightRAGInfoPanel from '@/components/LightRAGInfoPanel.vue';
|
|
||||||
import GraphDetailPanel from '@/components/GraphDetailPanel.vue';
|
import GraphDetailPanel from '@/components/GraphDetailPanel.vue';
|
||||||
import UploadModal from '@/components/FileUploadModal.vue';
|
import UploadModal from '@/components/FileUploadModal.vue';
|
||||||
import { useGraph } from '@/composables/useGraph';
|
import { useGraph } from '@/composables/useGraph';
|
||||||
@ -228,13 +209,6 @@ const state = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isNeo4j = computed(() => {
|
const isNeo4j = computed(() => {
|
||||||
// 当 selectedDbId 是 'neo4j' 时,或者以 'kb_' 开头时,我们认为它是 Neo4j 驱动的
|
|
||||||
// 但是对于 kb_ 开头的,我们可能想要使用 LightRAGInfoPanel 的展示风格(因为它有 stats)
|
|
||||||
// 或者复用 GraphInfoPanel。
|
|
||||||
// GraphInfoPanel 是为 'neo4j' 全局图设计的,包含了 model matching 检查等。
|
|
||||||
// KBs (kb_*) 使用 Neo4j 存储,但逻辑上更接近 LightRAG 的"知识库"概念。
|
|
||||||
// 为了让 LightRAGInfoPanel 能够展示 stats (get_stats 已经更新支持 kb_),
|
|
||||||
// 我们这里让 kb_ ID 返回 false,这样会进入 LightRAGInfoPanel 分支。
|
|
||||||
return state.selectedDbId === 'neo4j';
|
return state.selectedDbId === 'neo4j';
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -248,6 +222,20 @@ const unindexedCount = computed(() => {
|
|||||||
return graphInfo.value?.unindexed_node_count || 0;
|
return graphInfo.value?.unindexed_node_count || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formattedGraphInfo = computed(() => {
|
||||||
|
if (isNeo4j.value) {
|
||||||
|
return {
|
||||||
|
node_count: graphInfo.value?.entity_count || 0,
|
||||||
|
edge_count: graphInfo.value?.relationship_count || 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
node_count: state.lightragStats?.total_nodes || 0,
|
||||||
|
edge_count: state.lightragStats?.total_edges || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const loadDatabases = async () => {
|
const loadDatabases = async () => {
|
||||||
state.loadingDatabases = true;
|
state.loadingDatabases = true;
|
||||||
try {
|
try {
|
||||||
@ -636,8 +624,7 @@ const goToDatabasePage = () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--gray-10);
|
background: var(--gray-10);
|
||||||
|
|
||||||
.actions,
|
.actions {
|
||||||
.footer {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user