Browse Source

Merge branch 'mh' of http://124.70.32.114:3100/hanyuqing/KGPython into hanyuqing

# Conflicts:
#	vue/src/system/KGData.vue
hanyuqing
hanyuqing 3 months ago
parent
commit
a98947ebbb
  1. 132
      controller/GraphStyleController.py
  2. 42
      controller/OperationController.py
  3. 1
      controller/__init__.py
  4. 139
      service/GraphStyleService.py
  5. 66
      service/OperationService.py
  6. 32
      vue/src/api/data.js
  7. 96
      vue/src/api/style.js
  8. 1040
      vue/src/system/GraphStyle.vue
  9. 105
      vue/src/system/KGData.vue

132
controller/GraphStyleController.py

@ -0,0 +1,132 @@
# controller/GraphStyleController.py
import json
from robyn import jsonify, Response
from app import app
from service.GraphStyleService import GraphStyleService
# --- 核心工具函数:解决乱码 ---
def create_response(status_code, data_dict):
"""
统一响应格式封装强制使用 UTF-8 防止中文乱码
"""
return Response(
status_code=status_code,
description=json.dumps(data_dict, ensure_ascii=False),
headers={"Content-Type": "application/json; charset=utf-8"}
)
@app.post("/api/graph/style/save")
async def save_style_config(request):
"""保存配置接口 - 升级版:支持分组名"""
try:
body = request.json()
canvas_name = body.get('canvas_name')
current_label = body.get('current_label')
styles = body.get('styles')
# 新增:接收分组名称(字符串)
group_name = body.get('group_name')
if not all([canvas_name, current_label, styles]):
return create_response(200, {"code": 400, "msg": "参数不完整"})
# 调用 Service,逻辑内部处理:组名存在则用,不存在则建
success = GraphStyleService.save_config(canvas_name, current_label, styles, group_name)
if success:
return create_response(200, {"code": 200, "msg": "保存成功"})
else:
return create_response(200, {"code": 500, "msg": "保存失败"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"系统异常: {str(e)}"})
@app.get("/api/graph/style/list/grouped")
async def get_grouped_style_list(request):
"""获取【分组嵌套】格式的配置列表(用于右侧折叠面板)"""
try:
# 调用 Service 的嵌套聚合方法
data = GraphStyleService.get_grouped_configs()
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"查询异常: {str(e)}"})
@app.get("/api/graph/style/groups")
async def get_group_names(request):
"""获取所有已存在的方案组列表(用于保存弹窗的下拉选择)"""
try:
data = GraphStyleService.get_group_list()
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"查询异常: {str(e)}"})
@app.get("/api/graph/style/list")
async def get_style_list(request):
"""获取原始扁平配置列表(保留兼容性)"""
try:
data = GraphStyleService.get_all_configs()
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"查询异常: {str(e)}"})
@app.post("/api/graph/style/delete")
async def delete_style_config(request):
"""删除单条画布配置"""
try:
body = request.json()
config_id = body.get('id')
if not config_id:
return create_response(200, {"code": 400, "msg": "缺少ID"})
success = GraphStyleService.delete_config(config_id)
if success:
return create_response(200, {"code": 200, "msg": "删除成功"})
else:
return create_response(200, {"code": 500, "msg": "删除失败"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"操作异常: {str(e)}"})
@app.post("/api/graph/style/group/delete")
async def delete_style_group(request):
"""删除整个方案组及其下属所有配置"""
try:
body = request.json()
group_id = body.get('group_id')
if not group_id:
return create_response(200, {"code": 400, "msg": "缺少分组ID"})
success = GraphStyleService.delete_group(group_id)
if success:
return create_response(200, {"code": 200, "msg": "方案组已彻底删除"})
else:
return create_response(200, {"code": 500, "msg": "方案组删除失败"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"操作异常: {str(e)}"})
@app.post("/api/graph/style/batch_delete")
async def batch_delete_style(request):
"""批量删除配置接口"""
try:
body = request.json()
config_ids = body.get('ids')
if isinstance(config_ids, str):
try:
config_ids = json.loads(config_ids)
except:
pass
if not config_ids or not isinstance(config_ids, list):
return create_response(200, {"code": 400, "msg": "参数格式错误"})
count = GraphStyleService.batch_delete_configs(config_ids)
return create_response(200, {"code": 200, "msg": f"成功删除 {count} 条配置", "count": count})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"批量删除异常: {str(e)}"})

