4 changed files with 741 additions and 0 deletions
@ -0,0 +1,193 @@ |
|||
import json |
|||
import traceback |
|||
from app import app |
|||
from robyn import jsonify, Response |
|||
from service.OperationService import OperationService |
|||
from urllib.parse import unquote |
|||
|
|||
# 实例化业务逻辑对象 |
|||
operation_service = OperationService() |
|||
|
|||
def create_response(status_code, data_dict): |
|||
""" |
|||
统一响应格式封装,强制使用 UTF-8 防止中文乱码。 |
|||
""" |
|||
return Response( |
|||
status_code=status_code, |
|||
description=jsonify(data_dict), |
|||
headers={"Content-Type": "application/json; charset=utf-8"} |
|||
) |
|||
|
|||
def parse_request_body(req): |
|||
""" |
|||
通用请求体解析器:解决解析 bytes、字符串或多重序列化的情况。 |
|||
""" |
|||
body = req.body |
|||
try: |
|||
if isinstance(body, (bytes, bytearray)): |
|||
body = json.loads(body.decode('utf-8')) |
|||
if isinstance(body, str): |
|||
body = json.loads(body) |
|||
return body if isinstance(body, dict) else {} |
|||
except Exception as e: |
|||
print(f"Request Body Parse Error: {e}") |
|||
return {} |
|||
|
|||
# --- 1. 获取全量动态标签 --- |
|||
@app.get("/api/kg/labels") |
|||
def get_labels(req): |
|||
try: |
|||
labels = operation_service.get_all_labels() |
|||
return create_response(200, {"code": 200, "data": labels, "msg": "success"}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(500, {"code": 500, "msg": f"获取标签失败: {str(e)}"}) |
|||
|
|||
# --- 2. 输入联想建议 --- |
|||
@app.get("/api/kg/node/suggest") |
|||
def suggest_node(req): |
|||
try: |
|||
query_data = getattr(req, "queries", getattr(req, "query_params", {})) |
|||
raw_keyword = query_data.get("keyword", "") |
|||
keyword = raw_keyword[0] if isinstance(raw_keyword, list) and raw_keyword else str(raw_keyword) |
|||
clean_keyword = unquote(keyword).strip() |
|||
|
|||
suggestions = operation_service.suggest_nodes(clean_keyword) |
|||
return create_response(200, {"code": 200, "data": suggestions, "msg": "success"}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(500, {"code": 500, "msg": str(e)}) |
|||
|
|||
# --- 3. 获取分页节点列表 --- |
|||
@app.get("/api/kg/nodes") |
|||
def get_nodes(req): |
|||
try: |
|||
query_data = getattr(req, "queries", getattr(req, "query_params", {})) |
|||
page = int(query_data.get("page", "1")) |
|||
page_size = int(query_data.get("pageSize", "20")) |
|||
|
|||
res_data = operation_service.get_nodes_subset(page, page_size) |
|||
return create_response(200, {"code": 200, "data": res_data, "msg": "success"}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(500, {"code": 500, "msg": f"获取节点列表失败: {str(e)}"}) |
|||
|
|||
# --- 4. 获取分页关系列表 --- |
|||
@app.get("/api/kg/relationships") |
|||
def get_relationships(req): |
|||
try: |
|||
query_data = getattr(req, "queries", getattr(req, "query_params", {})) |
|||
page = int(query_data.get("page", "1")) |
|||
page_size = int(query_data.get("pageSize", "20")) |
|||
|
|||
res_data = operation_service.get_relationships_subset(page, page_size) |
|||
return create_response(200, {"code": 200, "data": res_data, "msg": "success"}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(500, {"code": 500, "msg": f"获取关系列表失败: {str(e)}"}) |
|||
|
|||
# --- 5. 新增节点 --- |
|||
@app.post("/api/kg/node/add") |
|||
def add_node(req): |
|||
try: |
|||
body = parse_request_body(req) |
|||
# 增加 strip() 防止空格导致的匹配失败 |
|||
label = str(body.get("label", "Drug")).strip() |
|||
name = str(body.get("name", "")).strip() |
|||
|
|||
if not name: |
|||
return create_response(200, {"code": 400, "msg": "添加失败:节点名称不能为空"}) |
|||
|
|||
result = operation_service.add_node(label, name) |
|||
return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
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) |
|||
node_id = body.get("id") |
|||
name = str(body.get("name", "")).strip() |
|||
label = str(body.get("label", "")).strip() |
|||
|
|||
if not node_id or not name: |
|||
return create_response(200, {"code": 400, "msg": "修改失败:参数缺失(ID或Name)"}) |
|||
|
|||
result = operation_service.update_node(node_id, name, label) |
|||
return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(200, {"code": 500, "msg": str(e)}) |
|||
|
|||
# --- 7. 新增关系 --- |
|||
@app.post("/api/kg/rel/add") |
|||
def add_relationship(req): |
|||
try: |
|||
body = parse_request_body(req) |
|||
# 强制格式化所有字符串,彻底杜绝数据类型导致的 MATCH 失败 |
|||
source = str(body.get("source", "")).strip() |
|||
target = str(body.get("target", "")).strip() |
|||
rel_type = str(body.get("type", "")).strip() |
|||
rel_label = str(body.get("label", "")).strip() |
|||
|
|||
if not all([source, target, rel_type]): |
|||
return create_response(200, {"code": 400, "msg": "添加失败:起始点、结束点和类型不能为空"}) |
|||
|
|||
result = operation_service.add_relationship(source, target, rel_type, rel_label) |
|||
return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(200, {"code": 500, "msg": f"新增关系异常: {str(e)}"}) |
|||
|
|||
# --- 8. 修改关系 --- |
|||
@app.post("/api/kg/rel/update") |
|||
def update_rel(req): |
|||
try: |
|||
body = parse_request_body(req) |
|||
rel_id = body.get("id") |
|||
source = str(body.get("source", "")).strip() |
|||
target = str(body.get("target", "")).strip() |
|||
rel_type = str(body.get("type", "")).strip() |
|||
rel_label = str(body.get("label", "")).strip() |
|||
|
|||
if not all([rel_id, source, target, rel_type]): |
|||
return create_response(200, {"code": 400, "msg": "修改失败:必要参数(ID/Source/Target/Type)缺失"}) |
|||
|
|||
result = operation_service.update_relationship(rel_id, source, target, rel_type, rel_label) |
|||
return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(200, {"code": 500, "msg": str(e)}) |
|||
|
|||
# --- 9. 删除节点 --- |
|||
@app.post("/api/kg/node/delete") |
|||
def delete_node(req): |
|||
try: |
|||
body = parse_request_body(req) |
|||
node_id = body.get("id") |
|||
if not node_id: |
|||
return create_response(200, {"code": 400, "msg": "删除失败:节点ID不能为空"}) |
|||
|
|||
operation_service.delete_node(node_id) |
|||
return create_response(200, {"code": 200, "msg": "节点及其关联关系已成功删除"}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(200, {"code": 500, "msg": str(e)}) |
|||
|
|||
# --- 10. 删除关系 --- |
|||
@app.post("/api/kg/rel/delete") |
|||
def delete_rel(req): |
|||
try: |
|||
body = parse_request_body(req) |
|||
rel_id = body.get("id") |
|||
if not rel_id: |
|||
return create_response(200, {"code": 400, "msg": "删除失败:关系ID不能为空"}) |
|||
|
|||
operation_service.delete_relationship(rel_id) |
|||
return create_response(200, {"code": 200, "msg": "关系已成功从数据库移除"}) |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return create_response(200, {"code": 500, "msg": str(e)}) |
|||
@ -0,0 +1,203 @@ |
|||
from util.neo4j_utils import neo4j_client |
|||
from urllib.parse import unquote |
|||
import traceback |
|||
|
|||
class OperationService: |
|||
def __init__(self): |
|||
self.db = neo4j_client |
|||
|
|||
# --- 1. 节点查询:去掉强制命名的逻辑 --- |
|||
def get_nodes_subset(self, page: int = 1, page_size: int = 20): |
|||
skip_val = int((page - 1) * page_size) |
|||
limit_val = int(page_size) |
|||
# 去掉 Cypher 中的排序干扰,确保 ID 稳定 |
|||
cypher = """ |
|||
MATCH (n) |
|||
WITH count(n) AS total_count |
|||
MATCH (n) |
|||
RETURN elementId(n) AS id, labels(n) AS labels, n.name AS name, n.nodeId AS nodeId, total_count AS total |
|||
ORDER BY toInteger(n.nodeId) DESC |
|||
SKIP $skip LIMIT $limit |
|||
""" |
|||
try: |
|||
params = {"skip": skip_val, "limit": limit_val} |
|||
raw_data = self.db.execute_read(cypher, params) |
|||
if not raw_data: return {"items": [], "total": 0} |
|||
items = [] |
|||
for item in raw_data: |
|||
items.append({ |
|||
"id": item["id"], |
|||
"labels": item["labels"], |
|||
# 修复:不再强转为“未命名”,保留真实状态 |
|||
"name": item.get("name"), |
|||
"nodeId": item.get("nodeId") or 0 |
|||
}) |
|||
return {"items": items, "total": raw_data[0]['total']} |
|||
except Exception as e: |
|||
print(f"Service Error (Nodes): {e}") |
|||
return {"items": [], "total": 0} |
|||
|
|||
# --- 2. 关系查询:去掉 coalesce 强制命名 --- |
|||
def get_relationships_subset(self, page: int = 1, page_size: int = 20): |
|||
skip_val = int((page - 1) * page_size) |
|||
limit_val = int(page_size) |
|||
# 修复:移除 coalesce(..., "未知节点") |
|||
cypher = """ |
|||
MATCH (a)-[r]->(b) |
|||
WITH count(r) AS total_count |
|||
MATCH (a)-[r]->(b) |
|||
RETURN elementId(r) as id, |
|||
type(r) as type, |
|||
r.label as label, |
|||
a.name as source, |
|||
b.name as target, |
|||
coalesce(r.createTime, 0) as createTime, |
|||
total_count |
|||
ORDER BY createTime DESC |
|||
SKIP $skip LIMIT $limit |
|||
""" |
|||
try: |
|||
params = {"skip": skip_val, "limit": limit_val} |
|||
raw_data = self.db.execute_read(cypher, params) |
|||
if not raw_data: return {"items": [], "total": 0} |
|||
items = [] |
|||
for row in raw_data: |
|||
items.append({ |
|||
"id": row["id"], |
|||
"type": row["type"], |
|||
"label": row["label"] if row["label"] is not None else "", |
|||
"source": row["source"], |
|||
"target": row["target"] |
|||
}) |
|||
return {"items": items, "total": raw_data[0]['total_count']} |
|||
except Exception as e: |
|||
print(f"Service Error (Rels): {e}") |
|||
return {"items": [], "total": 0} |
|||
|
|||
# --- 3. 联想建议:排除“未命名”干扰 --- |
|||
def suggest_nodes(self, keyword: str): |
|||
if not keyword: return [] |
|||
try: |
|||
kw = unquote(str(keyword)).strip() |
|||
# 增加过滤:不返回名为“未命名”的建议 |
|||
cypher = """ |
|||
MATCH (n) |
|||
WHERE n.name CONTAINS $kw AND n.name <> '未命名' |
|||
RETURN DISTINCT n.name as name LIMIT 15 |
|||
""" |
|||
results = self.db.execute_read(cypher, {"kw": kw}) |
|||
db_suggestions = [row["name"] for row in results if row["name"]] |
|||
suffix_suggestions = [f"{kw}片", f"{kw}胶囊", f"{kw}注射液"] |
|||
final_res = list(dict.fromkeys(db_suggestions + suffix_suggestions)) |
|||
return final_res[:15] |
|||
except: |
|||
return [] |
|||
|
|||
# --- 4. 节点管理 --- |
|||
def add_node(self, label: str, name: str): |
|||
try: |
|||
nm = str(name).strip() |
|||
if not nm: return {"success": False, "msg": "名称不能为空"} |
|||
|
|||
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}' 的节点"} |
|||
|
|||
create_cypher = f"CREATE (n:`{label}` {{name: $name, nodeId: timestamp()}}) RETURN elementId(n) as id" |
|||
self.db.execute_write(create_cypher, {"name": nm}) |
|||
return {"success": True, "msg": "添加成功"} |
|||
except Exception as e: |
|||
return {"success": False, "msg": f"写入失败: {str(e)}"} |
|||
|
|||
def update_node(self, node_id: str, name: str, label: str): |
|||
try: |
|||
nm = 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}' 的节点"} |
|||
|
|||
cypher = f""" |
|||
MATCH (n) WHERE elementId(n) = $id |
|||
SET n.name = $name, n.nodeId = timestamp() |
|||
WITH n |
|||
REMOVE n:Drug:Disease:Symptom:Entity |
|||
WITH n |
|||
SET n:`{label}` |
|||
RETURN n |
|||
""" |
|||
result = self.db.execute_write(cypher, {"id": node_id, "name": nm}) |
|||
return {"success": True, "msg": "节点修改成功"} if result else {"success": False, "msg": "找不到该节点"} |
|||
except Exception as e: |
|||
return {"success": False, "msg": str(e)} |
|||
|
|||
def delete_node(self, node_id: str): |
|||
cypher = "MATCH (n) WHERE elementId(n) = $id DETACH DELETE n" |
|||
return self.db.execute_write(cypher, {"id": node_id}) |
|||
|
|||
def get_all_labels(self): |
|||
cypher = "CALL db.labels()" |
|||
try: |
|||
results = self.db.execute_read(cypher) |
|||
labels = [list(row.values())[0] for row in results] |
|||
# 排除掉 Neo4j 默认的一些内部标签(如果有) |
|||
return labels if labels else ["Drug", "Disease", "Symptom"] |
|||
except: |
|||
return ["Drug", "Disease", "Symptom"] |
|||
|
|||
# --- 5. 关系管理 --- |
|||
def add_relationship(self, source_name: str, target_name: str, rel_type: str, rel_label: str): |
|||
try: |
|||
s, t, l = str(source_name).strip(), str(target_name).strip(), str(rel_label).strip() |
|||
|
|||
check_nodes = """ |
|||
OPTIONAL MATCH (a) WHERE a.name = $s |
|||
OPTIONAL MATCH (b) WHERE b.name = $t |
|||
RETURN a IS NOT NULL as hasA, b IS NOT NULL as hasB |
|||
""" |
|||
exists = self.db.execute_read(check_nodes, {"s": s, "t": t}) |
|||
|
|||
if not exists or not exists[0]['hasA'] or not exists[0]['hasB']: |
|||
err_msg = "添加失败: " |
|||
if not exists[0]['hasA']: err_msg += f"起始节点'{s}'不存在; " |
|||
if not exists[0]['hasB']: err_msg += f"结束节点'{t}'不存在" |
|||
return {"success": False, "msg": err_msg} |
|||
|
|||
cypher = f""" |
|||
MATCH (a {{name: $s}}), (b {{name: $t}}) |
|||
MERGE (a)-[r:`{rel_type}`]->(b) |
|||
ON CREATE SET r.label = $l, r.createTime = timestamp() |
|||
ON MATCH SET r.label = $l, r.updateTime = timestamp() |
|||
RETURN r |
|||
""" |
|||
self.db.execute_write(cypher, {"s": s, "t": t, "l": l}) |
|||
return {"success": True, "msg": "操作成功"} |
|||
|
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return {"success": False, "msg": f"数据库写入异常: {str(e)}"} |
|||
|
|||
def update_relationship(self, rel_id: str, source_name: str, target_name: str, rel_type: str, rel_label: str): |
|||
try: |
|||
s, t, l = str(source_name).strip(), str(target_name).strip(), str(rel_label).strip() |
|||
find_old = "MATCH (a)-[r]->(b) WHERE elementId(r) = $id RETURN type(r) as type, a.name as s, b.name as t" |
|||
old = self.db.execute_read(find_old, {"id": rel_id}) |
|||
if not old: return {"success": False, "msg": "修改失败:原关系不存在"} |
|||
|
|||
if old[0]['s'] != s or old[0]['t'] != t or old[0]['type'] != rel_type: |
|||
self.delete_relationship(rel_id) |
|||
return self.add_relationship(s, t, rel_type, l) |
|||
else: |
|||
update_cypher = "MATCH ()-[r]->() WHERE elementId(r) = $id SET r.label = $l, r.updateTime = timestamp() RETURN r" |
|||
self.db.execute_write(update_cypher, {"id": rel_id, "l": l}) |
|||
return {"success": True, "msg": "修改成功"} |
|||
except Exception as e: |
|||
traceback.print_exc() |
|||
return {"success": False, "msg": f"修改异常: {str(e)}"} |
|||
|
|||
def delete_relationship(self, rel_id: str): |
|||
cypher = "MATCH ()-[r]->() WHERE elementId(r) = $id DELETE r" |
|||
return self.db.execute_write(cypher, {"id": rel_id}) |
|||
@ -0,0 +1 @@ |
|||
overrides=entities@3.19.0 |
|||
@ -0,0 +1,344 @@ |
|||
<template> |
|||
<div style="padding: 20px;"> |
|||
<el-tabs v-model="activeName" @tab-click="handleTabClick"> |
|||
|
|||
<el-tab-pane label="节点管理" name="first"> |
|||
<div style="margin-bottom: 15px; text-align: left;"> |
|||
<el-button type="primary" @click="openNodeDialog(null)">新增节点</el-button> |
|||
</div> |
|||
<el-table v-loading="loading" :data="nodeData" border height="550" style="width: 100%"> |
|||
<el-table-column prop="id" label="ID" width="180" show-overflow-tooltip /> |
|||
|
|||
<el-table-column label="名称" min-width="150"> |
|||
<template #default="scope"> |
|||
<span :style="{ color: scope.row.name ? 'inherit' : '#909399' }"> |
|||
{{ scope.row.name || 'N/A' }} |
|||
</span> |
|||
</template> |
|||
</el-table-column> |
|||
|
|||
<el-table-column label="标签" width="150"> |
|||
<template #default="scope"> |
|||
<el-tag size="small" v-for="label in scope.row.labels" :key="label" style="margin-right: 5px;">{{ label }}</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="操作" width="160" fixed="right"> |
|||
<template #default="scope"> |
|||
<el-button type="primary" size="small" @click="openNodeDialog(scope.row)">编辑</el-button> |
|||
<el-button type="danger" size="small" @click="handleDelete(scope.row, 'node')">删除</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<div style="margin-top: 15px; display: flex; justify-content: flex-end;"> |
|||
<el-pagination |
|||
background |
|||
layout="total, sizes, prev, pager, next, jumper" |
|||
:total="nodeTotal" |
|||
:page-sizes="[10, 20, 50, 100]" |
|||
v-model:page-size="pageSize" |
|||
v-model:current-page="nodePage" |
|||
@current-change="fetchNodes" |
|||
@size-change="handleSizeChange" |
|||
/> |
|||
</div> |
|||
</el-tab-pane> |
|||
|
|||
<el-tab-pane label="关系管理" name="second"> |
|||
<div style="margin-bottom: 15px; text-align: left;"> |
|||
<el-button type="primary" @click="openRelDialog(null)">新增关系</el-button> |
|||
</div> |
|||
<el-table v-loading="loading" :data="relData" border height="550" style="width: 100%"> |
|||
<el-table-column label="起始节点" min-width="150"> |
|||
<template #default="scope"> |
|||
<span :style="{ color: scope.row.source ? 'inherit' : '#909399' }"> |
|||
{{ scope.row.source || 'N/A' }} |
|||
</span> |
|||
</template> |
|||
</el-table-column> |
|||
|
|||
<el-table-column prop="type" label="关系类型" width="180" /> |
|||
|
|||
<el-table-column label="结束节点" min-width="150"> |
|||
<template #default="scope"> |
|||
<span :style="{ color: scope.row.target ? 'inherit' : '#909399' }"> |
|||
{{ scope.row.target || 'N/A' }} |
|||
</span> |
|||
</template> |
|||
</el-table-column> |
|||
|
|||
<el-table-column prop="label" label="描述" width="120" /> |
|||
<el-table-column label="操作" width="160" fixed="right"> |
|||
<template #default="scope"> |
|||
<el-button type="primary" size="small" @click="openRelDialog(scope.row)">编辑</el-button> |
|||
<el-button type="danger" size="small" @click="handleDelete(scope.row, 'rel')">删除</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<div style="margin-top: 15px; display: flex; justify-content: flex-end;"> |
|||
<el-pagination |
|||
background |
|||
layout="total, sizes, prev, pager, next, jumper" |
|||
:total="relTotal" |
|||
:page-sizes="[10, 20, 50, 100]" |
|||
v-model:page-size="pageSize" |
|||
v-model:current-page="relPage" |
|||
@current-change="fetchRels" |
|||
@size-change="handleSizeChange" |
|||
/> |
|||
</div> |
|||
</el-tab-pane> |
|||
</el-tabs> |
|||
|
|||
<el-dialog v-model="nodeDialogVisible" :title="isEdit ? '修改节点' : '新增节点'" width="420px" destroy-on-close> |
|||
<el-form :model="nodeForm" label-width="80px" @submit.prevent> |
|||
<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" placeholder="请选择或输入标签" filterable allow-create style="width: 100%"> |
|||
<el-option v-for="tag in dynamicLabels" :key="tag" :label="tag" :value="tag" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="nodeDialogVisible = false">取消</el-button> |
|||
<el-button type="primary" native-type="button" :loading="submitting" @click="submitNode">确认</el-button> |
|||
</template> |
|||
</el-dialog> |
|||
|
|||
<el-dialog v-model="relDialogVisible" :title="isEdit ? '修改关系信息' : '新增关系'" width="480px" destroy-on-close> |
|||
<el-form :model="relForm" label-width="100px" @submit.prevent> |
|||
<el-form-item label="起始节点" required> |
|||
<el-autocomplete |
|||
v-model="relForm.source" |
|||
:fetch-suggestions="queryNodeSearch" |
|||
placeholder="搜索并选择起始节点" |
|||
style="width: 100%" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="结束节点" required> |
|||
<el-autocomplete |
|||
v-model="relForm.target" |
|||
:fetch-suggestions="queryNodeSearch" |
|||
placeholder="搜索并选择结束节点" |
|||
style="width: 100%" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="关系类型" required> |
|||
<el-select v-model="relForm.type" placeholder="关系类别" style="width: 100%"> |
|||
<el-option label="不良反应 (adverseReactions)" value="adverseReactions" /> |
|||
<el-option label="治疗 (treats)" value="treats" /> |
|||
<el-option label="包含 (contains)" value="contains" /> |
|||
<el-option label="禁忌 (contraindicated)" value="contraindicated" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="关系描述"> |
|||
<el-input v-model="relForm.label" placeholder="例如:引起、属于、禁用于" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="relDialogVisible = false">取消</el-button> |
|||
<el-button type="primary" native-type="button" :loading="submitting" @click="submitRel">确认</el-button> |
|||
</template> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import {ref, onMounted, reactive} from 'vue' |
|||
import axios from 'axios' |
|||
import {ElMessage, ElMessageBox} from 'element-plus' |
|||
|
|||
const BASE_URL = 'http://localhost:8088' |
|||
const pageSize = ref(20) |
|||
|
|||
const activeName = ref('first') |
|||
const loading = ref(false) |
|||
const submitting = ref(false) |
|||
|
|||
const nodeData = ref([]); |
|||
const nodeTotal = ref(0); |
|||
const nodePage = ref(1) |
|||
const relData = ref([]); |
|||
const relTotal = ref(0); |
|||
const relPage = ref(1) |
|||
|
|||
const nodeDialogVisible = ref(false) |
|||
const relDialogVisible = ref(false) |
|||
const isEdit = ref(false) |
|||
const dynamicLabels = ref([]) |
|||
|
|||
const nodeForm = reactive({id: '', name: '', label: ''}) |
|||
const relForm = reactive({id: '', source: '', target: '', type: 'adverseReactions', label: ''}) |
|||
|
|||
const handleSizeChange = (val) => { |
|||
pageSize.value = val |
|||
activeName.value === 'first' ? fetchNodes() : fetchRels() |
|||
} |
|||
|
|||
const fetchAllLabels = async () => { |
|||
try { |
|||
const res = await axios.get(`${BASE_URL}/api/kg/labels`) |
|||
if (res.data.code === 200) dynamicLabels.value = res.data.data |
|||
} catch (e) { |
|||
console.error('获取标签失败:', e) |
|||
} |
|||
} |
|||
|
|||
const queryNodeSearch = async (queryString, cb) => { |
|||
if (!queryString) return cb([]) |
|||
try { |
|||
const res = await axios.get(`${BASE_URL}/api/kg/node/suggest`, {params: {keyword: queryString}}) |
|||
if (res.data.code === 200) { |
|||
cb(res.data.data.map(name => ({value: name}))) |
|||
} else cb([]) |
|||
} catch (e) { |
|||
cb([]) |
|||
} |
|||
} |
|||
|
|||
const openNodeDialog = (row = null) => { |
|||
fetchAllLabels() |
|||
isEdit.value = !!row |
|||
if (row) { |
|||
nodeForm.id = row.id |
|||
nodeForm.name = row.name || '' |
|||
nodeForm.label = row.labels ? row.labels[0] : '' |
|||
} else { |
|||
Object.assign(nodeForm, {id: '', name: '', label: 'Drug'}) |
|||
} |
|||
nodeDialogVisible.value = true |
|||
} |
|||
|
|||
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 || '' |
|||
}) |
|||
} else { |
|||
Object.assign(relForm, {id: '', source: '', target: '', type: 'adverseReactions', label: ''}) |
|||
} |
|||
relDialogVisible.value = true |
|||
} |
|||
|
|||
const fetchNodes = async () => { |
|||
loading.value = true |
|||
try { |
|||
const res = await axios.get(`${BASE_URL}/api/kg/nodes`, {params: {page: nodePage.value, pageSize: pageSize.value}}) |
|||
if (res.data.code === 200) { |
|||
nodeData.value = res.data.data.items |
|||
nodeTotal.value = res.data.data.total |
|||
} |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
const fetchRels = async () => { |
|||
loading.value = true |
|||
relData.value = [] |
|||
try { |
|||
const res = await axios.get(`${BASE_URL}/api/kg/relationships`, { |
|||
params: { page: relPage.value, pageSize: pageSize.value } |
|||
}) |
|||
if (res.data.code === 200) { |
|||
relData.value = res.data.data.items |
|||
relTotal.value = res.data.data.total |
|||
} |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
const submitNode = async () => { |
|||
if (!nodeForm.name || !nodeForm.name.trim()) return ElMessage.warning('名称不能为空') |
|||
submitting.value = true |
|||
const url = isEdit.value ? `${BASE_URL}/api/kg/node/update` : `${BASE_URL}/api/kg/node/add` |
|||
|
|||
const payload = { |
|||
name: nodeForm.name.trim(), |
|||
label: nodeForm.label |
|||
} |
|||
if (isEdit.value) payload.id = nodeForm.id |
|||
|
|||
try { |
|||
const res = await axios.post(url, payload) |
|||
if (res.data.code === 200) { |
|||
ElMessage.success(res.data.msg || '操作成功') |
|||
nodeDialogVisible.value = false |
|||
fetchNodes() |
|||
} else { |
|||
ElMessage.error(res.data.msg || '操作失败') |
|||
} |
|||
} catch (e) { |
|||
ElMessage.error('网络连接错误') |
|||
} finally { |
|||
submitting.value = false |
|||
} |
|||
} |
|||
|
|||
const submitRel = async () => { |
|||
if (!relForm.source || !relForm.target) return ElMessage.warning('节点信息不完整') |
|||
submitting.value = true |
|||
const url = isEdit.value ? `${BASE_URL}/api/kg/rel/update` : `${BASE_URL}/api/kg/rel/add` |
|||
|
|||
const payload = { |
|||
source: String(relForm.source).trim(), |
|||
target: String(relForm.target).trim(), |
|||
type: relForm.type, |
|||
label: (relForm.label || '').trim() |
|||
} |
|||
if (isEdit.value) { |
|||
payload.id = relForm.id |
|||
} |
|||
|
|||
try { |
|||
const res = await axios.post(url, payload) |
|||
if (res.data.code === 200) { |
|||
relDialogVisible.value = false |
|||
ElMessage.success(res.data.msg || '关系已同步') |
|||
fetchRels() |
|||
} else { |
|||
ElMessage.warning(res.data.msg || '操作受限') |
|||
} |
|||
} catch (e) { |
|||
ElMessage.error('系统响应异常') |
|||
} finally { |
|||
submitting.value = false |
|||
} |
|||
} |
|||
|
|||
const handleDelete = (row, type) => { |
|||
const isNode = type === 'node' |
|||
const displayName = row.name || '未命名节点' |
|||
ElMessageBox.confirm( |
|||
isNode ? `删除节点 "${displayName}" 会同步删除所有关联关系,确认吗?` : '确认删除该关系吗?', |
|||
'风险提示', {type: 'error', confirmButtonText: '确定删除', cancelButtonText: '取消'} |
|||
).then(async () => { |
|||
try { |
|||
const res = await axios.post(`${BASE_URL}/api/kg/${isNode ? 'node' : 'rel'}/delete`, {id: row.id}) |
|||
if (res.data.code === 200) { |
|||
ElMessage.success('已从数据库移除') |
|||
isNode ? (fetchNodes(), fetchRels()) : fetchRels() |
|||
} |
|||
} catch (e) { |
|||
ElMessage.error('操作执行失败') |
|||
} |
|||
}).catch(() => { |
|||
}) |
|||
} |
|||
|
|||
const handleTabClick = (pane) => { |
|||
pane.paneName === 'first' ? fetchNodes() : fetchRels() |
|||
} |
|||
|
|||
onMounted(() => { |
|||
fetchNodes() |
|||
fetchAllLabels() |
|||
}) |
|||
</script> |
|||
Loading…
Reference in new issue