hanyuqing 3 months ago
parent
commit
13b8847a06
  1. 1
      controller/BuilderController.py
  2. 986
      vue/src/system/GraphStyle.vue
  3. 403
      vue/src/system/KGData.vue

1
controller/BuilderController.py

@ -10,7 +10,6 @@ import traceback
from docx import Document from docx import Document
import httpx import httpx
from multipart import FormParser
from robyn import jsonify, Response, Request from robyn import jsonify, Response, Request
from app import app from app import app

986
vue/src/system/GraphStyle.vue

File diff suppressed because it is too large

403
vue/src/system/KGData.vue

@ -208,7 +208,8 @@
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
<el-dialog v-model="nodeDialogVisible" :title="isEdit ? '修改节点' : '新增节点'" width="450px" class="custom-dialog" header-class="bold-header"> <el-dialog v-model="nodeDialogVisible" :title="isEdit ? '修改节点' : '新增节点'" width="450px" class="custom-dialog"
header-class="bold-header">
<el-form :model="nodeForm" label-width="90px" class="custom-form"> <el-form :model="nodeForm" label-width="90px" class="custom-form">
<el-form-item label="名称" required> <el-form-item label="名称" required>
<el-input v-model="nodeForm.name" placeholder="请输入实体名称" clearable/> <el-input v-model="nodeForm.name" placeholder="请输入实体名称" clearable/>
@ -227,13 +228,16 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="relDialogVisible" :title="isEdit ? '修改关系' : '新增关系'" width="450px" class="custom-dialog" header-class="bold-header"> <el-dialog v-model="relDialogVisible" :title="isEdit ? '修改关系' : '新增关系'" width="450px" class="custom-dialog"
header-class="bold-header">
<el-form :model="relForm" label-width="90px" class="custom-form"> <el-form :model="relForm" label-width="90px" class="custom-form">
<el-form-item label="起始节点" required> <el-form-item label="起始节点" required>
<el-autocomplete v-model="relForm.source" :fetch-suggestions="queryNodeSearch" style="width:100%" placeholder="请输入起点名称"/> <el-autocomplete v-model="relForm.source" :fetch-suggestions="queryNodeSearch" style="width:100%"
placeholder="请输入起点名称"/>
</el-form-item> </el-form-item>
<el-form-item label="结束节点" required> <el-form-item label="结束节点" required>
<el-autocomplete v-model="relForm.target" :fetch-suggestions="queryNodeSearch" style="width:100%" placeholder="请输入终点名称"/> <el-autocomplete v-model="relForm.target" :fetch-suggestions="queryNodeSearch" style="width:100%"
placeholder="请输入终点名称"/>
</el-form-item> </el-form-item>
<el-form-item label="关系类型" required> <el-form-item label="关系类型" required>
<el-input v-model="relForm.type" placeholder="例如:TREATS"/> <el-input v-model="relForm.type" placeholder="例如:TREATS"/>
@ -245,7 +249,9 @@
<template #footer> <template #footer>
<div class="dialog-footer-wrap"> <div class="dialog-footer-wrap">
<el-button class="btn-cancel" @click="relDialogVisible = false">取消</el-button> <el-button class="btn-cancel" @click="relDialogVisible = false">取消</el-button>
<el-button class="btn-confirm" type="primary" :loading="submitting" :disabled="submitting" @click="submitRel">确认</el-button> <el-button class="btn-confirm" type="primary" :loading="submitting" :disabled="submitting" @click="submitRel">
确认
</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -253,8 +259,8 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, reactive } from 'vue' import {ref, onMounted, reactive} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import {ElMessage, ElMessageBox} from 'element-plus'
import Menu from '@/components/Menu.vue' import Menu from '@/components/Menu.vue'
import { import {
getKgStats, getLabels, getNodeSuggestions, getNodesList, getRelationshipsList, getKgStats, getLabels, getNodeSuggestions, getNodesList, getRelationshipsList,
@ -266,18 +272,18 @@ const pageSize = ref(20);
const activeName = ref('first'); const activeName = ref('first');
const loading = ref(false); const loading = ref(false);
const submitting = ref(false); // const submitting = ref(false); //
const stats = reactive({ totalNodes: 0, totalRels: 0, todayNodes: 0 }); const stats = reactive({totalNodes: 0, totalRels: 0, todayNodes: 0});
// --- --- // --- ---
const nodeData = ref([]); const nodeData = ref([]);
const nodeTotal = ref(0); const nodeTotal = ref(0);
const nodePage = ref(1); const nodePage = ref(1);
const nodeSearch = reactive({ name: '', label: '' }); const nodeSearch = reactive({name: '', label: ''});
const relData = ref([]); const relData = ref([]);
const relTotal = ref(0); const relTotal = ref(0);
const relPage = ref(1); const relPage = ref(1);
const relSearch = reactive({ source: '', target: '' }); const relSearch = reactive({source: '', target: ''});
// --- --- // --- ---
const nodeDialogVisible = ref(false); const nodeDialogVisible = ref(false);
@ -289,8 +295,8 @@ const isEdit = ref(false);
const dynamicLabels = ref([]); const dynamicLabels = ref([]);
// --- --- // --- ---
const nodeForm = reactive({ id: '', name: '', label: '' }); const nodeForm = reactive({id: '', name: '', label: ''});
const relForm = reactive({ id: '', source: '', target: '', type: '', label: '' }); const relForm = reactive({id: '', source: '', target: '', type: '', label: ''});
// --- --- // --- ---
@ -298,7 +304,9 @@ const fetchStats = async () => {
try { try {
const res = await getKgStats(); const res = await getKgStats();
if (res?.code === 200) Object.assign(stats, res.data); if (res?.code === 200) Object.assign(stats, res.data);
} catch (e) { console.error("Stats Error", e); } } catch (e) {
console.error("Stats Error", e);
}
}; };
const fetchNodes = async () => { const fetchNodes = async () => {
@ -314,8 +322,11 @@ const fetchNodes = async () => {
nodeData.value = res.data.items; nodeData.value = res.data.items;
nodeTotal.value = res.data.total; nodeTotal.value = res.data.total;
} }
} catch (e) { ElMessage.error('加载节点失败'); } } catch (e) {
finally { loading.value = false; } ElMessage.error('加载节点失败');
} finally {
loading.value = false;
}
}; };
const fetchRels = async () => { const fetchRels = async () => {
@ -331,16 +342,19 @@ const fetchRels = async () => {
relData.value = res.data.items; relData.value = res.data.items;
relTotal.value = res.data.total; relTotal.value = res.data.total;
} }
} catch (e) { ElMessage.error('加载关系失败'); } } catch (e) {
finally { loading.value = false; } ElMessage.error('加载关系失败');
} finally {
loading.value = false;
}
}; };
const openNodeDialog = (row = null) => { const openNodeDialog = (row = null) => {
isEdit.value = !!row; isEdit.value = !!row;
if (row) { if (row) {
Object.assign(nodeForm, { id: row.id, name: row.name, label: row.labels?.[0] || '' }); Object.assign(nodeForm, {id: row.id, name: row.name, label: row.labels?.[0] || ''});
} else { } else {
Object.assign(nodeForm, { id: '', name: '', label: 'Drug' }); Object.assign(nodeForm, {id: '', name: '', label: 'Drug'});
} }
nodeDialogVisible.value = true; nodeDialogVisible.value = true;
}; };
@ -362,15 +376,23 @@ const submitNode = async () => {
} }
} catch (e) { } catch (e) {
ElMessage.error('接口响应异常'); ElMessage.error('接口响应异常');
} finally { submitting.value = false; } } finally {
submitting.value = false;
}
}; };
const openRelDialog = (row = null) => { const openRelDialog = (row = null) => {
isEdit.value = !!row; isEdit.value = !!row;
if (row) { if (row) {
Object.assign(relForm, { id: row.id, source: row.source, target: row.target, type: row.type, label: row.label || '' }); Object.assign(relForm, {
id: row.id,
source: row.source,
target: row.target,
type: row.type,
label: row.label || ''
});
} else { } else {
Object.assign(relForm, { id: '', source: '', target: '', type: '', label: '' }); Object.assign(relForm, {id: '', source: '', target: '', type: '', label: ''});
} }
relDialogVisible.value = true; relDialogVisible.value = true;
}; };
@ -381,7 +403,7 @@ const submitRel = async () => {
submitting.value = true; submitting.value = true;
try { try {
const payload = isEdit.value ? { ...relForm } : { const payload = isEdit.value ? {...relForm} : {
source: relForm.source, source: relForm.source,
target: relForm.target, target: relForm.target,
type: relForm.type, type: relForm.type,
@ -400,80 +422,321 @@ const submitRel = async () => {
} }
} catch (e) { } catch (e) {
ElMessage.error('网络连接异常'); ElMessage.error('网络连接异常');
} finally { submitting.value = false; } } finally {
submitting.value = false;
}
}; };
const handleDelete = (row, type) => { const handleDelete = (row, type) => {
ElMessageBox.confirm('确认删除此项数据?', '警告', { type: 'warning' }).then(async () => { ElMessageBox.confirm('确认删除此项数据?', '警告', {type: 'warning'}).then(async () => {
const res = type === 'node' ? await deleteNode(row.id) : await deleteRelationship(row.id); const res = type === 'node' ? await deleteNode(row.id) : await deleteRelationship(row.id);
if (res?.code === 200) { if (res?.code === 200) {
ElMessage.success('删除成功'); ElMessage.success('删除成功');
type === 'node' ? fetchNodes() : fetchRels(); type === 'node' ? fetchNodes() : fetchRels();
fetchStats(); fetchStats();
} }
}).catch(() => {}); }).catch(() => {
});
}; };
const handleNodeSearch = () => { nodePage.value = 1; fetchNodes(); }; const handleNodeSearch = () => {
const handleRelSearch = () => { relPage.value = 1; fetchRels(); }; nodePage.value = 1;
fetchNodes();
};
const handleRelSearch = () => {
relPage.value = 1;
fetchRels();
};
const queryNodeSearch = async (queryString, cb) => { const queryNodeSearch = async (queryString, cb) => {
if (!queryString) return cb([]); if (!queryString) return cb([]);
const res = await getNodeSuggestions(queryString); const res = await getNodeSuggestions(queryString);
cb((res.data || []).map(n => ({ value: n }))); cb((res.data || []).map(n => ({value: n})));
}; };
const handleView = (row, type) => { const handleView = (row, type) => {
detailType.value = type; detailType.value = type;
currentDetail.value = { ...row }; currentDetail.value = {...row};
detailVisible.value = true; detailVisible.value = true;
}; };
onMounted(() => { onMounted(() => {
fetchStats(); fetchStats();
fetchNodes(); fetchNodes();
getLabels().then(res => { if(res?.code === 200) dynamicLabels.value = res.data; }); getLabels().then(res => {
if (res?.code === 200) dynamicLabels.value = res.data;
});
}); });
</script> </script>
<style scoped> <style scoped>
.knowledge-graph-data-container { background-color: #f4f7fa; display: flex; height: 100vh; width: 100vw; } .knowledge-graph-data-container {
.main-body { flex: 1; padding: 25px 40px; overflow-y: auto; } background-color: #f4f7fa;
.page-header { display: flex; align-items: center; margin-bottom: 25px; } display: flex;
.header-decorator { width: 10px; height: 26px; background-color: #165dff; border-radius: 5px; margin-right: 15px; } height: 100vh;
.header-title { font-size: 26px; font-weight: bold; color: #165dff; margin: 0; } width: 100vw;
.stat-container { display: flex; gap: 80px; margin-bottom: 30px; } }
.custom-stat-card { flex: 1; max-width: 280px; height: 200px; background: #ffffff; border-radius: 30px; padding: 0 35px; box-shadow: 0 0 40px 0px rgba(22, 93, 255, 0.12); border: 1px solid #ffffff; display: flex; align-items: center; transition: transform 0.3s ease; }
.stat-inner { display: flex; flex-direction: column; align-items: flex-start; width: 100%; } .main-body {
.stat-label { color: #636364; font-size: 21px; font-weight: 600; margin-bottom: 15px; } flex: 1;
.stat-value { color: #165dff; font-size: 48px; font-weight: 800; margin-bottom: 15px; line-height: 1; } padding: 25px 40px;
.stat-desc { color: #999; font-size: 14px; line-height: 1.4; } overflow-y: auto;
.data-content-wrapper { margin-top: 20px; display: flex; flex-direction: column; } }
.custom-folder-tabs { display: flex; padding-left: 60px; }
.folder-tab-item { padding: 8px 20px; font-size: 12px; color: #86909c; cursor: pointer; background-color: #ecf2ff; border: 1px solid #dcdfe6; border-bottom: none; border-radius: 8px 8px 0 0; } .page-header {
.folder-tab-item.active { background-color: #f1f6ff !important; color: #2869ff; font-weight: bold; border: 2px solid #6896ff; border-bottom: 2px solid #ffffff; margin-bottom: -1px; z-index: 3; } display: flex;
.data-card-container { background: #ffffff; border-radius: 30px; padding: 40px 20px; box-shadow: 0 0 40px 0px rgba(22, 93, 255, 0.15); border: 1px solid #eff4ff; position: relative; z-index: 2; min-height: 300px; } align-items: center;
.filter-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; } margin-bottom: 25px;
.filter-inputs { display: flex; gap: 35px; flex-wrap: nowrap; } }
.input-group-inline { display: flex; align-items: center; gap: 12px; flex-shrink: 0; white-space: nowrap; }
.filter-label-text { font-size: 14px; color: #165dff; font-weight: 600; flex-shrink: 0; } .header-decorator {
.search-input, .search-select { width: 200px !important; } width: 10px;
.btn-search-ref { background: #165dff !important; border-radius: 8px; height: 38px; } height: 26px;
.btn-orange { background: #ffb142 !important; color: white !important; border-radius: 8px; height: 38px; border: none !important; } background-color: #165dff;
.table-compact { border-radius: 16px; box-shadow: 0 4px 20px rgba(22, 93, 255, 0.08); overflow: hidden; } border-radius: 5px;
.ref-table :deep(.el-table__header) th { background-color: #e8f0ff !important; color: #2869ff; font-weight: 700; } margin-right: 15px;
.op-group { display: flex; gap: 8px; justify-content: center; } }
.ref-op-btn { border: none !important; color: white !important; padding: 6px 14px !important; border-radius: 8px !important; }
.ref-op-btn.edit { background-color: #4379ff !important; } .header-title {
.ref-op-btn.delete { background-color: #ff6060 !important; } font-size: 26px;
.ref-op-btn.view { background-color: #ffb142 !important; } font-weight: bold;
.pagination-footer { margin-top: 20px; display: flex; justify-content: flex-end; } color: #165dff;
:deep(.bold-header) { padding: 20px 25px !important; margin-right: 0 !important; display: flex !important; justify-content: flex-start !important; } margin: 0;
:deep(.bold-header .el-dialog__title) { font-family: "Microsoft YaHei", sans-serif !important; font-weight: 900 !important; font-size: 22px !important; color: #000000 !important; } }
.custom-form :deep(.el-form-item__label) { color: #767676 !important; font-weight: bold !important; }
.dialog-footer-wrap { display: flex; justify-content: flex-end; gap: 15px; padding: 10px 0; } .stat-container {
.btn-cancel { background-color: #e6e6e6 !important; border: none !important; color: #444 !important; padding: 10px 25px !important; font-weight: 500; } display: flex;
.btn-confirm { background-color: #165dff !important; border: none !important; padding: 10px 25px !important; font-weight: 500; } gap: 80px;
.animate-fade { animation: fadeIn 0.4s ease-out; } margin-bottom: 30px;
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } }
.custom-stat-card {
flex: 1;
max-width: 280px;
height: 200px;
background: #ffffff;
border-radius: 30px;
padding: 0 35px;
box-shadow: 0 0 40px 0px rgba(22, 93, 255, 0.12);
border: 1px solid #ffffff;
display: flex;
align-items: center;
transition: transform 0.3s ease;
}
.stat-inner {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.stat-label {
color: #636364;
font-size: 21px;
font-weight: 600;
margin-bottom: 15px;
}
.stat-value {
color: #165dff;
font-size: 48px;
font-weight: 800;
margin-bottom: 15px;
line-height: 1;
}
.stat-desc {
color: #999;
font-size: 14px;
line-height: 1.4;
}
.data-content-wrapper {
margin-top: 20px;
display: flex;
flex-direction: column;
}
.custom-folder-tabs {
display: flex;
padding-left: 60px;
}
.folder-tab-item {
padding: 8px 20px;
font-size: 12px;
color: #86909c;
cursor: pointer;
background-color: #ecf2ff;
border: 1px solid #dcdfe6;
border-bottom: none;
border-radius: 8px 8px 0 0;
}
.folder-tab-item.active {
background-color: #f1f6ff !important;
color: #2869ff;
font-weight: bold;
border: 2px solid #6896ff;
border-bottom: 2px solid #ffffff;
margin-bottom: -1px;
z-index: 3;
}
.data-card-container {
background: #ffffff;
border-radius: 30px;
padding: 40px 20px;
box-shadow: 0 0 40px 0px rgba(22, 93, 255, 0.15);
border: 1px solid #eff4ff;
position: relative;
z-index: 2;
min-height: 300px;
}
.filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.filter-inputs {
display: flex;
gap: 35px;
flex-wrap: nowrap;
}
.input-group-inline {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
white-space: nowrap;
}
.filter-label-text {
font-size: 14px;
color: #165dff;
font-weight: 600;
flex-shrink: 0;
}
.search-input, .search-select {
width: 200px !important;
}
.btn-search-ref {
background: #165dff !important;
border-radius: 8px;
height: 38px;
}
.btn-orange {
background: #ffb142 !important;
color: white !important;
border-radius: 8px;
height: 38px;
border: none !important;
}
.table-compact {
border-radius: 16px;
box-shadow: 0 4px 20px rgba(22, 93, 255, 0.08);
overflow: hidden;
}
.ref-table :deep(.el-table__header) th {
background-color: #e8f0ff !important;
color: #2869ff;
font-weight: 700;
}
.op-group {
display: flex;
gap: 8px;
justify-content: center;
}
.ref-op-btn {
border: none !important;
color: white !important;
padding: 6px 14px !important;
border-radius: 8px !important;
}
.ref-op-btn.edit {
background-color: #4379ff !important;
}
.ref-op-btn.delete {
background-color: #ff6060 !important;
}
.ref-op-btn.view {
background-color: #ffb142 !important;
}
.pagination-footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.bold-header) {
padding: 20px 25px !important;
margin-right: 0 !important;
display: flex !important;
justify-content: flex-start !important;
}
:deep(.bold-header .el-dialog__title) {
font-family: "Microsoft YaHei", sans-serif !important;
font-weight: 900 !important;
font-size: 22px !important;
color: #000000 !important;
}
.custom-form :deep(.el-form-item__label) {
color: #767676 !important;
font-weight: bold !important;
}
.dialog-footer-wrap {
display: flex;
justify-content: flex-end;
gap: 15px;
padding: 10px 0;
}
.btn-cancel {
background-color: #e6e6e6 !important;
border: none !important;
color: #444 !important;
padding: 10px 25px !important;
font-weight: 500;
}
.btn-confirm {
background-color: #165dff !important;
border: none !important;
padding: 10px 25px !important;
font-weight: 500;
}
.animate-fade {
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style> </style>
Loading…
Cancel
Save