From 33a8d8034a398ea361b5c17872861beacda48753 Mon Sep 17 00:00:00 2001
From: hanyuqing <1106611654@qq.com>
Date: Tue, 6 Jan 2026 15:24:53 +0800
Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=A1=B5=E9=9D=A2=E6=A0=B7?=
=?UTF-8?q?=E5=BC=8F=E4=BF=AE=E6=94=B92.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
controller/GraphStyleController.py | 42 +++++-
service/GraphStyleService.py | 107 ++++++++++----
vue/src/api/style.js | 35 ++++-
vue/src/system/GraphStyle.vue | 293 ++++++++++++++++++++-----------------
4 files changed, 306 insertions(+), 171 deletions(-)
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