2025-08-05 18:00:45 +08:00
|
|
|
<template>
|
2025-11-06 19:47:22 +08:00
|
|
|
<div class="graph-section" v-if="isGraphSupported">
|
|
|
|
|
<div class="graph-container-compact">
|
2025-08-05 18:00:45 +08:00
|
|
|
<div v-if="!isGraphSupported" class="graph-disabled">
|
|
|
|
|
<div class="disabled-content">
|
|
|
|
|
<h4>知识图谱不可用</h4>
|
|
|
|
|
<p>当前知识库类型 "{{ kbTypeLabel }}" 不支持知识图谱功能。</p>
|
|
|
|
|
<p>只有 LightRAG 类型的知识库支持知识图谱。</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-15 23:25:56 +08:00
|
|
|
<div v-else class="graph-wrapper">
|
|
|
|
|
<GraphCanvas
|
|
|
|
|
ref="graphRef"
|
|
|
|
|
:graph-data="graph.graphData"
|
|
|
|
|
@node-click="graph.handleNodeClick"
|
|
|
|
|
@edge-click="graph.handleEdgeClick"
|
|
|
|
|
@canvas-click="graph.handleCanvasClick"
|
|
|
|
|
>
|
|
|
|
|
<template #top>
|
|
|
|
|
<div class="compact-actions">
|
|
|
|
|
<a-input-search
|
|
|
|
|
v-model:value="searchInput"
|
|
|
|
|
placeholder="搜索实体"
|
|
|
|
|
style="width: 200px"
|
|
|
|
|
@search="onSearch"
|
|
|
|
|
allow-clear
|
|
|
|
|
/>
|
|
|
|
|
<a-button
|
|
|
|
|
type="text"
|
|
|
|
|
:icon="h(ReloadOutlined)"
|
|
|
|
|
:loading="graph.fetching"
|
|
|
|
|
@click="loadGraph"
|
|
|
|
|
title="刷新"
|
|
|
|
|
/>
|
|
|
|
|
<a-button
|
|
|
|
|
type="text"
|
|
|
|
|
:icon="h(SettingOutlined)"
|
|
|
|
|
@click="showSettings = true"
|
|
|
|
|
title="设置"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</GraphCanvas>
|
|
|
|
|
|
|
|
|
|
<!-- 详情浮动卡片 -->
|
|
|
|
|
<GraphDetailPanel
|
|
|
|
|
:visible="graph.showDetailDrawer"
|
|
|
|
|
:item="graph.selectedItem"
|
|
|
|
|
:type="graph.selectedItemType"
|
|
|
|
|
@close="graph.handleCanvasClick"
|
|
|
|
|
style="top: 50px; right: 10px;"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-08-05 18:00:45 +08:00
|
|
|
</div>
|
2025-08-10 21:58:41 +08:00
|
|
|
|
|
|
|
|
<!-- 设置模态框 -->
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:open="showSettings"
|
|
|
|
|
title="图谱设置"
|
|
|
|
|
:footer="null"
|
|
|
|
|
width="300px"
|
|
|
|
|
>
|
|
|
|
|
<div class="settings-form">
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
<a-form-item label="最大节点数 (limit)">
|
|
|
|
|
<a-input-number
|
|
|
|
|
v-model:value="graphLimit"
|
|
|
|
|
:min="10"
|
|
|
|
|
:max="1000"
|
|
|
|
|
:step="10"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="搜索深度 (depth)">
|
|
|
|
|
<a-input-number
|
|
|
|
|
v-model:value="graphDepth"
|
|
|
|
|
:min="1"
|
|
|
|
|
:max="5"
|
|
|
|
|
:step="1"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item>
|
|
|
|
|
<a-button type="primary" @click="applySettings" style="width: 100%">
|
|
|
|
|
应用
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
</div>
|
|
|
|
|
</a-modal>
|
2025-08-05 18:00:45 +08:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-12-15 23:25:56 +08:00
|
|
|
import { ref, computed, watch, nextTick, onUnmounted, reactive, h } from 'vue';
|
2025-08-05 18:00:45 +08:00
|
|
|
import { useDatabaseStore } from '@/stores/database';
|
2025-12-15 23:25:56 +08:00
|
|
|
import { ReloadOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
|
|
|
|
import GraphCanvas from '@/components/GraphCanvas.vue';
|
|
|
|
|
import GraphDetailPanel from '@/components/GraphDetailPanel.vue';
|
2025-11-06 21:59:48 +08:00
|
|
|
import { getKbTypeLabel } from '@/utils/kb_utils';
|
2025-12-15 23:25:56 +08:00
|
|
|
import { unifiedApi } from '@/apis/graph_api';
|
|
|
|
|
import { message } from 'ant-design-vue';
|
|
|
|
|
import { useGraph } from '@/composables/useGraph';
|
2025-11-06 21:59:48 +08:00
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
active: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
const store = useDatabaseStore();
|
|
|
|
|
|
|
|
|
|
const databaseId = computed(() => store.databaseId);
|
|
|
|
|
const kbType = computed(() => store.database.kb_type);
|
2025-11-06 21:59:48 +08:00
|
|
|
const kbTypeLabel = computed(() => getKbTypeLabel(kbType.value || 'lightrag'));
|
2025-08-05 18:00:45 +08:00
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
const graphRef = ref(null);
|
2025-08-10 21:58:41 +08:00
|
|
|
const showSettings = ref(false);
|
2025-11-06 22:46:25 +08:00
|
|
|
const graphLimit = ref(50);
|
2025-08-10 21:58:41 +08:00
|
|
|
const graphDepth = ref(2);
|
2025-12-15 23:25:56 +08:00
|
|
|
const searchInput = ref('');
|
2025-08-10 21:58:41 +08:00
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
const graph = reactive(useGraph(graphRef));
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
// 计算属性:是否支持知识图谱
|
|
|
|
|
const isGraphSupported = computed(() => {
|
|
|
|
|
const type = kbType.value?.toLowerCase();
|
|
|
|
|
return type === 'lightrag';
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-06 21:59:48 +08:00
|
|
|
let pendingLoadTimer = null;
|
|
|
|
|
|
2025-11-10 22:33:47 +08:00
|
|
|
const loadGraph = async () => {
|
2025-12-15 23:25:56 +08:00
|
|
|
if (!databaseId.value || !isGraphSupported.value) return;
|
|
|
|
|
|
|
|
|
|
graph.fetching = true;
|
|
|
|
|
try {
|
|
|
|
|
const res = await unifiedApi.getSubgraph({
|
|
|
|
|
db_id: databaseId.value,
|
|
|
|
|
node_label: searchInput.value || '*',
|
|
|
|
|
max_nodes: graphLimit.value,
|
|
|
|
|
max_depth: graphDepth.value
|
2025-11-10 22:33:47 +08:00
|
|
|
});
|
2025-08-05 18:00:45 +08:00
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
if (res.success && res.data) {
|
|
|
|
|
graph.updateGraphData(res.data.nodes, res.data.edges);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load graph:', e);
|
|
|
|
|
message.error('加载图谱失败');
|
|
|
|
|
} finally {
|
|
|
|
|
graph.fetching = false;
|
2025-08-05 18:00:45 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-10 21:58:41 +08:00
|
|
|
const applySettings = () => {
|
|
|
|
|
showSettings.value = false;
|
2025-12-15 23:25:56 +08:00
|
|
|
loadGraph();
|
2025-08-10 21:58:41 +08:00
|
|
|
};
|
|
|
|
|
|
2025-12-15 23:25:56 +08:00
|
|
|
const onSearch = () => {
|
|
|
|
|
loadGraph();
|
|
|
|
|
}
|
2025-08-10 21:58:41 +08:00
|
|
|
|
2025-11-06 21:59:48 +08:00
|
|
|
const scheduleGraphLoad = (delay = 200) => {
|
2025-11-10 22:33:47 +08:00
|
|
|
// 确保组件激活且数据库支持图谱功能
|
2025-12-15 23:25:56 +08:00
|
|
|
if (!props.active || !isGraphSupported.value || !databaseId.value) {
|
2025-11-06 21:59:48 +08:00
|
|
|
return;
|
|
|
|
|
}
|
2025-11-10 22:33:47 +08:00
|
|
|
|
2025-11-06 21:59:48 +08:00
|
|
|
if (pendingLoadTimer) {
|
|
|
|
|
clearTimeout(pendingLoadTimer);
|
|
|
|
|
}
|
2025-11-10 22:33:47 +08:00
|
|
|
pendingLoadTimer = setTimeout(async () => {
|
|
|
|
|
await nextTick();
|
|
|
|
|
if (props.active && isGraphSupported.value && databaseId.value) {
|
|
|
|
|
await loadGraph();
|
|
|
|
|
}
|
2025-11-06 21:59:48 +08:00
|
|
|
}, delay);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-06 22:46:25 +08:00
|
|
|
|
2025-11-06 21:59:48 +08:00
|
|
|
watch(
|
|
|
|
|
() => props.active,
|
|
|
|
|
(active) => {
|
|
|
|
|
if (active) {
|
|
|
|
|
scheduleGraphLoad();
|
2025-08-05 18:00:45 +08:00
|
|
|
}
|
2025-11-06 21:59:48 +08:00
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(databaseId, () => {
|
2025-12-15 23:25:56 +08:00
|
|
|
graph.clearGraph();
|
2025-11-10 22:33:47 +08:00
|
|
|
if (isGraphSupported.value) {
|
|
|
|
|
scheduleGraphLoad(300);
|
|
|
|
|
}
|
2025-11-06 21:59:48 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(isGraphSupported, (supported) => {
|
2025-11-06 22:46:25 +08:00
|
|
|
if (!supported) {
|
2025-12-15 23:25:56 +08:00
|
|
|
graph.clearGraph();
|
2025-11-06 22:46:25 +08:00
|
|
|
return;
|
2025-11-06 21:59:48 +08:00
|
|
|
}
|
2025-11-06 22:46:25 +08:00
|
|
|
scheduleGraphLoad(200);
|
2025-11-06 21:59:48 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (pendingLoadTimer) {
|
|
|
|
|
clearTimeout(pendingLoadTimer);
|
|
|
|
|
pendingLoadTimer = null;
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
.graph-section {
|
2025-11-06 19:47:22 +08:00
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
2025-12-15 23:25:56 +08:00
|
|
|
position: relative;
|
2025-11-06 19:47:22 +08:00
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
2025-11-06 19:47:22 +08:00
|
|
|
.graph-container-compact {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
2025-12-15 23:25:56 +08:00
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.graph-wrapper {
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.compact-actions {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 10px;
|
|
|
|
|
left: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
background: var(--color-trans-light);
|
|
|
|
|
backdrop-filter: blur(4px);
|
|
|
|
|
padding: 6px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 0px 4px var(--shadow-3);
|
2025-11-06 19:47:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.graph-disabled {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.disabled-content {
|
|
|
|
|
text-align: center;
|
2025-11-23 01:39:44 +08:00
|
|
|
color: var(--gray-400);
|
2025-11-06 19:47:22 +08:00
|
|
|
|
|
|
|
|
h4 {
|
|
|
|
|
margin-bottom: 8px;
|
2025-08-05 18:00:45 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-15 23:25:56 +08:00
|
|
|
</style>
|