Compare commits

...

4 Commits

  1. 9
      controller/GraphStyleController.py
  2. 46
      service/GraphStyleService.py
  3. 87
      service/OperationService.py
  4. 7
      vue/src/api/style.js
  5. 225
      vue/src/system/GraphDemo.vue
  6. 179
      vue/src/system/GraphStyle.vue

9
controller/GraphStyleController.py

@ -50,6 +50,15 @@ async def get_grouped_style_list(request):
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"}) return create_response(200, {"code": 200, "data": data, "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)}"})
@app.get("/api/graph/style/active")
async def get_active_style(request):
"""获取【分组嵌套】格式的配置列表(用于右侧折叠面板)"""
try:
# 调用 Service 的嵌套聚合方法,现在内部已包含 is_active/is_default 逻辑
data = GraphStyleService.get_active_configs()
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"查询异常: {str(e)}"})
@app.post("/api/graph/style/group/apply") @app.post("/api/graph/style/group/apply")

46
service/GraphStyleService.py

@ -87,6 +87,52 @@ class GraphStyleService:
return result return result
@staticmethod @staticmethod
def get_active_configs() -> list:
"""
获取 is_active = 1 的方案组及其配置项
返回长度为 0 1 的列表因为通常只有一个激活组
"""
# 1. 查询 is_active = 1 的组(按 id 排序,取第一个以防有多个)
group_sql = """
SELECT id, group_name, is_active, is_default
FROM graph_style_groups
WHERE is_active = 1
ORDER BY id ASC
LIMIT 1
"""
groups = mysql_client.execute_query(group_sql) or []
if not groups:
return [] # 没有激活的组
group = groups[0]
group_id = group['id']
# 2. 查询该组下的所有配置
configs_sql = """
SELECT id, group_id, canvas_name, current_label, config_json, create_time
FROM graph_configs
WHERE group_id = %s
"""
configs = mysql_client.execute_query(configs_sql, (group_id,)) or []
# 3. 处理配置项
for conf in configs:
if conf.get('config_json'):
try:
conf['styles'] = json.loads(conf['config_json'])
except (TypeError, ValueError):
conf['styles'] = {}
del conf['config_json']
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')
# 4. 组装结果
group['configs'] = configs
return [group] # 返回单元素列表,保持接口兼容性
@staticmethod
def apply_group_all(group_id: int) -> bool: def apply_group_all(group_id: int) -> bool:
""" """
核心新增应用全案 核心新增应用全案

87
service/OperationService.py

@ -389,4 +389,89 @@ class OperationService:
result = self.db.execute_write_and_return(cypher, {"id": rel_id}) result = self.db.execute_write_and_return(cypher, {"id": rel_id})
return {"success": True, "msg": "删除成功"} if result else {"success": False, "msg": "关系不存在"} return {"success": True, "msg": "删除成功"} if result else {"success": False, "msg": "关系不存在"}
except Exception as e: except Exception as e:
return {"success": False, "msg": f"删除失败: {str(e)}"} return {"success": False, "msg": f"删除失败: {str(e)}"}
# --- 7. 导出功能 ---
def export_nodes_to_json(self, label=None, name=None):
"""
按照条件导出节点确保包含 identity, elementId, labels, properties 等所有原始字段
"""
try:
conditions = []
params = {}
# 构建过滤条件(复用查询逻辑,但去掉分页)
if name:
params["name"] = unquote(str(name)).strip()
conditions.append("n.name CONTAINS $name")
lb_clause = ""
if label and label not in ["全部", ""]:
# 为了保证原生对象的完整性,这里直接 MATCH 标签
lb_clause = f":`{label}`"
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
# 注意:这里 RETURN n,返回的是整个节点对象
cypher = f"MATCH (n{lb_clause}) {where_clause} RETURN n"
raw_data = self.db.execute_read(cypher, params)
export_items = []
for row in raw_data:
node = row['n']
# 核心逻辑:提取 Neo4j 节点对象的所有原生属性
node_data = {
"identity": node.id, # 对应你截图中的 identity (旧版 ID)
"elementId": node.element_id, # 对应你截图中的 elementId (新版 ID)
"labels": list(node.labels),
"properties": dict(node.items())
}
export_items.append(node_data)
return {"success": True, "data": export_items}
except Exception as e:
traceback.print_exc()
return {"success": False, "msg": f"导出节点失败: {str(e)}"}
def export_relationships_to_json(self, source=None, target=None, rel_type=None):
"""
按照条件导出关系确保包含起始/结束节点信息及完整属性
"""
try:
conditions = []
params = {}
if source:
params["source"] = unquote(str(source)).strip()
conditions.append("a.name CONTAINS $source")
if target:
params["target"] = unquote(str(target)).strip()
conditions.append("b.name CONTAINS $target")
if rel_type and rel_type not in ["全部", ""]:
conditions.append(f"type(r) = $rel_type")
params["rel_type"] = rel_type
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
# 返回关系对象 r 以及起止节点的 elementId 以便追溯
cypher = f"MATCH (a)-[r]->(b) {where_clause} RETURN r, elementId(a) as startNode, elementId(b) as endNode"
raw_data = self.db.execute_read(cypher, params)
export_items = []
for row in raw_data:
rel = row['r']
rel_data = {
"identity": rel.id,
"elementId": rel.element_id,
"type": rel.type,
"startNodeElementId": row['startNode'],
"endNodeElementId": row['endNode'],
"properties": dict(rel.items())
}
export_items.append(rel_data)
return {"success": True, "data": export_items}
except Exception as e:
traceback.print_exc()
return {"success": False, "msg": f"导出关系失败: {str(e)}"}

