Browse Source

工具页面功能补充

mh
hanyuqing 3 months ago
parent
commit
4013f919cd
  1. 24
      controller/GraphStyleController.py
  2. 38
      service/GraphStyleService.py
  3. 210
      vue/src/system/GraphStyle.vue

24
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)}"})

38
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

210
vue/src/system/GraphStyle.vue

@ -221,33 +221,51 @@
</div>
</template>
<div
<el-dropdown
trigger="contextmenu"
placement="bottom-start"
v-for="item in group.configs"
:key="item.id"
class="config-card"
:class="{
'card-using': usingConfigIds.includes(item.id),
'card-checked': checkedConfigIds.includes(item.id)
}"
@click="toggleApplyConfig(item)"
style="width: 100%; display: block;"
>
<div class="card-left">
<div class="checkbox-wrapper">
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop
class="config-checkbox"/>
</div>
<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>
<div
class="config-card"
:class="{
'card-using': usingConfigIds.includes(item.id),
'card-checked': checkedConfigIds.includes(item.id)
}"
@click="toggleApplyConfig(item)"
>
<div class="card-left">
<div class="checkbox-wrapper">
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop class="config-checkbox"/>
</div>
<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>
</div>
<span class="card-tag">标签: {{ item.current_label }}</span>
</div>
<span class="card-tag">标签: {{ item.current_label }}</span>
</div>
<div class="card-right">
<i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i>
</div>
</div>
<div class="card-right">
<i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i>
</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item disabled style="font-weight: bold; color: #333;">移动至方案</el-dropdown-item>
<el-dropdown-item
v-for="targetGroup in styleGroups.filter(g => g.id !== group.id)"
:key="targetGroup.id"
@click="moveConfigToGroup(item, targetGroup)"
>
{{ targetGroup.group_name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-collapse-item>
</el-collapse>
</div>
@ -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 个核心标签。` +
`<br/>目前缺失:<b style="color: #f56c6c">${missingTags.join('、')}</b>`,
'校验未通过',
{
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);

Loading…
Cancel
Save