feat: 更新知识图谱组件,优化统计信息显示和图表加载逻辑

This commit is contained in:
Wenjie Zhang 2025-11-06 22:46:25 +08:00
parent f0275aaadc
commit b8bfc97b80
5 changed files with 75 additions and 224 deletions

View File

@ -372,6 +372,7 @@ import { parseToShanghai } from '@/utils/time';
<style scoped>
.file-table-container {
display: flex;
flex-grow: 1;
flex-direction: column;
max-height: 100%;
overflow: hidden;

View File

@ -2,32 +2,11 @@
<div class="graph-section" v-if="isGraphSupported">
<div class="graph-toolbar">
<div class="toolbar-left">
<div v-if="graphStats.displayed_nodes > 0 || graphStats.displayed_edges > 0" class="graph-stats">
<a-tag color="blue" size="small">节点: {{ graphStats.displayed_nodes }}</a-tag>
<a-tag color="green" size="small">: {{ graphStats.displayed_edges }}</a-tag>
<div class="graph-stats">
<a-tag color="blue" size="small">节点: {{ graphStats.total_nodes || 0 }}</a-tag>
<a-tag color="green" size="small">: {{ graphStats.total_edges || 0 }}</a-tag>
</div>
</div>
<div class="toolbar-right">
<a-button
type="primary"
size="small"
@click="loadGraph"
:disabled="!isGraphSupported"
:icon='h(ReloadOutlined)'
>
加载图谱
</a-button>
<a-button
type="text"
size="small"
:icon="h(ExpandOutlined)"
title="最大化"
@click="toggleGraphMaximize"
:disabled="!isGraphSupported"
>
最大化
</a-button>
</div>
</div>
<div class="graph-container-compact">
<div v-if="!isGraphSupported" class="graph-disabled">
@ -41,12 +20,10 @@
v-else
:initial-database-id="databaseId"
:hide-db-selector="true"
:hide-stats="true"
:hide-controls="!store.state.isGraphMaximized"
:initial-limit="graphLimit"
:initial-depth="graphDepth"
@update:stats="handleStatsUpdate"
ref="graphViewerRef"
@update:stats="handleViewerStats"
/>
</div>
@ -115,7 +92,7 @@
<script setup>
import { ref, computed, watch, nextTick, onUnmounted } from 'vue';
import { useDatabaseStore } from '@/stores/database';
import { ReloadOutlined, ExpandOutlined } from '@ant-design/icons-vue';
import { ReloadOutlined } from '@ant-design/icons-vue';
import KnowledgeGraphViewer from '@/components/KnowledgeGraphViewer.vue';
import { h } from 'vue';
import { getKbTypeLabel } from '@/utils/kb_utils';
@ -139,7 +116,7 @@ const graphStats = computed({
const graphViewerRef = ref(null);
const showSettings = ref(false);
const graphLimit = ref(200);
const graphLimit = ref(50);
const graphDepth = ref(2);
const showExportModal = ref(false);
@ -168,14 +145,6 @@ const clearGraph = () => {
}
};
const toggleGraphMaximize = () => {
store.state.isGraphMaximized = !store.state.isGraphMaximized;
};
const handleStatsUpdate = (stats) => {
graphStats.value = stats;
};
const applySettings = () => {
showSettings.value = false;
// props
@ -241,6 +210,20 @@ const scheduleGraphLoad = (delay = 200) => {
}, delay);
};
// Viewer database store graphStats
const handleViewerStats = (stats) => {
if (!stats) return;
// store.graphStats使 viewer
store.graphStats = {
total_nodes: stats.total_nodes ?? store.graphStats.total_nodes ?? 0,
total_edges: stats.total_edges ?? store.graphStats.total_edges ?? 0,
displayed_nodes: stats.displayed_nodes ?? store.graphStats.displayed_nodes ?? 0,
displayed_edges: stats.displayed_edges ?? store.graphStats.displayed_edges ?? 0,
is_truncated: stats.is_truncated ?? store.graphStats.is_truncated ?? false,
}
}
watch(
() => props.active,
(active) => {
@ -252,13 +235,17 @@ watch(
);
watch(databaseId, () => {
store.graphStats = defaultGraphStats();
scheduleGraphLoad(300);
});
watch(isGraphSupported, (supported) => {
if (supported) {
scheduleGraphLoad(200);
if (!supported) {
store.graphStats = defaultGraphStats();
clearGraph();
return;
}
scheduleGraphLoad(200);
});
onUnmounted(() => {

View File

@ -71,24 +71,8 @@
>
<SearchOutlined v-if="!loading" /> 加载图谱
</a-button>
<a-button
type="default"
size="small"
:loading="loading"
@click="loadTestData"
:disabled="!selectedDatabase"
style="margin-left: 6px"
title="加载测试数据(用于演示图谱功能)"
>
<ReloadOutlined v-if="!loading" /> 测试数据
</a-button>
</div>
<div v-if="!props.hideStats" class="stats-section">
<a-tag color="blue" size="small">节点: {{ stats.displayed_nodes || 0 }}</a-tag>
<a-tag color="green" size="small">: {{ stats.displayed_edges || 0 }}</a-tag>
<!-- <a-tag v-if="stats.is_truncated" color="red" size="small">已截断</a-tag> -->
</div>
</div>
<!-- Sigma.js图可视化容器 -->
@ -98,6 +82,11 @@
:class="{ 'loading': loading }"
></div>
<div v-if="!props.hideStats" class="graph-overlay-stats">
<a-tag color="blue" size="small">节点: {{ stats.displayed_nodes || 0 }}</a-tag>
<a-tag color="green" size="small">: {{ stats.displayed_edges || 0 }}</a-tag>
</div>
<!-- 节点详情面板 -->
<div
v-if="selectedNodeData"
@ -234,7 +223,6 @@ import { ref, reactive, onMounted, onUnmounted, computed, watch, nextTick } from
import { message } from 'ant-design-vue'
import {
SearchOutlined,
ReloadOutlined,
ClearOutlined,
CloseOutlined,
PlusOutlined,
@ -248,6 +236,7 @@ import { EdgeArrowProgram } from 'sigma/rendering'
import { lightragApi } from '@/apis/graph_api'
import { useGraphStore } from '@/stores/graphStore'
import { useDatabaseStore } from '@/stores/database'
import '@/assets/css/sigma.css'
// props
@ -283,6 +272,7 @@ const emit = defineEmits(['update:stats', 'refresh-graph', 'clear-graph'])
//
const graphStore = useGraphStore()
const databaseStore = useDatabaseStore()
//
const loading = ref(false)
@ -714,9 +704,13 @@ const loadGraphData = async () => {
//
graphStore.setRawGraph(rawGraph)
// displayed total
graphStore.stats = {
displayed_nodes: graphResponse.data.nodes.length,
displayed_edges: graphResponse.data.edges.length,
// statsResponse total_nodes/total_edges
total_nodes: statsResponse.data.total_nodes ?? graphStore.stats.total_nodes ?? 0,
total_edges: statsResponse.data.total_edges ?? graphStore.stats.total_edges ?? 0,
is_truncated: graphResponse.data.is_truncated
}
@ -755,82 +749,6 @@ const loadGraphData = async () => {
}
}
//
const loadTestData = async () => {
if (!selectedDatabase.value) {
message.warning('请先选择数据库')
return
}
loading.value = true
loadingMessage.value = '加载测试数据中...'
try {
const response = await fetch(`/api/graph/lightrag/test-data?db_id=${selectedDatabase.value}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const testData = await response.json()
if (testData.success) {
//
const rawGraph = graphStore.createGraphFromApiData(
testData.data.nodes,
testData.data.edges
)
//
graphStore.setRawGraph(rawGraph)
graphStore.stats = {
displayed_nodes: testData.data.nodes.length,
displayed_edges: testData.data.edges.length,
is_truncated: testData.data.is_truncated
}
console.log('Test graph created:', {
nodes: rawGraph.nodes.length,
edges: rawGraph.edges.length,
sampleNode: rawGraph.nodes[0],
sampleEdge: rawGraph.edges[0]
})
// Sigma
const sigmaGraph = graphStore.createSigmaGraph(rawGraph)
graphStore.setSigmaGraph(sigmaGraph)
//
await applyLayout(sigmaGraph)
// Sigma
if (sigmaInstance) {
sigmaInstance.setGraph(sigmaGraph)
sigmaInstance.refresh()
} else {
await nextTick()
await initSigma()
}
message.success(`测试数据加载成功:${rawGraph.nodes.length} 个节点,${rawGraph.edges.length} 条边`)
} else {
throw new Error('Failed to load test data')
}
} catch (error) {
console.error('加载测试数据失败:', error)
message.error('加载测试数据失败: ' + error.message)
} finally {
loading.value = false
loadingMessage.value = '加载图数据中...'
}
}
//
const loadFullGraph = async () => {
selectedLabel.value = '*'
@ -1277,7 +1195,7 @@ defineExpose({
.control-panel {
background: white;
padding: 8px 0; /* Reduced from 16px */
padding: 6px 16px; /* Reduced from 16px */
border-bottom: none;
display: flex;
gap: 12px; /* Reduced from 16px */
@ -1301,9 +1219,9 @@ defineExpose({
.sigma-container {
flex: 1;
background: white;
// background: white;
position: relative; /* 确保子元素可以相对于此容器定位 */
border: 1px solid var(--main-20);
// border: 1px solid var(--main-20);
border-radius: 8px;
overflow: hidden;
@ -1312,6 +1230,16 @@ defineExpose({
}
}
.graph-overlay-stats {
position: absolute;
left: 16px;
bottom: 16px;
display: flex;
gap: 8px;
pointer-events: none;
z-index: 1100;
}
.detail-panel {
position: absolute;
width: 240px; /* Reduced from 300px */
@ -1546,4 +1474,4 @@ defineExpose({
font-size: 12px; /* Added smaller font size */
}
}
</style>
</style>

View File

@ -18,6 +18,8 @@ export const useDatabaseStore = defineStore('database', () => {
const queryParams = ref([]);
const meta = reactive({});
const graphStats = ref({
total_nodes: 0,
total_edges: 0,
displayed_nodes: 0,
displayed_edges: 0,
is_truncated: false,
@ -35,7 +37,6 @@ export const useDatabaseStore = defineStore('database', () => {
chunkLoading: false,
autoRefresh: false,
queryParamsLoading: false,
isGraphMaximized: false,
rightPanelVisible: true,
});

View File

@ -1,38 +1,5 @@
<template>
<div class="database-info-container">
<!-- Maximize Graph Modal -->
<a-modal
v-model:open="isGraphMaximized"
:footer="null"
:closable="false"
width="100%"
wrap-class-name="full-modal"
:mask-closable="false"
>
<template #title>
<div class="maximized-graph-header">
<h3>知识图谱 (最大化)</h3>
<a-button type="text" @click="toggleGraphMaximize">
<CompressOutlined /> 退出最大化
</a-button>
</div>
</template>
<div class="maximized-graph-content">
<div v-if="!isGraphSupported" class="graph-disabled">
<div class="disabled-content">
<h4>知识图谱不可用</h4>
<p>当前知识库类型 "{{ getKbTypeLabel(database.kb_type || 'lightrag') }}" 不支持知识图谱功能</p>
<p>只有 LightRAG 类型的知识库支持知识图谱</p>
</div>
</div>
<KnowledgeGraphViewer
v-else-if="isGraphMaximized"
:initial-database-id="databaseId"
:hide-db-selector="true"
/>
</div>
</a-modal>
<FileDetailModal />
<FileUploadModal
@ -79,9 +46,6 @@
import { onMounted, reactive, ref, watch, onUnmounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useDatabaseStore } from '@/stores/database';
import { getKbTypeLabel } from '@/utils/kb_utils';
import { CompressOutlined } from '@ant-design/icons-vue';
import KnowledgeGraphViewer from '@/components/KnowledgeGraphViewer.vue';
import KnowledgeBaseCard from '@/components/KnowledgeBaseCard.vue';
import FileTable from '@/components/FileTable.vue';
import FileDetailModal from '@/components/FileDetailModal.vue';
@ -96,11 +60,6 @@ const store = useDatabaseStore();
const databaseId = computed(() => store.databaseId);
const database = computed(() => store.database);
const state = computed(() => store.state);
const isGraphMaximized = computed({
get: () => store.state.isGraphMaximized,
set: (val) => store.state.isGraphMaximized = val
});
//
const isGraphSupported = computed(() => {
const kbType = database.value.kb_type?.toLowerCase();
@ -110,15 +69,34 @@ const isGraphSupported = computed(() => {
// Tab -
const activeTab = ref('query');
const resetGraphStats = () => {
store.graphStats = {
total_nodes: 0,
total_edges: 0,
displayed_nodes: 0,
displayed_edges: 0,
is_truncated: false
};
};
// LightRAG
watch(
() => [databaseId.value, isGraphSupported.value],
([newDbId, supported], oldValue = []) => {
const [oldDbId, previouslySupported] = oldValue;
if (!newDbId) {
return;
}
if (newDbId && newDbId !== oldDbId) {
resetGraphStats();
} else if (!supported && previouslySupported) {
resetGraphStats();
}
if (supported && (newDbId !== oldDbId || previouslySupported === false || previouslySupported === undefined)) {
activeTab.value = 'graph';
return;
@ -149,11 +127,7 @@ const showAddFilesModal = () => {
addFilesModalVisible.value = true;
};
//
const toggleGraphMaximize = () => {
isGraphMaximized.value = !isGraphMaximized.value;
};
//
const resetFileSelectionState = () => {
store.selectedRowKeys = [];
store.selectedFile = null;
@ -163,6 +137,7 @@ const resetFileSelectionState = () => {
watch(() => route.params.database_id, async (newId) => {
store.databaseId = newId;
resetFileSelectionState();
resetGraphStats();
store.stopAutoRefresh();
await store.getDatabaseInfo(newId, false); // Explicitly load query params on initial load
store.startAutoRefresh();
@ -360,47 +335,6 @@ const handleMouseUp = () => {
</style>
<style lang="less">
:deep(.full-modal) {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
padding: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh - 200px);
}
.ant-modal-body {
flex: 1;
}
}
.maximized-graph-header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
color: var(--gray-800);
}
}
.maximized-graph-content {
height: calc(100vh - 300px);
border-radius: 6px;
overflow: hidden;
}
/* 全局样式作为备用方案 */
.ant-popover .query-params-compact {
width: 220px;