diff --git a/vue/src/components/GraphToolbar.vue b/vue/src/components/GraphToolbar.vue index 9ae4173..b83a14d 100644 --- a/vue/src/components/GraphToolbar.vue +++ b/vue/src/components/GraphToolbar.vue @@ -127,9 +127,14 @@ export default { }, exportToSVGManual() { - const graph = this.graph; - if (!graph) return; + const graph = this._graph || this.graph; + if (!graph) { + this.$message.error("未找到图表实例"); + return; + } + const { nodes, edges } = graph.getData(); + // 1. 计算画布边界,确保所有节点都在视口内 const bBox = graph.getCanvas().getRoot().getRenderBounds(); const padding = 60; const minX = bBox.min[0] - padding; @@ -139,7 +144,6 @@ export default { const vWidth = maxX - minX; const vHeight = maxY - minY; - // 1. 定义与问答页面一致的颜色映射(用于兜底和逻辑计算) const colorMap = { 'Disease': { fill: '#EF4444', stroke: '#B91C1C', edge: '#EF4444' }, 'Drug': { fill: '#91cc75', stroke: '#047857', edge: '#91cc75' }, @@ -148,39 +152,101 @@ export default { 'Other': { fill: '#59d1d4', stroke: '#40999b', edge: '#59d1d4' } }; + // 2. 预收集颜色用于生成对应的箭头定义 + const usedColors = new Set(); + edges.forEach(edge => { + const s = nodes.find(n => n.id === edge.source); + const sourceType = s?.data?.label || 'Other'; + const color = edge.style?.stroke || colorMap[sourceType]?.edge || '#cccccc'; + usedColors.add(color); + }); + let svgContent = ``; + + // 3. 动态生成 Marker 定义 + svgContent += ``; + const colorList = Array.from(usedColors); + colorList.forEach((color, index) => { + svgContent += ` + + + `; + }); + svgContent += ``; + svgContent += ``; - // 2. 渲染边 + // 4. 渲染连边 edges.forEach(edge => { const s = nodes.find(n => n.id === edge.source); const t = nodes.find(n => n.id === edge.target); if (!s || !t) return; - // 获取坐标 (从 style 中取,G6 会把布局后的坐标存这) - const x1 = s.style?.x || 0; - const y1 = s.style?.y || 0; - const x2 = t.style?.x || 0; - const y2 = t.style?.y || 0; + const x1 = s.style?.x || 0, y1 = s.style?.y || 0; + const x2 = t.style?.x || 0, y2 = t.style?.y || 0; + const nodeRadius = (t.style?.size || 50) / 2; - // --- 核心修正:根据起点节点的类型来确定边的颜色 --- - // 这对应了你问答页面里 edge: { style: { stroke: (d) => ... } } 的逻辑 - const sourceType = s.data?.label || 'Other'; // 获取起点类型 (Disease/Drug等) - const strokeColor = colorMap[sourceType]?.edge || '#cccccc'; + const sourceType = s.data?.label || 'Other'; + const strokeColor = edge.style?.stroke || colorMap[sourceType]?.edge || '#cccccc'; + const markerId = `arrow-${colorList.indexOf(strokeColor)}`; + + let pathD = ""; + let tx, ty; + + const dx = x2 - x1, dy = y2 - y1; + const dist = Math.sqrt(dx * dx + dy * dy); + const nx = -dy / dist, ny = dx / dist; + const curveOffset = edge.style?.curveOffset || 40; + + // --- 核心逻辑:区分类型并修正切线箭头 --- + if (edge.type === 'cubic' && dist > 0) { + // 三次贝塞尔 + const cp1X = x1 + dx / 3 + nx * curveOffset; + const cp1Y = y1 + dy / 3 + ny * curveOffset; + const cp2X = x1 + (dx * 2) / 3 + nx * curveOffset; + const cp2Y = y1 + (dy * 2) / 3 + ny * curveOffset; + + const angle = Math.atan2(y2 - cp2Y, x2 - cp2X); + const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1); + const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1); + + pathD = `M ${x1} ${y1} C ${cp1X} ${cp1Y} ${cp2X} ${cp2Y} ${realX2} ${realY2}`; + tx = (x1 + cp1X + cp2X + x2) / 4; + ty = (y1 + cp1Y + cp2Y + y2) / 4; + + } else if (edge.type === 'quadratic' && dist > 0) { + // 二次贝塞尔 + const cpX = (x1 + x2) / 2 + nx * curveOffset; + const cpY = (y1 + y2) / 2 + ny * curveOffset; + + const angle = Math.atan2(y2 - cpY, x2 - cpX); + const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1); + const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1); + + pathD = `M ${x1} ${y1} Q ${cpX} ${cpY} ${realX2} ${realY2}`; + tx = (x1 + cpX * 2 + x2) / 4; + ty = (y1 + cpY * 2 + y2) / 4; + + } else { + // 直线 + const angle = Math.atan2(y2 - y1, x2 - x1); + const realX2 = x2 - Math.cos(angle) * nodeRadius; + const realY2 = y2 - Math.sin(angle) * nodeRadius; + pathD = `M ${x1} ${y1} L ${realX2} ${realY2}`; + tx = (x1 + x2) / 2; + ty = (y1 + y2) / 2; + } - // 绘制线条 (增加 stroke-width 确保可见) - svgContent += ``; + svgContent += ``; - // 渲染边文字 - const labelText = edge.data?.relationship?.properties?.label || edge.data?.label || ""; + // 渲染文字 + const labelText = edge.data?.label || edge.label || ""; if (labelText) { - const mx = (x1 + x2) / 2; - const my = (y1 + y2) / 2; - svgContent += `${labelText}`; + svgContent += `${labelText}`; } }); - // 3. 渲染节点 + // 5. 渲染节点 nodes.forEach(node => { const type = node.data?.label || 'Other'; const colors = colorMap[type] || colorMap['Other']; @@ -188,25 +254,28 @@ export default { const x = node.style?.x || 0; const y = node.style?.y || 0; - svgContent += ``; + svgContent += ``; - if (node.data?.name) { - svgContent += `${node.data.name}`; + const name = node.label || node.data?.name || ""; + if (name) { + svgContent += `${name}`; } }); svgContent += ``; - // 保存文件逻辑保持不变... + // 6. 导出文件 try { - const blob = new Blob([svgContent], {type: 'image/svg+xml;charset=utf-8'}); + const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = `矢量图谱_${Date.now()}.svg`; + link.download = `医疗图谱导出_${Date.now()}.svg`; + document.body.appendChild(link); link.click(); + document.body.removeChild(link); URL.revokeObjectURL(url); - this.$message.success('全量矢量图导出成功'); + this.$message.success('高精度矢量图导出成功'); } catch (err) { this.$message.error('导出失败'); } diff --git a/vue/src/system/GraphDemo.vue b/vue/src/system/GraphDemo.vue index f80e6ee..fee8ee3 100644 --- a/vue/src/system/GraphDemo.vue +++ b/vue/src/system/GraphDemo.vue @@ -4,113 +4,113 @@ -
-
-
- - -
-
-
-
+
+
+ + +
+
+
+
- {{ item.label }} + class="color-block" + @click="toggleCategory(item.key)" + >
+ {{ item.label }} +
+
+
+
+
+
+
+
+ 疾病信息 + 药品信息 + 检查信息
+
{{diseaseCount.toLocaleString()}}种
+
{{ drugCount.toLocaleString() }}种
+
{{checkCount.toLocaleString()}}种
- -
-
-
-
-
- 疾病信息 - 药品信息 - 检查信息 -
-
{{diseaseCount.toLocaleString()}}种
-
{{ drugCount.toLocaleString() }}种
-
{{checkCount.toLocaleString()}}种
-
-
- -
- -
+
+ +
+ +
{{ currentTypeLabel }} - -
- - -
-
- {{ item.label }} -
-
+ +
+ + +
+
+ {{ item.label }}
-
- - - - - - - - - - - - - - - - - -
- - ICD10 - 科室 - 首字母 - + +
+
+ + + + + + + + + + + + + + + + + +
+ + ICD10 + 科室 + 首字母 + -
-
- - 药物分类 - 首字母 - +
+
+ + 药物分类 + 首字母 + -
-
+
+
{ - this.diseaseCount=res.Disease - this.drugCount=res.Drug - this.checkCount=res.Check + getCount() { + getCount().then(res => { + this.diseaseCount = res.Disease + this.drugCount = res.Drug + this.checkCount = res.Check console.log(res) }) }, buildCategoryIndex() { const index = {}; - if (this._graph!=null){ + if (this._graph != null) { const nodes = this._graph.getNodeData() // 获取所有节点 nodes.forEach(node => { console.log(node.data.label) const category = node.data.label; // 假设 label 是类别 - if(category=='Drug'||category=='Symptom'|| - category=='Disease'||category=='Check'){ + if (category == 'Drug' || category == 'Symptom' || + category == 'Disease' || category == 'Check') { if (!index[category]) index[category] = []; index[category].push(node.id); - }else{ + } else { if (!index["Other"]) index["Other"] = []; index["Other"].push(node.id); } @@ -692,7 +693,7 @@ export default { }, // 切换某个类别的显示状态 - toggleCategory (key){ + toggleCategory(key) { if (this.visibleCategories.has(key)) { this.visibleCategories.delete(key) } else { @@ -747,28 +748,28 @@ export default { } }, - changeTree(){ - if(this.typeRadio=="Disease"){ - if(this.DiseaseRadio=="ICD10") { - this.treeData=this.diseaseICD10Tree + changeTree() { + if (this.typeRadio == "Disease") { + if (this.DiseaseRadio == "ICD10") { + this.treeData = this.diseaseICD10Tree } - if(this.DiseaseRadio=="Department") { - this.treeData=this.diseaseDepartTree + if (this.DiseaseRadio == "Department") { + this.treeData = this.diseaseDepartTree } - if(this.DiseaseRadio=="SZM") { - this.treeData=this.diseaseSZMTree + if (this.DiseaseRadio == "SZM") { + this.treeData = this.diseaseSZMTree } } - if(this.typeRadio=="Drug") { - if(this.DrugRadio=="Subject") { - this.treeData=this.drugSubjectTree + if (this.typeRadio == "Drug") { + if (this.DrugRadio == "Subject") { + this.treeData = this.drugSubjectTree } - if(this.DrugRadio=="SZM") { - this.treeData=this.drugTree + if (this.DrugRadio == "SZM") { + this.treeData = this.drugTree } } - if(this.typeRadio=="Check") { - this.treeData=this.checkTree + if (this.typeRadio == "Check") { + this.treeData = this.checkTree } }, async loadDiseaseICD10TreeData() { @@ -828,15 +829,15 @@ export default { const response = await getGraph(data); // 等待 Promise 解析 this.formatData(response) } - if(data.level=="category"|| - data.level=="subcategory"|| - data.level=="diagnosis"){ - data.type="Disease" + if (data.level == "category" || + data.level == "subcategory" || + data.level == "diagnosis") { + data.type = "Disease" const response = await getGraph(data); // 等待 Promise 解析 this.formatData(response) } - if(data.type === "Disease"){ - data.type="Disease" + if (data.type === "Disease") { + data.type = "Disease" const response = await getGraph(data); // 等待 Promise 解析 this.formatData(response) } @@ -852,17 +853,17 @@ export default { // 1. 清除所有节点和边的状态 this._graph.getNodeData().forEach(node => { - this._graph.setElementState(node.id,[]); + this._graph.setElementState(node.id, []); }); this._graph.getEdgeData().forEach(edge => { - this._graph.setElementState(edge.id,[]); + this._graph.setElementState(edge.id, []); }); // 2. (可选)取消所有 pending 的交互 // 比如如果你有高亮定时器,这里 clearTimeout }, - formatData(data){ + formatData(data) { this._graph.stopLayout(); this.clearGraphState(); @@ -903,7 +904,7 @@ export default { return { ...edge, id: edge.data?.relationship?.id || edge.id, - type: styleConf.edgeType ||this.edgeType, + type: styleConf.edgeType || this.edgeType, style: { endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, stroke: styleConf.edgeStroke || this.edgeStroke, @@ -949,7 +950,7 @@ export default { }, initGraph() { - if (this._graph!=null){ + if (this._graph != null) { this._graph.destroy() this._graph = null; } @@ -960,7 +961,7 @@ export default { console.log(this._nodeLabelMap) const container = this.$refs.graphContainer; if (!container) return; - if (container!=null){ + if (container != null) { const width = container.clientWidth || 800; const height = container.clientHeight || 600; console.log(width) @@ -1000,8 +1001,8 @@ export default { easing: 'ease-in-out', // 动画缓动函数 }, }, - behaviors: [ 'zoom-canvas', 'drag-element', - 'click-select','focus-element', { + behaviors: ['zoom-canvas', 'drag-element', + 'click-select', 'focus-element', { type: 'hover-activate', degree: 1, }, @@ -1048,7 +1049,7 @@ export default { shadowBlur: 10, opacity: 1 }, - highlight:{ + highlight: { stroke: '#FF5722', lineWidth: 4, opacity: 1 @@ -1056,7 +1057,7 @@ export default { inactive: { opacity: 0.8 }, - normal:{ + normal: { opacity: 1 } @@ -1104,14 +1105,14 @@ export default { inactive: { opacity: 0.8 }, - normal:{ + normal: { opacity: 1 } }, }, - data:this.defaultData, + data: this.defaultData, }); this.$nextTick(() => { @@ -1169,10 +1170,10 @@ export default { // }); graph.on('node:click', (evt) => { const nodeItem = evt.target.id; // 获取当前鼠标进入的节点元素 - let node=graph.getNodeData(nodeItem).data - let data={ - label:node.name, - type:node.label + let node = graph.getNodeData(nodeItem).data + let data = { + label: node.name, + type: node.label } getGraph(data).then(response => { console.log(response) @@ -1205,7 +1206,7 @@ export default { const sourceName = sourceNode?.data.name || sourceId; const targetName = targetNode?.data.name || targetId; - const rel = data.relationship.properties.label || '关联'; + const rel = data.relationship.properties.label || '关联'; return `
${sourceName} — ${rel} —> ${targetName} @@ -1214,31 +1215,26 @@ export default { }, { type: 'toolbar', - onClick: (id) => - { + onClick: (id) => { if (id === 'reset') { // 1. 如果是重置,直接在本地执行方法 this.localResetGraph(); - } - else if (this.$refs.toolbarRef) { + } else if (this.$refs.toolbarRef) { // 2. 其他功能(放大、导出等)继续交给组件处理 this.$refs.toolbarRef.handleToolbarAction(id); } }, getItems: () => { return [ - { id: 'zoom-in', value: 'zoom-in', title: '放大' }, - { id: 'zoom-out', value: 'zoom-out', title: '缩小' }, - { id: 'undo', value: 'undo', title: '撤销' }, - { id: 'redo', value: 'redo', title: '重做' }, - { id: 'auto-fit', value: 'auto-fit', title: '聚焦' }, - { id: 'reset', value: 'reset', title: '重置' }, - { id: 'export', value: 'export', title: '导出图谱' }, + {id: 'zoom-in', value: 'zoom-in', title: '放大'}, + {id: 'zoom-out', value: 'zoom-out', title: '缩小'}, + {id: 'auto-fit', value: 'auto-fit', title: '聚焦'}, + {id: 'reset', value: 'reset', title: '重置'}, + {id: 'export', value: 'export', title: '导出图谱'}, ]; }, }, - // 如果需要撤销重做功能,必须加上 history 插件 - { type: 'history', key: 'history' }]) + ]) } }, localResetGraph() { @@ -1290,7 +1286,7 @@ export default { const updatedNodes = this.defaultData.nodes.map(node => ({ ...node, type: this.nodeShape, - style:{ + style: { size: this.nodeSize, fill: this.nodeFill, stroke: this.nodeStroke, @@ -1548,6 +1544,7 @@ button:hover { width: 16px; } + /*.search-btn:hover { background-color: rgba(34, 101, 244, 0.64); border-radius: 50%; @@ -1709,7 +1706,6 @@ button:hover { } - /* 自定义选中后的样式 */ /deep/ .radio-disease .el-radio__input.is-checked + .el-radio__label { color: rgb(153, 10, 0); @@ -1722,6 +1718,7 @@ button:hover { /deep/ .radio-check .el-radio__input.is-checked + .el-radio__label { color: #1890ff; } + /* 自定义下拉样式 */ .select-container { position: relative; @@ -1751,7 +1748,7 @@ button:hover { background: rgba(255, 255, 255, 0.64); border: 1px solid #e0e0e0; border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10; min-width: 80px; color: #000; diff --git a/vue/src/system/GraphStyle.vue b/vue/src/system/GraphStyle.vue index 62cffd9..5011d83 100644 --- a/vue/src/system/GraphStyle.vue +++ b/vue/src/system/GraphStyle.vue @@ -53,7 +53,7 @@
- +
@@ -82,7 +82,7 @@
- +
@@ -90,7 +90,7 @@
- +
@@ -140,7 +140,7 @@
- +
@@ -169,7 +169,7 @@
- +
@@ -717,66 +717,130 @@ export default { exportToSVGManual() { const graph = this._graph; + if (!graph) return; const { nodes, edges } = graph.getData(); const width = graph.getSize()[0]; const height = graph.getSize()[1]; - // 1. 定义 SVG 头部和滤镜/渐变 + const usedColors = new Set(); + edges.forEach(edge => { if (edge.style?.stroke) usedColors.add(edge.style.stroke); }); + if (usedColors.size === 0) usedColors.add('#cccccc'); + let svgContent = ``; - svgContent += ``; // 背景 - // 2. 先画边 + + // 1. 定义动态颜色箭头 + svgContent += ``; + Array.from(usedColors).forEach((color, index) => { + const hexColor = color.includes('rgba') ? color.replace(/rgba?\((\d+),\s*(\d+),\s*(\d+).*/, 'rgb($1,$2,$3)') : color; + svgContent += ` + + + `; + }); + svgContent += ``; + + svgContent += ``; + + // 2. 渲染连边 edges.forEach(edge => { - const sourceNode = nodes.find(n => n.id === edge.source); - const targetNode = nodes.find(n => n.id === edge.target); - if (!sourceNode || !targetNode) return; - const x1 = sourceNode.style.x || 0; - const y1 = sourceNode.style.y || 0; - const x2 = targetNode.style.x || 0; - const y2 = targetNode.style.y || 0; + const s = nodes.find(n => n.id === edge.source); + const t = nodes.find(n => n.id === edge.target); + if (!s || !t) return; + + const x1 = s.style?.x || 0, y1 = s.style?.y || 0; + const x2 = t.style?.x || 0, y2 = t.style?.y || 0; + const nodeRadius = (t.style?.size || 60) / 2; + const style = edge.style || {}; const stroke = style.stroke || '#ccc'; - const lineWidth = style.lineWidth || 1; - // 绘制直线 - svgContent += ``; + const colorIdx = Array.from(usedColors).indexOf(stroke); + const markerId = `arrow-${colorIdx !== -1 ? colorIdx : 0}`; + + let pathD = ""; + let tx, ty; // 文字坐标 + + const dx = x2 - x1, dy = y2 - y1; + const dist = Math.sqrt(dx * dx + dy * dy); + const nx = -dy / dist, ny = dx / dist; + + // --- 逻辑分支开始 --- + + if (edge.type === 'cubic') { + // 💡 三次贝塞尔曲线 (Cubic Bezier) + const offset = style.curveOffset || 40; + // 计算两个控制点,模拟 G6 的贝塞尔形态 + const cp1X = x1 + dx / 3 + nx * offset; + const cp1Y = y1 + dy / 3 + ny * offset; + const cp2X = x1 + (dx * 2) / 3 + nx * offset; + const cp2Y = y1 + (dy * 2) / 3 + ny * offset; + + // 箭头角度:由第二个控制点 cp2 指向终点 + const angle = Math.atan2(y2 - cp2Y, x2 - cp2X); + const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1); + const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1); + + pathD = `M ${x1} ${y1} C ${cp1X} ${cp1Y} ${cp2X} ${cp2Y} ${realX2} ${realY2}`; + // 文字位置取两个控制点中心 + tx = (x1 + cp1X + cp2X + x2) / 4; + ty = (y1 + cp1Y + cp2Y + y2) / 4; + + } else if (edge.type === 'quadratic') { + // 💡 二次贝塞尔曲线 (Quadratic Bezier) + const offset = style.curveOffset || 40; + const cpX = (x1 + x2) / 2 + nx * offset; + const cpY = (y1 + y2) / 2 + ny * offset; + + const angle = Math.atan2(y2 - cpY, x2 - cpX); + const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1); + const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1); + + pathD = `M ${x1} ${y1} Q ${cpX} ${cpY} ${realX2} ${realY2}`; + tx = (x1 + cpX * 2 + x2) / 4; + ty = (y1 + cpY * 2 + y2) / 4; - // 如果有文字 - if (style.labelText) { - const mx = (x1 + x2) / 2; - const my = (y1 + y2) / 2; - svgContent += `${style.labelText}`; + } else { + // 💡 直线 (Line) + const angle = Math.atan2(y2 - y1, x2 - x1); + const realX2 = x2 - Math.cos(angle) * nodeRadius; + const realY2 = y2 - Math.sin(angle) * nodeRadius; + pathD = `M ${x1} ${y1} L ${realX2} ${realY2}`; + tx = (x1 + x2) / 2; + ty = (y1 + y2) / 2; + } + + // --- 逻辑分支结束 --- + + const markerAttr = style.endArrow ? `marker-end="url(#${markerId})"` : ""; + svgContent += ``; + + const label = edge.data?.relationship?.properties?.label || style.labelText || ""; + if (style.edgeShowLabel !== false && label) { + svgContent += `${label}`; } }); - // 3. 后画节点 + // 3. 渲染节点 nodes.forEach(node => { const style = node.style || {}; - const x = style.x || 0; - const y = style.y || 0; const r = (style.size || 60) / 2; - const fill = style.fill || '#EF4444'; - const stroke = style.stroke || '#B91C1C'; - const lineWidth = style.lineWidth || 2; - - // 绘制圆形 - svgContent += ``; - - // 绘制标签文字 + svgContent += ``; if (style.labelText) { - svgContent += `${style.labelText}`; + svgContent += `${style.labelText}`; } }); svgContent += ``; - // 4. 触发下载 + // 4. 下载 const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = `矢量图谱_${Date.now()}.svg`; + link.download = `医疗图谱导出_${Date.now()}.svg`; + document.body.appendChild(link); link.click(); + document.body.removeChild(link); URL.revokeObjectURL(url); - ElMessage.success(' SVG 导出成功'); }, initDraggableToolbar() { @@ -970,7 +1034,7 @@ export default { } }; }); - this._graph.setData({ nodes, edges }); + this._graph.setData({nodes, edges}); this._graph.render(); }, safeNum(val, defaultVal = 1) { @@ -986,7 +1050,7 @@ export default { ...conf, styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles })); - return { ...group, configs: uniqueConfigs }; + return {...group, configs: uniqueConfigs}; }); const activeGroup = this.styleGroups.find(g => g.is_active); @@ -1005,7 +1069,9 @@ export default { this.applyStylesToPanel(currentActiveConf.styles); this.editingConfigId = currentActiveConf.id; this.editingConfigLabel = currentActiveConf.current_label; - this.$nextTick(() => { this.isInitialEcho = false; }); + this.$nextTick(() => { + this.isInitialEcho = false; + }); } } this.updateAllElements(); @@ -1037,12 +1103,14 @@ export default { const isLabelExist = group.configs.some(c => c.current_label === labelName && c.id !== excludeId); if (isLabelExist) { - ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置。`, '校验失败', { type: 'error' }).catch(() => {}); + ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置。`, '校验失败', {type: 'error'}).catch(() => { + }); return false; } if (group.configs.length >= 5 && !group.configs.some(c => c.id === excludeId)) { - ElMessageBox.alert(`方案【${groupName}】已满(上限5个)。`, '校验失败', { type: 'error' }).catch(() => {}); + ElMessageBox.alert(`方案【${groupName}】已满(上限5个)。`, '校验失败', {type: 'error'}).catch(() => { + }); return false; } return true; @@ -1093,7 +1161,7 @@ export default { canvas_name: this.saveForm.canvas_name.trim(), group_name: this.saveForm.group_name.trim(), current_label: this.activeTags, - styles: { ...this.tagStyles[labelEn] }, + styles: {...this.tagStyles[labelEn]}, is_auto_save: false // 标记为手动保存 }; const res = await saveGraphStyle(payload); @@ -1118,7 +1186,8 @@ export default { const missingTags = REQUIRED_TAGS.filter(tag => !new Set(currentLabels).has(tag)); if (missingTags.length > 0) { this.isInitialEcho = false; - return ElMessageBox.alert(`该方案配置不完整,缺失:${missingTags.join('、')}`, '提示', { type: 'warning' }).catch(() => {}); + return ElMessageBox.alert(`该方案配置不完整,缺失:${missingTags.join('、')}`, '提示', {type: 'warning'}).catch(() => { + }); } this.usingConfigIds = group.configs.map(c => c.id); const res = await applyGraphStyleGroup(group.id); @@ -1128,7 +1197,10 @@ export default { if (group.configs.length > 0) this.handleEditConfig(group.configs[0]); ElMessage.success(`已应用方案【${group.group_name}】`); } - } catch (err) { this.isInitialEcho = false; ElMessage.error("切换失败"); } + } catch (err) { + this.isInitialEcho = false; + ElMessage.error("切换失败"); + } }, resetStyle() { @@ -1148,8 +1220,12 @@ export default { try { await ElMessageBox.confirm('确定删除吗?', '提示'); const res = await deleteGraphStyle(id); - if (res.code === 200) { ElMessage.success("删除成功"); this.fetchConfigs(); } - } catch (err) { } + if (res.code === 200) { + ElMessage.success("删除成功"); + this.fetchConfigs(); + } + } catch (err) { + } }, async deleteGroup(groupId) { @@ -1158,21 +1234,35 @@ export default { try { await ElMessageBox.confirm('确定删除全案吗?', '提示'); const res = await deleteGraphStyleGroup(groupId); - if (res.code === 200) { ElMessage.success("已删除"); this.fetchConfigs(); } - } catch (err) { } + if (res.code === 200) { + ElMessage.success("已删除"); + this.fetchConfigs(); + } + } catch (err) { + } }, async handleUnifiedBatchDelete() { if (this.checkedConfigIds.length === 0 && this.checkedGroupIds.length === 0) return; try { await ElMessageBox.confirm('确定批量删除吗?', '提示'); - 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.fetchConfigs(); - } catch (e) { } + 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.fetchConfigs(); + } catch (e) { + } }, - clearSelection() { this.checkedConfigIds = []; this.checkedGroupIds = []; }, + clearSelection() { + this.checkedConfigIds = []; + this.checkedGroupIds = []; + }, handleResize() { if (this._graph && this.$refs.graphContainer) { this._graph.setSize(this.$refs.graphContainer.clientWidth, this.$refs.graphContainer.clientHeight); @@ -1284,7 +1374,11 @@ export default { padding-bottom: 10px; margin-top: 10px; } -:deep(.el-collapse){ --el-collapse-border-color: transparent;} + +:deep(.el-collapse) { + --el-collapse-border-color: transparent; +} + .tag-pill { flex-shrink: 0; padding: 1px 10px; @@ -1459,8 +1553,8 @@ export default { flex: 1; overflow-y: auto; padding-bottom: 100px; - scrollbar-width: none; /* 针对 Firefox */ - -ms-overflow-style: none; /* 针对 IE 和 Edge */ + scrollbar-width: none; /* 针对 Firefox */ + -ms-overflow-style: none; /* 针对 IE 和 Edge */ } :deep(.el-collapse-item__content) { @@ -1475,7 +1569,7 @@ export default { display: flex; align-items: center; justify-content: space-between; - background-color:#f6f9fc !important; + background-color: #f6f9fc !important; padding: 7px 15px; margin-bottom: 10px; border-radius: 6px; @@ -1517,6 +1611,7 @@ export default { height: auto; margin-top: 4px; } + .config-checkbox { margin: 0; cursor: pointer; @@ -1679,6 +1774,7 @@ export default { border-color: #e6e6e6 !important; color: #333 !important; } + :deep(.el-message-box__btns .el-button--primary) { background-color: #1559f3 !important; border-color: #1559f3 !important;