Browse Source

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

# Conflicts:
#	vue/src/router/index.js
#	web_main.py
yangrongze
hanyuqing 3 months ago
parent
commit
474e99b231
  1. 193
      controller/OperationController.py
  2. 203
      service/OperationService.py
  3. 1
      vue/.npmrc
  4. 9
      vue/src/router/index.js
  5. 344
      vue/src/system/KGData.vue
  6. 9
      web_main.py

193
controller/OperationController.py

@ -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)})

203
service/OperationService.py

@ -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})

1
vue/.npmrc

@ -0,0 +1 @@
overrides=entities@3.19.0

9
vue/src/router/index.js

@ -7,6 +7,8 @@ import Display from '../system/GraphDemo.vue'
import Builder from '../system/GraphBuilder.vue' import Builder from '../system/GraphBuilder.vue'
import Style from '../system/GraphStyle.vue' import Style from '../system/GraphStyle.vue'
import QA from '../system/GraphQA.vue' import QA from '../system/GraphQA.vue'
import KGData from '../system/KGData.vue'
const routes = [ const routes = [
{ {
path: '/', path: '/',
@ -59,7 +61,12 @@ const routes = [
path: '/kg-qa', path: '/kg-qa',
name: 'QA', name: 'QA',
component: QA component: QA
} },
{
path: '/kg-data',
name: 'KGData',
component: KGData
},
] ]
const router = createRouter({ const router = createRouter({

344
vue/src/system/KGData.vue

@ -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>

9
web_main.py

@ -3,6 +3,10 @@ import controller
from service.UserService import init_mysql_connection from service.UserService import init_mysql_connection
import os import os
# 开启全局跨域支持,允许所有来源访问
# 这将解决浏览器报 "CORS error" 或请求被拦截的问题
ALLOW_CORS(app, ["*"])
# 添加静态文件服务 # 添加静态文件服务
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
resource_dir = os.path.join(current_dir, "resource") resource_dir = os.path.join(current_dir, "resource")
@ -11,4 +15,7 @@ if os.path.exists(resource_dir):
print(f"静态资源目录已配置: {resource_dir}") print(f"静态资源目录已配置: {resource_dir}")
if __name__ == "__main__": if __name__ == "__main__":
init_mysql_connection() and app.start(host="0.0.0.0", port=8088) # 启动服务
# 确保 init_mysql_connection 返回 True 或者去掉 and 逻辑以保证 start 执行
init_mysql_connection()
app.start(host="0.0.0.0", port=8088)
Loading…
Cancel
Save