Browse Source

知识图谱数据功能修复

mh
hanyuqing 3 months ago
parent
commit
f551df1944
  1. 57
      controller/OperationController.py
  2. 89
      service/OperationService.py
  3. 26
      vue/src/api/data.js
  4. 253
      vue/src/system/KGData.vue

57
controller/OperationController.py

@ -8,6 +8,7 @@ from urllib.parse import unquote
# 实例化业务逻辑对象
operation_service = OperationService()
# --- 核心工具函数 ---
def create_response(status_code, data_dict):
@ -20,6 +21,7 @@ def create_response(status_code, data_dict):
headers={"Content-Type": "application/json; charset=utf-8"}
)
def parse_request_body(req):
"""
解析器适配 Robyn 框架确保能准确拿到前端传来的 IDnodeIdname label
@ -57,6 +59,7 @@ def parse_request_body(req):
print(f"Request Body Parse Error: {e}")
return {}
def get_query_param(req, key, default=""):
"""
提取 URL 查询参数
@ -79,7 +82,24 @@ def get_query_param(req, key, default=""):
print(f"Get Param Error ({key}): {e}")
return default
# --- 1. 获取全量动态标签 ---
# --- 0. 数据治理修复接口 ---
@app.post("/api/kg/admin/fix-ids")
def fix_node_ids(req):
"""
手动触发修复数据库中 nodeId 为空或为 0 的存量数据
"""
try:
result = operation_service.fix_all_missing_node_ids()
return create_response(200, {
"code": 200 if result.get("success") else 500,
"msg": result.get("msg")
})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"修复接口异常: {str(e)}"})
# --- 1. 获取全量动态标签 (节点管理用) ---
@app.get("/api/kg/labels")
def get_labels(req):
try:
@ -89,6 +109,21 @@ def get_labels(req):
traceback.print_exc()
return create_response(200, {"code": 500, "msg": f"获取标签失败: {str(e)}"})
# --- 新增:获取全量动态关系类型 (关系管理用) ---
@app.get("/api/kg/relationship-types")
def get_rel_types(req):
"""
从数据库动态获取所有关系类型 type 及其 label 映射
"""
try:
rel_types = operation_service.get_all_relationship_types()
return create_response(200, {"code": 200, "data": rel_types, "msg": "success"})
except Exception as e:
traceback.print_exc()
return create_response(200, {"code": 500, "msg": f"获取关系类型失败: {str(e)}"})
# --- 2. 输入联想建议 ---
@app.get("/api/kg/node/suggest")
def suggest_node(req):
@ -99,7 +134,8 @@ def suggest_node(req):
except Exception as e:
return create_response(200, {"code": 500, "msg": str(e)})
# --- 3. 获取分页节点列表 (无变动,Service已处理nodeId) ---
# --- 3. 获取分页节点列表 ---
@app.get("/api/kg/nodes")
def get_nodes(req):
try:
@ -120,6 +156,7 @@ def get_nodes(req):
traceback.print_exc()
return create_response(200, {"code": 500, "msg": f"获取节点失败: {str(e)}"})
# --- 4. 获取分页关系列表 ---
@app.get("/api/kg/relationships")
def get_relationships(req):
@ -143,6 +180,7 @@ def get_relationships(req):
traceback.print_exc()
return create_response(200, {"code": 500, "msg": f"获取关系失败: {str(e)}"})
# --- 5. 新增节点 ---
@app.post("/api/kg/node/add")
def add_node(req):
@ -150,9 +188,7 @@ def add_node(req):
body = parse_request_body(req)
label = str(body.get("label", "Drug")).strip()
name = str(body.get("name", "")).strip()
# 注意:Service 里的 add_node 目前只接了 label 和 name,
# timestamp 由 Service 生成。如果以后需要接收前端传的 nodeId,
# 需在 Service 增加对应形参。目前保持兼容。
if not name:
return create_response(200, {"code": 400, "msg": "名称不能为空"})
@ -164,12 +200,12 @@ def add_node(req):
except Exception as e:
return create_response(200, {"code": 500, "msg": f"新增异常: {str(e)}"})
# --- 6. 修改节点 ---
@app.post("/api/kg/node/update")
def update_node(req):
try:
body = parse_request_body(req)
# elementId 依然作为核心修改标识
node_id = body.get("id")
name = str(body.get("name", "")).strip()
label = str(body.get("label", "")).strip()
@ -177,7 +213,6 @@ def update_node(req):
if not node_id or not name:
return create_response(200, {"code": 400, "msg": "参数缺失: 修改必须包含ID和名称"})
# 调用 Service,注意参数顺序:node_id, name, label
result = operation_service.update_node(node_id, name, label)
return create_response(200, {
"code": 200 if result.get("success") else 400,
@ -186,6 +221,7 @@ def update_node(req):
except Exception as e:
return create_response(200, {"code": 500, "msg": f"更新异常: {str(e)}"})
# --- 7. 新增关系 ---
@app.post("/api/kg/rel/add")
def add_relationship(req):
@ -194,7 +230,6 @@ def add_relationship(req):
source = str(body.get("source", "")).strip()
target = str(body.get("target", "")).strip()
rel_type = str(body.get("type", "")).strip()
# label 属性在 Neo4j 关系中常用于可视化展示
rel_label = str(body.get("label", "")).strip() or rel_type
if not all([source, target, rel_type]):
@ -208,6 +243,7 @@ def add_relationship(req):
except Exception as e:
return create_response(200, {"code": 500, "msg": f"新增关系异常: {str(e)}"})
# --- 8. 修改关系 ---
@app.post("/api/kg/rel/update")
def update_rel(req):
@ -230,12 +266,13 @@ def update_rel(req):
except Exception as e:
return create_response(200, {"code": 500, "msg": f"修改关系异常: {str(e)}"})
# --- 9. 删除节点 ---
@app.post("/api/kg/node/delete")
def delete_node(req):
try:
body = parse_request_body(req)
node_id = body.get("id") # 这里传的是 elementId
node_id = body.get("id")
if not node_id:
return create_response(200, {"code": 400, "msg": "删除失败: 未指定节点系统ID"})
@ -247,6 +284,7 @@ def delete_node(req):
except Exception as e:
return create_response(200, {"code": 500, "msg": f"删除节点异常: {str(e)}"})
# --- 10. 删除关系 ---
@app.post("/api/kg/rel/delete")
def delete_rel(req):
@ -264,6 +302,7 @@ def delete_rel(req):
except Exception as e:
return create_response(200, {"code": 500, "msg": f"删除关系异常: {str(e)}"})
# --- 11. 获取图谱全局统计数据 ---
@app.get("/api/kg/stats")
def get_kg_stats(req):

89
service/OperationService.py

@ -1,13 +1,36 @@
import json
import traceback
import datetime
import random
import time
from urllib.parse import unquote
from util.neo4j_utils import neo4j_client
class OperationService:
def __init__(self):
self.db = neo4j_client
# --- 0. 数据修复工具 ---
def fix_all_missing_node_ids(self):
try:
check_cypher = "MATCH (n) WHERE n.nodeId IS NULL OR n.nodeId = 0 OR n.nodeId = '0' RETURN count(n) as cnt"
res = self.db.execute_read(check_cypher)
if not res or res[0]['cnt'] == 0:
return {"success": True, "msg": "没有需要修复的节点"}
update_cypher = """
MATCH (n)
WHERE n.nodeId IS NULL OR n.nodeId = 0 OR n.nodeId = '0'
WITH n, toInteger(100000 + rand() * 899999) as newId
SET n.nodeId = newId
RETURN count(n) as fixedCount
"""
result = self.db.execute_write_and_return(update_cypher)
return {"success": True, "msg": f"修复完成,共处理 {result[0]['fixedCount']} 个节点"}
except Exception as e:
return {"success": False, "msg": f"修复失败: {str(e)}"}
# --- 1. 全局统计接口 ---
def get_kg_stats(self):
try:
@ -58,7 +81,6 @@ class OperationService:
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
# 核心 Cypher:同时返回 elementId(用于后端操作) 和 nodeId(用于前端展示)
cypher = f"""
MATCH (n)
{where_clause}
@ -76,9 +98,10 @@ class OperationService:
items = []
for item in raw_data:
db_node_id = item.get("nodeId")
items.append({
"id": item["id"], # 后端操作用的 elementId
"nodeId": item.get("nodeId") or item["id"], # 前端展示用的业务ID,若无则降级
"id": item["id"],
"nodeId": db_node_id if (db_node_id and db_node_id != 0 and db_node_id != '0') else item["id"],
"labels": item["labels"],
"name": item.get("name") or "N/A"
})
@ -160,27 +183,31 @@ class OperationService:
try:
nm = unquote(str(name)).strip()
if not nm: return {"success": False, "msg": "名称不能为空"}
create_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
now = datetime.datetime.now()
create_time = now.strftime('%Y-%m-%d %H:%M:%S')
# 查重
check_cypher = "MATCH (n) WHERE n.name = $name RETURN n LIMIT 1"
existing = self.db.execute_read(check_cypher, {"name": nm})
if existing:
return {"success": False, "msg": f"添加失败:已存在名为 '{nm}' 的节点"}
# nodeId 使用当前毫秒时间戳,确保其为业务可读的短ID
new_node_id = int(time.time() * 1000)
create_cypher = f"""
CREATE (n:`{label}` {{
name: $name,
nodeId: timestamp(),
nodeId: $nodeId,
createTime: $createTime
}})
RETURN n
"""
# 使用 write_and_return 确保能拿到返回对象,从而判断成功
result = self.db.execute_write_and_return(create_cypher, {"name": nm, "createTime": create_time})
result = self.db.execute_write_and_return(create_cypher, {
"name": nm,
"nodeId": new_node_id,
"createTime": create_time
})
if result:
return {"success": True, "msg": "添加成功"}
return {"success": True, "msg": "添加成功", "nodeId": new_node_id}
return {"success": False, "msg": "节点创建失败"}
except Exception as e:
return {"success": False, "msg": f"写入失败: {str(e)}"}
@ -190,13 +217,11 @@ class OperationService:
nm = unquote(str(name)).strip()
if not nm: return {"success": False, "msg": "名称不能为空"}
# 排除自身查重
check_name = "MATCH (n) WHERE n.name = $name AND elementId(n) <> $id RETURN n LIMIT 1"
existing = self.db.execute_read(check_name, {"name": nm, "id": node_id})
if existing:
return {"success": False, "msg": f"修改失败:库中已有其他名为 '{nm}' 的节点"}
# 修改标签需要先移除旧标签(Neo4j不支持直接覆盖所有标签)
cypher = f"""
MATCH (n) WHERE elementId(n) = $id
SET n.name = $name
@ -216,7 +241,6 @@ class OperationService:
def delete_node(self, node_id: str):
try:
# 使用 RETURN count(n) 来确认是否执行了删除
cypher = "MATCH (n) WHERE elementId(n) = $id DETACH DELETE n RETURN 1 as deleted"
result = self.db.execute_write_and_return(cypher, {"id": node_id})
if result:
@ -229,24 +253,50 @@ class OperationService:
cypher = "CALL db.labels()"
try:
results = self.db.execute_read(cypher)
# 处理 Neo4j 返回的列表格式
labels = [list(row.values())[0] for row in results]
return labels if labels else ["Drug", "Disease", "Symptom"]
except:
return ["Drug", "Disease", "Symptom"]
# --- 6. 关系管理 ---
# 新增:动态获取全库关系类型及其对应的中文映射
def get_all_relationship_types(self):
"""
获取数据库中所有的关系类型 type 及其对应的中文 label 映射
"""
cypher = """
MATCH ()-[r]->()
RETURN DISTINCT type(r) AS type, r.label AS label
"""
try:
results = self.db.execute_read(cypher)
type_map = []
seen_types = set()
for row in results:
t_name = row["type"]
t_label = row["label"] if row.get("label") else t_name # 若没label属性则降级显示type
# 去重逻辑:确保每个 type 只出现一次(以第一个发现的 label 为准)
if t_name not in seen_types:
type_map.append({
"type": t_name,
"label": t_label
})
seen_types.add(t_name)
return type_map if type_map else []
except Exception as e:
print(f"Fetch RelTypes Error: {e}")
return []
def add_relationship(self, source_name: str, target_name: str, rel_type: str, rel_label: str):
try:
# 1. 数据清洗
s = unquote(str(source_name)).strip()
t = unquote(str(target_name)).strip()
l = str(rel_label).strip()
clean_rel_type = rel_type.strip().replace("`", "")
create_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 2. 预检:节点是否存在 (仿照 add_node 的 check_cypher)
# 使用 LIMIT 1 避免同名节点导致的笛卡尔积重复
check_nodes = """
OPTIONAL MATCH (a) WHERE a.name = $s
WITH a LIMIT 1
@ -262,14 +312,11 @@ class OperationService:
if not nodes_res[0]['hasB']: err_msg += f"结束节点'{t}'不存在"
return {"success": False, "msg": err_msg}
# 3. 查重:检查是否已存在相同关系 (仿照 add_node 的查重逻辑)
check_rel = f"MATCH (a {{name: $s}})-[r:`{clean_rel_type}`]->(b {{name: $t}}) RETURN r LIMIT 1"
existing_rel = self.db.execute_read(check_rel, {"s": s, "t": t})
if existing_rel:
return {"success": False, "msg": f"添加失败:已存在该关系"}
# 4. 写入:创建新关系
# 同样使用 WITH...LIMIT 1 确保即使有重名点也只连一对线
create_cypher = f"""
MATCH (a {{name: $s}}), (b {{name: $t}})
WITH a, b LIMIT 1
@ -301,13 +348,11 @@ class OperationService:
old = self.db.execute_read(find_old, {"id": rel_id})
if not old: return {"success": False, "msg": "修改失败:原关系不存在"}
# 如果只是修改 label,不修改节点和类型,则直接 SET
if old[0]['s'] == s and old[0]['t'] == t and old[0]['type'] == rel_type:
update_cypher = "MATCH ()-[r]->() WHERE elementId(r) = $id SET r.label = $l RETURN r"
self.db.execute_write_and_return(update_cypher, {"id": rel_id, "l": l})
return {"success": True, "msg": "修改成功"}
else:
# 涉及节点或类型变动,由于 Neo4j 不支持直接更改关系类型,需删掉重建
self.delete_relationship(rel_id)
return self.add_relationship(s, t, rel_type, l)
except Exception as e:

26
vue/src/api/data.js

@ -5,6 +5,17 @@ import request from '@/utils/request';
* 知识图谱管理接口
*/
// --- 存量数据 ID 自动修复 ---
/**
* 触发后端检查并修复 nodeId 0 或缺失的节点
*/
export function fixNodeIds() {
return request({
url: '/api/kg/admin/fix-ids',
method: 'post'
})
}
// --- 0. 获取图谱全局统计数据 ---
export function getKgStats() {
return request({
@ -13,7 +24,7 @@ export function getKgStats() {
})
}
// --- 1. 获取全量动态标签 ---
// --- 1. 获取全量动态标签 (用于节点管理下拉框) ---
export function getLabels() {
return request({
url: '/api/kg/labels',
@ -21,6 +32,18 @@ export function getLabels() {
})
}
// --- 新增:获取全量动态关系类型 (用于关系管理下拉框) ---
/**
* 从后端获取所有关系类型 type 及其对应的中文 label 映射
* 返回格式示例: [{type: 'adverseReactions', label: '不良反应'}, ...]
*/
export function getRelationshipTypes() {
return request({
url: '/api/kg/relationship-types',
method: 'get'
})
}
// --- 2. 输入联想建议 ---
export function getNodeSuggestions(keyword) {
return request({
@ -89,7 +112,6 @@ export function deleteNode(id) {
return request({
url: '/api/kg/node/delete',
method: 'post',
// 建议封装成对象,以便后端 parse_request_body 统一处理
data: { id }
})
}

253
vue/src/system/KGData.vue

@ -72,7 +72,8 @@
@clear="handleNodeSearch"
class="search-select">
<el-option label="全部" value="全部"/>
<el-option v-for="tag in dynamicLabels" :key="tag" :label="tag" :value="tag"/>
<el-option v-for="item in dynamicLabels" :key="item" :label="translateToChinese(item)"
:value="item"/>
</el-select>
</div>
</div>
@ -87,7 +88,7 @@
<el-table-column prop="nodeId" label="节点ID" width="180" align="center" show-overflow-tooltip/>
<el-table-column label="实体类型" width="160" align="center">
<template #default="scope">
{{ scope.row.labels ? scope.row.labels[0] : 'Drug' }}
<span>{{ translateToChinese(scope.row.labels ? scope.row.labels[0] : '') }}</span>
</template>
</el-table-column>
<el-table-column prop="name" label="实体名称" min-width="250" align="center" show-overflow-tooltip/>
@ -133,6 +134,25 @@
/>
</div>
<div class="input-group-inline">
<span class="filter-label-text">关系类型</span>
<el-select
v-model="relSearch.type"
placeholder="选择关系"
clearable
filterable
@change="handleRelSearch"
class="search-select"
>
<el-option label="全部" value=""/>
<el-option
v-for="item in dynamicRelTypes"
:key="item.type"
:label="item.label"
:value="item.type"
/>
</el-select>
</div>
<div class="input-group-inline">
<span class="filter-label-text">结束节点</span>
<el-autocomplete
v-model="relSearch.target"
@ -154,9 +174,9 @@
<div class="table-shadow-wrapper table-compact">
<el-table v-loading="loading" :data="relData" class="ref-table" height="calc(100vh - 560px)">
<el-table-column prop="source" label="起始节点" min-width="200" align="center" show-overflow-tooltip/>
<el-table-column prop="type" label="关系类型" width="150" align="center">
<el-table-column label="关系描述" width="150" align="center">
<template #default="scope">
{{ scope.row.type }}
<span>{{ translateToChinese(scope.row.type) }}</span>
</template>
</el-table-column>
<el-table-column prop="target" label="结束节点" min-width="200" align="center" show-overflow-tooltip/>
@ -196,26 +216,27 @@
<el-descriptions-item label="业务 ID (NodeId)">{{ currentDetail.nodeId }}</el-descriptions-item>
<el-descriptions-item label="实体名称">{{ currentDetail.name }}</el-descriptions-item>
<el-descriptions-item label="所属标签">
<el-tag v-for="l in currentDetail.labels" :key="l" style="margin-right: 5px">{{ l }}</el-tag>
{{ (currentDetail.labels || []).map(l => translateToChinese(l)).join(', ') }}
</el-descriptions-item>
</template>
<template v-else>
<el-descriptions-item label="起始节点">{{ currentDetail.source }}</el-descriptions-item>
<el-descriptions-item label="关系类型">{{ currentDetail.type }}</el-descriptions-item>
<el-descriptions-item label="关系标签">{{ currentDetail.label || '无' }}</el-descriptions-item>
<el-descriptions-item label="关系描述">{{ translateToChinese(currentDetail.type) }}</el-descriptions-item>
<el-descriptions-item label="关系标签属性">{{ currentDetail.label || '无' }}</el-descriptions-item>
<el-descriptions-item label="结束节点">{{ currentDetail.target }}</el-descriptions-item>
</template>
</el-descriptions>
</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-item label="名称" required>
<el-input v-model="nodeForm.name" placeholder="请输入实体名称" clearable/>
</el-form-item>
<el-form-item label="标签" required>
<el-select v-model="nodeForm.label" filterable allow-create placeholder="选择或输入标签" style="width: 100%">
<el-option v-for="tag in dynamicLabels" :key="tag" :label="tag" :value="tag"/>
<el-select v-model="nodeForm.label" filterable placeholder="选择标签" style="width: 100%">
<el-option v-for="item in dynamicLabels" :key="item" :label="translateToChinese(item)" :value="item"/>
</el-select>
</el-form-item>
</el-form>
@ -227,25 +248,47 @@
</template>
</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-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 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 label="关系类型" required>
<el-input v-model="relForm.type" placeholder="例如:TREATS"/>
<el-select
v-model="relForm.type"
filterable
allow-create
placeholder="选择或输入关系类型"
style="width: 100%"
@change="(val) => {
const selected = dynamicRelTypes.find(i => i.type === val);
if(selected) relForm.label = selected.label;
}"
>
<el-option
v-for="item in dynamicRelTypes"
:key="item.type"
:label="item.label"
:value="item.type"
/>
</el-select>
</el-form-item>
<el-form-item label="关系显示名">
<el-input v-model="relForm.label" placeholder="如果不填则同关系类型"/>
<el-form-item label="显示名">
<el-input v-model="relForm.label" placeholder="用于前端展示的中文名称"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer-wrap">
<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>
</template>
</el-dialog>
@ -253,31 +296,96 @@
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {ref, onMounted, reactive, computed} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import Menu from '@/components/Menu.vue'
import {
getKgStats, getLabels, getNodeSuggestions, getNodesList, getRelationshipsList,
addNode, updateNode, addRelationship, updateRelationship, deleteNode, deleteRelationship
getKgStats, getLabels, getRelationshipTypes, getNodeSuggestions, getNodesList, getRelationshipsList,
addNode, updateNode, addRelationship, updateRelationship, deleteNode, deleteRelationship,
fixNodeIds
} from '@/api/data'
// --- () ---
const CHINESE_TO_ENGLISH_LABEL = {
"疾病": "Disease",
"症状": "Symptom",
"检查项目": "AuxiliaryExamination",
"药物": "Drug",
"手术": "Operation",
"解剖部位": "CheckSubject",
"并发症": "Complication",
"诊断": "Diagnosis",
"治疗": "Treatment",
"辅助治疗": "AdjuvantTherapy",
"不良反应": "adverseReactions",
"检查": "Check",
"部门": "Department",
"疾病部位": "DiseaseSite",
"相关疾病": "RelatedDisease",
"相关症状": "RelatedSymptom",
"传播途径": "SpreadWay",
"阶段": "Stage",
"主题/主体": "Subject",
"症状与体征": "SymptomAndSign",
"治疗方案": "TreatmentPrograms",
"类型": "Type",
"原因": "Cause",
"属性": "Attribute",
"指示/适应症": "Indications",
"成分": "Ingredients",
"病原学": "Pathogenesis",
"病理类型": "PathologicalType",
"发病机制": "Pathophysiology",
"注意事项": "Precautions",
"预后": "Prognosis",
"预后生存时间": "PrognosticSurvivalTime",
"疾病比率": "DiseaseRatio",
"药物治疗": "DrugTherapy",
"感染性": "Infectious",
"实体": "Entity"
};
//
const dynamicRelTypes = ref([]);
const dynamicLabels = ref([]);
//
const ENGLISH_TO_CHINESE = computed(() => {
const map = {};
// 1.
for (const [chi, eng] of Object.entries(CHINESE_TO_ENGLISH_LABEL)) {
map[eng] = chi;
map[eng.toLowerCase()] = chi;
}
// 2. ()
dynamicRelTypes.value.forEach(item => {
map[item.type] = item.label;
});
return map;
});
const translateToChinese = (val) => {
if (!val) return '';
return ENGLISH_TO_CHINESE.value[val] || ENGLISH_TO_CHINESE.value[val.toLowerCase()] || val;
};
// --- ---
const pageSize = ref(20);
const activeName = ref('first');
const loading = ref(false);
const submitting = ref(false); //
const stats = reactive({ totalNodes: 0, totalRels: 0, todayNodes: 0 });
const submitting = ref(false);
const stats = reactive({totalNodes: 0, totalRels: 0, todayNodes: 0});
// --- ---
const nodeData = ref([]);
const nodeTotal = ref(0);
const nodePage = ref(1);
const nodeSearch = reactive({ name: '', label: '' });
const nodeSearch = reactive({name: '', label: ''});
const relData = ref([]);
const relTotal = ref(0);
const relPage = ref(1);
const relSearch = reactive({ source: '', target: '' });
const relSearch = reactive({source: '', target: '', type: ''});
// --- ---
const nodeDialogVisible = ref(false);
@ -286,19 +394,42 @@ const detailVisible = ref(false);
const detailType = ref('node');
const currentDetail = ref({});
const isEdit = ref(false);
const dynamicLabels = ref([]);
// --- ---
const nodeForm = reactive({ id: '', name: '', label: '' });
const relForm = reactive({ id: '', source: '', target: '', type: '', label: '' });
const nodeForm = reactive({id: '', name: '', label: ''});
const relForm = reactive({id: '', source: '', target: '', type: '', label: ''});
// --- ---
const fetchAllMetadata = async () => {
//
getLabels().then(res => {
if (res?.code === 200) dynamicLabels.value = res.data;
});
//
getRelationshipTypes().then(res => {
if (res?.code === 200) dynamicRelTypes.value = res.data;
});
};
const initDataGovernance = async () => {
try {
const res = await fixNodeIds();
if (res?.code === 200 && activeName.value === 'first') {
fetchNodes();
}
} catch (e) {
console.error(e);
}
};
const fetchStats = async () => {
try {
const res = await getKgStats();
if (res?.code === 200) Object.assign(stats, res.data);
} catch (e) { console.error("Stats Error", e); }
} catch (e) {
console.error(e);
}
};
const fetchNodes = async () => {
@ -314,8 +445,11 @@ const fetchNodes = async () => {
nodeData.value = res.data.items;
nodeTotal.value = res.data.total;
}
} catch (e) { ElMessage.error('加载节点失败'); }
finally { loading.value = false; }
} catch (e) {
ElMessage.error('加载节点失败');
} finally {
loading.value = false;
}
};
const fetchRels = async () => {
@ -325,30 +459,33 @@ const fetchRels = async () => {
page: relPage.value,
pageSize: pageSize.value,
source: relSearch.source?.trim() || null,
target: relSearch.target?.trim() || null
target: relSearch.target?.trim() || null,
type: relSearch.type || null // type
});
if (res?.code === 200) {
relData.value = res.data.items;
relTotal.value = res.data.total;
}
} catch (e) { ElMessage.error('加载关系失败'); }
finally { loading.value = false; }
} catch (e) {
ElMessage.error('加载关系失败');
} finally {
loading.value = false;
}
};
const openNodeDialog = (row = null) => {
isEdit.value = !!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] || 'Drug'});
} else {
Object.assign(nodeForm, { id: '', name: '', label: 'Drug' });
Object.assign(nodeForm, {id: '', name: '', label: 'Drug'});
}
nodeDialogVisible.value = true;
};
const submitNode = async () => {
if (!nodeForm.name?.trim()) return ElMessage.warning('名称不能为空');
if (submitting.value) return; //
if (submitting.value) return;
submitting.value = true;
try {
const res = isEdit.value ? await updateNode(nodeForm) : await addNode(nodeForm);
@ -357,18 +494,27 @@ const submitNode = async () => {
nodeDialogVisible.value = false;
fetchNodes();
fetchStats();
fetchAllMetadata(); //
} else {
ElMessage.error(res?.msg || '操作失败');
}
} catch (e) {
ElMessage.error('接口响应异常');
} finally { submitting.value = false; }
ElMessage.error('接口异常');
} finally {
submitting.value = false;
}
};
const openRelDialog = (row = null) => {
isEdit.value = !!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 {
Object.assign(relForm, {id: '', source: '', target: '', type: '', label: ''});
}
@ -377,29 +523,21 @@ const openRelDialog = (row = null) => {
const submitRel = async () => {
if (!relForm.source || !relForm.target || !relForm.type) return ElMessage.warning('必填项缺失');
if (submitting.value) return; //
if (submitting.value) return;
submitting.value = true;
try {
const payload = isEdit.value ? {...relForm} : {
source: relForm.source,
target: relForm.target,
type: relForm.type,
label: relForm.label || relForm.type
};
const res = isEdit.value ? await updateRelationship(payload) : await addRelationship(payload);
const res = isEdit.value ? await updateRelationship(relForm) : await addRelationship(relForm);
if (res?.code === 200) {
ElMessage.success('提交成功');
relDialogVisible.value = false;
fetchRels();
fetchStats();
fetchAllMetadata(); //
} else {
ElMessage.error(res?.msg || '提交失败');
}
} catch (e) {
ElMessage.error('网络连接异常');
ElMessage.error('网络异常');
} finally {
submitting.value = false;
}
@ -412,6 +550,7 @@ const handleDelete = (row, type) => {
ElMessage.success('删除成功');
type === 'node' ? fetchNodes() : fetchRels();
fetchStats();
fetchAllMetadata();
}
}).catch(() => {
});
@ -439,15 +578,15 @@ const handleView = (row, type) => {
};
onMounted(() => {
initDataGovernance();
fetchStats();
fetchNodes();
getLabels().then(res => {
if (res?.code === 200) dynamicLabels.value = res.data;
});
fetchAllMetadata(); //
});
</script>
<style scoped>
/* 样式保持不变... */
.knowledge-graph-data-container {
background-color: #f4f7fa;
display: flex;
@ -602,7 +741,7 @@ onMounted(() => {
}
.search-input, .search-select {
width: 200px !important;
width: 160px !important;
}
.btn-search-ref {

Loading…
Cancel
Save