285 lines
6.4 KiB
Vue
285 lines
6.4 KiB
Vue
<template>
|
||
<div class="graph-container" v-if="configStore.config.enable_knowledge_base">
|
||
<div class="info">
|
||
<h1>Neo4j 图数据库</h1>
|
||
<p>基于 Neo4j 构建的图数据库。</p>
|
||
</div>
|
||
<div class="actions">
|
||
<div class="actions-left">
|
||
<a-button @click="state.showModal = true">上传文件</a-button>
|
||
<a-modal
|
||
:open="state.showModal" title="上传文件"
|
||
@ok="addDocumentByFile"
|
||
@cancel="() => state.showModal = false"
|
||
ok-text="添加到图数据库" cancel-text="取消"
|
||
:confirm-loading="state.precessing">
|
||
<div class="upload">
|
||
<a-upload-dragger
|
||
class="upload-dragger"
|
||
v-model:fileList="fileList"
|
||
name="file"
|
||
:fileList="fileList"
|
||
:max-count="1"
|
||
:disabled="state.precessing"
|
||
action="/api/database/upload"
|
||
@change="handleFileUpload"
|
||
@drop="handleDrop"
|
||
>
|
||
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
||
<p class="ant-upload-hint">
|
||
目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加。
|
||
</p>
|
||
</a-upload-dragger>
|
||
</div>
|
||
</a-modal>
|
||
</div>
|
||
<div class="action-right">
|
||
<input
|
||
v-model="state.searchInput"
|
||
placeholder="输入要查询的实体"
|
||
style="width: 200px"
|
||
@keydown.enter="onSearch"
|
||
/>
|
||
<a-button
|
||
type="primary"
|
||
:loading="state.searchLoading"
|
||
@click="onSearch"
|
||
>
|
||
检索实体
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
<div class="main" id="container"></div>
|
||
|
||
</div>
|
||
<div class="database-empty" v-else>
|
||
<a-empty>
|
||
<template #description>
|
||
<span>
|
||
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link> 页面配置知识库。
|
||
</span>
|
||
</template>
|
||
</a-empty>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { Graph } from "@antv/g6";
|
||
import { computed, onMounted, reactive, ref } from 'vue';
|
||
import { message } from "ant-design-vue";
|
||
import { useConfigStore } from '@/stores/config';
|
||
|
||
const configStore = useConfigStore()
|
||
|
||
let graphInstance
|
||
const fileList = ref([]);
|
||
const subgraph = reactive({
|
||
nodes: [
|
||
{ id: '1', name: 'node1' },
|
||
{ id: '2', name: 'node2' },
|
||
{ id: '3', name: 'node3' },
|
||
{ id: '4', name: 'node4' },
|
||
{ id: '5', name: 'node5' },
|
||
],
|
||
edges: [
|
||
{ id: 'e1', source_id: '1', target_id: '2', type: 'edge1' },
|
||
{ id: 'e2', source_id: '1', target_id: '3', type: 'edge2' },
|
||
{ id: 'e3', source_id: '2', target_id: '4', type: 'edge3' },
|
||
{ id: 'e4', source_id: '2', target_id: '5', type: 'edge4' },
|
||
],
|
||
});
|
||
|
||
const state = reactive({
|
||
searchInput: '',
|
||
searchLoading: false,
|
||
showModal: false,
|
||
precessing: false,
|
||
})
|
||
|
||
const getCurWidth = () => document.getElementById("container").offsetWidth
|
||
const getCurHeight = () => document.getElementById("container").offsetHeight
|
||
|
||
const graphData = computed(() => {
|
||
return {
|
||
nodes: subgraph.nodes.map(node => {
|
||
return {
|
||
id: node.id,
|
||
data: {
|
||
label: node.name
|
||
},
|
||
}
|
||
}),
|
||
edges: subgraph.edges.map(edge => {
|
||
return {
|
||
source: edge.source_id,
|
||
target: edge.target_id,
|
||
data: {
|
||
label: edge.type
|
||
}
|
||
}
|
||
}),
|
||
}
|
||
})
|
||
|
||
const addDocumentByFile = () => {
|
||
state.precessing = true
|
||
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path)
|
||
fetch('/api/database/graph/add', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
file_path: files[0]
|
||
}),
|
||
})
|
||
.then(response => response.json())
|
||
.then((data) => {
|
||
message.success(data.message);
|
||
state.showModal = false;
|
||
})
|
||
.catch((error) => {
|
||
message.error(error.message);
|
||
})
|
||
.finally(() => state.precessing = false)
|
||
};
|
||
|
||
const onSearch = () => {
|
||
if (!state.searchInput) {
|
||
message.error('请输入要查询的实体')
|
||
return
|
||
}
|
||
|
||
state.searchLoading = true
|
||
fetch(`/api/database/graph/node?entity_name=${state.searchInput}`)
|
||
.then((res) => {
|
||
if (res.ok) {
|
||
return res.json();
|
||
} else {
|
||
throw new Error("查询失败");
|
||
}
|
||
})
|
||
.then((data) => {
|
||
subgraph.nodes = data.result.nodes
|
||
subgraph.edges = data.result.edges
|
||
console.log(data)
|
||
console.log(subgraph)
|
||
randerGraph()
|
||
})
|
||
.catch((error) => {
|
||
message.error(error.message);
|
||
})
|
||
.finally(() => state.searchLoading = false)
|
||
};
|
||
|
||
const randerGraph = () => {
|
||
graphInstance.setData(graphData.value);
|
||
graphInstance.render();
|
||
}
|
||
|
||
|
||
|
||
|
||
onMounted(() => {
|
||
graphInstance = new Graph({
|
||
container: document.getElementById("container"),
|
||
width: getCurWidth(),
|
||
height: getCurHeight(),
|
||
autoFit: true,
|
||
autoResize: true,
|
||
layout: {
|
||
type: 'force-atlas2',
|
||
preventOverlap: true,
|
||
kr: 100,
|
||
},
|
||
node: {
|
||
type: 'circle',
|
||
style: {
|
||
labelText: (d) => d.data.label,
|
||
size: 40,
|
||
},
|
||
palette: {
|
||
field: 'label',
|
||
color: 'tableau',
|
||
},
|
||
},
|
||
edge: {
|
||
type: 'line',
|
||
style: {
|
||
labelText: (d) => d.data.label,
|
||
labelBackground: '#fff',
|
||
},
|
||
},
|
||
behaviors: ['drag-element'],
|
||
});
|
||
graphInstance.setData(graphData.value);
|
||
graphInstance.render();
|
||
window.addEventListener('resize', randerGraph);
|
||
});
|
||
|
||
|
||
const handleFileUpload = (event) => {
|
||
console.log(event)
|
||
console.log(fileList.value)
|
||
}
|
||
|
||
const handleDrop = (event) => {
|
||
console.log(event)
|
||
console.log(fileList.value)
|
||
}
|
||
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.graph-container {
|
||
padding: 20px;
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px;
|
||
|
||
input {
|
||
margin-right: 10px;
|
||
border-radius: 8px;
|
||
padding: 4px 12px;
|
||
border: 2px solid #d9d9d9;
|
||
outline: none;
|
||
height: 42px;
|
||
|
||
&:focus {
|
||
border-color: var(--main-color);
|
||
}
|
||
}
|
||
|
||
button {
|
||
height: 40px;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
|
||
|
||
.upload {
|
||
margin-bottom: 20px;
|
||
|
||
.upload-dragger {
|
||
margin: 0px;
|
||
}
|
||
}
|
||
|
||
#container {
|
||
background: #F7F7F7;
|
||
margin: 20px 0;
|
||
border-radius: 16px;
|
||
width: 100%;
|
||
height: calc(100% - 200px);
|
||
}
|
||
|
||
.database-empty {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100%;
|
||
flex-direction: column;
|
||
color: var(--c-text-light-1);
|
||
}
|
||
</style>
|