Compare commits

...

4 Commits

  1. 24
      controller/GraphStyleController.py
  2. 28
      service/GraphStyleService.py
  3. 78
      vue/src/system/GraphDemo.vue
  4. 244
      vue/src/system/GraphQA.vue
  5. 192
      vue/src/system/GraphStyle.vue
  6. 18
      vue/src/system/KGData.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)}"})

28
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. 【核心修复】:判断是更新(移动)还是新建
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 = """ sql = """
INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id) INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id)
VALUES (%s, %s, %s, %s) VALUES (%s, %s, %s, %s)
""" """
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id)) 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

78
vue/src/system/GraphDemo.vue

@ -855,37 +855,63 @@ export default {
formatData(data){ formatData(data){
this._graph.stopLayout(); this._graph.stopLayout();
this.clearGraphState(); this.clearGraphState();
const updatedEdges = data.edges.map(edge => ({
...edge,
type: this.edgeType,
style: {
endArrow: this.edgeEndArrow,
stroke: this.edgeStroke,
lineWidth: this.edgeLineWidth,
label: this.edgeShowLabel,
labelFontSize: this.edgeFontSize,
labelFontFamily: this.edgeFontFamily,
labelFill: this.edgeFontColor,
},
})) // === 1. nodeId label ===
const updatedNodes = data.nodes.map(node => ({ const nodeIdToEnLabel = {};
data.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.label; // e.g. "Disease"
});
// === 2. label ===
const updatedNodes = data.nodes.map(node => {
const enLabel = node.data.label;
const zhLabel = this.enToZhLabelMap[enLabel] || '其他'; // 退
const styleConf = this.parsedStyles[zhLabel] || {};
return {
...node, ...node,
type: this.nodeShape, type: styleConf.nodeShape || this.nodeShape,
style:{ style: {
size: this.nodeSize, size: styleConf.nodeSize || this.nodeSize,
lineWidth: this.nodeLineWidth, fill: styleConf.nodeFill || this.nodeFill,
label: this.nodeShowLabel, stroke: styleConf.nodeStroke || this.nodeStroke,
labelFontSize: this.nodeFontSize, lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
labelFontFamily: this.nodeFontFamily, label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFill: this.nodeFontColor, labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
opacity: 1, labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
} }
})) };
const updatedData = { });
// === 3. source label ===
const updatedEdges = data.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const sourceZhLabel = this.enToZhLabelMap[sourceEnLabel] || '其他';
const styleConf = this.parsedStyles[sourceZhLabel] || {};
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType ||this.edgeType,
style: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: styleConf.edgeFontColor || this.edgeFontColor
}
};
});
// === 4. ===
let updatedData = {
nodes: updatedNodes, nodes: updatedNodes,
edges: updatedEdges edges: updatedEdges
} };
this.buildNodeLabelMap(updatedNodes); this.buildNodeLabelMap(updatedNodes);
this.updateGraph(updatedData) this.updateGraph(updatedData)
this.buildCategoryIndex(); this.buildCategoryIndex();

244
vue/src/system/GraphQA.vue

