feat: 更新知识图谱组件,优化统计信息显示和图表加载逻辑
This commit is contained in:
parent
f0275aaadc
commit
b8bfc97b80
@ -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;
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user