Browse Source

Merge branch 'mh' of http://124.70.32.114:3100/hanyuqing/KGPython into hanyuqing

# Conflicts:
#	vue/src/system/GraphStyle.vue
hanyuqing
hanyuqing 3 months ago
parent
commit
34e439d953
  1. 36
      controller/GraphStyleController.py
  2. 91
      service/GraphStyleService.py
  3. 587
      vue/src/system/GraphStyle.vue

36
controller/GraphStyleController.py

@ -19,29 +19,38 @@ def create_response(status_code, data_dict):
@app.post("/api/graph/style/save")
async def save_style_config(request):
"""保存配置接口 - 修复版:支持移动与更新逻辑"""
"""
保存配置接口 - 增强防跑偏版
逻辑
1. 如果 body 中包含 is_auto_save: true则强制忽略 group_name防止自动保存篡改归属
2. 如果是普通保存或移动则正常传递 group_name
"""
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
group_name = body.get('group_name')
# 增加一个前端标识:如果是实时同步(防抖保存),前端可以传这个字段
is_auto_save = body.get('is_auto_save', False)
if not all([canvas_name, current_label, styles]):
return create_response(200, {"code": 400, "msg": "参数不完整"})
# 2. 核心修改:将 config_id 传给 Service 层
# 这样 Service 就能根据是否有 id 来判断是执行 UPDATE 还是 INSERT
# 如果是自动保存模式,显式清空 group_name,强制 Service 进入“仅更新样式”逻辑
final_group_name = None if is_auto_save else group_name
# 将处理后的参数传给 Service 层
success = GraphStyleService.save_config(
canvas_name=canvas_name,
current_label=current_label,
styles_dict=styles,
group_name=group_name,
group_name=final_group_name,
config_id=config_id
)
@ -55,9 +64,8 @@ async def save_style_config(request):
@app.get("/api/graph/style/list/grouped")
async def get_grouped_style_list(request):
"""获取【分组嵌套】格式的配置列表(用于右侧折叠面板)"""
"""获取【分组嵌套】格式的配置列表"""
try:
# 调用 Service 的嵌套聚合方法,现在内部已包含 is_active/is_default 逻辑
data = GraphStyleService.get_grouped_configs()
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"})
except Exception as e:
@ -75,7 +83,7 @@ async def get_active_style(request):
@app.post("/api/graph/style/group/apply")
async def apply_style_group(request):
"""应用全案:将某个方案组设为当前激活状态"""
"""应用全案"""
try:
body = request.json()
group_id = body.get('group_id')
@ -85,7 +93,7 @@ async def apply_style_group(request):
success = GraphStyleService.apply_group_all(group_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": "应用全案失败"})
except Exception as e:
@ -94,7 +102,7 @@ async def apply_style_group(request):
@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')
@ -113,7 +121,7 @@ async def set_default_style_group(request):
@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": "查询成功"})
@ -123,7 +131,7 @@ async def get_group_names(request):
@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": "查询成功"})

91
service/GraphStyleService.py

@ -7,12 +7,18 @@ class GraphStyleService:
@staticmethod
def save_config(canvas_name: str, current_label: str, styles_dict: dict, group_name: str = None, config_id: int = None) -> bool:
"""
保存图谱样式配置修复版支持移动与更新逻辑
保存图谱样式配置修复版防止自动保存导致的分组乱跑
"""
# 1. 处理分组逻辑
if not group_name or group_name.strip() == "":
group_name = "默认方案"
# 2. 转换样式 JSON
config_json = json.dumps(styles_dict, ensure_ascii=False)
# 3. 【核心修改点】:区分 更新 还是 新建
if config_id:
# --- 更新逻辑 ---
# 如果带了 ID,我们要极其谨慎地处理 group_id,防止在自动保存时被误改
# A. 如果调用者明确传了 group_name,说明是“移动”或“初次保存到某组”
if group_name and group_name.strip() != "":
# 检查/创建 目标方案组
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,))
@ -24,13 +30,7 @@ class GraphStyleService:
mysql_client.execute_update(create_group_sql, (group_name, False, False))
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. 【核心修复】:判断是更新(移动)还是新建
if config_id:
# 如果带了 ID,说明是“移动”或“修改”,执行 UPDATE
# 这样 group_id 会被更新,且不会产生新记录,配置就从原方案“消失”并出现在新方案了
# 执行带分组更新的 SQL
sql = """
UPDATE graph_configs
SET canvas_name = %s, current_label = %s, config_json = %s, group_id = %s
@ -38,7 +38,29 @@ class GraphStyleService:
"""
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id, config_id))
else:
# 如果没有 ID,说明是点“保存”按钮新建的,执行 INSERT
# B. 如果没有传 group_name,说明是“实时自动保存”,严禁修改 group_id
# 这样即使前端变量乱了,数据库的分组也不会变
sql = """
UPDATE graph_configs
SET canvas_name = %s, current_label = %s, config_json = %s
WHERE id = %s
"""
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, config_id))
else:
# --- 新建逻辑 ---
# 新建时必须有组名,默认“默认方案”
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:
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))
target_group_id = mysql_client.execute_query("SELECT LAST_INSERT_ID() as last_id")[0]['last_id']
sql = """
INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id)
VALUES (%s, %s, %s, %s)
@ -50,10 +72,8 @@ class GraphStyleService:
@staticmethod
def get_grouped_configs() -> list:
"""
核心优化获取嵌套结构的方案列表
增加 is_active is_default 字段支持并按默认/激活状态排序
获取嵌套结构的方案列表按默认/激活状态排序
"""
# 1. 查询所有方案组:让默认方案排在最上面
groups_sql = """
SELECT id, group_name, is_active, is_default
FROM graph_style_groups
@ -61,11 +81,9 @@ class GraphStyleService:
"""
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. 内存聚合
for conf in configs:
if conf.get('config_json'):
try:
@ -77,18 +95,12 @@ class GraphStyleService:
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:
# 兼容 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'] = g['is_active']
result.append(g)
@ -142,16 +154,10 @@ class GraphStyleService:
return [group] # 返回单元素列表,保持接口兼容性
@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
@ -161,16 +167,10 @@ class GraphStyleService:
@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
@ -180,7 +180,7 @@ class GraphStyleService:
@staticmethod
def get_all_configs() -> list:
"""获取扁平查询,增加关联方案的激活状态"""
"""获取扁平查询"""
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
@ -204,30 +204,27 @@ class GraphStyleService:
@staticmethod
def delete_group(group_id: int) -> bool:
"""逻辑级联删除"""
"""级联删除"""
del_configs_sql = "DELETE FROM graph_configs WHERE group_id = %s"
mysql_client.execute_update(del_configs_sql, (group_id,))
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
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})"
@ -235,6 +232,6 @@ class GraphStyleService:
@staticmethod
def get_group_list() -> list:
"""单独获取方案列表,增加状态返回"""
sql = "SELECT id, group_name, is_active, is_default FROM graph_style_groups ORDER BY is_default DESC, create_time DESC"
"""获取方案列表"""
sql = "SELECT id, group_name, is_active, is_default FROM graph_style_groups ORDER BY is_default DESC, id DESC"
return mysql_client.execute_query(sql) or []

587
vue/src/system/GraphStyle.vue

@ -218,8 +218,7 @@
size="small"
type="info"
disabled
plain
@click.stop>已应用
plain>已应用
</el-button>
<i class="el-icon-delete group-del" @click.stop="deleteGroup(group.id)"></i>
@ -236,10 +235,10 @@
<div
class="config-card"
:class="{
'card-using': usingConfigIds.includes(item.id),
'card-using': item.id === editingConfigId,
'card-checked': checkedConfigIds.includes(item.id)
}"
@click="toggleApplyConfig(item)"
@click="handleEditConfig(item)"
>
<div class="card-left">
<div class="checkbox-wrapper">
@ -248,7 +247,7 @@
<div class="card-info">
<div class="card-title-row">
<span class="card-name">{{ item.canvas_name }}</span>
<span v-if="usingConfigIds.includes(item.id)" class="status-badge">已应用</span>
<span v-if="item.id === editingConfigId" class="status-badge">编辑中</span>
</div>
<span class="card-tag">标签: {{ item.current_label }}</span>
</div>
@ -359,6 +358,8 @@ export default {
checkedConfigIds: [],
checkedGroupIds: [],
usingConfigIds: [],
editingConfigId: null,
editingConfigLabel: '',
saveDialogVisible: false,
saveForm: { group_name: '', canvas_name: '' },
tagStyles: {
@ -376,7 +377,7 @@ export default {
nodeFontSize: 12,
nodeFontColor: '#ffffff',
nodeShape: 'circle',
nodeSize: 50,
nodeSize: 60,
nodeFill: '#EF4444',
nodeStroke: '#B91C1C',
nodeLineWidth: 2,
@ -414,7 +415,9 @@ export default {
{ id: "e9", source: "node1", target: "node10", data: { relationship: { properties: { label: "并发症" } } } },
{ id: "e10", source: "node1", target: "node11", data: { relationship: { properties: { label: "检查" } } } }
]
}
},
saveTimer: null,
isInitialEcho: false
}
},
watch: {
@ -459,40 +462,141 @@ export default {
this._graph = null;
}
window.removeEventListener('resize', this.handleResize);
if (this.saveTimer) clearTimeout(this.saveTimer);
},
methods: {
handleEditConfig(item) {
if (this.saveTimer) clearTimeout(this.saveTimer);
this.isInitialEcho = true;
this.editingConfigId = item.id;
this.editingConfigLabel = item.current_label;
this.activeTags = item.current_label;
const s = item.styles;
if (!s) {
this.isInitialEcho = false;
return;
}
this.nodeShowLabel = s.nodeShowLabel;
this.nodeFontFamily = s.nodeFontFamily;
this.nodeFontSize = s.nodeFontSize;
this.nodeFontColor = s.nodeFontColor;
this.nodeShape = s.nodeShape;
this.nodeSize = s.nodeSize;
this.nodeFill = s.nodeFill;
this.nodeStroke = s.nodeStroke;
this.nodeLineWidth = s.nodeLineWidth;
this.edgeShowLabel = s.edgeShowLabel;
this.edgeEndArrow = s.edgeEndArrow;
this.edgeFontFamily = s.edgeFontFamily;
this.edgeFontSize = s.edgeFontSize;
this.edgeFontColor = s.edgeFontColor;
this.edgeType = s.edgeType;
this.edgeLineWidth = s.edgeLineWidth;
this.edgeStroke = s.edgeStroke;
const labelEn = tagToLabelMap[item.current_label];
if (labelEn) this.tagStyles[labelEn] = JSON.parse(JSON.stringify(s));
if (!this.usingConfigIds.includes(item.id)) {
this.styleGroups.forEach(g => {
g.configs.forEach(c => {
if (this.usingConfigIds.includes(c.id) && c.current_label === item.current_label) {
this.usingConfigIds = this.usingConfigIds.filter(id => id !== c.id);
}
});
});
this.usingConfigIds.push(item.id);
}
this.updateAllElements();
this.$nextTick(() => {
setTimeout(() => {
this.isInitialEcho = false;
}, 100);
});
},
syncAndRefresh() {
if (this.isInitialEcho) return;
const labelEn = tagToLabelMap[this.activeTags];
if (!labelEn) return;
const currentStyle = {
nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize,
nodeFontColor: this.nodeFontColor, nodeShape: this.nodeShape, nodeSize: this.nodeSize,
nodeFill: this.nodeFill, nodeStroke: this.nodeStroke, nodeLineWidth: this.nodeLineWidth,
edgeShowLabel: this.edgeShowLabel, edgeEndArrow: this.edgeEndArrow, edgeFontFamily: this.edgeFontFamily,
edgeFontSize: this.edgeFontSize, edgeFontColor: this.edgeFontColor, edgeType: this.edgeType,
edgeLineWidth: this.edgeLineWidth, edgeStroke: this.edgeStroke
};
this.tagStyles[labelEn] = currentStyle;
this.updateAllElements();
if (this.editingConfigId) {
if (this.saveTimer) clearTimeout(this.saveTimer);
const currentEditId = this.editingConfigId;
this.saveTimer = setTimeout(async () => {
try {
let targetConf = null;
let groupName = null;
for (const group of this.styleGroups) {
targetConf = group.configs.find(c => c.id === currentEditId);
if (targetConf) {
groupName = group.group_name;
break;
}
}
if (targetConf) {
const payload = {
id: currentEditId,
canvas_name: targetConf.canvas_name,
current_label: targetConf.current_label,
group_name: groupName,
styles: currentStyle,
is_auto_save: true
};
await saveGraphStyle(payload);
targetConf.styles = JSON.parse(JSON.stringify(currentStyle));
}
} catch (err) {
console.error("同步失败:", err);
}
}, 800);
}
},
validateNodeSize(event) {
const inputVal = event.target.value;
const val = parseInt(inputVal);
const val = parseInt(event.target.value);
if (isNaN(val) || val < 30 || val > 100) {
ElMessage({ message: `节点尺寸请输入 30 到 100 之间的数字`, type: 'warning', duration: 1500 });
ElMessage({ message: `请输入 30 到 100`, type: 'warning' });
event.target.value = this.nodeSize;
return;
}
this.nodeSize = val;
this.syncAndRefresh();
},
validateNodeLineWidth(event) {
const inputVal = event.target.value;
const val = parseInt(inputVal);
const val = parseInt(event.target.value);
if (isNaN(val) || val < 1 || val > 5) {
ElMessage({ message: `边框尺寸请输入 1 到 5 之间的数字`, type: 'warning', duration: 1500 });
ElMessage({ message: `请输入 1 到 5`, type: 'warning' });
event.target.value = this.nodeLineWidth;
return;
}
this.nodeLineWidth = val;
this.syncAndRefresh();
},
validateEdgeLineWidth(event) {
const inputVal = event.target.value;
const val = parseInt(inputVal);
const val = parseInt(event.target.value);
if (isNaN(val) || val < 1 || val > 5) {
ElMessage({ message: `线条粗细请输入 1 到 5 之间的数字`, type: 'warning', duration: 1500 });
ElMessage({ message: `请输入 1 到 5`, type: 'warning' });
event.target.value = this.edgeLineWidth;
return;
}
this.edgeLineWidth = val;
this.syncAndRefresh();
},
getInitialTagParams(label) {
const fill = INITIAL_FILL_MAP[label] || '#59d1d4';
@ -504,27 +608,35 @@ export default {
edgeLineWidth: 2, edgeStroke: fill
};
},
handleTagClick(tag) {
this.activeTags = tag;
const label = tagToLabelMap[tag];
const style = this.tagStyles[label];
if (style) Object.assign(this, style);
if (this.editingConfigId && this.editingConfigLabel !== tag) {
ElMessageBox.confirm(
`当前正在编辑【${this.editingConfigLabel}】的存储配置,切换到【${tag}】将退出编辑模式,未保存的修改可能丢失。`,
'提示',
{ confirmButtonText: '确定切换', cancelButtonText: '取消', type: 'warning' }
).then(() => {
this.editingConfigId = null;
this.editingConfigLabel = '';
this.performTagSwitch(tag);
}).catch(() => {});
} else {
this.performTagSwitch(tag);
}
},
syncAndRefresh() {
const label = tagToLabelMap[this.activeTags];
console.log(this.nodeFill)
if (label) {
this.tagStyles[label] = {
nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize,
nodeFontColor: this.nodeFontColor, nodeShape: this.nodeShape, nodeSize: this.nodeSize,
nodeFill: this.nodeFill, nodeStroke: this.nodeStroke, nodeLineWidth: this.nodeLineWidth,
edgeShowLabel: this.edgeShowLabel, edgeEndArrow: this.edgeEndArrow, edgeFontFamily: this.edgeFontFamily,
edgeFontSize: this.edgeFontSize, edgeFontColor: this.edgeFontColor, edgeType: this.edgeType,
edgeLineWidth: this.edgeLineWidth, edgeStroke: this.edgeStroke
};
performTagSwitch(tag) {
this.activeTags = tag;
const labelEn = tagToLabelMap[tag];
const style = this.tagStyles[labelEn];
if (style) {
this.isInitialEcho = true;
Object.assign(this, style);
this.$nextTick(() => { this.isInitialEcho = false; });
}
this.updateAllElements();
},
initGraph() {
const container = this.$refs.graphContainer;
if (!container || container.clientWidth === 0) return;
@ -534,7 +646,7 @@ export default {
container, width: container.clientWidth, height: container.clientHeight || 600,
layout: { type: 'radial', unitRadius: 100, preventOverlap: true, nodeSpacing: 50 },
behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element', 'hover-activate'],
autoFit: 'center', animation: true
autoFit: 'center', animation: false
});
this._graph = markRaw(graph);
this.updateAllElements();
@ -545,6 +657,7 @@ export default {
updateAllElements() {
if (!this._graph) return;
const currentActiveLabelEn = tagToLabelMap[this.activeTags];
const labelToAppliedConfigMap = {};
this.styleGroups.forEach(group => {
group.configs.forEach(conf => {
@ -565,7 +678,18 @@ export default {
const nodes = this.defaultData.nodes.map(node => {
const rawLabel = node.data?.label || '';
const effectiveKey = this.getEffectiveStyleKey(rawLabel);
const s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey];
let s;
if (effectiveKey === currentActiveLabelEn) {
s = {
nodeShape: this.nodeShape, nodeSize: this.nodeSize, nodeFill: this.nodeFill,
nodeStroke: this.nodeStroke, nodeLineWidth: this.nodeLineWidth,
nodeShowLabel: this.nodeShowLabel, nodeFontColor: this.nodeFontColor,
nodeFontSize: this.nodeFontSize, nodeFontFamily: this.nodeFontFamily
};
} else {
s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey];
}
return {
...node, type: s?.nodeShape || 'circle',
@ -582,26 +706,30 @@ export default {
const edges = this.defaultData.edges.map(edge => {
const sRawLabel = this._nodeLabelMap.get(edge.source);
const effectiveKey = this.getEffectiveStyleKey(sRawLabel);
const s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey] || this;
const strokeColor = hexToRgba(s.edgeStroke, 0.6);
let s;
if (effectiveKey === currentActiveLabelEn) {
s = {
edgeType: this.edgeType, edgeStroke: this.edgeStroke, edgeLineWidth: this.edgeLineWidth,
edgeEndArrow: this.edgeEndArrow, edgeShowLabel: this.edgeShowLabel,
edgeFontColor: this.edgeFontColor, edgeFontSize: this.edgeFontSize, edgeFontFamily: this.edgeFontFamily
};
} else {
s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey];
}
const strokeColor = hexToRgba(s?.edgeStroke || '#EF4444', 0.6);
return {
...edge, type: s.edgeType || 'line',
...edge, type: s?.edgeType || 'line',
style: {
stroke: strokeColor,
lineWidth: this.safeNum(s.edgeLineWidth, 2),
endArrow: s.edgeEndArrow,
labelText: s.edgeShowLabel ? (edge.data?.relationship?.properties?.label || '') : '',
labelFill: s.edgeFontColor || '#666',
labelFontSize: this.safeNum(s.edgeFontSize, 10),
labelFontFamily: s.edgeFontFamily || 'Microsoft YaHei',
labelBackground: true,
labelBackgroundFill: '#fff',
labelBackgroundOpacity: 0.7
stroke: strokeColor, lineWidth: this.safeNum(s?.edgeLineWidth, 2), endArrow: s?.edgeEndArrow,
labelText: s?.edgeShowLabel ? (edge.data?.relationship?.properties?.label || '') : '',
labelFill: s?.edgeFontColor || '#666', labelFontSize: this.safeNum(s?.edgeFontSize, 10),
labelFontFamily: s?.edgeFontFamily || 'Microsoft YaHei', labelBackground: true,
labelBackgroundFill: '#fff', labelBackgroundOpacity: 0.7
}
};
});
this._graph.setData({ nodes, edges });
this._graph.render();
},
@ -613,66 +741,37 @@ export default {
try {
const res = await getGroupedGraphStyleList();
if (res.code === 200) {
// 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({
const uniqueConfigs = (group.configs || []).map(conf => ({
...conf,
// styles
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles
}));
return { ...group, configs: uniqueConfigs };
});
} 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);
}
//
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}】已存在而被忽略`);
}
});
const activeGroup = this.styleGroups.find(g => g.is_active);
if (activeGroup) {
this.usingConfigIds = activeGroup.configs.map(c => c.id);
activeGroup.configs.forEach(conf => {
const labelEn = tagToLabelMap[conf.current_label];
if (labelEn) {
this.tagStyles[labelEn] = JSON.parse(JSON.stringify(conf.styles));
}
});
//
this.usingConfigIds = tempUsingIds;
const currentActiveConf = activeGroup.configs.find(c => c.current_label === this.activeTags);
if (currentActiveConf) {
this.isInitialEcho = true;
Object.assign(this, currentActiveConf.styles);
this.editingConfigId = currentActiveConf.id;
this.editingConfigLabel = currentActiveConf.current_label;
this.$nextTick(() => { this.isInitialEcho = false; });
}
}
// 3.
this.updateAllElements();
}
} catch (err) {
console.error("加载配置失败:", err);
ElMessage.error("获取方案列表失败");
}
},
async fetchGroupNames() {
@ -683,117 +782,68 @@ export default {
const idx = this.usingConfigIds.indexOf(item.id);
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) {
this.usingConfigIds = this.usingConfigIds.filter(id => id !== c.id);
if (this.editingConfigId === item.id) {
this.editingConfigId = null;
this.editingConfigLabel = '';
}
});
});
this.usingConfigIds.push(item.id);
} else {
this.handleEditConfig(item);
}
this.updateAllElements();
},
/**
* 新增功能将配置移动到另一个方案
*/
// validateGroupConstraint
validateGroupConstraint(groupName, labelName, excludeId = null) {
const group = this.styleGroups.find(g => g.group_name === groupName);
if (!group) return true;
// 1.
const isLabelExist = group.configs.some(c => c.current_label === labelName && c.id !== excludeId);
if (isLabelExist) {
// 使 alert confirm .catch()
ElMessageBox.alert(
`方案【${groupName}】中已存在【${labelName}】标签的配置,请先删除旧配置或选择其他方案。`,
'校验失败',
{ type: 'error' }
).catch(() => {}); //
return false;
}
// 2. 5
if (group.configs.length >= 5 && !group.configs.some(c => c.id === excludeId)) {
ElMessageBox.alert(
`方案【${groupName}】的配置已满(上限5个),无法添加。`,
'校验失败',
{ type: 'error' }
).catch(() => {});
return false;
}
return true;
},
async moveConfigToGroup(config, targetGroup) {
//
if (!this.validateGroupConstraint(targetGroup.group_name, config.current_label, config.id)) {
return;
}
try {
// 1.
const payload = {
id: config.id, // ID
id: config.id,
canvas_name: config.canvas_name,
group_name: targetGroup.group_name, //
group_name: targetGroup.group_name,
current_label: config.current_label,
styles: config.styles
styles: config.styles,
is_auto_save: false
};
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.
ElMessage.success(`已移动至【${targetGroup.group_name}`);
await this.fetchConfigs();
}
} catch (err) {
console.error("移动配置失败:", err);
ElMessage.error("移动操作失败,请重试");
}
},
/**
* 修改后的应用全案方法
* 逻辑保留当前已手动选中的配置新方案中冲突的配置不予应用
*/
async applyWholeGroup(group) {
try {
// 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 ElMessage.warning('该方案配置不完整,无法应用。目前缺少:'+missingTags.join('、'));
}
// 使
const currentlyUsingLabels = [];
this.styleGroups.forEach(g => {
g.configs.forEach(c => {
if (this.usingConfigIds.includes(c.id)) {
currentlyUsingLabels.push(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);
}
});
//
const filteredNewConfigIds = uniqueNewConfigs
.filter(newConf => !currentlyUsingLabels.includes(newConf.current_label))
.map(newConf => newConf.id);
if (filteredNewConfigIds.length === 0) {
return ElMessage.info("该方案中的标签配置已存在,无需重复应用");
}
// ID
this.usingConfigIds = [...this.usingConfigIds, ...filteredNewConfigIds];
//
const res = await applyGraphStyleGroup(group.id);
if (res.code === 200) {
await this.fetchConfigs();
ElMessage.success(`方案【${group.group_name}】已应用,已自动过滤重复标签`);
}
} catch (err) {
console.error(err);
ElMessage.error("应用全案失败");
ElMessage.error("操作失败");
}
},
@ -802,55 +852,83 @@ export default {
this.saveForm.canvas_name = `${this.activeTags}_${Date.now()}`;
this.saveDialogVisible = true;
},
async confirmSave() {
if (!this.saveForm.group_name || !this.saveForm.group_name.trim()) {
return ElMessage.warning("请选择或输入方案名称");
if (!this.saveForm.group_name?.trim() || !this.saveForm.canvas_name?.trim()) {
return ElMessage.warning("请完善名称");
}
if (!this.saveForm.canvas_name || !this.saveForm.canvas_name.trim()) {
return ElMessage.warning("请输入配置名称");
//
if (!this.validateGroupConstraint(this.saveForm.group_name.trim(), this.activeTags)) {
return;
}
const labelEn = tagToLabelMap[this.activeTags];
const payload = {
canvas_name: this.saveForm.canvas_name.trim(),
group_name: this.saveForm.group_name.trim(),
current_label: this.activeTags,
styles: {...this.tagStyles[tagToLabelMap[this.activeTags]]}
styles: { ...this.tagStyles[labelEn] },
is_auto_save: false
};
const res = await saveGraphStyle(payload);
if (res.code === 200) {
ElMessage.success("保存成功");
this.saveDialogVisible = false;
this.saveForm.canvas_name = '';
this.saveForm.group_name = '';
this.resetAllTagsToDefault();
this.fetchConfigs();
await 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();
// ===========================================================================
async applyWholeGroup(group) {
if (this.saveTimer) clearTimeout(this.saveTimer);
this.isInitialEcho = true;
this.editingConfigId = null;
this.editingConfigLabel = '';
try {
const REQUIRED_TAGS = ['疾病', '症状', '药品', '检查', '其他'];
const currentLabels = group.configs.map(conf => conf.current_label);
const hasTags = new Set(currentLabels);
const missingTags = REQUIRED_TAGS.filter(tag => !hasTags.has(tag));
if (missingTags.length > 0) {
this.isInitialEcho = false;
return ElMessageBox.alert(
`该方案配置不完整,无法应用。<br/>缺失:<b style="color: #f56c6c">${missingTags.join('、')}</b>`,
'提示', { dangerouslyUseHTMLString: true, type: 'warning' }
);
}
this.usingConfigIds = group.configs.map(c => c.id);
const res = await applyGraphStyleGroup(group.id);
if (res.code === 200) {
await this.fetchConfigs();
if (group.configs.length > 0) {
this.handleEditConfig(group.configs[0]);
}
ElMessage.success(`已应用方案【${group.group_name}`);
}
} catch (err) {
this.isInitialEcho = false;
ElMessage.error("切换失败");
}
},
resetStyle() {
const labelEn = tagToLabelMap[this.activeTags];
const initial = this.getInitialTagParams(labelEn);
this.tagStyles[labelEn] = initial;
this.isInitialEcho = true;
Object.assign(this, initial);
this.updateAllElements();
ElMessage.info(`已重置【${this.activeTags}】样式`);
this.$nextTick(() => {
this.isInitialEcho = false;
this.syncAndRefresh();
});
},
// --- ---
async deleteSingleConfig(id) {
if (this.usingConfigIds.includes(id)) {
return ElMessage.error("该配置正在应用中,请取消应用或切换方案后再删除");
}
if (this.usingConfigIds.includes(id)) return ElMessage.error("应用中无法删除");
try {
await ElMessageBox.confirm('确定删除此配置吗?', '提示');
await ElMessageBox.confirm('确定删除吗?', '提示');
const res = await deleteGraphStyle(id);
if (res.code === 200) {
ElMessage.success("删除成功");
@ -858,86 +936,33 @@ export default {
}
} catch (err) { }
},
// --- ---
async deleteGroup(groupId) {
const group = this.styleGroups.find(g => g.id === groupId);
if (!group) return;
// 1.
const isGroupUsing = group.configs.some(c => this.usingConfigIds.includes(c.id));
if (isGroupUsing || group.is_active) {
return ElMessage.error("该方案中包含正在应用的配置,无法删除");
}
// 2.
if (this.styleGroups.length <= 1) {
return ElMessage.error("系统至少需保留一个方案,无法全部删除");
}
if (!group || group.is_active) return ElMessage.error("应用中无法删除");
try {
await ElMessageBox.confirm('确定删除整个方案吗?', '提示');
await ElMessageBox.confirm('确定删除全案吗?', '提示');
const res = await deleteGraphStyleGroup(groupId);
if (res.code === 200) {
ElMessage.success("方案已删除");
ElMessage.success("已删除");
this.fetchConfigs();
}
} catch (err) { }
},
// --- ---
async handleUnifiedBatchDelete() {
// 1.
if (this.checkedConfigIds.length === 0 && this.checkedGroupIds.length === 0) {
return ElMessage.warning("请先勾选要删除的项目");
}
// 2.
const isAnyCheckedConfigUsing = this.checkedConfigIds.some(id => this.usingConfigIds.includes(id));
const isAnyCheckedGroupUsing = this.styleGroups
.filter(g => this.checkedGroupIds.includes(g.id))
.some(g => g.configs.some(c => this.usingConfigIds.includes(c.id)));
if (isAnyCheckedConfigUsing || isAnyCheckedGroupUsing) {
return ElMessage.warning('选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。');
}
// 3. ()
if (this.checkedGroupIds.length >= this.styleGroups.length && this.styleGroups.length > 0) {
return ElMessage.error("系统至少需要保留一个方案,请勿全部勾选删除");
}
if (this.checkedConfigIds.length === 0 && this.checkedGroupIds.length === 0) return;
try {
await ElMessageBox.confirm(
'确定执行批量删除吗?此操作不可恢复。',
'批量删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}
);
//
await ElMessageBox.confirm('确定批量删除吗?', '提示');
if (this.checkedGroupIds.length > 0) {
for (const gid of this.checkedGroupIds) {
await deleteGraphStyleGroup(gid);
for (const gid of this.checkedGroupIds) await deleteGraphStyleGroup(gid);
}
}
if (this.checkedConfigIds.length > 0) {
await batchDeleteGraphStyle({ ids: this.checkedConfigIds });
}
ElMessage.success("批量删除成功");
ElMessage.success("成功");
this.clearSelection();
this.fetchConfigs();
this.updateAllElements();
} catch (e) {
console.log("用户取消或删除失败", e);
}
} catch (e) { }
},
clearSelection() {
this.checkedConfigIds = [];
this.checkedGroupIds = [];

Loading…
Cancel
Save