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