Browse Source

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

hanyuqing
hanyuqing 3 months ago
parent
commit
f030e0501c
  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") @app.post("/api/graph/style/save")
async def save_style_config(request): async def save_style_config(request):
"""保存配置接口 - 升级版:支持分组名""" """保存配置接口 - 修复版:支持移动与更新逻辑"""
try: try:
body = request.json() body = request.json()
# 1. 核心修改:接收前端传来的 id
# 当执行“移动”操作时,前端会传 config.id;当执行“保存当前配置”时,id 为空
config_id = body.get('id')
canvas_name = body.get('canvas_name') canvas_name = body.get('canvas_name')
current_label = body.get('current_label') current_label = body.get('current_label')
styles = body.get('styles') styles = body.get('styles')
# 接收分组名称(字符串)
group_name = body.get('group_name') group_name = body.get('group_name')
if not all([canvas_name, current_label, styles]): if not all([canvas_name, current_label, styles]):
return create_response(200, {"code": 400, "msg": "参数不完整"}) return create_response(200, {"code": 400, "msg": "参数不完整"})
# 调用 Service,逻辑内部处理:组名存在则用,不存在则建 # 2. 核心修改:将 config_id 传给 Service 层
success = GraphStyleService.save_config(canvas_name, current_label, styles, group_name) # 这样 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: if success:
return create_response(200, {"code": 200, "msg": "保存成功"}) return create_response(200, {"code": 200, "msg": "操作成功"})
else: else:
return create_response(200, {"code": 500, "msg": "保存失败"}) return create_response(200, {"code": 500, "msg": "操作失败"})
except Exception as e: except Exception as e:
return create_response(200, {"code": 500, "msg": f"系统异常: {str(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: class GraphStyleService:
@staticmethod @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() == "": if not group_name or group_name.strip() == "":
group_name = "默认方案" group_name = "默认方案"
# 检查组名是否已存在 # 检查/创建 目标方案
check_group_sql = "SELECT id FROM graph_style_groups WHERE group_name = %s LIMIT 1" 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,)) existing_group = mysql_client.execute_query(check_group_sql, (group_name,))
if existing_group: if existing_group:
target_group_id = existing_group[0]['id'] target_group_id = existing_group[0]['id']
else: else:
# 如果不存在,新建一个组(is_active 和 is_default 默认为 false)
create_group_sql = "INSERT INTO graph_style_groups (group_name, is_active, is_default) VALUES (%s, %s, %s)" 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)) 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']
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
# 2. 转换样式 JSON # 2. 转换样式 JSON
config_json = json.dumps(styles_dict, ensure_ascii=False) config_json = json.dumps(styles_dict, ensure_ascii=False)
# 3. 插入配置表 # 3. 【核心修复】:判断是更新(移动)还是新建
sql = """ if config_id:
INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id) # 如果带了 ID,说明是“移动”或“修改”,执行 UPDATE
VALUES (%s, %s, %s, %s) # 这样 group_id 会被更新,且不会产生新记录,配置就从原方案“消失”并出现在新方案了
""" sql = """
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id)) 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 return affected_rows > 0
@staticmethod @staticmethod

210
vue/src/system/GraphStyle.vue

@ -221,33 +221,51 @@
</div> </div>
</template> </template>
<div <el-dropdown
trigger="contextmenu"
placement="bottom-start"
v-for="item in group.configs" v-for="item in group.configs"
:key="item.id" :key="item.id"
class="config-card" style="width: 100%; display: block;"
:class="{
'card-using': usingConfigIds.includes(item.id),
'card-checked': checkedConfigIds.includes(item.id)
}"
@click="toggleApplyConfig(item)"
> >
<div class="card-left"> <div
<div class="checkbox-wrapper"> class="config-card"
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop :class="{
class="config-checkbox"/> 'card-using': usingConfigIds.includes(item.id),
</div> 'card-checked': checkedConfigIds.includes(item.id)
<div class="card-info"> }"
<div class="card-title-row"> @click="toggleApplyConfig(item)"
<span class="card-name">{{ item.canvas_name }}</span> >
<span v-if="usingConfigIds.includes(item.id)" class="status-badge">已应用</span> <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> </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> </div>
<div class="card-right">
<i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i> <template #dropdown>
</div> <el-dropdown-menu>
</div> <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-item>
</el-collapse> </el-collapse>
</div> </div>
@ -589,31 +607,66 @@ export default {
try { try {
const res = await getGroupedGraphStyleList(); const res = await getGroupedGraphStyleList();
if (res.code === 200) { if (res.code === 200) {
this.styleGroups = res.data.map(group => ({ // 1.
...group, // ID
configs: group.configs.map(conf => ({ this.styleGroups = res.data.map(group => {
...conf, const idSet = new Set();
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles 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) { if (this.usingConfigIds.length === 0) {
const tempUsingIds = [];
const seenLabels = new Set(); //
this.styleGroups.forEach(group => { this.styleGroups.forEach(group => {
// active
if (group.is_active) { if (group.is_active) {
//
if (!this.activeCollapseNames.includes(group.id)) { if (!this.activeCollapseNames.includes(group.id)) {
this.activeCollapseNames.push(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(); this.updateAllElements();
} }
} catch (err) { } catch (err) {
console.error("加载配置失败:", err); console.error("加载配置失败:", err);
ElMessage.error("获取方案列表失败");
} }
}, },
async fetchGroupNames() { async fetchGroupNames() {
@ -637,40 +690,109 @@ export default {
} }
this.updateAllElements(); 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) { async applyWholeGroup(group) {
try { try {
// 1. 使 // 1. 5
const currentlyUsingConfigs = []; 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 => { this.styleGroups.forEach(g => {
g.configs.forEach(c => { g.configs.forEach(c => {
if (this.usingConfigIds.includes(c.id)) { 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 const filteredNewConfigIds = uniqueNewConfigs
.filter(newConf => !currentlySelectedLabels.includes(newConf.current_label)) .filter(newConf => !currentlyUsingLabels.includes(newConf.current_label))
.map(newConf => newConf.id); .map(newConf => newConf.id);
// 4. ID if (filteredNewConfigIds.length === 0) {
return ElMessage.info("该方案中的标签配置已存在,无需重复应用");
}
// ID
this.usingConfigIds = [...this.usingConfigIds, ...filteredNewConfigIds]; this.usingConfigIds = [...this.usingConfigIds, ...filteredNewConfigIds];
// 5. //
const res = await applyGraphStyleGroup(group.id); const res = await applyGraphStyleGroup(group.id);
if (res.code === 200) { if (res.code === 200) {
// UI
await this.fetchConfigs(); await this.fetchConfigs();
ElMessage.success(`方案【${group.group_name}】已应用,已保留您手动选择的标签`); ElMessage.success(`方案【${group.group_name}】已应用,已自动过滤重复标签`);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

Loading…
Cancel
Save