From 4013f919cd04788e0aebaa12b44b2c239ee67b12 Mon Sep 17 00:00:00 2001
From: hanyuqing <1106611654@qq.com>
Date: Wed, 7 Jan 2026 11:18:00 +0800
Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=A1=B5=E9=9D=A2=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E8=A1=A5=E5=85=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
controller/GraphStyleController.py | 24 +++--
service/GraphStyleService.py | 38 ++++---
vue/src/system/GraphStyle.vue | 210 +++++++++++++++++++++++++++++--------
3 files changed, 207 insertions(+), 65 deletions(-)
diff --git a/controller/GraphStyleController.py b/controller/GraphStyleController.py
index 8141396..7c63874 100644
--- a/controller/GraphStyleController.py
+++ b/controller/GraphStyleController.py
@@ -19,24 +19,36 @@ def create_response(status_code, data_dict):
@app.post("/api/graph/style/save")
async def save_style_config(request):
- """保存配置接口 - 升级版:支持分组名"""
+ """保存配置接口 - 修复版:支持移动与更新逻辑"""
try:
body = request.json()
+
+ # 1. 核心修改:接收前端传来的 id
+ # 当执行“移动”操作时,前端会传 config.id;当执行“保存当前配置”时,id 为空
+ config_id = body.get('id')
+
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)
+ # 2. 核心修改:将 config_id 传给 Service 层
+ # 这样 Service 就能根据是否有 id 来判断是执行 UPDATE 还是 INSERT
+ success = GraphStyleService.save_config(
+ canvas_name=canvas_name,
+ current_label=current_label,
+ styles_dict=styles,
+ group_name=group_name,
+ config_id=config_id
+ )
+
if success:
- return create_response(200, {"code": 200, "msg": "保存成功"})
+ return create_response(200, {"code": 200, "msg": "操作成功"})
else:
- return create_response(200, {"code": 500, "msg": "保存失败"})
+ return create_response(200, {"code": 500, "msg": "操作失败"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"系统异常: {str(e)}"})
diff --git a/service/GraphStyleService.py b/service/GraphStyleService.py
index e41c38d..1246eb8 100644
--- a/service/GraphStyleService.py
+++ b/service/GraphStyleService.py
@@ -5,38 +5,46 @@ 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:
+ def save_config(canvas_name: str, current_label: str, styles_dict: dict, group_name: str = None, config_id: int = None) -> bool:
"""
- 保存图谱样式配置(增强版:自动处理分组逻辑)
+ 保存图谱样式配置(修复版:支持移动与更新逻辑)
"""
- # 1. 处理分组逻辑:查不到就建,查到了就用
+ # 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:
target_group_id = existing_group[0]['id']
else:
- # 如果不存在,新建一个组(is_active 和 is_default 默认为 false)
create_group_sql = "INSERT INTO graph_style_groups (group_name, is_active, is_default) VALUES (%s, %s, %s)"
mysql_client.execute_update(create_group_sql, (group_name, False, False))
-
- 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
+ target_group_id = mysql_client.execute_query("SELECT LAST_INSERT_ID() as last_id")[0]['last_id']
# 2. 转换样式 JSON
config_json = json.dumps(styles_dict, ensure_ascii=False)
- # 3. 插入配置表
- 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))
+ # 3. 【核心修复】:判断是更新(移动)还是新建
+ if config_id:
+ # 如果带了 ID,说明是“移动”或“修改”,执行 UPDATE
+ # 这样 group_id 会被更新,且不会产生新记录,配置就从原方案“消失”并出现在新方案了
+ sql = """
+ UPDATE graph_configs
+ SET canvas_name = %s, current_label = %s, config_json = %s, group_id = %s
+ WHERE id = %s
+ """
+ affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id, config_id))
+ else:
+ # 如果没有 ID,说明是点“保存”按钮新建的,执行 INSERT
+ 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
diff --git a/vue/src/system/GraphStyle.vue b/vue/src/system/GraphStyle.vue
index 81fdd52..62520bc 100644
--- a/vue/src/system/GraphStyle.vue
+++ b/vue/src/system/GraphStyle.vue
@@ -221,33 +221,51 @@
-
-
-
-
-
-
-
-
{{ item.canvas_name }}
-
已应用
+
+
+
+
+
+
+
+ {{ item.canvas_name }}
+ 已应用
+
+
标签: {{ item.current_label }}
-
标签: {{ item.current_label }}
+
+
+
-
-
-
-
+
+
+
+ 移动至方案:
+
+ {{ targetGroup.group_name }}
+
+
+
+
@@ -589,31 +607,66 @@ export default {
try {
const res = await getGroupedGraphStyleList();
if (res.code === 200) {
- this.styleGroups = res.data.map(group => ({
- ...group,
- configs: group.configs.map(conf => ({
- ...conf,
- styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles
- }))
- }));
-
- // 仅在初始加载且没有选中项时执行自动同步逻辑
+ // 1. 数据处理与【强制去重】
+ // 这里的逻辑确保了同一个配置 ID 在同一个分组内不会出现两次
+ this.styleGroups = res.data.map(group => {
+ const idSet = new Set();
+ const uniqueConfigs = [];
+
+ (group.configs || []).forEach(conf => {
+ if (!idSet.has(conf.id)) {
+ idSet.add(conf.id);
+ uniqueConfigs.push({
+ ...conf,
+ // 安全解析 styles 字符串
+ styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles
+ });
+ } else {
+ console.warn(`检测到重复 ID: ${conf.id},已在前端过滤`);
+ }
+ });
+
+ return {
+ ...group,
+ configs: uniqueConfigs
+ };
+ });
+
+ // 2. 初始加载时的应用逻辑(带排他性同步)
if (this.usingConfigIds.length === 0) {
+ const tempUsingIds = [];
+ const seenLabels = new Set(); // 记录已处理的标签(如:疾病、药品)
+
this.styleGroups.forEach(group => {
+ // 只处理后端标记为 active 的方案组
if (group.is_active) {
+ // 自动展开激活的折叠面板
if (!this.activeCollapseNames.includes(group.id)) {
this.activeCollapseNames.push(group.id);
}
- const ids = group.configs.map(c => c.id);
- this.usingConfigIds = [...this.usingConfigIds, ...ids];
+
+ // 遍历组内配置进行标签去重
+ group.configs.forEach(conf => {
+ if (!seenLabels.has(conf.current_label)) {
+ tempUsingIds.push(conf.id);
+ seenLabels.add(conf.current_label);
+ } else {
+ console.warn(`标签冲突拦截:方案【${group.group_name}】中的配置【${conf.canvas_name}】因标签【${conf.current_label}】已存在而被忽略`);
+ }
+ });
}
});
+
+ // 最终赋值
+ this.usingConfigIds = tempUsingIds;
}
+ // 3. 驱动图谱重新渲染
this.updateAllElements();
}
} catch (err) {
console.error("加载配置失败:", err);
+ ElMessage.error("获取方案列表失败");
}
},
async fetchGroupNames() {
@@ -637,40 +690,109 @@ export default {
}
this.updateAllElements();
},
+ /**
+ * 新增功能:将配置移动到另一个方案
+ */
+ async moveConfigToGroup(config, targetGroup) {
+ try {
+ // 1. 构建更新负载
+ const payload = {
+ id: config.id, // 确保有 ID,后端才会执行更新而非新增
+ canvas_name: config.canvas_name,
+ group_name: targetGroup.group_name, // 目标方案名
+ current_label: config.current_label,
+ styles: config.styles
+ };
+ const res = await saveGraphStyle(payload);
+
+ if (res.code === 200) {
+ ElMessage.success(`已成功移动至【${targetGroup.group_name}】`);
+
+ // 2. 【核心修复】手动清理本地 styleGroups 中的旧引用,避免 fetchConfigs 响应延迟导致的视觉重复
+ this.styleGroups = this.styleGroups.map(group => {
+ return {
+ ...group,
+ // 过滤掉所有组里 ID 等于当前移动配置 ID 的项
+ configs: group.configs.filter(c => c.id !== config.id)
+ };
+ });
+
+ // 3. 重新获取最新的服务器状态
+ await this.fetchConfigs();
+ }
+ } catch (err) {
+ console.error("移动配置失败:", err);
+ ElMessage.error("移动操作失败,请重试");
+ }
+ },
/**
* 修改后的应用全案方法
* 逻辑:保留当前已手动选中的配置,新方案中冲突的配置不予应用
*/
async applyWholeGroup(group) {
try {
- // 1. 获取当前正在使用的所有配置项对象
- const currentlyUsingConfigs = [];
+ // 1. 【新增】定义必须包含的 5 个核心标签
+ const REQUIRED_TAGS = ['疾病', '症状', '药品', '检查', '其他'];
+
+ // 2. 【新增】获取当前方案组里已有的标签
+ const currentLabels = group.configs.map(conf => conf.current_label);
+ const hasTags = new Set(currentLabels);
+
+ // 3. 【新增】找出缺失的标签
+ const missingTags = REQUIRED_TAGS.filter(tag => !hasTags.has(tag));
+
+ // 4. 【新增】拦截逻辑:如果标签不全,弹出提示并返回
+ if (missingTags.length > 0) {
+ return ElMessageBox.alert(
+ `该方案配置不完整,无法应用。必须配齐 5 个核心标签。` +
+ `
目前缺失:
${missingTags.join('、')}`,
+ '校验未通过',
+ {
+ confirmButtonText: '我知道了',
+ dangerouslyUseHTMLString: true,
+ type: 'warning'
+ }
+ );
+ }
+ // 获取当前正在使用的所有标签名(用于外部排他)
+ const currentlyUsingLabels = [];
this.styleGroups.forEach(g => {
g.configs.forEach(c => {
if (this.usingConfigIds.includes(c.id)) {
- currentlyUsingConfigs.push(c);
+ currentlyUsingLabels.push(c.current_label);
}
});
});
- // 2. 提取出当前已选中的标签列表 (例如: ['疾病', '药品'])
- const currentlySelectedLabels = currentlyUsingConfigs.map(c => c.current_label);
+ // 对新方案内部进行去重:如果方案内有多个同标签配置,只取第一个
+ const uniqueNewConfigs = [];
+ const seenLabelsInNewGroup = new Set();
+
+ group.configs.forEach(conf => {
+ if (!seenLabelsInNewGroup.has(conf.current_label)) {
+ uniqueNewConfigs.push(conf);
+ seenLabelsInNewGroup.add(conf.current_label);
+ }
+ });
- // 3. 过滤新方案:如果新方案里的配置标签 已经存在于当前已选列表中,则剔除
- const filteredNewConfigIds = group.configs
- .filter(newConf => !currentlySelectedLabels.includes(newConf.current_label))
+ // 过滤掉与当前已选标签冲突的配置(外部排他)
+ const filteredNewConfigIds = uniqueNewConfigs
+ .filter(newConf => !currentlyUsingLabels.includes(newConf.current_label))
.map(newConf => newConf.id);
- // 4. 将过滤后的新 ID 追加到现有的选中列表中
+ if (filteredNewConfigIds.length === 0) {
+ return ElMessage.info("该方案中的标签配置已存在,无需重复应用");
+ }
+
+ // 追加 ID
this.usingConfigIds = [...this.usingConfigIds, ...filteredNewConfigIds];
- // 5. 调用后端接口更新激活状态(保持后端数据同步)
+ // 调用后端接口更新状态
const res = await applyGraphStyleGroup(group.id);
if (res.code === 200) {
- // 重新获取列表以刷新 UI 状态(如“已应用”按钮状态)
await this.fetchConfigs();
- ElMessage.success(`方案【${group.group_name}】已应用,已保留您手动选择的标签`);
+ ElMessage.success(`方案【${group.group_name}】已应用,已自动过滤重复标签`);
}
} catch (err) {
console.error(err);