refactor(components): 移除独立信息面板组件并整合到GraphCanvas中
将GraphInfoPanel和LightRAGInfoPanel组件删除,将其功能整合到GraphCanvas组件中 简化视图层代码,移除GraphView中不必要的模板和计算逻辑
This commit is contained in:
parent
3f7e70db31
commit
c5dde79051
@ -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;
|
||||
|
||||
@ -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
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user