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

28
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. 插入配置表
# 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

78
vue/src/system/GraphDemo.vue

@ -855,37 +855,63 @@ export default {
formatData(data){
this._graph.stopLayout();
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,
},
}))
const updatedNodes = data.nodes.map(node => ({
// === 1. nodeId label ===
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,
type: this.nodeShape,
style:{
size: this.nodeSize,
lineWidth: this.nodeLineWidth,
label: this.nodeShowLabel,
labelFontSize: this.nodeFontSize,
labelFontFamily: this.nodeFontFamily,
labelFill: this.nodeFontColor,
opacity: 1,
type: styleConf.nodeShape || this.nodeShape,
style: {
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
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,
edges: updatedEdges
}
};
this.buildNodeLabelMap(updatedNodes);
this.updateGraph(updatedData)
this.buildCategoryIndex();

244
vue/src/system/GraphQA.vue

@ -76,6 +76,7 @@ import Menu from "@/components/Menu.vue";
import {qaAnalyze} from "@/api/qa";
import {Graph} from "@antv/g6";
import {getGraph} from "@/api/graph";
import {getGraphStyleActive} from "@/api/style";
export default {
@ -109,7 +110,16 @@ export default {
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 ===============
this.restoreDataFromLocalStorage();
// =======================================================================
@ -145,6 +156,36 @@ export default {
// =======================================================================
},
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) {
this._nodeLabelMap = new Map();
nodes.forEach(node => {
@ -229,37 +270,58 @@ export default {
formatData(data){
// this._graph.stopLayout();
// this.clearGraphState();
const updatedEdges = data.edges.map(edge => ({
...edge,
type: this.edgeType,
// === 1. nodeId label ===
const nodeIdToEnLabel = {};
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: {
endArrow: this.edgeEndArrow,
stroke: this.edgeStroke,
lineWidth: this.edgeLineWidth,
label: this.edgeShowLabel,
labelFontSize: this.edgeFontSize,
labelFontFamily: this.edgeFontFamily,
labelFill: this.edgeFontColor,
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
}
};
});
},
}))
const updatedNodes = data.nodes.map(node => ({
...node,
type: this.nodeShape,
style:{
size: this.nodeSize,
lineWidth: this.nodeLineWidth,
label: this.nodeShowLabel,
labelFontSize: this.nodeFontSize,
labelFontFamily: this.nodeFontFamily,
labelFill: this.nodeFontColor,
opacity: 1,
// === 3. source label ===
const updatedEdges = data.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const styleConf = this.parsedStyles[sourceEnLabel] || {};
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
}
}))
const updatedData = {
};
});
// === 4. ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
}
};
this.updateGraph(updatedData)
},
@ -274,37 +336,60 @@ export default {
this._graph.destroy()
this._graph = null;
}
const updatedEdges = data.edges.map(edge => ({
...edge,
type: this.edgeType,
console.log(data)
// === 1. nodeId label ===
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: {
endArrow: this.edgeEndArrow,
stroke: this.edgeStroke,
lineWidth: this.edgeLineWidth,
label: this.edgeShowLabel,
labelFontSize: this.edgeFontSize,
labelFontFamily: this.edgeFontFamily,
labelFill: this.edgeFontColor,
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
}
};
});
},
}))
const updatedNodes = data.nodes.map(node => ({
...node,
type: this.nodeShape,
style:{
size: this.nodeSize,
lineWidth: this.nodeLineWidth,
label: this.nodeShowLabel,
labelFontSize: this.nodeFontSize,
labelFontFamily: this.nodeFontFamily,
labelFill: this.nodeFontColor,
opacity: 1,
// === 3. source label ===
const updatedEdges = data.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const styleConf = this.parsedStyles[sourceEnLabel] || {};
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
}
}))
const updatedData = {
};
});
// === 4. ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
}
};
this.buildNodeLabelMap(updatedNodes);
const container = this.$refs.graphContainer;
console.log(container)
@ -342,23 +427,23 @@ export default {
node: {
style: {
fill: (d) => {
const label = d.data?.type;
if (label === '疾病') return '#EF4444'; //
if (label === '药品'||label === '药物') return '#91cc75'; // 绿
if (label === '症状') return '#fac858'; //
if (label === '检查') return '#336eee'; //
return '#59d1d4'; //
},
stroke: (d) => {
const label = d.data?.type;
if (label === '疾病') return '#B91C1C';
if (label === '药品'||label === '药物') return '#047857';
if (label === '检查') return '#1D4ED8'; //
if (label === '症状') return '#B45309';
return '#40999b';
},
// fill: (d) => {
//
// const label = d.data?.type;
// if (label === '') return '#EF4444'; //
// if (label === ''||label === '') return '#91cc75'; // 绿
// if (label === '') return '#fac858'; //
// if (label === '') return '#336eee'; //
// return '#59d1d4'; //
// },
// stroke: (d) => {
// const label = d.data?.type;
// if (label === '') return '#B91C1C';
// if (label === ''||label === '') return '#047857';
// if (label === '') return '#1D4ED8'; //
// if (label === '') return '#B45309';
// return '#40999b';
// },
labelText: (d) => d.label,
labelPlacement: 'center',
labelWordWrap: true,
@ -389,16 +474,15 @@ export default {
edge: {
style: {
labelText: (d) => {
console.log(d)
return d.data.label},
stroke: (d) => {
const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
if (targetLabel === '疾病') return 'rgba(239,68,68,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(51,110,238,0.5)'; //
return 'rgba(89,209,212,0.5)'; // default
},
// stroke: (d) => {
// const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
// if (targetLabel === '') return 'rgba(239,68,68,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(51,110,238,0.5)'; //
// return 'rgba(89,209,212,0.5)'; // default
// },
// labelFill: (d) => {
// // target label
// 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">
<label>字体颜色</label>
<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>
@ -80,14 +81,16 @@
<div class="color-picker-item">
<label>填充颜色</label>
<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 class="color-picker-item">
<label>边框颜色</label>
<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>
@ -136,7 +139,8 @@
<div class="color-picker-item">
<label>字体颜色</label>
<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>
@ -164,7 +168,8 @@
<div class="color-picker-item">
<label>线条颜色</label>
<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>
@ -221,9 +226,14 @@
</div>
</template>
<div
<el-dropdown
trigger="contextmenu"
placement="bottom-start"
v-for="item in group.configs"
:key="item.id"
style="width: 100%; display: block;"
>
<div
class="config-card"
:class="{
'card-using': usingConfigIds.includes(item.id),
@ -233,8 +243,7 @@
>
<div class="card-left">
<div class="checkbox-wrapper">
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop
class="config-checkbox"/>
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop class="config-checkbox"/>
</div>
<div class="card-info">
<div class="card-title-row">
@ -248,6 +257,20 @@
<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>
@ -362,7 +385,7 @@ export default {
edgeFontFamily: 'Microsoft YaHei, sans-serif',
edgeFontSize: 10,
edgeFontColor: '#666666',
edgeType: 'line',
edgeType: 'quadratic',
edgeLineWidth: 2,
edgeStroke: '#EF4444',
defaultData: {
@ -489,6 +512,7 @@ export default {
},
syncAndRefresh() {
const label = tagToLabelMap[this.activeTags];
console.log(this.nodeFill)
if (label) {
this.tagStyles[label] = {
nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize,
@ -589,31 +613,66 @@ export default {
try {
const res = await getGroupedGraphStyleList();
if (res.code === 200) {
this.styleGroups = res.data.map(group => ({
...group,
configs: group.configs.map(conf => ({
// 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 +696,100 @@ 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 ElMessage.warning('该方案配置不完整,无法应用。目前缺少:'+missingTags.join('、'));
}
// 使
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);
@ -780,11 +899,7 @@ export default {
.some(g => g.configs.some(c => this.usingConfigIds.includes(c.id)));
if (isAnyCheckedConfigUsing || isAnyCheckedGroupUsing) {
return ElMessageBox.alert(
'选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。',
'无法执行删除',
{ type: 'error', confirmButtonText: '我知道了' }
);
return ElMessage.warning('选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。');
}
// 3. ()
@ -1022,9 +1137,11 @@ export default {
.form-group select, .form-group input[type="number"] {
flex: 1;
padding: 5px;
border: 1px solid #e2e8f0;
border: none;
border-radius: 4px;
width: 100px;
box-shadow: 0 0 0 2px #EBF0FF;
outline: none;
}
.slider-wrapper {
@ -1056,14 +1173,11 @@ export default {
.val-text-black {
color: #000;
font-weight: bold;
font-size: 13px;
min-width: 35px;
}
.color-picker-border {
padding: 3px;
border: 1px solid #e2e8f0;
border-radius: 4px;
display: flex;
}
@ -1367,7 +1481,6 @@ export default {
<style>
.el-message-box__header {
text-align: left !important;
padding-top: 15px !important;
}
.el-message-box__title {
@ -1380,4 +1493,5 @@ export default {
background-color: #1559f3 !important;
border-color: #1559f3 !important;
}
</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.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; }
.filter-bar { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 20px; }
.filter-inputs { display: flex; gap: 35px; flex-wrap: nowrap;margin-right: 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;}
.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; }
.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;
}
.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 {
display: flex;
@ -845,9 +834,6 @@ onMounted(() => {
flex-shrink: 0;
}
.search-input, .search-select {
width: 160px !important;
}
.btn-search-ref {
background: #165dff !important;

Loading…
Cancel
Save