diff --git a/controller/GraphStyleController.py b/controller/GraphStyleController.py
new file mode 100644
index 0000000..bcf89a8
--- /dev/null
+++ b/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)}"})
\ No newline at end of file
diff --git a/controller/OperationController.py b/controller/OperationController.py
index e2741cc..afe162a 100644
--- a/controller/OperationController.py
+++ b/controller/OperationController.py
@@ -24,30 +24,32 @@ def create_response(status_code, data_dict):
def parse_request_body(req):
"""
- 解析器:适配 Robyn 框架,确保能准确拿到前端传来的 ID、nodeId、name 和 label。
+ 解析器:适配 Robyn 框架。
+ 针对前端 Vue3 + ElementPlus 的请求进行深度解析,确保获取 ID、nodeId、name 和 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()
diff --git a/controller/__init__.py b/controller/__init__.py
index 8104eb4..82bad8b 100644
--- a/controller/__init__.py
+++ b/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
diff --git a/service/GraphStyleService.py b/service/GraphStyleService.py
new file mode 100644
index 0000000..2156782
--- /dev/null
+++ b/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 []
\ No newline at end of file
diff --git a/service/OperationService.py b/service/OperationService.py
index 7200d81..206e741 100644
--- a/service/OperationService.py
+++ b/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 ""
@@ -164,18 +165,49 @@ class OperationService:
traceback.print_exc()
return {"items": [], "total": 0}
- # --- 4. 联想建议 ---
- def suggest_nodes(self, keyword: str):
- if not keyword: return []
+ # --- 4. 联想建议 ---
+ 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)}"}
diff --git a/vue/src/api/data.js b/vue/src/api/data.js
index 05cd815..690b355 100644
--- a/vue/src/api/data.js
+++ b/vue/src/api/data.js
@@ -5,10 +5,7 @@ import request from '@/utils/request';
* 知识图谱管理接口
*/
-// --- 存量数据 ID 自动修复 ---
-/**
- * 触发后端检查并修复 nodeId 为 0 或缺失的节点
- */
+// --- 存量数据 ID 自动修复 ---
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 }
})
}
diff --git a/vue/src/api/style.js b/vue/src/api/style.js
new file mode 100644
index 0000000..681f81f
--- /dev/null
+++ b/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'
+ }
+ });
+}
\ No newline at end of file
diff --git a/vue/src/system/GraphStyle.vue b/vue/src/system/GraphStyle.vue
index 370f873..e640cb8 100644
--- a/vue/src/system/GraphStyle.vue
+++ b/vue/src/system/GraphStyle.vue
@@ -1,6 +1,6 @@