42
controller/OperationController.py

@ -24,30 +24,32 @@ def create_response(status_code, data_dict):
def parse_request_body(req):
"""
解析器适配 Robyn 框架确保能准确拿到前端传来的 IDnodeIdname label
解析器适配 Robyn 框架
针对前端 Vue3 + ElementPlus 的请求进行深度解析确保获取 IDnodeIdname label
"""
try:
body = getattr(req, "body", None)
if not body:
return {}
# 1. 处理 bytes 类型
# 1. 处理 bytes 类型 (Robyn 常见的 body 类型)
if isinstance(body, (bytes, bytearray)):
body = body.decode('utf-8')
# 2. 如果已经是字典
# 2. 如果已经是字典,直接返回
if isinstance(body, dict):
return body
# 3. 处理字符串
# 3. 处理字符串 (JSON 序列化后的字符串)
if isinstance(body, str):
try:
data = json.loads(body)
# 处理双层 JSON 字符串转义的情况
# 处理双层 JSON 序列化的情况 (有些前端框架会序列化两次)
if isinstance(data, str):
data = json.loads(data)
return data
except json.JSONDecodeError:
# 尝试解析 URL 编码格式 (application/x-www-form-urlencoded)
try:
from urllib.parse import parse_qs
params = parse_qs(body)
@ -62,13 +64,15 @@ def parse_request_body(req):
def get_query_param(req, key, default=""):
"""
提取 URL 查询参数
提取 URL 查询参数适配不同版本的 Robyn 参数存放位置
"""
try:
# 尝试从新版/旧版 Robyn 的不同属性中提取
data_source = getattr(req, "queries", None)
if data_source is None or (isinstance(data_source, dict) and not data_source):
data_source = getattr(req, "query_params", {})
# 适配 Robyn 特有的 Query 对象
if hasattr(data_source, "to_dict"):
data_source = data_source.to_dict()
@ -76,6 +80,7 @@ def get_query_param(req, key, default=""):
if val is None:
return default
# 提取值并进行 URL 解码
raw_val = str(val[0]) if isinstance(val, list) else str(val)
return unquote(raw_val).strip()
except Exception as e:
@ -110,7 +115,7 @@ def get_labels(req):
return create_response(200, {"code": 500, "msg": f"获取标签失败: {str(e)}"})
# --- 新增:获取全量动态关系类型 (关系管理用) ---
# --- 新增:获取全量动态关系类型 ---
@app.get("/api/kg/relationship-types")
def get_rel_types(req):
"""
@ -127,12 +132,23 @@ def get_rel_types(req):
# --- 2. 输入联想建议 ---
@app.get("/api/kg/node/suggest")
def suggest_node(req):
"""
联想词接口
支持 keyword 模糊搜索同时支持 label 强过滤
"""
try:
# 1. 提取前端传来的参数
clean_keyword = get_query_param(req, "keyword", "")
suggestions = operation_service.suggest_nodes(clean_keyword)
clean_label = get_query_param(req, "label", "")
# 2. 调用 Service 层
# 如果 label 为 "全部" 或空,Service 层会自动处理成全库建议
suggestions = operation_service.suggest_nodes(clean_keyword, clean_label)
return create_response(200, {"code": 200, "data": suggestions, "msg": "success"})
except Exception as e:
return create_response(200, {"code": 500, "msg": str(e)})
print(f"Suggest Interface Error: {e}")
return create_response(200, {"code": 500, "msg": f"联想接口异常: {str(e)}"})
# --- 3. 获取分页节点列表 ---
@ -147,8 +163,9 @@ def get_nodes(req):
page = int(page_str) if page_str.isdigit() else 1
page_size = int(size_str) if size_str.isdigit() else 20
# 清洗参数
name = name_raw if name_raw else None
label = label_raw if (label_raw and label_raw != "全部") else None
label = label_raw if (label_raw and label_raw not in ["全部", "", "null"]) else None
res_data = operation_service.get_nodes_subset(page, page_size, name=name, label=label)
return create_response(200, {"code": 200, "data": res_data, "msg": "success"})
@ -172,7 +189,7 @@ def get_relationships(req):
source = source_raw if source_raw else None
target = target_raw if target_raw else None
rel_type = type_raw if (type_raw and type_raw != "全部") else None
rel_type = type_raw if (type_raw and type_raw not in ["全部", ""]) else None
res_data = operation_service.get_relationships_subset(page, page_size, source, target, rel_type)
return create_response(200, {"code": 200, "data": res_data, "msg": "success"})
@ -206,7 +223,8 @@ def add_node(req):
def update_node(req):
try:
body = parse_request_body(req)
node_id = body.get("id")
# 兼容两种写法:id (elementId) 或 nodeId (业务ID)
node_id = body.get("id") or body.get("nodeId")
name = str(body.get("name", "")).strip()
label = str(body.get("label", "")).strip()

1
controller/__init__.py

@ -7,6 +7,7 @@ from .LoginController import *
from .QAController import *
from .RegisterController import *
from .OperationController import *
from .GraphStyleController import *
# 可选:如果控制器里定义了 blueprint,也可以在这里统一导出
# from .BuilderController import builder_bp
# from .GraphController import graph_bp

139
service/GraphStyleService.py

@ -0,0 +1,139 @@
# service/GraphStyleService.py
import json
from util.mysql_utils import mysql_client
class GraphStyleService:
@staticmethod
def save_config(canvas_name: str, current_label: str, styles_dict: dict, group_name: str = None) -> bool:
"""
保存图谱样式配置增强版自动处理分组逻辑
:param canvas_name: 画布显示名称
:param current_label: 针对的标签名称
:param styles_dict: 样式字典
:param group_name: 分组名称前端传来的字符串
"""
# 1. 处理分组逻辑:查不到就建,查到了就用
if not group_name or group_name.strip() == "":
group_name = "默认方案"
# 检查组名是否已存在
check_group_sql = "SELECT id FROM graph_style_groups WHERE group_name = %s LIMIT 1"
existing_group = mysql_client.execute_query(check_group_sql, (group_name,))
if existing_group:
# 如果存在,直接使用已有 ID
target_group_id = existing_group[0]['id']
else:
# 如果不存在,新建一个组
create_group_sql = "INSERT INTO graph_style_groups (group_name) VALUES (%s)"
mysql_client.execute_update(create_group_sql, (group_name,))
# 获取新生成的 ID
get_id_sql = "SELECT LAST_INSERT_ID() as last_id"
id_res = mysql_client.execute_query(get_id_sql)
target_group_id = id_res[0]['last_id'] if id_res else 1
# 2. 转换样式 JSON
config_json = json.dumps(styles_dict, ensure_ascii=False)
# 3. 插入配置表(关联 target_group_id)
sql = """
INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id)
VALUES (%s, %s, %s, %s)
"""
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id))
return affected_rows > 0
@staticmethod
def get_grouped_configs() -> list:
"""
核心优化获取嵌套结构的方案列表 (Group -> Configs)
用于前端右侧折叠面板展示
"""
# 1. 查询所有方案组
groups_sql = "SELECT id, group_name FROM graph_style_groups ORDER BY id ASC"
groups = mysql_client.execute_query(groups_sql) or []
# 2. 查询所有配置项
configs_sql = "SELECT id, group_id, canvas_name, current_label, config_json, create_time FROM graph_configs"
configs = mysql_client.execute_query(configs_sql) or []
# 3. 内存聚合:将配置项塞进对应的组
# 先处理配置项的 JSON 和 时间
for conf in configs:
if conf.get('config_json'):
try:
conf['styles'] = json.loads(conf['config_json'])
except:
conf['styles'] = {}
del conf['config_json']
if conf.get('create_time') and not isinstance(conf['create_time'], str):
conf['create_time'] = conf['create_time'].strftime('%Y-%m-%d %H:%M:%S')
# 组装数据结构
result = []
for g in groups:
# 找到属于该组的所有配置
g_children = [c for c in configs if c['group_id'] == g['id']]
g['configs'] = g_children
# 增加一个前端控制开关用的字段
g['expanded'] = False
result.append(g)
return result
@staticmethod
def get_all_configs() -> list:
"""保持原有的扁平查询功能,仅增加 group_id 字段返回"""
sql = "SELECT id, group_id, canvas_name, current_label, config_json, create_time FROM graph_configs ORDER BY create_time DESC"
rows = mysql_client.execute_query(sql)
if not rows: return []
for row in rows:
if row.get('config_json'):
try: row['styles'] = json.loads(row['config_json'])
except: row['styles'] = {}
del row['config_json']
if row.get('create_time') and not isinstance(row['create_time'], str):
row['create_time'] = row['create_time'].strftime('%Y-%m-%d %H:%M:%S')
return rows
@staticmethod
def delete_group(group_id: int) -> bool:
"""
逻辑级联删除删除方案组及其关联的所有配置
"""
# 1. 删除组下的所有配置
del_configs_sql = "DELETE FROM graph_configs WHERE group_id = %s"
mysql_client.execute_update(del_configs_sql, (group_id,))
# 2. 删除组本身
del_group_sql = "DELETE FROM graph_style_groups WHERE id = %s"
affected_rows = mysql_client.execute_update(del_group_sql, (group_id,))
return affected_rows > 0
@staticmethod
def delete_config(config_id: int) -> bool:
"""删除单个配置"""
sql = "DELETE FROM graph_configs WHERE id = %s"
affected_rows = mysql_client.execute_update(sql, (config_id,))
return affected_rows > 0
@staticmethod
def batch_delete_configs(config_ids: list) -> int:
"""批量删除配置"""
if not config_ids: return 0
try:
clean_ids = [int(cid) for cid in config_ids if str(cid).isdigit()]
except: return 0
if not clean_ids: return 0
placeholders = ', '.join(['%s'] * len(clean_ids))
sql = f"DELETE FROM graph_configs WHERE id IN ({placeholders})"
return mysql_client.execute_update(sql, tuple(clean_ids))
@staticmethod
def get_group_list() -> list:
"""单独获取方案名称列表,供前端下拉框使用"""
sql = "SELECT id, group_name FROM graph_style_groups ORDER BY create_time DESC"
return mysql_client.execute_query(sql) or []

66
service/OperationService.py

@ -76,8 +76,9 @@ class OperationService:
params["name"] = decoded_name
if label and str(label).strip() and label not in ["全部", ""]:
conditions.append("ANY(l IN labels(n) WHERE l = $label)")
# 使用标准的标签匹配语法
params["label"] = str(label).strip()
conditions.append("$label IN labels(n)")
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
@ -165,17 +166,48 @@ class OperationService:
return {"items": [], "total": 0}
# --- 4. 联想建议 ---
def suggest_nodes(self, keyword: str):
if not keyword: return []
def suggest_nodes(self, keyword: str, label: str = None):
"""
修复后的建议逻辑
1. 优化 Label 过滤语法确保在 keyword 为空时也能根据 Label 返回数据
2. 增加对空字符串的宽容处理
"""
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:
kw = unquote(str(keyword or "")).strip()
lb = str(label).strip() if label and label not in ["全部", "", "null", "undefined"] else None
# 如果既没有关键词也没有标签,直接返回空
if not kw and not lb:
return []
params = {}
# 基础匹配语句,排除无意义节点
match_clause = "MATCH (n)"
if lb:
# 动态构建标签匹配,使用 :`label` 语法更高效且准确
match_clause = f"MATCH (n:`{lb}`)"
conditions = ["n.name <> '未命名'"]
if kw:
conditions.append("n.name CONTAINS $kw")
params["kw"] = kw
where_clause = "WHERE " + " AND ".join(conditions)
# 查询数据库
cypher = f"{match_clause} {where_clause} RETURN DISTINCT n.name as name LIMIT 15"
results = self.db.execute_read(cypher, params)
db_suggestions = [row["name"] for row in results if row.get("name")]
# 如果依然没有结果,尝试去掉 Label 限制进行全库模糊匹配(保底逻辑)
if not db_suggestions and kw and lb:
fallback_cypher = "MATCH (n) WHERE n.name CONTAINS $kw AND n.name <> '未命名' RETURN DISTINCT n.name as name LIMIT 5"
fallback_res = self.db.execute_read(fallback_cypher, {"kw": kw})
db_suggestions = [row["name"] for row in fallback_res if row.get("name")]
return db_suggestions
except Exception as e:
print(f"Suggest Error Trace: {traceback.format_exc()}")
return []
# --- 5. 节点管理 ---
@ -226,7 +258,7 @@ class OperationService:
MATCH (n) WHERE elementId(n) = $id
SET n.name = $name
WITH n
REMOVE n:Drug:Disease:Symptom:Entity:Medicine:Check:Food
REMOVE n:Drug:Disease:Symptom:Entity:Medicine:Check:Food:Operation:CheckSubject:Complication:Diagnosis:Treatment:AdjuvantTherapy:adverseReactions:Department:DiseaseSite:RelatedDisease:RelatedSymptom:SpreadWay:Stage:Subject:SymptomAndSign:TreatmentPrograms:Type:Cause:Attribute:Indications:Ingredients:Pathogenesis:PathologicalType:Pathophysiology:Precautions:Prognosis:PrognosticSurvivalTime:DiseaseRatio:DrugTherapy:Infectious:MultipleGroups:DiseaseRate
WITH n
SET n:`{label}`
RETURN n
@ -259,12 +291,7 @@ class OperationService:
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
@ -275,9 +302,7 @@ class OperationService:
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 为准)
t_label = row["label"] if row.get("label") else t_name
if t_name not in seen_types:
type_map.append({
"type": t_name,
@ -333,7 +358,6 @@ class OperationService:
if result:
return {"success": True, "msg": "添加成功"}
return {"success": False, "msg": "关系创建失败"}
except Exception as e:
traceback.print_exc()
return {"success": False, "msg": f"数据库写入异常: {str(e)}"}

32
vue/src/api/data.js

@ -6,9 +6,6 @@ import request from '@/utils/request';
*/
// --- 存量数据 ID 自动修复 ---
/**
* 触发后端检查并修复 nodeId 0 或缺失的节点
*/
export function fixNodeIds() {
return request({
url: '/api/kg/admin/fix-ids',
@ -24,7 +21,7 @@ export function getKgStats() {
})
}
// --- 1. 获取全量动态标签 (用于节点管理下拉框) ---
// --- 1. 获取全量动态标签 ---
export function getLabels() {
return request({
url: '/api/kg/labels',
@ -32,11 +29,7 @@ export function getLabels() {
})
}
// --- 新增:获取全量动态关系类型 (用于关系管理下拉框) ---
/**
* 从后端获取所有关系类型 type 及其对应的中文 label 映射
* 返回格式示例: [{type: 'adverseReactions', label: '不良反应'}, ...]
*/
// --- 新增:获取全量动态关系类型 ---
export function getRelationshipTypes() {
return request({
url: '/api/kg/relationship-types',
@ -44,12 +37,21 @@ export function getRelationshipTypes() {
})
}
// --- 2. 输入联想建议 ---
export function getNodeSuggestions(keyword) {
// --- 2. 输入联想建议 (重点修改位置) ---
/**
* 获取联想词
* @param {string} keyword - 用户输入的文字
* @param {string} label - 当前选择的标签 (用于过滤图二的列表)
*/
export function getNodeSuggestions(keyword, label) {
return request({
url: '/api/kg/node/suggest',
method: 'get',
params: { keyword }
// 关键点:将 label 传给后端
params: {
keyword,
label
}
})
}
@ -76,7 +78,7 @@ export function addNode(data) {
return request({
url: '/api/kg/node/add',
method: 'post',
data // 格式: { label, name }
data // { label, name }
})
}
@ -94,7 +96,7 @@ export function addRelationship(data) {
return request({
url: '/api/kg/rel/add',
method: 'post',
data // 格式: { source, target, type, label }
data // { source, target, type, label }
})
}
@ -103,7 +105,7 @@ export function updateRelationship(data) {
return request({
url: '/api/kg/rel/update',
method: 'post',
data // 格式: { id, source, target, type, label }
data // { id, source, target, type, label }
})
}

96
vue/src/api/style.js

@ -0,0 +1,96 @@
// vue/src/api/style.js
import request from '@/utils/request';
/**
* 保存图谱样式配置
* @param {Object} data { canvas_name, current_label, styles, group_name }
* 说明group_name 为字符串后端会自动判断是使用已有组还是新建组
*/
export function saveGraphStyle(data) {
return request({
url: '/api/graph/style/save',
method: 'post',
data: data,
headers: {
'Content-Type': 'application/json'
}
});
}
/**
* 获取分组嵌套格式的样式配置列表 (核心新增)
* 用于右侧折叠面板渲染Group -> Configs
*/
export function getGroupedGraphStyleList() {
return request({
url: '/api/graph/style/list/grouped',
method: 'get'
});
}
/**
* 获取所有已存在的方案组名称列表
* 用于保存配置弹窗中的下拉选择框
*/
export function getGraphStyleGroups() {
return request({
url: '/api/graph/style/groups',
method: 'get'
});
}
/**
* 获取所有图谱样式配置列表
* 保留此接口用于兼容旧版逻辑或后台管理
*/
export function getGraphStyleList() {
return request({
url: '/api/graph/style/list',
method: 'get'
});
}
/**
* 删除整个方案组及其下属所有配置
* @param {Number} group_id 分组ID
*/
export function deleteGraphStyleGroup(group_id) {
return request({
url: '/api/graph/style/group/delete',
method: 'post',
data: { group_id },
headers: {
'Content-Type': 'application/json'
}
});
}
/**
* 删除指定的单个画布样式配置
* @param {Number} id 配置ID
*/
export function deleteGraphStyle(id) {
return request({
url: '/api/graph/style/delete',
method: 'post',
data: { id },
headers: {
'Content-Type': 'application/json'
}
});
}
/**
* 批量删除多个画布样式配置
* @param {Object} payload { ids: [1, 2, 3] }
*/
export function batchDeleteGraphStyle(payload) {
return request({
url: '/api/graph/style/batch_delete',
method: 'post',
data: payload,
headers: {
'Content-Type': 'application/json'
}
});
}

1040
vue/src/system/GraphStyle.vue

File diff suppressed because it is too large

105
vue/src/system/KGData.vue

@ -61,6 +61,7 @@
:fetch-suggestions="queryNodeSearch"
placeholder="搜索节点名称..."
clearable
:trigger-on-focus="true"
@select="handleNodeSearch"
@clear="handleNodeSearch"
class="search-input"
@ -68,10 +69,14 @@
</div>
<div class="input-group-inline">
<span class="filter-label-text">选择标签</span>
<el-select v-model="nodeSearch.label" placeholder="请选择标签" clearable @change="handleNodeSearch"
@clear="handleNodeSearch"
class="search-select">
<el-option label="全部" value="全部"/>
<el-select
v-model="nodeSearch.label"
placeholder="请选择标签"
clearable
@change="handleNodeLabelChange"
@clear="handleNodeLabelChange"
class="search-select"
>
<el-option v-for="item in dynamicLabels" :key="item" :label="translateToChinese(item)"
:value="item"/>
</el-select>
@ -111,8 +116,10 @@
background
layout="slot, sizes, prev, pager, next, jumper"
:total="nodeTotal"
:page-sizes="[10, 20, 50, 100]"
v-model:page-size="pageSize"
v-model:current-page="nodePage"
@size-change="handleNodeSizeChange"
@current-change="fetchNodes"
>
<span class="pagination-custom-text">共计 {{ nodeTotal.toLocaleString() }} 条数据</span>
@ -130,6 +137,7 @@
:fetch-suggestions="queryNodeSearch"
placeholder="搜索起点..."
clearable
:trigger-on-focus="true"
@select="handleRelSearch"
@clear="handleRelSearch"
class="search-input"
@ -161,6 +169,7 @@
:fetch-suggestions="queryNodeSearch"
placeholder="搜索终点..."
clearable
:trigger-on-focus="true"
@select="handleRelSearch"
@clear="handleRelSearch"
class="search-input"
@ -199,8 +208,10 @@
background
layout="slot, sizes, prev, pager, next, jumper"
:total="relTotal"
:page-sizes="[10, 20, 50, 100]"
v-model:page-size="pageSize"
v-model:current-page="relPage"
@size-change="handleRelSizeChange"
@current-change="fetchRels"
>
<span class="pagination-custom-text">共计 {{ relTotal.toLocaleString() }} 条数据</span>
@ -255,11 +266,11 @@
<el-form :model="relForm" label-width="120px" class="custom-form">
<el-form-item label="起始节点" required>
<el-autocomplete v-model="relForm.source" :fetch-suggestions="queryNodeSearch" style="width:100%"
placeholder="请输入起点名称"/>
placeholder="请输入起点名称" :trigger-on-focus="true"/>
</el-form-item>
<el-form-item label="结束节点" required>
<el-autocomplete v-model="relForm.target" :fetch-suggestions="queryNodeSearch" style="width:100%"
placeholder="请输入终点名称"/>
placeholder="请输入终点名称" :trigger-on-focus="true"/>
</el-form-item>
<el-form-item label="关系类型" required>
<el-select
@ -308,7 +319,7 @@ import {
} from '@/api/data'
import {preload} from "@/api/graph";
// --- () ---
// --- ---
const CHINESE_TO_ENGLISH_LABEL = {
"疾病": "Disease",
"症状": "Symptom",
@ -350,19 +361,15 @@ const CHINESE_TO_ENGLISH_LABEL = {
"发病率":"DiseaseRate"
};
//
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;
});
@ -392,7 +399,6 @@ const relTotal = ref(0);
const relPage = ref(1);
const relSearch = reactive({source: '', target: '', type: ''});
// --- ---
const nodeDialogVisible = ref(false);
const relDialogVisible = ref(false);
const detailVisible = ref(false);
@ -400,18 +406,27 @@ const detailType = ref('node');
const currentDetail = ref({});
const isEdit = ref(false);
// --- ---
const nodeForm = reactive({id: '', name: '', label: ''});
const relForm = reactive({id: '', source: '', target: '', type: '', label: ''});
// --- ---
// --- ---
const handleNodeSizeChange = (val) => {
pageSize.value = val;
nodePage.value = 1;
fetchNodes();
};
const handleRelSizeChange = (val) => {
pageSize.value = val;
relPage.value = 1;
fetchRels();
};
// --- ---
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;
});
@ -420,9 +435,7 @@ const fetchAllMetadata = async () => {
const initDataGovernance = async () => {
try {
const res = await fixNodeIds();
if (res?.code === 200 && activeName.value === 'first') {
fetchNodes();
}
if (res?.code === 200 && activeName.value === 'first') fetchNodes();
} catch (e) {
console.error(e);
}
@ -444,7 +457,7 @@ const fetchNodes = async () => {
page: nodePage.value,
pageSize: pageSize.value,
name: nodeSearch.name?.trim() || null,
label: (nodeSearch.label && nodeSearch.label !== "全部") ? nodeSearch.label : null
label: nodeSearch.label || null
});
if (res?.code === 200) {
nodeData.value = res.data.items;
@ -465,7 +478,7 @@ const fetchRels = async () => {
pageSize: pageSize.value,
source: relSearch.source?.trim() || null,
target: relSearch.target?.trim() || null,
type: relSearch.type || null // type
type: relSearch.type || null
});
if (res?.code === 200) {
relData.value = res.data.items;
@ -481,9 +494,9 @@ const fetchRels = async () => {
const openNodeDialog = (row = null) => {
isEdit.value = !!row;
if (row) {
Object.assign(nodeForm, {id: row.id, name: row.name, label: row.labels?.[0] || 'Drug'});
Object.assign(nodeForm, {id: row.id, name: row.name, label: row.labels?.[0] || ''});
} else {
Object.assign(nodeForm, {id: '', name: '', label: 'Drug'});
Object.assign(nodeForm, {id: '', name: '', label: ''});
}
nodeDialogVisible.value = true;
};
@ -501,6 +514,7 @@ const submitNode = async () => {
fetchStats();
fetchAllMetadata(); //
preload();
fetchAllMetadata();
} else {
ElMessage.error(res?.msg || '操作失败');
}
@ -515,11 +529,7 @@ 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 || ''
id: row.id, source: row.source, target: row.target, type: row.type, label: row.label || ''
});
} else {
Object.assign(relForm, {id: '', source: '', target: '', type: '', label: ''});
@ -538,7 +548,7 @@ const submitRel = async () => {
relDialogVisible.value = false;
fetchRels();
fetchStats();
fetchAllMetadata(); //
fetchAllMetadata();
} else {
ElMessage.error(res?.msg || '提交失败');
}
@ -567,19 +577,48 @@ const handleDelete = (row, type) => {
});
};
//
const handleNodeSearch = () => {
nodePage.value = 1;
fetchNodes();
};
//
const handleNodeLabelChange = () => {
nodeSearch.name = ''; //
nodePage.value = 1;
fetchNodes();
};
const handleRelSearch = () => {
relPage.value = 1;
fetchRels();
};
/**
* 联想搜索建议逻辑修改
* 重点将当前 nodeSearch.label 传给后端
*/
const queryNodeSearch = async (queryString, cb) => {
if (!queryString) return cb([]);
const res = await getNodeSuggestions(queryString);
cb((res.data || []).map(n => ({value: n})));
//
if (!nodeSearch.label && !queryString?.trim()) {
return cb([]);
}
try {
// nodeSearch.label
const res = await getNodeSuggestions(queryString || "", nodeSearch.label || null);
if (res?.code === 200) {
//
const results = (res.data || []).map(n => ({value: n}));
cb(results);
} else {
cb([]);
}
} catch (e) {
console.error("联想查询失败", e);
cb([]);
}
};
const handleView = (row, type) => {
@ -592,7 +631,7 @@ onMounted(() => {
initDataGovernance();
fetchStats();
fetchNodes();
fetchAllMetadata(); //
fetchAllMetadata();
});
</script>

Loading…
Cancel
Save