@ -76,6 +76,7 @@ import Menu from "@/components/Menu.vue";
import {qaAnalyze} from "@/api/qa"; import {qaAnalyze} from "@/api/qa";
import {Graph} from "@antv/g6"; import {Graph} from "@antv/g6";
import {getGraph} from "@/api/graph"; import {getGraph} from "@/api/graph";
import {getGraphStyleActive} from "@/api/style";
export default { export default {
@ -109,7 +110,16 @@ export default {
queryRecord:"", queryRecord:"",
isSending:false isSending:false,
configs:[],
parsedStyles:{},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
Check: '检查',
Symptom: '症状',
Other: '其他'
}
}; };
}, },
@ -120,7 +130,8 @@ export default {
}, },
// ======================================================================= // =======================================================================
mounted() { async mounted() {
await this.getDefault()
// =============== 👇 localStorage =============== // =============== 👇 localStorage ===============
this.restoreDataFromLocalStorage(); this.restoreDataFromLocalStorage();
// ======================================================================= // =======================================================================
@ -145,6 +156,36 @@ export default {
// ======================================================================= // =======================================================================
}, },
methods: { methods: {
safeParseStyles(stylesStr) {
try {
return JSON.parse(stylesStr || '{}');
} catch (e) {
console.warn('Failed to parse styles:', stylesStr);
return {};
}
},
async getDefault(){
const response = await getGraphStyleActive();
const data = response.data;
if (!Array.isArray(data) || data.length === 0) {
this.configs = [];
this.parsedStyles = {};
return;
}
// is_active=1
const activeGroup = data[0];
this.configs = Array.isArray(activeGroup.configs) ? activeGroup.configs : [];
// label -> style
const styleMap = {};
this.configs.forEach(config => {
const label = config.current_label;
styleMap[label] = this.safeParseStyles(config.styles);
});
this.parsedStyles = styleMap;
console.log(this.parsedStyles)
},
buildNodeLabelMap(nodes) { buildNodeLabelMap(nodes) {
this._nodeLabelMap = new Map(); this._nodeLabelMap = new Map();
nodes.forEach(node => { nodes.forEach(node => {
@ -229,37 +270,58 @@ export default {
formatData(data){ formatData(data){
// this._graph.stopLayout(); // this._graph.stopLayout();
// this.clearGraphState(); // this.clearGraphState();
const updatedEdges = data.edges.map(edge => ({ // === 1. nodeId label ===
...edge, const nodeIdToEnLabel = {};
type: this.edgeType, data.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease"
});
// === 2. label ===
const updatedNodes = data.nodes.map(node => {
const enLabel = node.data.type;
const styleConf = this.parsedStyles[enLabel] || {};
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
style: { style: {
endArrow: this.edgeEndArrow, size: styleConf.nodeSize || this.nodeSize,
stroke: this.edgeStroke, fill: styleConf.nodeFill || this.nodeFill,
lineWidth: this.edgeLineWidth, stroke: styleConf.nodeStroke || this.nodeStroke,
label: this.edgeShowLabel, lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
labelFontSize: this.edgeFontSize, label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontFamily: this.edgeFontFamily, labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFill: this.edgeFontColor, labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
}
};
});
}, // === 3. source label ===
})) const updatedEdges = data.edges.map(edge => {
const updatedNodes = data.nodes.map(node => ({ console.log(edge)
...node, const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
type: this.nodeShape, const styleConf = this.parsedStyles[sourceEnLabel] || {};
style:{
size: this.nodeSize, return {
lineWidth: this.nodeLineWidth, ...edge,
label: this.nodeShowLabel, id: edge.data?.relationship?.id || edge.id,
labelFontSize: this.nodeFontSize, type: styleConf.edgeType ||this.edgeType,
labelFontFamily: this.nodeFontFamily, style: {
labelFill: this.nodeFontColor, endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
opacity: 1, stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: styleConf.edgeFontColor || this.edgeFontColor
} }
})) };
const updatedData = { });
// === 4. ===
let updatedData = {
nodes: updatedNodes, nodes: updatedNodes,
edges: updatedEdges edges: updatedEdges
} };
this.updateGraph(updatedData) this.updateGraph(updatedData)
}, },
@ -274,37 +336,60 @@ export default {
this._graph.destroy() this._graph.destroy()
this._graph = null; this._graph = null;
} }
const updatedEdges = data.edges.map(edge => ({ console.log(data)
...edge, // === 1. nodeId label ===
type: this.edgeType, const nodeIdToEnLabel = {};
data.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease"
});
console.log(nodeIdToEnLabel)
// === 2. label ===
const updatedNodes = data.nodes.map(node => {
const enLabel = node.data.type;
const styleConf = this.parsedStyles[enLabel] || {};
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
style: { style: {
endArrow: this.edgeEndArrow, size: styleConf.nodeSize || this.nodeSize,
stroke: this.edgeStroke, fill: styleConf.nodeFill || this.nodeFill,
lineWidth: this.edgeLineWidth, stroke: styleConf.nodeStroke || this.nodeStroke,
label: this.edgeShowLabel, lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
labelFontSize: this.edgeFontSize, label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontFamily: this.edgeFontFamily, labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFill: this.edgeFontColor, labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
}
};
});
}, // === 3. source label ===
})) const updatedEdges = data.edges.map(edge => {
const updatedNodes = data.nodes.map(node => ({ console.log(edge)
...node, const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
type: this.nodeShape, const styleConf = this.parsedStyles[sourceEnLabel] || {};
style:{
size: this.nodeSize, return {
lineWidth: this.nodeLineWidth, ...edge,
label: this.nodeShowLabel, id: edge.data?.relationship?.id || edge.id,
labelFontSize: this.nodeFontSize, type: styleConf.edgeType ||this.edgeType,
labelFontFamily: this.nodeFontFamily, style: {
labelFill: this.nodeFontColor, endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
opacity: 1, stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: styleConf.edgeFontColor || this.edgeFontColor
} }
})) };
const updatedData = { });
// === 4. ===
let updatedData = {
nodes: updatedNodes, nodes: updatedNodes,
edges: updatedEdges edges: updatedEdges
} };
this.buildNodeLabelMap(updatedNodes); this.buildNodeLabelMap(updatedNodes);
const container = this.$refs.graphContainer; const container = this.$refs.graphContainer;
console.log(container) console.log(container)
@ -342,23 +427,23 @@ export default {
node: { node: {
style: { style: {
fill: (d) => { // fill: (d) => {
//
const label = d.data?.type; // const label = d.data?.type;
if (label === '疾病') return '#EF4444'; // // if (label === '') return '#EF4444'; //
if (label === '药品'||label === '药物') return '#91cc75'; // 绿 // if (label === ''||label === '') return '#91cc75'; // 绿
if (label === '症状') return '#fac858'; // // if (label === '') return '#fac858'; //
if (label === '检查') return '#336eee'; // // if (label === '') return '#336eee'; //
return '#59d1d4'; // // return '#59d1d4'; //
}, // },
stroke: (d) => { // stroke: (d) => {
const label = d.data?.type; // const label = d.data?.type;
if (label === '疾病') return '#B91C1C'; // if (label === '') return '#B91C1C';
if (label === '药品'||label === '药物') return '#047857'; // if (label === ''||label === '') return '#047857';
if (label === '检查') return '#1D4ED8'; // // if (label === '') return '#1D4ED8'; //
if (label === '症状') return '#B45309'; // if (label === '') return '#B45309';
return '#40999b'; // return '#40999b';
}, // },
labelText: (d) => d.label, labelText: (d) => d.label,
labelPlacement: 'center', labelPlacement: 'center',
labelWordWrap: true, labelWordWrap: true,
@ -389,16 +474,15 @@ export default {
edge: { edge: {
style: { style: {
labelText: (d) => { labelText: (d) => {
console.log(d)
return d.data.label}, return d.data.label},
stroke: (d) => { // stroke: (d) => {
const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID // const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
if (targetLabel === '疾病') return 'rgba(239,68,68,0.5)'; // if (targetLabel === '') return 'rgba(239,68,68,0.5)';
if (targetLabel === '药品'||targetLabel === '药物') return 'rgba(145,204,117,0.5)'; // if (targetLabel === ''||targetLabel === '') return 'rgba(145,204,117,0.5)';
if (targetLabel === '症状') return 'rgba(250,200,88,0.5)'; // if (targetLabel === '') return 'rgba(250,200,88,0.5)';
if (targetLabel === '检查') return 'rgba(51,110,238,0.5)'; // // if (targetLabel === '') return 'rgba(51,110,238,0.5)'; //
return 'rgba(89,209,212,0.5)'; // default // return 'rgba(89,209,212,0.5)'; // default
}, // },
// labelFill: (d) => { // labelFill: (d) => {
// // target label // // target label
// const targetLabel = this._nodeLabelMap.get(d.target); // d.target ID // const targetLabel = this._nodeLabelMap.get(d.target); // d.target ID

192
vue/src/system/GraphStyle.vue

@ -52,7 +52,8 @@
<div class="color-picker-item"> <div class="color-picker-item">
<label>字体颜色</label> <label>字体颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="nodeFontColor" type="color" class="square-picker"/> <el-color-picker v-model="nodeFontColor" show-alpha class="square-picker"/>
<!-- <input v-model="nodeFontColor" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
@ -80,14 +81,16 @@
<div class="color-picker-item"> <div class="color-picker-item">
<label>填充颜色</label> <label>填充颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="nodeFill" type="color" class="square-picker"/> <el-color-picker v-model="nodeFill" show-alpha class="square-picker"/>
<!-- <input v-model="nodeFill" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
<div class="color-picker-item"> <div class="color-picker-item">
<label>边框颜色</label> <label>边框颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="nodeStroke" type="color" class="square-picker"/> <el-color-picker v-model="nodeStroke" show-alpha class="square-picker"/>
<!-- <input v-model="nodeStroke" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
@ -136,7 +139,8 @@
<div class="color-picker-item"> <div class="color-picker-item">
<label>字体颜色</label> <label>字体颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="edgeFontColor" type="color" class="square-picker"/> <el-color-picker v-model="edgeFontColor" class="square-picker" show-alpha />
<!-- <input v-model="edgeFontColor" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
@ -164,7 +168,8 @@
<div class="color-picker-item"> <div class="color-picker-item">
<label>线条颜色</label> <label>线条颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="edgeStroke" type="color" class="square-picker"/> <el-color-picker v-model="edgeStroke" class="square-picker" show-alpha />
<!-- <input v-model="edgeStroke" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
</div> </div>
@ -221,9 +226,14 @@
</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"
style="width: 100%; display: block;"
>
<div
class="config-card" class="config-card"
:class="{ :class="{
'card-using': usingConfigIds.includes(item.id), 'card-using': usingConfigIds.includes(item.id),
@ -233,8 +243,7 @@
> >
<div class="card-left"> <div class="card-left">
<div class="checkbox-wrapper"> <div class="checkbox-wrapper">
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop <input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop class="config-checkbox"/>
class="config-checkbox"/>
</div> </div>
<div class="card-info"> <div class="card-info">
<div class="card-title-row"> <div class="card-title-row">
@ -248,6 +257,20 @@
<i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i> <i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i>
</div> </div>
</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-item>
</el-collapse> </el-collapse>
</div> </div>
@ -362,7 +385,7 @@ export default {
edgeFontFamily: 'Microsoft YaHei, sans-serif', edgeFontFamily: 'Microsoft YaHei, sans-serif',
edgeFontSize: 10, edgeFontSize: 10,
edgeFontColor: '#666666', edgeFontColor: '#666666',
edgeType: 'line', edgeType: 'quadratic',
edgeLineWidth: 2, edgeLineWidth: 2,
edgeStroke: '#EF4444', edgeStroke: '#EF4444',
defaultData: { defaultData: {
@ -489,6 +512,7 @@ export default {
}, },
syncAndRefresh() { syncAndRefresh() {
const label = tagToLabelMap[this.activeTags]; const label = tagToLabelMap[this.activeTags];
console.log(this.nodeFill)
if (label) { if (label) {
this.tagStyles[label] = { this.tagStyles[label] = {
nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize, nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize,
@ -589,31 +613,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 => {
const idSet = new Set();
const uniqueConfigs = [];
(group.configs || []).forEach(conf => {
if (!idSet.has(conf.id)) {
idSet.add(conf.id);
uniqueConfigs.push({
...conf, ...conf,
// styles
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : 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 +696,100 @@ 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 ElMessage.warning('该方案配置不完整,无法应用。目前缺少:'+missingTags.join('、'));
}
// 使
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);
@ -780,11 +899,7 @@ export default {
.some(g => g.configs.some(c => this.usingConfigIds.includes(c.id))); .some(g => g.configs.some(c => this.usingConfigIds.includes(c.id)));
if (isAnyCheckedConfigUsing || isAnyCheckedGroupUsing) { if (isAnyCheckedConfigUsing || isAnyCheckedGroupUsing) {
return ElMessageBox.alert( return ElMessage.warning('选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。');
'选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。',
'无法执行删除',
{ type: 'error', confirmButtonText: '我知道了' }
);
} }
// 3. () // 3. ()
@ -1022,9 +1137,11 @@ export default {
.form-group select, .form-group input[type="number"] { .form-group select, .form-group input[type="number"] {
flex: 1; flex: 1;
padding: 5px; padding: 5px;
border: 1px solid #e2e8f0; border: none;
border-radius: 4px; border-radius: 4px;
width: 100px; width: 100px;
box-shadow: 0 0 0 2px #EBF0FF;
outline: none;
} }
.slider-wrapper { .slider-wrapper {
@ -1056,14 +1173,11 @@ export default {
.val-text-black { .val-text-black {
color: #000; color: #000;
font-weight: bold;
font-size: 13px; font-size: 13px;
min-width: 35px; min-width: 35px;
} }
.color-picker-border { .color-picker-border {
padding: 3px;
border: 1px solid #e2e8f0;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
} }
@ -1367,7 +1481,6 @@ export default {
<style> <style>
.el-message-box__header { .el-message-box__header {
text-align: left !important; text-align: left !important;
padding-top: 15px !important;
} }
.el-message-box__title { .el-message-box__title {
@ -1380,4 +1493,5 @@ export default {
background-color: #1559f3 !important; background-color: #1559f3 !important;
border-color: #1559f3 !important; border-color: #1559f3 !important;
} }
</style> </style>

18
vue/src/system/KGData.vue

@ -656,8 +656,8 @@ onMounted(() => {
.folder-tab-item { padding: 8px 20px; font-size: 12px; color: #86909c; cursor: pointer; background-color: #ecf2ff; border: 1px solid #dcdfe6; border-bottom: none; border-radius: 8px 8px 0 0; } .folder-tab-item { padding: 8px 20px; font-size: 12px; color: #86909c; cursor: pointer; background-color: #ecf2ff; border: 1px solid #dcdfe6; border-bottom: none; border-radius: 8px 8px 0 0; }
.folder-tab-item.active { background-color: #f1f6ff !important; color: #2869ff; font-weight: bold; border: 2px solid #6896ff; border-bottom: 2px solid #ffffff; margin-bottom: -1px; z-index: 3; } .folder-tab-item.active { background-color: #f1f6ff !important; color: #2869ff; font-weight: bold; border: 2px solid #6896ff; border-bottom: 2px solid #ffffff; margin-bottom: -1px; z-index: 3; }
.data-card-container { background: #ffffff; border-radius: 30px; padding: 20px 20px; box-shadow: 2px -1px 14px 4px #E1EAFF; border: 1px solid #eff4ff; position: relative; z-index: 4; } .data-card-container { background: #ffffff; border-radius: 30px; padding: 20px 20px; box-shadow: 2px -1px 14px 4px #E1EAFF; border: 1px solid #eff4ff; position: relative; z-index: 4; }
.filter-bar { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 20px; } .filter-bar { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 20px;gap: 20px }
.filter-inputs { display: flex; gap: 35px; flex-wrap: nowrap;margin-right: 20px; } .filter-inputs { display: flex; gap: 35px; flex-wrap: nowrap;}
.input-group-inline { display: flex; align-items: center; gap: 12px; flex-shrink: 0; white-space: nowrap; } .input-group-inline { display: flex; align-items: center; gap: 12px; flex-shrink: 0; white-space: nowrap; }
.filter-label-text { font-size: 14px; color: #165dff; font-weight: 600; flex-shrink: 0; } .filter-label-text { font-size: 14px; color: #165dff; font-weight: 600; flex-shrink: 0; }
.search-input,.input-group-inline :deep(.el-input) { width: 200px !important;box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px; } .search-input,.input-group-inline :deep(.el-input) { width: 200px !important;box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px; }
@ -817,18 +817,7 @@ onMounted(() => {
min-height: 300px; min-height: 300px;
} }
.filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.filter-inputs {
display: flex;
gap: 35px;
flex-wrap: nowrap;
}
.input-group-inline { .input-group-inline {
display: flex; display: flex;
@ -845,9 +834,6 @@ onMounted(() => {
flex-shrink: 0; flex-shrink: 0;
} }
.search-input, .search-select {
width: 160px !important;
}
.btn-search-ref { .btn-search-ref {
background: #165dff !important; background: #165dff !important;

Loading…
Cancel
Save