diff --git a/controller/GraphStyleController.py b/controller/GraphStyleController.py index bcf89a8..8141396 100644 --- a/controller/GraphStyleController.py +++ b/controller/GraphStyleController.py @@ -25,7 +25,7 @@ async def save_style_config(request): 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]): @@ -45,13 +45,51 @@ async def save_style_config(request): async def get_grouped_style_list(request): """获取【分组嵌套】格式的配置列表(用于右侧折叠面板)""" try: - # 调用 Service 的嵌套聚合方法 + # 调用 Service 的嵌套聚合方法,现在内部已包含 is_active/is_default 逻辑 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.post("/api/graph/style/group/apply") +async def apply_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.apply_group_all(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/group/set_default") +async def set_default_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.set_default_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.get("/api/graph/style/groups") async def get_group_names(request): """获取所有已存在的方案组列表(用于保存弹窗的下拉选择)""" diff --git a/service/GraphStyleService.py b/service/GraphStyleService.py index 2156782..e41c38d 100644 --- a/service/GraphStyleService.py +++ b/service/GraphStyleService.py @@ -2,15 +2,12 @@ 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() == "": @@ -21,13 +18,12 @@ class GraphStyleService: 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 + # 如果不存在,新建一个组(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 @@ -35,7 +31,7 @@ class GraphStyleService: # 2. 转换样式 JSON config_json = json.dumps(styles_dict, ensure_ascii=False) - # 3. 插入配置表(关联 target_group_id) + # 3. 插入配置表 sql = """ INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id) VALUES (%s, %s, %s, %s) @@ -46,19 +42,22 @@ class GraphStyleService: @staticmethod def get_grouped_configs() -> list: """ - 核心优化:获取嵌套结构的方案列表 (Group -> Configs) - 用于前端右侧折叠面板展示 + 核心优化:获取嵌套结构的方案列表 + 增加 is_active 和 is_default 字段支持,并按默认/激活状态排序 + """ + # 1. 查询所有方案组:让默认方案排在最上面 + groups_sql = """ + SELECT id, group_name, is_active, is_default + FROM graph_style_groups + ORDER BY is_default DESC, id ASC """ - # 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 和 时间 + # 3. 内存聚合 for conf in configs: if conf.get('config_json'): try: @@ -73,41 +72,88 @@ class GraphStyleService: # 组装数据结构 result = [] for g in groups: + # 兼容 MySQL 布尔值转换 (某些驱动返回 0/1) + g['is_active'] = bool(g['is_active']) + g['is_default'] = bool(g['is_default']) + # 找到属于该组的所有配置 g_children = [c for c in configs if c['group_id'] == g['id']] g['configs'] = g_children - # 增加一个前端控制开关用的字段 - g['expanded'] = False + + # 如果是激活状态,默认让它在前端展开 + g['expanded'] = g['is_active'] result.append(g) return result @staticmethod + def apply_group_all(group_id: int) -> bool: + """ + 核心新增:应用全案 + 逻辑:将该组设为 is_active=true,其余所有组设为 false + """ + try: + # 1. 全部重置为非激活 + reset_sql = "UPDATE graph_style_groups SET is_active = %s" + mysql_client.execute_update(reset_sql, (False,)) + + # 2. 激活指定组 + apply_sql = "UPDATE graph_style_groups SET is_active = %s WHERE id = %s" + affected_rows = mysql_client.execute_update(apply_sql, (True, group_id)) + return affected_rows > 0 + except Exception as e: + print(f"Apply group error: {e}") + return False + + @staticmethod + def set_default_group(group_id: int) -> bool: + """ + 核心新增:设为系统初始默认方案 + 逻辑:唯一性切换 + """ + try: + # 1. 全部取消默认 + reset_sql = "UPDATE graph_style_groups SET is_default = %s" + mysql_client.execute_update(reset_sql, (False,)) + + # 2. 设置新的默认项 + set_sql = "UPDATE graph_style_groups SET is_default = %s WHERE id = %s" + affected_rows = mysql_client.execute_update(set_sql, (True, group_id)) + return affected_rows > 0 + except Exception as e: + print(f"Set default error: {e}") + return False + + @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" + """获取扁平查询,增加关联方案的激活状态""" + sql = """ + SELECT c.id, c.group_id, c.canvas_name, c.current_label, c.config_json, c.create_time, g.is_active + FROM graph_configs c + LEFT JOIN graph_style_groups g ON c.group_id = g.id + ORDER BY c.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'] = {} + 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') + row['is_active'] = bool(row.get('is_active', False)) 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 @@ -125,7 +171,8 @@ class GraphStyleService: if not config_ids: return 0 try: clean_ids = [int(cid) for cid in config_ids if str(cid).isdigit()] - except: return 0 + except: + return 0 if not clean_ids: return 0 placeholders = ', '.join(['%s'] * len(clean_ids)) @@ -134,6 +181,6 @@ class GraphStyleService: @staticmethod def get_group_list() -> list: - """单独获取方案名称列表,供前端下拉框使用""" - sql = "SELECT id, group_name FROM graph_style_groups ORDER BY create_time DESC" + """单独获取方案列表,增加状态返回""" + sql = "SELECT id, group_name, is_active, is_default FROM graph_style_groups ORDER BY is_default DESC, create_time DESC" return mysql_client.execute_query(sql) or [] \ No newline at end of file diff --git a/vue/src/api/style.js b/vue/src/api/style.js index 681f81f..ed88a88 100644 --- a/vue/src/api/style.js +++ b/vue/src/api/style.js @@ -18,8 +18,9 @@ export function saveGraphStyle(data) { } /** - * 获取【分组嵌套】格式的样式配置列表 (核心新增) + * 获取【分组嵌套】格式的样式配置列表 * 用于右侧折叠面板渲染:Group -> Configs + * 后端已按 is_default 排序,并将 is_active 的项标记为 expanded */ export function getGroupedGraphStyleList() { return request({ @@ -29,6 +30,38 @@ export function getGroupedGraphStyleList() { } /** + * 应用全案 + * @param {Number} group_id 分组ID + * 说明:将该方案组设为 is_active=true,其余设为 false + */ +export function applyGraphStyleGroup(group_id) { + return request({ + url: '/api/graph/style/group/apply', + method: 'post', + data: { group_id }, + headers: { + 'Content-Type': 'application/json' + } + }); +} + +/** + * 设为默认方案 + * @param {Number} group_id 分组ID + * 说明:将该方案组设为系统初始加载时的默认配置 + */ +export function setDefaultGraphStyleGroup(group_id) { + return request({ + url: '/api/graph/style/group/set_default', + method: 'post', + data: { group_id }, + headers: { + 'Content-Type': 'application/json' + } + }); +} + +/** * 获取所有已存在的方案组名称列表 * 用于保存配置弹窗中的下拉选择框 */ diff --git a/vue/src/system/GraphStyle.vue b/vue/src/system/GraphStyle.vue index 82027c3..95f5560 100644 --- a/vue/src/system/GraphStyle.vue +++ b/vue/src/system/GraphStyle.vue @@ -199,11 +199,10 @@ style="margin-right: 8px;"/> {{ group.group_name }} - (已应用全案) 取消全案 + @click.stop>已应用 @@ -303,15 +303,18 @@ import { getGraphStyleGroups, deleteGraphStyle, batchDeleteGraphStyle, - deleteGraphStyleGroup + deleteGraphStyleGroup, + applyGraphStyleGroup // 新增导入 } from '@/api/style'; import {ElMessageBox, ElMessage} from 'element-plus'; import {markRaw} from 'vue'; const tagToLabelMap = { - '疾病': 'Disease', '症状': 'Symptom', '病因': 'Cause', '药品': 'Drug', '科室': 'Department', '检查': 'Check','其他':'Other' + '疾病': 'Disease', '症状': 'Symptom', '病因': 'Cause', '药品': 'Drug', '科室': 'Department', '检查': 'Check', '其他': 'Other' }; +const CORE_LABELS = ['Disease', 'Symptom', 'Drug', 'Check']; + const INITIAL_FILL_MAP = { 'Disease': '#EF4444', 'Drug': '#91cc75', 'Symptom': '#fac858', 'Check': '#336eee', 'Cause': '#59d1d4', 'Department': '#59d1d4', 'Other': '#59d1d4' @@ -361,7 +364,7 @@ export default { edgeFontColor: '#666666', edgeType: 'line', edgeLineWidth: 2, - edgeStroke: '#EF4444', // 初始同步节点颜色 + edgeStroke: '#EF4444', defaultData: { nodes: [ {id: "node1", data: {name: "霍乱", label: "Disease"}}, @@ -392,23 +395,17 @@ export default { } }, watch: { - // 新增:监听方案勾选,同步勾选其下的所有画布 checkedGroupIds(newGroupIds) { - // 遍历所有方案 this.styleGroups.forEach(group => { const isGroupChecked = newGroupIds.includes(group.id); const childIds = group.configs.map(c => c.id); - if (isGroupChecked) { - // 如果方案被勾选,将子画布 ID 全部加入 checkedConfigIds (去重) this.checkedConfigIds = Array.from(new Set([...this.checkedConfigIds, ...childIds])); } else { - // 如果方案取消勾选,将子画布 ID 从 checkedConfigIds 中移除 this.checkedConfigIds = this.checkedConfigIds.filter(id => !childIds.includes(id)); } }); }, - // 监听节点填充色,强制同步线条色 nodeFill(newVal) { this.edgeStroke = newVal; }, @@ -481,7 +478,7 @@ export default { nodeShape: 'circle', nodeSize: 60, nodeFill: fill, nodeStroke: INITIAL_STROKE_MAP[label] || '#40999b', nodeLineWidth: 2, edgeShowLabel: true, edgeEndArrow: true, edgeFontFamily: 'Microsoft YaHei, sans-serif', edgeFontSize: 10, edgeFontColor: '#666666', edgeType: 'line', - edgeLineWidth: 2, edgeStroke: fill // 初始线条颜色同步填充色 + edgeLineWidth: 2, edgeStroke: fill }; }, handleTagClick(tag) { @@ -518,8 +515,12 @@ export default { this._graph = markRaw(graph); this.updateAllElements(); }, + getEffectiveStyleKey(label) { + return CORE_LABELS.includes(label) ? label : 'Other'; + }, updateAllElements() { if (!this._graph) return; + const labelToAppliedConfigMap = {}; this.styleGroups.forEach(group => { group.configs.forEach(conf => { @@ -538,8 +539,10 @@ export default { }; const nodes = this.defaultData.nodes.map(node => { - const labelEn = node.data?.label || ''; - const s = labelToAppliedConfigMap[labelEn] || this.tagStyles[labelEn]; + const rawLabel = node.data?.label || ''; + const effectiveKey = this.getEffectiveStyleKey(rawLabel); + const s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey]; + return { ...node, type: s?.nodeShape || 'circle', style: { @@ -553,13 +556,11 @@ export default { }); const edges = this.defaultData.edges.map(edge => { - const sLabel = this._nodeLabelMap.get(edge.source); - // 获取源节点对应的样式(可能是方案里的,也可能是实时调整的) - const s = labelToAppliedConfigMap[sLabel] || this.tagStyles[sLabel] || this; + const sRawLabel = this._nodeLabelMap.get(edge.source); + const effectiveKey = this.getEffectiveStyleKey(sRawLabel); + const s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey] || this; - // 线条颜色直接取该标签配置下的 edgeStroke const strokeColor = hexToRgba(s.edgeStroke, 0.6); - return { ...edge, type: s.edgeType || 'line', style: { @@ -589,10 +590,28 @@ export default { 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 + ...group, + configs: group.configs.map(conf => ({ + ...conf, + styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles })) })); + + // --- 核心改动:初始加载同步逻辑 --- + this.activeCollapseNames = []; + this.usingConfigIds = []; + + this.styleGroups.forEach(group => { + if (group.is_active) { + // 1. 自动展开激活方案 + this.activeCollapseNames.push(group.id); + // 2. 将激活方案下的所有配置加入渲染列表 + const ids = group.configs.map(c => c.id); + this.usingConfigIds = [...this.usingConfigIds, ...ids]; + } + }); + + this.updateAllElements(); } } catch (err) { console.error("加载配置失败:", err); @@ -607,6 +626,7 @@ export default { if (idx > -1) { this.usingConfigIds.splice(idx, 1); } else { + // 同类标签排他性应用 this.styleGroups.forEach(g => { g.configs.forEach(c => { if (this.usingConfigIds.includes(c.id) && c.current_label === item.current_label) { @@ -618,47 +638,48 @@ export default { } this.updateAllElements(); }, - applyWholeGroup(group) { - const idsToApply = group.configs.map(c => c.id); - group.configs.forEach(conf => { - this.styleGroups.forEach(g => { - g.configs.forEach(c => { - if (c.current_label === conf.current_label) this.usingConfigIds = this.usingConfigIds.filter(id => id !== c.id); - }); - }); - }); - this.usingConfigIds = Array.from(new Set([...this.usingConfigIds, ...idsToApply])); - this.updateAllElements(); - ElMessage.success(`方案【${group.group_name}】已应用`); - }, - cancelWholeGroup(group) { - const idsToCancel = group.configs.map(c => c.id); - this.usingConfigIds = this.usingConfigIds.filter(id => !idsToCancel.includes(id)); - this.updateAllElements(); - }, - isGroupFullyApplied(group) { - if (!group.configs || group.configs.length === 0) return false; - return group.configs.every(c => this.usingConfigIds.includes(c.id)); + async applyWholeGroup(group) { + try { + // 1. 调用后端接口更新激活状态 + const res = await applyGraphStyleGroup(group.id); + if (res.code === 200) { + // 2. 刷新列表(fetchConfigs 会自动同步 usingConfigIds 和 expanded 状态) + await this.fetchConfigs(); + ElMessage.success(`方案【${group.group_name}】已成功应用全案`); + } + } catch (err) { + ElMessage.error("应用全案失败"); + } }, handleSaveClick() { this.fetchGroupNames(); - - // 使用 Date.now() 获取当前 13 位毫秒时间戳 this.saveForm.canvas_name = Date.now().toString(); this.saveDialogVisible = true; }, async confirmSave() { const payload = { - canvas_name: this.saveForm.canvas_name, group_name: this.saveForm.group_name, current_label: this.activeTags, + canvas_name: this.saveForm.canvas_name, + group_name: this.saveForm.group_name, + current_label: this.activeTags, styles: {...this.tagStyles[tagToLabelMap[this.activeTags]]} }; const res = await saveGraphStyle(payload); if (res.code === 200) { ElMessage.success("保存成功"); this.saveDialogVisible = false; + this.resetAllTagsToDefault(); this.fetchConfigs(); } }, + resetAllTagsToDefault() { + Object.keys(this.tagStyles).forEach(labelKey => { + this.tagStyles[labelKey] = this.getInitialTagParams(labelKey); + }); + this.activeTags = '疾病'; + const diseaseInitial = this.getInitialTagParams('Disease'); + Object.assign(this, diseaseInitial); + this.updateAllElements(); + }, resetStyle() { const labelEn = tagToLabelMap[this.activeTags]; const initial = this.getInitialTagParams(labelEn); @@ -669,7 +690,7 @@ export default { }, async deleteSingleConfig(id) { try { - await ElMessageBox.confirm('确定删除此配置吗?'); + await ElMessageBox.confirm('确定删除此配置吗?', '提示'); const res = await deleteGraphStyle(id); if (res.code === 200) { this.usingConfigIds = this.usingConfigIds.filter(cid => cid !== id); @@ -680,7 +701,7 @@ export default { }, async deleteGroup(groupId) { try { - await ElMessageBox.confirm('确定删除整个方案吗?'); + await ElMessageBox.confirm('确定删除整个方案吗?', '提示'); const res = await deleteGraphStyleGroup(groupId); if (res.code === 200) { this.fetchConfigs(); @@ -692,13 +713,11 @@ export default { try { await ElMessageBox.confirm( '确定执行批量删除吗?', - '批量删除', // 这里添加标题 + '批量删除', { confirmButtonText: '确定', cancelButtonText: '取消', - type: 'warning', - // 确保标题和内容对齐 - distinguishCancelAndClose: true, + type: 'warning' } ); for (const gid of this.checkedGroupIds) await deleteGraphStyleGroup(gid); @@ -718,7 +737,6 @@ export default { } } - + \ No newline at end of file