7
vue/src/api/style.js

@ -72,6 +72,13 @@ export function getGraphStyleGroups() {
}); });
} }
export function getGraphStyleActive() {
return request({
url: '/api/graph/style/active',
method: 'get'
});
}
/** /**
* 获取所有图谱样式配置列表 * 获取所有图谱样式配置列表
* 保留此接口用于兼容旧版逻辑或后台管理 * 保留此接口用于兼容旧版逻辑或后台管理

225
vue/src/system/GraphDemo.vue

@ -157,6 +157,7 @@ import Menu from "@/components/Menu.vue";
import {a} from "vue-router/dist/devtools-EWN81iOl.mjs"; import {a} from "vue-router/dist/devtools-EWN81iOl.mjs";
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import {getGraphStyleActive} from "@/api/style";
export default { export default {
name: 'Display', name: 'Display',
components: {Menu}, components: {Menu},
@ -232,7 +233,16 @@ export default {
{ value: 'Disease', label: '疾病' }, { value: 'Disease', label: '疾病' },
{ value: 'Drug', label: '药品' }, { value: 'Drug', label: '药品' },
{ value: 'Check', label: '检查' } { value: 'Check', label: '检查' }
] ],
configs:[],
parsedStyles:{},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
Check: '检查',
Symptom: '症状',
Other: '其他'
}
} }
}, },
computed: { computed: {
@ -307,41 +317,66 @@ export default {
this.treeData=this.diseaseICD10Tree this.treeData=this.diseaseICD10Tree
await this.$nextTick(); await this.$nextTick();
try { try {
await this.getDefault()
const response = await getTestGraphData(); // Promise const response = await getTestGraphData(); // Promise
const updatedNodes = response.nodes.map(node => ({ // === 1. nodeId label ===
...node, const nodeIdToEnLabel = {};
type: this.nodeShape, response.nodes.forEach(node => {
style:{ nodeIdToEnLabel[node.id] = node.data.label; // e.g. "Disease"
size: this.nodeSize, });
fill: this.nodeFill,
stroke: this.nodeStroke, // === 2. label ===
lineWidth: this.nodeLineWidth, const updatedNodes = response.nodes.map(node => {
label: this.nodeShowLabel, const enLabel = node.data.label;
labelFontSize: this.nodeFontSize, const zhLabel = this.enToZhLabelMap[enLabel] || '其他'; // 退
labelFontFamily: this.nodeFontFamily, const styleConf = this.parsedStyles[zhLabel] || {};
labelFill: this.nodeFontColor, return {
} ...node,
})) type: styleConf.nodeShape || this.nodeShape,
const updatedEdges = response.edges.map(edge => ({ style: {
...edge, size: styleConf.nodeSize || this.nodeSize,
id: edge.data.relationship.id, fill: styleConf.nodeFill || this.nodeFill,
type: this.edgeType, stroke: styleConf.nodeStroke || this.nodeStroke,
style: { lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
endArrow: this.edgeEndArrow, label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
stroke: this.edgeStroke, labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
lineWidth: this.edgeLineWidth, labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
label: this.edgeShowLabel, labelFill: styleConf.nodeFontColor || this.nodeFontColor
labelFontSize: this.edgeFontSize, }
labelFontFamily: this.edgeFontFamily, };
labelFill: this.edgeFontColor, });
},
})) // === 3. source label ===
const updatedData = { const updatedEdges = response.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
} };
console.log(updatedData)
this.defaultData = updatedData this.defaultData = updatedData
setTimeout(() => { setTimeout(() => {
this.initGraph(); this.initGraph();
this.buildCategoryIndex(); this.buildCategoryIndex();
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
@ -349,6 +384,50 @@ export default {
} catch (error) { } catch (error) {
console.error('加载图谱数据失败:', error); console.error('加载图谱数据失败:', error);
} }
// try {
// await this.getDefault()
// const response = await getTestGraphData(); // Promise
// const updatedNodes = response.nodes.map(node => ({
// ...node,
// type: this.nodeShape,
// style:{
// size: this.nodeSize,
// fill: this.nodeFill,
// stroke: this.nodeStroke,
// lineWidth: this.nodeLineWidth,
// label: this.nodeShowLabel,
// labelFontSize: this.nodeFontSize,
// labelFontFamily: this.nodeFontFamily,
// labelFill: this.nodeFontColor,
// }
// }))
// const updatedEdges = response.edges.map(edge => ({
// ...edge,
// id: edge.data.relationship.id,
// 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 updatedData = {
// nodes: updatedNodes,
// edges: updatedEdges
// }
// this.defaultData = updatedData
// setTimeout(() => {
// this.initGraph();
// this.buildCategoryIndex();
// window.addEventListener('resize', this.handleResize);
// }, 1000);
// } catch (error) {
// console.error(':', error);
// }
}, },
@ -384,6 +463,36 @@ export default {
edgeFontFamily: 'updateAllEdges', edgeFontFamily: 'updateAllEdges',
}, },
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)
},
// //
toggleDropdown() { toggleDropdown() {
this.isDropdownOpen = !this.isDropdownOpen; this.isDropdownOpen = !this.isDropdownOpen;
@ -871,22 +980,22 @@ export default {
node: { node: {
style: { style: {
fill: (d) => { // fill: (d) => {
const label = d.data?.label; // const label = d.data?.label;
if (label === 'Disease') return '#EF4444'; // // if (label === 'Disease') return '#EF4444'; //
if (label === 'Drug') return '#91cc75'; // 绿 // if (label === 'Drug') return '#91cc75'; // 绿
if (label === 'Symptom') return '#fac858'; // // if (label === 'Symptom') return '#fac858'; //
if (label === 'Check') return '#336eee'; // // if (label === 'Check') return '#336eee'; //
return '#59d1d4'; // // return '#59d1d4'; //
}, // },
stroke: (d) => { // stroke: (d) => {
const label = d.data?.label; // const label = d.data?.label;
if (label === 'Disease') return '#B91C1C'; // if (label === 'Disease') return '#B91C1C';
if (label === 'Drug') return '#047857'; // if (label === 'Drug') return '#047857';
if (label === 'Check') return '#1D4ED8'; // // if (label === 'Check') return '#1D4ED8'; //
if (label === 'Symptom') return '#B45309'; // if (label === 'Symptom') return '#B45309';
return '#40999b'; // return '#40999b';
}, // },
labelText: (d) => d.data.name, labelText: (d) => d.data.name,
labelPlacement: 'center', labelPlacement: 'center',
labelWordWrap: true, labelWordWrap: true,
@ -921,16 +1030,16 @@ export default {
edge: { edge: {
style: { style: {
labelText: (d) => d.data.relationship.properties.label, labelText: (d) => d.data.relationship.properties.label,
stroke: (d) => { // stroke: (d) => {
// target label // // target label
const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID // const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
// target // // target
if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)'; // if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)'; // if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)'; // if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
if (targetLabel === 'Check') return 'rgba(51,110,238,0.5)'; // // if (targetLabel === 'Check') 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

179
vue/src/system/GraphStyle.vue

@ -32,7 +32,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体名称:</label> <label>字体名称</label>
<select v-model="nodeFontFamily"> <select v-model="nodeFontFamily">
<option value="Microsoft YaHei, sans-serif">微软雅黑</option> <option value="Microsoft YaHei, sans-serif">微软雅黑</option>
<option value="SimSun, serif">宋体SimSun</option> <option value="SimSun, serif">宋体SimSun</option>
@ -42,7 +42,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体大小:</label> <label>字体大小</label>
<div class="slider-wrapper"> <div class="slider-wrapper">
<input v-model.number="nodeFontSize" type="range" min="10" max="24" step="1" class="theme-slider"/> <input v-model.number="nodeFontSize" type="range" min="10" max="24" step="1" class="theme-slider"/>
<span class="val-text-black">{{ nodeFontSize }}px</span> <span class="val-text-black">{{ nodeFontSize }}px</span>
@ -50,14 +50,14 @@
</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="nodeFontColor" type="color" class="square-picker"/> <input v-model="nodeFontColor" type="color" class="square-picker"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>图形:</label> <label>图形</label>
<select v-model="nodeShape"> <select v-model="nodeShape">
<option value="circle">圆形</option> <option value="circle">圆形</option>
<option value="diamond">菱形</option> <option value="diamond">菱形</option>
@ -67,7 +67,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>尺寸:</label> <label>尺寸</label>
<input <input
:value="nodeSize" :value="nodeSize"
type="number" type="number"
@ -78,21 +78,21 @@
</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="nodeFill" type="color" 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"/> <input v-model="nodeStroke" type="color" class="square-picker"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>边框尺寸:</label> <label>边框尺寸</label>
<input <input
:value="nodeLineWidth" :value="nodeLineWidth"
type="number" type="number"
@ -116,7 +116,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体名称:</label> <label>字体名称</label>
<select v-model="edgeFontFamily"> <select v-model="edgeFontFamily">
<option value="Microsoft YaHei, sans-serif">微软雅黑</option> <option value="Microsoft YaHei, sans-serif">微软雅黑</option>
<option value="SimSun, serif">宋体</option> <option value="SimSun, serif">宋体</option>
@ -126,7 +126,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体大小:</label> <label>字体大小</label>
<div class="slider-wrapper"> <div class="slider-wrapper">
<input v-model.number="edgeFontSize" type="range" min="8" max="16" step="1" class="theme-slider"/> <input v-model.number="edgeFontSize" type="range" min="8" max="16" step="1" class="theme-slider"/>
<span class="val-text-black">{{ edgeFontSize }}px</span> <span class="val-text-black">{{ edgeFontSize }}px</span>
@ -134,14 +134,14 @@
</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="edgeFontColor" type="color" class="square-picker"/> <input v-model="edgeFontColor" type="color" class="square-picker"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>连边类型:</label> <label>连边类型</label>
<select v-model="edgeType"> <select v-model="edgeType">
<option value="line">直线</option> <option value="line">直线</option>
<option value="polyline">折线</option> <option value="polyline">折线</option>
@ -151,7 +151,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>线粗细:</label> <label>线粗细</label>
<input <input
:value="edgeLineWidth" :value="edgeLineWidth"
type="number" type="number"
@ -162,7 +162,7 @@
</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="edgeStroke" type="color" class="square-picker"/> <input v-model="edgeStroke" type="color" class="square-picker"/>
</div> </div>
@ -304,7 +304,7 @@ import {
deleteGraphStyle, deleteGraphStyle,
batchDeleteGraphStyle, batchDeleteGraphStyle,
deleteGraphStyleGroup, deleteGraphStyleGroup,
applyGraphStyleGroup applyGraphStyleGroup, getGraphStyleActive
} from '@/api/style'; } from '@/api/style';
import {ElMessageBox, ElMessage} from 'element-plus'; import {ElMessageBox, ElMessage} from 'element-plus';
import {markRaw} from 'vue'; import {markRaw} from 'vue';
@ -684,19 +684,15 @@ export default {
this.saveDialogVisible = true; this.saveDialogVisible = true;
}, },
async confirmSave() { async confirmSave() {
if (!this.saveForm.group_name || !this if (!this.saveForm.group_name || !this.saveForm.group_name.trim()) {
.saveForm.group_name.trim()) { return ElMessage.warning("请选择或输入方案名称");
return this.$message.warning("请选择或输入方案名称"
);
} }
if (!this.saveForm.canvas_name || !this if (!this.saveForm.canvas_name || !this.saveForm.canvas_name.trim()) {
.saveForm.canvas_name.trim()) { return ElMessage.warning("请输入配置名称");
return this.$message.warning("请输入配置名称"
);
} }
const payload = { const payload = {
canvas_name: this.saveForm.canvas_name, canvas_name: this.saveForm.canvas_name.trim(),
group_name: this.saveForm.group_name, group_name: this.saveForm.group_name.trim(),
current_label: this.activeTags, current_label: this.activeTags,
styles: {...this.tagStyles[tagToLabelMap[this.activeTags]]} styles: {...this.tagStyles[tagToLabelMap[this.activeTags]]}
}; };
@ -704,6 +700,8 @@ export default {
if (res.code === 200) { if (res.code === 200) {
ElMessage.success("保存成功"); ElMessage.success("保存成功");
this.saveDialogVisible = false; this.saveDialogVisible = false;
this.saveForm.canvas_name = '';
this.saveForm.group_name = '';
this.resetAllTagsToDefault(); this.resetAllTagsToDefault();
this.fetchConfigs(); this.fetchConfigs();
} }
@ -725,51 +723,114 @@ export default {
this.updateAllElements(); this.updateAllElements();
ElMessage.info(`已重置【${this.activeTags}】样式`); ElMessage.info(`已重置【${this.activeTags}】样式`);
}, },
// --- ---
async deleteSingleConfig(id) { async deleteSingleConfig(id) {
if (this.usingConfigIds.includes(id)) {
return ElMessage.error("该配置正在应用中,请取消应用或切换方案后再删除");
}
try { try {
await ElMessageBox.confirm('确定删除此配置吗?', '提示'); await ElMessageBox.confirm('确定删除此配置吗?', '提示');
const res = await deleteGraphStyle(id); const res = await deleteGraphStyle(id);
if (res.code === 200) { if (res.code === 200) {
this.usingConfigIds = this.usingConfigIds.filter(cid => cid !== id); ElMessage.success("删除成功");
this.fetchConfigs(); this.fetchConfigs();
this.updateAllElements();
} }
} catch (err) {} } catch (err) {}
}, },
// --- ---
async deleteGroup(groupId) { 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("系统至少需保留一个方案,无法全部删除");
}
try { try {
await ElMessageBox.confirm('确定删除整个方案吗?', '提示'); await ElMessageBox.confirm('确定删除整个方案吗?', '提示');
const res = await deleteGraphStyleGroup(groupId); const res = await deleteGraphStyleGroup(groupId);
if (res.code === 200) { if (res.code === 200) {
ElMessage.success("方案已删除");
this.fetchConfigs(); this.fetchConfigs();
this.updateAllElements();
} }
} catch (err) {} } catch (err) {}
}, },
// --- ---
async handleUnifiedBatchDelete() { 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 ElMessageBox.alert(
'选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。',
'无法执行删除',
{ type: 'error', confirmButtonText: '我知道了' }
);
}
// 3. ()
if (this.checkedGroupIds.length >= this.styleGroups.length && this.styleGroups.length > 0) {
return ElMessage.error("系统至少需要保留一个方案,请勿全部勾选删除");
}
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
'确定执行批量删除吗?', '确定执行批量删除吗?此操作不可恢复。',
'批量删除', '批量删除确认',
{ {
confirmButtonText: '确定', confirmButtonText: '确定删除',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
} }
); );
for (const gid of this.checkedGroupIds) await deleteGraphStyleGroup(gid);
if (this.checkedConfigIds.length > 0) await batchDeleteGraphStyle({ids: this.checkedConfigIds}); //
if (this.checkedGroupIds.length > 0) {
for (const gid of this.checkedGroupIds) {
await deleteGraphStyleGroup(gid);
}
}
if (this.checkedConfigIds.length > 0) {
await batchDeleteGraphStyle({ids: this.checkedConfigIds});
}
ElMessage.success("批量删除成功");
this.clearSelection(); this.clearSelection();
this.fetchConfigs(); this.fetchConfigs();
this.updateAllElements(); this.updateAllElements();
} catch (e) {} } catch (e) {
console.log("用户取消或删除失败", e);
}
}, },
clearSelection() { clearSelection() {
this.checkedConfigIds = []; this.checkedConfigIds = [];
this.checkedGroupIds = []; this.checkedGroupIds = [];
}, },
handleResize() { handleResize() {
if (this._graph && this.$refs.graphContainer) this._graph.setSize(this.$refs.graphContainer.clientWidth, this.$refs.graphContainer.clientHeight); if (this._graph && this.$refs.graphContainer) {
this._graph.setSize(this.$refs.graphContainer.clientWidth, this.$refs.graphContainer.clientHeight);
}
} }
} }
} }
@ -821,7 +882,7 @@ export default {
} }
.control-panel { .control-panel {
width: 260px; width: 250px;
background: #ffffff; background: #ffffff;
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.08); box-shadow: 4px 0 12px rgba(0, 0, 0, 0.08);
padding: 10px; padding: 10px;
@ -833,7 +894,7 @@ export default {
} }
.config-list-panel { .config-list-panel {
width: 320px; width: 250px;
background: #ffffff; background: #ffffff;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.08); box-shadow: -4px 0 12px rgba(0, 0, 0, 0.08);
padding: 18px; padding: 18px;
@ -845,14 +906,15 @@ export default {
} }
.panel-header-container { .panel-header-container {
margin-bottom: 15px; margin-bottom: 10px;
} }
.panel-header { .panel-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 8px; padding-bottom: 10px;
border-bottom: 1px solid #E2E6F3;
} }
/*.header-line { /*.header-line {
@ -878,7 +940,7 @@ export default {
.tag-pill { .tag-pill {
flex-shrink: 0; flex-shrink: 0;
padding: 0 10px; padding: 1px 10px;
border-radius: 20px; border-radius: 20px;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
@ -897,14 +959,11 @@ export default {
background-color: #4a68db; background-color: #4a68db;
} }
.section {
margin-bottom: 25px;
}
.section-title { .section-title {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 16px; font-size: 14px;
font-weight: bold; font-weight: bold;
color: #334155; color: #334155;
margin-bottom: 12px; margin-bottom: 12px;
@ -918,7 +977,7 @@ export default {
position: absolute; position: absolute;
left: 0; left: 0;
width: 4px; width: 4px;
height: 16px; height: 15px;
background-color: #1559f3; background-color: #1559f3;
border-radius: 2px; border-radius: 2px;
} }
@ -927,15 +986,16 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
font-size: 14px; font-size: 13px;
margin-bottom: 12px; margin-bottom: 10px;
color: #475569; color: #475569;
} }
.checkbox-label { .checkbox-label {
width: 150px; width: 80px;
flex-shrink: 0; flex-shrink: 0;
text-align: left; text-align: left;
margin-right: 8px;
} }
.theme-checkbox { .theme-checkbox {
@ -948,13 +1008,15 @@ export default {
.form-group, .color-picker-item { .form-group, .color-picker-item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 10px;
font-size: 14px; font-size: 13px;
} }
.form-group label, .color-picker-item label { .form-group label, .color-picker-item label {
width: 80px; width: 80px;
flex-shrink: 0; flex-shrink: 0;
text-align: right;
margin-right: 8px;
} }
.form-group select, .form-group input[type="number"] { .form-group select, .form-group input[type="number"] {
@ -962,6 +1024,7 @@ export default {
padding: 5px; padding: 5px;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 4px; border-radius: 4px;
width: 100px;
} }
.slider-wrapper { .slider-wrapper {
@ -1014,9 +1077,6 @@ export default {
} }
.button-footer { .button-footer {
display: flex;
gap: 10px;
padding-top: 10px;
} }
.btn-confirm-save { .btn-confirm-save {
@ -1024,10 +1084,12 @@ export default {
color: #fff; color: #fff;
border: none; border: none;
flex: 1; flex: 1;
padding: 10px; padding: 5px 14px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-weight: bold; width: 100px;
font-size: 12px;
margin-right: 15px;
} }
.btn-reset-style { .btn-reset-style {
@ -1035,9 +1097,11 @@ export default {
color: #1559f3; color: #1559f3;
border: 1px solid #1559f3; border: 1px solid #1559f3;
flex: 1; flex: 1;
padding: 10px; padding: 5px 14px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
width: 100px;
font-size: 12px;
} }
.graph-container { .graph-container {
@ -1081,11 +1145,10 @@ export default {
.card-using { .card-using {
background-color: #eff6ff !important; background-color: #eff6ff !important;
outline: 1.5px solid #1559f3;
} }
.card-checked { .card-checked {
border-left: 4px solid #ef4444 !important; border-left: 4px solid rgb(239, 68, 68) !important;
} }

Loading…
Cancel
Save