refactor(components): 移除独立信息面板组件并整合到GraphCanvas中

将GraphInfoPanel和LightRAGInfoPanel组件删除,将其功能整合到GraphCanvas组件中
简化视图层代码,移除GraphView中不必要的模板和计算逻辑
This commit is contained in:
Wenjie Zhang 2025-12-20 15:44:37 +08:00
parent 3f7e70db31
commit c5dde79051
4 changed files with 78 additions and 240 deletions

View File

@ -8,6 +8,19 @@
<div class="canvas-content">
<slot name="content" />
</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">
<slot name="bottom" />
</div>
@ -26,6 +39,10 @@ const props = defineProps({
required: true,
default: () => ({ nodes: [], edges: [] })
},
graphInfo: {
type: Object,
default: () => ({})
},
labelField: { type: String, default: 'name' },
autoFit: { type: Boolean, default: true },
autoResize: { type: Boolean, default: true },
@ -423,6 +440,45 @@ defineExpose({
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 {
// 便穿
pointer-events: none;

View File

@ -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>

View File

@ -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>

View File

@ -50,6 +50,7 @@
<GraphCanvas
ref="graphRef"
:graph-data="graph.graphData"
:graph-info="formattedGraphInfo"
:highlight-keywords="[state.searchInput]"
@node-click="graph.handleNodeClick"
@edge-click="graph.handleEdgeClick"
@ -94,24 +95,6 @@
<template #content>
<a-empty v-show="graph.graphData.nodes.length === 0" style="padding: 4rem 0;"/>
</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>
<!-- 详情浮动卡片 -->
<GraphDetailPanel
@ -125,12 +108,12 @@
</div>
<a-modal
:open="state.showModal" title="上传文件"
@ok="addDocumentByFile"
@cancel="handleModalCancel"
ok-text="添加到图数据库" cancel-text="取消"
:confirm-loading="state.processing"
:ok-button-props="{ disabled: !hasValidFile }">
:open="state.showModal" title="上传文件"
@ok="addDocumentByFile"
@cancel="handleModalCancel"
ok-text="添加到图数据库" cancel-text="取消"
:confirm-loading="state.processing"
:ok-button-props="{ disabled: !hasValidFile }">
<div class="upload">
<div class="note">
<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 { useUserStore } from '@/stores/user';
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 UploadModal from '@/components/FileUploadModal.vue';
import { useGraph } from '@/composables/useGraph';
@ -228,13 +209,6 @@ const state = reactive({
})
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';
});
@ -248,6 +222,20 @@ const unindexedCount = computed(() => {
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 () => {
state.loadingDatabases = true;
try {
@ -636,8 +624,7 @@ const goToDatabasePage = () => {
overflow: hidden;
background: var(--gray-10);
.actions,
.footer {
.actions {
display: flex;
justify-content: space-between;
margin: 20px 0;