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